Nine interfaces had exactly one implementation and no test double — they existed
only to be named twice in a DI registration: IFindingsStore, IFindingsStoreLocator,
IPrimeScheduleSignal, IRefineRunner, IWeekReportService, IMergeCoordinator,
IMissionControlPane, IOnlineLoginService, ITaskListFilter. Consumers now depend on
the concrete type; the DTO records that shared those files moved next to their
implementation. IInteractiveLaunchSpecService stays — it carries 54 lines of
contract documentation, which is not ceremony.
IMergeCoordinator in particular had a redundant null object: MergeCoordinator with
a null Handler already no-ops, and every test used the real class with Handler set.
Filtering/ collapses from 8 files to 1. ITaskListFilter and TaskListFilterBase were
a double abstraction over four predicates, with MatchesAsContext => false declared
in both. SmartFlagFilter also compiled its expression twice (its own _flag plus the
inherited Matches cache) — it now uses the cache.
StaticTokenAuthProvider was in src but production uses ZitadelAuthProvider; it is
a test double, so it moves to the test project. Its own test goes away with it.
Zwei liegengebliebene Consumer aus den letzten beiden Commits:
- TaskRunner klassifiziert seit 07dd7570 "usage_limit", aber weder
TaskRowViewModel.FailureReasonLabel noch vm.failureReason (de/en) noch
die get_task-Tool-Beschreibung kannten den Wert — die UI zeigte
"Grund unbekannt", das MCP-Doc listete weiterhin max_turns|timeout|error.
- TaskRowViewModel.CanOpenWorktree prüfte nur auf einen nicht-leeren
String. Die Zeile behält den Path eines gemergten/verworfenen Worktrees,
also war der Menüpunkt aktiv und Process.Start warf in den Footer.
Jetzt zusätzlich Directory.Exists — dieselbe Prüfung, die
WorktreesOverviewModalViewModel und MergeSectionViewModel schon machen.
Teil A: update_task bekommt einen optionalen isManual-Parameter (null =
unveraendert); TaskDto/TaskRefDto spiegeln IsManual zurueck.
Teil B: neuer submit_task_for_review MCP-Tool (LifecycleMcpTools) laesst
einen Handler-Task selbst auf WaitingForReview gehen -- fuer einen
worktree-losen Task wird HandlerHeadCommit vom aktuellen HEAD gestempelt,
sonst werden offene Aenderungen committet. Die Submit-Logik ist aus
WorkerHub.SubmitTaskForReview in InteractiveReviewSubmissionService
extrahiert; der Hub ruft sie jetzt nur noch auf. Der Merge-Prompt-Default
weist die Endzweige (merge ohne Rerun, merge_final) an, den eigenen
Handler-Task nach der Summary einzureichen.
A child merge that came back blocked/verify_failed/untracked_collision during a
parent/children unit merge used to vanish: DrainAsync only logged it server-side,
PlanningMergeAborted carried no reason, and ApproveReview/review_task always
reported StatusMerged for a task with children regardless of the real outcome,
so a failed unit merge left the parent stuck with no visible error.
- PlanningMergeOrchestrator.StartAsync/ContinueAsync/DrainAsync now return a
PlanningMergeResult(Status, Reason) instead of void, and PlanningMergeAborted
carries that reason to the UI.
- WorkerHub.ApproveReview and ExternalMcpService.ReviewTask's approve branch
propagate the real status/reason for a parent with children instead of
hardcoding "merged" (or masking a non-conflict failure as "conflict").
- StartAsync now requires the parent to already be WaitingForReview for
improvement parents too, not just planning ones, so a stale caller can no
longer trigger a partial child merge.
- HasActiveMerge now also covers the window between the last child merging and
FinalizeParentDoneAsync completing, closing a gap where a concurrent Cancel
could race the parent's own approve-to-Done transition.
- IslandsShellViewModel.OnPlanningMergeAborted flashes the reason via
FlashFooterError instead of only clearing the external-merge banner.
continue_merge and the parent/children unit-merge drain (PlanningMergeOrchestrator)
re-run the post-merge verify gate but never forwarded their IProgress token into it,
so a slow verify command on either path went silent past Claude Code's 300s MCP
idle-abort even though D1-D3 already fixed this for merge_task/review_task's
childless path. list_worktrees also gets elapsed-time progress: many tracked
worktrees means many concurrent git subprocess spawns.
Worker CLAUDE.md's existing progress rule now points at ProgressReporter as the
one implementation instead of a fresh polling loop.
Wraps the single-element git/worktree long-runners in ProgressReporter.RunAsync
(time-based, ExternalMcpService.ProgressReportInterval) and adds i/n reporting
to preview_merge_set's per-task loop, so these MCP calls survive Claude Code's
~300s idle-silence abort instead of leaving the caller with no signal that work
is still happening.
Each batch tool now sends an i/n progress ping via ProgressReporter.ReportItem
after processing every item, so a waiting agent doesn't see the MCP client's
300s idle-silence abort while the worker keeps looping. Covers BatchGetTasks,
BatchAddTasks, BatchUpdateTaskStatus, BatchCancelTasks, BatchDeleteTasks,
BatchSetMyDay, and BatchCleanupTaskWorktrees (the slowest of the seven, since
it does git work per task).
The merge commit message was hand-rolled per caller ("Merge task: <title>",
"Merge <branch>", "Merge subtask") and ignored the task's commit type. Every
caller now passes a blank message and TaskMergeService fills in
CommitMessageBuilder.BuildMerge -> {commitType}(list-slug): merge <title> plus the
ClaudeDo-Task trailer; the merge modal prefills it from GetMergeTargets.
A merge whose list has a verify command holds the MergeTask call for minutes (5m46s
on this repo), during which the modal only disabled its button - no spinner, no
message, so a landed merge looked like a dead app. TaskMergeService now broadcasts
MergeProgress(taskId, phase, elapsedSeconds) for the merging and verifying phases
(re-reported every 30s) plus a WorkerLog line when verify starts; the modal shows a
spinner and the localized phase.
## Symptom (real aufgetreten, 2026-08-11)
Ein `review_task(decision="approve")` über MCP lief >5 Min. Claude Codes MCP-Client brach den Call ab mit:
> MCP server "claudedo" tool "review_task" sent no response or progress for 300s; aborting.
Ergebnis: der Merge war **schon gelaufen und comitted** (`0e12be4`, `mergeCommit` am Worktree gesetzt), aber der Task blieb auf `WaitingForReview` hängen —
ClaudeDo-Task: d8199f1f-3df3-447f-8de2-e7aca9ec5064
wait_for_task_change resolved #<number>/bare-number ids up front via TaskIdResolver, which
throws for an unknown number -- breaking the tool's own documented promise that an unknown
id reports status "NotFound" instead of failing the whole call. Resolve per id and fall back
to the original id on a resolution failure so CheckOnceAsync can still report it.
TaskNumberAllocator.AddWithNumberAsync indexed into the app_settings UPDATE...RETURNING result
without checking for an empty result, throwing on a missing singleton row; it also caught any
DbUpdateException as a number collision, burning up to 5 numbers on an unrelated failure (e.g.
FK violation) before the real error surfaced. Now recreates the missing row and only retries on
the actual unique-index collision (SQLite error 19 on tasks.number), rethrowing everything else
immediately.
Audited the other TaskIdResolver.ResolveAsync/ResolveManyAsync call sites (ExternalMcpService,
HandoffMcpTools, ConfigMcpTools, RunHistoryMcpTools, AttachmentMcpTools, LifecycleMcpTools,
BatchMcpTools): none of their tool descriptions promise a found/NotFound flag for the id itself
(BatchMcpTools.BatchGetTasks already handles this correctly via its own per-id try/catch;
PreviewMergeSet promises a per-task "error" field, not a found/NotFound flag; the rest are
single-id tools that already throw on a missing task downstream) -- left throwing behavior as-is.
HandoffMcpTools.HandoffListHandler only validated the phase name and
broadcast it -- nothing bounded how many times a ConPTY session could
hand off wait<->merge, so a model that skimmed past the prompt's
"final round" line could loop indefinitely. Add HandoffRoundTracker,
an in-memory per-handler-task counter (a list-handler run always
creates a fresh handler task, so no reset logic is needed): past 4
handoffs (two full wait/merge cycles) a non-final nextPhase is
coerced to its "_final" counterpart, and any handoff after a
merge_final round for that task is rejected outright.
TaskIdResolver resolves a #123/bare-123 taskId parameter to its GUID
before any lookup, across every External/ MCP tool that takes a task
id, including the batch tools' id arrays (via delegation to the
already-resolving single-entity methods) and update_task's
dependsOnTaskId (empty string still passes through unchanged as the
clear-link sentinel). An unknown number throws a clear error instead
of a silent null. McpToolDocs.TaskNumberHint tells the agent to refer
to tasks as #<number> when reporting to the user, added to the
description of get_task, list_tasks, add_task, update_task_status and
review_task.
Adds Number alongside every task id in External/'s DTOs -- the two
central mappers (ToDto/ToRefDto -> TaskDto/TaskRefDto) plus every
DTO that carries a bare task id and bypasses them (batch results,
queue state, wait-for-change, config, attachments, handoff,
lifecycle, merge-preview-set, worktree list). Input resolution
(#123 as an argument) stays for slice 3.
Threads a nextPhase parameter (wait/merge/wait_final/merge_final, validated
by the new MergeHelperPhase) from handoff_list_handler through
HubBroadcaster/WorkerHub into InteractiveLaunchSpecService, which now picks
the next session's system prompt (MergeHelperWait/MergeHelperMerge) and
model (HandlerWaitAlias/HandlerMergeAlias) from it instead of hardcoding the
old two-phase Execute prompt -- this also fixes a build break left by the
prior prompt-split task, which removed PromptKind.MergeHelperExecute without
updating its only caller.
Also sets --model/--effort/--permission-mode explicitly for every list-handler
session (Triage included) via PermissionModeResolver instead of inheriting the
CLI's ambient model and hardcoding "auto", and adds Task to the merge-helper
allowlist so the Merge phase can delegate diff reviews to subagents.
SetListConfig previously could only pass VerifyCommand through unchanged;
only the UI hub could set it. Add an optional verifyCommand parameter with
matching clear/merge/create-vs-delete handling, and split the list result
into ListConfigDto so the task-only SetTaskConfig/GetTaskConfig DTOs stay
untouched. Also corrects Worker/CLAUDE.md's claim that tasks can override
verify_command — it's list-only.
Queuing (update_task_status, batch_update_task_status) and run_task_now
now surface a non-blocking baseDirty warning (separate modified/untracked
counts) when the list's working dir has uncommitted changes at enqueue
time, since a new worktree forks from the commit tip and silently misses
them. BaseDirtyChecker caches per working dir for a few seconds so a
batch queue over many tasks in one list only shells out to git once. The
UI surfaces the same warning via the footer error strip on queue actions.
MCP_TOOL_TIMEOUT (raised by ClaudeDo's launchers) is a wall-clock cap unrelated
to Claude Code's idle-silence abort (default 300s for HTTP-transport MCP
servers), which no launcher raises. A wait near the 900s recommendation was
silently killed at ~300s in any session, launcher or not.
WaitForTaskChange now reports MCP progress every 30s while polling, which
resets that idle timer. Verified against a real claude -p call (no launcher
env overrides) surviving a 341s wait via 60s pings -- the same silence that
previously aborted at 300s. Tool description and the list-handler prompt no
longer claim ClaudeDo's launchers guarantee a long wait survives.
get_task/batch_get_tasks now return failureReason (max_turns|timeout|error|
cancelled|unknown) plus failureTurnsUsed/failureMaxTurns on a Failed task, so
max_turns (worktree usually fine, continue_task) is distinguishable from a
real error (reset_failed_task) without pulling get_task_log's raw NDJSON.
Classified and stamped onto TaskEntity by TaskRunner.MarkFailed via
TaskStateService.FailAsync; TaskRunEntity also keeps the CLI's raw
terminal_reason/result_subtype/errors for deeper diagnosis. reset_failed_task's
description now warns explicitly that it discards the worktree and points at
continue_task for max_turns. Surfaced on the task card's status-chip tooltip.
batch_get_tasks with includeDescription=true had no size guard: 8 tasks'
full Description/Result serialized to a single 51k-char line, blowing past
the tool-result token budget and forcing a file/PowerShell workaround to
read it back. Roadblock text was also only reachable via a second get_task
call per task, since roadblockCount (in the lean ref) has no text sibling.
- descriptionMaxChars (default 1500, was unlimited) truncates Description
and Result independently, flagged via *Truncated/*FullLength so a caller
never silently works off a cut string.
- fields narrows taskFull to just the named properties, including the new
roadblockText (the tail of Result after TaskRunner's roadblock marker) —
reachable without pulling in the rest of Result/Description.
- taskFull is now BatchTaskDetailDto, a batch-only shape decoupled from
TaskDto so get_task's own contract is untouched.
- The whole response is capped (MaxResponseChars); over that, the call
throws naming which parameter (descriptionMaxChars/fields/taskIds) to
adjust instead of shipping an oversized payload.
10 tasks with 5000-char descriptions (default settings) serialize to
~13.8k chars, comfortably under the 25k-char cap.
Adds a user/MCP-declared task dependency (DependsOnTaskId) distinct from the
planning chain's internal BlockedByTaskId: add_task/update_task can set it,
the queue picker skips a Queued task until the dependency reaches Done, a
Failed/Cancelled dependency leaves the dependent blocked instead of starving
silently, and setting a link rejects self-reference/unknown-id/cycles.
get_task/list_tasks/batch_get_tasks now report blocked/blockedReason, and
wait_for_task_change reports "Blocked" immediately instead of running out its
timeout on a task the picker will never claim.
preview_merge/preview_merge_set gain staleFiles: files a branch touches that
the target branch also changed since the branch's fork point, a more honest
staleness signal than `behind` alone.
Omitted fields on set_task_config/set_list_config used to be indistinguishable
from an explicit clear, so any single-field update silently wiped every other
override (including SessionSkills, which wasn't even a tool parameter). Now
only the fields you pass are changed; clearing an override requires naming it
in the new clearFields array (a sentinel string doesn't work uniformly since
maxTurns is an int). set_list_config's "all four null deletes the config" case
is preserved but only reachable via clearFields. Return values now reflect the
full resulting config.
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.
## 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.
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.
NormalizeTitleWords transliterates umlauts before tokenizing ('für' ->
'fuer'), but TitleStopWords listed 'fur' -- a token the pipeline can
never produce -- so 'für' was never filtered as filler and could push
unrelated titles past the near-duplicate threshold.
review_task/continue_merge on a planning parent always leaves conflicts in
the tree, and the UI auto-opened the resolver on every PlanningMergeConflict
broadcast regardless of who started the merge -- so a running Claude session
resolving a unit-merge conflict could race a human editing the same shared
checkout in a resolver window neither of them asked for.
PlanningMergeOrchestrator.StartAsync now takes an externallyDriven flag (set
by ExternalMcpService's MCP-driven review_task path, left false for the UI's
ApproveReview) that rides along on the PlanningMergeConflict broadcast. The
UI only auto-opens the resolver when it's false; otherwise it shows a
persistent banner with a manual "Open resolver" button, cleared on
PlanningMergeAborted/PlanningCompleted. A new GetActiveExternalConflictsAsync
query (checked against GitService.IsMidMergeAsync rather than the in-memory
flag alone) lets the UI resync the banner on reconnect instead of trusting a
one-shot broadcast that isn't replayed after a restart.
The childless single-task conflict path was checked and needed no change --
it only broadcasts the generic TaskUpdated, never PlanningMergeConflict.
preview_merge/preview_merge_set now report isEmpty (ahead==0, or
HandlerBaseCommit==HandlerHeadCommit for a worktree-less list-handler
task) instead of leaving an empty branch indistinguishable from a small
one. preview_merge also stops throwing for worktree-less handler tasks,
falling back to their fixed commit range. review_task's parent approve
returns emptyChildren, naming the Done children whose review range
contributed nothing before the unit merge lands. TaskRefDto/TaskDto now
expose roadblockCount so a CLAUDEDO_BLOCKED child is identifiable over
MCP, since it still reaches Done per the unified parent model.