LoadForListAsync loaded the entire tasks table (all lists, two Include
joins) then filtered to the selected list in C#, never using
idx_tasks_list_id/idx_tasks_status. ITaskListFilter now exposes
MatchExpression, an Expression<Func<TaskEntity,bool>> that EF Core can
translate to SQL; a new TaskListFilterBase compiles it on demand for the
existing in-memory Matches(). The primary match query now runs as
db.Tasks.Where(filter.MatchExpression), with two small follow-up queries
(scoped by parent-id IN-lists) for contextual planning-parent rows and
pulled-in children, followed by an explicit re-sort to restore the global
SortOrder/CreatedAt ordering Regroup() depends on.
get_merge_conflicts and resolve_conflict_hunk let an MCP caller resolve a
leaveConflictsInTree merge hunk-by-hunk (ours/theirs/base/literal text)
instead of grep-parsing diff3 markers itself. Reuses the existing
TaskMergeService/ConflictMarkerParser machinery (adds MergeSegment.StartLine
and a non-staging TaskMergeService.WriteConflictFileAsync) rather than a
second parser. A partially resolved file is written without git add so
continue_merge keeps refusing while any hunk still has markers.
MergeHelperSelectionModalViewModel now loads ParentTaskId and orders
rows hierarchy-aware (parent immediately followed by its children,
mirroring TasksIslandViewModel.Regroup). Children render indented with
the same indent-track visual as the main task list; ticking a parent
cascades selection to its children, while a child can still be
deselected on its own. A child whose parent got filtered out (e.g.
already Done) renders top-level with an explicit hint instead of
disappearing. Confirm keeps returning parents ahead of their children.
## Befund (Batch-Lauf 2026-08-06, Liste "Bandel.Hub")
maxParallelExecutions = 5, 23 Geschwister-Tasks auf demselben Plugin. Die Parallelitaet hat die
Ausfuehrung verkuerzt, aber die Konfliktlast erhoeht: eine CSS-Datei wurde von 10 Tasks
angefasst, drei Razor-Dateien von je 6. Der Engpass des Gesamtdurchlaufs war nicht die
Ausfuehrung, sondern Review und Merge — die sind zwingend seriell. Netto wa
ClaudeDo-Task: 89f989e0-4fcf-43f5-a802-11d63df7a6d1
get_task_diff gained an optional paths param (git pathspec, both stat and
full-diff modes) so a large multi-file task can be narrowed to the files
actually in question instead of shipping the whole diff.
preview_merge_set now also reports Subsets: pairs where one task's changed
files are a proper subset of another's in the same set, the strongest
available post-hoc signal that a task may be redundant with another.
Before merging (or staging a conflict resolution's git add -A), compare the
branch's newly-added paths against what's currently untracked in the target
working directory. A collision aborts with a new untracked_collision status
naming the path and size, surfaced through merge_task/review_task,
preview_merge/preview_merge_set (which merge-tree alone can't see), and the
UI merge paths via FlashFooterError/ShowErrorAsync instead of a silent
catch{}. git's own preflight already refuses this while the path stays
untracked at merge time; this closes the gap once a path becomes trackable
in between (e.g. an unrelated conflict resolution's git add -A) or across
the continue_merge TOCTOU window.
preview_merge could only see file-overlap cleanliness via git merge-tree, never
whether the result compiles -- the two costliest findings of the 2026-08-06 batch
run were both merge-tree-clean but build-broken. When a list has a verify command
configured, a clean preview is now additionally materialized (via a commit-tree +
detached scratch worktree, outside the real repo, always cleaned up) and
built/tested there, without ever touching the real working tree.
preview_merge always attempts a verify run when a command is configured;
preview_merge_set only does when its new runVerify parameter is set (default off),
so a set preview never starts N builds unasked. The post-merge verify gate is
unchanged.
Path.GetDirectoryName returned workingDir itself instead of its parent
whenever list.WorkingDir ended in a directory separator, placing
.claudedo-worktrees inside the target repo's own working tree. Add
WorktreeRootResolver to normalize the trailing separator before deriving
the sibling root and to guard that the resulting worktree path never
lands inside workingDir, used by both WorktreeManager and
PlanningSessionManager. Add a startup sweep (LegacyWorktreeFolderRecovery)
that warns when a list's working dir already contains a leftover
.claudedo-worktrees folder from before the fix.
The prime_schedules.prompt_override column existed end-to-end but was
never populated or read. DailyPrepPrompt.BuildPrompt now takes an
optional override and appends it as an extra paragraph after the fixed
prompt (additive, never a replacement, so a user can't disable the
required get_daily_prep_candidates/set_my_day flow). PrimeRunner passes
schedule.PromptOverride through. The Prime tab in Settings now has a
multiline field per schedule wired to a new PromptOverride property on
PrimeScheduleRowViewModel (blank persists as null).
git add -A ran against the shared target checkout, sweeping in
whatever untracked or unrelated modified files a concurrent session
left there and committing them into an unrelated merge. Stage exactly
the paths that were reported conflicted and resolved instead.
A hand-driven task produces no streamed agent output, so the Output tab
opened empty. Bind now selects git for manual/interactive rows and resets
to output for autonomous ones.
QueueServiceSlotFailureTests's throwing-slot test broke its poll loop the
instant the DB read observed Status==Failed, but TaskStateService.FailAsync
commits the status flip before calling the broadcaster's TaskUpdated, so the
assertion could race ahead of the broadcast landing in hub.Proxy.Calls
(~1-in-5 failures in isolation). Wait for both signals before breaking.
RunInSlotAsync only logged an unexpected exception, leaving a task stuck
Running in the DB forever with the UI never notified (the raw-SQL queue
claim that put it there never broadcasts). Cancellation is handled
separately and left alone, since the cancel path already wrote the
terminal status.
Every tool description now leads with what the tool does AND when to reach for
it, since MCP clients rank tools by that text. Per-parameter prose moved onto
the parameters as [Description], exhaustive result-shape enumerations and
design/history rationale dropped, and the repeated boilerplate clauses
(lean-task-ref, batch cap, refused-while-Running) pulled into McpToolDocs,
which also documents the style for future tools.
Tool-level description text: 20494 -> 13605 chars (-34%); combined with the new
parameter descriptions 18517 (-10%).
Closes gaps that caused wrong calls rather than just verbose ones:
- list_task_attachments returns metadata only, no file content
- run_task_now shares continue_task's single override slot and throws when busy
- list_runs is ordered oldest-first and feeds get_run
- workingDir on create_list/update_list is an existing local git repo path,
unvalidated until the first task run
- get_task_worktree's behind=0 also means the main ref was unreachable
Removes get_task_status_values: a whole tool entry for static reference text.
GetTask's description is now the canonical place for status meanings.
Audited all 12 prompt kinds against the code they drive. Every real defect sat on
the boundary between prompt text and the --allowedTools the launcher passes.
- Planning: "Use nothing else" after a six-tool list forbade the brainstorming
Skill the same prompt demands two paragraphs earlier. WindowsTerminalLauncher
allowlists mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill -- name them,
and tell the planner to ground subtasks in the repo with Read/Grep/Glob.
- System: SuggestImprovement is only allowlisted when ParentTaskId is null and
PlanningPhase is None, and TaskRunMcpService throws for any child, but this
prompt reaches every run. Planning children were told to use a tool they lack.
- MergeHelperExecute: derived "effective max-turns" from task/list/preset by hand,
which misses TaskRunner's MaxTurnsCeiling clamp. Call get_effective_run_config
instead -- built for exactly this and reports the clamp.
- MergeHelperExecute: quoted the override-slot error as the raw lowercase throw
rather than the string ExternalMcpService actually surfaces.
- Refine: listed Read/Grep/Glob unconditionally though RefinePrompt.BuildArgs only
appends them when a repo is available.
Two findings deliberately left open, both needing a code decision rather than a
prompt edit: the System prompt's worktree claim is false for a list without a
WorkingDir (task runs in a plain sandbox dir), and 'fable' is missing from both the
prompt's cost ordering and ModelRegistry.ByCostAscending.
The execute prompt told the handler to wait with timeoutSeconds up to 170 -- a
leftover from the retired MCP_TOOL_TIMEOUT=200000ms era. The real server-side
clamp is TaskWaitMcpTools.MaxTimeoutSeconds = 900 and every launcher sets
930000ms, so the handler was making ~5x the wait_for_task_change calls it needed
and burning turns on re-waiting.
It also never passed treatWaitingForChildrenAsBusy, and only waited on ids that
were Queued or Running. A task with children reports "changed" the moment it
reaches WaitingForChildren, so such a task both dropped out of the wait set and
signalled completion early -- the handler could reach review/merge while
children were still running.
RenderTemplate replaced tokens one key at a time over a StringBuilder, so a
token appearing inside an already-substituted value got substituted again on a
later pass. The prompt briefs only escaped this because their callers happen to
pass "tasks" last -- reordering the dictionary or adding a fourth token would
have started rewriting user-authored task descriptions, which after the enhance
phase carry file paths and config snippets.
Single-pass regex over the template instead; unknown tokens still pass through.
Both merge-helper ConPTY sessions loaded PromptKind.MergeHelper, so the
post-handoff session received the phase 0-2 dedupe/enhance instructions and was
told to ignore them by its brief alone.
Split into MergeHelperTriage (phases 0-2 + handoff) and MergeHelperExecute
(phases 3-5), so each session carries only its own phases. Consolidated the
generic ask-the-user rule to one place per prompt, scoped Phase 5's summary to
what the execute session actually knows, and moved the dedupe/enhance bilanz to
the triage handoff. Regression guards assert neither prompt carries the other's
phase headings and that the shared-checkout git rule stays in execute.
The ConPTY host flattens LaunchSpec.Args into one Windows command line and quotes
each token, so a list working dir stored as "C:\repo\" produced the token
"C:\repo\" -- whose trailing backslash escapes its own closing quote. Everything
after it collapsed into --add-dir's variadic list, including
--append-system-prompt-file and the positional kickoff, so "Let Claude handle it"
opened a session with no prompt at all and the CLI warned that brief.md is not a
directory. Only user-supplied working dirs can carry a trailing separator; the
session dirs the worker builds never do.
Register(taskId, cts) already refuses (and logs) a double-registration, but
both call sites discarded the bool and dispatched anyway under an
unregistered CTS. If the loser then unregistered the winner's CTS during
its own cleanup, TryCancel could silently no-op against a live process.
- OverrideSlotService.StartInSlot now fails RunNow/ContinueTask loudly
(throws) when it loses the registration race instead of registering
over — or silently proceeding despite losing to — the queue picker.
- QueueService's picker loop retries registration briefly (the loser's own
claim-attempt-then-unregister resolves fast) before dispatching; if
registration never resolves it marks the already-claimed row Failed
instead of running it unregistered.
- RunCancellationRegistry.Unregister already had compare-and-remove
semantics (TryRemove(KeyValuePair)), so a loser's cleanup could not have
removed the winner's CTS once registration correctly failed.
Added regression tests exercising the real registry through both dispatch
paths: RunNow losing the registration race throws without disturbing the
winner, the picker's retry succeeds and TryCancel reaches the live run when
the loser unregisters in time, and the picker fails the task instead of
running unregistered when it never does.
InteractiveSessionId is persisted before the ConPTY process spawns (1a988ff).
If `claude --session-id <guid>` exits immediately after launch (auth/network
hiccup, crash before the TUI starts), OnSessionProcessExited set HasExited
but never StartError, so ConPtyPaneViewModel.CanRetry (which requires
StartError) never offered Retry — every reopen just resumed the same dead
session id, permanently poisoning that task's interactive sessions.
Now a nonzero exit within 5s of the session becoming "running" is treated
as a died-at-startup failure and routed through the same StartError path
as a launch-time exception, so the pane shows the error banner + Retry.
Retry re-fetches the LaunchSpec via BuildForTaskAsync, which reuses the
same persisted InteractiveSessionId — correct for a transient failure
(fresh process, same id), but does not help a genuinely dead session id.
Clearing a dead session id server-side is a separate design question,
left out of scope here.
TasksIslandViewModel.CancelReviewAsync swallowed the HubException the
worker raises when a task's unit merge is draining (79f90a9), unlike
the details-pane version fixed in the same commit. Mirror that fix:
report the rejection via ErrorReported instead of a bare catch.
Reset & Retry discarded the branch and queued an autonomous run even while the
user had an interactive ConPTY pane open on the task, and finalizing a plan
queued every child unconditionally (the hub has no notion of a UI-hosted
session) — both bypassed the HasInteractiveSession gate added for
CanSendToQueue. CanResetAndRetry now checks it too, with a subscription on the
bound task so the command re-evaluates when the flag flips without Task
itself changing; SendToQueueAsync now blocks queuing the whole plan and
surfaces the affected child titles when any child has an open session.
DeleteTaskAsync had no IsConnected guard, unlike every other worker-dependent
command in DetailsIslandViewModel, so an offline delete was a silent no-op.
WorkerClient.DeleteTaskAsync also only caught HubException, letting the
InvalidOperationException thrown by an inactive hub connection escape into
the unobserved command task and vanish.
Gate DeleteTaskCommand behind CanDeleteTask (Task != null && IsConnected),
re-evaluate it on connection-state changes, widen WorkerClient to catch the
connection-inactive case too, and wrap the ViewModel's call in try/catch as
a second line of defense against a race between the guard and the call.