Generates the claude session id up front (--session-id <guid>) for a fresh interactive task
session and persists it to TaskEntity.InteractiveSessionId before launch, so an abort at any
point still leaves a resumable id. BuildForTaskAsync now resumes this task's own last
interactive session in preference to the latest autonomous run's session, but never across a
freshly (re)created worktree.
Stamps ClaudeDo.Worker's build with its exact git SHA (SourceRevisionId ->
InformationalVersion) and exposes it via a new GetWorkerBuildInfo hub call.
For the currently selected list, the shell compares that SHA against the
list's git HEAD (GitService.IsAncestorAsync) and shows a persistent footer
banner -- never auto-clearing, never shown on an unrelated repo or when the
ancestry can't be determined -- so "verified against a merge" claims aren't
silently made against a stale process. No auto-restart; the banner just
offers the existing RestartWorkerCommand.
Two remaining single-line lists that accumulate entries over time
(table names in Data/CLAUDE.md, HubBroadcaster event names in
Worker/CLAUDE.md) collide when parallel tasks each append a new
entry, same as the AppSettingsEntity/migration-list lines fixed
earlier today. Content unchanged, one bullet per existing item.
The merge-helper ("Let Claude handle it") system prompt now calls a new
handoff_list_handler MCP tool once every surviving task is enhanced,
instead of continuing into Phases 3-5 in the same session -- avoiding
paying for Phases 0-2's dedupe/rewrite context on every polling round of
the run/review/merge phases.
The tool broadcasts HandoffRequested; Mission Control opens a second
ConPTY tile for the SAME handler task id (no new task, HandlerBaseCommit
untouched) running a fresh handoff brief that starts at Phase 3. The
original tile stays open. Adds PromptKind.MergeHelperHandoff,
InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync, and the
GetMergeHelperHandoffLaunchSpec hub method.
Four IEnvironmentCheck implementations in src/ClaudeDo.Installer/Checks/:
- ClaudeCliCheck (Error) — resolves ctx.ClaudeBin via ExecutableResolver, runs
--version; failure message lists searched PATH entries + fallback dirs, flags
shim resolution (.cmd/.ps1) in Detail.
- ClaudeVersionCheck (Error) — tolerant version parsing (ignores surrounding
text), numeric System.Version comparison against a named floor constant
(2.1.220, see docs/explore-notes/installer-preflight.md §3). Unparseable or
missing CLI -> Unknown, never Failed.
- ClaudeAuthCheck (Error) — `claude auth status --json`, never sends a prompt.
- PermissionModeAutoCheck (Warning) — per the note's §2 conclusion, real
auto-mode eligibility (org/model/plan) has no cheap static signal, so this
only confirms `claude --help` still lists "auto" as a --permission-mode
choice. Kept as its own check rather than folded into ClaudeVersionCheck,
since the note explicitly separates "flag recognized" from "mode eligible"
and only the former is checkable at all.
All four share a new ClaudeCliLookup that resolves the CLI and runs
--version exactly once per install run (memoized, semaphore-guarded), so
none of the three version-dependent checks repeats that call.
Foundation prerequisites this task depends on (docs/explore-notes/
installer-preflight.md, ExecutableResolver, the IEnvironmentCheck/CheckResult/
EnvironmentCheckService abstraction, ProcessCommand, IProcessRunner) existed
only on an unmerged sibling branch, not on main. Rather than merging that
whole unreviewed branch, the specific prerequisite files were copied in
as-is (git history shows their origin). GitCheck/GitIdentityCheck/PortCheck/
WriteAccessCheck from that branch were intentionally NOT brought in — out of
scope for this task.
Deviations/decisions worth flagging:
- Added ExecutableResolver.FallbackDirectories() (public) so ClaudeCliCheck
can name the checked fallback locations in its failure message; the prior
branch kept that list private.
- Paths.cs now qualifies System.Environment explicitly. Adding the
ClaudeDo.Data.Environment namespace makes bare `Environment.X` inside any
ClaudeDo.Data.* namespace resolve to the sibling namespace instead of
System.Environment (C# prefers nested/enclosing namespace members over
usings) — this broke the build until qualified.
Not done (explicitly out of scope): no DI wiring into the wizard UI, no
XAML — matches how the prerequisite Git/Port/WriteAccess checks were also
left unwired.
UseShellExecute=false only appends .exe when searching PATH, so an
npm-installed claude.cmd was never found even though it works from a shell.
Adds a shared ExecutableResolver in ClaudeDo.Data (PATH/PATHEXT aware, with
known npm/claude install-dir fallbacks) and wires it into ClaudeCliPreflight
and ClaudeProcess; shims are launched via cmd.exe /c.
EnsureExists blindly seeded ~/.todo-app/prompts/*.md with the bundled
default and never revisited it, so any file created by opening the
Files settings tab shadowed every later default change permanently
(SuggestImprovement/AskUser sections never reached real runs since
2026-06-04). PromptFiles now hashes what a file was seeded/saved with
in prompts/.defaults.json: Classify() tells missing/current-default/
known-past-default/edited apart, ReconcileStaleDefaults() drops files
that only ever matched a superseded default, and real edits are left
alone and surfaced in the Files tab with a diff + reset-to-default
action. QuarantineOrphans() moves stale-named leftovers (agent.md,
planning.md) into prompts/_orphans instead of silently deleting them.
Wired as a Worker startup sweep (PromptFileRecovery) alongside the
existing OrphanRecovery/AttachmentOrphanRecovery services.
## Kontext: Limits sind Fenster, nicht Summen
Die Runs laufen ueber das Claude-Abo. Limits greifen pro 5h-Fenster und pro 7 Tage. Nicht die Wochensumme tut weh, sondern dass ein Agent-Burst ein Fenster leerraeumt, in dem Mika selbst interaktiv arbeiten will.
## Messgrundlage (alle Transcripts unter ~/.claude/projects)
Agent-Runs sind ueber die ganze Historie nur **18,4 %** des Account-Verbrauch
ClaudeDo-Task: 87105f5e-c4f4-4af4-ae60-89cd2e153e3c
task_runs.tokens_in only ever held the API's uncached "input_tokens" field
(off by a factor of ~400,000 on a resumed session), and tokens_out summed
only the last result event instead of the whole session. TaskRunner now
reads each run's cache-read/cache-write/input/output totals from the
session transcript via a new ITranscriptUsageReader.ReadSessionTotalsAsync,
storing the delta against prior runs on the same session so a --resume
doesn't double-count. New task_runs.cache_read_tokens/cache_write_tokens
columns; the Session tab now shows the raw total (what actually counts
against the 5h/7d usage limit) with a breakdown tooltip.
Runaway sessions were the single biggest cost driver: model_presets was
never persisted (stayed code-only), default_max_turns shipped at 100, and
ResolveMaxTurns had no upper bound, so a task/list override could run
hundreds of turns unchecked.
- TaskRunner.ResolveMaxTurns now clamps the resolved value to
AppSettings.MaxTurnsCeiling (new column, default 80) and logs a warning
with task id / requested / effective value when it clamps.
- default_max_turns default lowered from 100 to 40 (entity, EF config,
and the seeded row via the new AddMaxTurnsCeiling migration).
- AppSettingsRepository.GetAsync backfills model_presets with the
shipping defaults on first read instead of leaving the column null.
- Settings > General's per-model preset table and the task/list agent
editor now show a hint when a set max-turns value exceeds the ceiling.
Adds a "Reading efficiently" section to SystemDefault covering
locate-before-read (Grep/Glob), targeted reads (offset/limit), avoiding
re-reads, and using an exploration subagent as a context firewall for
orientation questions. Based on measurement across 144 real sessions
showing context resend (Read especially) dominates token cost.
Persists the merge commit SHA on WorktreeEntity for every successful
single-task and unit merge, and adds a TaskMergeService.RevertMergeAsync
+ revert_merge MCP tool that undoes a merged task's merge via
`git revert -m 1` (never reset/rewrite, since the target checkout is
shared). Rejects cleanly when there's no recorded SHA, the repo is
mid-merge/mid-revert, or the target has foreign uncommitted changes;
a conflicting revert aborts immediately. Also exposes the new
mergeCommit field via get_task_worktree.
Planning sessions could already steer a subtask's model but not its turn
budget, so a visibly large subtask would still die at the global default
turn limit. maxTurns is optional (default null = inherit list/global
default, matching model); 0/negative values are rejected as invalid input,
consistent with the existing model-alias validation.
Per-list optional VerifyCommand (list_config.verify_command) runs via
VerifyCommandRunner in the list's working dir right after a successful
merge/continue-merge, before the task is allowed to reach Done. A
non-zero exit or timeout leaves the merge in place but keeps the task
out of Done and reports StatusVerifyFailed with an output excerpt
through MergeResultDto/review_task; no command configured behaves
exactly as before. Merges against the same repo are now serialized
per working dir so a running verify can't be interrupted by a second
merge landing mid-build. Adds the field to the List Settings modal
(en/de localized) and covers success/failure/timeout in
TaskMergeServiceTests + VerifyCommandRunnerTests.
Give an autonomous reviewer a non-destructive merge-tree preview
(status/conflicts/changedFileCount/behind) for a task's worktree branch,
plus a file-overlap check across a batch of tasks so same-file collisions
between sibling branches are visible before merging. MergeHelperDefault's
Phase 4 now calls preview_merge_set before merging a batch.
Phase 2 can now propose splitting a bundled/blocked task via add_task/add_subtask
instead of forcing it into one description. Phase 3 reports each task's effective
max-turns and asks before queuing a substantial task with a low value. Phase 4
checks Phase 2's file lists for cross-task collisions and may reorder merges with
a stated reason instead of always following brief order.
Replaces the list handler's Start-Sleep + blind get_task poll (Phase 3
of the merge-helper prompt) with a blocking MCP tool that returns as
soon as a task leaves Queued/Running, or times out. Implemented as an
async DB poll (short-lived DbContext, 500ms delay, no held connection)
rather than hooking HubBroadcaster, keeping the existing broadcast
callers untouched. timeoutSeconds is clamped server-side to 170s, under
the list handler's 200s MCP_TOOL_TIMEOUT.
Phase 0 forced a batch_get_tasks full-fetch across every task just to see
descriptions, which blew past the client's token limit on larger lists.
brief.md lives on disk and has no such limit, so descriptions now render
there directly (fenced with an extended backtick run, indented under the
list bullet, so embedded headings/lists/code fences can't break the task
list's own structure). Phase 0 now treats the brief as the primary source
and only falls back to batch_get_tasks for fields it doesn't carry.
Adds AppSettings.UsageGateFiveHourPct/UsageGateSevenDayPct (defaults
80/90, clamped 0..100 in UpdateAsync) and a nullable TaskRunEntity.Model
column, laying the data foundation for the usage monitor. No worker/UI
changes.
"Let Claude handle it" now creates one ClaudeDo task per run to host the
ConPTY session (Idle/IsManual, never queued) instead of an untracked
ad-hoc tile, so the run has a real title, diff, and review outcome.
Since the handler merges its own changes straight into the list's
working dir, the task never gets a WorktreeEntity; its review range
lives as new HandlerBaseCommit/HandlerHeadCommit columns on TaskEntity
instead, reusing the existing commit-range diff machinery and keeping
it out of the worktrees overview entirely.
A full model id like claude-sonnet-4-6 in list_config.model never matched any
ModelPresets row (only bare aliases did), so every run under it landed on the
hardcoded 30-turn/DefaultLevel fallback instead of the sonnet preset row -
this is what killed two autonomous tasks at the 30-turn limit.
ModelRegistry.TryNormalizeAlias (non-throwing: exact match, then substring
match against a full model id) lets ModelPresets.For resolve a full model id
to its alias's preset row. For a model that still doesn't resolve, the
hardcoded 30 is replaced by a caller-supplied fallbackMaxTurns, and TaskRunner
now passes AppSettings.DefaultMaxTurns there - so that setting has a real
effect instead of being dead, matching the direction already noted in
docs/open.md.
Phase 1 previously asked the user to confirm the absence of duplicates
even when no candidate pair was found. The handler now decides that
itself and moves straight to Phase 2; per-pair questions remain when
at least one candidate exists.
Dragging a task row onto a user list in the Lists island now reassigns it
to that list. The task drag holds the pointer capture, so the release is
resolved geometrically (new case between the Mission Control and reorder
cases) instead of going through the Lists island's own DragDrop path,
which never sees a DragEventArgs during a task drag.
TaskRepository.MoveToListAsync reassigns the task plus every descendant
(a child must never sit in a different list than its parent) and appends
the task at the end of the target list. Guards: running tasks and tasks
holding an Active/Kept worktree are rejected to the footer error strip;
a move that changes repo asks for confirmation naming both repos.
The source repo is read from the task's own list rather than the island's
current list, which is a smart/virtual list with no working dir of its own
whenever one of those is shown.
ClaudeDo never passed --effort, so every session inherited whatever effortLevel
the user's Claude Code config happened to carry. Settings -> General now holds one
row per model alias (haiku medium/20, sonnet high/30, opus high/40, fable high/25)
supplying the global effort and turn defaults; list- and task-level max-turns
overrides still win, and the agent editor's inherited badge follows the model.
--effort is applied to autonomous runs and to every ConPTY spec (task session,
planning start/resume, ad-hoc, list handler). The model itself is deliberately not
forced on interactive sessions. The single global 'Max turns' field is replaced by
the table, and 'fable' joins ModelRegistry.Aliases.
The migration also adds the is_manual columns used by the next commit.
HasChangesAsync counted untracked files, so a stray file in the shared
target working dir (e.g. left by a concurrent session) blocked merge
preflights even though nothing tracked changed. Add an includeUntracked
overload defaulting to true, and pass includeUntracked: false only from
the two target-working-tree merge preflights (TaskMergeService.MergeAsync,
PlanningMergeOrchestrator.StartAsync). Auto-commit and the worktree
cleanup data-loss guard keep counting untracked files, since those
callers need to know about them.
- GitService.MergeFfOnlyAsync had no callers anywhere in code; removed.
- AgentMcpTools moved out of LifecycleMcpTools.cs into External/AgentMcpTools.cs (no behavior change).
- Worker CLAUDE.md described ZitadelAuthProvider as a Phase 2 stub; it's fully implemented (OIDC discovery + refresh-token flow), updated both mentions.