feat(tasks): mark tasks and lists as manual

Reminders written down as todos had no home: every task looked like Claude work.
A manual task now shows a MANUAL badge and hides send-to-queue, refine and the
planning session; the queue picker, daily prep and the list handler all skip it,
with a TaskStateService guard so the MCP surface and hub cannot start one either.
Opening a hand-driven ConPTY session stays available on purpose.

A list can be marked manual in its settings, which makes tasks created there
(UI and MCP add_task) start out manual. Toggle per task from its context menu.
This commit is contained in:
Mika Kuns
2026-07-27 15:02:51 +02:00
parent fde9615b34
commit 3a648b7d77
19 changed files with 168 additions and 11 deletions
@@ -41,6 +41,9 @@ public sealed class TaskStateService : ITaskStateService
if (await IsDraftChildAsync(ctx, taskId, ct))
return new TransitionResult(false, "Draft subtask: finalize the plan before queuing it.");
if (await IsManualAsync(ctx, taskId, ct))
return new TransitionResult(false, "Manual task: mark it as a Claude task before queuing it.");
var affected = await ctx.Tasks
.Where(t => t.Id == taskId && t.Status != TaskStatus.Running)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, TaskStatus.Queued), ct);
@@ -60,6 +63,9 @@ public sealed class TaskStateService : ITaskStateService
if (await IsDraftChildAsync(ctx, taskId, ct))
return new TransitionResult(false, "Draft subtask: finalize the plan before running it.");
if (await IsManualAsync(ctx, taskId, ct))
return new TransitionResult(false, "Manual task: mark it as a Claude task before running it.");
var affected = await ctx.Tasks
.Where(t => t.Id == taskId && t.Status != TaskStatus.Running)
.ExecuteUpdateAsync(s => s
@@ -418,6 +424,11 @@ public sealed class TaskStateService : ITaskStateService
// A subtask is "draft" only while its planning parent has an open (Active) session.
// Improvement children whose parent has PlanningPhase.None are not drafts and may be
// queued freely. Standalone tasks (no parent) are never draft.
// Server-side backstop for the manual flag: the UI hides the hand-off affordances, but the
// MCP surface and hub can still be driven directly.
private static Task<bool> IsManualAsync(ClaudeDoDbContext ctx, string taskId, CancellationToken ct)
=> ctx.Tasks.AsNoTracking().AnyAsync(t => t.Id == taskId && t.IsManual, ct);
private static async Task<bool> IsDraftChildAsync(ClaudeDoDbContext ctx, string taskId, CancellationToken ct)
{
var parentId = await ctx.Tasks.AsNoTracking()