Compare commits

...
339 Commits
Author SHA1 Message Date
mika kuns 214e53b714 feat(merge): resolve conflicts straight from the merge modal
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 49s
The merge modal previews the selected target and offers 'Resolve conflicts' next to the stash retry when it predicts a conflict. A dirty target is autostashed. The resolver now aborts when closed natively (Alt+F4), so the merge and its autostash can never be left pending.
2026-10-02 18:53:34 +02:00
mika kuns 8cf3a3a9d0 fix(claude-do): merge Bug: conflict resolver no longer opens on a merge conflict
ClaudeDo-Task: 612a70a9-814e-4300-acec-1384262588cd
2026-10-02 17:27:47 +02:00
mika kuns 36ce8fb622 fix(claude-do): merge Lists sidebar: auto-scroll while dragging a list/folder near
ClaudeDo-Task: 653f3991bb564379ae80edd3d16e8224
2026-10-02 17:27:02 +02:00
mika kunsandClaude Sonnet 5.5 feb0afa33a fix(merge): pass the stash consent to the conflict resolver so it opens on a dirty target
The resolver restarts the merge via StartConflictMerge, which had no autostash.
After a stash-retry approve reported 'conflict', the restart hit the same dirty
target, got 'dirty_target' and the resolver silently never opened. Thread the
consent through the hub, client, coordinator and resolver; surface a refusal in
the footer instead of dropping it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 17:25:52 +02:00
mika kunsandClaude Sonnet 5.5 bba7117d54 fix(ui): auto-scroll the lists sidebar while dragging near its top/bottom edge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 17:16:34 +02:00
mika kuns ff7197c410 refactor(worker): drop ForceSetStatusAsync, route every status write through a guarded transition
Mark-done now fires OnChildTerminalAsync, so ticking a chain child or the last open child
off by hand unblocks the successor and advances the parent. The hub's Mark-as menu and
update_task_status map onto MarkDone/Cancel(retire)/Enqueue/Reset; revert_merge uses the
new ReopenForReviewAsync (Done|WaitingForReview -> WaitingForReview).
2026-10-02 16:03:59 +02:00
mika kuns 59d990c267 perf(worker): read untracked-worktree sweep from .git instead of spawning git per list 2026-10-02 15:37:35 +02:00
mika kuns a75acbb2c0 fix(ui): close the pseudo console when a conpty tile is disposed 2026-10-02 15:30:12 +02:00
mika kuns b35755e1f0 fix(ssh): remove remote mcp config via trap when the tile kills ssh 2026-10-02 15:30:12 +02:00
mika kuns 4b1f5c65c3 fix(worker): put ~/.local/bin on PATH for remote claude in ssh sessions 2026-10-02 14:36:01 +02:00
mika kuns 2e29e03a8e docs(ssh): document ssh workspace sessions 2026-10-02 14:34:34 +02:00
mika kuns 8c5db95c01 fix(ui): ssh settings tab survives an offline worker without wiping workspaces 2026-10-02 14:33:57 +02:00
mika kuns 89ed885295 feat(ui): ssh workspaces settings tab 2026-10-02 14:32:05 +02:00
mika kuns 6c400dfb1a feat(ui): ssh workspaces as quick-session targets in mission control 2026-10-02 14:29:26 +02:00
mika kuns 1b58e628e6 feat(worker): ssh launch spec with reverse-tunnelled claudedo MCP 2026-10-02 14:26:52 +02:00
mika kuns 34970ca600 feat(worker): remote MCP listener gated by per-session tokens 2026-10-02 14:23:37 +02:00
mika kuns 0314319872 feat(data): store ssh workspaces on app_settings 2026-10-02 14:21:21 +02:00
mika kuns f7625baa24 docs(ssh): spec and plan for ssh workspace sessions 2026-10-02 14:18:59 +02:00
mika kuns 9e4a14040e chore(claude-do): merge Remove http logs from worker logs window. they are just nois
ClaudeDo-Task: 2184fc2e73434ce59c1ddfb1cac9cbfa
2026-10-02 13:03:13 +02:00
mika kuns d9f42d7d48 fix(conpty): stop pinning --effort on hand-driven interactive sessions
Task, ad-hoc, planning, handoff and resume sessions now inherit the CLI
default / the user's /effort setting. List-handler sessions keep their
per-role effort.
2026-10-02 12:09:19 +02:00
mika kunsandClaude Sonnet 5.5 542501cbe2 fix(worker): drop outbound HttpClient request logs from the worker log
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 11:18:40 +02:00
mika kuns 309bdcc903 feat(claude-do): merge Task context menu: "Mark as → Idle" for cancelled tasks
ClaudeDo-Task: ad39dbb38d314e96803decc4e194e180
2026-10-02 10:33:36 +02:00
mika kuns 6f7dcff719 feat(claude-do): merge Pasting a clipboard image into the task description attaches
ClaudeDo-Task: 46c733e18d6c4bbb8742d0a1b0e3d829
2026-10-02 10:33:28 +02:00
mika kuns d9d4f931ae fix(claude-do): merge Usage monitor tables: stop header spill and trim long names
ClaudeDo-Task: 4607fc529278430cba4825e82f740981
2026-10-02 10:33:13 +02:00
mika kuns 01dc44174a feat(claude-do): merge Diff viewer file tree: folder/file-type icons and change-kin
ClaudeDo-Task: c5106d39c4e849a6ad0027f77fad742c
2026-10-02 10:33:02 +02:00
mika kunsandClaude Sonnet 5.5 ae778d5c30 feat(diff-viewer): folder/file-type icons and change-kind colours in the file tree
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:31:55 +02:00
mika kunsandClaude Sonnet 5.5 3fab4016f7 feat(tasks): offer Mark as Idle for cancelled tasks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:31:51 +02:00
mika kunsandClaude Sonnet 5.5 393e59460d feat(details): attach clipboard image on Ctrl+V in the task description
Extracts the clipboard bitmap read into a shared ClipboardBitmap helper
(also used by the terminal view); the description TextBox now routes an
image paste through AddFilesAsync as a unique paste-*.png attachment.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:31:47 +02:00
mika kunsandClaude Sonnet 5.5 b7e7aebce9 fix(usage-monitor): wrap table headers, ellipsis-trim long cells with tooltip
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:31:40 +02:00
mika kuns d8f78056c1 chore(claude-do): merge I cant reset a Parked task to idle in the UI
ClaudeDo-Task: ebec622a293b45c1a7437446a68e73fe
2026-10-02 10:10:10 +02:00
mika kunsandClaude Opus 5.5 9a9b86fdcd fix(details): offer clean reset for parked tasks in the Git tab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:09:23 +02:00
mika kuns 0b2802efbb chore(claude-do): merge fix(worktrees): Merge-Erkennung, Cleanup leerer Verzeichniss
ClaudeDo-Task: fad77fdc-d166-41a5-971c-fac16d84ab2c
2026-10-02 09:14:23 +02:00
mika kuns 0c0bb7f24c fix(worktrees): detect external merges, clean up empty dirs, prune in owning repo
- ContentInMain: HEAD ancestor of the default branch or squash-merged
  (merge-tree == target tree), against local origin/<default> without fetch;
  list_worktrees mergedIntoMain and the overview modal ("merged (external)")
  use it, "Cleanup finished" takes clean Active rows that are in main
- cleanup_task_worktree skips the dirty check for a non-git directory
- prune / worktree remove / branch -D run in the repo owning the worktree
  (git-common-dir), not the list's working dir; branch already gone reports
  branchDeleted=true
2026-10-02 09:07:43 +02:00
mika kuns 9c015f34cc fix(claude-do): merge Log Visualizer shows every footer-log message, with footer c
ClaudeDo-Task: 199f5a36ad8a4f329fc8bdeac10c5304
2026-10-01 17:36:45 +02:00
mika kuns c8af06d0bf feat(claude-do): merge Rename sessions in Mission Control's "Recent sessions" flyou
ClaudeDo-Task: e69653d6f4ac44aa8d46a41a12eaf015
2026-10-01 17:36:35 +02:00
mika kunsandClaude Sonnet 5.5 5bf08873bd feat(mission-control): rename sessions inline in the Recent sessions flyout
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 17:35:13 +02:00
mika kunsandClaude Sonnet 5.5 c956ab102e fix(ui): Log Visualizer shows every footer-log message
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 17:35:03 +02:00
mika kuns d9355383a4 refactor(claude-do): merge Material icons: delete dead Icon.* geometries + guard test
ClaudeDo-Task: 13b9cdfe-2b78-4764-9edb-8e944ec260ee
2026-10-01 17:13:19 +02:00
mika kuns f6e0e824ef test(prompts): align improvement-child assertion with a76788a5 2026-10-01 17:03:14 +02:00
mika kunsandClaude Sonnet 5.5 de68515128 refactor(ui): delete dead Icon.* geometries and icon-stroke styles, add MDI name guard test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:56:45 +02:00
mika kuns 03f5eae35b style(claude-do): merge Material icons: shared controls + modals (incl. Unicode glyp
ClaudeDo-Task: 4d7d12b8-bdb2-44c1-be41-8975740bbcc0
2026-10-01 16:53:44 +02:00
mika kuns 72141b133a style(claude-do): merge Material icons: detail pane
ClaudeDo-Task: 5bc6c034-fd5a-47ea-ac05-035d38e67489
2026-10-01 16:53:32 +02:00
mika kuns 426d8eb137 style(claude-do): merge Material icons: task rows + notes editor
ClaudeDo-Task: e940ffc0-4889-4f73-bae3-95bab3efc633
2026-10-01 16:53:19 +02:00
mika kuns ef50973d01 refactor(claude-do): merge Material icons: list + smart-list icon keys with legacy mapp
ClaudeDo-Task: b150b07a-a8d3-4093-8237-4877c2f695c8
2026-10-01 16:53:07 +02:00
mika kuns ce3f233710 style(claude-do): merge Material icons: window chrome + Mission Control
ClaudeDo-Task: d92ba41f-11ba-4760-b6ae-c9e1f5cf40dd
2026-10-01 16:52:46 +02:00
mika kuns 3e11090fe3 style(claude-do): merge Material icons: lists sidebar
ClaudeDo-Task: a9873dfa-7a76-4795-ab54-419554a985c0
2026-10-01 16:52:17 +02:00
mika kunsandClaude Sonnet 5.5 7c0fe92a3e refactor(ui): Material icons for list, folder and smart-list icon keys with legacy mapping
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:50:31 +02:00
mika kunsandClaude Sonnet 5.5 89312e0b22 style(ui): Material Design icons for window chrome, modal shell and Mission Control
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:50:22 +02:00
mika kunsandClaude Sonnet 5.5 476a94960e style(ui): Material Design icons for shared controls and modals
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:50:06 +02:00
mika kunsandClaude Sonnet 5.5 20b5ff8f3e style(ui): Material Design icons for the detail pane
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:48:49 +02:00
mika kunsandClaude Sonnet 5.5 2d1570ed15 style(ui): Material Design icons for task rows, notes editor and add-task row
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:48:32 +02:00
mika kunsandClaude Sonnet 5.5 203112a051 style(ui): Material Design icons for the lists sidebar
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:47:56 +02:00
mika kuns aef41f0594 style(ui): filled cog for the list settings header button 2026-10-01 16:31:35 +02:00
mika kuns cbab4da54c style(ui): Material Design icons for the tasks island header bar 2026-10-01 16:17:30 +02:00
mika kuns 34c71bbe02 fix(ui): align island header separators via shared header height 2026-10-01 16:07:05 +02:00
mika kuns a76788a5b9 refactor(prompts): drop harness duplicates and maintainer notes from bundled prompts
- system.md: remove read-before-edit / prefer-editing / no-restating-comments
  (enforced by the Edit tool and stated by the Claude Code system prompt)
- improvement-child.md: stop restating the Finishing rules system.md already
  carries in the same session
- merge-helper-wait.md: drop the MaxParallelExecutions history note
- merge-helper-triage.md: read the handoff cap via get_handoff_status instead
  of assuming two rounds; shorten the handoff explanation
- merge-helper-merge.md: replace the German quote with the rule it refers to
- power-use.md: drop the get_handoff_status instruction now that triage does it
2026-10-01 15:56:23 +02:00
mika kuns d0c931e128 fix(claude-do): merge Open diff viewer and merge window as independent top-level w
ClaudeDo-Task: 29564718-d27e-40ad-a61e-9e2b79ede53d
2026-10-01 14:52:38 +02:00
mika kuns 1200a599cc fix(claude-do): merge Mission Control: show post-merge verify as "Verifying" inste
ClaudeDo-Task: eec77f99-1986-4db7-80d9-8294dcc1c146
2026-10-01 14:52:07 +02:00
mika kunsandClaude Sonnet 5.5 46e9303da6 fix(mission-control): show post-merge verify as Verifying instead of In review
Derived from the VerifyMerge entry in RunningOperations (no new TaskStatus). The Running now row stays, reworded to 'Verifying after merge'.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 14:50:55 +02:00
mika kunsandClaude Sonnet 5.5 0fea339b6d fix(ui): open diff viewer and merge window as independent top-level windows
Owned windows are pinned above their owner by Windows, so the main window
could never come to the front. Show() without an owner and centre manually.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 14:49:27 +02:00
mika kuns 5dae5ff679 chore(claude-do): merge The Drag pointer, doesnt change back over the Buttons on a P
ClaudeDo-Task: 87578ec02e2540a387ebc1672e4f1bfe
2026-10-01 14:32:38 +02:00
mika kunsandClaude Sonnet 5.5 74e06d6219 fix(mission-control): reset cursor to arrow over pane header buttons
Cursor inherits down the visual tree, so buttons in the draggable pane header kept the SizeAll cursor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 14:30:31 +02:00
mika kuns e79f1bf22c fix(claude-do): merge Worker log: stop writing SignalR hub invocation arguments (P
ClaudeDo-Task: ae4e878c-e275-4c4f-94b5-ecb9b9602fd5
2026-10-01 13:33:19 +02:00
mika kuns eaa697b08e fix(claude-do): merge batch_get_tasks: accept "number" (and "id") in the fields fi
ClaudeDo-Task: e2ee8054-eb93-456a-98b2-f3362818f410
2026-10-01 13:32:59 +02:00
mika kuns a4596eea5b fix(claude-do): merge preview_merge_set: don't report a symbol as removed when the
ClaudeDo-Task: 4882c459-374c-4afe-a60d-2b25b8cce407
2026-10-01 13:32:48 +02:00
mika kuns 57257c3ead fix(claude-do): merge Fix ghost rows in Mission Control's "Running now" section (s
ClaudeDo-Task: 7b02a754-9472-4e2c-a6bc-02e50013eddb
2026-10-01 13:32:38 +02:00
mika kuns f97914878b test(claude-do): merge Fix Worker.Tests worktree temp-path collisions between concu
ClaudeDo-Task: c098eb71-792c-4dda-842c-6ee10c67daf5
2026-10-01 13:32:27 +02:00
mika kunsandClaude Sonnet 5.5 a736cf3098 test(worker): isolate GitRepoFixture repos under a unique parent so sibling worktree paths never collide
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:31:26 +02:00
mika kunsandClaude Sonnet 5.5 06fb73c4dd fix(worker): preview_merge_set no longer reports a symbol the same diff re-declares
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:30:21 +02:00
mika kuns ef5dad2700 fix(worker): batch_get_tasks accept "number" and "id" in fields filter
batch_get_tasks with fields: ["number", ...] was rejected with "Unknown
field(s)" even though every task DTO always carries id and number. Add
both to KnownDetailFields so they are accepted; BuildDetail already emits
them regardless of the fields filter.

Also add a test verifying fields: ["number", "title"] works and
description is properly excluded.
2026-10-01 13:26:47 +02:00
mika kunsandClaude Sonnet 5.5 e9c1ff2e70 fix(ui): Mission Control no longer lists finished operations as running
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:26:05 +02:00
mika kunsandClaude Sonnet 5.5 61645acf17 fix(worker): drop SignalR hub invocation events so PATs stay out of the worker log
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 13:26:01 +02:00
mika kuns c834f17c78 fix(azdo): strip credentials from origin URL in the not-ready message 2026-10-01 12:38:02 +02:00
mika kuns 27dca48c43 Merge branch 'claudedo/387-unit-branch-teil-3-firmen-modus-fetch-pu' 2026-10-01 12:30:13 +02:00
mika kuns 4917533cb4 fix(worker): kept worktree in a company repo opens a PR, unexpected ADO/git errors never fail a task start 2026-10-01 12:25:15 +02:00
mika kuns 93fd6740a1 docs(worker): document company mode (fetch, push, draft PR) and its open smoke tests 2026-10-01 12:18:07 +02:00
mika kuns 674db27f6e fix(worker): keep Git Credential Manager from prompting during company fetch/push 2026-10-01 12:18:06 +02:00
mika kuns 0e6a48e268 feat(prompts): merge helper knows company-repo approve ends in a draft PR 2026-10-01 12:15:20 +02:00
mika kuns 4976ded7ae feat(ui): external services tab with Azure DevOps PAT, draft PR button and link in company repos 2026-10-01 12:13:57 +02:00
mika kuns ec25ed3c75 feat(claude-do): merge Show currently running Worker operations (verify, merge, reb
ClaudeDo-Task: ea44c9dd0c4942bfa3490bc3c1014205
2026-10-01 12:10:49 +02:00
mika kunsandClaude Sonnet 5.5 2dbec543df feat(mission-control): show running Worker operations in a Running now section
Registry of active verify/merge/rebase/worktree-creation operations in the Worker,
GetActiveOperations snapshot + OperationStarted/OperationEnded hub events, and a
Running now section in Mission Control with ticking elapsed time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:08:43 +02:00
mika kuns fed0b6ac51 feat(hub): Azure DevOps PAT settings and connection test 2026-10-01 12:06:56 +02:00
mika kuns bc959f25b3 feat(worker): approve in a company repo verifies in the worktree, pushes and opens a draft PR 2026-10-01 12:05:33 +02:00
mika kuns 7f7f237656 feat(worker): company unit roots start from a fetched origin base, local base fast-forwarded best-effort 2026-10-01 11:59:11 +02:00
mika kuns 78f4d182e8 feat(worker): Azure DevOps client and PAT store, ported from pr-create.ps1 2026-10-01 11:55:11 +02:00
mika kuns f85b9cddfd fix(claude-do): merge Hide the "Submit for review" button on list-handler panes in
ClaudeDo-Task: ea129b1c8c2f47cbaa4db19a2bb5b441
2026-10-01 11:52:52 +02:00
mika kuns 61105ff9d3 fix(claude-do): merge Make the merge modal non-modal so a running merge no longer
ClaudeDo-Task: b2b4c74fc265435b9f083f56d0e47546
2026-10-01 11:52:40 +02:00
mika kuns 488c28a9d5 fix(claude-do): merge Prevent dragging completed tasks in the task list
ClaudeDo-Task: 855b766589bf42bc872f03bdbfb9abd5
2026-10-01 11:52:30 +02:00
mika kuns acf3987a8a fix(claude-do): merge Fix the text caret overlapping the last typed letter in the
ClaudeDo-Task: c0619bf81ed645e7a1c861ddc6af5905
2026-10-01 11:52:20 +02:00
mika kuns c33e57b9c7 feat(claude-do): merge Show the task number (#N) on running autonomous tasks in Mis
ClaudeDo-Task: 6cd67a16ec6647c1846880481c497b20
2026-10-01 11:52:09 +02:00
mika kuns 76f260a9a5 feat(git): fetch/push/ff primitives with auth env, no task trailer in company repos 2026-10-01 11:51:28 +02:00
mika kunsandClaude Sonnet 5.5 3d74e3dac5 fix(ui): open the merge modal non-modal so a running merge no longer blocks the main window
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:50:58 +02:00
mika kuns bc0aa28afb feat(data): worktrees.pull_request_url for the company-repo draft PR 2026-10-01 11:49:30 +02:00
mika kuns 2b2f61d7fc docs(planning): plan for unit branch part 3 (company mode, draft PR) 2026-10-01 11:48:11 +02:00
mika kunsandClaude Sonnet 5.5 0cfc28adb9 fix(ui): hide Submit for review on list-handler panes in Mission Control
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:44:48 +02:00
mika kunsandClaude Sonnet 5.5 b776fd28d8 fix(ui): prevent dragging completed tasks in the task list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:44:17 +02:00
mika kunsandClaude Sonnet 5.5 f66cbca65d feat(ui): show task number in Mission Control activity strip
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:43:50 +02:00
mika kunsandClaude Sonnet 5.5 335bfe957b fix(ui): add right padding to add-task input so the caret clears the last glyph
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:41:40 +02:00
mika kuns 2b7b58be37 chore(claude-do): merge Unit-Branch Teil 2: Children per fast-forward in den Unit-Br
ClaudeDo-Task: cd39f83b-f80b-479c-bdeb-55c632dcffd8
2026-10-01 11:37:21 +02:00
mika kuns acd188bca9 fix(worker): block approve of a planning parent with an unfinished child, surface git stderr on ff failure 2026-10-01 11:36:45 +02:00
mika kuns a353897345 docs(external): describe unit-branch routing in the review_task tool description 2026-10-01 11:27:22 +02:00
mika kuns 823368484e docs(worker): document unit branch, child absorb and legacy approve routing 2026-10-01 11:26:17 +02:00
mika kuns 44d94caabc feat(planning): combined diff of a unit is base..head, no integration branch checkout 2026-10-01 11:25:48 +02:00
mika kuns 1f373a5a67 feat(worker): approve a unit parent like a single task, orchestrator only for legacy units 2026-10-01 11:25:48 +02:00
mika kuns 9da522b0c9 feat(worker): fast-forward finished children into the parent's unit branch
Planning parents get their own worktree on Queue plan, children fork from the
unit head and are absorbed via merge --ff-only before Done (ff failure -> Failed,
interactive child approve -> absorb instead of merge to the target).
2026-10-01 11:25:47 +02:00
mika kuns 724ad90f88 feat(git): add ff-only merge and allow a merged worktree without merge commit 2026-10-01 11:25:47 +02:00
mika kuns dc73f138d2 docs(planning): plan for unit branch part 2 (children fast-forward) 2026-10-01 11:13:50 +02:00
mika kuns 07e9f8d60e chore(claude-do): merge Unit-Branch Teil 1: Branch-Naming + Basis-Branch pro Liste
ClaudeDo-Task: e202e601-64c0-4ecb-8dc9-2fb0da01a84a
2026-10-01 11:03:02 +02:00
mika kuns cd5b27c063 fix(planning): return to the list base branch after integration cleanup, not a guessed task branch 2026-10-01 10:55:12 +02:00
mika kuns fb86a3c1bf chore(claude-do): merge Actual highlighting in the Autonomus agount output window
ClaudeDo-Task: 87096034c52f4c869fb1518572c7f49f
2026-10-01 10:55:00 +02:00
mika kuns 9bb0e67a97 feat(ui): colour agent output by kind and stop cutting tool calls at 120 chars
StreamLineFormatter now returns kind-tagged entries (text, tool, edit,
tool result, error, sys, result) instead of a flat string, so the
existing LogKind colours finally apply to stream-json output. Tool
argument/result limit raised from 120 to 500 chars.
2026-10-01 10:49:52 +02:00
mika kuns 8ac272df50 docs(worker): document new branch naming and per-list base branch 2026-10-01 10:49:08 +02:00
mika kuns ae845bf06b feat(ui): base branch field in list settings, real branch names in dialogs 2026-10-01 10:48:32 +02:00
mika kuns 69c7803bdf feat(worker): use the list base branch as worktree base and default merge target 2026-10-01 10:46:21 +02:00
mika kuns 3fe1feadf9 feat(data): per-list base branch, preserved by every config writer 2026-10-01 10:39:49 +02:00
mika kuns 8911408bca refactor(worker): detect untracked worktrees by path instead of branch prefix 2026-10-01 10:37:37 +02:00
mika kuns 814b083a4e feat(worker): name task branches <type>/<number>-<slug> 2026-10-01 10:35:52 +02:00
mika kuns 4a9cff5543 feat(worker): branch type and number rules for task branches 2026-10-01 10:30:51 +02:00
mika kuns 0e4de9e4c0 feat(claude-do): merge Selectable colors for list dots and folders: accent-aware pa
ClaudeDo-Task: 98844480cc1f4e748908d2ce8ac6c50b
2026-10-01 10:30:48 +02:00
mika kuns 68445a9f2a fix(claude-do): merge ConPTY tiles: deliver AltGr characters (@, \, {, }, €, …) as
ClaudeDo-Task: 4abf8b93e1d94c98b122521a8b850a77
2026-10-01 10:29:59 +02:00
mika kuns a6926f53b5 feat(lists): selectable list/folder colors with accent-aware swatches and custom picker 2026-10-01 10:29:53 +02:00
mika kuns d316f3c6a5 feat(claude-do): merge Stream Claude CLI update progress into the footer strip
ClaudeDo-Task: 92130a5d447b4b0da301ce72e42c041a
2026-10-01 10:29:48 +02:00
mika kuns faa91e0661 feat(worker): detect company repos by origin remote pattern 2026-10-01 10:28:54 +02:00
mika kuns db3c7e69e5 docs(plan): unit branch part 1 - naming and per-list base branch 2026-10-01 10:24:43 +02:00
mika kunsandClaude Sonnet 5.5 2b19c8c49d feat(ui): stream claude update progress into the footer strip
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:24:07 +02:00
mika kunsandClaude Sonnet 5.5 cf3db9197c fix(ui): deliver AltGr characters to ConPTY tiles as text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:24:05 +02:00
mika kuns 3dc735b2af docs(spec): per-list base branch and fetch before unit branch 2026-10-01 10:16:32 +02:00
mika kuns 0e9373c083 docs(spec): unit branch and draft PR design 2026-10-01 09:56:36 +02:00
mika kuns 97af85c816 feat(mission-control): swap tiles by dragging their header 2026-10-01 09:07:02 +02:00
mika kuns 16adcad84f docs(worker): note the external MCP DI registration trap 2026-10-01 08:49:53 +02:00
mika kuns 2b40ea2d2b fix(mcp): register ticket import tools in the external MCP container
The external MCP server has its own DI container; TicketMcpTools could not
resolve TicketImportService there, so both tools failed at runtime. Also
surface a skipped import in the prime outcome and treat a sweep where every
project board fails as an error instead of an empty backlog.
2026-10-01 08:49:31 +02:00
mika kuns 7724f5eb70 docs(tickets): document cross-project ticket import and open checks 2026-10-01 08:45:54 +02:00
mika kuns cb775958a7 docs(ui): note the ImportTickets prime radio and the ticket fallback list 2026-10-01 08:45:14 +02:00
mika kuns e35b054a2e feat(ui): tickets + plan day button, fallback list and prime action 2026-10-01 08:44:21 +02:00
mika kuns 9fc2e26edb feat(hub): add RunTicketImportNow and ticket fallback list endpoints 2026-10-01 08:41:12 +02:00
mika kuns 915f2c8352 feat(prime): ImportTickets runs the ticket import before filling MyDay 2026-10-01 08:39:38 +02:00
mika kuns 57c30c4a18 feat(mcp): add get_ticket_import_candidates and import_ticket tools 2026-10-01 08:37:44 +02:00
mika kuns 47583ab1fe feat(tickets): sweep all project boards for assigned tickets and tasks 2026-10-01 08:36:11 +02:00
mika kuns a0b234fe10 feat(data): add ImportTickets prime kind, ticket fallback list and import prompt 2026-10-01 08:33:48 +02:00
mika kuns c34680b42d docs(tickets): implementation plan for cross-project ticket import 2026-10-01 08:31:13 +02:00
mika kuns 5bb49495e4 docs(tickets): switch import source to project-board sweep after live probe 2026-10-01 08:12:17 +02:00
mika kuns 0ac49653c9 docs(tickets): design for cross-project ticket import with day planning 2026-10-01 08:05:58 +02:00
ClaudeDo CI 899648adc0 docs(changelog): update for v2.18.0 2026-09-30 09:37:28 +00:00
Mika Kuns f3ecb99492 chore(claude-do): merge Customizable Spalten Anzahl in MIssion controll
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 43s
ClaudeDo-Task: 99bfb985efb64c02ba3f27b53eae790d
2026-09-30 10:50:21 +02:00
Mika Kuns a2496e056a chore(claude-do): merge Worktrees sollten nach dem Task benannt werden
ClaudeDo-Task: 63f08d944d3341c081267158ef4bbb3e
2026-09-30 10:50:09 +02:00
Mika Kuns 44ec89b0e6 chore(claude-do): Worktrees sollten nach dem Task benannt werden
Durch die GUID namen kann ein User die Worktrees keinem Task zuordnen, und in Rider Müllen mir die Worktrees die BRanches voll. Also sollten wir einfach den Task titel oder Nummer stattdessen nehmen

ClaudeDo-Task: 63f08d944d3341c081267158ef4bbb3e
2026-09-30 10:49:07 +02:00
Mika Kuns a7c3d18533 feat(mission-control): column picker for the overview grid
Auto keeps the pane-count heuristic; 1-4 fixes the column count (capped at
the pane count). Persisted to ui.config.json as MissionControlColumns.
2026-09-30 10:39:23 +02:00
mika kuns 0d029717f8 style(ui): folder chevron and icon take the folder colour 2026-09-30 10:28:14 +02:00
mika kuns 2be8ba5d66 feat(ui): folders slide open and closed, rows below follow 2026-09-30 10:25:17 +02:00
mika kuns 928e3905d9 feat(ui): animate folder expand in the sidebar 2026-09-30 10:19:35 +02:00
mika kuns 60358100be fix(ui): Recent-Sessions Resume/Brief-Buttons fuehren ihren Command wieder aus
Der Click-Handler schloss das Flyout synchron; Button liest Command erst nach dem ClickEvent, das Hide hatte die Bindung da schon geloest.
2026-09-30 10:14:55 +02:00
mika kuns 2a41f7e641 style(ui): smart list rows match the user list row height 2026-09-30 10:13:51 +02:00
mika kuns bb92dc279f feat(lists): assign a colour to a sidebar folder
Tints the folder name and its guide line; picked from a swatch row in the folder settings.
2026-09-30 10:12:41 +02:00
mika kuns 8a31a67c74 feat(report): write weekly report as short bullets with haiku 2026-09-30 10:05:24 +02:00
mika kuns f2ff865dca feat(ui): searchable Material Design icon library for lists and folders 2026-09-30 09:52:34 +02:00
mika kuns 5da8036b9a style(ui): only top-level lists align to the folder chevron 2026-09-30 08:48:36 +02:00
mika kuns cf4b5c507c style(ui): lists start at the folder chevron's edge 2026-09-30 08:43:10 +02:00
mika kuns 3e2d5bab6a style(ui): keep child row highlight clear of the folder guide line 2026-09-30 08:39:20 +02:00
mika kuns 27d2b1d8ef fix(tickets): convert ticket HTML to markdown on import 2026-09-30 08:39:17 +02:00
mika kuns ccd11a8033 style(ui): tighter sidebar tree rows and shallower child indent 2026-09-30 08:34:48 +02:00
mika kuns 9991f1fc1f feat(ui): coloured dots for icon-less lists, guide line under chevron
Lists without an icon show a dot in a stable identity colour (same
palette/hash as Mission Control tiles); "Dot" is the new picker default.
2026-09-30 08:29:46 +02:00
mika kuns 75b78cb2c0 feat(ui): make sidebar folder hierarchy readable
Lists default to the Text icon instead of the folder icon, rows align
across depths with a guide line per open folder, folder names are
semibold, folder counts only show while collapsed and zero counts hide.
2026-09-30 08:21:34 +02:00
mika kuns 05deda8343 docs: add visual verification session notes 2026-09-16 2026-09-30 08:14:26 +02:00
mika kuns 52ba538339 test(worker): cover WorktreeUpdated broadcast on interactive review submission 2026-09-30 08:14:26 +02:00
mika kuns 2558e02ba3 fix(ui): stop row buttons flashing disabled while a row command runs
Shared AsyncRelayCommands disabled every row's button during execution.
2026-09-30 08:13:26 +02:00
mika kuns 81dcaebfba Merge branch 'claudedo/f518b71f990643cfaf73174e8617b2b5'
Drop the unused deferredUntil parameter the branch added to HandoffRequested: the worker still sends three args, so the four-arg client handler would never bind. The reset time already arrives via HandlerHandoffDeferred.
2026-09-29 08:54:14 +02:00
mika kuns 1396649519 Merge branch 'claudedo/ff26e146286f409aacc453247dd86155' 2026-09-29 08:52:02 +02:00
ClaudeDo CI f88d2a898e docs(changelog): update for v2.17.0 2026-09-29 06:24:37 +00:00
Mika Kuns 87dbb117d7 fix: Interactive sessions no longer Instantly start working
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 45s
2026-09-28 14:33:06 +02:00
Mika Kuns 0f82fa67e1 feat(claude-do): merge UI-Prozess loggt auf Platte (ui-.log) + fehlgeschlagene Hub-
ClaudeDo-Task: d2ae3831-733f-4d40-93d7-70c93da11590
2026-09-28 13:39:18 +02:00
Mika Kuns 9f2e63d337 feat(claude-do): merge Ordner in Ordner erlauben (beliebig tief, nur per Drag & Dro
ClaudeDo-Task: dc3d5f8cf9e74821ba99b18e443fe5f5
2026-09-28 13:37:19 +02:00
Mika Kuns 3009ff0f8f fix(claude-do): snake_case Serilog tokens + no Warning-log on cancelled hub calls
Message-template tokens must be snake_case (repo rule) -- fixes 4 PascalCase
tokens introduced in 57dd6766. TryInvokeAsync's swallowing catch also no
longer logs a Warning for OperationCanceledException, which is routine
(e.g. worker shutdown/reconnect), not a failure worth a trace.

ClaudeDo-Task: d2ae3831-733f-4d40-93d7-70c93da11590
2026-09-28 13:32:38 +02:00
Mika KunsandClaude Sonnet 5 0dac047c46 feat(lists): allow nested sidebar folders via drag and drop
Folders can now contain other folders, to arbitrary depth, via a
self-referencing parent_folder_id (ON DELETE SET NULL, same pattern as
lists.folder_id). Nesting/un-nesting is drag-and-drop only: dropping a
folder onto another folder's header middle moves it in, the outer
quarters or any top-level/in-folder drop insert it beside a target.
Dropping a folder into itself or one of its own subfolders is silently
ignored. Deleting a folder releases its lists and subfolders back to
the top level, unchanged from the existing list behavior.

ListFolderNavItemViewModel.Lists became Children (object, mixing lists
and subfolders, mirroring TopLevel) so the existing implicit, type-matched
DataTemplates render nested folders recursively without new XAML, and each
nesting level gets its indent for free from the folder template's own
margin. Folders/RootLists/ContainerOf/PersistOrderAsync/RecomputeFolderCounts
now walk the tree instead of assuming one level.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 13:31:26 +02:00
Mika Kuns 57dd676672 feat(claude-do): UI-Prozess loggt auf Platte (ui-.log) + fehlgeschlagene Hub-
## Problem
Beim DeleteTask-Bug (Fix 352aaa52) gab es null Spuren: Der UI-Prozess (ClaudeDo.App) hat gar kein File-Logging (weder Serilog noch `Microsoft.Extensions.Logging` sind in App/Ui referenziert), und im Worker loggt SignalR fehlgeschlagene Hub-Invocations nur auf Debug (`Microsoft.AspNetCore` steht in `src/ClaudeDo.Worker/Program.cs` Z. 60 auf Warning). Die Ursache ließ sich nur finden, ind

ClaudeDo-Task: d2ae3831-733f-4d40-93d7-70c93da11590
2026-09-28 13:27:21 +02:00
mika kuns 352aaa525b fix(worker): DeleteTask aus der UI schlug immer mit generischem Serverfehler fehl
Der CancellationToken-Parameter auf der nicht-streamenden Hub-Methode liess SignalR jede Invocation vor dem Methodenrumpf abbrechen. Die Tests riefen den Hub direkt auf und sahen das nicht; neuer Reflection-Guard verbietet CancellationToken auf allen nicht-streamenden Hub-Methoden.
2026-09-28 13:02:47 +02:00
Mika Kuns c8b2ad046a chore(claude-do): merge Mission Control: Recent Sessions mit verschachtelten Handoff
ClaudeDo-Task: e51c2b8a-614b-4e4f-8e93-6f90a7ba9960
2026-09-28 12:24:39 +02:00
Mika Kuns d3db4455fa feat(ui): Mission Control Recent Sessions mit verschachtelten Handoffs
Neue Tabelle mission_control_sessions (Migration AddMissionControlSessions), befuellt vom
InteractiveLaunchSpecService bei Task-, Ad-hoc- und Handoff-Kacheln. Flyout im Mission-Control-Header
listet die letzten 14 Tage / 50 Sessions, Handoffs eingerueckt unter ihrer Ursprungs-Session, mit
Fortsetzen (--resume im gespeicherten cwd) und Brief anzeigen.
2026-09-28 12:11:21 +02:00
Mika Kuns 216620c52e chore(claude-do): merge Mission Control: allgemeiner Session-Handoff an eine frische
ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
2026-09-28 11:58:14 +02:00
Mika Kuns 08d1797ead chore(claude-do): Mission Control: allgemeiner Session-Handoff an eine frische
## Ziel

Jede Mission-Control-Session (Ad-hoc UND Task-Session) soll ihre Arbeit an eine frische Session
uebergeben koennen: "gib das ab an eine neue Session". Die alte Kachel wird geschlossen, an
derselben Position oeffnet eine frische Session im selben cwd, die mit einem Brief startet, den
die abgebende Session selbst geschrieben hat.

Motivation: eine lange Session mit vollem/verrottetem Kontex

ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
2026-09-28 11:57:37 +02:00
mika kuns e0a1448f44 feat(ui): Mission-Control-Kacheln behalten Position und Identitaetsfarbe ueber Handoffs
Ein Handoff schloss die alte Pane und haengte die neue hinten an -- die Kachel
sprang ans Ende und das ganze Grid ordnete sich neu. Die neue Pane wird jetzt am
Index der alten eingesetzt.

Dazu bekommt jede Kachel eine aus IdentityKey (Default: TaskId) abgeleitete Farbe:
3px-Streifen links am Tile, Punkt im Focus-Mode-Tab. Kein State, keine Persistenz --
derselbe Task faerbt nach App-Neustart gleich, und ein Handoff erbt die Farbe, weil
er dieselbe TaskId behaelt. Nur kuehle Hues, damit nichts als Status gelesen wird.
2026-09-28 08:56:50 +02:00
ClaudeDo CI c9dd885fe0 docs(changelog): update for v2.16.0 2026-09-16 13:44:19 +00:00
mika kunsandClaude Opus 5 31dfaffc8b docs(changelog): update for v2.16.0
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:43:58 +02:00
Mika Kuns 09bcd696dd feat(ui): dirty Ziel-Verzeichnis beim Oeffnen des Merge-Dialogs melden statt nach dem Fehlschlag
MergeTargetsDto/MergeTargets bekommen TargetDirty (via HasChangesAsync,
includeUntracked: false). Das Merge-Modal zeigt Warnung + Stash-Retry
sofort in InitializeAsync statt erst nach dem dirty_target-Fehlschlag;
der REVIEW-Block in der Detail-Pane zeigt denselben Hinweis neben
Approve. Der dirty_target-Rueckgabepfad bleibt als Absicherung bestehen.

Behebt Befund F6 aus docs/test-session-2026-09-16.md.
2026-09-16 15:43:39 +02:00
Mika Kuns 86cc309ba9 fix(ui): duplizierter Confirm-Dialog mit "Delete"-Knopf – zusammenfuehren und lokalisieren
F4: DetailsIslandView/TasksIslandView hatten je eine view-lokale Confirm/Error-Dialog-Kopie,
teils mit hartkodiertem "Delete"-Knopf fuer jede Aktion (inkl. der Stash+Merge-Bestaetigung,
die nichts loescht). Beide Kopien entfernt; DetailsIslandViewModel/TasksIslandViewModel routen
Confirm/Error jetzt durch den geteilten Dialogs-Seam (IDialogService, propagiert vom Shell wie
schon bei Lists/Tasks) statt ueber eine vom Code-behind gesetzte Func-Property.

IDialogService.ConfirmAsync bekommt einen optionalen confirmLabel-Parameter (Default: lokalisiertes
"Confirm"); nur die echten Delete-Aktionen (Task loeschen, abgeschlossene Tasks leeren) uebergeben
das lokalisierte "Delete"-Label.

F5: "Cancel"/"Confirm"/"Delete"/"OK" und die Titel "Confirm"/"Error" durch Loc.T mit neuen Keys
unter "dialog.*" ersetzt (en.json + de.json). Mission Controls eigene Dialog-Kopie bleibt bestehen
(eigenes Fenster, WindowDialogService ist an den Haupt-Owner gebunden) — nur ihre Labels wurden
lokalisiert.

Out of scope (siehe Task-Beschreibung): die Nachrichtentexte der Dialoge bleiben unlokalisiert.
2026-09-16 15:43:38 +02:00
Mika KunsandClaude Sonnet 5 684b0115e5 feat(worker): preview_merge_set flags removed symbols and locale-key overlaps
Overlap detection was file-name-only: 9/15 tasks touching en.json/de.json in
run #316 all got flagged even though only one pair actually collided on a
key, and a removed method with no textual file conflict (#297 vs #286) went
unflagged entirely.

- removedSymbols per task entry: a line-pattern scan of `-` diff lines for
  C# method/property/field/type/event declarations (skips .axaml/.json).
- symbolWarnings at the set level: cross-references those names against
  every other task's `+` lines, catching same-symbol collisions across
  files that overlaps/subsets (both purely file-based) can't see.
- overlaps narrows to key-path level for .json files (base commit vs. task
  branch head, both sides parsed and flattened) so two tasks adding
  different locale keys to the same file no longer collide; unparsable
  JSON on either side falls back to the old file-level check.
- GitService.ShowFileAsync: `git show ref:path`, used to read a task
  branch's json content at its base/head commit.

Out of scope per the task: no Roslyn/parser, no AXAML analysis, and the
merge-helper prompt in PromptFiles.cs doesn't mention the new fields yet.

Build: dotnet build src/ClaudeDo.App (pulls in Ui+Data) + src/ClaudeDo.Worker, both clean.
Tests: dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~MergeMcpTools|FullyQualifiedName~ExternalMcpToolSchema" -- 51 passed, 0 failed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:43:38 +02:00
Mika Kuns 1325ab9438 fix(ui): Worktrees-Uebersicht zeigt Task-Nummern, DIFF-Spalte trimmt statt abzuschneiden
Befund F7 aus der Sichtpruefung 2026-09-16. TaskNumber war bereits im
WorktreeMaintenanceService-Join und in WorktreeOverviewRow vorhanden, fehlte
aber im Wire-DTO und in der UI-Kette; jetzt bis zur Row-/Conflict-Anzeige
durchgereicht (#<number> dimmed vor dem Titel, ausgeblendet bei 0). DIFF-Spalte
bekommt CharacterEllipsis-Trimming + Tooltip statt hartem Cut, Spalte von 80
auf 110px verbreitert (Header und Row synchron).
2026-09-16 15:43:38 +02:00
Mika Kuns 56161e4e9b feat(ui): Bilder aus der Zwischenablage ins interaktive Terminal einfuegen
Ctrl+V mit einem Bild in der Zwischenablage schreibt es als temporaeres
PNG unter ~/.claudeDo/tmp/ und fuegt den Pfad in den Composer ein, genau
wie ein Datei-Drop -- Claude liest die Datei selbst. Text-Paste laeuft
unveraendert ueber den bestehenden bracketed-paste-Pfad.

Avalonia 12.0.4 liest Clipboard-Bitmaps bereits selbst (IClipboard.
TryGetDataAsync + AsyncDataTransferExtensions.TryGetBitmapAsync), kein
Win32-P/Invoke noetig. Alte Tmp-Dateien (>24h) werden beim App-Start
aufgeraeumt (ClaudeDo.App/Program.cs), keine eigene Aufraeum-Infrastruktur.
2026-09-16 15:43:38 +02:00
Mika Kuns 4ff5383b35 fix(ui): Diff-Viewer scrollt jetzt auch horizontal synchron
VerticalScrollSync mirrort nur noch Y, X blieb pro Pane unabhaengig. Beide
Panes nutzen dieselbe Schrift/Breite, also X 1:1 mitspiegeln statt wie bei
Y ueber TargetOffset umzurechnen. Die Echo-Abbruchbedingung prueft jetzt
beide Achsen (vorher nur Y), sonst wuerde der neue X-Schreibvorgang seine
eigene Echo-Bedingung nie erfuellen und endlos weiterlaufen.

Klasse in ScrollSync umbenannt (DiffTextView + ConflictResolverView
mitgezogen), da sie nicht mehr nur vertikal synchronisiert.

Kein Headless-Test hinzugefuegt: DiffDispatcherGuardTests.cs dokumentiert
bereits, dass ein bootstrapptes Avalonia Application/Dispatcher in
ClaudeDo.Ui.Tests nach drei eskalierenden Versuchen als Sackgasse verifiziert
wurde (Dispatcher-Thread-Affinitaet kollidiert mit xUnits Worker-Pool ohne
Thread-Garantie; Avalonia.Headless.XUnit kollidiert mit xunit 2.9.3). Als
Finding avaloniaedit-ui-test-dead-end festgehalten. Sichtpruefung bleibt
offen (Side-by-side + Merge-Editor mit ueberbreiten Zeilen).
2026-09-16 15:43:37 +02:00
Mika Kuns 5cdfb14ff1 fix(ui): Ordner-Toggle in der Sidebar schluckt schnelle Klicks
ToggleFolderAsync war ein async [RelayCommand]; AsyncRelayCommand sperrt
sich waehrend des DB-Writes selbst, schnelle Klicks gingen verloren
(Befund F1, docs/test-session-2026-09-16.md).

Variante A: Toggle ist jetzt synchron (reiner UI-State), Persistierung
fire-and-forget mit einem CancellationTokenSource pro Ordner, damit ein
zweiter Klick vor dem ersten Schreibvorgang diesen abbricht - die DB
landet immer beim zuletzt sichtbaren Zustand, nie bei einem veralteten.
Gleiches Muster wie SaveTitleAsync/SaveDescriptionAsync in
DetailsIslandViewModel.

Gegenprueft: RemoveFromFolderAsync/CreateListAsync haben denselben
async-RelayCommand-Schnitt, sind aber Einzelklick-Aktionen (Kontextmenue/
Button), kein Rapid-Click-Ziel wie der Ordner-Toggle - bewusst nicht
umgebaut. Die Modal-oeffnenden Commands sind unkritisch, ein Modal
blockiert ohnehin.
2026-09-16 15:43:37 +02:00
Mika KunsandClaude Sonnet 5 7452f6cfe4 fix(worker): preview_merge braucht einen runVerify-Parameter wie preview_merge_set
preview_merge baute bisher immer, sobald die Liste ein Verify-Kommando
konfiguriert hatte -- anders als das Geschwister-Tool preview_merge_set,
dessen runVerify-Parameter per Default false ist. Ein einzelnes preview_merge
im Merge-Helper-Recheck stiess dadurch einen ungewollten Scratch-Build an.

runVerify (Default false) jetzt symmetrisch bei preview_merge ergaenzt,
Description-Text und die Merge-Helper-Prompt-Doku angepasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:43:37 +02:00
Mika KunsandClaude Opus 5 c874bf7e52 fix(worker): depends_on-Nachfolger schon beim gelandeten Merge freigeben
Der Queue-Picker gatete eine task_dependencies-Kante auf status='done'.
review_task(skipVerify) laesst einen erfolgreich gemergten Task aber
bewusst auf WaitingForReview stehen, bis verify_merges einmal zentral
laeuft -- damit blockierte jeder Nachfolger ueber den gesamten Batch plus
Full-Suite-Verify hinweg, bei freien Slots.

Eine Kante gilt jetzt als erfuellt, wenn der Vorgaenger 'done' ist ODER
sein Merge gelandet ist (worktrees.state = 'merged'). TaskBlocking
(get_task/list_tasks/wait_for_task_change) und der UI-Wartehinweis folgen
derselben Regel, damit "blocked" nicht meldet, was der Picker claimt.

Abgeleitet: revert_merge/abort_merge setzen den Worktree auf Active und
blockieren wieder; reject_rerun/reject_park haben nie gemergt und bleiben
unerfuellt; ein Task ohne Worktree wird nur ueber 'done' erfuellt; ein
fehlgeschlagener verify_merges gibt Nachfolger frei, weil der Code so
oder so auf main liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:43:37 +02:00
Mika KunsandClaude Sonnet 5 50b6b56f87 feat(usage): poll the OAuth usage endpoint as fast as its measured rate limit allows
Measured 2026-09-16 against the live endpoint: a clean burst allows exactly 5
requests before a 429, with Retry-After: 300 (a flat 5-req/5-min quota, no
ratelimit-* headers on either the 200s or the 429s). Lowered
UsagePollIntervalActiveSeconds/UsagePollIntervalIdleSeconds default from
300/900 to 120 each (clamp floor 90, up from 60) — leaves ~2.5 requests of
headroom per window for manual refreshes and any other consumer of the same
OAuth token, unlike 60s which consumes the whole quota by itself. The quota
is a flat per-account limit, not tied to ClaudeDo's own running/idle state,
so both keys share the same default now (kept separate for config
compatibility).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:43:37 +02:00
Mika Kuns 9a41bed0f5 fix(ui): Worker-Neustart trifft nun den tatsaechlich laufenden Prozess
RestartWorkerService verliess sich auf WorkerLocator.Find(), das bei einem
Debug-Build ohne install.json in der Verzeichniskette auf die Registry
zurueckfaellt und den Pfad der *installierten* Worker.exe liefert. Der
passt nicht zum laufenden Debug-Prozess, also ueberspringt
KillMatchingWorkerProcesses ihn stillschweigend (Pfadvergleich) - der neu
gestartete Worker startet vom falschen Pfad und stirbt sofort am bereits
belegten Port, waehrend der echte Worker unberuehrt weiterlaeuft. Der
Neustart nimmt jetzt den Pfad des tatsaechlich laufenden
ClaudeDo.Worker-Prozesses als Quelle, mit dem Locator nur noch als
Fallback, wenn keiner laeuft.
2026-09-16 15:43:36 +02:00
Mika Kuns 103eb9a687 fix(ui): Shutdown terminates the app even with secondary windows open
ShutdownWorkerAndCloseApp only closed MainWindow via CloseWindow, relying
on ShutdownMode.OnMainWindowClose to bring the process down. That mode
backs off if any window cancels its own Closing event, and
MissionControlWindow always does (it hides instead of closing) - so the
process never actually exited while Mission Control, Cheat Sheets, or the
diff viewer were still open. Call IClassicDesktopStyleApplicationLifetime
.Shutdown() directly, which force-closes every window regardless of
per-window cancellation.
2026-09-16 15:43:36 +02:00
Mika Kuns 7ecb1fa0b5 Fix: some Reactive stuff 2026-09-16 15:43:36 +02:00
ClaudeDo CI 18394dd6b2 docs(changelog): update for v2.15.0 2026-09-11 09:43:40 +00:00
mika kuns 251b70ce83 feat(merge): Stash-Option fuer uncommittete Aenderungen im Ziel-Tree
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 43s
Ein dirty Ziel-Arbeitsverzeichnis gibt jetzt den eigenen Status
dirty_target zurueck statt blocked. Die UI bietet daraufhin einen
Retry an, der per git merge --autostash stasht, merged und die
Aenderungen danach wiederherstellt - opt-in pro Merge:

- Approve & Merge: Bestaetigungsdialog
- Merge-Modal: Button "Aenderungen stashen und mergen"
- MCP (merge_task/review_task) wirft weiterhin, kein autostash dort
2026-09-11 11:43:28 +02:00
ClaudeDo CI 8e2d37e63e docs(changelog): update for v2.14.0 2026-09-10 14:36:23 +00:00
mika kuns edcaee77dc Merge remote-tracking branch 'origin/main'
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 42s
2026-09-10 15:41:57 +02:00
mika kuns bfb1c6523c feat(ui): Listen und Ordner in einer gemischten Sidebar-Reihenfolge
Die oberste Sidebar-Ebene ist jetzt eine gemischte TopLevel-Collection, so
dass eine Liste auch oberhalb eines Ordners stehen kann. DropAsync deckt
alle vier Quelle/Ziel-Kombinationen ab, ReorderSidebarAsync numeriert die
gesamte sichtbare Reihenfolge ueber lists und list_folders hinweg.
2026-09-10 15:40:10 +02:00
mika kuns 1770b67713 fix(tickets): Import broadcastet TaskUpdated fuer neue Tasks
ImportTickets sendete nur ListUpdated. Die UI liest das als Aenderung der
Listen-Metadaten und patcht nur bestehende Zeilen, also tauchten die
importierten Tasks erst nach Schliessen und Neuoeffnen der Liste auf.
TicketImportResult liefert jetzt die angelegten Task-Ids, der Hub
broadcastet pro Id ein TaskUpdated - derselbe Pfad, den jeder andere
out-of-band Creator nutzt.
2026-09-10 15:39:58 +02:00
mika kuns 3d09323ee1 fix(worker): ConnectedClientTracker in den externen MCP-Container teilen 2026-09-10 14:58:29 +02:00
mika kuns 7c399b26ba Merge branch 'claudedo/ea798a952ab54bcc95dbb442168ee0dd' 2026-09-10 13:59:50 +02:00
mika kuns d6696c3317 fix(localization): de.json-Konsistenz-Pass und restliche harte EN-Strings
Queue/Task(s)/Gate in deutschem Fliesstext auf Warteschlange/Aufgabe(n)/
Limit-Stopp vereinheitlicht, columnTask-Keys auf AUFGABE angeglichen,
Kill-Tonalitaet (closeSession/killSessionTip) ehrlich-destruktiv gemacht.
RepoImportModalViewModel.CreateButtonText und alle Installer-Check-Messages
(Git/GitIdentity/WriteAccess/ClaudeCli/ClaudeVersion/ClaudeAuth/
PermissionModeAuto/Ports) ueber Locale-Keys geroutet statt hartem Englisch,
mit dem bestehenden TrExtension.Localizer-Fallback-Muster.
2026-09-10 13:57:33 +02:00
mika kuns 73e038edd9 fix(claude-do): merge GitService stderr matches are English-only, same locale bug
ClaudeDo-Task: 66b7ad7b-ffc4-4ebd-8b21-54bba28e5b04
2026-09-10 13:56:14 +02:00
mika kuns 9d22c775f2 fix(claude-do): merge fix(ui,worker): Diff-Zähler locale-fest machen und toten Par
ClaudeDo-Task: 1085277d-ba7e-4e58-b06d-751e1b99b96f
2026-09-10 13:56:11 +02:00
mika kuns 407bc30554 feat(claude-do): merge feat(ui): Rückfrage bevor das X eine laufende ConPTY-Session
ClaudeDo-Task: 50c0d3ff-ad95-4612-8b16-344ac1c24f41
2026-09-10 13:54:26 +02:00
mika kuns 5ecf6f750a fix(data): force English git output for locale-fragile stderr matches
RevParseHeadAsync and CommitAsync's identity-error detection match
English-only substrings against git stderr. Under a non-English OS/git
locale these silently fail to match, falling through to a generic
error. Force LC_ALL=C and LANGUAGE=C on the git ProcessStartInfo so
git always emits English output regardless of ambient locale.
2026-09-10 13:54:14 +02:00
mika kuns eb62371430 feat(ui): confirm before closing a running ConPTY session
The tile's own X killed a live Claude session with no confirmation, unlike
the semantically similar Abandon-run action. Gate the close behind the same
ConfirmAsync mechanism, but only when Terminal.IsRunning -- a dead session
(StartError or exited-after-startup) still closes immediately since the X
there is pure cleanup.
2026-09-10 13:52:36 +02:00
mika kuns 395874d834 feat(claude-do): merge feat(ui): Worker-Menü "Shutdown" beendet nach Bestätigung Wo
ClaudeDo-Task: f97295c1e25a44db9db23df07a38062a
2026-09-10 13:52:30 +02:00
mika kuns 29ea18c427 fix(ui): make diff-stat parsing locale-safe and dedupe the two parsers
TaskRowViewModel had its own ParseDiffStat expecting "+12"/"-3" tokens that
`git diff --stat` never emits, so the row badge always showed 0/0. Meanwhile
DetailsIslandViewModel's parser matched only the English words
"insertion"/"deletion", so it broke on a localized git (e.g. German Windows:
"Einfügung(en)"/"Löschung(en)").

Row and detail badge now share one parser, anchored on git's untranslated
(+)/(-) suffix instead of the language-dependent word before it, so it reads
correctly in any locale. Also corrected the Worker CLAUDE.md's branch-name
claim (`claudedo/{taskId[:8]}`) to match the actual full-GUID branch name.
2026-09-10 13:50:35 +02:00
mika kuns 1bdf20272d feat(ui): worker-menu Shutdown ends worker + app after confirm
Adds a Shutdown entry after the Worker menu's Restart/Update Claude CLI
items. Confirms via IDialogService.ConfirmAsync, then kills this
installation's ClaudeDo.Worker process(es) (shared kill loop, extracted
from RestartWorkerService) and force-closes the window through the same
_forceClose path MainWindow already uses for the Mission-Control close
confirm, so no second confirmation dialog shows.
2026-09-10 13:48:50 +02:00
mika kuns 7eba0affbe feat(claude-do): merge feat(worker): normaler Parent mit Kindern — erst Parent ausf
ClaudeDo-Task: 646dd82f-4d3f-4aff-aef3-8e9f24b6e141
2026-09-10 13:36:24 +02:00
mika kuns d2648f965c feat(worker): chain a normal parent's children on WaitingForChildren
A standalone parent (PlanningPhase.None) with existing Idle/Queued children
now runs itself first, then sequences its children instead of flat-enqueuing
all of them at once: SubmitForChildrenAsync calls
PlanningChainCoordinator.SetupChainAsync(enqueue: true) right after the
Running -> WaitingForChildren transition, mirroring the planning path but
firing immediately instead of waiting for an explicit "Queue plan". Planning
parents are left untouched (they queue via QueuePlanAsync), and a child
already Running makes SetupChainAsync throw, which is now caught and logged
so it can't fail the parent's own transition.
2026-09-10 13:36:08 +02:00
mika kuns 897f287ee1 chore(claude-do): merge chore(ui): IWorkerClient.MergeProgressEvent-Forwarder entfer
ClaudeDo-Task: 2feaa8b5-0484-4b11-9519-6627f0bb95a3
2026-09-10 13:35:54 +02:00
mika kuns 1a5728749b fix(claude-do): merge fix(worker): max_turns-Fehlertext nicht fest deutsch — konsi
ClaudeDo-Task: 6549e3b5-cee3-4aa4-ba3a-6240bab952e6
2026-09-10 13:35:45 +02:00
mika kuns e628d97d29 fix(claude-do): merge fix(ui): fehlgeschlagenes Einfügen/Droppen im interaktiven T
ClaudeDo-Task: 81931eef-4d05-49d0-a476-7346cba7fbe5
2026-09-10 13:35:22 +02:00
mika kuns 0cec0a77ee fix(ui): report failed paste/drop in the interactive terminal
PasteAsync silently swallowed every exception and returned void, so a
package update renaming Iciclecreek's private write seam would break
Ctrl+V/drop with zero user-visible signal. PasteAsync/SendTextAsync now
return bool (mirroring SendLineAsync), and the view checks the result,
routing a failure through InteractiveTerminalViewModel.ErrorReported ->
ConPtyPaneViewModel.ErrorReported -> the existing footer error strip.
2026-09-10 13:35:16 +02:00
mika kuns acff815706 feat(claude-do): merge feat(ui): WorkConsole, DescriptionStepsCard und ROADBLOCK-La
ClaudeDo-Task: 6c91f950-f93b-44be-abb4-b12f65fb68ba
2026-09-10 13:35:14 +02:00
mika kuns 8e6f0d1605 feat(ui): localize WorkConsole, DescriptionStepsCard and the ROADBLOCK label
The review/merge cockpit was the last unlocalized surface in the detail
pane, mixing hard English with translated tooltips on the most-used screen.
Routes all literal strings through details.workConsole.*, details.stepsCard.*
and details.roadblockLabel, with en/de parity.
2026-09-10 13:35:07 +02:00
mika kuns bd393a0599 fix(claude-do): merge fix(ui): WorkerConnection-Modal meldet Fehlschläge sichtbar;
ClaudeDo-Task: 1db17881-7105-4f44-b035-0d13872bbfa0
2026-09-10 13:34:52 +02:00
mika kuns b4b6225432 fix(ui): surface worker connect/restart failures instead of no-opting
WorkerConnectionModalViewModel's StartWorker/RerunInstaller swallowed
missing-exe and Process.Start failures behind an empty catch and closed
the dialog anyway, looking like success. Now both show an inline error
and keep the dialog open; only a real launch closes it.

IslandsShellViewModel.RestartWorkerAsync never checked whether the
relaunched worker actually stayed up, and its status property was never
bound in any view. RestartWorkerService now fails if the new process
exits within a short survival window, and the outcome routes through
the footer error/success strip instead of the dead RestartWorkerStatus.

InstallArtifactLocator.Find() is now virtual and its two subclasses
unsealed so tests can fake exe resolution without touching the real
filesystem/registry.
2026-09-10 13:34:32 +02:00
mika kuns 46a0a27620 fix(claude-do): merge fix(ui): Settings-Politur — PowerUse-Zeitfenster validieren,
ClaudeDo-Task: f3f0a355-936c-4933-8236-d41c21fb73dc
2026-09-10 13:34:29 +02:00
mika kuns 423d39e9fb fix(claude-do): merge fix(worker): WeekReportService durch PermissionModeResolver
ClaudeDo-Task: b4198327-6045-451c-abf1-dbabad88e91a
2026-09-10 13:34:20 +02:00
mika kuns 951c2fe92a fix(ui): validate PowerUse window, explain effort and Prime Claude in Settings
Three usability-audit findings in Settings:
- An unparseable PowerUse window (e.g. "9:0") used to save silently as null
  (= round the clock), the opposite of what the typo implied. Validate() now
  rejects an unparseable start/end unless blank; the worker-side read-time
  tolerance is unchanged.
- "Effort" column header gets a tooltip explaining it's the model's reasoning
  effort (cost/depth tradeoff).
- The "Prime Claude" tab gets a tooltip explaining scheduled Claude runs
  (Ping / Fill My Day / custom prompt).

New locale keys added to en+de. Updated the PowerUse round-trip test that
asserted the old silent-normalize behavior.
2026-09-10 13:34:06 +02:00
mika kuns 6f1fbe5b93 fix(claude-do): merge fix(ui): Working-Dir beim Speichern der Listen-Settings als
ClaudeDo-Task: e8986885-add7-43b4-94bf-f12be5712848
2026-09-10 13:34:03 +02:00
mika kuns cfda3e9044 chore(ui): remove IWorkerClient.MergeProgressEvent forwarder
Group C of the operation-feedback batch is fully merged; the two consumers
(DetailsIslandViewModel.ApproveReviewAsync, MergeModalViewModel) now handle
the verifying phase from the generic OperationProgressEvent handler instead
of a separate forwarder event.
2026-09-10 13:33:57 +02:00
mika kuns 2a34d2453f fix(claude-do): merge fix(worker): start_list_handler ehrlich machen, wenn keine A
ClaudeDo-Task: 33e0f8f9-abac-4867-a873-ddc1c136a587
2026-09-10 13:33:40 +02:00
mika kuns 451c01848c fix(worker): start_list_handler ehrlich machen, wenn keine App verbunden ist
ConnectedClientTracker zaehlt verbundene UI-Clients (WorkerHub.OnConnectedAsync/
OnDisconnectedAsync). StartListHandler prueft ihn vor dem Broadcast und liefert
bei 0 verbundenen Clients Requested=false plus Reason statt unbedingtem true.
Tool-Beschreibung und external-mcp.md an das reale Verhalten angepasst.
2026-09-10 13:33:30 +02:00
mika kuns 0235b43bad fix(claude-do): merge fix(worker): Port-Konflikt beim Start laut melden statt stil
ClaudeDo-Task: 1eb4cfcd-a31e-4216-8c05-88cd662a211b
2026-09-10 13:33:28 +02:00
mika kuns 3348736e9d fix(ui): warn and block save when a list's working dir isn't a git repo
Reuses RepoScanner.IsRepo (now public) to validate WorkingDir whenever it
changes in ListSettingsModalViewModel — Browse or typing. A missing folder
or a non-repo folder shows an inline warning and gates Save via the same
CanExecute pattern the load-failure gate already uses; an empty WorkingDir
is untouched. Previously this only surfaced as a Failed task deep in
WorktreeManager on first run.

Also localizes the two hardcoded EN strings in the same file (delete
confirm, planning-sessions block) into vm.listSettings.* keys.
2026-09-10 13:32:52 +02:00
mika kuns 752cf0926b feat(claude-do): merge feat(ui): depends_on-Blockierung sichtbar machen (Status-Chi
ClaudeDo-Task: 701de3ab-c326-4152-9488-8ac21fa21ffc
2026-09-10 13:32:48 +02:00
mika kuns 4cf601e423 feat(ui): surface depends_on blocking in status chip, chain label, and detail pane
Usability-Audit 2026-09-10 found three gaps versus the dependency-chain-display
spec: a Queued task blocked on an unmet task_dependencies edge rendered the same
chip as one about to run, a cross-list predecessor silently dropped the
"after #N" chip, and the detail pane showed nothing about a wait at all.

- TaskRowViewModel gains IsBlockedByDependency/DependencyWaitLabel (extrinsic,
  assigned by TasksIslandViewModel like the existing chain fields); IsQueued/
  IsWaiting/StatusChipClass/StatusLabel/StatusChipTooltip now account for it
  alongside the existing BlockedByTaskId planning-chain link.
- TasksIslandViewModel.ApplyDependencyWaitState computes the blocked flag and
  wait label per row; a new RefreshExternalDependencyCacheAsync backfills
  Number/Title/Status for predecessors not loaded into the current list's
  Items (reusing the already-open DbContext, no new hub method), so
  BuildAfterLabel/blocking both resolve a listenfremder Vorgänger correctly.
- New "waiting" chip style (IslandStyles.axaml) and locale keys (en+de);
  TaskHeaderBar shows a "Waiting on #N" line bound straight to the same row.

Tests: TasksIslandChainTests (blocked/unblocked chip, cross-list predecessor),
DetailsIslandDependencyWaitTests (new), TaskRowViewModelNotificationsTests
updated for the new notification set.
2026-09-10 13:32:36 +02:00
mika kuns c1b133ff90 fix(claude-do): merge fix(ui): gestorbene ConPTY-Session auf der Kachel sichtbar m
ClaudeDo-Task: 4f97df3c-be1e-4513-9b86-742f243649f0
2026-09-10 13:32:34 +02:00
mika kuns 256bea5ffa fix(ui): show a banner when a ConPTY session dies after startup
InteractiveTerminalViewModel.HasExitedAfterStartup now covers every
post-startup death that ExitedAfterStartup alone missed (crash, clean
exit, quiet exit inside the grace window) -- previously the tile just
stayed looking alive/thinking with zero signal. ConPtyPaneView binds a
new banner to it (message + Retry + Close); CanRetry simplifies to
"any HasExited state" since the same descriptor factory can relaunch
either kind of death. HandlerRestartCoordinator's worker-side
auto-restart path is untouched.
2026-09-10 13:32:19 +02:00
mika kuns 2b0562b2ed fix(claude-do): merge fix(localization): doppelten Top-Level-Block "checks" in en.
ClaudeDo-Task: ba3b1423-fd00-4771-8b16-735cb441ebce
2026-09-10 13:32:16 +02:00
mika kuns 741f66fe14 fix(localization): merge duplicate top-level "checks" block in en/de.json
en.json and de.json each defined the checks object twice; JSON silently
kept the second block's values, so the first block's friendlier titles/hints
(e.g. "Permission Mode" over the raw "--permission-mode auto support" flag)
were dead text. Merged into one block per key, keeping the friendlier title
and the more actionable hint.

Added a Utf8JsonReader-based duplicate-top-level-key test, since
JsonDocument accepts duplicates without error and the existing parity test
can't see this failure mode either.
2026-09-10 13:31:01 +02:00
mika kuns 2d67ca3171 feat(claude-do): merge feat(ui): Worker-Menü "Update Claude CLI" führt `claude upda
ClaudeDo-Task: 7a02b6957f094dceb0f923c300c4675f
2026-09-10 13:30:58 +02:00
mika kuns 0fcfe7c524 fix(worker): translate max_turns failure text to English
TaskRunner.BuildFailureMarkdown returned a hardcoded German string for
TerminalReason == "max_turns", the only worker-generated string not in
English. Surfaced as the outcome/roadblock card for one of the most likely
first failures, looking broken to non-German users.
2026-09-10 13:22:30 +02:00
mika kuns 5be972564a feat(ui): mission-control pane title shows usage limit reset time
When a list-handler handoff is deferred waiting for the 5h usage limit to
reset, the Mission Control pane title is augmented with ⏳ waiting for
limit reset (HH:mm) so the wait is visible at a glance. Extends
HubBroadcaster.HandoffRequested with an optional deferredUntil DateTime
parameter, wired through SignalR to MissionControlViewModel, which formats
it into the pane title via Loc.T(missionControl.waitingForLimitReset).
2026-09-10 13:18:09 +02:00
mika kuns 1c76852957 fix(worker): route WeekReportService permission-mode through PermissionModeResolver
Closes the last unrouted --permission-mode call site: haiku default models were
hardcoding raw "auto", which the CLI silently downgrades to interactive "default"
and hangs the report generation. Also drops the author's personal C:\Private
default exclude in favor of an empty default (config stays via ReportExcludedPaths).
2026-09-10 13:13:16 +02:00
mika kuns c9d4823f06 fix(worker): report startup port conflicts instead of crashing silently
Mutex was Local\ (per Windows session), so a second RDP/fast-user-switching
session bypassed the single-instance guard. A Kestrel bind failure on top of
that had no try/catch, so a second worker (or any process on 47821/47822)
died as an unhandled crash with nothing in the log to explain why the UI
never connects. Mutex is now Global\, bind failures and any last-resort
unhandled exception log a Fatal line and exit(1) instead of crashing raw.
2026-09-10 13:12:25 +02:00
mika kuns dbee4a0de9 feat(ui): Worker-Menü "Update Claude CLI" führt claude update aus
Neuer Eintrag im Worker-Menü der Titelleiste, der die Claude CLI über
das konfigurierte claude_bin (ExecutableResolver, inkl. Shim-Handling)
mit `claude update` aktualisiert. Ausgabe/Fehler landen im Footer-Strip
(FlashFooterSuccess/FlashFooterError), Menüpunkt ist während des Laufs
deaktiviert. 5-Minuten-Timeout tötet den Prozessbaum.
2026-09-10 13:10:30 +02:00
mika kuns 1177463a82 feat(ui): triage model picker (opus/fable) in the merge helper start modal 2026-09-10 09:42:55 +02:00
mika kuns 5477ae5302 docs: record list folders in the Data and Ui CLAUDE.md 2026-09-10 08:33:31 +02:00
mika kuns a0a85c5c24 test: cover list folders in the data and sidebar layers
Data: folder delete keeps its lists (ON DELETE SET NULL), rename patches
name/icon without resetting sort order or collapse state.
Sidebar: grouping on load, folder badge as the sum of its children, both
move routes, and the persisted collapse state.

Drops the LoadAsync/AddRowAsync normalization of a dangling folder_id:
FK enforcement is on for every connection, so the row it guarded against
cannot exist, and ContainerOf already falls back to the top level.
2026-09-10 08:32:52 +02:00
mika kuns 25c9a619dc feat(ui): collapsible folders for the list sidebar
Folders render above the top-level lists, each with its own collapsible
children; tapping a folder header folds it instead of selecting it.
Lists move in by dragging onto a folder header (or onto a row inside it)
and out by dropping between top-level rows or via the context menu.

Creating a folder is a toggle in the New-list modal, not a third sidebar
button: it reduces the modal to name + icon and writes a folder row
instead of the list row the create flow pre-created.
2026-09-10 08:29:56 +02:00
mika kuns 13e860f06b feat(data): list_folders table plus lists.folder_id
Sidebar folders get their own table rather than a flag on lists, so the
places that enumerate lists (MCP tools, move-to-list, queue picker, repo
import) need no folder filtering. ON DELETE SET NULL keeps a folder's
lists alive when the folder goes.
2026-09-10 08:29:56 +02:00
ClaudeDo CI ec1e60be1d docs(changelog): update for v2.13.6 2026-09-07 06:18:03 +00:00
mika kuns f231f08a27 feat(ui): Home-Ordner als Sitzungsziel in Mission Control
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 41s
Erster Eintrag im Sitzungsmenue, fuer schnelle allgemeine Fragen, die zu
keinem Projekt gehoeren.
2026-09-07 08:17:37 +02:00
mika kuns 23491d3b49 feat(ui): Einfuegen und Datei-Drop im interaktiven Terminal
Ctrl+V wurde von TerminalControl als rohes Control-Byte an das Kind
weitergegeben und tat nichts; jetzt wird es im Tunnel abgefangen und geht
ueber TerminalView.PasteAsync, damit Bracketed Paste greift und ein
mehrzeiliger Paste nicht auf der ersten Zeile abschickt. Gedroppte Dateien
werden als Pfade in den Composer getippt, ohne Enter.
2026-09-07 08:17:37 +02:00
mika kuns 0db9fadee9 feat(ui): Cheat-Sheet-Fenster im Hilfe-Menue
Sechs Markdown-Sheets (git, Conventional Commits, Claude CLI, Claude-Config,
dotnet CLI, PowerShell) als EmbeddedResource, gerendert im modelosen, per
Topmost anpinnbaren CheatSheetsWindow. MarkdownView bekommt dafuer das Opt-in
CommandRows: `befehl` - beschreibung wird zu Mono-Chip + Copy-Button, jede
##-Sektion landet in einem Expander. Enthaelt ausserdem: Mission Control
wird beim Oeffnen aus dem Minimiert-Zustand geholt, nicht nur aktiviert.
2026-09-07 08:17:37 +02:00
mika kuns 7c7c33d677 feat(ui): PowerUse-Einstellungen nach Settings -> Ausfuehrung verschieben
Schalter, Zeitfenster und Handoff-Prozent lagen im Usage-Monitor und mussten
in SettingsModalViewModel nur load->save durchgeschleift werden, damit ein
Settings-Speichern sie nicht zurueckwirft. Jetzt gehoeren sie dem General-Tab,
der Monitor ist wieder nur Anzeige + Threshold-Drag, und "Jetzt aktualisieren"
sitzt als Icon in der Titelleiste (neuer ModalShell.HeaderActions-Slot).
2026-09-07 08:17:37 +02:00
ClaudeDo CI c7392e085f docs(changelog): update for v2.13.5 2026-09-03 08:20:49 +00:00
mika kuns c16c7b97ef refactor(ui): "Nach Updates suchen" ins Hilfe-Menue verschieben
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 41s
2026-09-03 10:20:35 +02:00
mika kuns cccfe2eca1 fix(ui): Version im About-Dialog aus der FileVersion lesen
MinVer setzt die AssemblyVersion auf {Major}.0.0.0 (Binding-Kompatibilitaet),
darum zeigte der Dialog immer "2.0.0". Die echte Major.Minor.Patch steckt in
der FileVersion.
2026-09-03 10:20:35 +02:00
ClaudeDo CI a8ec550740 docs(changelog): update for v2.13.4 2026-09-03 08:18:22 +00:00
mika kuns 40d2537486 Merge remote-tracking branch 'origin/main'
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 42s
2026-09-03 10:17:58 +02:00
mika kuns f70503d038 fix(worker): interaktive Session ohne Arbeitsverzeichnis im User-Verzeichnis starten
Eine manuelle Liste (Erinnerungen) hat kein WorkingDir. BuildForTaskAsync
warf dafuer bisher "no working directory configured", die interaktive
Session liess sich also nicht oeffnen. Jetzt faellt der Pfad auf das
User-Verzeichnis zurueck; Session-Skills werden dort bewusst nicht
geseedet, weil das Ziel das echte ~/.claude/skills waere.
2026-09-03 10:17:06 +02:00
ClaudeDo CI 9360cd2fe6 docs(changelog): update for v2.13.3 2026-09-03 05:48:10 +00:00
mika kuns ee502883f8 chore(claudedo): update CHANGELOG for v2.13.3
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 40s
2026-09-03 07:47:43 +02:00
mika kuns e64d570554 fix(worker): raise SignalR MaximumParallelInvocationsPerClient
Default of 1 serializes every hub call on a connection - a slow one
(GetTicketProjects hitting an unreachable ticket system, 10s timeout)
blocked unrelated calls like UpdateList/UpdateListConfig behind it for
the same duration, making list-settings save appear to hang.
2026-09-03 07:47:26 +02:00
ClaudeDo CI fc25ee45d1 docs(changelog): update for v2.13.2 2026-09-03 05:23:15 +00:00
mika kuns 51ee505057 fix(ui): strip CLAUDE_CODE_CHILD_SESSION/CLAUDECODE before every ConPTY launch
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 42s
ClaudeDo.App inherits its own process environment from whatever started it
(e.g. a dev/test launch from inside a claude session). Every embedded
claude.exe it spawns into a Mission Control tile then inherited those
markers too, so the child session mistook itself for a subagent: transcript
saving disabled and a degraded startup banner (white instead of orange,
missing box rules). PtyTerminalSession.StartAsync is the one choke point
all ConPTY launches already share for env handling, so clearing both vars
there covers task, ad-hoc, planning, and list-handler sessions regardless
of how ClaudeDo.App itself was started.
2026-09-03 07:22:59 +02:00
mika kuns 06bc3d2105 feat(claude-do): merge Handler-Run per Knopf abbrechen (Session killen + offene Run
ClaudeDo-Task: 3c387f8e-096f-42c6-a2c3-91c69fe3915c
2026-09-02 22:39:18 +02:00
mika kuns bd5aa265d1 feat(mission-control): Handler-Run per Knopf abbrechen
Ein "Abandon run"-Knopf an Handler-Panes fragt HandlerRoundState.Get(handlerTaskId)
nach den noch offenen (Queued/Running) Tasks des Laufs, bestaetigt mit Anzahl,
cancelt jeden ueber den bestehenden CancelTaskAsync-Pfad und schliesst danach die
Session -- ein Schritt statt Tile schliessen + Queue separat abbrechen. Ohne
offene Tasks (oder ohne Round-Eintrag) schliesst die Aktion direkt, ohne Dialog.
Nur an den beiden Handler-Panes sichtbar (IsListHandler), normale Task-/Ad-hoc-
Panes bleiben unveraendert.
2026-09-02 22:39:12 +02:00
mika kuns b298594a2c chore(claude-do): merge fix(ui): Listen-Settings-Modal verschluckt Ladefehler nach d
ClaudeDo-Task: 9256fcfe-6cad-4350-a979-0c49cd5b9799
2026-09-02 22:38:49 +02:00
mika kuns 62855ac575 Merge branch 'claudedo/c209d54cef5642278673d41e8b98fc43' 2026-09-02 22:38:42 +02:00
mika kuns 18b4dcd56f Merge branch 'claudedo/a9603b34c1e74074b904cd5028de0a1d' 2026-09-02 22:38:12 +02:00
mika kuns b02e0f80f0 chore(claude-do): merge feat(ui): Empty-State fuer den Session-Skills-Tab
ClaudeDo-Task: 5d889e35-660e-4376-a5bd-6df4e2d69e16
2026-09-02 22:36:49 +02:00
mika kuns df3e7ba0c7 fix(ui): Dequeue-X auf blockierten Queue-Zeilen anzeigen und Kette konsistent halten
CanRemoveFromQueue schloss IsWaiting (queued+blocked) faelschlich aus, sodass ein
Kettenglied in der Mitte nicht entfernbar war. Zusaetzlich unblockt
TaskStateService.DequeueToIdleAsync jetzt den Nachfolger eines dequeueten
Kettenglieds, sonst bliebe er dauerhaft auf einen nicht mehr laufenden
Vorgaenger blockiert.
2026-09-02 22:34:50 +02:00
mika kuns a1b20d9f27 fix(ui): report and gate ListSettingsModal load failures instead of swallowing them
BeginLoad's fire-and-forget LoadAsync now catches instead of leaving an
unobserved task exception, reports it via the new ErrorReported event
(wired into FlashFooterError like the other modal VMs), and keeps Save
locked so the failure can't clobber list_config with blank fields.
LoadTicketProjectsAsync's empty catch now reports too; behavior for the
dropdown itself is unchanged.
2026-09-02 22:33:48 +02:00
mika kuns e0c6b670cb fix(ui): move ScrollViewer padding to inner content in modal bodies
Avalonia 12 excludes ScrollViewer.Padding from the scroll Extent, clipping
the last line of these five modal bodies unreachable at max scroll.
2026-09-02 22:30:52 +02:00
mika kuns 2bd48ea1c7 docs(open): remove stale session-skills empty-state entry
Feature was already implemented in fe8186514 (locale key, IsEmpty
property, TextBlock in SettingsModalView.axaml).
2026-09-02 22:30:37 +02:00
mika kuns 77c70d74cb perf(claude-do): merge perf: Listen-Settings-Modal öffnet erst nach 7 seriellen Rou
ClaudeDo-Task: 1a07fa19b27347c1b65591ca6c0c0980
2026-09-02 22:19:55 +02:00
mika kuns f24654dfb8 perf(ui): show list settings modal before its seven-round-trip load finishes
OpenListSettingsAsync/CreateListAsync used to await LoadAsync (name/agent
config/list_config x2/ticket settings/ticket projects, all serial) before
showing the window -- an unreachable ticket system alone could add a 10s
HTTP timeout to every open. LoadAsync now runs via BeginLoad (fire-and-forget,
mirrors UsageMonitorModalViewModel), the modal shows immediately and fields
fill in behind it.

GetListConfigAsync no longer runs twice per open -- AgentConfigEditorViewModel
exposes the row it already fetched (LastLoadedListConfig) instead of the
caller re-fetching it. Ticket-project loading is split into its own
fire-and-forget step so a slow/unreachable ticket system can't block the
rest of the fields, and its tri-state Save logic now also treats "never
finished loading" like "not available" -- leaving the stored
TicketProjectId link untouched instead of overwriting it with 0. Save/Delete
are gated (CanExecute) on the core load being done, so a save can't land
before the fields it writes are populated.

Not measured with OperationTiming against a live worker in this session (no
running app instance available) -- verified instead via 4 new unit tests
that assert GetListConfigAsync fires exactly once and that a hung/failing
ticket-projects call neither blocks LoadAsync's other fields nor corrupts
the saved TicketProjectId. Before/after OperationTiming numbers and a visual
pass are still open follow-ups.
2026-09-02 22:18:54 +02:00
mika kuns b05b6fbaec fix(ui): Mission Control oeffnen, wenn ein Handler-Run per MCP startet oder ein Handoff eintrifft 2026-09-02 22:05:34 +02:00
ClaudeDo CI eb3aee41a4 docs(changelog): update for v2.13.1 2026-09-02 20:04:37 +00:00
ClaudeDo CI 2b8733227d docs(changelog): update for v2.13.0 2026-09-02 19:58:58 +00:00
mika kuns d258ab4164 test(worker): start_list_handler ins erwartete MCP-Tool-Inventar aufnehmen
Changelog / changelog (push) Successful in 2s
Release / release (push) Successful in 42s
2026-09-02 21:57:16 +02:00
mika kuns 574edf6c1a Merge branch 'claudedo/f2f0c6ee1e974c09bc8a8261978cba34'
feat(worker): Handler-Session vor dem Usage-Limit sauber uebergeben (#275); Konflikte mit start_list_handler aufgeloest, WorkerLog-Aufruf im Fire-and-forget-Pfad wieder abgesichert
2026-09-02 21:50:26 +02:00
mika kuns fe8186514f feat(worker): Handler-Session vor dem Usage-Limit warnen und Handoff bis zum Reset zurueckhalten
Eine PowerUse-Handler-Session soll am 5h-Limit nicht mehr mitten im Merge
sterben, sondern vorher selbst sauber uebergeben:

- HandlerUsageGuard (neu) warnt die laufende Session genau einmal pro Runde,
  sobald FiveHour.Utilization den Schwellwert PowerUseHandoffPct erreicht.
  Die Warnung wird per HandlerUsageWarning-Broadcast an Mission Control
  geschickt und dort in das Pane der Handler-TaskId getippt.
- handoff_list_handler broadcastet den Handoff dann nicht sofort, sondern parkt
  ihn bis ResetsAt + 60 s; das Ergebnis traegt deferredUntil. Ohne ResetsAt
  wird wie bisher sofort gesendet.
- Der geparkte Handoff liegt im HandlerRoundState (handler-rounds.json) und
  wird vom Guard-Tick ausgeloest, also auch nach einem Worker-Neustart wieder
  eingeplant bzw. sofort gesendet, wenn er ueberfaellig ist.
- Review-Befund aus #272: HandlerRestartCoordinator wartet nicht mehr selbst
  bis ResetsAt, sondern parkt den Neustart im selben Slot. AutoRestartUsed wird
  erst gesetzt, wenn der Broadcast wirklich rausging - ein Worker-Absturz
  waehrend der Wartezeit laesst die Runde nicht mehr dauerhaft tot zurueck.
  Zurueckgehaltener Handoff und Auto-Neustart teilen sich einen Slot, also
  oeffnet nie beides dieselbe Runde. UsageLimitProofPct entfaellt zugunsten von
  PowerUseHandoffPct.
- PtyTerminalSession.SendLineAsync schreibt in die laufende Session. Gegen eine
  echte claude-Session verifiziert: Text und Enter muessen zwei getrennte
  Writes sein, die Enter-Bytes kommen von Terminal.GenerateKeyInput.
2026-09-02 21:35:29 +02:00
mika kuns c2166a0d4f feat(worker): start_list_handler MCP-Tool startet einen List-Handler-Run wie der Button 2026-09-02 21:05:55 +02:00
mika kuns c3a418f077 Merge branch 'claudedo/b346a534f0fc4635bd7614a00ab2a20e' 2026-09-02 20:55:43 +02:00
mika kuns 2ec3af96d5 chore(claude-do): merge PowerUse-Zeitfenster + Handoff-Schwellwert als Settings (Usa
ClaudeDo-Task: aa446890-82f0-49b9-aef9-13a788d9c514
2026-09-02 19:27:01 +02:00
mika kuns 016b9203f8 feat(worker): Handler-Session nach Usage-Limit-Reset automatisch neu starten
Eine List-Handler-ConPTY-Session, die mitten in einer Phase stirbt, war bisher
ein stilles totes Tile: InteractiveTerminalViewModel.OnSessionProcessExited
setzte nur bei einem Tod innerhalb des 5s-Grace-Fensters ein sichtbares Banner.

Neu:
- HandlerRoundState (JSON unter ~/.claudeDo/handler-rounds.json, keine
  EF-Migration): Phase + Task-Set der laufenden Runde, geschrieben von
  BuildForMergeHelperAsync ("triage") und BuildForMergeHelperHandoffAsync.
  Ohne das existieren beide nur im transienten HandoffRequested-Broadcast.
- HandlerRestartCoordinator: entscheidet den Neustart, fail-closed. Beleg fuer
  "war das Usage-Limit" ist FiveHour.Utilization >= 95 zum Todeszeitpunkt --
  eine ConPTY-Session hat kein stdout, TaskRunner.IsUsageLimitMessage hat hier
  nichts zu lesen. Nur bei PowerUse (beide Keys), max ein Neustart pro Runde,
  Warten bis FiveHour.ResetsAt, dann HandoffRequested erneut broadcasten.
  Verbraucht kein Handoff-Budget (HandoffRoundTracker.Register wird nicht
  angefasst). WorkerLog-Zeilen beim Tod und beim Neustart.
- Ui meldet den Exit-Code (nur dort bekannt) via
  WorkerHub.ReportInteractiveSessionExited, ohne selbst zu filtern.

Sibling zu UsageLimitAutoContinueCoordinator, kein Umbau davon: der sieht nur
headless Runs.
2026-09-02 19:17:51 +02:00
mika kuns 69898be5c9 feat(usage): PowerUse-Zeitfenster + Handoff-Schwellwert als Settings
Drei neue app_settings-Spalten (Migration AddPowerUseWindow):
power_use_window_start/end (HH:mm, lokal, nullable) und
power_use_handoff_pct (NOT NULL DEFAULT 95, geclampt 50-99).

Die Fensterlogik liegt als reine Funktion in PowerUseWindow
(Contains/Normalize/IsOpen): beide Grenzen leer oder nur eine gesetzt =
rund um die Uhr, Ende vor Start wrappt ueber Mitternacht, Grenzen
halboffen. Geprueft wird in ComposeHandlerPromptAsync, also pro Phase
zum Zeitpunkt des Prompt-Baus - die Uhr ist per Func<DateTime>
injizierbar. Bedienung im Usage-Monitor neben dem PowerUse-Toggle
(Commit auf Blur/Enter, ein Write fuer alle vier Felder); in
Settings->General unsichtbar, aber load->save durchgereicht.

Nebenbefund, gleicher Pfad: AppSettingsRepository.UpdateAsync kopiert
Feld fuer Feld und hat ModelPresets, PowerUseEnabled und
ListHandlerMaxHandoffs nie geschrieben - jeder Save dieser drei war
still wirkungslos. Mit ergaenzt und per Round-trip-Test gepinnt.
2026-09-02 19:09:47 +02:00
mika kuns 20b3b4ce2b docs(claude-do): Handler-Prompt praezisieren (Wave-Plan, Task-Split, review_task-Aufruf) 2026-09-02 18:43:15 +02:00
Mika Kuns 42356241e3 feat(claude-do): PowerUse fuer List-Handler-Runs + konfigurierbares Handoff-Budget 2026-09-02 15:56:10 +02:00
Mika Kuns 675b36e923 Merge branch 'main' into feat/poweruse-list-handler
Kollision mit #264 (Handoff-Budget im Brief sichtbar machen): beide Seiten haben
HandoffRoundTracker, HandoffMcpTools und InteractiveLaunchSpecService angefasst.

Der Konflikt war inhaltlich, nicht nur textuell. #264 liest den Cap an zwei
Stellen aus der Konstante (GetStatus und das roundInfo-Rendering), waehrend
dieser Branch ihn zum Parameter aus app_settings gemacht hat. Ein reiner
Text-Merge haette kompiliert und trotzdem gelogen: Register haette den
konfigurierten Cap durchgesetzt, waehrend der Brief dem Modell weiter "2 Runden
uebrig" gemeldet haette, obwohl es 39 hat.

Aufgeloest so, dass der Cap eine einzige Quelle hat:
- GetStatus(taskId, maxHandoffs) nimmt den Cap als Parameter, mit demselben
  Clamp wie Register. Die Rechnerei liegt damit an einer Stelle.
- GetHandoffStatus liest das Setting und gibt es weiter.
- Das roundInfo-Rendering benutzt GetStatus statt eigener Arithmetik.

Zwei Tests nageln genau diesen Drift fest, damit er beim naechsten Merge nicht
unbemerkt zurueckkommt: einer auf GetHandoffStatus, einer auf den Brief.

MaxHandoffs -> DefaultMaxHandoffs in #264s vier Test-Referenzen; die laufen alle
mit Default-Settings, die Semantik bleibt.
2026-09-02 15:48:43 +02:00
Mika Kuns 416997d7e0 docs: Test-Scoping fuer Implementer-Tasks, PowerUse in den Explore-Notes
Die Build-Sektion hat bisher ein Rezept mit allen sechs Testprojekten vorgegeben;
Implementer haben daraus "volle Suite vor fertig" gelesen. Bei bis zu 5
parallelen Tasks kostet das Stunden - und kann das nicht finden, wofuer es
teuer ist: jeder Task arbeitet in seinem eigenen Worktree mit nur seiner eigenen
Aenderung, eine Kollision zwischen zwei parallelen Tasks ist fuer beide
unsichtbar. Kollisionen zeigen sich nach dem Merge, und genau dort laeuft
verify_merges den Verify-Command einmal pro Batch.

Neue Regel: Solution-Build immer (42s, faengt projektuebergreifende
Compile-Brueche), Tests nur --no-build --filter auf das Beruehrte, und melden
welcher Filter benutzt wurde. Die Messwerte stehen dabei, damit die Regel nicht
als Geschmacksfrage gelesen wird.

Messung 2026-09-02, sequenziell nach einem Build: Worker.Tests 507s, Ui 33s,
Data 8s, Rest je 3s, Build 42s. Parallel ueber die Solution ist es schlechter,
nicht besser (Worker 801s, Ui 82s) - diese Tests legen echte Git-Worktrees an
und spawnen Prozesse, sie drangeln sich statt sich zu teilen.

Ausserdem korrigiert: die Zeile "dotnet build ClaudeDo.slnx requires .NET 9"
war veraltet.
2026-09-02 15:44:13 +02:00
Mika Kuns 193752540b feat(ui): PowerUse-Toggle im Usage-Monitor, Opt-in pro Liste
Der Toggle sitzt unter den Gauges im Usage-Monitor, weil "das restliche Fenster
ausnutzen" eine Entscheidung ist, die man beim Blick auf das restliche Fenster
trifft. Er speichert sofort per Read-modify-write, gleiche Disziplin wie
SaveStagesAsync, damit er keine Felder ueberschreibt, die er nicht besitzt.

Zwei Stellen, die das Opt-in still ausgeschaltet haetten:
- WorkerHub.UpdateListConfig loescht die Config-Zeile, wenn alle Felder leer
  sind. PowerUseAllowed muss in diese Leer-Pruefung, sonst verschwindet das
  Opt-in sofort wieder, wenn es das einzige gesetzte Feld ist.
- SettingsModalViewModel baut das AppSettingsDto positional neu auf. Ohne
  Round-trip haette jedes Speichern der Settings PowerUseEnabled auf den
  Record-Default false zurueckgesetzt. Gleiches Muster wie _throttleStages.

Das Handoff-Budget ist als Spinner in Settings -> General editierbar, weil es
auch fuer normale (interaktive) Handler-Runs gilt und nicht an PowerUse haengt.
Das Addendum selbst ist ueber Settings -> Files anpassbar.
2026-09-02 15:44:13 +02:00
Mika Kuns 453d61ca58 feat(worker): PowerUse-Addendum an die Handler-Prompts, Cap aus den Settings
HandoffRoundTracker.Register nimmt den Cap jetzt als Parameter statt aus einer
Konstante; HandoffMcpTools liest ihn pro Aufruf aus app_settings. Werte unter 1
werden auf 1 geklemmt (in WorkerHub UND in Register): der Cap ist das Einzige,
was eine entlaufene Handoff-Kette begrenzt, "0 = unbegrenzt" darf kein Aufrufer
waehlen koennen.

InteractiveLaunchSpecService.ComposeHandlerPromptAsync haengt das PowerUse-
Addendum an den Phasen-Prompt, aber nur wenn globaler Schalter und Listen-Opt-in
beide gesetzt sind. Bewusst ein Addendum statt einer zweiten Prompt-Garnitur:
die gemeinsamen 95% bleiben an einer Stelle, und ein Run ohne PowerUse bekommt
einen byte-identischen Prompt wie vorher. Das Addendum ist als Override der
interaktiven Regeln formuliert (nie fragen, entscheiden und protokollieren;
Konflikte loesen oder abbrechen, nie im Tree liegen lassen; Arbeit ernten, wenn
eine Runde sonst leer laeuft).

Drei Tests pinnen alle vier Ecken des UND, weil die Sicherheitsaussage des
Features genau daran haengt.
2026-09-02 15:43:48 +02:00
Mika Kuns 3cf9628f46 feat(data): PowerUse-Flags und konfigurierbares Handoff-Budget
app_settings bekommt power_use_enabled (Master-Schalter, wird im Usage-Monitor
gesetzt) und list_handler_max_handoffs (Default 4 = zwei Merge-Runden).
list_config bekommt power_use_allowed als Opt-in pro Liste.

Ein Handler-Run laeuft nur unbeaufsichtigt, wenn BEIDE Schalter gesetzt sind.
Damit kann der globale Schalter ueber Nacht an bleiben, ohne dass der Handler
einer nicht zugestimmten Liste sein Verhalten aendert.

UpdateListConfigDto.PowerUseAllowed ist bewusst bool? (tri-state, null =
gespeicherten Wert behalten), analog zu TicketProjectId: SetConfigAsync kopiert
verbatim, ein non-nullable bool haette jedem fremden Writer erlaubt, das Opt-in
per Auslassung zu widerrufen. Der Test dazu nagelt genau das fest.

Migration ist drei reine ALTER TABLE ADD COLUMN, kein Table-Rebuild.
2026-09-02 15:43:32 +02:00
Mika Kuns b00bdf28ae Merge branch 'claudedo/0a185f8e823d42fc9a5b97c5eb91c616' 2026-09-02 15:30:04 +02:00
Mika Kuns 91a37984c6 feat(claude-do): merge Handoff-Budget im Brief sichtbar machen und pro Task mitzähl
ClaudeDo-Task: bdcd601a-2fed-44d8-9164-6d2d008e7f31
2026-09-02 15:28:59 +02:00
Mika Kuns 7ee7e33654 feat(claude-do): merge Mehrfach-Abhängigkeiten: dependsOnTaskId kann nur einen Vorg
ClaudeDo-Task: cda94cd3-71c3-447f-8971-9d57be939cad
2026-09-02 15:18:54 +02:00
Mika Kuns c25732cff8 feat(claude-do): depends_on als Menge statt Einzelfeld
Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte
tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies
ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das
alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder
pickbar). BlockedByTaskId bleibt unberuehrt.

- Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id
  verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird
- QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten
  wird blockiert, alle Kanten muessen done sein, eine Kante auf einen
  verschwundenen Task blockiert weiter
- SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine
  BFS ueber den Graphen, faengt also auch A->B->C->A
- blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von
  ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt)
- add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt
  als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig
- UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label,
  die lineare Kette rendert unveraendert
2026-09-02 15:08:05 +02:00
Mika Kuns 2330b426c6 feat(claude-do): Expose HandoffRoundTracker budget via an MCP tool (e.g. get_
The handoff-budget task (round/remaining/final now surfaced in the handoff.md brief via HandoffRoundTracker.GetStatus, wired through InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync) only reaches the session at the start of a handoff round. There is no way to query the current round/remaining budget for a handler task mid-session or from outside (e.g. the UI, or a script). Consider add

ClaudeDo-Task: 0a185f8e-823d-42fc-9a5b-97c5eb91c616
2026-09-02 14:43:44 +02:00
Mika Kuns 81a5fc5d33 feat(worker): surface the handoff round budget in the merge-helper handoff brief
A handler session only learned the chain was capped when handoff_list_handler
threw at the end of its work, stranding open tasks. The round count already
lived in HandoffRoundTracker the whole time -- it just never reached the
brief. InteractiveLaunchSpecService now reads HandoffRoundTracker.GetState
(read-only, doesn't advance the round) and renders round N of MaxHandoffs
plus the remaining count into the handoff brief, replacing the old one-line
finalNote. The rejection error also names the next action (start a new
handler run) instead of just failing.
2026-09-02 14:26:56 +02:00
Mika Kuns 84d955dfc7 docs(claude-do): plan for multi-dependency depends_on relation table 2026-09-02 14:15:39 +02:00
Mika Kuns 20685827fb feat(claude-do): merge Merge-Session soll die Queue selbst beobachten statt fertige
ClaudeDo-Task: 1788adc4-c671-4942-ab78-1ef1812da494
2026-09-02 13:59:27 +02:00
Mika Kuns 4ee1d05f06 fix(worker): carry ExpectsNoDiff into the relocated merge MCP tools
Conflict resolution for the #266 merge: #247 had moved PreviewMerge/PreviewMergeSet
and MergePreviewSetEntryDto out of ExternalMcpService into MergeMcpTools, so #266's
in-place edits to those had to be re-applied at the new location, together with the
PreviewMergeSet_ReportsExpectsNoDiffPerTask test.
2026-09-02 13:59:22 +02:00
Mika Kuns 6c1ea1ae25 feat(claude-do): Merge-Session soll die Queue selbst beobachten statt fertige
## WO — verifiziert am 2026-09-02, nicht neu suchen

- Merge-Phasen-Prompt: `src/ClaudeDo.Data/PromptFiles.cs`, Konstante `MergeHelperMergeDefault` (ab Z. 495). Der neue Warte-Abschnitt gehört zwischen "## Verify once — after the last merge" (Z. 526) und "## Handoff / end" (Z. 542) — also nach dem Verify, vor der Handoff-Entscheidung.
- Brief-Template: `MergeHelperInitialDefault` (Z. 555) und `Mer

ClaudeDo-Task: 1788adc4-c671-4942-ab78-1ef1812da494
2026-09-02 13:49:58 +02:00
Mika Kuns 7e847638cf Merge branch 'claudedo/9a02814991ef42a5a9a063d5ea579d62' 2026-09-02 13:49:56 +02:00
Mika Kuns aadc926ffe refactor(claude-do): merge refactor(worker): move the merge and worktree MCP tools out
ClaudeDo-Task: 1862e160-ffde-4a50-aa03-bb7251f56088
2026-09-02 13:43:52 +02:00
Mika Kuns c2ef446fe1 test(data,worker): cover ExpectsNoDiff persistence and MCP wiring
Data.Tests: default-false and true-roundtrip on TaskEntity.
Worker.Tests: add_task/update_task set/clear/leave-unchanged, and
preview_merge_set surfaces the flag per task.
2026-09-02 13:39:25 +02:00
Mika Kuns 7bcca3647e refactor(claude-do): refactor(worker): move the merge and worktree MCP tools out
`src/ClaudeDo.Worker/External/ExternalMcpService.cs` ist **1897 Zeilen** (nachgezählt 2026-09-02; die Datei ist seit dem Anlegen dieses Tasks von 1696 weiter gewachsen): ~30 DTO-Records oben plus ~25 Tool-Methoden — während **13** Geschwisterdateien `*McpTools.cs` im selben Ordner das Topic-Split-Muster schon tragen (`ListMcpTools`, `ConfigMcpTools`, `BatchMcpTools`, `NotesMcpTools`, `QueueStateMc

ClaudeDo-Task: 1862e160-ffde-4a50-aa03-bb7251f56088
2026-09-02 13:28:33 +02:00
Mika Kuns 244ba9b72f chore: merge main into task branch 2026-09-02 13:13:59 +02:00
Mika Kuns 0b506f529a feat(data): add ExpectsNoDiff flag for verification/audit tasks
An empty diff from a review/audit/verification task looks identical to
one that silently failed to commit. ExpectsNoDiff (TaskEntity, add_task/
update_task, preview_merge_set, review_task via TaskRefDto) lets the
merge helper tell them apart instead of treating every 0-changed-files
result as suspicious.
2026-09-02 13:13:13 +02:00
Mika Kuns 7a0b97df59 refactor(claude-do): refactor(worker): move the merge and worktree MCP tools out
`src/ClaudeDo.Worker/External/ExternalMcpService.cs` ist **1897 Zeilen** (nachgezählt 2026-09-02; die Datei ist seit dem Anlegen dieses Tasks von 1696 weiter gewachsen): ~30 DTO-Records oben plus ~25 Tool-Methoden — während **13** Geschwisterdateien `*McpTools.cs` im selben Ordner das Topic-Split-Muster schon tragen (`ListMcpTools`, `ConfigMcpTools`, `BatchMcpTools`, `NotesMcpTools`, `QueueStateMc

ClaudeDo-Task: 1862e160-ffde-4a50-aa03-bb7251f56088
2026-09-02 13:02:10 +02:00
Mika Kuns a0c277100e test(worker): assert the full external MCP tool-name inventory
Guardrail for the upcoming ExternalMcpService.cs topic split: proves the
move is name-preserving by pinning the complete set of registered tool
names (computed via the SDK's own McpServerTool.Create, not a
reimplementation of the naming rule) before any file is moved.
2026-09-02 13:02:10 +02:00
Mika Kuns 651ddbc31c chore(claude-do): merge feat(worktrees): warn about git worktrees ClaudeDo doesn't k
ClaudeDo-Task: 765fed13-db66-44e4-9285-c4b2d415b443
2026-09-02 13:01:57 +02:00
Mika Kuns 477a216567 chore(claude-do): merge refactor(tests): one IWorkerClient test double instead of tw
ClaudeDo-Task: 9ff7ec49-36c9-4b09-8747-e57bf74e37f2
2026-09-02 13:00:14 +02:00
Mika Kuns 6a4fa15372 fix(claude-do): merge batch_get_tasks: failureReason wird im fields-Parameter abge
ClaudeDo-Task: df16476b-5a10-446c-9ebc-99ddcc64e476
2026-09-02 12:59:52 +02:00
Mika Kuns 411eb7487b chore(claude-do): merge TaskRowViewModel: manuelle OnPropertyChanged-Kette auf [Noti
ClaudeDo-Task: 87976462-5845-4e01-8233-32afadca02bd
2026-09-02 12:59:35 +02:00
Mika Kuns ce70ba1c46 chore(claude-do): merge test(data): assert the migrated schema matches the EF model
ClaudeDo-Task: 9b8d47d7-e42f-4ea5-8c86-fd10a420e7f6
2026-09-02 12:59:18 +02:00
Mika Kuns 21769eb928 chore(claude-do): merge fix(localization): make the locale-parity test actually able
ClaudeDo-Task: dee67d25-ac34-4a71-b71d-6bbb1130943f
2026-09-02 12:59:07 +02:00
Mika Kuns 9b3f7ac5c8 chore(claude-do): merge fix(tests): Ui.Tests order-dependent flakiness — Loc.Current
ClaudeDo-Task: 618729f9-2d23-48e0-8952-7f17a7eb69a3
2026-09-02 12:58:44 +02:00
Mika Kuns 25f6691aa0 feat(claude-do): merge List-Handler-Prompt: Abhängigkeits-Audit in der Triage + Sch
ClaudeDo-Task: 04e9bc67-2612-4124-b513-83a02ecaa602
2026-09-02 12:58:31 +02:00
Mika Kuns dc1e67d07b fix(tests): stop Ui.Tests order-dependent flakiness on shared Loc.Current
Loc.Current (src/ClaudeDo.Ui/Localization/Loc.cs) is a process-wide mutable
static; which localizer is active depended on xUnit's parallel test
scheduling. Ui.Tests: disable collection parallelization (cheap, pure VM
suite) and install the real localizer in a module initializer so no test
can ever observe the uninitialized key-echo default. Worker.Tests: install
the same module initializer but leave parallelization on — disabling it
measured a 27% wall-clock regression on its real-SQLite/git suite, and the
initializer alone already removes the race. AgentConfigEditorViewModelTests
now installs its own localizer instead of relying on ambient state.
2026-09-02 12:55:51 +02:00
Mika KunsandClaude Haiku 4.5 912201770f fix(worker): add failureReason field to batch_get_tasks
The tool description claimed Failed tasks carry failureReason, but the field
was missing from BatchTaskDetailDto and rejected when requested in `fields`.
Add failureReason to the DTO, KnownDetailFields, and BuildDetail so list
handlers and other callers can read failure reasons without per-task get_task
calls. Tested: 31 BatchMcpToolsTests pass, including two new cases for
failureReason retrieval on Failed and Idle tasks.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-02 12:20:02 +02:00
Mika Kuns 9f90e75a85 refactor(tests): share StubWorkerClient between Ui.Tests and Worker.Tests
Worker.Tests carried its own hand-rolled FakeWorkerClient implementing the
whole IWorkerClient surface, duplicating StubWorkerClient in Ui.Tests. Every
hub-signature change had to be applied twice. Link StubWorkerClient.cs into
Worker.Tests via a csproj Compile Include and subclass it there; add
SetTaskStatusCalls tracking to the shared stub (the one behavior Worker.Tests
needed that Ui.Tests didn't have).
2026-09-02 12:17:00 +02:00
Mika Kuns 9e80564665 feat(data): add dependency audit and diff-review threshold to list-handler prompts
Triage phase now audits dependsOnTaskId chains before handoff: distinguishes real
from artificial (plan-numbered) dependencies, resolves artificial ones via
update_task, groups survivors into a wave plan, and checks the wave count against
the 4-handoff cap (2 merge rounds) so a chain too long to finish says so explicitly
instead of stranding mid-run.

Merge phase now reads a task's own diff when preview_merge_set's changedFileCount
is below 5, reserving the sonnet review subagent for larger diffs where the
context-protection cost is actually worth it.

Updates DefaultFor_merge_helper_merge_never_reads_diffs_itself, renamed to
DefaultFor_merge_helper_merge_gates_the_diff_subagent_on_a_file_count_threshold,
to match the new threshold behavior.
2026-09-02 12:16:44 +02:00
Mika Kuns b24ff614ff chore(claude-do): TaskRowViewModel: manuelle OnPropertyChanged-Kette auf [Noti
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs enthält ~107 handgeschriebene OnPropertyChanged(nameof(...))-Aufrufe in partial void OnXChanged-Methoden. Das Projekt nutzt an anderen Stellen den Generator: [NotifyPropertyChangedFor] in DetailsIslandViewModel.cs, ConflictResolverViewModel.cs, AgentConfigEditorViewModel.cs, Installer/WizardViewModel.cs.

Das ist die eine Datei, in der eine ve

ClaudeDo-Task: 87976462-5845-4e01-8233-32afadca02bd
2026-09-02 12:16:06 +02:00
Mika Kuns 25ead4c8dd chore(claude-do): feat(worktrees): warn about git worktrees ClaudeDo doesn't k
From `docs/open.md` (re-verified against the code 2026-09-02, still open): nothing reconciles `git worktree list` against the `worktrees` table. `ExternalMcpService.ListWorktrees` and the Worktrees overview read DB rows only; `WorktreeManager` calls git solely inside its self-heal path (`ListWorktreePathsForBranchAsync`). Observed once: 22 git-registered worktrees + 26 `claudedo/*` branches in `C:

ClaudeDo-Task: 765fed13-db66-44e4-9285-c4b2d415b443
2026-09-02 12:09:12 +02:00
Mika Kuns e099d4f6cf test(data): assert migrated schema matches EF model (schema-drift guard)
Sibling migrations branched off the same parent have silently dropped
each other's columns during a SQLite table rebuild before, and every
other Data.Tests fixture uses EnsureCreated, which skips the migration
chain entirely and would never catch that.
2026-09-02 11:53:28 +02:00
Mika Kuns 4c78a5d6fd fix(localization): make locale-parity test able to detect a malformed locale file
LocaleStore.Load silently skips a *.json that fails to parse, so a broken
de.json left store.Available with only "en" and the parity loop compared
English against itself, always green. Add two guards before the loop:
loaded-locale count must match the number of *.json files in the folder,
and "de" must be present via TryGet, both with failure messages naming the
missing file. Also independently JsonDocument.Parse every *.json so a
syntax error surfaces as a syntax error with the file name, not as
"locale missing". LocaleStore's catch-and-skip is untouched -- a broken
translation must still not crash the app at runtime.
2026-09-02 11:52:34 +02:00
mika kuns 56bc1651a8 fix(ui): don't leave an orphaned list behind on cancel
"New list" pre-creates the DB row before opening List Settings.
Cancelling or closing that modal left an untitled empty list behind;
CreateListAsync now rolls the row back unless the modal reports Saved.

Also moves the new-list/import-repo buttons up next to the "My Lists"
label instead of a row below the list, and puts Notes above Important
in the smart-list nav order.
2026-09-02 10:00:43 +02:00
mika kuns 80fe188a48 feat(ui): add "move to list" to the task row context menu
Reuses the existing drag-drop move (MoveTaskToListAsync) so the same
running/worktree guards apply — only ListId changes.

Also count WaitingForReview tasks (not Done-with-a-branch) for the
review portion of the status pill, and localize the pill and open-task
count strings that were still hardcoded English.
2026-09-02 10:00:21 +02:00
mika kuns c347e4fc5f perf(worker): switch to workstation GC to cut idle memory
Sdk.Web defaults to Server GC, one heap per core with a ~350 MB
gen0 budget each — an idle worker sat at ~230 MB private. Workstation
GC drops that to ~40 MB with no server-side concurrency to lose.
2026-09-02 09:59:51 +02:00
mika kuns 6be62f02cc refactor(ui): remove the stale-worker banner
The banner compared the running worker's build SHA against the
selected list's HEAD via a git ancestor check on every list switch
and reconnect. It added a git-rev-parse MSBuild target, a hub call,
and a poll loop for a corner case that hasn't come up in practice —
not worth the moving parts.
2026-09-02 09:59:39 +02:00
mika kuns b6e5876b07 fix(ui): stop treating git worktrees and submodules as repos
.git as a file (worktrees, submodules) isn't a standalone repo — the
scanner was flooding the import list with worktree entries alongside
their real repo.
2026-09-02 09:59:12 +02:00
mika kuns 30d0611b8e fix(worker): fix Bandel ticket sync and reactivate the integration
The status PATCH needs statusName alongside statusId or the API 400s,
so every write-back had been silently failing. The board's flat
assigneeName is null on every live ticket; the real assignee list is
ticketAssignees[]. Import now also picks up In Bearbeitung tickets,
not just Offen ones.

Both issues are live-verified against the real API, so the feature
flag flips back on.
2026-09-02 09:59:06 +02:00
CubeGameLPandClaude Opus 5 f640580463 fix(notes): Shift+Enter in the capture box, not just in the note rows
The previous fix put AcceptsReturn on the rows in the DataTemplate and
missed the box you actually type new notes into, which still had it off
and only a KeyBinding for Enter — so Shift+Enter there did nothing.

Both now go through the same tunnel handler: Enter files the note,
Shift+Enter is left to the TextBox for the line break. The KeyBinding is
gone; with AcceptsReturn on it could never have fired anyway, because
TextBox consumes Enter in its own OnKeyDown before bindings or bubbling
handlers run.

Verified against the running app: a note typed with Shift+Enter is
stored as "fe\r\ndwa".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:46:59 +02:00
CubeGameLPandClaude Opus 5 b90c5855f4 fix(notes): let Shift+Enter break the line, and clear the leftovers around the editor
Shift+Enter did nothing because AcceptsReturn was only set from a style
setter and never reached the box. Setting it on the element is half the
fix: TextBox.OnKeyDown consumes Enter regardless of modifiers and runs
before any bubbling handler, so the split chain would have died in its
place. Enter is now taken in the tunnel phase and Shift+Enter is left to
the box.

Alongside, the notes list stops borrowing furniture it has no use for:
no "0 open tasks" subtitle, no show-completed and list-settings buttons,
and no details pane beside it — the editor spans that column instead of
leaving it empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:36:28 +02:00
CubeGameLPandClaude Opus 5 0f04b796f4 feat(notes): notes get their own list, a day log and an Enter chain
The editor was reachable from one button that only appeared in My Day,
which is how it got forgotten. It is now a permanent sidebar entry next
to My Day and Planned, and it fills the tasks island rather than the
details pane, because a note list is content.

Inside, the date picker is gone: today sits at the top with the capture
box, older days follow as dated groups. Enter in a note saves it and
opens the next one, Shift+Enter breaks the line so notes can be several,
Backspace on an empty note deletes it and steps back up. Deleting also
has a visible hover button instead of only "clear the text and hope".
Agent-written notes carry a marker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:19:37 +02:00
CubeGameLPandClaude Opus 5 ae0c7d3771 feat(notes): add_note MCP tool with a per-day agent cap
Agents can write into the user's daily notes, but only within limits:
max 10 agent notes a day, max 500 characters, add only — no edit, no
delete. A new from_agent column separates their lines from the user's
so the cap counts the right ones and the UI can mark them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 21:02:37 +02:00
Mika Kuns ecb1cdbb06 fix(ui): show diff window before parsing so ParseOp spinner is visible 2026-08-12 13:23:03 +02:00
405 changed files with 47160 additions and 5476 deletions
+246
View File
@@ -1,5 +1,251 @@
# Changelog
## v2.18.0 — 2026-09-30
### Features
- column picker for the overview grid (a7c3d185)
- folders slide open and closed, rows below follow (2be8ba5d)
- animate folder expand in the sidebar (928e3905)
- assign a colour to a sidebar folder (bb92dc27)
- write weekly report as short bullets with haiku (8a31a67c)
- searchable Material Design icon library for lists and folders (f2ff865d)
- coloured dots for icon-less lists, guide line under chevron (9991f1fc)
- make sidebar folder hierarchy readable (75b78cb2)
- mission-control pane title shows usage limit reset time (5be97256)
### Fixes
- Recent-Sessions Resume/Brief-Buttons fuehren ihren Command wieder aus (60358100)
- convert ticket HTML to markdown on import (27d2b1d8)
- stop row buttons flashing disabled while a row command runs (2558e02b)
- show diff window before parsing so ParseOp spinner is visible (ecb1cdbb)
### Documentation
- add visual verification session notes 2026-09-16 (05deda83)
- update for v2.17.0 (f88d2a89)
## v2.17.0 — 2026-09-28
### Features
- allow nested sidebar folders via drag and drop (0dac047c)
- UI-Prozess loggt auf Platte (ui-.log) + fehlgeschlagene Hub- (57dd6766)
- Mission Control Recent Sessions mit verschachtelten Handoffs (d3db4455)
- Mission-Control-Kacheln behalten Position und Identitaetsfarbe ueber Handoffs (e0a1448f)
### Fixes
- Interactive sessions no longer Instantly start working (87dbb117)
- snake_case Serilog tokens + no Warning-log on cancelled hub calls (3009ff0f)
- DeleteTask aus der UI schlug immer mit generischem Serverfehler fehl (352aaa52)
### Documentation
- update for v2.16.0 (c9dd885f)
## v2.16.0 — 2026-09-16
### Features
- dirty Ziel-Verzeichnis beim Oeffnen des Merge-Dialogs melden statt nach dem Fehlschlag (09bcd696)
- preview_merge_set flags removed symbols and locale-key overlaps (684b0115)
- Bilder aus der Zwischenablage ins interaktive Terminal einfuegen (56161e4e)
- poll the OAuth usage endpoint as fast as its measured rate limit allows (50b6b56f)
### Fixes
- duplizierter Confirm-Dialog mit "Delete"-Knopf – zusammenfuehren und lokalisieren (86cc309b)
- Worktrees-Uebersicht zeigt Task-Nummern, DIFF-Spalte trimmt statt abzuschneiden (1325ab94)
- Diff-Viewer scrollt jetzt auch horizontal synchron (4ff5383b)
- Ordner-Toggle in der Sidebar schluckt schnelle Klicks (5cdfb14f)
- preview_merge braucht einen runVerify-Parameter wie preview_merge_set (7452f6cf)
- depends_on-Nachfolger schon beim gelandeten Merge freigeben (c874bf7e)
- Worker-Neustart trifft nun den tatsaechlich laufenden Prozess (9a41bed0)
- Shutdown terminates the app even with secondary windows open (103eb9a6)
### Documentation
- update for v2.16.0 (31dfaffc)
- update for v2.15.0 (18394dd6)
## v2.15.0 — 2026-09-11
### Features
- Stash-Option fuer uncommittete Aenderungen im Ziel-Tree (251b70ce)
### Documentation
- update for v2.14.0 (8e2d37e6)
## v2.14.0 — 2026-09-10
### Features
- Listen und Ordner in einer gemischten Sidebar-Reihenfolge (bfb1c652)
- confirm before closing a running ConPTY session (eb623714)
- worker-menu Shutdown ends worker + app after confirm (1bdf2027)
- chain a normal parent's children on WaitingForChildren (d2648f96)
- localize WorkConsole, DescriptionStepsCard and the ROADBLOCK label (8e6f0d16)
- surface depends_on blocking in status chip, chain label, and detail pane (4cf601e4)
- Worker-Menü "Update Claude CLI" führt claude update aus (dbee4a0d)
- triage model picker (opus/fable) in the merge helper start modal (1177463a)
- collapsible folders for the list sidebar (25c9a619)
- list_folders table plus lists.folder_id (13e860f0)
### Fixes
- Import broadcastet TaskUpdated fuer neue Tasks (1770b677)
- ConnectedClientTracker in den externen MCP-Container teilen (3d09323e)
- de.json-Konsistenz-Pass und restliche harte EN-Strings (d6696c33)
- force English git output for locale-fragile stderr matches (5ecf6f75)
- make diff-stat parsing locale-safe and dedupe the two parsers (29ea18c4)
- report failed paste/drop in the interactive terminal (0cec0a77)
- surface worker connect/restart failures instead of no-opting (b4b62254)
- validate PowerUse window, explain effort and Prime Claude in Settings (951c2fe9)
- start_list_handler ehrlich machen, wenn keine App verbunden ist (451c0184)
- warn and block save when a list's working dir isn't a git repo (3348736e)
- show a banner when a ConPTY session dies after startup (256bea5f)
- merge duplicate top-level "checks" block in en/de.json (741f66fe)
- translate max_turns failure text to English (0fcfe7c5)
- route WeekReportService permission-mode through PermissionModeResolver (1c768529)
- report startup port conflicts instead of crashing silently (c9d4823f)
### Documentation
- record list folders in the Data and Ui CLAUDE.md (5477ae53)
- update for v2.13.6 (ec1e60be)
## v2.13.6 — 2026-09-07
### Features
- Home-Ordner als Sitzungsziel in Mission Control (f231f08a)
- Einfuegen und Datei-Drop im interaktiven Terminal (23491d3b)
- Cheat-Sheet-Fenster im Hilfe-Menue (0db9fade)
- PowerUse-Einstellungen nach Settings -> Ausfuehrung verschieben (7c7c33d6)
### Documentation
- update for v2.13.5 (c7392e08)
## v2.13.5 — 2026-09-03
### Fixes
- Version im About-Dialog aus der FileVersion lesen (cccfe2ec)
### Refactoring
- "Nach Updates suchen" ins Hilfe-Menue verschieben (c16c7b97)
### Documentation
- update for v2.13.4 (a8ec5507)
## v2.13.4 — 2026-09-03
### Fixes
- interaktive Session ohne Arbeitsverzeichnis im User-Verzeichnis starten (f70503d0)
### Documentation
- update for v2.13.3 (9360cd2f)
## v2.13.3 — 2026-09-03
### Fixes
- raise SignalR MaximumParallelInvocationsPerClient (e64d5705)
### Documentation
- update for v2.13.2 (fc25ee45)
## v2.13.2 — 2026-09-03
### Features
- Handler-Run per Knopf abbrechen (bd5aa265)
### Fixes
- strip CLAUDE_CODE_CHILD_SESSION/CLAUDECODE before every ConPTY launch (51ee5050)
- Dequeue-X auf blockierten Queue-Zeilen anzeigen und Kette konsistent halten (df3e7ba0)
- report and gate ListSettingsModal load failures instead of swallowing them (a1b20d9f)
- move ScrollViewer padding to inner content in modal bodies (e0c6b670)
- Mission Control oeffnen, wenn ein Handler-Run per MCP startet oder ein Handoff eintrifft (b05b6fba)
### Performance
- show list settings modal before its seven-round-trip load finishes (f24654df)
### Documentation
- remove stale session-skills empty-state entry (2bd48ea1)
- update for v2.13.1 (eb3aee41)
- update for v2.13.0 (2b873322)
## v2.13.1 — 2026-09-02
### Fixes
- Mission Control oeffnen, wenn ein Handler-Run per MCP startet oder ein Handoff eintrifft (9f64ba50)
## v2.13.0 — 2026-09-02
### Features
- Handler-Session vor dem Usage-Limit warnen und Handoff bis zum Reset zurueckhalten (fe818651)
- start_list_handler MCP-Tool startet einen List-Handler-Run wie der Button (c2166a0d)
- Handler-Session nach Usage-Limit-Reset automatisch neu starten (016b9203)
- PowerUse-Zeitfenster + Handoff-Schwellwert als Settings (69898be5)
- PowerUse-Toggle im Usage-Monitor, Opt-in pro Liste (19375254)
- PowerUse-Addendum an die Handler-Prompts, Cap aus den Settings (453d61ca)
- PowerUse-Flags und konfigurierbares Handoff-Budget (3cf9628f)
- depends_on als Menge statt Einzelfeld (c25732cf)
- surface the handoff round budget in the merge-helper handoff brief (81a5fc5d)
- add ExpectsNoDiff flag for verification/audit tasks (0b506f52)
- add dependency audit and diff-review threshold to list-handler prompts (9e805646)
- add "move to list" to the task row context menu (80fe188a)
- notes get their own list, a day log and an Enter chain (0f04b796)
- add_note MCP tool with a per-day agent cap (ae0c7d37)
### Fixes
- carry ExpectsNoDiff into the relocated merge MCP tools (4ee1d05f)
- stop Ui.Tests order-dependent flakiness on shared Loc.Current (dc1e67d0)
- add failureReason field to batch_get_tasks (91220177)
- make locale-parity test able to detect a malformed locale file (4c78a5d6)
- don't leave an orphaned list behind on cancel (56bc1651)
- stop treating git worktrees and submodules as repos (b6e5876b)
- fix Bandel ticket sync and reactivate the integration (30d0611b)
- Shift+Enter in the capture box, not just in the note rows (f6405804)
- let Shift+Enter break the line, and clear the leftovers around the editor (b90c5855)
- auto-bootstrap commit-less repos before creating worktrees (e0748ba3)
- bootstrap commit-less repos and survive missing git identity (de44196b)
### Performance
- switch to workstation GC to cut idle memory (c347e4fc)
### Refactoring
- share StubWorkerClient between Ui.Tests and Worker.Tests (9f90e75a)
- remove the stale-worker banner (6be62f02)
### Documentation
- Handler-Prompt praezisieren (Wave-Plan, Task-Split, review_task-Aufruf) (20b3b4ce)
- Test-Scoping fuer Implementer-Tasks, PowerUse in den Explore-Notes (416997d7)
- plan for multi-dependency depends_on relation table (84d955df)
- empty-repo hardening notes + verification item (cf093e31)
- spec + plan for empty-repo (unborn HEAD) hardening (fea8236e)
- update for v2.12.0 (1d6cd49b)
## v2.12.0 — 2026-08-28
### Features
+29 -5
View File
@@ -66,17 +66,41 @@ Commit freely (per task + the spec/plan docs). Never push without asking.
## Building & Testing
`dotnet build ClaudeDo.slnx` requires .NET 9; on .NET 8 build individual projects with `-c Release` (a running Worker locks the `Debug` output).
**Build the whole solution, test only what you touched.** Always `-c Release` — a running Worker locks the `Debug` output.
```bash
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release # pulls in Ui + Data
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release # also: Data.Tests, Ui.Tests, Localization.Tests, Installer.Tests, Releases.Tests
dotnet build ClaudeDo.slnx -c Release # ~40s, catches cross-project breaks
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build \
--filter "FullyQualifiedName~Handoff" # seconds, not minutes
```
`.slnx` needs SDK 9+ (this machine is on 10). On an older SDK build the projects individually:
`src/ClaudeDo.App` (pulls in Ui + Data) and `src/ClaudeDo.Worker`.
### Do NOT run the full test suite to finish a task
Measured 2026-09-02, sequentially after one build: **Worker.Tests 507s**, Ui.Tests 33s, Data.Tests 8s,
Localization/Installer/Releases 3s each, build 42s — ~10 min for all six. Run in parallel across
projects it is *worse*, not better (Worker.Tests 801s, Ui.Tests 82s): these tests create real git
worktrees and spawn processes, so they thrash rather than share.
With up to 5 implementer tasks running at once, a full suite per task costs hours — and **cannot find
what it costs that for.** Each task works in its own worktree containing only its own change, so a
collision between two parallel tasks is invisible to both. Collisions surface after the merge, which
is exactly where `verify_merges` runs the list's verify command **once per batch**. The full suite is
the merge gate's job, not the task's.
So, per task:
- **Build:** the whole solution, always. It's cheap and it catches the compile break your change
caused three projects over.
- **Tests:** `--no-build --filter` scoped to what you touched. Add a filter for the classes covering
your change; if you genuinely can't scope it, run the one relevant test project, never all six.
- **Report** what you ran and what you did not, so the merge session knows what is still unverified.
- Never report "all tests pass" when you ran a filtered subset. Say which filter you used.
### Gotchas
- **Subagents:** use the `sonnet` model; stage files explicitly by path — never `git add -A` (parallel sessions often leave unrelated WIP in the tree).
- **Icons:** `PathIcon` *fills* its geometry — line-art icons must be filled geometry or a `Path Classes="icon-stroke"`, else they render invisible. Colour comes from the `ICON PAINTING` block in `IslandStyles.axaml` (class + state); setting `Foreground`/`Stroke` inline kills the hover state.
- **Icons:** Material Design Icons via `mdi:Name` + the `IconKey` converter (a misspelled name silently renders nothing — `MdiIconNamesTests` guards it). Colour comes from the `ICON PAINTING` block in `IslandStyles.axaml` (class + state); setting `Foreground`/`Stroke` inline kills the hover state.
- **Localization:** `locales/en.json` and `locales/de.json` keys must stay in parity (Localization.Tests enforces it).
- **Test fakes:** changing `IWorkerClient` / `WorkerHub` / ViewModel constructors breaks hand-rolled fakes in both test projects — update them.
+108 -9
View File
@@ -11,14 +11,15 @@ process hosts (real `claude` TUI in a Mission Control tile).
Autonomous queue tasks are **not** covered here — they stay on the stream-json path via
`TaskRunner`. See [worker-task-pipeline.md](worker-task-pipeline.md).
## The four session kinds
## The five session kinds
| Kind | Hub spec method | Notes |
|---|---|---|
| Task session | `GetInteractiveLaunchSpec` | Effort from the task/list model preset |
| Ad-hoc | `GetAdHocLaunchSpec` | Effort from the global default |
| Planning | (planning start/resume) | Effort from `PlanningAlias`; uses `--permission-mode default`, **not** `plan` |
| Task session | `GetInteractiveLaunchSpec` | No `--effort` — CLI default |
| Ad-hoc | `GetAdHocLaunchSpec` | No `--effort` — CLI default |
| Planning | (planning start/resume) | No `--effort`; uses `--permission-mode default`, **not** `plan` |
| List handler | `GetMergeHelperLaunchSpec` / `GetMergeHelperHandoffLaunchSpec` | Model + effort fixed per role (`ModelRegistry.HandlerTriageAlias`/`HandlerWaitAlias`/`HandlerMergeAlias`), **not** from list config; `--permission-mode` via `PermissionModeResolver` (unattended) |
| SSH workspace | `GetSshLaunchSpec` | Exe is `ssh.exe`, not claude: `-R <random port>:127.0.0.1:<remote_mcp_port>` + one remote shell command that base64-decodes a `claudedo` MCP config (per-session token) to `~/.claudedo/mcp-<paneId>.json` and runs claude. **Not** recorded in `mission_control_sessions`, no `claudedo_pane`/handoff hint. Workspaces: `app_settings.ssh_workspaces`, Settings → SSH-Workspaces |
## ⚠️ Gotcha: never pass task free-text as a CLI argument
@@ -39,9 +40,10 @@ description skips the file **and** the positional arg entirely — it still gets
## Argument ordering
Every spec passes `--effort <level>` from the relevant model's preset. It **leads** the args —
except for a fresh task session with a brief, where `--add-dir <sessionDir>` must come first so
`--effort` (a single-value flag) can sit directly before the positional kickoff.
Only the list-handler specs pass `--effort` (from the role model's preset). Hand-driven sessions
(task, ad-hoc, planning, handoff, resume) deliberately pass none, so the CLI default / the user's
`/effort` setting applies. A positional kickoff must follow a single-value flag (`--session-id`,
`--append-system-prompt-file`), never a variadic one (`--add-dir`, `--allowedTools`).
`--model` is deliberately **NOT** forced on an interactive session — the user can still switch
models in the TUI.
@@ -87,7 +89,7 @@ own transcript:
`TaskRepository.SetInteractiveSessionIdAsync` — **before** the `LaunchSpec` is returned, i.e.
before the ConPTY host ever spawns `claude`. `BuildFreshTaskArgsAsync` then passes it as
`--session-id <guid>`, placed as the single-value flag directly before the positional kickoff
(or, with no brief, right after `--effort`).
(or, with no brief, as the only arg).
3. **Fresh worktree wins.** `isFreshWorktree` forces `run` to `null` *and* is checked before
reading `task.InteractiveSessionId`, so a recreated worktree never resumes a stale id from
either source — it always takes the fresh path, which overwrites the stale
@@ -97,6 +99,48 @@ Net effect: reopening an interactive session for a task (pane closed, process ki
resumes the same claude conversation, because the id was committed to the DB before the previous
launch even started.
## Session handoff (`handoff_session`)
Task and ad-hoc tiles can hand off to a fresh session in the same tile. The **paneId is the pane's
`IdentityKey`**: the task id for a task tile, a GUID minted by `OpenAdHocConPtySessionAsync` for an
ad-hoc one.
**The model never passes an id** (it guesses GUIDs). Instead, every such spec gets
`--mcp-config ~/.claudeDo/pane-sessions/<paneId>/mcp.json`, which adds a second entry,
`claudedo_pane`. It points at the same external MCP, with headers `X-ClaudeDo-Pane` and
`X-ClaudeDo-Session` (plus `X-ClaudeDo-Key` when one is configured). An `AddListToolsFilter`
(`SessionHandoffMcpTools.FilterByPane`) splits the tool list by that header:
- `claudedo_pane` lists only `handoff_session`;
- the plain `claudedo` entry lists everything except `handoff_session`.
The hint (`handoff-hint.md`, via `--append-system-prompt-file`) says when to call
`mcp__claudedo_pane__handoff_session(brief)` and what goes into the brief, and contains no ids. Ad-hoc
tiles get a pre-assigned `--session-id` so the header has one. With `ExternalMcpPort = 0` the tile
gets neither the entry nor the hint.
`handoff_session(brief)` writes `pane-sessions/<paneId>/handoffs/<guid>/brief.md` (first line:
`Handed off from session <id>`) and broadcasts `SessionHandoffRequested(paneId, briefPath)`. The
worker has no pane registry: the UI looks up the pane by `IdentityKey`, takes its `Cwd` (from the
resolved launch spec) and its `TaskId`, and closes it. It then reopens the tile at the same slot
with the same `IdentityKey` via `GetSessionHandoffLaunchSpec` → `BuildHandoffAsync`. For a task
tile, the new session id replaces `InteractiveSessionId`, so reopening the task resumes the
handed-off session. The old transcript remains under `~/.claude/projects/`.
## Recent sessions
Every task, ad-hoc and handoff tile launch (only those — `paneId != null`) records its session in
`mission_control_sessions` from inside `InteractiveLaunchSpecService`, which is where the session id
is known. A handoff's predecessor comes from the brief's first line
(`SessionHandoffMcpTools.ReadHandedOffFrom`), and it inherits the predecessor's title and task.
The clock button in the Mission Control header opens a flyout (`RefreshRecentSessionsAsync`, last 14
days / 50 rows, read straight from the DB). `NestHandoffs` groups each chain under its origin.
- **Resume**: a task's *current* `InteractiveSessionId` reopens the task tile (deduped by TaskId).
Any other session gets a fresh ad-hoc tile via `GetResumeSessionLaunchSpec` → `BuildResumeAsync`
(`--resume <id>` in the recorded cwd, handoff hint included).
- **Brief**: `ShellOpen.Path(brief.md)`, which opens the file in the default editor.
- Not guarded: resuming a session that is still live in another ad-hoc tile, since panes don't
track their session id.
## List handler ("Let Claude handle it")
`BuildForMergeHelperAsync`/`BuildForMergeHelperHandoffAsync` resolve `--permission-mode` via
@@ -115,6 +159,22 @@ share the same prompt/model as their non-final counterpart and differ only in on
rendered into the handoff kickoff file (`PromptKind.MergeHelperHandoff`) marking the final round
and forbidding further reruns.
The chain length is bounded server-side by `HandoffRoundTracker`, whose cap comes from
`AppSettingsEntity.ListHandlerMaxHandoffs` (default 4 = two merge rounds; `HandoffMcpTools` reads it
per call and `Register` clamps to min 1, so 0 can never mean "unbounded"). Past the cap a
`wait`/`merge` request is silently coerced to its `_final` counterpart — check the result's
`coerced` flag — and any handoff after a `merge_final` round throws.
**PowerUse.** When `AppSettingsEntity.PowerUseEnabled` (Settings → Execution) **and** the
list's own `ListConfigEntity.PowerUseAllowed` are both set, `InteractiveLaunchSpecService.
ComposeHandlerPromptAsync` appends `PromptKind.PowerUseAddendum` to whichever handler prompt the
phase resolved to. The addendum is written as an *override* of the base prompt's interactive rules
(never ask, decide and record instead; resolve or abort conflicts rather than leaving them in the
tree; harvest new tasks when a round would starve). Both keys are required on purpose: the global
switch can stay on overnight without turning an unrelated list's handler run unattended. A run that
is not in PowerUse gets a byte-identical prompt to the pre-PowerUse behaviour — there is deliberately
no second set of handler prompts to keep in sync.
Each handoff hands the SAME handler task id to a NEW tile, and `MissionControlViewModel.
OpenMergeHelperHandoffConPtySessionAsync` closes the outgoing phase's pane (`CloseConPtySession`,
which disposes its `PtyTerminalSession` and kills the underlying `claude` process) before opening
@@ -127,6 +187,43 @@ outgoing session is only ever told to end its turn, never to exit. This keeps th
`MCP_TOOL_TIMEOUT` is 200 s here — `TaskWaitMcpTools` clamps its own timeout to 170 s to stay
comfortably under it (see [external-mcp.md](external-mcp.md)).
### Auto-restart after the usage limit
A handler session that dies mid-phase used to be a silent dead tile: `InteractiveTerminalViewModel.
OnSessionProcessExited` only raised a visible banner for a death **inside** its 5 s
`StartupGraceWindow`. It now raises `ExitedAfterStartup` for a later non-zero exit; the pane
forwards it as `SessionDiedAfterStartup(taskId, exitCode)` and `MissionControlViewModel` reports it
via `WorkerHub.ReportInteractiveSessionExited`. Every task-based pane reports — the UI does no
filtering, because the exit code is the *only* thing it knows.
`HandlerRestartCoordinator` (Worker) decides the rest and **fails closed** — unlike the usage gate,
every missing signal means "don't relaunch":
- **Which round was running** comes from `HandlerRoundState`, a JSON file at
`~/.claudeDo/handler-rounds.json` (one `HandlerRound` per handler task id, deliberately not a DB
column). `BuildForMergeHelperAsync` writes the opening round as phase `"triage"`;
`BuildForMergeHelperHandoffAsync` writes each subsequent `MergeHelperPhase`. Without it the phase
and task set exist *only* in the transient `HandoffRequested` broadcast, so a dead tile could not
be reopened. A `"triage"` round is never reopened — there is no handoff spec for it.
- **Was it really the usage limit?** A ConPTY session has no captured stdout, so `TaskRunner.
IsUsageLimitMessage` (which matches the CLI's own "session limit" text) has nothing to read. The
corroborating witness is `UsageState.Snapshot.FiveHour.Utilization ≥ 95`
(`HandlerRestartCoordinator.UsageLimitProofPct`) at the moment of death. A crash at 40 % is not
restarted; no snapshot at all is not restarted.
- **PowerUse only** — both keys, exactly like the prompt addendum above.
- **One restart per round**, claimed via `HandlerRoundState.TryClaimAutoRestart` before the wait, so
a restart that dies again stays dead. `HandlerRoundState.Save` carries `AutoRestartUsed` over when
the phase is unchanged, because the restart's own launch-spec build writes the round again — a
naive overwrite would hand out a fresh budget every time and loop all night.
- The restart waits out `FiveHour.ResetsAt`, then re-broadcasts `HandoffRequested` with the
persisted phase + task set, which the UI already handles by reopening the tile. It goes **around**
`handoff_list_handler`, so `HandoffRoundTracker.Register` is never called and the run loses no
handoff budget.
Sibling to `UsageLimitAutoContinueCoordinator`, not an extension of it: that one only ever sees
headless runs (`FailureReason == "usage_limit"`, resumed through the override slot), and a ConPTY
session has no `task_runs` row to classify.
### The host task and its commit range
The handler run **owns a real ClaudeDo task**, created by `CreateMergeHelperTask` (hub) →
@@ -232,6 +329,8 @@ bails out immediately instead of racing past the collection-based dedupe.
(scoped to `MainWindow` via `AddClassHandler<MainWindow>`) never runs there — **Escape always
reaches the PTY**. See the note in `src/ClaudeDo.Ui/CLAUDE.md`.
**AltGr bypass:** Windows reports AltGr as Ctrl+Alt, and Iciclecreek's `TerminalView.OnKeyDown` turns any Ctrl/Alt combo into a control sequence (NUL/0x1C/ESC) and marks it handled, so `@ \ { } € ~ |` never arrive as text. `InteractiveTerminalView.OnPreviewKeyDown` (tunnel) detects it via `IsAltGrText` and sends `KeySymbol` through `SendTextAsync` first.
`TaskRowViewModel.HasInteractiveSession` shows an accent "Interactive" chip instead of "Parked";
tapping it jumps to that Mission Control pane. `TasksIslandViewModel.SyncInteractiveSessions`
mirrors Mission Control's open panes onto the rows.
@@ -258,7 +357,7 @@ Autonomous and interactive sessions do **not** share a system prompt. Per start
|---|---|---|
| Autonomous run/continue/retry | `TaskRunner.ResolveConfigAsync` → `ClaudeArgsBuilder.Build` | `--append-system-prompt <text>`, recomputed and re-sent on **every** invocation including a `--resume` continue (`PromptKind.System` + improvement/list/task overrides) |
| Interactive task session, fresh | `InteractiveLaunchSpecService.BuildForTaskAsync` → `BuildFreshTaskArgsAsync` | none — no `--append-system-prompt(-file)` at all |
| Interactive task session, resume | `InteractiveLaunchSpecService.BuildForTaskAsync` → `WindowsTerminalLauncher.BuildResumeArgs` | none — only `--resume <id>` (+ `--effort`) |
| Interactive task session, resume | `InteractiveLaunchSpecService.BuildForTaskAsync` → `WindowsTerminalLauncher.BuildResumeArgs` | none — only `--resume <id>` |
| Ad-hoc directory session | `InteractiveLaunchSpecService.BuildForDirectoryAsync` | none |
| Planning session start | `InteractiveLaunchSpecService.BuildPlanningStart` → `WindowsTerminalLauncher.BuildPlanningStartArgs` | `--append-system-prompt-file <path>` (`PromptKind.Planning`) |
| Planning session resume | `InteractiveLaunchSpecService.BuildPlanningResume` → `WindowsTerminalLauncher.BuildPlanningResumeArgs` | none — only `--permission-mode default --allowedTools <planning allowlist> --resume <id>` |
+20 -11
View File
@@ -1,8 +1,8 @@
# External MCP tool surface
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
> Last verified against commit `38af549` (2026-08-11).
> Drift check: `git log --oneline 38af549..HEAD -- src/ClaudeDo.Worker/External`
> Last verified against commit `cb775958` (2026-10-01).
> Drift check: `git log --oneline cb775958..HEAD -- src/ClaudeDo.Worker/External`
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
Covers `src/ClaudeDo.Worker/External/` — the always-on MCP tools ClaudeDo exposes to general
@@ -25,8 +25,8 @@ is wired through `TaskIdResolver`, which resolves:
This is **not** ambiguous: a GUID is never all-digits, so a pure-integer parameter is always a
number, not a partial GUID. All task-returning tools (`GetTask`, `ListTasks`, `AddTask`,
`BatchGetTasks`, etc.) stamp the resolved task's `Number` in the DTO — both `TaskDto` and the
lean `TaskRefDto` carry an `int Number` field. **Branch names and worktree paths** (`claudedo/{id}`)
continue to use the GUID; the number is a display alias, never the identity.
lean `TaskRefDto` carry an `int Number` field. **Task worktree branches and folders** are `claudedo/{number}-{title-slug}`;
otherwise the number is a display alias, never the identity.
Every tool description carries a shared boilerplate clause (defined in `McpToolDocs.TaskNumberHint`)
instructing the agent to **refer to tasks as `#<number>` when reporting results to the user** —
@@ -98,6 +98,8 @@ Daily prep: `GetDailyPrepCandidates`, `SetMyDay`.
| `TaskWaitMcpTools` | `WaitForTaskChange` |
| `QueueStateMcpTools` | `GetQueueState` |
| `AttachmentMcpTools` | `AddTaskAttachment`, `ListTaskAttachments`, `RemoveTaskAttachment` |
| `ListHandlerMcpTools` | `StartListHandler` |
| `TicketMcpTools` | `GetTicketImportCandidates` (open assigned ticket-system items not yet imported + all lists + fallback list; `available=false` + `reason` when no ticket system), `ImportTicket` (one Idle task per item; no `list` → fallback list; `created=false` always carries `skippedReason`) |
## Per-tool behaviour worth knowing
@@ -107,13 +109,13 @@ Description/Result) in `tasksFull` instead (`tasks` null). Filtering by `created
happens before the lean/full projection either way.
**`UpdateTaskStatus`** accepts `Idle` / `Queued` / `Cancelled` / `Done` only.
- `Cancelled` goes through `TaskStateService.CancelAsync(..., allowFromIdle: true)` — the
**only** caller that opts into cancelling from `Idle`. `PlanningChainCoordinator` relies on
`Idle` staying a no-op there by default, because a child parked back to `Idle` mid-chain is
a manual opt-out signal.
- `Done` goes through `TaskStateService.ForceSetStatusAsync` (the same unconditional write the
UI's "set status freely" affordance uses) but is **refused** for a task with an active
worktree, since that would skip `review_task`'s merge.
- `Cancelled` goes through `TaskStateService.CancelAsync(..., retire: true)` — like the UI's
"Mark as cancelled", it opts into cancelling from `Idle`/`Failed`. `PlanningChainCoordinator`
relies on `Idle` staying a no-op there by default, because a child parked back to `Idle`
mid-chain is a manual opt-out signal.
- `Done` goes through `TaskStateService.MarkDoneAsync` (the done toggle, so a chain successor
unblocks and the parent advances) but is **refused** for a task with an active worktree,
since that would skip `review_task`'s merge.
**`AddTask`** — always creates the task; also returns `possibleDuplicates` (up to 3, id/title/status
only, no descriptions) — open (non-terminal) tasks in the *same list* whose normalized title
@@ -235,6 +237,13 @@ task already outside those two (unknown ids reported as status `"NotFound"`, als
**`AttachmentMcpTools`** — re-attaching the same `fileName` overwrites. Add/remove refuse on a
`Running` task.
**`StartListHandler`** — the worker only broadcasts `ListHandlerStartRequested`; the connected
ClaudeDo App does the actual work (creates the handler task, opens the triage ConPTY tile). Before
broadcasting it checks `ConnectedClientTracker` (a `WorkerHub.OnConnectedAsync`/`OnDisconnectedAsync`-driven
singleton count) — with zero connected clients the broadcast would reach nobody, so the tool
returns `Requested: false` plus a `Reason` instead of claiming success. Does not queue or retry the
request for a later connect.
**`GetDailyPrepCandidates`** — Idle, non-blocked tasks in a git repo **not** excluded by
`AppSettings.ReportExcludedPaths` and not already `IsMyDay`, plus the current Idle MyDay tasks
and `maxTasks` (= `DailyPrepMaxTasks`). Repo-exclusion logic lives in the `DailyPrepFilter`
+37 -11
View File
@@ -1,8 +1,8 @@
# Review, merge & conflict resolution
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
> Last verified against `168ab1cd` (2026-08-26), which added the batch verify
> (`skipVerify` + `verify_merges`).
> Last verified against `0e6a48e2` (2026-10-01): parent's worktree is the unit branch (children
> fast-forwarded into it); in a company repo approve ends in a pushed branch + draft PR.
> Planning mode renders per file and `DiffLinesView` is retired.
> Drift check: `git log --oneline 20bce9b..HEAD -- src/ClaudeDo.Worker/Lifecycle src/ClaudeDo.Worker/State src/ClaudeDo.Worker/Planning src/ClaudeDo.Ui/ViewModels/Conflicts src/ClaudeDo.Worker/External`
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
@@ -15,12 +15,32 @@ Covers the review→merge path: `TaskStateService` review transitions, `TaskMerg
`ApproveReview` (hub) and `review_task` approve (MCP) are the **single** review+merge action.
There is no separate "Merge all" entry.
- **Task with children** → drives `PlanningMergeOrchestrator`: merges the parent worktree if
`Active`, then each `Done` child in order, then sets the parent `Done`. A mid-merge conflict
pauses for `ContinuePlanningMerge` / `AbortPlanningMerge`.
- **Childless task** → `TaskMergeService.ApproveAndMergeAsync`. A conflict keeps the task in
`WaitingForReview`.
- **Legacy unit** (`UnitBranchService.HasUnabsorbedChildrenAsync`: a `Done` child whose worktree
is still `Active`, i.e. planned before the unit branch existed) → drives
`PlanningMergeOrchestrator`: merges the parent worktree if `Active` (planning parents too, for a
mixed unit), then each `Done` child in order, then sets the parent `Done`. A mid-merge conflict
pauses for `ContinuePlanningMerge` / `AbortPlanningMerge`. Children in `WaitingForReview` are
silently skipped there.
- **Everything else** — a childless task, or a parent whose children were absorbed into its unit
branch → `TaskMergeService.ApproveAndMergeAsync` on the task's own worktree. A conflict keeps
the task in `WaitingForReview`. A planning parent with any child not `Done` is `blocked` there
(`subtask X is not Done (status …)`, same as the orchestrator) — no half plan on the target.
- **Child of a unit** (parent has an `Active` worktree) → `ApproveAndMergeAsync` absorbs it into
the unit branch (`UnitBranchService.AbsorbChildAsync`, ff-only) instead of merging to the target.
- **No active worktree** (sandbox run) → straight to `Done`.
- **Company repo** (`PullRequestPublisher.IsCompanyRepoAsync`, origin matches
`company_remote_pattern`, checked live) with an `Active` or `Kept` worktree → `ApproveCompanyAsync`
instead of a merge, after the planning-parent gate and the child absorb: missing PAT / non-ADO
remote is `blocked` before anything runs, then the verify command in the **worktree**
(`skipVerify` ignored — no local merge to defer it to; red = `verify_failed`, nothing pushed),
then `PullRequestPublisher.PublishAsync`: `git push -u origin <branch>` (PAT via `GIT_CONFIG_*`
env, `AzureDevOpsClient.GitAuthEnv`), find-or-create the draft PR, store
`worktrees.pull_request_url`, `WorktreeUpdated`. Result `pr_created` / `pr_updated` + URL
(`MergeResult.PullRequestUrl`; MCP puts it in `mergeMessage`). Task stays `WaitingForReview`,
worktree `Active`, ticket `InBearbeitung`; `Done` is the user's manual toggle after the ADO
merge. A PR failure after a successful push is `blocked` with `gepusht, PR fehlgeschlagen: …`.
⚠️ Only approve branches — `merge_task`/the merge modal and the legacy-unit orchestrator still
merge locally in a company repo.
Review transitions all live in `TaskStateService`: `SubmitForReviewAsync`,
`SubmitForChildrenAsync`, `ApproveReviewAsync`, `RejectToQueueAsync`, `RejectToIdleAsync`,
@@ -184,9 +204,9 @@ detached-HEAD scratch worktree under the OS temp dir (`GitService.WorktreeAddDet
is always removed afterward (`finally`, non-cancellable cleanup). No verify command configured, or
`runVerify=false`, reproduces the pre-existing preview exactly — no delay, no scratch worktree.
Wired into `ExternalMcpService`: `preview_merge` (single task) always requests a verify run when the
list has a command configured; `preview_merge_set` only does when its `runVerify` parameter is
explicitly set (default `false`) — a set preview never starts N builds unasked. The Hub's
Wired into `ExternalMcpService`: both `preview_merge` (single task) and `preview_merge_set` only
request a verify run when their `runVerify` parameter is explicitly set (default `false`) — a
preview never starts a build unasked. The Hub's
`PreviewMerge` (the UI's live mergeability indicator) always passes `runVerify: false`, since
running a build on every poll would be a bad regression; `MergePreviewDto` carries the
verify fields anyway so a future UI entry point can opt in. Result fields: `VerifyExitCode` (null =
@@ -203,7 +223,8 @@ succeeds, written **only** by `WorktreeRepository.SetMergedAsync` (which atomica
It is the only thing that makes `revert_merge` possible without heuristically searching
`git log` — see [external-mcp.md](external-mcp.md) → `RevertMerge`. Null for any worktree
merged before the field existed.
merged before the field existed, and for a child absorbed into its unit branch (a fast-forward
has no merge commit) — such a child is reverted with its whole unit, never alone.
## Review gate in the UI
@@ -231,6 +252,11 @@ Single-task mode starts the conflict merge, then parses each conflicted file int
conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`. Types live in
`ConflictModels`: `MergeFile` / `MergeFileSegment` / `MergeConflictBlock`.
⚠️ **The resolver restarts the merge itself** (`StartConflictMerge`, `leaveConflictsInTree`), so the caller's
`autostash` consent must travel with it (`MergeCoordinator.ResolveConflictAsync(..., autostash)` →
`OpenAsync(target, autostash)` → hub). Without it a dirty target answers `dirty_target`, the
resolver stays closed after the stash-retry approve already reported `conflict`.
Exposed per active file: `ActiveOursText` / `ActiveResultText` / `ActiveTheirsText`
(reconstructed from `MergeFile.OursText/ResultText/TheirsText`; Result seeds unresolved
conflicts with Ours), plus `ActiveFile` / `SelectFileCommand` (multi-file switcher),
+9 -3
View File
@@ -21,6 +21,11 @@ authenticated with the Bearer access token Claude Code itself keeps fresh at
Because the endpoint is undocumented and can change without notice, **every consumer
fails open**. That is the single most important invariant here.
**Measured 2026-09-16** (clean burst against the live endpoint, Worker otherwise idle): exactly
5 requests succeed, the 6th 429s with `Retry-After: 300` — a flat 5-requests-per-5-minute quota,
no `ratelimit-*`/`x-ratelimit-*` headers on either the 200s or the 429. The quota is per-account,
not per-process, so a manual refresh or another consumer of the same OAuth token shares it.
## Components (`Usage/`)
| Type | Role |
@@ -28,7 +33,7 @@ fails open**. That is the single most important invariant here.
| `UsageModels` | `UsageBucket` / `UsageLimitRow` / `UsageSnapshot`. `UsageBucket.Utilization` is already a 0–100 percent — compare directly against thresholds, don't rescale. |
| `ClaudeOAuthUsageClient` | Reads the token, calls the endpoint. Defensive parsing: missing/null buckets → null, missing `limits` → empty list. |
| `UsageState` | Threadsafe singleton. A failed poll **never** overwrites the last good snapshot — it only sets `LastError`. |
| `UsageMonitorService` | `BackgroundService`; polls on `usage_poll_interval_seconds` (default 60, clamped to min 15 on config load), one poll at startup. Logs a failure at most once per distinct error message. Broadcasts `HubBroadcaster.UsageUpdated` after **every** tick, success or failure. |
| `UsageMonitorService` | `BackgroundService`; polls on `usage_poll_interval_active_seconds` / `usage_poll_interval_idle_seconds` (default 120 each, clamped to min 90 on config load), one poll at startup. Logs a failure at most once per distinct error message. Broadcasts `HubBroadcaster.UsageUpdated` after **every** tick, success or failure. |
| `UsageSnapshotBuilder` | Builds the Hub-facing `UsageSnapshotDto` from `UsageState` + `IUsageGate` + `AppSettings`. The one shared place for stale/threshold/gate logic — `WorkerHub.GetUsageSnapshot` and `UsageMonitorService` must not diverge. |
| `UsageGate` | Hard pause decision → `UsageGateDecision(IsBlocked, Reason)`. |
| `UsageThrottle` | Pure static staging of parallelism ahead of the gate. |
@@ -254,6 +259,7 @@ degrades instead of throwing. Worker config: `usage_poll_interval_active_seconds
`usage_poll_interval_idle_seconds`.
The gate percentages are editable in **two** places that both write the same `app_settings` row:
Settings → General (typed) and the usage-monitor gauges (dragged). The throttle stages are
Settings → Execution (typed) and the usage-monitor gauges (dragged). The throttle stages are
gauge-only — `SettingsModalViewModel` therefore carries them load→save verbatim so saving Settings
can't reset a dragged value.
can't reset a dragged value. PowerUse (switch, time window, handoff %) used to be usage-monitor-only
and is now typed on the same Settings tab, so it needs no such round-trip.
+63 -26
View File
@@ -15,9 +15,7 @@ Checkliste lebt in `docs/verification-handoff.md`.
- **Planning-aktiver Parent zeigt weiter „Idle":** Parent in `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber `TaskRowViewModel.StatusLabel` (Zeile 132) kennt nur `HasInteractiveSession`/`IsParked` als Overrides — der `PlanningBadge` ersetzt den Chip nicht sichtbar → liest sich wie ein normaler Idle-Task. Wunsch: klarer „Planning/Draft aktiv"-Zustand, der Idle überschreibt. (Kinder zeigen korrekt „Draft".)
- **Blocked-by-Kette nicht sichtbar:** Nach Finalize ist die sequentielle Kette korrekt gesetzt (child[i] blocked-by child[i-1]), aber die UI stellt Reihenfolge/Abhängigkeit nicht dar — es gibt nur den „waiting"-Chip. Wunsch: Kette visualisieren (z.B. „wartet auf <Vorgänger>").
- **Dequeue-„X" fehlt auf wartenden (blockierten) Kettengliedern:** `CanRemoveFromQueue = IsQueued || HasQueuedSubtasks` (TaskRowViewModel.cs:99), `IsQueued` verlangt leeres `blocked_by`. Ein gequeuetes, aber blockiertes Kind (`IsWaiting`) bekommt daher kein Remove-from-queue-X — nur Parent + erstes (entsperrtes) Kind.
- **Session-Skills-Tab hat keinen Empty-State:** Bei 0 installierten Skills zeigt der Skills-Tab (Settings) nur eine nackte leere Fläche unter der Install-Zeile — kein erklärender Hinweis (z.B. „Noch keine Skills installiert — GitHub-URL oben einfügen"). Im `sessionSkillsTab`-Locale-Namespace (en.json:674) gibt es keinen `empty`-Key.
- **Modal-Bodies sind unten abgeschnitten** (Sichtprüfung 2026-08-06, beobachtet in den Listen-Settings: der Hinweistext unter „Verify command" ist mitten in der Zeile vom Footer weggeschnitten und lässt sich nicht herunterscrollen; laut Mika „fast überall" in den Settings-Modals). Gemeinsames Muster: `<ScrollViewer Padding="20,16">` als Modal-Body — in `ListSettingsModalView:36`, `MergeModalView:33`, `WorktreesOverviewModalView:131`, `AboutModalView:20`, `MergeHelperSelectionModal:47`, `RepoImportModalView:45`, `LogVisualizerView:45` sowie `WorkConsole:295/395`. **Vermutete** Ursache (nicht verifiziert): das untere `Padding` des ScrollViewers zählt nicht zum scrollbaren Extent, die letzten Pixel sind also unerreichbar. Erst am echten Fall nachmessen, dann ggf. einheitlich das Padding vom ScrollViewer auf ein `Margin` des inneren Inhalts umziehen.
- **Modal-Bodies unten abgeschnitten — gefixt, Sichtprüfung offen** (Ursache 2026-09-02 verifiziert: `ScrollViewer.Padding` zählt in Avalonia 12 nicht zum Extent; Fix ist das Padding auf ein `Margin` des inneren Inhalts umzuziehen). Betraf `AboutModalView`, `MergeHelperSelectionModal`, `MergeModalView`, `RepoImportModalView`, `WorktreesOverviewModalView` — alle fünf umgestellt. `ListSettingsModalView`, `LogVisualizerView` und `WorkConsole` hatten zu diesem Zeitpunkt bereits kein ScrollViewer-Padding mehr.
- **Usage-Monitor-Modal: Analyse-Tabellen ausbaufähig** (Sichtprüfung 2026-08-06; Gauges, Info-Bänder, Presets/Custom-Range und Refresh-Button sind in Ordnung). Models-Tab: die Spaltenköpfe „OTHER CACHE" und „SHARE" kollidieren zu `OTHER CACHISHARE`; `claude-haiku-4-5-20251001` läuft in die IN-Spalte; alle Zahlen linksbündig und ohne Tausendertrenner (`506830708`). Tasks-Tab: Task-Titel wird ohne Ellipse hart an der LIST-Spalte abgeschnitten, der LIST-Text ebenfalls; MODEL ist bei Runs von vor der `task_runs.model`-Spalte leer (besser „—"). Wunsch: Zahlen rechtsbündig + `#,##0` (oder k/M), Spaltenbreiten/Truncation fixen.
- **Conflict-Resolver: mehrere Konfliktdateien schlecht erkennbar:** Beim 2-Datei-Konflikt schwer zu sehen, dass zwei Dateien betroffen sind (File-Switcher/Anzahl zu unauffällig). Prominentere Datei-Liste / „x von y Dateien".
- **Attachments: erster Drag&Drop der Session schlug einmalig fehl („An error occurred"):** Beim allerersten Drop-to-attach einer UI-Session zeigte die `DropStatus`-Zeile inline einen generischen Fehler und es wurde nichts persistiert (kein File, keine DB-Row); alle folgenden Drops derselben Session + der „Add file…"-Picker + Remove funktionierten fehlerfrei. Nicht reproduzierbar nach dem ersten Mal. Kandidaten: transiente SQLite-Contention (der UI-Prozess schreibt `todo.db` direkt via `new TaskAttachmentRepository`, während der Worker dieselbe DB hält) ODER ein Fehler im Drop-Stream-Pfad (`IStorageFile.OpenReadAsync` im Code-Behind, außerhalb des `try` in `AddFilesAsync`). Falls es erneut auftritt: `AddFilesAsync`/`OnDrop` mit robusterem Error-Logging versehen.
@@ -34,7 +32,7 @@ Der Sichtprüfungs-Block dieses Batches (Ctrl+K/`#`, Titel-Edit, ConPTY-/Refine-
Interactive-Chip, Diff-Viewer-Abstände, Manual-Tasks, Vorgaben-pro-Modell-Tabelle) ist von Mika
verifiziert und deshalb hier entfernt. Es bleiben die Entscheidungen, die daran hängen:
- Interaktive ConPTY-Sessions bekommen `--effort`, aber **kein** `--model` — die Session läuft weiter unter dem Modell aus Mikas Claude-Config, der Effort kommt aus dem Preset des Modells, das ClaudeDo für die Task auflösen würde. Falls ClaudeDo auch interaktiv das Modell erzwingen soll, ist das ein Folge-Task.
- Interaktive (handgesteuerte) ConPTY-Sessions bekommen weder `--model` noch `--effort` — beides kommt aus Mikas Claude-Config bzw. dem CLI-Default. Nur die List-Handler-Sessions pinnen Modell + Effort pro Rolle.
- `AppSettings.DefaultMaxTurns` ist jetzt tatsächlich verdrahtet (`ModelPresets.For(..., global.DefaultMaxTurns)` in `TaskRunner.ResolveConfigAsync`): reiner Fallback für ein Modell, das auch nach `ModelRegistry.TryNormalizeAlias` auf keine Preset-Zeile trifft — vorher war das Feld tot (hartkodierte 30). Hat weiterhin keinen eigenen Editor mehr (nur die Preset-Tabelle pro Modell); Spalte könnte später entfallen, falls das nie zutrifft.
## Beobachtung (offen — Entscheidung Mika)
@@ -46,33 +44,64 @@ verifiziert und deshalb hier entfernt. Es bleiben die Entscheidungen, die daran
## Offene Verifikation (2026-08-27, Ticketsystem-Anbindung)
> **PAUSIERT.** `TicketSettingsTabViewModel.FeatureEnabled = false` versteckt Settings-Tab,
> Projekt-Dropdown und Kontextmenü-Eintrag; zum Reaktivieren die Konstante umlegen. Grund: die
> API ist unter `http://bandelapis.fb-tuning.local` nicht erreichbar (*connection actively
> refused* — nichts lauscht dort). Kein ClaudeDo-Bug und kein CORS; wahrscheinlich fehlt eine
> IIS-Hostname-Bindung oder der Port stimmt nicht (`deploy/deploy-iis-winrm.ps1` der API notiert,
> dass srv-04b diese Sites *by IP:port rather than by hostname* erreicht). Wird mit dem
> API-Kollegen geklärt. Die Punkte unten bleiben stehen, bis die Erreichbarkeit steht.
> **Reaktiviert am 2026-09-01.** `FeatureEnabled = true`. Die API läuft unter
> `http://192.168.45.161` — der Hostname `bandelapis.fb-tuning.local` lehnt die Verbindung
> weiterhin ab, die Base-URL muss also die **IP** sein.
Tasks 1–9 sind implementiert und gemergt (Build + Tests grün, `_broadcaster.TaskUpdated` genau
einmal in `TaskStateService.cs`), aber **kein Smoke-Test gegen die echte Bandel-API ist
gelaufen**. Die fünf Punkte aus der „Manuellen Verifikation" des Specs
(`docs/superpowers/specs/2026-08-27-ticketsystem-integration-design.md`):
Alle vier Routen sind jetzt live gegen die echte API geprüft. Die aus dem `Bandel.APIs`-Quellcode
abgeleiteten Shapes stimmten bis auf zwei Punkte, beide gefixt und mit Tests belegt:
- [ ] PAT in den Settings hinterlegen, „Verbindung testen" → korrekter Benutzername + Scopes.
- [ ] Liste mit Projekt `393 Bandel.LagerApp` verknüpfen, „Update from Ticketsystem" → nur
eigene, offene Tickets erscheinen als Tasks.
- [ ] Einen Task laufen lassen → Ticket steht im Ticketsystem auf `InBearbeitung`.
- `PATCH /api/Ticket/{id}/status` braucht **`statusId` UND `statusName`** — ohne den Namen 400
(„The StatusName field is required."), also hätte *jede* Write-Back fehlgeschlagen. Der Name
wird nicht gegen die Id validiert, muss aber nicht-leer sein (`TicketStatusMap.NameOf`).
- Das flache `assigneeName` im Board-Item ist auf **jedem** Ticket null (alle 63 in Abteilung
Entwicklung geprüft). Der Assignee-Filter des Imports hätte damit immer 0 Tasks erzeugt. Echte
Quelle: `ticketAssignees[].assigneeName`, das exakt das Format von `/pat/me`s `userName` hat.
`personInChargeName` ist die falsche Quelle (mal Login, mal Anzeigename).
Import zieht jetzt `Offen` **und** `InBearbeitung`, weiterhin nur eigene Tickets.
Bleibt manuell zu prüfen (UI-Pfad, nie sichtbar gelaufen):
- [ ] PAT in Settings → Ticketsystem hinterlegen (Base-URL `http://192.168.45.161`),
„Verbindung testen" → `mika.kuns` + 9 Scopes.
- [ ] Liste mit einem Projekt verknüpfen (Rechtsklick → Listeneinstellungen → Ticket-Projekt),
„Update from Ticketsystem" → nur eigene offene/laufende Tickets erscheinen als Tasks.
Achtung: aktuell ist **kein** offenes Ticket in Entwicklung dir zugewiesen — zum Testen
erst im Ticketsystem zuweisen, sonst ist 0 importiert das korrekte Ergebnis.
- [ ] Einen Task laufen lassen → Ticket steht auf `InBearbeitung`.
- [ ] Approve/Merge → Ticket steht auf `Fertig`.
- [ ] Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr.
- [ ] Sichtprüfung des Settings-Tabs + des Ticket-Projekt-Dropdowns (nie gerendert gesehen).
**Wichtig:** die Response-Shapes der Bandel-API (`BandelEnvelope<T>`, `TicketIdentity`,
`TicketBoardItem`, `SidebarDepartment`/`SidebarProject`, `TicketDtos.cs`) wurden aus dem
**Quellcode** von `Bandel.APIs` abgeleitet, **nie live gegen die echte API verifiziert** — es gab
keinen einzigen echten Netzwerkaufruf während der Implementierung. Die Punkte 1–4 oben sind
zugleich der erste Test dieser Annahme: schlägt einer davon mit einem Deserialisierungs- oder
404/Formatfehler fehl, ist das erwartbar und kein Regressions-Bug — dann müssen die DTOs in
`src/ClaudeDo.Worker/Tickets/TicketDtos.cs` an die tatsächliche Antwort angepasst werden.
## Offene Verifikation (2026-10-01, Unit-Branch Teil 3: Firmen-Modus / Draft-PR)
Build + gefilterte Tests grün (Worker: AzureDevOps/CompanyFetchBase/CompanyApprove/CommitMessageBuilder,
Ui: TicketSettingsTab/MergeSectionCompanyRepo, Data: PromptFiles, Localization). **Nie gegen echtes ADO,
nicht visuell geprüft:**
- [ ] Sichtprüfung: Settings-Reiter heißt „Externe Dienste", Abschnitt Azure DevOps (PAT, Test, Löschen);
Detail-Pane im Firmen-Repo: kein Ziel-Dropdown, Button „Draft-PR erstellen" → nach dem Approve
„PR aktualisieren" + Button „Pull Request öffnen"; Approve bleibt hinter „Diff öffnen" gegated.
- [ ] PAT eintragen, „Verbindung testen" — meldet Repo + Default-Branch der ersten Firmen-Liste.
- [ ] Ticket-Task in einem echten Firmen-Repo bis zum Draft-PR: Branch `<typ>/<ticket>-<slug>` startet
von frisch gefetchtem `origin/<basis>`, Commit ohne `ClaudeDo-Task:`-Trailer, PR-Titel
`…, Resolves #<id>`, Beschreibung mit Ticket-Link (Projekt-Id-Lookup über `GET /api/ticket/{id}`
ist nur gegen den Stub getestet) und `Geprüft:`-Zeile, Task bleibt WaitingForReview.
- [ ] Zweiter Approve nach Reject mit Feedback → `pr_updated`, derselbe PR bekommt die neuen Commits.
- [ ] Planning-Unit mit 2 Children im Firmen-Repo → ein PR für die ganze Unit.
- [ ] API-Version-Fallback (Server meldet „supports is X") ist nur gegen den Stub getestet.
## Offene Verifikation (2026-10-01, Ticket-Import über alle Projekte)
Build + Unit-Tests grün, **nicht visuell/live verifiziert**:
- [ ] Sichtprüfung: Button „Tickets + Tag planen" im Prep-Panel (nur sichtbar mit Base-URL + Token),
Fallback-Listen-Dropdown in Settings → Ticketsystem, Radio „Tickets + Tag planen" im Prime-Tab.
- [ ] E2E: Button einmal gegen das echte Ticketsystem klicken — Items landen in sinnvollen Listen, die
Fallback-Liste fängt den Rest, danach ist MyDay gefüllt; ein zweiter Klick importiert nichts Neues.
- [ ] `PATCH /api/Ticket/{id}/status` auf einem **Task** (nicht Ticket) ist ungetestet — nachdem der erste
importierte Task den Status wechselt, im Worker-Log auf eine `TicketStatusSync`-Warnung prüfen.
## Offene Verifikation (2026-07-27)
@@ -273,6 +302,14 @@ Weitere Punkte, gebaut + unit-getestet auf `main`, aber **nicht visuell verifizi
`ClaudeProcess`/`ClaudeCliPreflight` den Shim über `cmd.exe /c` tatsächlich startet, nicht
nur, dass der Check ihn findet).
## Offene Verifikation (2026-10-02, SSH-Workspaces)
- [ ] Sichtprüfung Settings → SSH-Workspaces: Reiter wählt richtig, Zeilen-Layout, Host-Vorschläge aus `~/.ssh/config`, Papierkorb-Hover, Fehlermeldung bei ungültiger Zeile.
- [ ] VPS-Eintrag im Quick-Session-Menü startet eine Kachel; ssh-Prompts (Host-Key, Passphrase) erscheinen in der Kachel.
- [ ] In der Remote-Session zeigt `/mcp` `claudedo` verbunden, `list_task_lists` funktioniert.
- [ ] Kachel schließen baut den Tunnel ab (Port auf der VPS frei), zwei parallele Kacheln auf denselben Host gehen.
- [ ] Worker nach Update neu starten — erst dann lauscht der Remote-Listener auf 47823.
---
## Bewusst verworfen (nicht erneut vorschlagen)
@@ -0,0 +1,147 @@
# Mehrfach-Abhängigkeiten (`depends_on` als Menge) — Implementation Plan
**Goal:** A task can declare *n* predecessors instead of exactly one. The queue picker
claims it only once **all** of them are `Done`.
**Architecture:** Replace the single `tasks.depends_on_task_id` column with a
`task_dependencies(task_id, depends_on_task_id)` relation table (composite PK, both FKs to
`tasks.id`, `ON DELETE CASCADE`). Cascade reproduces today's `ON DELETE SET NULL` semantics
on a join table: deleting a predecessor drops the edge, so the dependent becomes pickable
instead of blocked forever. `BlockedByTaskId` (the internal planning chain) is untouched —
the split stays deliberate.
**Tech Stack:** .NET 8, EF Core (SQLite), SignalR, Avalonia 12, xUnit.
**Not in scope:** `BlockedByTaskId`; rewriting the list-handler prompt to *use* multi-deps;
a graph visualizer.
---
## File Structure
**Create:**
- `src/ClaudeDo.Data/Models/TaskDependencyEntity.cs`
- `src/ClaudeDo.Data/Configuration/TaskDependencyEntityConfiguration.cs`
- `src/ClaudeDo.Data/Migrations/<ts>_AddTaskDependencies.cs` — scaffolded
- `src/ClaudeDo.Worker/External/TaskBlocking.cs` — the shared `blockedReason` builder
(two consumers: `ExternalMcpService`, `TaskWaitMcpTools`)
- `tests/ClaudeDo.Data.Tests/TaskDependencyMigrationTests.cs` — real `Migrate()` on a DB
seeded with legacy `depends_on_task_id` rows
**Modify:**
- `src/ClaudeDo.Data/Models/TaskEntity.cs` — `DependsOnTaskId` → `Dependencies` nav +
`[NotMapped] DependsOnTaskIds`
- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — drop column/FK/index
- `src/ClaudeDo.Data/ClaudeDoDbContext.cs` — `DbSet<TaskDependencyEntity>`
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — `.Include(t => t.Dependencies)` on
the three read paths; `UpdateAsync` must not push the nav graph
- `src/ClaudeDo.Worker/Queue/QueuePicker.cs` — `NOT EXISTS` over the relation table
- `src/ClaudeDo.Worker/State/TaskStateService.cs` — `SetDependsOnAsync` takes a set,
graph-wide cycle check
- `src/ClaudeDo.Worker/State/Interfaces/ITaskStateService.cs`
- `src/ClaudeDo.Worker/External/ExternalMcpService.cs` — DTOs, `add_task`, `update_task`,
`ComputeBlockedInfoAsync`
- `src/ClaudeDo.Worker/External/TaskWaitMcpTools.cs`
- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`,
`TasksIslandViewModel.cs` — display of *n* predecessors
- `src/ClaudeDo.Data/CLAUDE.md`, `src/ClaudeDo.Worker/CLAUDE.md`
- test files listed per task below
---
## Task 1 — Schema + migration
- [ ] `TaskDependencyEntity { string TaskId; string DependsOnTaskId; }`, table
`task_dependencies`, composite PK, both FKs cascade, index on `depends_on_task_id`.
- [ ] `TaskEntity.Dependencies` (`ICollection<TaskDependencyEntity>`), `[NotMapped]
DependsOnTaskIds` projection. `DependsOnTaskId` and its FK/index are removed.
- [ ] Scaffold the migration, then **hand-edit the Up body** so the data copy sits between
create and drop:
`CreateTable` → `INSERT INTO task_dependencies (task_id, depends_on_task_id) SELECT
id, depends_on_task_id FROM tasks WHERE depends_on_task_id IS NOT NULL` →
`DropColumn`. Down reverses it (add column, `UPDATE … FROM` the first edge, drop
table) — lossy for >1 edge, which is inherent and documented in the migration.
- [ ] Verify: `dotnet ef database update` against a temp DB seeded with legacy rows;
`pragma_table_info` diff. **`DropColumn` forces a SQLite table rebuild** — the
`ef-migration-parallel-designer-collision` trap applies: if another migration lands
in parallel, hand-check both `.Designer.cs` snapshots.
**Test:** `TaskDependencyMigrationTests` — migrate to the previous migration, seed a task
with `depends_on_task_id` via raw SQL, migrate to head, assert the edge is in
`task_dependencies` and nothing else changed. (Acceptance #2.)
## Task 2 — Queue gating
- [ ] `QueuePicker` replaces the `depends_on_task_id IS NULL OR EXISTS(...)` clause with
a doubly-negated `NOT EXISTS` over `task_dependencies`, so a dangling edge keeps
blocking exactly as today:
```sql
AND NOT EXISTS (
SELECT 1 FROM task_dependencies td
WHERE td.task_id = t.id
AND NOT EXISTS (SELECT 1 FROM tasks d
WHERE d.id = td.depends_on_task_id AND d.status = 'done')
)
```
**Tests** (`QueuePickerTests`, real SQLite): n=2 both `Done` → claimed; one `Done` one
`Running` → skipped; zero deps → claimed (regression); one dep `Failed` → still skipped,
no cascade (acceptance #6); one dep `Cancelled` → still skipped.
## Task 3 — Write path + cycle detection
- [ ] `ITaskStateService.SetDependsOnAsync(string taskId, IReadOnlyList<string>
dependsOnTaskIds, CancellationToken)` — empty list clears. Rejects self-reference,
unknown id, duplicate handling by de-dup.
- [ ] Cycle check walks the **graph**: load the whole edge set once (it only ever holds
*declared* dependencies, so it stays small — `ponytail:` note, switch to a recursive
CTE if it ever isn't), BFS from every proposed predecessor; reaching `taskId` is a
cycle. Visited set guards pre-existing bad data.
- [ ] Replace rows transactionally (`ExecuteDelete` for `taskId` + `AddRange`), wake the
queue when the set shrank.
**Tests** (`TaskStateServiceTests`): sets n edges; empty list clears + wakes; self-ref
rejected; unknown id rejected; **A→B→C→A rejected** (acceptance #3); adding a second edge
leaves the first intact.
## Task 4 — MCP surface
- [ ] `add_task` / `update_task` keep `dependsOnTaskId` (string) **and** gain
`dependsOnTaskIds` (`string[]?`). Precedence: `dependsOnTaskIds` non-null wins;
otherwise the legacy scalar is treated as a one-element set, with `""` still the
clear sentinel. Both run through `TaskIdResolver` so `#123` keeps working.
- [ ] `TaskDto`/`TaskRefDto`: `DependsOnTaskId` → `DependsOnTaskIds`
(`IReadOnlyList<string>`). Only the *input* form is contractually frozen.
- [ ] `TaskBlocking.Describe` builds one reason naming **every** unmet predecessor
(acceptance #4); `ComputeBlockedInfoAsync` and `TaskWaitMcpTools.CheckOnceAsync`
both use it.
**Tests** (`ExternalMcpServiceTests`, `TaskWaitMcpToolsTests`, `BatchMcpToolsTests`):
legacy scalar still sets + clears the link (acceptance #5, explicit back-compat test);
`dependsOnTaskIds` sets n; cycle via the list form throws; `blockedReason` names both
open predecessors.
## Task 5 — UI
- [ ] `TaskRowViewModel.DependsOnTaskIds`; `DependsOnTaskId` becomes a computed
"single predecessor or null" so the existing linear-chain rail is unchanged for the
1-dependency case.
- [ ] `BuildAfterLabel` joins all resolvable predecessors (`#3, #7`); a row with >1
predecessor gets the label, never a rail (a rail can only render a line).
- [ ] `GroupingKey` folds the joined ids so the reconcile tick still detects a change.
- [ ] `.Include(t => t.Dependencies)` on the `TasksIslandViewModel` query sites that
hydrate rows.
**Tests** (`TasksIslandChainTests`, `TasksIslandClassifyItemsTests`,
`TasksIslandReconcileTickTests`): existing chain tests keep passing via the 1-dep path;
a 2-dep row shows a joined after-label and no rail.
## Task 6 — Docs + verify
- [ ] `src/ClaudeDo.Data/CLAUDE.md` (Models, Schema) and
`src/ClaudeDo.Worker/CLAUDE.md` (Status Model table, picker bullet,
`TaskStateService` bullet) describe the relation table (acceptance #7).
- [ ] VERIFY (acceptance #8): `dotnet build src/ClaudeDo.App -c Release`,
`dotnet build src/ClaudeDo.Worker -c Release`, `dotnet test` for
`ClaudeDo.Data.Tests`, `ClaudeDo.Worker.Tests`, `ClaudeDo.Ui.Tests` (`-c Release`),
report counts.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,928 @@
# Unit-Branch Teil 1: Branch-Naming + Basis-Branch pro Liste — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Task-Branches heißen `<typ>/<nummer>-<slug>` statt `claudedo/{nr}-{slug}`, Firmen-Repos werden über ein konfigurierbares Remote-Muster erkannt, und jede Liste kann einen Basis-Branch (z.B. `dev`) festlegen, der Basis neuer Worktrees und Default-Merge-Ziel ist.
**Architecture:** Erkennung = statischer Helper `CompanyRepo` über `git remote get-url origin` + `WorkerConfig.CompanyRemotePattern`. Naming bleibt in `WorktreeManager` (`NameFor` + `BranchTypeFor`). `list_config.base_branch` ist eine neue nullable Spalte, tri-state durch alle Writer gereicht; `WorktreeManager.ResolveBaseCommitAsync` und `TaskMergeService.ResolveTargetAsync` lesen sie.
**Tech Stack:** .NET 8, EF Core (SQLite), xUnit mit echtem git, Avalonia 12.
**Spec:** `docs/superpowers/specs/2026-10-01-unit-branch-draft-pr-design.md` (Abschnitt 1 + Basis-Branch). Teil 2 (Unit-Branch/Children) und Teil 3 (Firmen-Modus: Fetch, Push, Draft-PR) folgen als eigene Pläne.
**Abweichung von der Spec (bewusst):** Der Self-Heal in `WorktreeManager.CreateAsync` bekommt **keinen** zusätzlichen DB-Ownership-Guard. Er feuert nur, wenn auch der Id-suffixte Name schon existiert — und der enthält die Task-Id, gehört also per Konstruktion diesem Task. Ein fremder Branch mit dem Klarnamen wird schon vom Kollisionscheck auf das Suffix umgelenkt. Nötig ist nur, dass der Kollisionscheck den **vollen neuen** Branchnamen prüft (Task 3).
**Konventionen für jede Task:**
- Build: `dotnet build ClaudeDo.slnx -c Release` (nie Debug — ein laufender Worker lockt es).
- Tests: `dotnet test <projekt> -c Release --no-build --filter "<filter>"` — nie die volle Suite.
- Commit: `git add <pfade>` explizit, dann `git commit -m "..." -- <pfade>` (geteilter Checkout, nie `git add -A`). Kein Co-Authored-Trailer.
---
## Dateiübersicht
| Datei | Änderung |
|---|---|
| `src/ClaudeDo.Worker/Config/WorkerConfig.cs` | + `CompanyRemotePattern` (`company_remote_pattern`, Default `/_git/`) |
| `src/ClaudeDo.Data/Git/GitService.cs` | + `GetRemoteUrlAsync` |
| `src/ClaudeDo.Worker/Git/CompanyRepo.cs` | neu: `Matches`, `IsCompanyAsync` |
| `src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs` | `ToSlug` löst Umlaute auf |
| `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` | `NameFor(task, ticketId, isCompany)`, `BranchTypeFor`, neuer Branchname, Ticket-Auflösung, Basis-Branch |
| `src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs` | Pfad- statt Präfix-Filter |
| `src/ClaudeDo.Data/Models/ListConfigEntity.cs`, `Configuration/ListConfigEntityConfiguration.cs`, `Repositories/ListRepository.cs`, Migration | + `BaseBranch` |
| `src/ClaudeDo.Data/Wire.cs` | `UpdateListConfigDto` / `ListConfigDto` + `BaseBranch` |
| `src/ClaudeDo.Worker/Hub/WorkerHub.cs` | `UpdateListConfig` / `GetListConfig` |
| `src/ClaudeDo.Worker/External/ConfigMcpTools.cs` | erhält `BaseBranch` **und** `PowerUseAllowed` (Bestandsbug), `get_list_config` liefert `baseBranch` |
| `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` | `ResolveTargetAsync`, leeres Ziel zentral in `MergeAsync` aufgelöst |
| `src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs`, `ViewModels/Modals/ListSettingsModalViewModel.cs`, `Views/Modals/ListSettingsModalView.axaml`, `locales/*.json` | Basis-Branch-Feld |
| `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` | echte Branchnamen in Dialogen, kein `← main` |
| `src/ClaudeDo.Worker/CLAUDE.md`, `src/ClaudeDo.Data/CLAUDE.md` | Doku |
---
### Task 1: Firmen-Repo-Erkennung
**Files:**
- Modify: `src/ClaudeDo.Worker/Config/WorkerConfig.cs` (nach `TicketApiBaseUrl`, ~Zeile 71)
- Modify: `src/ClaudeDo.Data/Git/GitService.cs` (neben `ListLocalBranchesAsync`, ~Zeile 404)
- Create: `src/ClaudeDo.Worker/Git/CompanyRepo.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs`
- [ ] **Step 1: Failing test schreiben**
```csharp
using ClaudeDo.Data.Git;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Tests.Infrastructure;
namespace ClaudeDo.Worker.Tests.Git;
public sealed class CompanyRepoTests : IDisposable
{
private readonly GitRepoFixture _repo = new();
public void Dispose() => _repo.Dispose();
[Theory]
[InlineData("https://srv.local/Coll/Proj/_git/Repo", "/_git/", true)]
[InlineData("https://mika@srv.local/tfs/Coll/Proj/_git/Mein%20Repo.git", "/_git/", true)]
[InlineData("ssh://git@git.kuns.dev:22222/kuns/ClaudeDo.git", "/_git/", false)]
[InlineData(null, "/_git/", false)]
[InlineData("https://srv.local/Coll/Proj/_git/Repo", "", false)]
[InlineData("https://GITHUB.com/acme/repo.git", "github.com/acme", true)]
public void Matches(string? url, string pattern, bool expected)
=> Assert.Equal(expected, CompanyRepo.Matches(url, pattern));
[Fact]
public async Task IsCompanyAsync_reads_origin()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var git = new GitService();
Assert.False(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));
GitRepoFixture.RunGit(_repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
Assert.True(await CompanyRepo.IsCompanyAsync(git, _repo.RepoDir, "/_git/", CancellationToken.None));
Assert.Equal("https://srv.local/Coll/Proj/_git/Repo", await git.GetRemoteUrlAsync(_repo.RepoDir));
}
}
```
- [ ] **Step 2: Test laufen lassen — muss am Compiler scheitern**
Run: `dotnet build ClaudeDo.slnx -c Release`
Expected: FAIL, `CompanyRepo` / `GetRemoteUrlAsync` existieren nicht.
- [ ] **Step 3: Implementieren**
`WorkerConfig.cs`, direkt nach der `TicketApiBaseUrl`-Property:
```csharp
/// <summary>Teilstring der <c>origin</c>-URL, an dem ein Firmen-Repo erkannt wird. Nur per Hand editierbar.</summary>
[JsonPropertyName("company_remote_pattern")]
public string CompanyRemotePattern { get; set; } = "/_git/";
```
`GitService.cs`, neben `ListLocalBranchesAsync`:
```csharp
/// <summary>URL des Remotes <paramref name="remote"/>, oder null wenn es keins gibt.</summary>
public async Task<string?> GetRemoteUrlAsync(string repoDir, string remote = "origin", CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["remote", "get-url", remote], ct);
return exitCode == 0 && !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : null;
}
```
`src/ClaudeDo.Worker/Git/CompanyRepo.cs`:
```csharp
using ClaudeDo.Data.Git;
namespace ClaudeDo.Worker.Git;
/// <summary>Erkennt Firmen-Repos am <c>origin</c>-Remote; das Muster kommt aus <c>WorkerConfig.CompanyRemotePattern</c>.</summary>
public static class CompanyRepo
{
/// <summary>True wenn <paramref name="remoteUrl"/> das (nicht-leere) Muster enthält, ohne Groß-/Kleinschreibung.</summary>
public static bool Matches(string? remoteUrl, string? pattern) =>
!string.IsNullOrWhiteSpace(remoteUrl) && !string.IsNullOrWhiteSpace(pattern)
&& remoteUrl.Contains(pattern, StringComparison.OrdinalIgnoreCase);
/// <summary>Liest <c>origin</c> live (kein Cache — ein Remote kann sich jederzeit ändern).</summary>
public static async Task<bool> IsCompanyAsync(GitService git, string workingDir, string? pattern, CancellationToken ct) =>
Matches(await git.GetRemoteUrlAsync(workingDir, "origin", ct), pattern);
}
```
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release` → 0 Fehler.
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CompanyRepoTests"`
Expected: PASS (7 Tests).
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Worker/Config/WorkerConfig.cs src/ClaudeDo.Data/Git/GitService.cs src/ClaudeDo.Worker/Git/CompanyRepo.cs tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs
git commit -m "feat(worker): detect company repos by origin remote pattern" -- src/ClaudeDo.Worker/Config/WorkerConfig.cs src/ClaudeDo.Data/Git/GitService.cs src/ClaudeDo.Worker/Git/CompanyRepo.cs tests/ClaudeDo.Worker.Tests/Git/CompanyRepoTests.cs
```
---
### Task 2: Naming-Bausteine (Typ, Nummer, Umlaute)
**Files:**
- Modify: `src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs:49-60` (`ToSlug`)
- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs:41-47` (`NameFor`) + neues `BranchTypeFor`
- Test: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs:82-89` (Theory ersetzen), `tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs`
- [ ] **Step 1: Tests anpassen/ergänzen**
In `WorktreeManagerTests.cs` die Theory `NameFor_NumberPlusCappedTitleSlug` (Zeilen 82-89) ersetzen durch:
```csharp
[Theory]
[InlineData("Fix: Löschen des Tasks!", null, false, "7-fix-loeschen-des-tasks")]
[InlineData("äöü", null, false, "7-aeoeue")]
[InlineData("a very long title that goes well beyond the forty char cap", null, false, "7-a-very-long-title-that-goes-well-beyond")]
[InlineData("test task", 412, false, "412-test-task")]
[InlineData("test task", 412, true, "412-test-task")]
[InlineData("test task", null, true, "test-task")]
[InlineData("!!!", null, true, "7")]
public void NameFor_NumberRules(string title, int? ticketId, bool isCompany, string expected)
{
var task = new TaskEntity { Id = "x", ListId = "y", Title = title, Number = 7, CreatedAt = DateTime.UtcNow };
Assert.Equal(expected, WorktreeManager.NameFor(task, ticketId, isCompany));
}
[Theory]
[InlineData("feat", "feature")]
[InlineData("fix", "fix")]
[InlineData("refactor", "refactor")]
[InlineData("docs", "docs")]
[InlineData("test", "test")]
[InlineData("chore", "chore")]
[InlineData("ci", "chore")]
[InlineData("perf", "chore")]
[InlineData(" FEAT ", "feature")]
[InlineData("", "chore")]
public void BranchTypeFor_maps_commit_types(string commitType, string expected)
=> Assert.Equal(expected, WorktreeManager.BranchTypeFor(commitType));
```
In `CommitMessageBuilderTests.cs` einen Test ergänzen:
```csharp
[Fact]
public void ToSlug_transliterates_umlauts()
{
Assert.Equal("aerger-ueber-groesse-strasse", CommitMessageBuilder.ToSlug("Ärger über Größe Straße"));
}
```
- [ ] **Step 2: Build — muss scheitern**
Run: `dotnet build ClaudeDo.slnx -c Release`
Expected: FAIL, `NameFor` hat keine 3-Parameter-Überladung, `BranchTypeFor` fehlt.
- [ ] **Step 3: Implementieren**
`CommitMessageBuilder.ToSlug` — erste Zeile ersetzen:
```csharp
// Umlaute auflösen statt strippen: "Löschen" -> "loeschen", nicht "lschen".
var lower = name.ToLowerInvariant()
.Replace("ä", "ae").Replace("ö", "oe").Replace("ü", "ue").Replace("ß", "ss");
```
`WorktreeManager.cs` — `NameFor` (Zeilen 41-47) ersetzen durch:
```csharp
/// <summary>
/// Branch leaf and folder name: <c>{nummer}-{slug}</c>. Nummer = Ticket-Id, sonst die Task-Nummer —
/// außer im Firmen-Repo ohne Ticket, dort sähe sie wie eine Ticket-Id aus und entfällt.
/// </summary>
public static string NameFor(TaskEntity task, int? ticketId, bool isCompanyRepo)
{
var titleSlug = CommitMessageBuilder.ToSlug(task.Title);
if (titleSlug.Length > 40) titleSlug = titleSlug[..40].TrimEnd('-');
var number = ticketId?.ToString() ?? (isCompanyRepo ? null : task.Number.ToString());
if (titleSlug.Length == 0) return number ?? task.Number.ToString();
return number is null ? titleSlug : $"{number}-{titleSlug}";
}
/// <summary>Branch-Präfix nach work-conventions: <c>feat</c> → <c>feature</c>, Typen ohne eigenes Präfix → <c>chore</c>.</summary>
public static string BranchTypeFor(string? commitType)
{
var t = (commitType ?? "").Trim().ToLowerInvariant();
return t switch
{
"feat" => "feature",
"fix" or "refactor" or "docs" or "test" or "chore" => t,
_ => "chore",
};
}
```
In `CreateAsync` vorerst nur den Aufruf kompilierbar machen (Zeile 62): `var name = NameFor(task, null, false);` — die echte Verdrahtung macht Task 3.
Die übrigen `NameFor(task)`-Aufrufe in `WorktreeManagerTests.cs` (Zeilen 131, 137, 160, 314, 320) auf `WorktreeManager.NameFor(task, null, false)` umstellen — sonst kompiliert das Testprojekt nicht. Die `claudedo/`-Literale dort bleiben in diesem Task stehen (Task 3 ändert sie).
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release` → 0 Fehler.
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~CommitMessageBuilderTests"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs
git commit -m "feat(worker): branch type and number rules for task branches" -- src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Runner/CommitMessageBuilderTests.cs
```
---
### Task 3: `CreateAsync` nutzt das neue Schema
**Files:**
- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs:49-90` (`CreateAsync`) + neue private `ResolveTicketIdAsync`
- Test: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs`
- [ ] **Step 1: Tests anpassen + neue Tests**
Bestehende Literale umstellen (der Test-Task hat `CommitType = "chore"`, also Präfix `chore/`):
- Zeile 69: `Assert.StartsWith($"chore/{task.Number}-test-task", ctx.BranchName);`
- Zeile 131: `var foreign = $"chore/{WorktreeManager.NameFor(task, null, false)}";`
- Zeile 138: `Assert.Equal($"chore/{suffixed}", ctx.BranchName);`
- Zeile 160: `var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";`
- Zeile 314: `var branchName = $"chore/{WorktreeManager.NameFor(task, null, false)}";`
Neue Tests in derselben Klasse (vor `MakeEntities`):
```csharp
[Fact]
public async Task CreateAsync_CompanyRepoWithTicket_UsesTicketIdAndFeaturePrefix()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
var (task, list) = MakeEntities(repo.RepoDir);
task.CommitType = "feat";
task.TicketRef = "bandel:412";
var (mgr, _) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
Assert.StartsWith("feature/412-test-task", ctx.BranchName);
}
[Fact]
public async Task CreateAsync_CompanyRepoWithoutTicket_OmitsNumber()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
GitRepoFixture.RunGit(repo.RepoDir, "remote", "add", "origin", "https://srv.local/Coll/Proj/_git/Repo");
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, _) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
Assert.StartsWith("chore/test-task", ctx.BranchName);
Assert.DoesNotContain($"/{task.Number}-", ctx.BranchName);
}
[Fact]
public async Task CreateAsync_ChildWithoutTicket_UsesParentTicket()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (child, list) = MakeEntities(repo.RepoDir);
var parent = new TaskEntity
{
Id = Guid.NewGuid().ToString(), ListId = list.Id, Title = "parent", CommitType = "feat",
TicketRef = "bandel:77", CreatedAt = DateTime.UtcNow,
};
child.ParentTaskId = parent.Id;
var db = new DbFixture();
_dbFixtures.Add(db);
using (var seedCtx = db.CreateContext())
{
await new ListRepository(seedCtx).AddAsync(list);
await new TaskRepository(seedCtx).AddAsync(parent);
await new TaskRepository(seedCtx).AddAsync(child);
}
var mgr = new WorktreeManager(new GitService(), db.CreateFactory(),
new WorkerConfig { WorktreeRootStrategy = "sibling" }, NullLogger<WorktreeManager>.Instance);
var ctx = await mgr.CreateAsync(child, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
Assert.StartsWith("chore/77-test-task", ctx.BranchName);
}
```
- [ ] **Step 2: Test laufen lassen — muss scheitern**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests"`
Expected: FAIL — Branches heißen noch `claudedo/…`.
- [ ] **Step 3: Implementieren**
In `CreateAsync` Zeile 62 (`var name = NameFor(task, null, false);`) ersetzen durch:
```csharp
var isCompany = await CompanyRepo.IsCompanyAsync(_git, workingDir, _cfg.CompanyRemotePattern, ct);
var name = NameFor(task, await ResolveTicketIdAsync(task, ct), isCompany);
var branchType = BranchTypeFor(task.CommitType);
```
Kollisionscheck + Branchname (Zeilen 80-89) ersetzen durch:
```csharp
// Numbers restart after a DB reset, and without the claudedo/ prefix a taken name may also be
// the user's own branch — the self-heal below would force-delete it, the id suffix keeps it untouched.
if (Directory.Exists(Path.Combine(parentDir, name))
|| (await _git.ListLocalBranchesAsync(workingDir, ct)).Contains($"{branchType}/{name}"))
{
var id = task.Id.Replace("-", "");
name = $"{name}-{id[..Math.Min(8, id.Length)]}";
}
var branchName = $"{branchType}/{name}";
```
Neue private Methode (vor `ResolveBaseCommitAsync`):
```csharp
/// <summary>Ticket-Id des Tasks, sonst die seines Parents (Children erben das Ticket ihrer Unit).</summary>
private async Task<int?> ResolveTicketIdAsync(TaskEntity task, CancellationToken ct)
{
var own = TicketStatusMap.ParseBandelTicketId(task.TicketRef);
if (own is not null || task.ParentTaskId is null) return own;
using var ctx = _dbFactory.CreateDbContext();
var parentRef = await ctx.Tasks.AsNoTracking()
.Where(t => t.Id == task.ParentTaskId)
.Select(t => t.TicketRef)
.FirstOrDefaultAsync(ct);
return TicketStatusMap.ParseBandelTicketId(parentRef);
}
```
Usings ergänzen: `using ClaudeDo.Worker.Tickets;` (für `TicketStatusMap`). `ClaudeDo.Worker.Git` ist schon importiert.
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~InteractiveLaunchSpecServiceTests|FullyQualifiedName~TaskRunnerTests"`
Expected: PASS. Schlägt ein Test fehl, der einen `claudedo/{nummer}-…`-Namen **erwartet** (nicht nur als Fixture seedet), auf `chore/…` umstellen und im Bericht nennen.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs
git commit -m "feat(worker): name task branches <type>/<number>-<slug>" -- src/ClaudeDo.Worker/Runner/WorktreeManager.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs
```
---
### Task 4: UntrackedWorktreeRecovery ohne Präfix
**Files:**
- Modify: `src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs`
- Test: `tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs` (bestehende Datei; per `Glob tests/**/UntrackedWorktreeRecoveryTests.cs` finden und dort ergänzen)
- [ ] **Step 1: Failing test**
```csharp
[Theory]
[InlineData(@"C:\src\.claudedo-worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
[InlineData(@"C:\src\.claudedo-worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
[InlineData(@"C:\Users\x\.claudeDo\worktrees\lager-app\412-fix", @"C:\Users\x\.claudeDo\worktrees", true)]
[InlineData(@"C:\Users\x\.claudeDo\worktrees\planning\abc123", @"C:\Users\x\.claudeDo\worktrees", false)]
[InlineData(@"C:\src\my-own-worktree", @"C:\Users\x\.claudeDo\worktrees", false)]
public void IsTaskWorktreePath(string path, string centralRoot, bool expected)
{
if (!OperatingSystem.IsWindows()) return;
Assert.Equal(expected, UntrackedWorktreeRecovery.IsTaskWorktreePath(path, centralRoot));
}
```
- [ ] **Step 2: Build — scheitert** (`IsTaskWorktreePath` fehlt).
Run: `dotnet build ClaudeDo.slnx -c Release` → FAIL.
- [ ] **Step 3: Implementieren**
Konstante `BranchPrefix` löschen. Konstruktor um `WorkerConfig cfg` erweitern (Feld `_cfg`; `using ClaudeDo.Worker.Config;`). Klassen-Summary auf „ClaudeDo-Task-Worktrees (unter `.claudedo-worktrees` oder dem Central-Root, ohne Planning-Sessions)“ ändern.
In `StartAsync` nach dem Laden der Listen den Central-Root bestimmen:
```csharp
var settings = await new AppSettingsRepository(ctx).GetAsync(cancellationToken);
var centralRoot = !string.IsNullOrWhiteSpace(settings.CentralWorktreeRoot)
? settings.CentralWorktreeRoot
: _cfg.CentralWorktreeRoot;
```
Den Filter `claudedoPaths` ersetzen:
```csharp
var claudedoPaths = registered
.Where(w => w.Branch is not null && IsTaskWorktreePath(w.Path, centralRoot))
.Select(w => w.Path);
```
Neue statische Methode (neben `FindUntrackedPaths`):
```csharp
/// <summary>True für einen Pfad unter <c>.claudedo-worktrees</c> oder dem Central-Root, außer Planning-Session-Worktrees.</summary>
public static bool IsTaskWorktreePath(string path, string centralRoot)
{
var sep = Path.DirectorySeparatorChar;
var full = Normalize(path);
string? rest = null;
var central = Normalize(centralRoot);
if (full.StartsWith(central + sep, StringComparison.OrdinalIgnoreCase))
rest = full[(central.Length + 1)..];
else
{
var marker = $"{sep}.claudedo-worktrees{sep}";
var idx = full.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (idx >= 0) rest = full[(idx + marker.Length)..];
}
return rest is not null && !rest.StartsWith($"planning{sep}", StringComparison.OrdinalIgnoreCase);
}
```
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~UntrackedWorktreeRecovery"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs
git commit -m "refactor(worker): detect untracked worktrees by path instead of branch prefix" -- src/ClaudeDo.Worker/Lifecycle/UntrackedWorktreeRecovery.cs tests/ClaudeDo.Worker.Tests/Lifecycle/UntrackedWorktreeRecoveryTests.cs
```
(Pfad der Testdatei an den tatsächlichen Fundort anpassen.)
---
### Task 5: `list_config.base_branch` durch alle Writer
**Files:**
- Modify: `src/ClaudeDo.Data/Models/ListConfigEntity.cs`, `src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs`, `src/ClaudeDo.Data/Repositories/ListRepository.cs:161-180`
- Create: Migration `AddListBaseBranch` (generiert)
- Modify: `src/ClaudeDo.Data/Wire.cs:111,115`
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:577-647`
- Modify: `src/ClaudeDo.Worker/External/ConfigMcpTools.cs:13,53-59,104-139`
- Test: `tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs`
- [ ] **Step 1: Failing tests**
`tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs` — Hub-Aufbau 1:1 aus `tests/ClaudeDo.Worker.Tests/Tickets/ListConfigTicketProjectTests.cs` (gleicher `CreateHub`, `SeedListAsync`, `ReadConfigAsync`):
```csharp
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Tests.Infrastructure;
using Xunit;
namespace ClaudeDo.Worker.Tests.Hub;
/// BaseBranch und PowerUseAllowed sitzen in der SetConfigAsync-kopiert-verbatim-Falle: jeder Writer,
/// der sie nicht durchreicht, setzt sie still zurück.
public sealed class ListConfigBaseBranchTests : IDisposable
{
private readonly DbFixture _db = new();
public void Dispose() => _db.Dispose();
private WorkerHub CreateHub()
{
var factory = _db.CreateFactory();
var broadcaster = new HubBroadcaster(new CapturingHubContext());
var hub = new WorkerHub(
null!, null!, null!, null!, broadcaster, factory,
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
hub.Context = new FakeHubCallerContext();
return hub;
}
private async Task<string> SeedListAsync(string? baseBranch, bool powerUse = false)
{
var listId = Guid.NewGuid().ToString();
await using var ctx = _db.CreateContext();
var repo = new ListRepository(ctx);
await repo.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
await repo.SetConfigAsync(new ListConfigEntity { ListId = listId, BaseBranch = baseBranch, PowerUseAllowed = powerUse });
return listId;
}
private async Task<ListConfigEntity?> ReadConfigAsync(string listId)
{
await using var ctx = _db.CreateContext();
return await new ListRepository(ctx).GetConfigAsync(listId);
}
[Fact]
public async Task set_list_config_preserves_BaseBranch_and_PowerUseAllowed()
{
var listId = await SeedListAsync("dev", powerUse: true);
var factory = _db.CreateFactory();
await using var ctx = factory.CreateDbContext();
var tool = new ConfigMcpTools(new ListRepository(ctx), new TaskRepository(ctx),
new HubBroadcaster(new CapturingHubContext()), factory);
await tool.SetListConfig(listId, model: "opus", cancellationToken: CancellationToken.None);
var cfg = await ReadConfigAsync(listId);
Assert.Equal("dev", cfg!.BaseBranch);
Assert.True(cfg.PowerUseAllowed);
}
[Fact]
public async Task get_list_config_returns_BaseBranch()
{
var listId = await SeedListAsync("dev");
var factory = _db.CreateFactory();
await using var ctx = factory.CreateDbContext();
var tool = new ConfigMcpTools(new ListRepository(ctx), new TaskRepository(ctx),
new HubBroadcaster(new CapturingHubContext()), factory);
var result = await tool.GetListConfig(listId, CancellationToken.None);
Assert.Equal("dev", result.Config!.BaseBranch);
}
[Fact]
public async Task UpdateListConfig_null_keeps_BaseBranch()
{
var listId = await SeedListAsync("dev");
await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null));
Assert.Equal("dev", (await ReadConfigAsync(listId))!.BaseBranch);
}
[Fact]
public async Task UpdateListConfig_empty_clears_BaseBranch()
{
var listId = await SeedListAsync("dev");
await CreateHub().UpdateListConfig(new UpdateListConfigDto(listId, "opus", null, null, BaseBranch: ""));
Assert.Null((await ReadConfigAsync(listId))!.BaseBranch);
}
[Fact]
public async Task UpdateListConfig_sets_BaseBranch_and_GetListConfig_returns_it()
{
var listId = await SeedListAsync(null);
var hub = CreateHub();
await hub.UpdateListConfig(new UpdateListConfigDto(listId, null, null, null, BaseBranch: "dev"));
Assert.Equal("dev", (await hub.GetListConfig(listId))!.BaseBranch);
}
}
```
- [ ] **Step 2: Build — scheitert** (`BaseBranch` fehlt).
- [ ] **Step 3: Implementieren**
`ListConfigEntity.cs`, nach `PowerUseAllowed`:
```csharp
/// <summary>
/// Basis neuer Worktrees und Default-Merge-/PR-Ziel (z.B. <c>dev</c>). Null = ausgecheckter Branch
/// (privat) bzw. Default-Branch des Remotes (Firma). ⚠️ SetConfigAsync kopiert verbatim.
/// </summary>
public string? BaseBranch { get; set; }
```
`ListConfigEntityConfiguration.cs`: `builder.Property(c => c.BaseBranch).HasColumnName("base_branch");`
`ListRepository.SetConfigAsync`, im `else`-Zweig: `existing.BaseBranch = config.BaseBranch;`
Migration erzeugen:
```bash
dotnet ef migrations add AddListBaseBranch --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --configuration Release
```
Erwartet: neue Migration mit genau einem `AddColumn<string>(name: "base_branch", table: "list_config", nullable: true)`. Steht mehr drin (fremde Snapshot-Drift), abbrechen und melden.
`Wire.cs` — Kommentar über `UpdateListConfigDto` um eine Zeile ergänzen: `// BaseBranch: null = keep stored, "" = clear, value = set.` Dann beide Records erweitern:
```csharp
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null, string? BaseBranch = null);
```
```csharp
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false, string? BaseBranch = null);
```
`WorkerHub.UpdateListConfig` — nach der `powerUseAllowed`-Zeile:
```csharp
// Tri-State wie oben: null = behalten, "" = löschen.
var baseBranch = dto.BaseBranch is null ? existing?.BaseBranch : dto.BaseBranch.NullIfBlank();
```
Leer-Prüfung um `&& baseBranch is null` ergänzen, im `new ListConfigEntity { … }` `BaseBranch = baseBranch,` ergänzen. `GetListConfig`: `config.BaseBranch` als letztes Argument an `new ListConfigDto(…)` hängen.
`ConfigMcpTools.cs`:
- Zeile 13: `public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns, string? VerifyCommand, string? BaseBranch = null);`
- `GetListConfig`: `new ListConfigDto(cfg.Model, cfg.SystemPrompt, cfg.AgentPath, cfg.MaxTurns, cfg.VerifyCommand, cfg.BaseBranch)`
- `hasUnrelatedSettings` erweitern:
```csharp
var hasUnrelatedSettings = existing is not null
&& (existing.SessionSkills is not null
|| existing.PermissionMode is not null || existing.TicketProjectId is not null
|| existing.BaseBranch is not null || existing.PowerUseAllowed);
```
- im `SetConfigAsync(new ListConfigEntity { … })` ergänzen: `BaseBranch = existing?.BaseBranch, PowerUseAllowed = existing?.PowerUseAllowed ?? false,`
- `config = allCleared ? null : new ListConfigDto(m, sp, ap, mt, vc, existing?.BaseBranch);`
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ListConfigBaseBranchTests|FullyQualifiedName~ListConfigTicketProjectTests|FullyQualifiedName~ListConfigHubTests|FullyQualifiedName~ExternalMcpToolSchemaTests"`
Run: `dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~Migration"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Migrations src/ClaudeDo.Data/Wire.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs
git commit -m "feat(data): per-list base branch, preserved by every config writer" -- src/ClaudeDo.Data/Models/ListConfigEntity.cs src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs src/ClaudeDo.Data/Repositories/ListRepository.cs src/ClaudeDo.Data/Migrations src/ClaudeDo.Data/Wire.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Worker/External/ConfigMcpTools.cs tests/ClaudeDo.Worker.Tests/Hub/ListConfigBaseBranchTests.cs
```
---
### Task 6: Basis-Branch als Worktree-Basis und Merge-Ziel
**Files:**
- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` (`ResolveBaseCommitAsync`, Fallback-Zeile)
- Modify: `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` (`GetTargetsAsync` ~869, `PreviewAsync` ~910, `ApproveAndMergeAsync` ~1077, `MergeAsync` ~375)
- Test: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs`, `tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs`
- [ ] **Step 1: Failing tests**
`WorktreeManagerTests.cs`:
```csharp
[Fact]
public async Task CreateAsync_BaseBranchSet_BasesOnItEvenWhenAnotherBranchIsCheckedOut()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "dev");
File.WriteAllText(Path.Combine(repo.RepoDir, "dev.txt"), "dev");
GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "dev work");
var devHead = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "elsewhere", repo.BaseCommit);
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, db) = await CreateManagerAsync(task, list);
using (var ctx0 = db.CreateContext())
await new ListRepository(ctx0).SetConfigAsync(new ListConfigEntity { ListId = list.Id, BaseBranch = "dev" });
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
Assert.Equal(devHead, ctx.BaseCommit);
}
```
`TaskMergeServiceTests.cs` — am vorhandenen Setup der Klasse orientieren (Repo + `SeedWorktree`-Helper, wie die übrigen Tests der Datei). Ein Test, der zeigt, dass `GetTargetsAsync` `BaseBranch` als Default liefert, auch wenn ein anderer Branch ausgecheckt ist:
```csharp
[Fact]
public async Task GetTargetsAsync_prefers_list_base_branch()
{
// Setup wie die anderen Tests dieser Datei: Repo, Liste, Task mit Worktree (SeedWorktree).
// Zusätzlich: Branch "dev" anlegen (`git branch dev`), list_config.BaseBranch = "dev" setzen,
// Main-Checkout bleibt auf seinem Default-Branch.
// Assert: targets.DefaultBranch == "dev".
}
```
Der Implementer schreibt diesen Test konkret mit dem Fixture-Setup, das in `TaskMergeServiceTests.cs` schon für `GetTargetsAsync`/Merge-Tests verwendet wird (per `Grep "GetTargetsAsync" tests/ClaudeDo.Worker.Tests` den nächstliegenden bestehenden Test kopieren und um `BaseBranch` ergänzen). Ein zweiter Test: `ApproveAndMergeAsync(taskId, "")` mit `BaseBranch = "dev"` merged auf `dev` (`git log dev --oneline` enthält den Merge-Commit, der vorher ausgecheckte Branch nicht).
- [ ] **Step 2: Tests laufen lassen — scheitern**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~BaseBranch|FullyQualifiedName~prefers_list_base_branch"`
Expected: FAIL.
- [ ] **Step 3: Implementieren**
`WorktreeManager.ResolveBaseCommitAsync` — die letzte Zeile `return await _git.RevParseHeadAsync(workingDir, ct);` ersetzen:
```csharp
using var cfgCtx = _dbFactory.CreateDbContext();
var baseBranch = await cfgCtx.ListConfigs.AsNoTracking()
.Where(c => c.ListId == task.ListId)
.Select(c => c.BaseBranch)
.FirstOrDefaultAsync(ct);
return string.IsNullOrWhiteSpace(baseBranch)
? await _git.RevParseHeadAsync(workingDir, ct)
: await _git.RevParseAsync(workingDir, baseBranch, ct);
```
(Ein nicht existierender Branch wirft aus `RevParseAsync` mit „git rev-parse 'dev' failed“ — gewollt, der Lauf scheitert mit lesbarer Meldung.)
`TaskMergeService` — neue öffentliche Methode (bei `GetTargetsAsync`):
```csharp
/// <summary>Leeres Ziel → <c>list_config.base_branch</c>, sonst der ausgecheckte Branch des Main-Checkouts.</summary>
public async Task<string> ResolveTargetAsync(ListEntity list, string? targetBranch, CancellationToken ct)
{
if (!string.IsNullOrWhiteSpace(targetBranch)) return targetBranch;
using var ctx = _dbFactory.CreateDbContext();
var baseBranch = await ctx.ListConfigs.AsNoTracking()
.Where(c => c.ListId == list.Id)
.Select(c => c.BaseBranch)
.FirstOrDefaultAsync(ct);
return string.IsNullOrWhiteSpace(baseBranch)
? await _git.GetCurrentBranchAsync(list.WorkingDir!, ct)
: baseBranch;
}
```
Aufrufer umstellen:
- `GetTargetsAsync`: `var current = await _git.GetCurrentBranchAsync(...)` → `var current = await ResolveTargetAsync(list, null, ct);`
- `PreviewAsync` (~910) und `ApproveAndMergeAsync` (~1077): die `var target = string.IsNullOrWhiteSpace(targetBranch) ? await _git.GetCurrentBranchAsync(...) : targetBranch;`-Blöcke → `var target = await ResolveTargetAsync(list, targetBranch, ct);`
- `MergeAsync`: direkt nachdem `list.WorkingDir` geprüft ist und **vor** dem `currentBranch`-Vergleich (~Zeile 414) `targetBranch = await ResolveTargetAsync(list, targetBranch, ct);` einfügen. Grund: `PlanningMergeOrchestrator.DrainAsync` reicht ein leeres Ziel (MCP `review_task` ohne `targetBranch`) unverändert durch, was heute in `git checkout ""` → Blocked endet. Mit der zentralen Auflösung ist das für alle Aufrufer behoben.
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~WorktreeManagerTests|FullyQualifiedName~TaskMergeServiceTests|FullyQualifiedName~PlanningMergeOrchestratorTests"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Worker/Runner/WorktreeManager.cs src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
git commit -m "feat(worker): use the list base branch as worktree base and default merge target" -- src/ClaudeDo.Worker/Runner/WorktreeManager.cs src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs tests/ClaudeDo.Worker.Tests/Services/TaskMergeServiceTests.cs
```
---
### Task 7: UI — Basis-Branch-Feld + echte Branchnamen
**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs:216-231`
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs` (Property ~57, Load ~276, Save ~375)
- Modify: `src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml` (nach dem WorkingDir-StackPanel, ~Zeile 125)
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `de.json` (Block `modals.listSettings`, nach `workingDirectoryPlaceholder`)
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:670,933,1223,1385`
- Test: `tests/ClaudeDo.Ui.Tests` — bestehende ListSettingsModal-Tests per `Grep "ListSettingsModalViewModel" tests/ClaudeDo.Ui.Tests` finden
- [ ] **Step 1: Failing test**
In der vorhandenen ListSettingsModal-Testklasse (gleiche Fake-`IWorkerClient`-Mechanik wie die dortigen Save-Tests, die `UpdateListConfigDto` mitschneiden):
```csharp
[Fact]
public async Task Save_sends_BaseBranch_explicitly()
{
// Arrange: VM wie in den Nachbartests laden; BaseBranch = "dev" setzen.
// Act: Save ausführen.
// Assert: der mitgeschnittene UpdateListConfigDto hat BaseBranch == "dev".
// Zweiter Fall: BaseBranch = "" → DTO.BaseBranch == "" (nicht null), damit Leeren wirklich löscht.
}
```
Der Implementer schreibt die beiden Fälle konkret gegen den Fake, den die Nachbartests (z.B. der PowerUseAllowed-Save-Test) verwenden.
- [ ] **Step 2: Build/Test — scheitert** (`BaseBranch` fehlt im VM).
- [ ] **Step 3: Implementieren**
`AgentConfigEditorViewModel.SaveAsync`: Signatur um `string? baseBranch = null` erweitern und im List-Zweig durchreichen:
```csharp
public async System.Threading.Tasks.Task SaveAsync(
string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null, string? baseBranch = null)
```
```csharp
await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed, baseBranch));
```
`ListSettingsModalViewModel`:
```csharp
// Basis neuer Worktrees und Default-Merge-Ziel; leer = ausgecheckter Branch.
[ObservableProperty] private string _baseBranch = "";
```
Load (nach `VerifyCommand = …`): `BaseBranch = cfg?.BaseBranch ?? "";`
Save: `await Agent.SaveAsync(…, PowerUseAllowed, BaseBranch.Trim());` — immer explizit, `""` löscht (das Modal besitzt das Feld).
View, nach dem WorkingDir-StackPanel:
```xml
<StackPanel Spacing="4" IsVisible="{Binding !IsFolder}">
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.baseBranch}"/>
<TextBox Text="{Binding BaseBranch, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="{loc:Tr modals.listSettings.baseBranchPlaceholder}"/>
<TextBlock Text="{loc:Tr modals.listSettings.baseBranchHint}"
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
```
Locales (beide Dateien, gleiche Keys, direkt nach `workingDirectoryPlaceholder`):
en.json:
```json
"baseBranch": "Base branch",
"baseBranchPlaceholder": "(checked-out branch)",
"baseBranchHint": "New task branches start here and approved work merges back here, e.g. dev. Leave empty to use whatever branch is checked out.",
```
de.json:
```json
"baseBranch": "Basis-Branch",
"baseBranchPlaceholder": "(ausgecheckter Branch)",
"baseBranchHint": "Neue Task-Branches starten hier, und freigegebene Arbeit wird hierhin gemergt, z.B. dev. Leer = der gerade ausgecheckte Branch.",
```
Nach dem Editieren beide JSON-Dateien separat parsen (`python -c "import json;json.load(open('src/ClaudeDo.Localization/locales/de.json',encoding='utf-8'))"`), weil ein kaputtes `de.json` vom LocaleStore still geschluckt wird.
`DetailsIslandViewModel`:
- Zeilen 670 und 933: `BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;` → `BranchLine = entity.Worktree?.BranchName;` (das Ziel ist nicht mehr immer `main`).
- Zeilen 1223 und 1385: `var branchName = $"claudedo/{Task.Id.Replace("-", "")}";` → `var branchName = BranchLine ?? "the task branch";`
- [ ] **Step 4: Build + Test**
Run: `dotnet build ClaudeDo.slnx -c Release && dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~ListSettings|FullyQualifiedName~DetailsIsland"`
Run: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release --no-build`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs <ui-testdatei>
git commit -m "feat(ui): base branch field in list settings, real branch names in dialogs" -- <dieselben pfade>
```
**Sichtprüfung offen (Mika):** Feld im Listen-Modal, Branchzeile im Detail-Pane.
---
### Task 8: Doku
**Files:**
- Modify: `src/ClaudeDo.Worker/CLAUDE.md` (Key Components → WorktreeManager; Notes → „Task worktree branches follow…“; External → „Task worktree branches and folders are named…“; Config → `company_remote_pattern`)
- Modify: `src/ClaudeDo.Data/CLAUDE.md` (list_config: `base_branch`)
- [ ] **Step 1: Texte ersetzen**
Überall `claudedo/{number}-{title-slug}` → `<typ>/<nummer>-<slug>` mit der Regel: Typ aus `WorktreeManager.BranchTypeFor` (`feat`→`feature`, sonst `fix/refactor/docs/test/chore`, Rest `chore`); Nummer = Ticket-Id (eigene oder des Parents), sonst Task-Nummer, im Firmen-Repo ohne Ticket keine. Planning bleibt `claudedo/planning/{guid}`.
Config-Abschnitt ergänzen: `` `company_remote_pattern` (`/_git/`) — Teilstring der origin-URL, an dem `CompanyRepo` ein Firmen-Repo erkennt; nur per Hand editierbar. ``
Data/CLAUDE.md: `list_config.base_branch` — Basis neuer Worktrees + Default-Merge-Ziel, tri-state in `UpdateListConfigDto`, ⚠️ verbatim-copy-Falle wie `ticket_project_id`.
- [ ] **Step 2: Commit**
```bash
git add src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Data/CLAUDE.md
git commit -m "docs(worker): document new branch naming and per-list base branch" -- src/ClaudeDo.Worker/CLAUDE.md src/ClaudeDo.Data/CLAUDE.md
```
@@ -0,0 +1,184 @@
# Unit-Branch Teil 2: Children per fast-forward — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Children of a unit start from the unit branch head and are fast-forwarded into it as soon as they finish; a parent is approved like a single task.
**Architecture:** New `UnitBranchService` (Worker/Runner) owns the git side: it creates the planning parent's worktree on "Queue plan" and absorbs a finished child (`merge --ff-only` in the parent worktree, child worktree removed, row `Merged`, `MergeCommit` null). It never touches task status — `TaskRunner` (`FailAsync` on a ff failure) and `TaskMergeService` (approve of an interactive child) own the transitions, so `TaskStateService`/`PlanningChainCoordinator` stay git-free and the DI graph stays acyclic. Legacy units (a `Done` child with an `Active` worktree) keep going through `PlanningMergeOrchestrator`.
**Tech Stack:** .NET 8, EF Core SQLite, git CLI, xUnit (real git + real SQLite in Worker.Tests).
Spec: `docs/superpowers/specs/2026-10-01-unit-branch-draft-pr-design.md`, section 2. Private behaviour only — company mode (push/PR) is part 3.
## File map
| File | Change |
|---|---|
| `src/ClaudeDo.Data/Git/GitService.cs` | + `MergeFastForwardOnlyAsync` |
| `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs` | `SetMergedAsync(…, string? mergeCommit)` |
| `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` | `ResolveBaseCommitAsync`: child → `rev-parse <parentWt.BranchName>` for any parent with an Active worktree |
| `src/ClaudeDo.Worker/Runner/UnitBranchService.cs` | **new** — `EnsureUnitWorktreeAsync`, `AbsorbChildAsync`, `HasUnabsorbedChildrenAsync` |
| `src/ClaudeDo.Worker/Program.cs` | register `UnitBranchService` (+ `Share<>` for the external container is not needed — the MCP side only uses the static helper) |
| `src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs` | `QueuePlanAsync` calls `EnsureUnitWorktreeAsync` |
| `src/ClaudeDo.Worker/Runner/TaskRunner.cs` | `HandleSuccess`: absorb before `CompleteAsync`, ff failure → `MarkFailed` |
| `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` | `ApproveAndMergeAsync`: child of a unit worktree → absorb instead of merge |
| `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, `src/ClaudeDo.Worker/External/ExternalMcpService.cs` | approve routing: orchestrator only for unabsorbed (legacy) children |
| `src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs` | also merges a *planning* parent's own Active worktree (mixed legacy unit) |
| `src/ClaudeDo.Worker/Planning/PlanningAggregator.cs` | combined diff = `unitBase..unitBranch` without `checkout -b` when the parent owns the unit; per-child diff of absorbed children from the commit range |
| `tests/ClaudeDo.Worker.Tests/Runner/UnitBranchTests.cs` | **new** — all unit-branch tests |
| `src/ClaudeDo.Worker/CLAUDE.md`, `docs/explore-notes/review-merge.md` | docs |
---
### Task 1: Git primitive + nullable MergeCommit
**Files:** Modify `src/ClaudeDo.Data/Git/GitService.cs` (after `MergeNoFfAsync`), `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs:39`
- [ ] **Step 1: Add the method**
```csharp
/// <summary><c>git merge --ff-only</c>: moves the checked-out branch to <paramref name="sourceRef"/> or fails without touching it.</summary>
public async Task<(int ExitCode, string Stderr)> MergeFastForwardOnlyAsync(
string worktreePath, string sourceRef, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["merge", "--ff-only", sourceRef], ct);
return (exitCode, stderr);
}
```
- [ ] **Step 2:** `SetMergedAsync(string taskId, string? mergeCommit, …)` — an absorbed child has no merge commit.
- [ ] **Step 3:** Covered by the Task 3 tests (no standalone test for a one-line git wrapper).
### Task 2: Child base = unit head
**Files:** Modify `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` (`ResolveBaseCommitAsync`)
- [ ] **Step 1: Failing test** (`UnitBranchTests.Child_starts_from_unit_head`): planning parent (`PlanningPhase.Finalized`) with a worktree, commit a file in the parent worktree (`git -C <wt> commit`), then `CreateAsync(child)` → `ctx.BaseCommit == rev-parse <parentBranch>`. Fails today because planning children fall through to `HEAD` of the main checkout.
- [ ] **Step 2: Implement**
```csharp
if (task.ParentTaskId is not null)
{
using var ctx = _dbFactory.CreateDbContext();
var parentWt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.ParentTaskId, ct);
if (parentWt is { State: WorktreeState.Active })
return await _git.RevParseAsync(workingDir, parentWt.BranchName, ct);
}
```
- [ ] **Step 3:** run `--filter "FullyQualifiedName~UnitBranchTests|FullyQualifiedName~WorktreeManagerTests"`, commit.
### Task 3: UnitBranchService
**Files:** Create `src/ClaudeDo.Worker/Runner/UnitBranchService.cs`, register in `Program.cs` next to `WorktreeManager`.
```csharp
public sealed record AbsorbResult(bool Applicable, string? Error)
{
public static readonly AbsorbResult NotApplicable = new(false, null);
public static readonly AbsorbResult Absorbed = new(true, null);
public bool Failed => Error is not null;
}
public sealed class UnitBranchService
{
// ctor: GitService, WorktreeManager, IDbContextFactory, HubBroadcaster?, ILogger
/// Planning parent → own worktree = unit branch; no-op if it already has an Active one or the list has no git working dir.
public async Task EnsureUnitWorktreeAsync(string parentId, CancellationToken ct);
/// ff the parent worktree to the child branch; child row Merged (MergeCommit null), child worktree + branch removed.
/// NotApplicable when the child or its parent has no Active worktree. Never changes task status.
public async Task<AbsorbResult> AbsorbChildAsync(string childId, CancellationToken ct);
/// Legacy routing: a Done child whose worktree is still Active was never absorbed.
public static Task<bool> HasUnabsorbedChildrenAsync(ClaudeDoDbContext ctx, string parentId, CancellationToken ct) =>
ctx.Tasks.AnyAsync(t => t.ParentTaskId == parentId && t.Status == TaskStatus.Done
&& t.Worktree != null && t.Worktree.State == WorktreeState.Active, ct);
}
```
Absorb order: ff → parent `UpdateHeadAsync(head, diffstat base..head)` → child `SetMergedAsync(null)` → best-effort `worktree remove --force` + `branch -D` → `WorktreeUpdated` for both. ff failure returns `Error` with a clear message and a `ponytail:` comment (no merge fallback).
- [ ] Tests (real git): two serial children absorbed, child 2 sees child 1's file, parent branch head == child 2 head, child rows `Merged` with null `MergeCommit`, child branches gone; a child forked from the old head after the unit moved → `Failed`, unit branch unchanged.
- [ ] Commit.
### Task 4: Queue plan creates the parent worktree
**Files:** `PlanningChainCoordinator.cs` (optional ctor param `UnitBranchService? unitBranch = null`), `TaskStateServiceBuilder.cs` (optional pass-through).
```csharp
if (_unitBranch is not null) await _unitBranch.EnsureUnitWorktreeAsync(parentTaskId, ct);
return await SetupChainAsync(parentTaskId, enqueue: true, ct);
```
- [ ] Test: finalized planning parent + 1 child in a git list → after `QueuePlanAsync` the parent has an Active worktree row whose base = repo HEAD; a second call keeps the same row.
- [ ] Commit.
### Task 5: TaskRunner absorbs before CompleteAsync
**Files:** `TaskRunner.cs` (optional trailing ctor param `UnitBranchService? unitBranch = null`; in the child branch of `HandleSuccess`):
```csharp
if (_unitBranch is not null)
{
var absorb = await _unitBranch.AbsorbChildAsync(task.Id, CancellationToken.None);
if (absorb.Failed)
{
await MarkFailed(task.Id, task.Number, task.Title, slot, absorb.Error, result.TurnCount);
return;
}
}
```
- [ ] Test: TaskRunner with `FakeClaudeProcess` writing a file into its working dir, two queued children of a unit → both Done, unit head has both files; a child whose base was overtaken → `Failed` with the ff message.
- [ ] Commit.
### Task 6: Interactive child approve absorbs
**Files:** `TaskMergeService.cs` (optional ctor param `UnitBranchService? unitBranch = null`); in `ApproveAndMergeAsync` after the `WaitingForReview` guard:
```csharp
if (task.ParentTaskId is not null && _unitBranch is not null)
{
var absorb = await _unitBranch.AbsorbChildAsync(taskId, ct);
if (absorb.Failed) return Blocked(absorb.Error!);
if (absorb.Applicable)
{
var approved = await _state.ApproveReviewAsync(taskId, ct);
return approved.Ok ? new MergeResult(StatusMerged, Array.Empty<string>(), null) : Blocked(approved.Reason ?? "approve failed");
}
}
```
A blocked approve keeps the child `WaitingForReview` (the reviewer can reject/re-run) — the `Failed` rule applies to autonomous runs.
- [ ] Test: child in `WaitingForReview` with a commit → approve → child Done, unit branch contains the file, main untouched.
- [ ] Commit.
### Task 7: Approve routing
**Files:** `WorkerHub.ApproveReview`, `ExternalMcpService.ReviewTask`, `PlanningMergeOrchestrator.StartAsync`.
- Replace `hasChildren` with `UnitBranchService.HasUnabsorbedChildrenAsync` for the orchestrator branch (MCP still computes `emptyChildren` whenever children exist; absorbed children count as empty when their row has no `HeadCommit`).
- Orchestrator: `if (parentHasWorktree) idsToMerge.Add(parentTaskId)` (drop `!isPlanning`) so a mixed legacy unit still lands the unit branch.
- [ ] Tests: approve of a new parent (all children absorbed) merges the unit branch onto `main` and the parent goes Done; a legacy parent (Done child with Active worktree, no parent worktree) still goes through the orchestrator and lands the child.
- [ ] Commit.
### Task 8: Combined diff without the integration branch
**Files:** `PlanningAggregator.cs`.
- `BuildIntegrationBranchAsync`: parent with Active worktree and no unabsorbed child → `Ok(parentWt.BranchName, git diff <BaseCommit> <BranchName>)`, no checkout in the main checkout. Otherwise legacy path.
- `GetAggregatedDiffAsync`: a node whose worktree directory is gone but has `HeadCommit` → `GetCommitRangeDiffAsync(repoDir, BaseCommit, HeadCommit)`; an improvement parent's own node is limited to `BaseCommit..firstChild.BaseCommit` once children were forked from it (else it would repeat the absorbed children).
- [ ] Test: unit with an absorbed child → combined diff contains the child's file, main checkout stays on `main`, no `planning/*-integration` branch exists.
- [ ] Commit.
### Task 9: Docs
`src/ClaudeDo.Worker/CLAUDE.md` (Planning Flow, Unified parent model, WorktreeManager base rule), `docs/explore-notes/review-merge.md` (Approve section, MergeCommit/revert note, Unit-Branch section). Commit.
## Verification
`dotnet build ClaudeDo.slnx -c Release`, then
`dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~UnitBranch|FullyQualifiedName~WorktreeManager|FullyQualifiedName~PlanningMerge|FullyQualifiedName~PlanningAggregator|FullyQualifiedName~StandaloneChildren|FullyQualifiedName~TaskMergeService|FullyQualifiedName~ReviewTask"`.
@@ -0,0 +1,112 @@
# Unit-Branch Teil 3: Firmen-Modus (Fetch, Push, Draft-PR) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** In a company repo (origin matches `company_remote_pattern`) a unit root starts from a freshly fetched `origin/<basis>`, and Approve ends in a pushed branch plus an Azure DevOps **draft PR** instead of a local merge.
**Architecture:** `AzureDevOpsClient` (port of `pr-create.ps1`) is the only HTTP surface to ADO; it also builds the git auth environment (PAT as `http.extraHeader` via `GIT_CONFIG_COUNT/KEY_0/VALUE_0`, never on the command line). `WorktreeManager.ResolveBaseCommitAsync` fetches the basis for unit roots in company repos. `TaskMergeService.ApproveAndMergeAsync` branches on `CompanyRepo.IsCompanyAsync` into `ApproveCompanyAsync`: verify in the unit-root worktree → `PullRequestPublisher.PublishAsync` (push, find-or-create draft PR, store `worktrees.pull_request_url`, `WorktreeUpdated`). The task stays `WaitingForReview`; result status `pr_created` / `pr_updated`. Every failure is a `blocked` result with a precise reason (hub → `HubException`, MCP → tool error, UI → footer strip).
**Tech Stack:** .NET 8, EF Core SQLite, git CLI, xUnit (real git + SQLite), Avalonia.
Spec: `docs/superpowers/specs/2026-10-01-unit-branch-draft-pr-design.md`, sections 3–5, decision rows "Basis-Branch" / "Basis aktualisieren", section 7.
**Live finding (ticket system, 2026-10-01):** `ParentId` of a ticket is its project (`ItemType 0`, `ParentTicketID` null); a task's parent is the ticket. Project id = walk `GET /api/ticket/{id}` up until `parentTicketID` is null (max 3 hops). Ticket web URL: `https://ticket.fb-tuning.local/board/project/<project>/ticket/<id>`.
## File map
| File | Change |
|---|---|
| `src/ClaudeDo.Data/Models/WorktreeEntity.cs`, `Configuration/WorktreeEntityConfiguration.cs`, `Migrations/*AddWorktreePullRequestUrl*` | `PullRequestUrl` → `worktrees.pull_request_url` |
| `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs` | `SetPullRequestUrlAsync` |
| `src/ClaudeDo.Data/Git/GitService.cs` | env-aware `RunGitAsync`; `FetchBranchAsync`, `PushBranchAsync`, `FastForwardLocalBranchAsync`, `GetRemoteHeadBranchAsync` |
| `src/ClaudeDo.Worker/Runner/CommitMessageBuilder.cs`, `WorktreeManager.cs` | trailer off in company repos; fetch basis for unit roots |
| `src/ClaudeDo.Worker/AzureDevOps/AzureDevOpsClient.cs` | **new** — remote parsing, auth env, default branch, find-or-create draft PR, api-version fallback |
| `src/ClaudeDo.Worker/AzureDevOps/PullRequestPublisher.cs` | **new** — push + PR + URL + ticket link |
| `src/ClaudeDo.Worker/DpapiTokenStore.cs`, `Program.cs` | `AzureDevOpsPatStore` (`~/.claudeDo/azdo.pat`), DI |
| `src/ClaudeDo.Worker/Lifecycle/TaskMergeService.cs` | `ApproveCompanyAsync`, `StatusPrCreated/Updated`, `MergeResult.PullRequestUrl`, `MergeTargets.IsCompanyRepo` |
| `src/ClaudeDo.Worker/Hub/WorkerHub.cs`, `External/ExternalMcpService.cs` | ADO settings methods; PR URL in results |
| `src/ClaudeDo.Data/Wire.cs` | `AzureDevOpsSettingsDto`, `AzureDevOpsConnectionDto`, `MergeResultDto.PullRequestUrl`, `MergeTargetsDto.IsCompanyRepo` |
| `src/ClaudeDo.Ui/...` | IWorkerClient/WorkerClient, settings tab "Externe Dienste", detail-pane PR button/link |
| `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs` | new members |
| `src/ClaudeDo.Data/PromptFiles.cs` | company-repo paragraph in the merge prompt (merge + merge_final share it) |
| `tests/ClaudeDo.Worker.Tests/AzureDevOps/*` | **new** — client, fetch basis, company approve |
| docs | Worker/CLAUDE.md, review-merge.md, open.md |
---
### Task 1: `worktrees.pull_request_url`
- [ ] `WorktreeEntity.PullRequestUrl` (`string?`), mapped to `pull_request_url`.
- [ ] `dotnet ef migrations add AddWorktreePullRequestUrl` (project `src/ClaudeDo.Data`). Check the generated Designer contains `base_branch` (parent `AddListBaseBranch`) — see trap `ef-migration-parallel-designer-collision`.
- [ ] `WorktreeRepository.SetPullRequestUrlAsync(taskId, url, ct)` via `ExecuteUpdateAsync`.
- [ ] Covered by Task 5 tests.
### Task 2: Git primitives + trailer
- [ ] `RunGitAsync(..., IReadOnlyDictionary<string,string>? env = null)`; network ops always set `GIT_TERMINAL_PROMPT=0`.
- [ ] `FetchBranchAsync(repoDir, branch, env, ct)` → `git fetch origin +refs/heads/<b>:refs/remotes/origin/<b>` (explicit refspec: works without a configured fetch refspec); returns `(ExitCode, Stderr)`.
- [ ] `FastForwardLocalBranchAsync(repoDir, branch, ct)` → `git fetch . refs/remotes/origin/<b>:refs/heads/<b>` (local, no network; git refuses a non-fast-forward itself).
- [ ] `PushBranchAsync(worktreePath, branch, env, ct)` → `git push -u origin <b>`.
- [ ] `GetRemoteHeadBranchAsync(repoDir, ct)` → `symbolic-ref --short refs/remotes/origin/HEAD` minus `origin/`, or null.
- [ ] `CommitMessageBuilder.Build(..., bool includeTrailer = true)`; `WorktreeManager.CommitIfChangedAsync` passes `!isCompany`. Test in `CommitMessageBuilderTests`: no `ClaudeDo-Task:` line when false.
### Task 3: `AzureDevOpsClient` + PAT store
- [ ] `AzureDevOpsPatStore(DpapiTokenStore)` marker, registered with `InAppData("azdo.pat")`.
- [ ] `AzureDevOpsClient(IHttpClientFactory, AzureDevOpsPatStore)`:
- `static AzdoRepo? ParseRemote(string remote)` — trim, strip `.git`, strip `user[:pw]@`, split at `/_git/`; `Repo` unescaped.
- `static IReadOnlyDictionary<string,string> GitAuthEnv(string? pat)` — `GIT_TERMINAL_PROMPT=0` + (pat ? `GIT_CONFIG_COUNT=1`, `GIT_CONFIG_KEY_0=http.extraHeader`, `GIT_CONFIG_VALUE_0=Authorization: Basic <b64(":pat")>`).
- `static string? SupportedVersion(string error)` — `supports is (\d+\.\d+)`.
- `GetDefaultBranchAsync(repo, ct)` → `GET …/_apis/git/repositories/<repo>` → `defaultBranch` without `refs/heads/`.
- `EnsureDraftPullRequestAsync(repo, source, target, title, description, ct)` → `GET …/pullrequests?searchCriteria.sourceRefName=refs/heads/<s>&searchCriteria.status=active`; first hit is reused, else `POST …/pullrequests` with `isDraft: true`. Returns `(Url, Created)`, URL `<webUrl>/pullrequest/<id>`.
- api version starts at `6.0`, one retry with the version the error names (remembered).
- errors → `AzureDevOpsException` with status code + server message; no PAT → `AzureDevOpsException("Kein Azure-DevOps-PAT hinterlegt …")`.
- [ ] Tests (`AzureDevOpsClientTests`, `StubHandler` from Tickets): the two `-SelfTest` remotes + non-ADO remote, auth header `Basic OmdlaGVpbQ==`, version parse (match / foreign error), reuse of an active PR (no POST), create with `isDraft` + refs, version fallback retry.
### Task 4: Fetch the basis before a unit root
`WorktreeManager.ResolveBaseCommitAsync`, unit-root branch only, when `CompanyRepo.IsCompanyAsync`:
1. basis = `base_branch` → ADO `defaultBranch` (if the remote parses and a PAT is set) → `GetRemoteHeadBranchAsync`. None → today's behaviour.
2. `FetchBranchAsync(basis, GitAuthEnv(pat))`. Failure → warning, base = local `basis` (else HEAD). No throw.
3. Success → base = `rev-parse origin/<basis>`; then best-effort local update: not checked out → `FastForwardLocalBranchAsync` (only if the local branch exists); checked out + clean (`HasChangesAsync(includeUntracked:false)`) → `MergeFastForwardOnlyAsync(workingDir, origin/<basis>)`; otherwise warning only.
- [ ] Tests (`CompanyFetchBaseTests`, bare repo as origin, pattern = bare path): bare one commit ahead → unit root's `BaseCommit` == bare head, local `main` fast-forwarded; dirty checked-out `main` stays untouched (file content + HEAD unchanged) but the root still starts from origin; unreachable origin → worktree created from local `main`; private repo (pattern mismatch) → no fetch (origin ahead is ignored).
### Task 5: Company approve
- [ ] `TaskMergeService`: `StatusPrCreated = "pr_created"`, `StatusPrUpdated = "pr_updated"`; `MergeResult(..., string? PullRequestUrl = null)`; `MergeTargets(..., bool IsCompanyRepo = false)` filled in `GetTargetsAsync`.
- [ ] `ApproveAndMergeAsync`: after the child-absorb block, if `wt` is Active and the repo is a company repo → `ApproveCompanyAsync` (ignores `skipVerify`):
- verify via `RunVerifyGateAsync(verifyCommand, wt.Path, …)` with `PhaseVerifying` broadcast; red → `verify_failed`, nothing pushed.
- `PullRequestPublisher.PublishAsync(task, list, wt, verified, ct)` wrapped in `ProgressReporter.RunAsync` ("push + draft PR running").
- [ ] `PullRequestPublisher` (`GitService`, `AzureDevOpsClient`, `TicketClientFactory?`, db factory, broadcaster, logger):
- no PAT → blocked; remote does not parse → blocked.
- push with `GitAuthEnv`; rejected → blocked `push rejected: <stderr>`.
- target = `base_branch` else ADO default branch.
- title `<type>(<list-slug>): <title>[, Resolves #<ticket>]`; description `Resolves [#id](<url>)` (link only with a project id) + task description + `Geprüft: <cmd> grün` / `Geprüft: nichts automatisch`.
- PR error after push → blocked `gepusht, PR fehlgeschlagen: …`.
- success → `SetPullRequestUrlAsync`, `WorktreeUpdated`, `pr_created`/`pr_updated` with URL. Task status and worktree state untouched.
- [ ] Hub `ApproveReview`: `MergeResultDto(..., r.PullRequestUrl)`. MCP `review_task`: `mergeMessage` = PR URL for `pr_*`; tool description mentions it.
- [ ] Tests (`CompanyApproveTests`; origin URL `http://ado.test/coll/proj/_git/Repo` + `url.<bare>.pushInsteadOf` so the push lands in a local bare repo while the remote still parses as ADO; HTTP stubbed): creates draft PR (branch in bare, URL stored, task still WaitingForReview, worktree Active, title/description), reuses existing PR → `pr_updated` without POST, verify red → no push, PR error after push → blocked "gepusht, PR fehlgeschlagen", `skipVerify` ignored.
### Task 6: Hub + wire + UI client
- [ ] Wire: `AzureDevOpsSettingsDto(bool TokenSet)`, `AzureDevOpsConnectionDto(bool Ok, string? Repo, string? DefaultBranch, string? Error)`, `MergeResultDto.PullRequestUrl`, `MergeTargetsDto.IsCompanyRepo`.
- [ ] Hub: `GetAzureDevOpsSettings`, `SetAzureDevOpsToken`, `ClearAzureDevOpsToken`, `TestAzureDevOpsConnection` (first list whose origin matches the pattern and parses → `GetDefaultBranchAsync`; every error is an answer, not a throw).
- [ ] `IWorkerClient`/`WorkerClient` + `StubWorkerClient`.
### Task 7: UI
- [ ] Settings: tab header "Externe Dienste"/"External services"; second section Azure DevOps (PAT field, test, clear) in `TicketSettingsTabViewModel` (saved together with the ticket settings).
- [ ] Detail pane: `Merge.IsCompanyRepo` (from targets) and `Merge.PullRequestUrl` (from the worktree row); target dropdown hidden in company repos; approve label "Draft-PR erstellen" / "PR aktualisieren"; PR link button (`ShellOpen.Url`); approve errors in company mode go to the footer strip (`ErrorReported`) instead of a modal; still gated behind "open diff".
- [ ] Locales en + de in parity, both parsed separately.
### Task 8: Merge-helper prompt
- [ ] Company-repo paragraph in `MergeHelperMergeDefault`: approve returns `pr_created`/`pr_updated` + URL, task stays WaitingForReview, no `verify_merges`, summary lists PR links.
### Task 9: Docs
- [ ] `src/ClaudeDo.Worker/CLAUDE.md` (tickets/config section, new Azure DevOps section), `docs/explore-notes/review-merge.md`, `docs/open.md` (manual smoke: PAT + test button, ticket task to draft PR, planning unit with 2 children).
## Verification
`dotnet build ClaudeDo.slnx -c Release`; tests filtered: `FullyQualifiedName~AzureDevOps|FullyQualifiedName~CompanyFetchBase|FullyQualifiedName~CompanyApprove|FullyQualifiedName~CommitMessageBuilder|FullyQualifiedName~UnitBranch|FullyQualifiedName~WorktreeManager` (Worker), Localization.Tests, Ui.Tests filtered to settings/details.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
# Ticket-Import über alle Projekte + Tagesplanung — Design
**Datum:** 2026-10-01 · **Status:** abgenommen
## Ziel
Ein Lauf holt **alle** Mika zugewiesenen, offenen Ticket-Items (Tickets, Tasks, Subtasks) aus dem
Bandel-Ticketsystem, lässt Claude jedes Item nach **Inhalt** der passenden ClaudeDo-Liste zuordnen
und plant danach den Tag (bestehender FillMyDay-Lauf). Auslösbar per Button und als Prime-Schedule.
## Warum nicht der bestehende Import
`TicketImportService.ImportAsync` arbeitet pro Liste über die feste Verknüpfung
`ListConfigEntity.TicketProjectId` und nur für Board-Items vom Typ Ticket. Live-Stand 2026-10-01:
- 6 von 10 offenen, zugewiesenen Items sind Tasks/Subtasks (z.B. 595/596/598/668 unter
„Lager Technik Plugin“) — der heutige Import sieht sie nie.
- Projekt „Lager“ (493) enthält Items für mehrere Repos (Hub Lagertechnik, SorterController) und
ist mit zwei Listen verknüpft. Projekt→Liste ist die falsche Zuordnungsachse; Inhalt ist die
richtige.
- DueDate ist fast überall null, Priorität fast überall „Niedrig“ — was heute dran ist, entscheidet
weiterhin FillMyDay, nicht die Ticketdaten.
## Entscheidungen
| Frage | Entscheidung |
|---|---|
| Zuordnung | Claude nach Inhalt (Titel, Parent, Projekt). Projekt-Verknüpfung einer Liste ist nur ein Hinweis. |
| Granularität | Flach: jedes zugewiesene Ticket/Task/Subtask wird ein eigener ClaudeDo-Task. Parent steht in der Beschreibung. Keine Hierarchie. |
| Ablauf | Ein Lauf, zwei Schritte: Zuordnungs-Lauf, danach FillMyDay — im selben `PrimeRunner`-Gate. |
| Kein Treffer | Item geht in die **Fallback-Liste** (App-Setting). Signal für Mika: dafür fehlt eine Liste. Ist keine gesetzt → übersprungen und in der Zusammenfassung genannt. |
| Fallback anwenden | Der **Worker**, nicht Claude: `import_ticket` ohne `list` → Fallback-Liste. |
## Komponenten
### 1. Datenquelle (`Worker/Tickets/`)
**Sweep über alle Projekt-Boards** — nur Routen, die `BandelTicketClient` schon hat und die seit
2026-09-01 live verifiziert sind: `GET /api/Navigation/sidebar` (alle sichtbaren Projekte) →
pro Projekt `GET /api/Board/project/{id}`. Filter, `statusId ∈ {1, 2}`:
- Ticket (Board-Item): `ticketAssignees[].assigneeName == userName`
- Task (`tasks[]` am Board-Item): **`ticketTaskAssignees[].assigneeName`** == userName —
unabhängig davon, wem das Parent-Ticket gehört (730/732 hängen an 728 von marcel.sievers).
Projekt-Titel kommt aus der Sidebar, Parent aus dem umschließenden Board-Item.
`TicketBoardItem` bekommt `Tasks` (`id`, `title`, `statusId`, `hasChildren`,
`ticketTaskAssignees`).
**Live-Befund 2026-10-01** (7 Projekte, ~1,2 s): exakt die 10 offenen Items, die das Ticket-MCP
`list_assigned_items` liefert — 665, 593 (+595/596/598/668), 1014, 1016, 1017, 730, 732.
Verworfen wurde `GET /api/board/global/{userId}`: liefert kein Projekt, braucht eine userId, die
`/pat/me` nicht hergibt (`/users/username/{u}` = 401 mit PAT), und ist **nicht** auf den
Token-Inhaber beschränkt (`global/22` lieferte 147 fremde Items).
Subtasks (Ebene 3): live existiert keiner (`hasChildren` überall false). Board-Tasks mit
`hasChildren = true` werden übersprungen.
<!-- ponytail: keine Rekursion über GET /api/item/{id}/children — nachrüsten, sobald Subtasks real vorkommen -->
**Offen, manuell zu prüfen:** `PATCH /api/Ticket/{id}/status` auf einem **Task** (nicht Ticket) —
nicht live getestet, weil es ein Schreibzugriff aufs Firmensystem ist. Laut Bandel.APIs-Breadcrumb
gleiche Tabelle, Statusrecht u.a. für Assignees. Scheitert es, loggt `TicketStatusSync` nur eine
Warnung (wirft nie) → beim ersten echten Lauf im Worker-Log prüfen.
Details pro Item für die Beschreibung: `GET /api/ticket/{id}` (live: liefert `description`,
`parentTicketID`).
### 2. `TicketImportService`
- `GetCandidatesAsync(ct)` → zugewiesene offene Items, deren `bandel:<id>` noch **in keiner Liste**
als `TicketRef` existiert, plus alle User-Listen (Id, Name, WorkingDir, verknüpftes
Ticket-Projekt) als Zuordnungskontext.
- `ImportItemAsync(itemId, listId?, ct)` → lädt Details via `GET /api/ticket/{id}` (Beschreibung,
HTML→Markdown über das bestehende `HtmlToMarkdown`), legt einen `Idle`-Task an:
`TicketRef = bandel:<id>`, `CreatedBy = "ticketsystem"`, Beschreibung endet mit
`Ticket #<id>` und bei Kind-Items `Teil von #<parentId> <parentTitle>`.
`listId == null` → `AppSettings.TicketFallbackListId`; ist die auch null → kein Task, Ergebnis
meldet „übersprungen“. Bereits importiert (global) → No-op mit entsprechender Meldung.
- **Fix im bestehenden `ImportAsync`:** Dedupe über `TicketRef` **listenübergreifend** statt nur
`t.ListId == listId` — sonst legt „Aus Ticketsystem aktualisieren“ ein bereits global
zugeordnetes Item ein zweites Mal an.
### 3. MCP-Tools (External-MCP, neue `External/TicketMcpTools`)
- `get_ticket_import_candidates()` → `{ available, items[], lists[], fallbackList }`.
`available = false` mit Grund, wenn das Ticketsystem nicht eingerichtet ist.
- `import_ticket(ticketId, list = null)` → Bestätigungs-Record (`created`, `taskNumber`,
`listName`, `usedFallback`, `skippedReason`). `list` akzeptiert Name oder Id.
Konventionen aus `src/ClaudeDo.Worker/CLAUDE.md` gelten (C#-Default für optionale Parameter,
kein nacktes `Task`-Return, Tool-Doku nach `McpToolDocs`).
### 4. Ablauf (`Worker/Prime/`)
- `PrimeActionKind.ImportTickets`, DB-Wert `import_tickets` (Converter in
`PrimeScheduleEntityConfiguration`).
- `TicketImportPrompt` (analog `DailyPrepPrompt`): Prompt über `PromptFiles`
(`PromptKind.TicketImport`), Args `--allowedTools` nur die zwei Tools, `--permission-mode
acceptEdits`. Prompt-Kern: Kandidaten holen, jedes Item der inhaltlich passenden Liste zuordnen
(Projekt-Verknüpfung = Hinweis), bei Unsicherheit `list` weglassen, am Ende Zusammenfassung
(importiert pro Liste, Fallback, übersprungen).
- `PrimeRunner.RunImportTicketsAsync`: Log einmal leeren, `PrepStarted`, Zuordnungs-Lauf, dann den
FillMyDay-Teil (bestehende Logik ohne erneutes Leeren/`PrepStarted`), `PrepFinished`. Schlägt
der Import fehl, läuft FillMyDay trotzdem (bestehende Tasks sind weiter planbar); das Outcome
meldet den Importfehler.
- Ist das Ticketsystem nicht eingerichtet, fällt der Lauf auf reines FillMyDay zurück.
### 5. Hub + UI
- `WorkerHub.RunTicketImportNow()` (synthetischer Schedule, Kind fix `ImportTickets`, wie
`RunDailyPrepNow`); `IWorkerClient`/`WorkerClient` + Test-Fakes nachziehen.
- Prep-Panel (`DetailsIslandView`, neben „Plan day“): Button „Tickets + Tag planen“, nur sichtbar
wenn das Ticketsystem eingerichtet ist; gleiche Enable-Logik wie `PlanDayCommand`.
- Prime-Tab: neuer Aktionstyp wählbar.
- Settings → Ticketsystem: Dropdown „Fallback-Liste“ (User-Listen + „keine“).
- `AppSettingsEntity.TicketFallbackListId` (nullable) + EF-Migration. Locale-Keys en + de.
## Tests
- `GetCandidatesAsync`: Status-Filter, Assignee-Filter über `ticketAssignees`, globale Dedupe.
- `ImportItemAsync`: explizite Liste, Fallback, keine Fallback-Liste → übersprungen, Duplikat.
- `ImportAsync`: Item bereits in anderer Liste → nicht erneut angelegt.
- `PrimeRunner`: `ImportTickets` → zwei Claude-Läufe in Reihenfolge, ein Gate, Import-Fehler
blockiert FillMyDay nicht.
- Fake-Client/Fake-Claude, **kein** echtes Claude. Live-Verifikation (Schritt 1) und
Sichtprüfung der UI sind manuelle Punkte → `docs/open.md`.
## Bewusst nicht drin
- Rückabgleich „Ticket im Ticketsystem erledigt, Task in ClaudeDo noch offen“.
- Ticket-Hierarchie als ClaudeDo-Parent/Subtasks.
- Aktualisieren bereits importierter Items (würde Notizen überschreiben — gleiche Regel wie heute).
@@ -0,0 +1,249 @@
# Unit-Branch + Draft-PR — Design
**Datum:** 2026-10-01 · **Status:** abgenommen (mit Basis-Branch pro Liste + Fetch)
**Diagramme:** Obsidian `07 - Plattform & Technik/ClaudeDo/ClaudeDo - Branch und PR Ablauf.md`
**Ersetzt:** die offene Entscheidung aus `2026-08-06-planning-chain-fork-base-design.md`
## Ziel
Aufgaben starten und später zu fertigen PRs zurückkommen. In Firmen-Repos (Azure DevOps Server)
heißen Branches nach dem work-conventions-Schema, und das Review endet in einem **Draft-PR** statt
in einem lokalen Merge auf `main`. Nebenbei sehen Planning-Children endlich die Arbeit ihres
Vorgängers.
## Begriffe
- **Unit:** ein einzelner Task, oder ein Parent (Planning oder Improvement) samt Children.
- **Unit-Branch:** der eine Branch, auf dem die ganze Arbeit einer Unit landet. Er gehört dem
Worktree des Unit-Roots (Einzeltask bzw. Parent).
- **Firmen-Repo:** Die `origin`-URL enthält `company_remote_pattern` (`worker.config.json`,
Default `/_git/`, nur per Hand editierbar). Geprüft wird live über `git remote get-url origin`
und ohne Cache.
## Entscheidungen
| Frage | Entscheidung |
|---|---|
| Branchname | Einheitlich `<typ>/<nummer>-<slug>`. Die Nummer ist die Ticket-Id aus `TicketRef` (beim Child die des Unit-Roots), sonst privat die Task-Nummer. Im Firmen-Repo ohne Ticket entfällt sie (`<typ>/<slug>`). `claudedo/{nr}-…` fällt weg. Planning-Session-Worktrees bleiben `claudedo/planning/{guid}`. |
| Typ | `feat`→`feature`. `fix`, `refactor`, `docs`, `test`, `chore` bleiben, wie sie sind. Alles andere (`ci`, `perf`, `style`, `build`) wird `chore`. |
| Slug | `CommitMessageBuilder.ToSlug` plus Umlaut-Auflösung (`ä→ae`, `ö→oe`, `ü→ue`, `ß→ss`), ≤40 Zeichen wie heute. |
| Children | Starten vom aktuellen Head des Unit-Branches. Ist ein Child fertig, wird der Unit-Branch sofort per **fast-forward** auf den Head des Childs gezogen. Das gilt privat und in Firmen-Repos. |
| Review-Ende privat | Unverändert: Approve merged den Unit-Branch auf `main`, dann läuft Verify auf `main`. |
| Review-Ende Firma | Approve führt Verify im Worktree des Unit-Roots aus, dann Push und Draft-PR. Der Task bleibt `WaitingForReview` mit PR-Link. `Done` setzt Mika per Toggle nach dem ADO-Merge. |
| PR-Automatik | Kein Auto-PR bei einem Statuswechsel. Automatisiert wird über den Merge-Helper, dessen `approve` dann den PR erzeugt. |
| ADO-Zugang | Eigener PAT (Scope Code Read & Write). Er wird per `DpapiTokenStore` in `~/.claudeDo/azdo.pat` gespeichert, ohne Abhängigkeit auf das work-conventions-Plugin. |
| dependsOn über Units | Wartet bewusst auf den echten Merge. Abhängige Arbeit gehört in eine Unit mit einem PR. |
| Basis-Branch | Pro Liste einstellbar (`list_config.base_branch`, z.B. `dev` für die LagerApp). Leer heißt: privat der gerade ausgecheckte Branch des Main-Checkouts (wie heute), Firma der Default-Branch des ADO-Repos. Gilt als Basis des Unit-Branches, als PR-Ziel und als Vorauswahl des privaten Merge-Ziels. |
| Basis aktualisieren | Firmen-Repo: `git fetch origin <basis>`, bevor der Unit-Branch angelegt wird. Der Unit-Branch startet von `origin/<basis>`, der lokale Basis-Branch wird best-effort per fast-forward nachgezogen. |
## 1. Branch-Naming (`WorktreeManager`)
- `NameFor(task, ticketId, isCompany)` liefert den Leaf `<nummer>-<slug>` bzw. `<slug>`, und
`BranchFor` liefert `<typ>/<leaf>`. Der Ordnername des Worktrees bleibt der Leaf.
- **Kollisionsprüfung** (heute `Contains($"claudedo/{name}")`, Zeile 83) prüft den neuen vollen
Branchnamen. Bei einem Treffer kommt das Id-Suffix dran, wie heute.
- **Self-Heal** (`git branch -D` nach „already exists“) läuft nur noch, wenn der Branch in einer
`worktrees`-Zeile steht, deren Task der aktuelle Task ist. Sonst gibt es keinen Delete, sondern
das Id-Suffix, und der Branch des Nutzers bleibt unangetastet. Ohne das Präfix `claudedo/` ist
das Pflicht, sonst löscht ClaudeDo einen fremden `feature/…`-Branch.
- **`UntrackedWorktreeRecovery`** filtert statt nach dem Präfix nach „Worktree-Pfad liegt unter
einer ClaudeDo-Worktree-Root (`.claudedo-worktrees` bzw. Central-Root), aber nicht unter
`planning/`“.
- `PlanningAggregator.CleanupIntegrationBranchAsync:137` (Präfix-Check beim Checkout-Fallback)
wird mit Abschnitt 3 ohnehin hinfällig.
- Bestehende Worktrees behalten ihren Namen. Der Branchname steht in der DB und wird nie neu
abgeleitet, eine Migration ist also nicht nötig.
- **Bugfix nebenbei:** `DetailsIslandViewModel.cs:1223/1385` baut für Dialogtexte
`claudedo/{Task.Id}`, das ist schon heute falsch. Künftig steht dort `worktree.BranchName`. Das
literale `"← main"` (`:670/:933`) zeigt das echte Ziel an.
## 2. Unit-Branch und Children
**Wer besitzt den Unit-Branch?**
- **Einzeltask:** sein eigener Worktree. Am Ablauf ändert sich nichts.
- **Improvement-Parent:** sein eigener Worktree, den er schon heute hat. Seine Children starten
schon heute von `parent.HeadCommit`.
- **Planning-Parent:** hat heute **keinen** Worktree. Neu: `QueuePlanAsync` legt per
`WorktreeManager.CreateAsync` einen Worktree für den Parent an, auf dem Unit-Branch und mit
`main` HEAD als Basis. Damit hat jeder Parent eine `worktrees`-Zeile, die den Unit-Branch und
später die PR-URL trägt.
**Basis eines Childs** (`ResolveBaseCommitAsync`): Das ist immer der aktuelle Head des
Unit-Branches (`rev-parse <unitBranch>`). Der Sonderfall „nur bei `PlanningPhase.None`“ fällt weg.
**Basis eines Unit-Roots** (Einzeltask, Parent):
- **Privat:** `list_config.base_branch`, sonst der ausgecheckte Branch des Main-Checkouts. Heute
ist es `RevParseHeadAsync`. Ist `base_branch` gesetzt, wird es `rev-parse <base_branch>`, auch
wenn gerade ein anderer Branch ausgecheckt ist.
- **Firma:**
- Zuerst `git fetch origin <basis>`. `<basis>` ist `base_branch`, sonst der Default-Branch des
Repos per ADO-API, ersatzweise `git symbolic-ref refs/remotes/origin/HEAD`.
- Der Unit-Branch startet von `origin/<basis>`.
- Dann wird der lokale `<basis>` best-effort nachgezogen:
- nicht ausgecheckt: `git fetch origin <basis>:<basis>` (lehnt einen nicht-fast-forward
selbst ab)
- ausgecheckt und sauber: `git -C <workingDir> merge --ff-only origin/<basis>`
- schmutzig oder divergiert: Warnung ins Log, kein Eingriff in den Checkout
- Schlägt der Fetch fehl (offline, keine Credentials), wird gewarnt und es geht mit dem
lokalen `<basis>` weiter. Der Task scheitert daran nicht.
- Der Fetch nutzt dieselbe PAT-Übergabe wie der Push (Abschnitt 3).
- Der Fetch läuft einmal pro Unit-Root, nicht pro Child. Children starten vom Unit-Branch.
**Spalte `list_config.base_branch`** (nullable string, Migration):
- Sie wird im Listeneinstellungs-Modal gepflegt, als Freitext mit Vorschlägen aus
`ListLocalBranchesAsync`, und in `get_list_config` mitgeliefert.
- ⚠️ `ListRepository.SetConfigAsync` kopiert verbatim (siehe `TicketProjectId`). Jeder Schreiber
muss den Wert durchreichen, sonst setzt ihn z.B. `set_list_config` still zurück:
`WorkerHub.UpdateListConfig`, `ConfigMcpTools`, alle Stellen, die eine frische
`ListConfigEntity` bauen.
- `UpdateListConfigDto` braucht ihn tri-state wie `TicketProjectId`, damit ein Aufrufer, der das
Feld nicht kennt, es nicht löscht.
- **Merge-Ziel privat:** `TaskMergeService.GetTargetsAsync` setzt `DefaultBranch` auf
`base_branch`, falls gesetzt. Das leere Ziel (`ApproveAndMergeAsync:1077`, `PreviewAsync:910`)
löst ebenfalls auf `base_branch` auf, sonst wie heute.
**Absorbieren eines Childs:** Neu ist `UnitBranchService.AbsorbChildAsync(childId)`, das in
`git -C <parentWorktree> merge --ff-only <childBranch>` mündet.
- Die Kette ist seriell (`BlockedByTaskId`), und das Child startet vom Head des Unit-Branches.
Deshalb ist es **immer** ein fast-forward, Konflikte sind ausgeschlossen. Lässt es sich nicht
fast-forwarden, z.B. weil jemand ein Child manuell parallel per „Run now“ gestartet hat, wird
das Child `Failed` mit einer klaren Meldung, ohne Merge-Versuch.
`ponytail: kein Merge-Fallback; einbauen, falls parallele Children ein echter Fall werden.`
- Danach wird der Worktree des Childs entfernt, der Branch gelöscht und die Zeile auf `Merged`
gesetzt (`MergeCommit` = null, denn es gibt keinen Merge-Commit). `revert_merge` greift auf
Child-Ebene deshalb nicht, revertiert wird die ganze Unit.
- Aufgerufen wird es aus `TaskRunner.HandleSuccess` direkt vor `_state.CompleteAsync`. Ein
ff-Fehler wird so zu `FailAsync` statt zu einem Unblock des Nachfolgers.
- Interaktive Children landen in `WaitingForReview`, und ihr Approve geht durch
`TaskMergeService.ApproveAndMergeAsync`. Dort gilt neu: Hat der Task einen Parent mit eigenem
Worktree, wird `AbsorbChildAsync` aufgerufen statt auf das Ziel gemergt.
- Kein Git-Code in `TaskStateService` oder `PlanningChainCoordinator`, die bleiben git-frei.
**Was dabei wegfällt:** Den Unit-Merge Child für Child am Ende gibt es für neue Units nicht mehr.
Ein Parent wird beim Approve wie ein Einzeltask behandelt, über seinen eigenen Worktree in
`TaskMergeService.ApproveAndMergeAsync`. Auch der Integration-Branch
(`PlanningAggregator`, `planning/{slug}-integration`, dafür `checkout -b` im Main-Checkout) ist
für neue Units unnötig, denn der kombinierte Diff ist `unitBase..unitHead`.
**Routing beim Approve** (`WorkerHub.ApproveReview`, `ExternalMcpService.ReviewTask`): Heute
geht jeder Task mit Children an den Orchestrator. Neu gilt: Gibt es ein `Done`-Child mit einem
`Active`-Worktree, also eins, das nicht absorbiert wurde, läuft der Legacy-Orchestrator. Sonst
geht es über `ApproveAndMergeAsync` auf den Worktree des Parents.
**Altbestand:** Parents ohne eigenen Worktree (vor dem Rollout geplant) laufen weiter über
`PlanningMergeOrchestrator` und den bestehenden Aggregator. Diesen Legacy-Pfad entfernen wir in
einem Folge-Task, sobald keine solchen Units mehr offen sind.
## 3. Approve im Firmen-Repo
`TaskMergeService.ApproveAndMergeAsync` verzweigt nach `IsCompanyRepo(list)`:
1. **Verify** im Worktree des Unit-Roots (`IVerifyCommandRunner.RunAsync(worktreePath, …)`), nicht
im Main-Checkout. Ohne Verify-Command entfällt der Schritt. Rot heißt: Ergebnis
`verify_failed`, der Task bleibt `WaitingForReview`, kein Push. `skipVerify` wird im
Firmen-Modus ignoriert, weil kein Merge landet, auf den man das Verify verschieben könnte.
2. **Push** mit `git -C <worktree> push -u origin <unitBranch>`. Der PAT geht als
`http.extraHeader` über `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_0`/`GIT_CONFIG_VALUE_0` in der
Prozess-Umgebung mit. Er steht also nicht in der Kommandozeile, und es gibt keinen
Credential-Manager-Prompt im Hintergrundprozess.
3. **PR**, neuer `AzureDevOpsClient`, portiert aus `pr-create.ps1`:
- Der Remote wird an `/_git/` zerlegt: API-Basis = alles davor, Repo = Segment danach.
Eingebettete Zugangsdaten fliegen raus.
- `GET …/pullrequests?searchCriteria.sourceRefName=refs/heads/<branch>&searchCriteria.status=active`.
Gibt es schon einen PR, wird er wiederverwendet (idempotent, auch wenn der Skill in der
Session schon einen angelegt hat).
- Sonst `POST …/pullrequests` mit `isDraft: true` und `targetRefName = <basis>`. Das ist
`list_config.base_branch`, sonst `defaultBranch` aus `GET …/repositories/<repo>`.
- Die API-Version fällt auf die zurück, die der Server in seiner Fehlermeldung nennt, wie im
Skript.
4. Die **PR-URL** wird gespeichert, in der neuen nullable Spalte `worktrees.pull_request_url`
(Migration). Danach folgen `WorktreeUpdated` und der Rückgabestatus `pr_created` bzw.
`pr_updated`.
5. Der Task bleibt `WaitingForReview`, der Worktree bleibt `Active`, und das Ticket bleibt auf
`InBearbeitung`.
**PR-Inhalt:**
- **Titel:** `<commitType>(<list-slug>): <titel>, Resolves #<ticket>`. Ohne Ticket entfällt
`Resolves`.
- **Beschreibung:**
- `Resolves [#412](<ticket-url>)`. Die Projekt-Id kommt über `BandelTicketClient.GetTicketAsync`
→ `ParentTicketID` (bei Tasks eine Ebene höher). Ohne Id gibt es keinen Link.
- Leerzeile, dann die Task-Beschreibung.
- Leerzeile, dann `Geprüft: <verify command> grün` bzw. `Geprüft: nichts automatisch`.
**Commits im Firmen-Repo:** `CommitMessageBuilder` lässt den Trailer `ClaudeDo-Task:` weg. Er
wird nirgends gelesen, und die Konvention verbietet Signatur-Trailer.
**Rerun nach Änderungswunsch:** Ein normaler Reject mit Feedback, der auf demselben Worktree
weiterläuft. Der nächste Approve pusht nach, und der vorhandene PR aktualisiert sich.
**Fehler:** Fehlt der PAT, wird der Push abgelehnt oder scheitert ein REST-Aufruf, dann landen
Statusmeldung und Footer-Strip (`FlashFooterError`) beim Nutzer, und der Task bleibt
`WaitingForReview`. Scheitert der PR nach einem erfolgreichen Push, sagt die Meldung das genau
so: „gepusht, PR fehlgeschlagen: …“.
**Laufzeit:** Verify plus Push können länger als 5 s dauern. Der MCP-Pfad meldet deshalb Progress
über `ProgressReporter`, sonst bricht der MCP-Client nach 300 s Leerlauf ab.
## 4. Merge-Helper
Die Prompts in `PromptFiles` (merge und merge_final) bekommen einen Firmen-Repo-Absatz:
- `review_task approve` liefert `pr_created`/`pr_updated` mit URL.
- `verify_merges` entfällt, weil das Verify bereits im Approve läuft.
- Die Abschluss-Zusammenfassung listet die PR-Links auf.
- Die Regel „erst mergen, dann neu starten“ bleibt harmlos, da `main` sich nicht bewegt.
## 5. UI
- Der Settings-Reiter „Ticketsystem“ heißt künftig **„Externe Dienste“**. Er bekommt einen
zweiten Abschnitt **Azure DevOps** mit PAT-Feld, Test-Button und Löschen-Button, gespiegelt vom
Ticket-PAT.
- Neue Hub-Methoden: `GetAzureDevOpsSettings`, `SetAzureDevOpsToken`, `ClearAzureDevOpsToken`,
`TestAzureDevOpsConnection`. Neuer DI-Marker `AzureDevOpsPatStore`.
- Der Test braucht ein Firmen-Repo als Kontext: Er nimmt die erste Liste, deren `origin` passt,
und ruft `GET …/repositories/<repo>` auf.
- Im Detail-Pane heißt der Approve-Button im Firmen-Repo **„Draft-PR erstellen“**. Er ist weiter
hinter „Diff öffnen“ gegated, und das Ziel-Dropdown ist ausgeblendet. Ist eine PR-URL gesetzt,
erscheint ein Link und der Button heißt „PR aktualisieren“.
- Locale-Keys kommen in `en.json` und `de.json` gleichzeitig hinzu.
## 6. Bewusst draußen
- PR-Status-Polling bzw. automatisches `Done` nach dem ADO-Merge.
- Andere PR-Provider als Azure DevOps. `company_remote_pattern` schaltet nur das Schema um, die
PR-API ist ADO-spezifisch.
- Aufräumen von Worktree und lokalem Branch, wenn ein Firmen-Task auf `Done` gesetzt wird. Das
geht über die bestehende Worktree-Übersicht.
- `git fetch` in privaten Repos. Die Basis bleibt dort lokal.
- Eine UI für `company_remote_pattern`.
## 7. Tests
Keine Tests mit der echten Claude-CLI und keine gegen das echte ADO.
- **Naming:** Typ-Mapping, Nummernregel (Ticket, privat, Firma ohne Ticket), Umlaute,
Kollision mit einem fremden Branch (der Branch überlebt, das Suffix greift), Self-Heal löscht
nur DB-eigene Branches. Die betroffenen Asserts in `WorktreeManagerTests` (`:69`, `:131/138`,
`:160`, `:314`) passe ich an.
- **Unit-Branch** (echtes git, Worker.Tests):
- Ein Child startet vom Head des Unit-Branches.
- Zwei serielle Children werden per fast-forward absorbiert, und Child 2 sieht die Dateien von
Child 1.
- Ein nicht fast-forwardbares Child wird `Failed`.
- Ein Planning-Parent bekommt seinen Worktree bei `QueuePlanAsync`.
- **Approve Firma:**
- Ein lokales Bare-Repo dient als `origin`, und das Muster wird auf dessen Pfad gesetzt.
- `AzureDevOpsClient` wird gefakt: PR anlegen, vorhandenen PR wiederverwenden, Verify rot
heißt kein Push, PR-Fehler nach dem Push.
- **Remote-Parsing:** die Fälle aus dem `-SelfTest` des Skripts.
- **Basis:**
- `base_branch` gesetzt: Der Unit-Root startet davon, auch wenn ein anderer Branch ausgecheckt
ist.
- Firma: Nach dem Fetch startet der Unit-Root von `origin/<basis>`. Das testet ein Bare-Repo,
das einen Commit voraus ist.
- Der lokale Basis-Branch wird nachgezogen, ein schmutziger Checkout bleibt unangetastet.
- Ein Fetch-Fehler bricht den Task nicht.
- `SetConfigAsync`, `set_list_config` und `UpdateListConfig` erhalten `base_branch`.
- **Manueller Smoke-Test (Mika):** PAT eintragen, Test-Button. Ein Ticket-Task in einem echten
Firmen-Repo bis zum Draft-PR durchspielen, danach eine Planning-Unit mit 2 Children.
@@ -0,0 +1,104 @@
# SSH workspaces — remote interactive sessions
**Date:** 2026-10-02 · **Status:** approved (inline, Mika)
## Goal
Start an interactive `claude` session on a server (e.g. the VPS) as a Mission Control tile,
with the `claudedo` MCP reachable from there. One extra entry per workspace in the
quick-session flyout ("VPS"), click → tile → real claude TUI over ssh.
## Scope
In:
- SSH workspaces configured in Settings: **Name, Host alias, Remote path**.
- Host alias is picked from `%USERPROFILE%\.ssh\config` (`Host` lines, wildcards skipped);
user/key/port stay in the ssh config — ClaudeDo stores none of it.
- Quick-session flyout lists each workspace after the local targets.
- The tile runs `ssh.exe` with a reverse tunnel to the worker's external MCP for the
lifetime of the ssh process; the remote claude gets a `claudedo` MCP entry pointing at it.
- Per-session token on a dedicated remote-only MCP listener.
Out (v1):
- Autonomous tasks on a remote host — the whole worktree/merge/verify pipeline is local-FS.
- `handoff_session` and "Recent sessions"/resume for SSH tiles — both build local paths
(brief.md, cwd). SSH sessions are **not recorded** in `mission_control_sessions` and get
**no** `claudedo_pane` entry / handoff hint.
- Path translation in MCP tool results (worktree/attachment paths stay Windows paths).
- Password/key management UI — ssh prompts inside the tile like in a terminal.
## Design
### Storage
`AppSettingsEntity.SshWorkspaces` (`ssh_workspaces`) — JSON array of
`{ id, name, host, remotePath }`. Same pattern as `RepoImportFolders`: dedicated repository
`Get/SetSshWorkspacesAsync` + hub `GetSshWorkspaces`/`SetSshWorkspaces`, **not** part of
`AppSettingsDto` (`AppSettingsRepository.UpdateAsync` copies named fields only, so the column is
never clobbered by a general settings save). One EF migration adds the column.
Validation at the boundary (`SshWorkspace.Validate`, used by Settings before save and by the hub on
set): name non-empty; host matches `^[A-Za-z0-9._@][A-Za-z0-9._@-]*$` — **a leading `-` would be
parsed by ssh as an option (`-oProxyCommand=…` = local code execution)**; remote path starts with
`/` or `~`, contains no `'` and no line break (it is single-quoted on the remote shell).
### Launch spec (Worker)
New hub method `GetSshLaunchSpec(string workspaceId, string paneId)` →
`InteractiveLaunchSpecService.BuildForSshAsync`:
- `Exe` = `ssh` resolved on PATH (Windows OpenSSH); clear error if missing.
- `Cwd` = local user profile (ssh needs a local cwd; nothing remote reads it).
- Remote port: random per session in `20000–60999`.
- Token: random 32 bytes (hex), registered in a singleton `RemoteSessionTokenRegistry`.
- Args (each a separate token, **no embedded double quotes** — ConPTY flattens Args into one
Windows command line, see `docs/explore-notes/conpty-sessions.md`):
```
-t -o ExitOnForwardFailure=yes
-R <remotePort>:127.0.0.1:<RemoteMcpPort>
<hostAlias>
umask 077 && mkdir -p ~/.claudedo && echo <b64> | base64 -d > ~/.claudedo/mcp-<paneId>.json
&& cd '<remotePath>' && MCP_TOOL_TIMEOUT=<ms> claude --mcp-config ~/.claudedo/mcp-<paneId>.json;
rm -f ~/.claudedo/mcp-<paneId>.json
```
`<b64>` = base64 of `{"mcpServers":{"claudedo":{"type":"http","url":"http://127.0.0.1:<remotePort>/mcp","headers":{"X-ClaudeDo-Remote-Token":"<token>"}}}}`.
Base64 has no shell- or argv-sensitive characters; the file (mode 600 via umask) keeps the
JSON out of claude's own argv.
- The server name is `claudedo` (not `claudedo_pane`) so tool names match local sessions.
- Remote prerequisites: POSIX shell with `base64`, `claude` on PATH and logged in.
### Remote-only MCP listener (security)
The external MCP is loopback-only and keyless by default (`ExternalMcpAuthMiddleware`).
Tunnel traffic arrives as loopback, so exposing the existing port would hand every process
on the server that can reach the forwarded port full control of the local ClaudeDo.
- New `WorkerConfig.RemoteMcpPort` (default `47823`, `0` = feature off). The external MCP app
additionally binds `127.0.0.1:<RemoteMcpPort>`.
- `ExternalMcpAuthMiddleware`: a request whose `Connection.LocalPort == RemoteMcpPort`
must carry a valid `X-ClaudeDo-Remote-Token` (registry lookup) → else 401. The existing
key/loopback rule applies unchanged to the normal port.
- Tunnels always target `RemoteMcpPort`, never `ExternalMcpPort`.
Known ceilings (accepted):
- The remote command line (incl. base64 → token) is visible to other users on the server via
`ps`/`/proc`. Protects against processes/containers that reach the port without reading
cmdlines; fine for a single-user VPS. Upgrade path: deliver the token via stdin before the TUI.
- Tokens live in memory until worker restart (no revoke on tile close). A leaked token only
works while some tunnel to that port is open.
### UI
- **Settings:** new "SSH" category (Erweitert group, last tab) — list of workspaces with add/remove,
name textbox, host combobox (from ssh config, editable for aliases not in the file),
remote path textbox. Saves via `UpdateAppSettingsAsync`.
- **Quick-session flyout:** `RefreshSessionTargetsAsync` appends one `SessionTargetViewModel`
per workspace (label = name, icon/suffix marks it as remote) →
`OpenSshConPtySessionAsync(workspace)` → new `ConPtyPaneViewModel` with title = name, spec
via `GetSshLaunchSpecAsync`.
- Localization keys in en + de.
## Testing
- `RemoteSessionTokenRegistry` + middleware: remote port without/with wrong/with valid token;
normal port unaffected.
- `BuildForSshAsync`: arg list shape, no `"` in any arg, base64 decodes to the expected JSON,
path validation rejects `'` / relative paths, unknown workspace id throws.
- ssh-config host parsing (wildcards, multiple aliases per `Host` line, comments).
- Manual: real VPS — tile opens, `/mcp` shows `claudedo` connected, `list_task_lists` works,
closing the tile tears the tunnel down; second tile to the same host works in parallel.
+161
View File
@@ -0,0 +1,161 @@
# Sichtprüfungs-Session 2026-09-16
Build: Debug 2.15.0 (App 08:05, Worker 09:40 — enthält den uncommitteten `WorktreeUpdated`-Fix).
Sandkasten: Liste **ClaudeDoTests** → `C:\TestRepos\ClaudeDoTests` (sauber, alle Alt-Tasks Done).
Vorgeseedet: **#334** (README.md) und **#335** (docs/notes.md) — laufen, landen in `WaitingForReview`.
Abgedeckt wird alles seit dem letzten Sichtprüfungs-Pass (06.08.), also v2.13.x–v2.15.
**Bewusst draußen:** Installer/SystemCheckPage (braucht echte Installation) und die
Ticketsystem-Anbindung (braucht ein zugewiesenes Ticket) — beide aus `docs/open.md`.
Notation: `[ ]` offen · `[x]` ok · `[!]` Fehler (Text dahinter = Fix-Kandidat)
---
## Pass 1 — Sidebar: Listen & Ordner (~8 min) · **ERLEDIGT** — 1.1–1.8 ok bis auf 1.2 (→ F1)
> 1.7 verhält sich gut: Ordner löschen schiebt die enthaltene Liste raus statt sie mitzunehmen.
- [ ] **1.1** Rechtsklick in die Listen-Sidebar → „Neue Liste": im Modal den Ordner-Toggle
einschalten → es entsteht ein **Ordner**, nicht eine Liste.
- [ ] **1.2** Ordner anklicken → klappt auf/zu, Pfeil dreht sich, Zustand bleibt nach
Listenwechsel erhalten.
- [ ] **1.3** Eine bestehende Liste per Drag in den Ordner ziehen → landet drin, Ordner zeigt sie.
- [ ] **1.4** Liste wieder aus dem Ordner heraus auf die oberste Ebene ziehen → geht zurück.
- [ ] **1.5** **Gemischte Reihenfolge** (das ist der Kern von 2.14): eine Liste **über** einen
Ordner ziehen → Reihenfolge hält. Ordner per Drag zwischen zwei Listen schieben → hält auch.
- [ ] **1.6** App neu starten (oder Worker-Menü → Reconnect) → die gemischte Reihenfolge ist
noch dieselbe. *Das ist der Test, der `ReorderSidebarAsync` wirklich prüft.*
- [ ] **1.7** Ordner löschen, während eine Liste drin liegt → was passiert mit der Liste?
(Erwartung unklar — notieren, was es tut.)
- [ ] **1.8** Rechtsklick auf einen Task → „In Liste verschieben" → Ziel wählen → Task ist weg
aus der Quelle, da in der Zielliste.
## Pass 2 — Review, Merge, Stash (~15 min) · der wichtigste Pass
Braucht #334/#335 in `WaitingForReview`. Sag mir Bescheid, falls sie noch laufen.
- [ ] **2.1** ⚠️ **Der uncommittete Fix.** Auf einem Task „Open ConPTY session" → im Terminal
irgendeine Kleinigkeit ändern → in der Mission-Control-Kachel „Submit for review".
**Erwartung:** Der Task landet in `WaitingForReview` und die REVIEW-Karte im Detail-Pane
zeigt **sofort** „Open diff" + Änderungs-Zusammenfassung — **ohne** dass du den Task neu
anklicken musst. (Vorher fehlte der ganze Block bis zur Neuauswahl.)
- [ ] **2.2** #334 anklicken → „Open diff" → Side-by-side-Diff zeigt die README-Änderung,
Zeilen/Farben stimmen, kein Abschneiden unten.
- [ ] **2.3** Approve-Gate: **ohne** geöffneten Diff ist Approve gesperrt, nach dem Diff aktiv.
- [ ] **2.4** Approve auf #334 → merged, Task geht auf **Done**, Chip wechselt, Worker-Log-Zeile
im Footer erscheint (mit `#334` im Text — deckt gleich Pass 5.1 mit ab).
- [ ] **2.5** 🔥 **Stash-Option (neuester Commit, komplett ungetestet).** Ich mache dir das
Ziel-Verzeichnis absichtlich dirty (sag „mach dirty"), dann auf #335 **Approve & Merge**:
Erwartung = **Bestätigungsdialog** „Änderungen stashen und mergen?" statt der alten
`blocked`-Sackgasse. Bestätigen → Merge läuft, und deine uncommitteten Änderungen in
`C:\TestRepos\ClaudeDoTests` sind danach **wieder da**.
- [ ] **2.6** Derselbe Fall über das **Merge-Modal** (statt Approve): Button
„Änderungen stashen und mergen" ist da und tut dasselbe.
- [ ] **2.7** Abbrechen im Stash-Dialog → nichts passiert, Task bleibt `WaitingForReview`,
Arbeitsverzeichnis unverändert.
- [ ] **2.8** Worktrees-Übersicht öffnen → die gemergten Worktrees stehen als `Merged` drin,
Cleanup funktioniert, keine WARN-Flut im Footer.
## Pass 3 — Interaktive Sessions & Mission Control (~12 min)
- [ ] **3.1** ConPTY-Session läuft → auf das **X** klicken → **Rückfrage** erscheint
(„Session läuft wirklich schließen?"), Abbrechen lässt sie laufen.
- [ ] **3.2** Im interaktiven Terminal **Text einfügen** (Ctrl+V) → landet im Prompt.
- [ ] **3.3** Eine **Datei ins Terminal droppen** → Pfad wird eingefügt.
Und: ein Drop, der scheitern muss (z.B. Ordner statt Datei) → Fehler wird **sichtbar**
gemeldet, nicht still geschluckt.
- [ ] **3.4** **Session stirbt nach dem Start** (im Terminal `exit` tippen) → Banner erscheint
in der Kachel, nicht einfach eine tote schwarze Fläche.
- [ ] **3.5** Mission Control: **Home-Ordner als Sitzungsziel** — neue Session ohne Liste/Task
starten → läuft im User-Verzeichnis, nicht irgendwo.
- [ ] **3.6** Mission Control ist **minimiert** → Handler-Run/Session startet → Fenster wird
aus dem Minimiert-Zustand geholt, nicht nur in der Taskleiste blinkend.
- [ ] **3.7** Handler-Run per Knopf **abbrechen** → Run endet sauber, Kachel verschwindet/
wechselt, kein Geister-Pane.
## Pass 4 — Menüs, Settings, Fenster-Layout (~12 min)
- [ ] **4.1** Hilfe-Menü → **Cheat-Sheets** → Fenster öffnet, sechs Sheets (git, Conventional
Commits, Claude CLI, Claude-Config, dotnet CLI, PowerShell).
- [ ] **4.2** Im Cheat-Sheet: Sektionen sind Expander, Befehle sind Mono-Chips mit
**Copy-Button** → Klick kopiert wirklich (irgendwo einfügen und prüfen).
- [ ] **4.3** Cheat-Sheet-Fenster **anpinnen** (Topmost) → bleibt über der App, App bleibt
bedienbar (modelos).
- [ ] **4.4** Hilfe-Menü → „Nach Updates suchen" und „Über" → Version zeigt **2.15.0**
(aus der FileVersion, nicht hartkodiert).
- [ ] **4.5** Worker-Menü → **„Update Claude CLI"** → führt sichtbar `claude update` aus,
Ausgabe landet irgendwo lesbar. *(Achtung: ändert wirklich deine CLI — überspring das,
wenn du gerade keine CLI-Version riskieren willst.)*
- [ ] **4.6** Worker-Menü → **Shutdown** → Rückfrage → beendet Worker **und** App.
*Ganz am Ende machen, sonst ist die Session vorbei.*
- [ ] **4.7** Settings → **Ausführung**: die PowerUse-Einstellungen sind hier (nicht mehr im
Usage-Monitor), Erklärtexte zu Effort und „Prime Claude" sind da und verständlich.
Ungültiges PowerUse-Zeitfenster eingeben → wird abgelehnt/erklärt, nicht still gespeichert.
- [ ] **4.8** **Modal-Layout** (Fix vom 02.09., Sichtprüfung offen): Über-Dialog,
Merge-Modal, Repo-Import, Worktrees-Übersicht, Merge-Helper-Auswahl **einmal öffnen** und
prüfen, dass unten **nichts abgeschnitten** ist (letzte Zeile/Button voll sichtbar).
- [ ] **4.9** Listeneinstellungen einer Liste öffnen → Modal erscheint **sofort** (lädt sichtbar
nach), und: Arbeitsverzeichnis auf einen Nicht-Git-Ordner setzen → **Warnung + Speichern
blockiert**, nicht stillschweigend akzeptiert.
- [ ] **4.10** Merge-Helper starten → Start-Modal hat den **Triage-Modell-Picker (opus/fable)**.
*(Nur das Modal ansehen, Lauf nicht starten — kostet sonst die halbe Stunde.)*
## Pass 5 — Reste & bekannte Nits (~8 min)
- [ ] **5.1** **Task-Nummern:** `#334` steht dimmed vor dem Titel in der Zeile, der Detail-Chip
zeigt `#334` (nicht mehr GUID), Footer-Worker-Log schreibt `#334 …`.
- [ ] **5.2** **depends_on sichtbar:** zwei Tasks verketten → der blockierte zeigt einen
Blocking-Chip, ein Ketten-Label („wartet auf …") und im Detail-Pane den Grund.
In der Queue-Zeile hat auch der **blockierte** Task ein Dequeue-**X**.
- [ ] **5.3** **Usage-Monitor-Modal → Models-Tab:** die bekannten Nits — kollidierende
Spaltenköpfe (`OTHER CACHISHARE`), Zahlen linksbündig ohne Tausendertrenner, Task-Titel
hart abgeschnitten. Nur bestätigen, dass sie noch da sind → geht auf die Fix-Liste.
- [ ] **5.4** **Deutsch:** einmal die Sprache auf DE stellen und durch Sidebar, Detail-Pane,
WorkConsole, ROADBLOCK-Karte und die Merge-Dialoge gehen → keine englischen Reste.
*(Der de.json-Konsistenz-Pass vom 10.09. ist nie angeschaut worden.)*
- [ ] **5.5** **Worker-Verbindung:** Worker-Menü → Reconnect bei laufendem Worker → Erfolg
sichtbar. Und einmal Worker beenden, dann Reconnect → **Fehlschlag wird gemeldet**
(Footer-Strip), nicht still no-op.
---
## Ergebnis
Alle fünf Pässe durchlaufen. **11 Befunde** aus ~45 Checks; Pass 4 (Menüs/Settings/Modal-Layout)
war komplett sauber. Die drei schweren (F9/F10/F11) sind alle erst ganz am Ende aufgetaucht —
Lebenszyklus der App, nicht Features.
Nicht getestet: Installer/SystemCheckPage, Ticketsystem-Anbindung (beide brauchen echtes Setup),
3.7 Handler-Abbruch nur oberflächlich.
## Delegiert als Tasks (Liste „Claude do", alle Idle)
| Task | Deckt ab |
|---|---|
| **#339** Shutdown beendet den Prozess nicht, Worker-Neustart tut nichts | F9 + F11 |
| **#340** App-Speicherverbrauch messen — nur Diagnose | F10 |
| **#341** duplizierter Confirm-Dialog mit „Delete"-Knopf | F4 + F5 |
| **#342** Ordner-Toggle schluckt schnelle Klicks | F1 |
| **#343** Diff-Viewer horizontal nicht synchron | F3 |
| **#344** dirty Ziel früh melden statt nach dem Fehlschlag | F6 |
| **#345** Bilder aus der Zwischenablage ins Terminal | F8 |
| **#346** Worktrees-Übersicht: Nummern + Truncation | F7 |
Nicht delegiert: **F2** (Modell für interaktive Sessions erzwingen) — Entscheidung Mika offen.
## Fix-Liste
| # | Pass | Befund | Schwere |
|---|------|--------|---------|
| F9 | 4.6 | **„Shutdown" beendet die App nicht — der Prozess überlebt.** `IslandsShellViewModel.cs:705` `ShutdownWorkerAndCloseApp` killt den Worker und ruft dann nur `CloseWindow` für das Hauptfenster. Mission Control, Cheat-Sheets und der Diff-Viewer sind eigene `Window`-Instanzen, bleiben offen und halten den Avalonia-Prozess am Leben (bestätigt: alle Nebenfenster standen nach dem „Shutdown" noch offen, `ClaudeDo.App` weiterhin im Task-Manager). Fix: alle Fenster schließen bzw. `IClassicDesktopStyleApplicationLifetime.Shutdown()` statt `CloseWindow`. | **hoch** |
| F10 | — | **App-Speicherverbrauch ~1,2 GB.** Gemessen 2026-09-16 am laufenden Debug-Build: `ClaudeDo.App` 1227 MB, `ClaudeDo.Worker` 209 MB. Der Worker wurde 2026-09-02 (`c347e4fc`) bewusst auf Workstation-GC umgestellt, die App offenbar nicht — erster Verdacht, aber ungeprüft. Braucht eine echte Messung (Server-GC? gehaltene Log-/Diff-Puffer? ConPTY-Scrollback?), keinen Blindfix. | **hoch** |
| F11 | 5.5 | **Worker-Neustart über den Button funktioniert nicht.** `IslandsShellViewModel.cs:656` `RestartWorkerService` beginnt mit `_workerLocator.Find()` und wirft „Worker executable not found", wenn nichts passt. **Hypothese, nicht verifiziert:** der Locator findet den *installierten* Worker (`C:\Program Files\ClaudeDo`), während der Debug-Build aus dem Repo läuft — dann wird der falsche oder gar kein Prozess gekillt/gestartet. Erst reproduzieren und die Footer-Meldung lesen, dann fixen. | **hoch** |
| F8 | 3.2 | **Bilder aus der Zwischenablage lassen sich nicht ins interaktive Terminal einfügen** (Text geht). `InteractiveTerminalViewModel.PasteAsync` → `PtyTerminalSession.PasteAsync` liest nur Text. Lazy Fix: Bild-Clipboard in eine temporäre PNG schreiben und deren Pfad über den bereits funktionierenden Drop-Pfad (`SendTextAsync`) einfügen — Claude liest die Datei dann selbst. Kein neuer Mechanismus nötig. | mittel |
| F6 | 2.6 | **Dirty-Target wird zu spät gemeldet.** `dirty_target` kommt erst als Fehlerergebnis *nachdem* der Merge gedrückt und abgelehnt wurde — der Zustand des Ziel-Trees ist aber schon beim Öffnen des Merge-Modals bekannt. Wunsch (Mika): beim Laden des Modals prüfen, Hinweis + „Änderungen stashen und mergen" direkt anbieten; ggf. schon im Diff-Viewer signalisieren. Betrifft beide Pfade (Approve + Merge-Modal). | mittel |
| F7 | 2.8 | **Worktrees-Übersicht zeigt keine Task-Nummern.** TASK-Spalte listet nur den Titel; die Task-Numbers-Feature hat Zeile, Detail-Chip und Worker-Log abgedeckt, diese Tabelle nicht. Zusätzlich ist die DIFF-Spalte hart abgeschnitten ohne Ellipse (`docs/notes.m`, `1 file chan`) — gleiches Truncation-Muster wie im Usage-Monitor (F-Nit 5.3). | niedrig |
| F4 | 2.5 | **Stash-Bestätigung trägt „Delete" als Bestätigungsknopf.** `DetailsIslandView.axaml.cs:262-263` ist eine zweite, kopierte Confirm-Dialog-Implementierung mit hartkodiertem `Content = "Delete"` (+ `Classes = { "danger" }`); `DetailsIslandViewModel.ConfirmAsync` hängt daran statt am gemeinsamen `WindowDialogService.ConfirmAsync:209` (dort korrekt „Confirm"). Der Stash-Dialog erbt damit die Beschriftung eines Lösch-Dialogs — irreführend bis gefährlich. Fix: Duplikat löschen, VM an den Service-Dialog hängen. | **hoch** |
| F5 | 2.5 | **Confirm-/Error-Dialoge sind nicht lokalisiert.** Beide Implementierungen setzen `"Cancel"`/`"Confirm"`/`"Delete"`/`"OK"` und den Fenstertitel `"Confirm"`/`"Error"` als String-Literal, kein `Loc.T`. Fällt mit F4 zusammen erledigen. | mittel |
| F3 | 2.2 | **Diff-Viewer scrollt nur vertikal synchron.** `DiffEditorSetup.cs` — die Sync-Klasse heißt `VerticalScrollSync`, Zeile 147 schreibt `new Vector(viewer.Offset.X, y)` und behält das X der Zielspalte bei; horizontal wurde nie verdrahtet, beide Seiten scrollen seitwärts unabhängig. Fix: X 1:1 mitspiegeln (gleiche Schrift/Breite in beiden Panes), Klasse entsprechend umbenennen. | mittel |
| F2 | 2.1 | **Feature-Wunsch (kein Bug):** interaktive ConPTY-Sessions starten unter dem Modell aus Mikas Claude-Config, nicht unter dem im Task gewählten. Bewusste Entscheidung, siehe `docs/open.md:35` (`--effort` ja, `--model` nein) — dort schon als möglicher Folge-Task vermerkt. Entscheidung Mika offen. | Wunsch |
| F1 | 1.2 | **Ordner-Toggle schluckt schnelle Klicks.** `ListsIslandViewModel.cs:626` `ToggleFolderAsync` ist ein async `[RelayCommand]` → `AsyncRelayCommand` setzt `CanExecute=false` für die Dauer des DB-Writes (`SetFolderCollapsedAsync`); jeder Klick in diesem Fenster wird verworfen. Fix: Toggle synchron (UI-State sofort), Persistierung fire-and-forget. Gleiches Muster in der Sidebar auf andere async RelayCommands gegenprüfen. | mittel |
+1
View File
@@ -32,6 +32,7 @@
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />
<!-- Global defaults: every Window inherits Inter Tight + body size.
+1
View File
@@ -52,6 +52,7 @@ public partial class App : Application
Dispatcher.UIThread.UnhandledException += (_, e) =>
{
e.Handled = true;
Serilog.Log.Error(e.Exception, "Unhandled exception on the UI dispatcher");
shell.FlashFooterError(Loc.T("vm.shell.unexpectedError", e.Exception.Message));
};
+2
View File
@@ -26,6 +26,8 @@
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
+39 -3
View File
@@ -13,6 +13,7 @@ using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System;
using System.Globalization;
using System.IO;
@@ -20,6 +21,7 @@ using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace ClaudeDo.App;
@@ -34,6 +36,35 @@ sealed class Program
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
var logRoot = Path.Combine(Paths.AppDataRoot(), "logs");
Directory.CreateDirectory(logRoot);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.File(
Path.Combine(logRoot, "ui-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
shared: true)
.CreateLogger();
var version = Assembly.GetEntryAssembly()?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? "0.0.0";
Log.Information("ClaudeDo.App starting (version: {version})", version);
// Last-resort net: any exception that misses every other handler still gets one
// Fatal line in the log before the process dies.
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception, terminating (is_terminating: {is_terminating})", e.IsTerminating);
Log.CloseAndFlush();
};
TaskScheduler.UnobservedTaskException += (_, e) =>
{
Log.Error(e.Exception, "Unobserved task exception");
e.SetObserved();
};
var services = BuildServices();
using (var scope = services.CreateScope())
@@ -42,6 +73,8 @@ sealed class Program
ClaudeDoDbContext.MigrateAndConfigure(db);
}
ClipboardImageTempStore.DeleteOlderThan(TimeSpan.FromHours(24), DateTime.UtcNow);
try
{
ConfigureAppBuilder(AppBuilder.Configure(() => new App(services)))
@@ -60,6 +93,8 @@ sealed class Program
{
/* best effort on shutdown */
}
Log.CloseAndFlush();
}
}
@@ -165,13 +200,13 @@ sealed class Program
sc.AddSingleton<TasksIslandViewModel>(sp =>
new TasksIslandViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<IWorkerClient>()));
sp.GetRequiredService<IWorkerClient>(),
sp.GetRequiredService<INotesApi>()));
sc.AddSingleton<DetailsIslandViewModel>(sp =>
new DetailsIslandViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<IWorkerClient>(),
sp,
sp.GetRequiredService<INotesApi>(),
sp.GetRequiredService<MergeCoordinator>()));
sc.AddSingleton<UsagePillViewModel>(sp =>
new UsagePillViewModel(sp.GetRequiredService<IWorkerClient>()));
@@ -179,7 +214,8 @@ sealed class Program
new MissionControlViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<IWorkerClient>(),
sp.GetRequiredService<UsagePillViewModel>()));
sp.GetRequiredService<UsagePillViewModel>(),
sp.GetRequiredService<AppSettings>()));
sc.AddSingleton<IslandsShellViewModel>(sp =>
{
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
+34 -11
View File
@@ -4,15 +4,19 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
## Models
- **TaskEntity** — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), DependsOnTaskId (FK to a user/MCP-declared predecessor, distinct from BlockedByTaskId), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns / PermissionMode (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId, TicketRef, Number (global immutable human-readable alias).
- **TaskEntity** — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), Dependencies (n user/MCP-declared predecessors via `task_dependencies`, distinct from BlockedByTaskId), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns / PermissionMode (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId, TicketRef, Number (global immutable human-readable alias).
- `TicketRef` (`ticket_ref`, nullable string) — link to an external ticket, format `<provider>:<id>` (currently always `bandel:<ticketId>`). Set by the ticket import, otherwise null. → `ClaudeDo.Worker/CLAUDE.md` Tickets section.
- Status / PlanningPhase / BlockedByTaskId / DependsOnTaskId semantics + allowed transitions: `ClaudeDo.Worker/CLAUDE.md` → Status Model.
- Status / PlanningPhase / BlockedByTaskId / dependency-set semantics + allowed transitions: `ClaudeDo.Worker/CLAUDE.md` → Status Model.
- `Dependencies` (`ICollection<TaskDependencyEntity>`) + the `[NotMapped] DependsOnTaskIds` id projection. **Only populated where the query `Include`s it** — `TaskRepository.GetByIdAsync`/`GetByNumberAsync`/`GetByListIdAsync` do, and so do the `TasksIslandViewModel` row-hydration queries. The queue gate itself is raw SQL in `QueuePicker` and never reads the nav, so a forgotten Include misreports the UI/MCP `blocked` flag but can never let a blocked task run. ⚠️ `TaskRepository.UpdateAsync` marks **only the root entity** Modified (`Entry().State`, not `Update()`) — `Update()` marks the loaded graph, and an all-key `task_dependencies` row has nothing to SET.
- `HandlerBaseCommit`/`HandlerHeadCommit` = the review range for a **worktree-less "list handler" host task** ("Let Claude handle it"), which commits straight into the list's working dir instead of a per-task worktree. Everything that reads a task's diff falls back to this pair whenever `Worktree` is null → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md).
- `InteractiveSessionId` = the claude session id an embedded ConPTY interactive task session runs under, persisted by `InteractiveLaunchSpecService` before launch so a closed/aborted session can be resumed → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md).
- `Number` (INTEGER NOT NULL, unique index `idx_tasks_number`) — a global, monotonically increasing display alias for tasks. **Never reused**: deleting a task leaves a gap in numbering, ensuring that a task number that appears in a log or report always points to the same task (if it exists). Allocated by `TaskNumberAllocator.AddWithNumberAsync` on every insert via a persistent counter (`app_settings.next_task_number`). **Why not `MAX(number) + 1`:** deleting the highest-numbered task would free its number for reuse, breaking the immutability guarantee. The allocator uses an `UPDATE…RETURNING` statement to claim numbers atomically, retrying up to 5 times if a uniqueness collision occurs (the counter advances on each attempt, so retries always allocate fresh numbers). The one-time backfill for pre-existing rows lived in the `AddTaskNumbers` migration, squashed away 2026-08-26.
- Legacy status values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired. The backfill migration was squashed away (2026-08-26) — a database still holding those values can't be upgraded, see Schema below.
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`)
- **ListConfigEntity** — ListId (PK, 1:1), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand, PermissionMode, TicketProjectId (all nullable). `VerifyCommand` is an optional post-merge gate; null/blank = no gate → [review-merge](../../docs/explore-notes/review-merge.md). `PermissionMode` is null = inherit the global default; resolution order task → list → global lives in `EffectiveRunConfigResolver`. `TicketProjectId` (`ticket_project_id`) links the list to a project in the external ticket system; null = not linked → `ClaudeDo.Worker/CLAUDE.md` Tickets section.
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt, IsManual (reminder list — tasks created here default to `IsManual`), Icon, Color, FolderId
- `Color` (`color`, nullable) — dot colour: a swatch key (`Swatch3`), a literal `#RRGGBB`, or null = the hash-derived identity colour. Same value format as `ListFolderEntity.Color`; resolved UI-side by `ColorBrushConverter`.
- `FolderId` (`folder_id`, nullable FK → `list_folders.id`, `ON DELETE SET NULL`) — the sidebar folder the list sits in; null = top level. Written only by the UI (`ListsIslandViewModel` drag/context menu), never by MCP.
- **ListFolderEntity** — Id, Name, Icon, SortOrder, IsCollapsed, ParentFolderId, CreatedAt → `list_folders`. A pure sidebar grouping: no tasks, no config, no working dir. `ParentFolderId` (self-FK, `ON DELETE SET NULL`) lets folders nest arbitrarily deep — drag-and-drop only, no UI to pick a parent otherwise. Deliberately its own table rather than a flag on `lists`, so every caller that enumerates lists (MCP `list_task_lists`, "move to list", the queue picker, repo import) needs no folder filtering. Folder CRUD lives on `ListRepository`.
- **ListConfigEntity** — ListId (PK, 1:1), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand, PermissionMode, TicketProjectId, BaseBranch (all nullable), PowerUseAllowed. `VerifyCommand` is an optional post-merge gate; null/blank = no gate → [review-merge](../../docs/explore-notes/review-merge.md). `PermissionMode` is null = inherit the global default; resolution order task → list → global lives in `EffectiveRunConfigResolver`. `TicketProjectId` (`ticket_project_id`) links the list to a project in the external ticket system; null = not linked → `ClaudeDo.Worker/CLAUDE.md` Tickets section. `BaseBranch` (`base_branch`) = base of new worktrees + default merge target; tri-state in `UpdateListConfigDto` (null = keep, "" = clear). ⚠️ Same verbatim-copy trap as `ticket_project_id`: `SetConfigAsync` overwrites every column, so every writer must carry it forward.
- ⚠️ `ListRepository.SetConfigAsync` copies the entity **verbatim**, so every writer must carry the fields it doesn't own (`SessionSkills`, `PermissionMode`, `TicketProjectId`) or they silently reset. `UpdateListConfigDto.TicketProjectId` is tri-state (`null` = keep stored) for exactly that reason — only the list-settings modal sends an explicit value. This bit `PermissionMode` for real: its update-branch copy was missing from `SetConfigAsync` until `f0a3a186`, so a list's permission-mode override only ever stuck on the list's *first* save, never on a later one.
- **WorktreeEntity** — TaskId (PK, 1:1), Path, BranchName, BaseCommit, HeadCommit, DiffStat, MergeCommit (nullable — SHA of the merge commit this branch produced; the only thing making `revert_merge` possible without searching `git log`), State (`Active|Merged|Discarded|Kept`)
- **TaskRunEntity** — per-run record: session_id, turns, result, structured output, exit code, log path, nullable `Model` (what the run actually executed with), and `TokensIn`/`TokensOut`/`CacheReadTokens`/`CacheWriteTokens`. ⚠️ Token fields come from the **session transcript**, not the stream-json event, as a per-run delta → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md).
@@ -20,6 +24,8 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → `daily_notes`
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → `week_reports`, unique index on (start_date, end_date)
- **TaskAttachmentEntity** — Id, TaskId (FK, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → `task_attachments`
- **TaskDependencyEntity** — one declared "run after" edge: TaskId + DependsOnTaskId, composite PK, table `task_dependencies`. Both FKs are `ON DELETE CASCADE`, which is what the old single `depends_on_task_id` column got from `ON DELETE SET NULL`: deleting a predecessor drops the edge so the dependent becomes pickable instead of blocked on a task that's gone. Written **only** by `TaskStateService.SetDependsOnAsync`.
- **MissionControlSessionEntity** — SessionId (PK), Cwd, TaskId (no FK), Title, StartedAt, PredecessorSessionId, BriefPath → `mission_control_sessions`. Index behind Mission Control's "Recent sessions" flyout; written only by `InteractiveLaunchSpecService` (task/ad-hoc/handoff tile launches), insert-if-absent → [conpty-sessions](../../docs/explore-notes/conpty-sessions.md).
- **SubtaskEntity**, **AgentInfo** — subtasks / record for scanned agent files
### AppSettingsEntity
@@ -37,10 +43,20 @@ Beyond the basics it carries:
| `ReportExcludedPaths` | `report_excluded_paths` | null | JSON array of excluded path prefixes. |
| `StandupWeekday` | `standup_weekday` | Wednesday | int `DayOfWeek`. |
| `NextTaskNumber` | `next_task_number` | 1 | Persistent counter for `TaskEntity.Number` allocation. Incremented atomically in `TaskNumberAllocator.AddWithNumberAsync` on every task insert. **Monotonic only:** the counter itself advances across all tasks (no per-list numbering), and it never resets. A deleted task leaves a gap. |
| `PowerUseEnabled` | `power_use_enabled` | false | PowerUse master switch, toggled in **Settings → Execution**. ⚠️ Never sufficient alone — a handler run is in PowerUse only when `list_config.power_use_allowed` is also set. |
| `ListHandlerMaxHandoffs` | `list_handler_max_handoffs` | 4 | Cap on a list-handler run's wait↔merge handoff chain (`HandoffRoundTracker`). 4 = two merge rounds. Independent of PowerUse — applies to interactive runs too. Clamped to min 1 in `WorkerHub.UpdateAppSettings` and again in `Register`, because 0 would remove the only bound on a runaway chain. |
| `PowerUseWindowStart` / `PowerUseWindowEnd` | `power_use_window_*` | null | Optional local-time window (`HH:mm`) narrowing when PowerUse applies. Both null (or only one set) = round the clock, i.e. byte-identical to the pre-window behaviour; end before start wraps over midnight. Semantics live in the pure `PowerUseWindow`; checked in `InteractiveLaunchSpecService.ComposeHandlerPromptAsync` **per phase** at prompt-build time, so a chain handing off past the end time drops the addendum from there on. Unparseable input is stored as null, not rejected. |
| `PowerUseHandoffPct` | `power_use_handoff_pct` | 95 | 5h-usage percentage at which a PowerUse handler session should hand off. Clamped 50..99. Typed in Settings → Execution; the consumer is the handoff prompt. |
All four usage percentages are clamped 0..100 by `AppSettingsRepository.UpdateAsync`.
Gate/throttle semantics → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md).
⚠️ **`AppSettingsRepository.UpdateAsync` copies the incoming entity field by field** onto the
singleton row — a property missing from that block never persists, however correctly the caller
filled the entity, and nothing fails. `ModelPresets`, `PowerUseEnabled` and
`ListHandlerMaxHandoffs` were all silently unwritable that way until 2026-09-02. Add a new column
to that block, not just to the entity + the hub.
### Model / effort registries
- **ModelPresets / ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`), one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1–200) and **never throw** — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns)` always returns a usable row; resolution order and the fallback trap → [worker-task-pipeline](../../docs/explore-notes/worker-task-pipeline.md).
@@ -53,7 +69,8 @@ All use EF Core LINQ via `ClaudeDoDbContext`. The atomic `Queued → Running` cl
Worker's `QueuePicker` (`FromSqlRaw`), **not** here.
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync`. ⚠️ Status-mutation primitives (`MarkRunningAsync`/`MarkDoneAsync`/`MarkFailedAsync`/`FlipAllRunningToFailedAsync`) are **`internal`** — only the Worker's `TaskStateService` may call them. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`. **Task-number allocation:** `AddAsync` (line 20) and `CreateChildAsync` (line 276) are the two sole task-creation paths; both call `TaskNumberAllocator.AddWithNumberAsync` to allocate a number atomically during the same transaction as the insert. Every other creation path (UI, MCP `add_task`, `batch_add_tasks`, Online Inbox sync, merge-helper handler tasks, planning children) routes through one of these two.
- **ListRepository** — CRUD, `GetConfigAsync`/`SetConfigAsync` (upsert)/`DeleteConfigAsync` for `list_config`
- **ListRepository** — CRUD, `GetConfigAsync`/`SetConfigAsync` (upsert)/`DeleteConfigAsync` for `list_config`, plus the `list_folders` set: `GetFoldersAsync`, `AddFolderAsync` (appends), `RenameFolderAsync`, `DeleteFolderAsync` (folder only — the FK releases its lists), `SetFolderCollapsedAsync`, `SetListFolderAsync`. The folder writers are `ExecuteUpdate` patches, not entity `Update()`s, so a rename can't reset `SortOrder`/`IsCollapsed` the way `SetConfigAsync`'s verbatim copy resets forgotten columns.
- ⚠️ **`lists.sort_order` and `list_folders.sort_order` share one number space** — the sidebar interleaves folders and top-level lists in a single order, so `ReorderSidebarAsync` numbers the whole visual sequence (each folder followed by its children) across both tables, and `AddAsync`/`AddFolderAsync` append via `MAX(sort_order)+1` over **both**. Never renumber one table on its own.
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`, `SetMergedAsync` (atomically sets State=Merged **and** stamps MergeCommit in one update — the only writer of MergeCommit)
- **TaskAttachmentRepository** — `AddAsync`, `UpdateAsync`, `GetAsync(taskId, fileName)`, `ListByTaskIdAsync`, `DeleteAsync(taskId, fileName)`. Bulk delete-per-task is the `ON DELETE CASCADE` on `task_attachments`, not a repository method.
- **DailyNoteRepository**, **WeekReportRepository**, **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
@@ -103,6 +120,9 @@ Tables (one per line so parallel migrations don't collide on the same line):
- `daily_notes`
- `week_reports`
- `task_attachments`
- `task_dependencies`
- `list_folders`
- `mission_control_sessions`
Managed by EF Core migrations in `Migrations/`. **The 40 incremental migrations were squashed into
a single `20260826094154_InitialCreate` on 2026-08-26** — the schema history is git history now,
@@ -112,8 +132,9 @@ not a migration chain. Consequences:
(pre-EF databases without a history, and databases whose history still lists the old chain ending
at `20260825063230_AddPrimeActionKind`). A database that stopped *mid*-chain can no longer be
upgraded and throws with a reinstall message instead of being stamped onto a schema it lacks.
Covered by `MigrationBaselineTests` — the only test that runs a real `Migrate()` (every other
Data.Tests fixture uses `EnsureCreated`, which skips migrations entirely). Its fixture builds the
Covered by `MigrationBaselineTests` — one of only two tests that run a real `Migrate()`
(`TaskDependencyMigrationTests` is the other; every other Data.Tests fixture uses
`EnsureCreated`, which skips migrations entirely). Its fixture builds the
legacy database by migrating **to** `InitialCreate` and seeding rows with raw SQL: `EnsureCreated`
would build *today's* schema, so every post-squash migration would then re-add an existing column
("duplicate column name"). Assert "nothing pending", never "InitialCreate is the only history row".
@@ -122,10 +143,12 @@ not a migration chain. Consequences:
Release` — `Data` is its own startup project (it holds the `Design` package and a design-time
factory), and `--configuration Release` is needed because a running Worker locks `Debug`.
`tasks` holds `status`, `planning_phase` (default `none`),
`blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`), and `depends_on_task_id` (same FK
shape, but a separate column — see Worker/CLAUDE.md → Status Model for why it isn't unified with
`blocked_by_task_id`).
`tasks` holds `status`, `planning_phase` (default `none`) and `blocked_by_task_id` (FK to
`tasks.id`, `ON DELETE SET NULL`). The user/MCP-declared predecessors live in `task_dependencies`
instead — a **set**, so a task can wait on n of them; see Worker/CLAUDE.md → Status Model for why
it isn't unified with `blocked_by_task_id`. `AddTaskDependencies` (2026-09-02) migrated the former
single `tasks.depends_on_task_id` column into that table (each value became a one-element set) and
dropped it; the `Down` direction is inherently lossy for more than one edge.
## Conventions
+3
View File
@@ -41,7 +41,9 @@ public class ClaudeDoDbContext : DbContext
=> optionsBuilder.AddInterceptors(SqliteForeignKeyInterceptor.Instance);
public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
public DbSet<TaskDependencyEntity> TaskDependencies => Set<TaskDependencyEntity>();
public DbSet<ListEntity> Lists => Set<ListEntity>();
public DbSet<ListFolderEntity> ListFolders => Set<ListFolderEntity>();
public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
@@ -52,6 +54,7 @@ public class ClaudeDoDbContext : DbContext
public DbSet<DailyNoteEntity> DailyNotes => Set<DailyNoteEntity>();
public DbSet<WeekReportEntity> WeekReports => Set<WeekReportEntity>();
public DbSet<SessionSkillEntity> SessionSkills => Set<SessionSkillEntity>();
public DbSet<MissionControlSessionEntity> MissionControlSessions => Set<MissionControlSessionEntity>();
private static readonly ValueConverter<DateTime, DateTime> UtcConverter =
new(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
@@ -43,6 +43,9 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
builder.Property(s => s.RepoImportFolders)
.HasColumnName("repo_import_folders");
builder.Property(s => s.SshWorkspaces)
.HasColumnName("ssh_workspaces");
builder.Property(s => s.ReportExcludedPaths).HasColumnName("report_excluded_paths");
builder.Property(s => s.StandupWeekday).HasColumnName("standup_weekday")
.IsRequired().HasDefaultValue((int)DayOfWeek.Wednesday);
@@ -70,6 +73,20 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
builder.Property(s => s.AutoContinueOnUsageLimit)
.HasColumnName("auto_continue_on_usage_limit").IsRequired().HasDefaultValue(false);
builder.Property(s => s.PowerUseEnabled)
.HasColumnName("power_use_enabled").IsRequired().HasDefaultValue(false);
builder.Property(s => s.PowerUseWindowStart)
.HasColumnName("power_use_window_start");
builder.Property(s => s.PowerUseWindowEnd)
.HasColumnName("power_use_window_end");
builder.Property(s => s.PowerUseHandoffPct)
.HasColumnName("power_use_handoff_pct").IsRequired().HasDefaultValue(95);
builder.Property(s => s.ListHandlerMaxHandoffs)
.HasColumnName("list_handler_max_handoffs").IsRequired().HasDefaultValue(4);
builder.Property(s => s.TicketFallbackListId)
.HasColumnName("ticket_fallback_list_id");
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
}
}
@@ -14,6 +14,7 @@ public class DailyNoteEntityConfiguration : IEntityTypeConfiguration<DailyNoteEn
builder.Property(n => n.Date).HasColumnName("note_date").IsRequired();
builder.Property(n => n.Text).HasColumnName("text").IsRequired();
builder.Property(n => n.SortOrder).HasColumnName("sort_order").IsRequired();
builder.Property(n => n.FromAgent).HasColumnName("from_agent").IsRequired();
builder.Property(n => n.CreatedAt).HasColumnName("created_at").IsRequired();
builder.HasIndex(n => n.Date);
}
@@ -20,5 +20,7 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
builder.Property(c => c.PermissionMode).HasColumnName("permission_mode");
builder.Property(c => c.TicketProjectId).HasColumnName("ticket_project_id");
builder.Property(c => c.PowerUseAllowed).HasColumnName("power_use_allowed");
builder.Property(c => c.BaseBranch).HasColumnName("base_branch");
}
}
@@ -20,8 +20,18 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
builder.Property(l => l.IsManual).HasColumnName("is_manual").IsRequired().HasDefaultValue(false);
builder.Property(l => l.FindingsTracked).HasColumnName("findings_tracked").IsRequired().HasDefaultValue(false);
builder.Property(l => l.Icon).HasColumnName("icon");
builder.Property(l => l.Color).HasColumnName("color");
builder.Property(l => l.FolderId).HasColumnName("folder_id");
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
builder.HasIndex(l => l.FolderId).HasDatabaseName("idx_lists_folder");
// No navigation property on either side: nothing needs the graph, and a nav would have to
// be Include'd (or silently come back empty) in every list query.
builder.HasOne<ListFolderEntity>()
.WithMany()
.HasForeignKey(l => l.FolderId)
.OnDelete(DeleteBehavior.SetNull);
builder.HasOne(l => l.Config)
.WithOne(c => c.List)
@@ -0,0 +1,31 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class ListFolderEntityConfiguration : IEntityTypeConfiguration<ListFolderEntity>
{
public void Configure(EntityTypeBuilder<ListFolderEntity> builder)
{
builder.ToTable("list_folders");
builder.HasKey(f => f.Id);
builder.Property(f => f.Id).HasColumnName("id");
builder.Property(f => f.Name).HasColumnName("name").IsRequired();
builder.Property(f => f.CreatedAt).HasColumnName("created_at").IsRequired();
builder.Property(f => f.Icon).HasColumnName("icon");
builder.Property(f => f.Color).HasColumnName("color");
builder.Property(f => f.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
builder.Property(f => f.IsCollapsed).HasColumnName("is_collapsed").IsRequired().HasDefaultValue(false);
builder.Property(f => f.ParentFolderId).HasColumnName("parent_folder_id");
builder.HasIndex(f => f.SortOrder).HasDatabaseName("idx_list_folders_sort");
builder.HasIndex(f => f.ParentFolderId).HasDatabaseName("idx_list_folders_parent");
builder.HasOne<ListFolderEntity>()
.WithMany()
.HasForeignKey(f => f.ParentFolderId)
.OnDelete(DeleteBehavior.SetNull);
}
}
@@ -0,0 +1,22 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class MissionControlSessionEntityConfiguration : IEntityTypeConfiguration<MissionControlSessionEntity>
{
public void Configure(EntityTypeBuilder<MissionControlSessionEntity> builder)
{
builder.ToTable("mission_control_sessions");
builder.HasKey(s => s.SessionId);
builder.Property(s => s.SessionId).HasColumnName("session_id").ValueGeneratedNever();
builder.Property(s => s.Cwd).HasColumnName("cwd").IsRequired();
builder.Property(s => s.TaskId).HasColumnName("task_id");
builder.Property(s => s.Title).HasColumnName("title").IsRequired();
builder.Property(s => s.StartedAt).HasColumnName("started_at").IsRequired();
builder.Property(s => s.PredecessorSessionId).HasColumnName("predecessor_session_id");
builder.Property(s => s.BriefPath).HasColumnName("brief_path");
builder.HasIndex(s => s.StartedAt);
}
}
@@ -11,12 +11,14 @@ public class PrimeScheduleEntityConfiguration : IEntityTypeConfiguration<PrimeSc
=> v == PrimeActionKind.Ping ? "ping"
: v == PrimeActionKind.FillMyDay ? "fill_my_day"
: v == PrimeActionKind.Custom ? "custom"
: v == PrimeActionKind.ImportTickets ? "import_tickets"
: throw new ArgumentOutOfRangeException(nameof(v));
private static PrimeActionKind KindFromString(string v)
=> v == "ping" ? PrimeActionKind.Ping
: v == "fill_my_day" ? PrimeActionKind.FillMyDay
: v == "custom" ? PrimeActionKind.Custom
: v == "import_tickets" ? PrimeActionKind.ImportTickets
: throw new ArgumentOutOfRangeException(nameof(v));
private static readonly ValueConverter<PrimeActionKind, string> KindConverter =
@@ -0,0 +1,32 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class TaskDependencyEntityConfiguration : IEntityTypeConfiguration<TaskDependencyEntity>
{
public void Configure(EntityTypeBuilder<TaskDependencyEntity> builder)
{
builder.ToTable("task_dependencies");
builder.HasKey(d => new { d.TaskId, d.DependsOnTaskId });
builder.Property(d => d.TaskId).HasColumnName("task_id");
builder.Property(d => d.DependsOnTaskId).HasColumnName("depends_on_task_id");
// Cascade on both sides reproduces what the old single depends_on_task_id column got from
// ON DELETE SET NULL: deleting the predecessor drops the edge, so the dependent becomes
// pickable rather than blocked forever on a task that no longer exists.
builder.HasOne<TaskEntity>()
.WithMany(t => t.Dependencies)
.HasForeignKey(d => d.TaskId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<TaskEntity>()
.WithMany()
.HasForeignKey(d => d.DependsOnTaskId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(d => d.DependsOnTaskId).HasDatabaseName("idx_task_dependencies_depends_on");
}
}
@@ -75,7 +75,6 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
builder.Property(t => t.PlanningPhase).HasColumnName("planning_phase").IsRequired()
.HasConversion(PhaseConverter).HasDefaultValue(PlanningPhase.None);
builder.Property(t => t.BlockedByTaskId).HasColumnName("blocked_by_task_id");
builder.Property(t => t.DependsOnTaskId).HasColumnName("depends_on_task_id");
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
builder.Property(t => t.Result).HasColumnName("result");
builder.Property(t => t.ReviewFeedback).HasColumnName("review_feedback");
@@ -97,6 +96,7 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
builder.Property(t => t.IsManual).HasColumnName("is_manual").HasDefaultValue(false);
builder.Property(t => t.ExpectsNoDiff).HasColumnName("expects_no_diff").HasDefaultValue(false);
builder.Property(t => t.Notes).HasColumnName("notes");
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
@@ -123,12 +123,8 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
.HasForeignKey(t => t.BlockedByTaskId)
.OnDelete(DeleteBehavior.SetNull);
// DependsOn: user/MCP-declared predecessor. SetNull on delete so the dependent becomes
// pickable rather than blocked forever on a task that no longer exists.
builder.HasOne<TaskEntity>()
.WithMany()
.HasForeignKey(t => t.DependsOnTaskId)
.OnDelete(DeleteBehavior.SetNull);
// DependsOn edges live in task_dependencies (a task can declare n predecessors) —
// configured in TaskDependencyEntityConfiguration, which also owns the Dependencies nav.
builder.HasOne(t => t.List)
.WithMany(l => l.Tasks)
@@ -145,6 +141,5 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort");
builder.HasIndex(t => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id");
builder.HasIndex(t => t.BlockedByTaskId).HasDatabaseName("idx_tasks_blocked_by");
builder.HasIndex(t => t.DependsOnTaskId).HasDatabaseName("idx_tasks_depends_on");
}
}
@@ -36,6 +36,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEnti
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
builder.Property(w => w.MergeCommit).HasColumnName("merge_commit");
builder.Property(w => w.PullRequestUrl).HasColumnName("pull_request_url");
builder.Property(w => w.State).HasColumnName("state").IsRequired()
.HasDefaultValue(WorktreeState.Active)
.HasConversion(StateConverter);
+122 -3
View File
@@ -338,6 +338,38 @@ public sealed class GitService
return paths;
}
/// <summary>All git-registered worktrees for <paramref name="repoDir"/>, with their branch (short name, null if detached).</summary>
public async Task<List<(string Path, string? Branch)>> ListWorktreesAsync(string repoDir, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["worktree", "list", "--porcelain"], ct);
if (exitCode != 0) return new();
var result = new List<(string Path, string? Branch)>();
string? currentPath = null;
string? currentBranch = null;
foreach (var raw in stdout.Split('\n'))
{
var line = raw.TrimEnd('\r');
if (line.StartsWith("worktree ", StringComparison.Ordinal))
{
currentPath = line["worktree ".Length..].Trim();
}
else if (line.StartsWith("branch ", StringComparison.Ordinal))
{
var b = line["branch ".Length..].Trim();
currentBranch = b.StartsWith("refs/heads/", StringComparison.Ordinal) ? b["refs/heads/".Length..] : b;
}
else if (string.IsNullOrWhiteSpace(line))
{
if (currentPath is not null) result.Add((currentPath, currentBranch));
currentPath = null;
currentBranch = null;
}
}
if (currentPath is not null) result.Add((currentPath, currentBranch));
return result;
}
public async Task WorktreePruneAsync(string repoDir, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["worktree", "prune"], ct);
@@ -382,6 +414,13 @@ public sealed class GitService
.ToList();
}
/// <summary>URL des Remotes <paramref name="remote"/>, oder null wenn es keins gibt.</summary>
public async Task<string?> GetRemoteUrlAsync(string repoDir, string remote = "origin", CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["remote", "get-url", remote], ct);
return exitCode == 0 && !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : null;
}
public async Task<bool> IsMidMergeAsync(string repoDir, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["rev-parse", "--git-dir"], ct);
@@ -392,14 +431,76 @@ public sealed class GitService
return File.Exists(Path.Combine(gitDir, "MERGE_HEAD"));
}
/// <param name="autostash">Stash the target working tree's uncommitted changes before the
/// merge and restore them after it. Git owns the bookkeeping: on a conflict the stash stays
/// pending and is re-applied by `merge --continue`/`--abort`, so a conflicted merge left in
/// the tree doesn't lose it. A restore that itself conflicts leaves the stash in
/// `git stash list` rather than dropping it.</param>
public async Task<(int ExitCode, string Stderr)> MergeNoFfAsync(
string repoDir, string sourceBranch, string message, CancellationToken ct = default)
string repoDir, string sourceBranch, string message, CancellationToken ct = default,
bool autostash = false)
{
// diff3 conflict style writes the merge base (|||||||) into conflict markers so the
// in-app resolver can show a true three-way view. It only enriches conflicted hunks;
// clean merges are unaffected.
string[] args = autostash
? ["-c", "merge.conflictStyle=diff3", "merge", "--no-ff", "--autostash", "-m", message, sourceBranch]
: ["-c", "merge.conflictStyle=diff3", "merge", "--no-ff", "-m", message, sourceBranch];
var (exitCode, _, stderr) = await RunGitAsync(repoDir, args, ct);
return (exitCode, stderr);
}
/// <summary>
/// <c>git fetch origin</c> for one branch into <c>refs/remotes/origin/&lt;branch&gt;</c>. The explicit
/// refspec works without a configured fetch refspec. <paramref name="env"/> carries the auth
/// header (never the command line) and must disable the terminal prompt.
/// </summary>
public async Task<(int ExitCode, string Stderr)> FetchBranchAsync(
string repoDir, string branch, IReadOnlyDictionary<string, string>? env, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
["-c", "merge.conflictStyle=diff3", "merge", "--no-ff", "-m", message, sourceBranch], ct);
["fetch", "origin", $"+refs/heads/{branch}:refs/remotes/origin/{branch}"], ct, env: env);
return (exitCode, stderr);
}
/// <summary>Moves the local, not checked-out <paramref name="branch"/> to <c>origin/&lt;branch&gt;</c>; git itself refuses a non-fast-forward. No network.</summary>
public async Task<(int ExitCode, string Stderr)> FastForwardLocalBranchAsync(
string repoDir, string branch, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
["fetch", ".", $"refs/remotes/origin/{branch}:refs/heads/{branch}"], ct);
return (exitCode, stderr);
}
/// <summary><c>git push -u origin &lt;branch&gt;</c> from <paramref name="worktreePath"/>; auth via <paramref name="env"/>.</summary>
public async Task<(int ExitCode, string Stderr)> PushBranchAsync(
string worktreePath, string branch, IReadOnlyDictionary<string, string>? env, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["push", "-u", "origin", branch], ct, env: env);
return (exitCode, stderr);
}
/// <summary>Branch that <c>refs/remotes/origin/HEAD</c> points to (without <c>origin/</c>), or null.</summary>
public async Task<string?> GetRemoteHeadBranchAsync(string repoDir, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], ct);
if (exitCode != 0 || string.IsNullOrWhiteSpace(stdout)) return null;
var head = stdout.Trim();
return head.StartsWith("origin/", StringComparison.Ordinal) ? head["origin/".Length..] : head;
}
/// <summary>Absolute path of the <c>.git</c> dir shared by all worktrees of the repo owning <paramref name="dir"/>, or null.</summary>
public async Task<string?> GetCommonDirAsync(string dir, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(dir, ["rev-parse", "--path-format=absolute", "--git-common-dir"], ct);
return exitCode == 0 && !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : null;
}
/// <summary><c>git merge --ff-only</c>: moves the checked-out branch to <paramref name="sourceRef"/> or fails without touching it.</summary>
public async Task<(int ExitCode, string Stderr)> MergeFastForwardOnlyAsync(
string worktreePath, string sourceRef, CancellationToken ct = default)
{
var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["merge", "--ff-only", sourceRef], ct);
return (exitCode, stderr);
}
@@ -518,6 +619,16 @@ public sealed class GitService
return exitCode == 0 ? stdout : null;
}
/// <summary>
/// Content of <paramref name="path"/> at <paramref name="revision"/> via `git show revision:path`.
/// Null when the path doesn't exist at that revision (new, deleted, or renamed file).
/// </summary>
public async Task<string?> ShowFileAsync(string repoDir, string revision, string path, CancellationToken ct = default)
{
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["show", $"{revision}:{path}"], ct, trimOutput: false);
return exitCode == 0 ? stdout : null;
}
/// <summary>
/// Non-destructive mergeability probe via `git merge-tree --write-tree`. Writes only
/// loose objects — the working tree, index, and refs are left untouched.
@@ -656,7 +767,8 @@ public sealed class GitService
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true)
string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null, bool trimOutput = true,
IReadOnlyDictionary<string, string>? env = null)
{
var psi = new ProcessStartInfo
{
@@ -672,6 +784,13 @@ public sealed class GitService
// (mktree "input format error") and would prepend to stdin commit messages.
StandardInputEncoding = stdinData is not null ? new UTF8Encoding(false) : null,
};
// Force English stderr/stdout so the English-only substring matches below
// (e.g. "ambiguous argument 'HEAD'", "tell me who you are") work regardless
// of the OS locale git for Windows picks up.
psi.Environment["LC_ALL"] = "C";
psi.Environment["LANGUAGE"] = "C";
if (env is not null)
foreach (var (key, value) in env) psi.Environment[key] = value;
psi.ArgumentList.Add("-C");
psi.ArgumentList.Add(workDir);
foreach (var a in args) psi.ArgumentList.Add(a);
@@ -0,0 +1,982 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260828185839_AddDailyNoteFromAgent")]
partial class AddDailyNoteFromAgent
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<bool>("AutoContinueOnUsageLimit")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("auto_continue_on_usage_limit");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<int>("DailyPrepMaxTasks")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(5)
.HasColumnName("daily_prep_max_tasks");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(40)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
b.Property<int>("MaxTurnsCeiling")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(80)
.HasColumnName("max_turns_ceiling");
b.Property<string>("ModelPresets")
.HasColumnType("TEXT")
.HasColumnName("model_presets");
b.Property<int>("NextTaskNumber")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("next_task_number");
b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
b.Property<string>("ReportExcludedPaths")
.HasColumnType("TEXT")
.HasColumnName("report_excluded_paths");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<int>("StandupWeekday")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<int>("UsageGateFiveHourPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(80)
.HasColumnName("usage_gate_five_hour_pct");
b.Property<int>("UsageGateSevenDayPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(90)
.HasColumnName("usage_gate_seven_day_pct");
b.Property<int>("UsageThrottleFiveHourHardPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(65)
.HasColumnName("usage_throttle_five_hour_hard_pct");
b.Property<int>("UsageThrottleFiveHourSoftPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(50)
.HasColumnName("usage_throttle_five_hour_soft_pct");
b.Property<int>("UsageThrottleSevenDayHardPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(65)
.HasColumnName("usage_throttle_seven_day_hard_pct");
b.Property<int>("UsageThrottleSevenDaySoftPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(50)
.HasColumnName("usage_throttle_seven_day_soft_pct");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
AutoContinueOnUsageLimit = false,
DailyPrepMaxTasks = 5,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 40,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
MaxParallelExecutions = 1,
MaxTurnsCeiling = 80,
NextTaskNumber = 1,
StandupWeekday = 3,
UsageGateFiveHourPct = 80,
UsageGateSevenDayPct = 90,
UsageThrottleFiveHourHardPct = 65,
UsageThrottleFiveHourSoftPct = 50,
UsageThrottleSevenDayHardPct = 65,
UsageThrottleSevenDaySoftPct = 50,
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("note_date");
b.Property<bool>("FromAgent")
.HasColumnType("INTEGER")
.HasColumnName("from_agent");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasColumnName("sort_order");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("text");
b.HasKey("Id");
b.HasIndex("Date");
b.ToTable("daily_notes", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("PermissionMode")
.HasColumnType("TEXT")
.HasColumnName("permission_mode");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<int?>("TicketProjectId")
.HasColumnType("INTEGER")
.HasColumnName("ticket_project_id");
b.Property<string>("VerifyCommand")
.HasColumnType("TEXT")
.HasColumnName("verify_command");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<bool>("FindingsTracked")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("findings_tracked");
b.Property<string>("Icon")
.HasColumnType("TEXT")
.HasColumnName("icon");
b.Property<bool>("IsManual")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_manual");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("Days")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(31)
.HasColumnName("days_of_week");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("enabled");
b.Property<string>("Kind")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("ping")
.HasColumnName("action_kind");
b.Property<DateTimeOffset?>("LastRunAt")
.HasColumnType("TEXT")
.HasColumnName("last_run_at");
b.Property<string>("PromptOverride")
.HasColumnType("TEXT")
.HasColumnName("prompt_override");
b.Property<TimeSpan>("TimeOfDay")
.HasColumnType("TEXT")
.HasColumnName("time_of_day");
b.HasKey("Id");
b.ToTable("prime_schedules", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
{
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<DateTimeOffset>("AddedAt")
.HasColumnType("TEXT")
.HasColumnName("added_at");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<string>("PinnedRef")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("pinned_ref");
b.Property<string>("SourceUrl")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("source_url");
b.Property<string>("Subpath")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("subpath");
b.HasKey("Name");
b.ToTable("session_skills", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<long>("ByteSize")
.HasColumnType("INTEGER")
.HasColumnName("byte_size");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("file_name");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_attachments_task_id");
b.ToTable("task_attachments", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("DependsOnTaskId")
.HasColumnType("TEXT")
.HasColumnName("depends_on_task_id");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<int?>("FailureMaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("failure_max_turns");
b.Property<string>("FailureReason")
.HasColumnType("TEXT")
.HasColumnName("failure_reason");
b.Property<int?>("FailureTurnsUsed")
.HasColumnType("INTEGER")
.HasColumnName("failure_turns_used");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<string>("HandlerBaseCommit")
.HasColumnType("TEXT")
.HasColumnName("handler_base_commit");
b.Property<string>("HandlerHeadCommit")
.HasColumnType("TEXT")
.HasColumnName("handler_head_commit");
b.Property<string>("InteractiveSessionId")
.HasColumnType("TEXT")
.HasColumnName("interactive_session_id");
b.Property<bool>("IsManual")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_manual");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<int>("Number")
.HasColumnType("INTEGER")
.HasColumnName("number");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<string>("PermissionMode")
.HasColumnType("TEXT")
.HasColumnName("permission_mode");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<string>("ReviewFeedback")
.HasColumnType("TEXT")
.HasColumnName("review_feedback");
b.Property<int>("RoadblockCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("roadblock_count");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("TicketRef")
.HasColumnType("TEXT")
.HasColumnName("ticket_ref");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.Property<DateTime?>("UsageLimitAutoContinuedAt")
.HasColumnType("TEXT")
.HasColumnName("usage_limit_auto_continued_at");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("DependsOnTaskId")
.HasDatabaseName("idx_tasks_depends_on");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Number")
.IsUnique()
.HasDatabaseName("idx_tasks_number");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<int?>("CacheReadTokens")
.HasColumnType("INTEGER")
.HasColumnName("cache_read_tokens");
b.Property<int?>("CacheWriteTokens")
.HasColumnType("INTEGER")
.HasColumnName("cache_write_tokens");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<string>("Errors")
.HasColumnType("TEXT")
.HasColumnName("errors");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<string>("ResultSubtype")
.HasColumnType("TEXT")
.HasColumnName("result_subtype");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("TerminalReason")
.HasColumnType("TEXT")
.HasColumnName("terminal_reason");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateOnly>("EndDate")
.HasColumnType("TEXT")
.HasColumnName("end_date");
b.Property<DateTime>("GeneratedAt")
.HasColumnType("TEXT")
.HasColumnName("generated_at");
b.Property<string>("Markdown")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("markdown");
b.Property<DateOnly>("StartDate")
.HasColumnType("TEXT")
.HasColumnName("start_date");
b.HasKey("Id");
b.HasIndex("StartDate", "EndDate")
.IsUnique();
b.ToTable("week_reports", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("MergeCommit")
.HasColumnType("TEXT")
.HasColumnName("merge_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany()
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("DependsOnTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddDailyNoteFromAgent : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "from_agent",
table: "daily_notes",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "from_agent",
table: "daily_notes");
}
}
}
@@ -0,0 +1,988 @@
// <auto-generated />
using System;
using ClaudeDo.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
[DbContext(typeof(ClaudeDoDbContext))]
[Migration("20260902102003_AddExpectsNoDiff")]
partial class AddExpectsNoDiff
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.11");
modelBuilder.Entity("ClaudeDo.Data.Models.AppSettingsEntity", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<bool>("AutoContinueOnUsageLimit")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("auto_continue_on_usage_limit");
b.Property<string>("CentralWorktreeRoot")
.HasColumnType("TEXT")
.HasColumnName("central_worktree_root");
b.Property<int>("DailyPrepMaxTasks")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(5)
.HasColumnName("daily_prep_max_tasks");
b.Property<string>("DefaultClaudeInstructions")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("")
.HasColumnName("default_claude_instructions");
b.Property<int>("DefaultMaxTurns")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(40)
.HasColumnName("default_max_turns");
b.Property<string>("DefaultModel")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sonnet")
.HasColumnName("default_model");
b.Property<string>("DefaultPermissionMode")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("max_parallel_executions");
b.Property<int>("MaxTurnsCeiling")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(80)
.HasColumnName("max_turns_ceiling");
b.Property<string>("ModelPresets")
.HasColumnType("TEXT")
.HasColumnName("model_presets");
b.Property<int>("NextTaskNumber")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("next_task_number");
b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
b.Property<string>("ReportExcludedPaths")
.HasColumnType("TEXT")
.HasColumnName("report_excluded_paths");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<int>("StandupWeekday")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<int>("UsageGateFiveHourPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(80)
.HasColumnName("usage_gate_five_hour_pct");
b.Property<int>("UsageGateSevenDayPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(90)
.HasColumnName("usage_gate_seven_day_pct");
b.Property<int>("UsageThrottleFiveHourHardPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(65)
.HasColumnName("usage_throttle_five_hour_hard_pct");
b.Property<int>("UsageThrottleFiveHourSoftPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(50)
.HasColumnName("usage_throttle_five_hour_soft_pct");
b.Property<int>("UsageThrottleSevenDayHardPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(65)
.HasColumnName("usage_throttle_seven_day_hard_pct");
b.Property<int>("UsageThrottleSevenDaySoftPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(50)
.HasColumnName("usage_throttle_seven_day_soft_pct");
b.Property<int>("WorktreeAutoCleanupDays")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(7)
.HasColumnName("worktree_auto_cleanup_days");
b.Property<bool>("WorktreeAutoCleanupEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("worktree_auto_cleanup_enabled");
b.Property<string>("WorktreeStrategy")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("sibling")
.HasColumnName("worktree_strategy");
b.HasKey("Id");
b.ToTable("app_settings", (string)null);
b.HasData(
new
{
Id = 1,
AutoContinueOnUsageLimit = false,
DailyPrepMaxTasks = 5,
DefaultClaudeInstructions = "",
DefaultMaxTurns = 40,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
MaxParallelExecutions = 1,
MaxTurnsCeiling = 80,
NextTaskNumber = 1,
StandupWeekday = 3,
UsageGateFiveHourPct = 80,
UsageGateSevenDayPct = 90,
UsageThrottleFiveHourHardPct = 65,
UsageThrottleFiveHourSoftPct = 50,
UsageThrottleSevenDayHardPct = 65,
UsageThrottleSevenDaySoftPct = 50,
WorktreeAutoCleanupDays = 7,
WorktreeAutoCleanupEnabled = false,
WorktreeStrategy = "sibling"
});
});
modelBuilder.Entity("ClaudeDo.Data.Models.DailyNoteEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("note_date");
b.Property<bool>("FromAgent")
.HasColumnType("INTEGER")
.HasColumnName("from_agent");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasColumnName("sort_order");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("text");
b.HasKey("Id");
b.HasIndex("Date");
b.ToTable("daily_notes", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.Property<string>("ListId")
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("PermissionMode")
.HasColumnType("TEXT")
.HasColumnName("permission_mode");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<int?>("TicketProjectId")
.HasColumnType("INTEGER")
.HasColumnName("ticket_project_id");
b.Property<string>("VerifyCommand")
.HasColumnType("TEXT")
.HasColumnName("verify_command");
b.HasKey("ListId");
b.ToTable("list_config", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DefaultCommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("default_commit_type");
b.Property<bool>("FindingsTracked")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("findings_tracked");
b.Property<string>("Icon")
.HasColumnType("TEXT")
.HasColumnName("icon");
b.Property<bool>("IsManual")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_manual");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<string>("WorkingDir")
.HasColumnType("TEXT")
.HasColumnName("working_dir");
b.HasKey("Id");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("Days")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(31)
.HasColumnName("days_of_week");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("enabled");
b.Property<string>("Kind")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("ping")
.HasColumnName("action_kind");
b.Property<DateTimeOffset?>("LastRunAt")
.HasColumnType("TEXT")
.HasColumnName("last_run_at");
b.Property<string>("PromptOverride")
.HasColumnType("TEXT")
.HasColumnName("prompt_override");
b.Property<TimeSpan>("TimeOfDay")
.HasColumnType("TEXT")
.HasColumnName("time_of_day");
b.HasKey("Id");
b.ToTable("prime_schedules", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SessionSkillEntity", b =>
{
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<DateTimeOffset>("AddedAt")
.HasColumnType("TEXT")
.HasColumnName("added_at");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<string>("PinnedRef")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("pinned_ref");
b.Property<string>("SourceUrl")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("source_url");
b.Property<string>("Subpath")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("subpath");
b.HasKey("Name");
b.ToTable("session_skills", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<bool>("Completed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("completed");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<int>("OrderNum")
.HasColumnType("INTEGER")
.HasColumnName("order_num");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_subtasks_task_id");
b.ToTable("subtasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<long>("ByteSize")
.HasColumnType("INTEGER")
.HasColumnName("byte_size");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("file_name");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_attachments_task_id");
b.ToTable("task_attachments", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("AgentPath")
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BlockedByTaskId")
.HasColumnType("TEXT")
.HasColumnName("blocked_by_task_id");
b.Property<string>("CommitType")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("chore")
.HasColumnName("commit_type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("CreatedBy")
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("DependsOnTaskId")
.HasColumnType("TEXT")
.HasColumnName("depends_on_task_id");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<bool>("ExpectsNoDiff")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("expects_no_diff");
b.Property<int?>("FailureMaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("failure_max_turns");
b.Property<string>("FailureReason")
.HasColumnType("TEXT")
.HasColumnName("failure_reason");
b.Property<int?>("FailureTurnsUsed")
.HasColumnType("INTEGER")
.HasColumnName("failure_turns_used");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<string>("HandlerBaseCommit")
.HasColumnType("TEXT")
.HasColumnName("handler_base_commit");
b.Property<string>("HandlerHeadCommit")
.HasColumnType("TEXT")
.HasColumnName("handler_head_commit");
b.Property<string>("InteractiveSessionId")
.HasColumnType("TEXT")
.HasColumnName("interactive_session_id");
b.Property<bool>("IsManual")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_manual");
b.Property<bool>("IsMyDay")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_my_day");
b.Property<bool>("IsStarred")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_starred");
b.Property<string>("ListId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("list_id");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Notes")
.HasColumnType("TEXT")
.HasColumnName("notes");
b.Property<int>("Number")
.HasColumnType("INTEGER")
.HasColumnName("number");
b.Property<string>("ParentTaskId")
.HasColumnType("TEXT")
.HasColumnName("parent_task_id");
b.Property<string>("PermissionMode")
.HasColumnType("TEXT")
.HasColumnName("permission_mode");
b.Property<DateTime?>("PlanningFinalizedAt")
.HasColumnType("TEXT")
.HasColumnName("planning_finalized_at");
b.Property<string>("PlanningPhase")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("none")
.HasColumnName("planning_phase");
b.Property<string>("PlanningSessionId")
.HasColumnType("TEXT")
.HasColumnName("planning_session_id");
b.Property<string>("PlanningSessionToken")
.HasColumnType("TEXT")
.HasColumnName("planning_session_token");
b.Property<string>("Result")
.HasColumnType("TEXT")
.HasColumnName("result");
b.Property<string>("ReviewFeedback")
.HasColumnType("TEXT")
.HasColumnName("review_feedback");
b.Property<int>("RoadblockCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("roadblock_count");
b.Property<DateTime?>("ScheduledFor")
.HasColumnType("TEXT")
.HasColumnName("scheduled_for");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("status");
b.Property<string>("SystemPrompt")
.HasColumnType("TEXT")
.HasColumnName("system_prompt");
b.Property<string>("TicketRef")
.HasColumnType("TEXT")
.HasColumnName("ticket_ref");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.Property<DateTime?>("UsageLimitAutoContinuedAt")
.HasColumnType("TEXT")
.HasColumnName("usage_limit_auto_continued_at");
b.HasKey("Id");
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("DependsOnTaskId")
.HasDatabaseName("idx_tasks_depends_on");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
b.HasIndex("Number")
.IsUnique()
.HasDatabaseName("idx_tasks_number");
b.HasIndex("ParentTaskId")
.HasDatabaseName("idx_tasks_parent_task_id");
b.HasIndex("Status")
.HasDatabaseName("idx_tasks_status");
b.HasIndex("ListId", "SortOrder")
.HasDatabaseName("idx_tasks_list_sort");
b.ToTable("tasks", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<int?>("CacheReadTokens")
.HasColumnType("INTEGER")
.HasColumnName("cache_read_tokens");
b.Property<int?>("CacheWriteTokens")
.HasColumnType("INTEGER")
.HasColumnName("cache_write_tokens");
b.Property<string>("ErrorMarkdown")
.HasColumnType("TEXT")
.HasColumnName("error_markdown");
b.Property<string>("Errors")
.HasColumnType("TEXT")
.HasColumnName("errors");
b.Property<int?>("ExitCode")
.HasColumnType("INTEGER")
.HasColumnName("exit_code");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("TEXT")
.HasColumnName("finished_at");
b.Property<bool>("IsRetry")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_retry");
b.Property<string>("LogPath")
.HasColumnType("TEXT")
.HasColumnName("log_path");
b.Property<string>("Model")
.HasColumnType("TEXT")
.HasColumnName("model");
b.Property<string>("Prompt")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("prompt");
b.Property<string>("ResultMarkdown")
.HasColumnType("TEXT")
.HasColumnName("result_markdown");
b.Property<string>("ResultSubtype")
.HasColumnType("TEXT")
.HasColumnName("result_subtype");
b.Property<int>("RunNumber")
.HasColumnType("INTEGER")
.HasColumnName("run_number");
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<DateTime?>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("StructuredOutputJson")
.HasColumnType("TEXT")
.HasColumnName("structured_output");
b.Property<string>("TaskId")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("TerminalReason")
.HasColumnType("TEXT")
.HasColumnName("terminal_reason");
b.Property<int?>("TokensIn")
.HasColumnType("INTEGER")
.HasColumnName("tokens_in");
b.Property<int?>("TokensOut")
.HasColumnType("INTEGER")
.HasColumnName("tokens_out");
b.Property<int?>("TurnCount")
.HasColumnType("INTEGER")
.HasColumnName("turn_count");
b.HasKey("Id");
b.HasIndex("TaskId")
.HasDatabaseName("idx_task_runs_task_id");
b.ToTable("task_runs", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WeekReportEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateOnly>("EndDate")
.HasColumnType("TEXT")
.HasColumnName("end_date");
b.Property<DateTime>("GeneratedAt")
.HasColumnType("TEXT")
.HasColumnName("generated_at");
b.Property<string>("Markdown")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("markdown");
b.Property<DateOnly>("StartDate")
.HasColumnType("TEXT")
.HasColumnName("start_date");
b.HasKey("Id");
b.HasIndex("StartDate", "EndDate")
.IsUnique();
b.ToTable("week_reports", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("BaseCommit")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("base_commit");
b.Property<string>("BranchName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("branch_name");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("DiffStat")
.HasColumnType("TEXT")
.HasColumnName("diff_stat");
b.Property<string>("HeadCommit")
.HasColumnType("TEXT")
.HasColumnName("head_commit");
b.Property<string>("MergeCommit")
.HasColumnType("TEXT")
.HasColumnName("merge_commit");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("active")
.HasColumnName("state");
b.HasKey("TaskId");
b.ToTable("worktrees", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithOne("Config")
.HasForeignKey("ClaudeDo.Data.Models.ListConfigEntity", "ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Subtasks")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskAttachmentEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany()
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("DependsOnTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Parent")
.WithMany("Children")
.HasForeignKey("ParentTaskId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("List");
b.Navigation("Parent");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskRunEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithMany("Runs")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.WorktreeEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
.WithOne("Worktree")
.HasForeignKey("ClaudeDo.Data.Models.WorktreeEntity", "TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.Navigation("Config");
b.Navigation("Tasks");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Navigation("Children");
b.Navigation("Runs");
b.Navigation("Subtasks");
b.Navigation("Worktree");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddExpectsNoDiff : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "expects_no_diff",
table: "tasks",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "expects_no_diff",
table: "tasks");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <summary>
/// Turns the single <c>tasks.depends_on_task_id</c> column into a set: one row per declared
/// "run after" edge in <c>task_dependencies</c>. Hand-edited after scaffolding so the data copy
/// sits between CreateTable and DropColumn — the scaffolded order dropped the column first and
/// would have discarded every existing link.
/// </summary>
public partial class AddTaskDependencies : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "task_dependencies",
columns: table => new
{
task_id = table.Column<string>(type: "TEXT", nullable: false),
depends_on_task_id = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_task_dependencies", x => new { x.task_id, x.depends_on_task_id });
table.ForeignKey(
name: "FK_task_dependencies_tasks_depends_on_task_id",
column: x => x.depends_on_task_id,
principalTable: "tasks",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_task_dependencies_tasks_task_id",
column: x => x.task_id,
principalTable: "tasks",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "idx_task_dependencies_depends_on",
table: "task_dependencies",
column: "depends_on_task_id");
// Every existing link becomes a one-element set. The old FK was ON DELETE SET NULL, so
// a surviving value always points at an existing task and can't violate the new FK.
migrationBuilder.Sql("""
INSERT INTO task_dependencies (task_id, depends_on_task_id)
SELECT id, depends_on_task_id FROM tasks WHERE depends_on_task_id IS NOT NULL
""");
migrationBuilder.DropForeignKey(
name: "FK_tasks_tasks_depends_on_task_id",
table: "tasks");
migrationBuilder.DropIndex(
name: "idx_tasks_depends_on",
table: "tasks");
migrationBuilder.DropColumn(
name: "depends_on_task_id",
table: "tasks");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "depends_on_task_id",
table: "tasks",
type: "TEXT",
nullable: true);
// Inherently lossy: a task with several predecessors keeps only one. There is no
// single-column shape that could hold the rest.
migrationBuilder.Sql("""
UPDATE tasks SET depends_on_task_id = (
SELECT td.depends_on_task_id FROM task_dependencies td
WHERE td.task_id = tasks.id
ORDER BY td.depends_on_task_id
LIMIT 1
)
""");
migrationBuilder.DropTable(
name: "task_dependencies");
migrationBuilder.CreateIndex(
name: "idx_tasks_depends_on",
table: "tasks",
column: "depends_on_task_id");
migrationBuilder.AddForeignKey(
name: "FK_tasks_tasks_depends_on_task_id",
table: "tasks",
column: "depends_on_task_id",
principalTable: "tasks",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddPowerUse : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "power_use_allowed",
table: "list_config",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "list_handler_max_handoffs",
table: "app_settings",
type: "INTEGER",
nullable: false,
defaultValue: 4);
migrationBuilder.AddColumn<bool>(
name: "power_use_enabled",
table: "app_settings",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
column: "list_handler_max_handoffs",
value: 4);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "power_use_allowed",
table: "list_config");
migrationBuilder.DropColumn(
name: "list_handler_max_handoffs",
table: "app_settings");
migrationBuilder.DropColumn(
name: "power_use_enabled",
table: "app_settings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddPowerUseWindow : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "power_use_handoff_pct",
table: "app_settings",
type: "INTEGER",
nullable: false,
defaultValue: 95);
migrationBuilder.AddColumn<string>(
name: "power_use_window_end",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "power_use_window_start",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
columns: new[] { "power_use_handoff_pct", "power_use_window_end", "power_use_window_start" },
values: new object[] { 95, null, null });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "power_use_handoff_pct",
table: "app_settings");
migrationBuilder.DropColumn(
name: "power_use_window_end",
table: "app_settings");
migrationBuilder.DropColumn(
name: "power_use_window_start",
table: "app_settings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListFolders : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "folder_id",
table: "lists",
type: "TEXT",
nullable: true);
migrationBuilder.CreateTable(
name: "list_folders",
columns: table => new
{
id = table.Column<string>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", nullable: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
icon = table.Column<string>(type: "TEXT", nullable: true),
sort_order = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
is_collapsed = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_list_folders", x => x.id);
});
migrationBuilder.CreateIndex(
name: "idx_lists_folder",
table: "lists",
column: "folder_id");
migrationBuilder.CreateIndex(
name: "idx_list_folders_sort",
table: "list_folders",
column: "sort_order");
migrationBuilder.AddForeignKey(
name: "FK_lists_list_folders_folder_id",
table: "lists",
column: "folder_id",
principalTable: "list_folders",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_lists_list_folders_folder_id",
table: "lists");
migrationBuilder.DropTable(
name: "list_folders");
migrationBuilder.DropIndex(
name: "idx_lists_folder",
table: "lists");
migrationBuilder.DropColumn(
name: "folder_id",
table: "lists");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddMissionControlSessions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "mission_control_sessions",
columns: table => new
{
session_id = table.Column<string>(type: "TEXT", nullable: false),
cwd = table.Column<string>(type: "TEXT", nullable: false),
task_id = table.Column<string>(type: "TEXT", nullable: true),
title = table.Column<string>(type: "TEXT", nullable: false),
started_at = table.Column<DateTime>(type: "TEXT", nullable: false),
predecessor_session_id = table.Column<string>(type: "TEXT", nullable: true),
brief_path = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_mission_control_sessions", x => x.session_id);
});
migrationBuilder.CreateIndex(
name: "IX_mission_control_sessions_started_at",
table: "mission_control_sessions",
column: "started_at");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "mission_control_sessions");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddNestedListFolders : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "parent_folder_id",
table: "list_folders",
type: "TEXT",
nullable: true);
migrationBuilder.CreateIndex(
name: "idx_list_folders_parent",
table: "list_folders",
column: "parent_folder_id");
migrationBuilder.AddForeignKey(
name: "FK_list_folders_list_folders_parent_folder_id",
table: "list_folders",
column: "parent_folder_id",
principalTable: "list_folders",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_list_folders_list_folders_parent_folder_id",
table: "list_folders");
migrationBuilder.DropIndex(
name: "idx_list_folders_parent",
table: "list_folders");
migrationBuilder.DropColumn(
name: "parent_folder_id",
table: "list_folders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListFolderColor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "color",
table: "list_folders",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "color",
table: "list_folders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddTicketFallbackList : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ticket_fallback_list_id",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
column: "ticket_fallback_list_id",
value: null);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ticket_fallback_list_id",
table: "app_settings");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListColor : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "color",
table: "lists",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "color",
table: "lists");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddListBaseBranch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "base_branch",
table: "list_config",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "base_branch",
table: "list_config");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddWorktreePullRequestUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "pull_request_url",
table: "worktrees",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "pull_request_url",
table: "worktrees");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ClaudeDo.Data.Migrations
{
/// <inheritdoc />
public partial class AddSshWorkspaces : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ssh_workspaces",
table: "app_settings",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "app_settings",
keyColumn: "id",
keyValue: 1,
column: "ssh_workspaces",
value: null);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ssh_workspaces",
table: "app_settings");
}
}
}
@@ -66,6 +66,12 @@ namespace ClaudeDo.Data.Migrations
.HasDefaultValue("bypassPermissions")
.HasColumnName("default_permission_mode");
b.Property<int>("ListHandlerMaxHandoffs")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(4)
.HasColumnName("list_handler_max_handoffs");
b.Property<int>("MaxParallelExecutions")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -88,6 +94,26 @@ namespace ClaudeDo.Data.Migrations
.HasDefaultValue(1)
.HasColumnName("next_task_number");
b.Property<bool>("PowerUseEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("power_use_enabled");
b.Property<int>("PowerUseHandoffPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(95)
.HasColumnName("power_use_handoff_pct");
b.Property<string>("PowerUseWindowEnd")
.HasColumnType("TEXT")
.HasColumnName("power_use_window_end");
b.Property<string>("PowerUseWindowStart")
.HasColumnType("TEXT")
.HasColumnName("power_use_window_start");
b.Property<string>("RepoImportFolders")
.HasColumnType("TEXT")
.HasColumnName("repo_import_folders");
@@ -100,12 +126,20 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("session_skills");
b.Property<string>("SshWorkspaces")
.HasColumnType("TEXT")
.HasColumnName("ssh_workspaces");
b.Property<int>("StandupWeekday")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(3)
.HasColumnName("standup_weekday");
b.Property<string>("TicketFallbackListId")
.HasColumnType("TEXT")
.HasColumnName("ticket_fallback_list_id");
b.Property<int>("UsageGateFiveHourPct")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -175,9 +209,12 @@ namespace ClaudeDo.Data.Migrations
DefaultMaxTurns = 40,
DefaultModel = "sonnet",
DefaultPermissionMode = "auto",
ListHandlerMaxHandoffs = 4,
MaxParallelExecutions = 1,
MaxTurnsCeiling = 80,
NextTaskNumber = 1,
PowerUseEnabled = false,
PowerUseHandoffPct = 95,
StandupWeekday = 3,
UsageGateFiveHourPct = 80,
UsageGateSevenDayPct = 90,
@@ -205,6 +242,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("note_date");
b.Property<bool>("FromAgent")
.HasColumnType("INTEGER")
.HasColumnName("from_agent");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER")
.HasColumnName("sort_order");
@@ -231,6 +272,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("agent_path");
b.Property<string>("BaseBranch")
.HasColumnType("TEXT")
.HasColumnName("base_branch");
b.Property<int?>("MaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("max_turns");
@@ -243,6 +288,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("permission_mode");
b.Property<bool>("PowerUseAllowed")
.HasColumnType("INTEGER")
.HasColumnName("power_use_allowed");
b.Property<string>("SessionSkills")
.HasColumnType("TEXT")
.HasColumnName("session_skills");
@@ -270,6 +319,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("Color")
.HasColumnType("TEXT")
.HasColumnName("color");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
@@ -287,6 +340,10 @@ namespace ClaudeDo.Data.Migrations
.HasDefaultValue(false)
.HasColumnName("findings_tracked");
b.Property<string>("FolderId")
.HasColumnType("TEXT")
.HasColumnName("folder_id");
b.Property<string>("Icon")
.HasColumnType("TEXT")
.HasColumnName("icon");
@@ -314,12 +371,104 @@ namespace ClaudeDo.Data.Migrations
b.HasKey("Id");
b.HasIndex("FolderId")
.HasDatabaseName("idx_lists_folder");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_lists_sort");
b.ToTable("lists", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListFolderEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("Color")
.HasColumnType("TEXT")
.HasColumnName("color");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("Icon")
.HasColumnType("TEXT")
.HasColumnName("icon");
b.Property<bool>("IsCollapsed")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_collapsed");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("ParentFolderId")
.HasColumnType("TEXT")
.HasColumnName("parent_folder_id");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("sort_order");
b.HasKey("Id");
b.HasIndex("ParentFolderId")
.HasDatabaseName("idx_list_folders_parent");
b.HasIndex("SortOrder")
.HasDatabaseName("idx_list_folders_sort");
b.ToTable("list_folders", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.MissionControlSessionEntity", b =>
{
b.Property<string>("SessionId")
.HasColumnType("TEXT")
.HasColumnName("session_id");
b.Property<string>("BriefPath")
.HasColumnType("TEXT")
.HasColumnName("brief_path");
b.Property<string>("Cwd")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("cwd");
b.Property<string>("PredecessorSessionId")
.HasColumnType("TEXT")
.HasColumnName("predecessor_session_id");
b.Property<DateTime>("StartedAt")
.HasColumnType("TEXT")
.HasColumnName("started_at");
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("SessionId");
b.HasIndex("StartedAt");
b.ToTable("mission_control_sessions", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.PrimeScheduleEntity", b =>
{
b.Property<Guid>("Id")
@@ -471,6 +620,24 @@ namespace ClaudeDo.Data.Migrations
b.ToTable("task_attachments", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskDependencyEntity", b =>
{
b.Property<string>("TaskId")
.HasColumnType("TEXT")
.HasColumnName("task_id");
b.Property<string>("DependsOnTaskId")
.HasColumnType("TEXT")
.HasColumnName("depends_on_task_id");
b.HasKey("TaskId", "DependsOnTaskId");
b.HasIndex("DependsOnTaskId")
.HasDatabaseName("idx_task_dependencies_depends_on");
b.ToTable("task_dependencies", (string)null);
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.Property<string>("Id")
@@ -500,14 +667,16 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("created_by");
b.Property<string>("DependsOnTaskId")
.HasColumnType("TEXT")
.HasColumnName("depends_on_task_id");
b.Property<string>("Description")
.HasColumnType("TEXT")
.HasColumnName("description");
b.Property<bool>("ExpectsNoDiff")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("expects_no_diff");
b.Property<int?>("FailureMaxTurns")
.HasColumnType("INTEGER")
.HasColumnName("failure_max_turns");
@@ -665,9 +834,6 @@ namespace ClaudeDo.Data.Migrations
b.HasIndex("BlockedByTaskId")
.HasDatabaseName("idx_tasks_blocked_by");
b.HasIndex("DependsOnTaskId")
.HasDatabaseName("idx_tasks_depends_on");
b.HasIndex("ListId")
.HasDatabaseName("idx_tasks_list_id");
@@ -857,6 +1023,10 @@ namespace ClaudeDo.Data.Migrations
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<string>("PullRequestUrl")
.HasColumnType("TEXT")
.HasColumnName("pull_request_url");
b.Property<string>("State")
.IsRequired()
.ValueGeneratedOnAdd()
@@ -880,6 +1050,22 @@ namespace ClaudeDo.Data.Migrations
b.Navigation("List");
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListFolderEntity", null)
.WithMany()
.HasForeignKey("FolderId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("ClaudeDo.Data.Models.ListFolderEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.ListFolderEntity", null)
.WithMany()
.HasForeignKey("ParentFolderId")
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity("ClaudeDo.Data.Models.SubtaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", "Task")
@@ -902,6 +1088,21 @@ namespace ClaudeDo.Data.Migrations
b.Navigation("Task");
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskDependencyEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("DependsOnTaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany("Dependencies")
.HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b =>
{
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
@@ -909,11 +1110,6 @@ namespace ClaudeDo.Data.Migrations
.HasForeignKey("BlockedByTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.TaskEntity", null)
.WithMany()
.HasForeignKey("DependsOnTaskId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ClaudeDo.Data.Models.ListEntity", "List")
.WithMany("Tasks")
.HasForeignKey("ListId")
@@ -963,6 +1159,8 @@ namespace ClaudeDo.Data.Migrations
{
b.Navigation("Children");
b.Navigation("Dependencies");
b.Navigation("Runs");
b.Navigation("Subtasks");
@@ -29,6 +29,8 @@ public sealed class AppSettingsEntity
// JSON array of parent folders remembered by the repo-import modal.
public string? RepoImportFolders { get; set; }
// JSON array of SshWorkspace rows offered as remote quick sessions in Mission Control.
public string? SshWorkspaces { get; set; }
// JSON array of path prefixes whose sessions are excluded from the weekly report.
public string? ReportExcludedPaths { get; set; }
// DayOfWeek the standup happens on; default Wednesday. Drives the report's default range.
@@ -60,4 +62,31 @@ public sealed class AppSettingsEntity
// automatic ContinueTask once the 5h window resets, and the gate-blocked queue wakes exactly
// at that reset time instead of only on the 30s backstop.
public bool AutoContinueOnUsageLimit { get; set; }
// Master switch for PowerUse: list-handler runs go unattended (never ask the user) and harvest
// new work when a round would otherwise starve. Toggled in Settings → Execution. Off by default,
// and on its own it changes nothing — a run is only in PowerUse when the LIST also opted in via
// ListConfigEntity.PowerUseAllowed, so leaving this on cannot make an unrelated list's handler
// run unattended. Composition lives in InteractiveLaunchSpecService.
public bool PowerUseEnabled { get; set; }
// Optional local-time window (both `HH:mm`) narrowing when PowerUse applies, checked per phase
// at prompt-build time. Both null (the default) = round the clock, byte-identical to the
// pre-window behaviour; only one set counts as unset too. End before start wraps over midnight.
// Semantics live in PowerUseWindow.
public string? PowerUseWindowStart { get; set; }
public string? PowerUseWindowEnd { get; set; }
// Percentage of the 5h usage window at which a PowerUse handler session is told to hand off
// rather than start more work. Stored and displayed here; the consumer is the handoff prompt.
public int PowerUseHandoffPct { get; set; } = 95;
// Server-side cap on a list-handler run's wait<->merge handoff chain (HandoffRoundTracker).
// 4 = two full wait/merge cycles, the long-standing default. Raise it for a run meant to keep
// going for hours. It exists to bound runaway handoff loops, so it must stay >= 1.
public int ListHandlerMaxHandoffs { get; set; } = 4;
// List that receives imported ticket items Claude could not place (import_ticket without a
// list). Null = such items are skipped. No FK: a deleted list simply reads as unset at import.
public string? TicketFallbackListId { get; set; }
}
@@ -6,5 +6,9 @@ public sealed class DailyNoteEntity
public DateOnly Date { get; set; }
public string Text { get; set; } = string.Empty;
public int SortOrder { get; set; }
/// <summary>True when an agent wrote this note through the MCP tool rather than the user.
/// Drives the UI marker and the per-day cap in <c>NotesMcpTools</c>.</summary>
public bool FromAgent { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -17,6 +17,18 @@ public sealed class ListConfigEntity
// Feld mitführen, sonst setzt es sich still zurück.
public int? TicketProjectId { get; set; }
// Per-Liste-Opt-in für PowerUse. Ein Handler-Run läuft nur unattended, wenn BEIDES gesetzt ist:
// dieses Flag und AppSettingsEntity.PowerUseEnabled. Der globale Toggle bleibt damit gefahrlos
// an — eine Liste, die hier nicht zugestimmt hat, behält das interaktive Verhalten. Default
// false, also opt-in. ⚠️ Gleiche Falle wie TicketProjectId: SetConfigAsync kopiert verbatim.
public bool PowerUseAllowed { get; set; }
/// <summary>
/// Basis neuer Worktrees und Default-Merge-/PR-Ziel (z.B. <c>dev</c>). Null = ausgecheckter Branch
/// (privat) bzw. Default-Branch des Remotes (Firma). ⚠️ SetConfigAsync kopiert verbatim.
/// </summary>
public string? BaseBranch { get; set; }
// Navigation property
public ListEntity List { get; set; } = null!;
}
+5
View File
@@ -16,6 +16,11 @@ public sealed class ListEntity
public bool FindingsTracked { get; set; }
// Icon key into the UI's icon library (e.g. "Star"); null = the default folder icon.
public string? Icon { get; set; }
// Dot colour: a swatch key ("Swatch3") or a literal "#RRGGBB"; null = the hash-derived default.
public string? Color { get; set; }
// Sidebar folder this list sits in (ListFolderEntity.Id); null = top level. ON DELETE SET NULL,
// so deleting a folder moves its lists back to the top level instead of deleting them.
public string? FolderId { get; set; }
// Navigation properties
public ListConfigEntity? Config { get; set; }
@@ -0,0 +1,25 @@
namespace ClaudeDo.Data.Models;
/// <summary>
/// A sidebar folder grouping user lists. Deliberately its own table rather than a flag on
/// <see cref="ListEntity"/>: every place that enumerates lists (MCP list_task_lists, "move to
/// list", the queue picker, repo import) would otherwise have to filter folders out.
/// Folders nest arbitrarily deep via <see cref="ParentFolderId"/>.
/// </summary>
public sealed class ListFolderEntity
{
public required string Id { get; init; }
public required string Name { get; set; }
public required DateTime CreatedAt { get; init; }
// Icon key into the UI's icon library, same set as ListEntity.Icon; null = default folder icon.
public string? Icon { get; set; }
/// <summary>Hex colour (#RRGGBB) for the sidebar name and guide line; null = theme default.</summary>
public string? Color { get; set; }
public int SortOrder { get; set; }
// Sidebar expand/collapse state. Lives here rather than in ui.config.json because the row is
// already being read for the folder itself.
public bool IsCollapsed { get; set; }
// Parent folder, or null for a top-level folder. Self-FK, ON DELETE SET NULL — deleting a
// folder releases its subfolders to the top level, same as it does for its lists.
public string? ParentFolderId { get; set; }
}
@@ -0,0 +1,27 @@
namespace ClaudeDo.Data.Models;
/// <summary>One claude session started in a Mission Control tile (ad-hoc, task or handoff) — the
/// index behind "Recent sessions". The transcript itself stays with Claude Code.</summary>
public sealed class MissionControlSessionEntity
{
/// <summary>The claude session id (`--session-id` / `--resume`).</summary>
public string SessionId { get; init; } = string.Empty;
/// <summary>Working directory the session ran in — a resume must start there.</summary>
public string Cwd { get; set; } = string.Empty;
/// <summary>Task the tile belonged to; null for an ad-hoc tile. No FK: the row outlives the task.</summary>
public string? TaskId { get; set; }
/// <summary>Tile title at start (task title or folder name; a handoff inherits its predecessor's).</summary>
public string Title { get; set; } = string.Empty;
/// <summary>UTC time the session was first launched.</summary>
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
/// <summary>Session this one was handed off from; null unless it is a handoff session.</summary>
public string? PredecessorSessionId { get; set; }
/// <summary>brief.md the session started from; non-null exactly for handoff sessions.</summary>
public string? BriefPath { get; set; }
}
@@ -0,0 +1,32 @@
namespace ClaudeDo.Data.Models;
/// <summary>
/// The optional local-time window restricting when PowerUse applies
/// (<see cref="AppSettingsEntity.PowerUseWindowStart"/> / <c>End</c>, both <c>HH:mm</c>).
/// Pure on purpose — the caller supplies "now", so the window logic is testable without a clock.
/// </summary>
public static class PowerUseWindow
{
/// <summary>Parses an <c>HH:mm</c> string; anything else (including null/blank) is null.</summary>
public static TimeOnly? Parse(string? value)
=> TimeOnly.TryParseExact(value?.Trim(), "HH:mm", out var t) ? t : null;
/// <summary>Round-trips a stored/typed value through <see cref="Parse"/>, so an unparseable
/// string is stored as null instead of sitting in the column pretending to be a window.</summary>
public static string? Normalize(string? value) => Parse(value)?.ToString("HH:mm");
/// <summary>
/// True when <paramref name="now"/> falls inside the window. Half-open: start is included,
/// end is not. Either bound missing = no window at all = always true (the pre-window
/// behaviour). A window whose end is before its start wraps over midnight.
/// </summary>
public static bool Contains(TimeOnly now, TimeOnly? start, TimeOnly? end)
{
if (start is not { } from || end is not { } to) return true;
return from < to ? now >= from && now < to : now >= from || now < to;
}
/// <summary>Convenience overload for the two stored strings and a wall clock.</summary>
public static bool IsOpen(DateTime now, string? start, string? end)
=> Contains(TimeOnly.FromDateTime(now), Parse(start), Parse(end));
}
+3 -1
View File
@@ -8,5 +8,7 @@ public enum PrimeActionKind
/// <summary>Run the daily prep prompt and let Claude fill MyDay.</summary>
FillMyDay,
/// <summary>Run the schedule's own prompt with the ClaudeDo MCP tools available.</summary>
Custom
Custom,
/// <summary>Import the assigned ticket-system items into lists by content, then fill MyDay.</summary>
ImportTickets
}
+29
View File
@@ -0,0 +1,29 @@
using System.Text.RegularExpressions;
namespace ClaudeDo.Data.Models;
/// <summary>A server an interactive claude session can be started on via ssh; connection details live in the user's ssh config.</summary>
public sealed record SshWorkspace(string Id, string Name, string Host, string RemotePath)
{
// A leading '-' would make ssh parse the alias as an option (-oProxyCommand=... runs local code).
private static readonly Regex HostPattern = new(@"^[A-Za-z0-9._@][A-Za-z0-9._@-]*$", RegexOptions.Compiled);
/// <summary>Returns the first invalid field ("name", "host", "path") or null when valid.</summary>
public static string? Validate(SshWorkspace w)
{
if (string.IsNullOrWhiteSpace(w.Name)) return "name";
if (string.IsNullOrEmpty(w.Host) || !HostPattern.IsMatch(w.Host)) return "host";
var p = w.RemotePath ?? "";
if (!(p.StartsWith('/') || p.StartsWith('~')) || p.Contains('\'') || p.Contains('\n') || p.Contains('\r'))
return "path";
return null;
}
/// <summary>Single-quotes a validated remote path for a POSIX shell, leaving a leading <c>~</c> unquoted so it still expands.</summary>
public static string QuoteRemotePath(string path)
{
if (path == "~") return "~";
if (path.StartsWith("~/")) return "~/'" + path[2..] + "'";
return "'" + path + "'";
}
}
@@ -0,0 +1,13 @@
namespace ClaudeDo.Data.Models;
/// <summary>
/// One declared "task must run after task" edge. A task can carry n of them; the queue picker
/// claims it only once every predecessor reached <see cref="TaskStatus.Done"/>.
/// Deliberately separate from <see cref="TaskEntity.BlockedByTaskId"/>, the planning chain's own
/// internal single link — see ClaudeDo.Worker/CLAUDE.md → Status Model.
/// </summary>
public sealed class TaskDependencyEntity
{
public required string TaskId { get; init; }
public required string DependsOnTaskId { get; init; }
}
+17 -6
View File
@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace ClaudeDo.Data.Models;
public enum TaskStatus
@@ -33,12 +35,17 @@ public sealed class TaskEntity
public PlanningPhase PlanningPhase { get; set; } = PlanningPhase.None;
public string? BlockedByTaskId { get; set; }
// A user/MCP-declared predecessor, distinct from BlockedByTaskId (the planning chain's own
// internal link): the picker also skips a Queued task while this is set and the referenced
// task's Status isn't Done. Unlike the chain, a Failed/Cancelled dependency does NOT cascade
// or auto-resolve -- the dependent just stays blocked and reports why (see TaskStateService.
// SetDependsOnAsync and QueuePicker).
public string? DependsOnTaskId { get; set; }
// User/MCP-declared predecessors, distinct from BlockedByTaskId (the planning chain's own
// internal link): the picker skips a Queued task until EVERY predecessor reached Done.
// Unlike the chain, a Failed/Cancelled dependency does NOT cascade or auto-resolve -- the
// dependent just stays blocked and reports why (see TaskStateService.SetDependsOnAsync and
// QueuePicker). Only loaded where a read path Includes it; the picker gates in raw SQL and
// never relies on this collection.
public ICollection<TaskDependencyEntity> Dependencies { get; set; } = new List<TaskDependencyEntity>();
[NotMapped]
public IReadOnlyList<string> DependsOnTaskIds =>
Dependencies.Select(d => d.DependsOnTaskId).ToList();
public DateTime? ScheduledFor { get; set; }
public string? Result { get; set; }
public string? ReviewFeedback { get; set; }
@@ -71,6 +78,10 @@ public sealed class TaskEntity
// list handler) and the Claude affordances are hidden; a hand-driven ConPTY session is still
// allowed. New tasks in a manual list default to true.
public bool IsManual { get; set; }
// Set on verification/audit/research tasks whose correct outcome is an empty diff (e.g. "build
// and test, don't change code"). Lets the merge-helper session tell that apart from a task that
// silently failed to commit -- both look identical to preview_merge_set otherwise.
public bool ExpectsNoDiff { get; set; }
public string? Notes { get; set; }
public int SortOrder { get; set; }
public string? SessionSkills { get; set; }
@@ -17,6 +17,8 @@ public sealed class WorktreeEntity
public string? HeadCommit { get; set; }
public string? DiffStat { get; set; }
public string? MergeCommit { get; set; }
/// <summary>Draft-PR auf Azure DevOps, gesetzt vom Approve im Firmen-Repo.</summary>
public string? PullRequestUrl { get; set; }
public WorktreeState State { get; set; } = WorktreeState.Active;
public required DateTime CreatedAt { get; init; }
+152 -29
View File
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
namespace ClaudeDo.Data;
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelperTriage, MergeHelperWait, MergeHelperMerge, MergeHelperInitial, MergeHelperHandoff }
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelperTriage, MergeHelperWait, MergeHelperMerge, MergeHelperInitial, MergeHelperHandoff, PowerUseAddendum, TicketImport }
/// <summary>
/// How a prompt kind's on-disk override (if any) relates to the bundled default.
@@ -44,6 +44,8 @@ public static class PromptFiles
PromptKind.MergeHelperMerge => "merge-helper-merge.md",
PromptKind.MergeHelperInitial => "merge-helper-initial.md",
PromptKind.MergeHelperHandoff => "merge-helper-handoff.md",
PromptKind.PowerUseAddendum => "power-use.md",
PromptKind.TicketImport => "ticket-import.md",
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
@@ -221,6 +223,8 @@ public static class PromptFiles
PromptKind.MergeHelperMerge => MergeHelperMergeDefault,
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
PromptKind.MergeHelperHandoff => MergeHelperHandoffDefault,
PromptKind.PowerUseAddendum => PowerUseAddendumDefault,
PromptKind.TicketImport => TicketImportDefault,
_ => ""
};
@@ -250,10 +254,7 @@ public static class PromptFiles
it, name the follow-up in your final report instead of trying to file it.
## Working in the repo
- Read a file before editing it. Match the conventions already in this codebase —
they override generic defaults.
- Prefer editing existing files to creating new ones. Don't write comments that
just restate the code.
- Match the conventions already in this codebase — they override generic defaults.
- Validate only at real boundaries (user input, external APIs).
## Reading efficiently
@@ -317,11 +318,7 @@ public static class PromptFiles
renames, reformatting, or "while I'm here" cleanup beyond what is asked.
- Touch as few files as possible. Do not restructure unrelated code.
- Do NOT file further improvements — improvements are one layer deep.
- Verify the build and relevant tests before finishing, and report what you ran.
- Make one focused commit in this task's own worktree using the repository's
commit-message convention — that commit is the deliverable, not an unrequested auto-commit.
Report the real outcome, including the commit SHA; don't say no commit was
made when there was.
- Make one focused commit; the Finishing rules above apply unchanged.
""";
private const string PlanningSystemDefault = """
@@ -383,6 +380,22 @@ public static class PromptFiles
(build + tests) before you stop.
""";
private const string TicketImportDefault = """
You are sorting my assigned items from the company ticket system into my ClaudeDo lists.
1. Call mcp__claudedo__get_ticket_import_candidates. If available is false, say why and stop.
If items is empty, say so and stop.
2. For every item, pick the list whose purpose fits the item's CONTENT — its title, its
parent and its project. A list's ticketProject is only a hint: one ticket project can
hold work for several repositories, and several lists can share one project.
3. Import each item exactly once with mcp__claudedo__import_ticket(ticketId, list).
If no list clearly fits, omit list — the item then goes to the fallback list.
Never force a weak match just to avoid the fallback.
4. Do not create, edit or move anything else.
5. Finish with a short summary: imported items per list, items that went to the fallback
list, and skipped items with their reason.
""";
private const string DailyPrepDefault = """
You are preparing my workday for {date}.
@@ -448,6 +461,15 @@ public static class PromptFiles
Cancel nothing without an explicit answer.
## Phase 1.5 — Audit dependencies
For every surviving task that carries a dependsOnTaskId, decide whether the link is real or artificial before you hand off. Real: task B genuinely needs something task A produces — a symbol/type B's code references, a changed signature, a migration that must land before repository code assumes it. Artificial: the tasks just got numbered in sequence by the plan but touch disjoint files for unrelated reasons. Task descriptions usually name their target files explicitly — use that to check for real file/symbol overlap instead of trusting the order.
Resolve every artificial link with update_task(taskId, dependsOnTaskId="") — a chain numbered 1→2→3→... is the default shape to suspect, not the default shape to trust.
Group what's left into waves: batches that can run in parallel, where a later wave depends only on tasks in an earlier one. Report this as a wave plan (e.g. "[3] → [4,5,6] → [7]"), not a flat list — the Wait/Merge sessions after you need the batching, not just which ids survived.
Then check the wave count against the handoff cap: call get_handoff_status with your own handler task id — it reports how many handoffs (wait, merge, wait_final, merge_final) this run still has, i.e. how many merge rounds. If your wave plan needs more rounds than that to clear, say so explicitly when you hand off — name which waves won't fit — so the Merge session ends by telling the user "the rest needs manual approval in the UI" instead of silently stranding mid-chain.
## Phase 2 — Enhance for execution
Before touching any task, check the list's verify command ONCE for this whole run (not per task): call get_list_config. If verifyCommand is already set, leave it alone — never overwrite an existing one — and move straight to sharpening tasks below.
@@ -471,7 +493,7 @@ public static class PromptFiles
Once every surviving task is enhanced, print your triage summary — one line per task from the brief:
title — dedupe action (kept / merged into X / cancelled as duplicate of X) — enhanced (yes/no).
Then call handoff_list_handler(taskId, survivingTaskIds, nextPhase: "wait"). That opens a fresh session to carry out the wait/merge phases with just that list, without dragging along this session's dedupe/rewrite context. Say a short goodbye line, then stop — do not continue into phase 3 yourself.
Then print the wave plan from Phase 1.5, including any "won't fit in two rounds" note, and call handoff_list_handler(taskId, survivingTaskIds, nextPhase: "wait"). That opens a fresh session for the wait/merge phases. Say a short goodbye line, then stop — do not continue into phase 3 yourself.
""";
private const string MergeHelperWaitDefault = """
@@ -486,8 +508,6 @@ public static class PromptFiles
Touch nothing else: do not read a diff, merge, reset, restart, or cancel any task. A task that lands in Failed is simply noted and passed on — deciding what to do about it belongs to the Merge session, not you.
MaxParallelExecutions is deliberately not reported here — the queue enforces it on its own; that reporting was the old combined phase's job and no longer belongs to this role.
## Handoff
Once every task has left Queued/Running/WaitingForChildren, call handoff_list_handler(taskId, <every task id from this round>, nextPhase: "merge") — or nextPhase: "merge_final" if your own phase was "wait_final". Then stop your turn immediately.
""";
@@ -505,14 +525,14 @@ public static class PromptFiles
One task at a time. Default to the order the brief lists them. If the overlap check shows two or more tasks touching the same file, tell the user which tasks collide and merge those in an order you can justify (e.g. the one making the smaller change first) — deviate from brief order only with that stated reason.
1. Never read a task's diff yourself. Instead start a sonnet subagent (Task tool) per task to inspect get_task_diff and sanity-check it against the task's title and description; only its short verdict (clean / risky + reason) comes back into your own context. If preview_merge_set flagged this task in an overlap, tell the subagent to also skim the diff of the other task(s) sharing that file.
2. Before approving, cross-check changedFileCount from preview_merge_set (or the length of get_task_diff's files list — the diff tool has no changedFileCount field). 0 changed files means an empty branch, not a clean success — do not approve it; treat it like a risky diff instead.
1. Check this task's changedFileCount from preview_merge_set first. Below 5 changed files, read get_task_diff yourself and sanity-check it against the task's title and description — a subagent hop is wasted context for a diff that small. At 5 or more, delegate instead: start a sonnet subagent (Task tool) to inspect get_task_diff and sanity-check it, so only its short verdict (clean / risky + reason) comes back into your own context — the whole point of never reading a large diff yourself. If preview_merge_set flagged this task in an overlap, also skim (or have the subagent skim) the diff of the other task(s) sharing that file.
2. Before approving, cross-check changedFileCount from preview_merge_set (or the length of get_task_diff's files list — the diff tool has no changedFileCount field). 0 changed files means an empty branch, not a clean success — do not approve it; treat it like a risky diff instead. Exception: if preview_merge_set's expectsNoDiff is true for this task (a verification/audit/research task deliberately expected to change nothing), 0 changed files is the correct outcome — read the task's Result to confirm it reports what was checked and that it passed, then approve normally.
3. If the subagent's verdict is risky, or the change looks wrong or incomplete, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
4. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true, skipVerify=true). skipVerify defers the list's verify command to ONE verify_merges call after the last merge (below) instead of paying it per merge.
- Clean merge → mergeStatus "merged_verify_pending" (expected: the task deliberately stays WaitingForReview until verify_merges passes) or "merged" (list has no verify command; the task is Done). Either way, move on.
- Conflict (markers left in the working tree, repoPath returned) → resolve it below.
After each merge, main has moved — a clean preview_merge_set result from before this merge is now stale for the remaining tasks. If you are unsure whether an earlier overlap warning still matters, call preview_merge for the next task again before merging it.
After each merge, main has moved — a clean preview_merge_set result from before this merge is now stale for the remaining tasks. If you are unsure whether an earlier overlap warning still matters, call preview_merge for the next task again before merging it (default runVerify=false — this is just a conflict recheck, not a build; the verify command still runs once via verify_merges after the last merge).
Every branch in this run forked from the same base, so conflicts between them are the NORMAL case, not a failure. Resolve them and keep going; do not abandon the run because a merge conflicted.
@@ -523,6 +543,10 @@ public static class PromptFiles
- Prefer the MCP tools whenever they apply. Only if the MCP tools cannot reach an in-progress merge may you finish it by hand: resolve the markers, then `git add -- <the resolved paths>` and `git commit` — NEVER `git add -A` or a bare commit, because the checkout is shared with other sessions.
- Never use raw `git merge`, `git reset`, or `git checkout` to force a merge. Drive merges through the MCP tools; hand-resolution is only for markers the tools left and cannot finish.
## Company repos (Azure DevOps)
If the list's repo is a company repo (origin on the Azure DevOps Server), approve does not merge anything locally: review_task approve runs the verify command in the task's own worktree, pushes its branch and opens a draft PR (or reuses the one already open for that branch). mergeStatus is "pr_created" or "pr_updated" and mergeMessage is the PR URL; the task deliberately stays WaitingForReview until the user merges the PR on Azure DevOps and ticks it Done. There are no merge conflicts to resolve and main does not move, so skip preview_merge_set's merge-order reasoning, do NOT call verify_merges (the verify already ran inside approve, skipVerify is ignored there), and the merge-before-rerun rule above does not apply. A blocked result names what failed (no Azure DevOps PAT, push rejected, or "gepusht, PR fehlgeschlagen: …" when the push landed but the PR call did not) — report it, do not retry blindly. In the Phase 5 summary, list every PR link.
## Verify once — after the last merge
When every merge above is done (including conflict resolutions), call verify_merges with the ids of ALL tasks that reported merged_verify_pending. It runs the list's verify command a single time and promotes them all to Done on success — that is the per-merge gate you skipped, paid once.
@@ -539,6 +563,30 @@ public static class PromptFiles
Allow at most ONE rerun per task in this session.
## Keep watching the queue
Do not assume the tasks you were handed are the only ones that matter. A prior Wait session
can already have finished before every one of its tasks actually reached a terminal status,
or new tasks can still be running when you start — merging only what happened to be ready the
moment you looked in and then stopping is how work silently falls through the cracks.
Before you go to Handoff / end, call get_queue_state and check whether any task id from your
brief still shows up in activeSlots or waitingTaskIds. If none do, your list is clear — go to
Handoff / end now.
If some do, call wait_for_task_change with exactly those still-busy task ids,
timeoutSeconds=900, treatWaitingForChildrenAsBusy=true. When it reports a change, go back to
Phase 4 and merge/review whatever it now says is WaitingForReview, then repeat this check.
This is NOT a `sleep`-and-poll loop — never write your own retry/sleep loop around get_task or
get_queue_state. wait_for_task_change already blocks server-side and reports progress so it
survives your own client's idle-silence abort; that is the only waiting mechanism allowed here.
Hard cap: stop after at most THREE consecutive wait_for_task_change calls made this way (three
900s timeouts ≈ 45 minutes total), even if tasks are still busy. When you stop — because the
queue cleared or because you hit the cap — report whatever is still Queued/Running in the
summary below and proceed to Handoff / end exactly as if this section didn't exist.
## Handoff / end
- If you started any reruns: call handoff_list_handler(taskId, <the rerun task ids>, nextPhase: "wait_final"), then stop your turn immediately — do not merge anything yet that you just restarted. Do NOT submit this run for review yet — the chain isn't finished.
@@ -547,7 +595,7 @@ public static class PromptFiles
## Phase 5 — Summary
Print one line per task from the brief:
title — final status — merge commit (if any) — conflicts resolved (if any) — verify-failed (yes/no).
title — final status — merge commit or PR link (if any) — conflicts resolved (if any) — verify-failed (yes/no).
Then list anything you skipped or left for the user and why, and any follow-ups worth turning into new tasks.
""";
@@ -584,24 +632,99 @@ public static class PromptFiles
Start with phase 3 (run), continuing through review/merge and the summary as your
instructions describe.
{finalNote}
{roundInfo}
""";
// Appended to the triage/wait/merge system prompt when a run is in PowerUse (global switch AND
// the list's own opt-in). Deliberately an addendum rather than a second set of prompts: the
// shared 95% stays in one place, and a normal run's prompt is byte-identical to what it was.
// It therefore has to read as an OVERRIDE of the rules above it, not as standalone instructions.
private const string PowerUseAddendumDefault = """
# PowerUse mode — these rules supersede the ones above
PowerUse is ON for this run. Everything above was written for a session with a user watching it. That assumption no longer holds, and where the two conflict, this section wins.
## No user exists
Nobody is watching and nobody can answer a question. Every instruction above that says to ask the user, confirm with the user, or stop and let the user decide is replaced by: decide it yourself, act, and record the decision in your summary. A question asked here is a hung session and a wasted run.
This does not lower the bar for what you may do — it removes escalation as an option, so the decisions you make must be ones you can defend in writing afterwards. Where a rule above offered "ask the user" as the safe path, the safe path is now the conservative action plus a written record, never the bold one.
Specifically:
- **Duplicate pairs (Triage Phase 1):** decide them. Clear duplicate — same goal, or one fully contained in the other — fold the loser's unique content into the survivor and cancel the loser. Genuinely different intent that merely touches the same area — keep both and chain them with dependsOnTaskId so they cannot run against the same files at once.
- **A task too vague to sharpen (Triage Phase 2):** cancel it with a recorded reason rather than guessing a specification for someone else's half-sentence. Guessing burns a whole slot on work nobody asked for.
- **Wave plan vs. handoff cap (Triage Phase 1.5):** never trim the wave plan to fit the cap or defer waves to "manual approval in the UI" — nobody will be there. Plan the waves the work actually needs and hand off the first one.
- **A task that bundles several independent features (Triage Phase 2):** split it yourself — create the pieces, chain them with dependsOnTaskId where one builds on another, cancel or narrow the original, and record what you did.
- **Verify command not set (Triage Phase 2):** determine it, run it once via Bash to prove it works, then set it. Never write a command you have not actually run.
- **A risky or wrong-looking diff (Merge Phase 4):** if it contradicts the task's own acceptance criteria, call review_task(taskId, decision="reject_rerun", feedback=...) with concrete feedback naming what was missing. If it merely looks unusual but meets the criteria, merge it and say why. If it looks actively destructive — deletes code no task asked to delete, drops a migration, rewrites unrelated files — leave it unmerged and record it as HELD FOR REVIEW. Never merge something you would describe as destructive.
- **A conflict you cannot resolve with confidence (Merge Phase 4):** call abort_merge(taskId) and record it as HELD FOR REVIEW. Aborting is always correct over guessing: the task keeps its status, and a rerun against the new main usually removes the conflict entirely.
## Conflicts are the normal case
Never end your turn with a conflicted merge left in the working tree. A half-merged tree makes the repo unbuildable for every session and every task after you, and any verify that runs against it reports a false red.
Prefer get_merge_conflicts + resolve_conflict_hunk over reading marker text by hand — for a large conflict that is the only tractable route; a 200k-character marker dump is not something to read linearly.
The common collision shape in a parallel run is one task MOVING code out of a file while another task ADDS to it. The resolution is almost always to keep the move and re-apply the addition in the file the code moved to — not to pick a side. Picking "ours" silently deletes the other task's work.
When ordering merges that overlap on a file, merge the task that moves or deletes code BEFORE the task that only adds to it. A move landing second turns every addition into a conflict.
## Rerun budget
At most TWO reruns per task across the whole run, replacing the single-rerun limit above. A task that has already been rerun twice is recorded as HELD FOR REVIEW instead of getting a third attempt — three failures on the same task is a specification problem, not bad luck, and further attempts starve the rest of the run.
## Harvest — so the run does not starve
This applies to the Merge phase only, after its merges and reruns are done.
If you have fewer than five tasks to hand to the next round, harvest new work for this list's repo before handing off. Only for the list named in the brief — never another list or repo.
Sources, in this order; prefer one you have not exhausted yet:
1. **Verify/build output** — real compiler warnings. One task per warning group.
2. **`ponytail:` comments** — deliberate shortcuts with a named ceiling and upgrade path. Take the ones whose upgrade is small.
3. **TODO / HACK / FIXME** — only the concretely formulated ones.
4. **Unfinished plan slices** — `docs/superpowers/plans/*.md` whose last slices never landed. Check against `git log` before believing a slice is open. One task per slice, naming the plan path and slice number.
5. **Test gaps at bug hotspots** — `git log --grep="^fix"` over recent commits; files with repeated fixes and no test.
6. **Over-engineering** — the ponytail-audit skill against individual subfolders, never the whole repo at once. Deletion candidates.
7. **Feature work** — improvements that follow clearly from what the code already does and from the plans and specs under `docs/`. Ground each one in something you actually read; a feature you cannot point at a file for is a guess, not a proposal.
Rules for every harvested task, without exception:
- Write it the way the Triage phase would: concrete acceptance criteria, real file paths found with Read/Grep/Glob, and what is out of scope. A vague harvested task wastes a whole slot.
- Keep it narrow — one concern, ideally under five changed files. Narrow tasks merge in parallel; broad ones collide with everything.
- Do not hand two new tasks to the same round if they touch the same file. Stagger them across rounds, or chain them with dependsOnTaskId.
- Tag every harvested task `createdBy: "power-use"` so a later review can tell your proposals from the ones a human asked for.
- A task that only verifies, audits or researches and is not meant to change code must be created with `expectsNoDiff: true`, or the next Merge session will read its empty diff as a failure.
## Verify red is the one thing you must not paper over
If verify_merges returns verify_failed, the target branch is now RED. Do not try to fix it inside this session. Create ONE task with add_task(queueImmediately: true, model: "sonnet") titled `fix: verify red after this round`, put the failing output tail in its description, and hand that task to the next round as its ONLY entry. Merge nothing further until it has landed.
## The summary is the only record
Nobody saw you work, so anything not written down did not happen. In addition to the per-task lines the Merge phase already asks for, your summary must carry these sections. An empty section is a valid answer; an omitted one is not:
- **HELD FOR REVIEW** — every task you did not merge, with the reason.
- **Conflicts resolved** — per conflict: the file, which two tasks collided, and which way you resolved it. Someone must be able to find your resolution without re-deriving it.
- **Harvested** — every task you created, grouped by source.
- **Round state** — which round this was and what you handed to the next one.
""";
private const string WeeklyReportDefault = """
You are generating a concise weekly standup report for a software developer,
covering {start} to {end}.
You are writing memory-jogger notes for a developer's weekly standup,
covering {start} to {end}. They read them aloud in their own words, so write
keywords, not sentences.
Rules:
- Write the ENTIRE report in German.
- Group by day. One "## <Wochentag>, <dd.MM.yyyy>" section per day that has
- Write in German.
- Group by day. One "## <Wochentag>, <dd.MM.>" heading per day that has
activity (German weekday names). Omit days with no activity.
- Within each day: 3–5 first-person, past-tense bullets ("- Habe X umgesetzt",
"- Y behoben"). Merge related small work into one bullet.
- Drop trivia: typo fixes, pure exploration, false starts, tooling/log noise.
- Blend the developer's own notes and the derived activity into ONE deduplicated
bullet list per day. The notes are authoritative — never omit or contradict them.
- Name the project/repo when it adds clarity.
- Output ONLY the dated sections. No preamble, no intro, no closing remarks.
- Per day: 1–3 bullets, each at most ~6 words, noun style
("- LagerApp: Etikettendruck gefixt", "- ClaudeDo Listen-Ordner"). No "Habe …".
- One bullet per topic — merge everything about the same feature/bug.
- Drop trivia: typo fixes, exploration, false starts, tooling/log noise.
- Merge the developer's notes with the derived activity, deduplicated.
The notes are authoritative — never omit or contradict them.
- Output ONLY the day sections. No preamble, no summary, no closing remarks.
Two sections follow below: an activity log derived from Claude session history,
and the developer's own notes. Base the report on both; the notes are
@@ -93,6 +93,16 @@ public sealed class AppSettingsRepository
row.UsageThrottleSevenDaySoftPct = Math.Clamp(updated.UsageThrottleSevenDaySoftPct, 0, 100);
row.UsageThrottleSevenDayHardPct = Math.Clamp(updated.UsageThrottleSevenDayHardPct, 0, 100);
row.AutoContinueOnUsageLimit = updated.AutoContinueOnUsageLimit;
// ModelPresets / PowerUse* / ListHandlerMaxHandoffs were missing from this copy block, so
// every write of them was silently dropped (the row keeps whatever it already had). This
// block is the single write path for the singleton row — a field absent here does not
// persist, no matter how correctly the hub fills the entity.
row.ModelPresets = string.IsNullOrWhiteSpace(updated.ModelPresets) ? row.ModelPresets : updated.ModelPresets;
row.PowerUseEnabled = updated.PowerUseEnabled;
row.PowerUseWindowStart = PowerUseWindow.Normalize(updated.PowerUseWindowStart);
row.PowerUseWindowEnd = PowerUseWindow.Normalize(updated.PowerUseWindowEnd);
row.PowerUseHandoffPct = Math.Clamp(updated.PowerUseHandoffPct, 50, 99);
row.ListHandlerMaxHandoffs = Math.Max(1, updated.ListHandlerMaxHandoffs);
await _context.SaveChangesAsync(ct);
}
@@ -117,4 +127,34 @@ public sealed class AppSettingsRepository
row.RepoImportFolders = list.Count == 0 ? null : JsonSerializer.Serialize(list);
await _context.SaveChangesAsync(ct);
}
/// <summary>Reads the configured SSH workspaces; missing or corrupt JSON yields an empty list.</summary>
public async Task<List<SshWorkspace>> GetSshWorkspacesAsync(CancellationToken ct = default)
{
var json = await _context.AppSettings.AsNoTracking()
.Where(s => s.Id == AppSettingsEntity.SingletonId)
.Select(s => s.SshWorkspaces)
.FirstOrDefaultAsync(ct);
if (string.IsNullOrWhiteSpace(json)) return new List<SshWorkspace>();
try { return JsonSerializer.Deserialize<List<SshWorkspace>>(json) ?? new List<SshWorkspace>(); }
catch (JsonException) { return new List<SshWorkspace>(); }
}
/// <summary>Writes only the SSH workspaces column; an empty list clears it.</summary>
public async Task SetSshWorkspacesAsync(IEnumerable<SshWorkspace> workspaces, CancellationToken ct = default)
{
var list = workspaces.ToList();
var row = await GetOrCreateTrackedRowAsync(ct);
row.SshWorkspaces = list.Count == 0 ? null : JsonSerializer.Serialize(list);
await _context.SaveChangesAsync(ct);
}
/// <summary>Writes only the ticket fallback list; blank clears it.</summary>
public async Task SetTicketFallbackListIdAsync(string? listId, CancellationToken ct = default)
{
var row = await GetOrCreateTrackedRowAsync(ct);
row.TicketFallbackListId = string.IsNullOrWhiteSpace(listId) ? null : listId.Trim();
await _context.SaveChangesAsync(ct);
}
}
@@ -22,7 +22,13 @@ public sealed class DailyNoteRepository
.OrderBy(n => n.Date).ThenBy(n => n.SortOrder)
.ToListAsync(ct);
public async Task<DailyNoteEntity> AddAsync(DateOnly day, string text, CancellationToken ct = default)
/// <summary>Notes an agent wrote on <paramref name="day"/> — the number the MCP cap counts against.</summary>
public async Task<int> CountAgentNotesAsync(DateOnly day, CancellationToken ct = default) =>
await _context.DailyNotes.AsNoTracking()
.CountAsync(n => n.Date == day && n.FromAgent, ct);
public async Task<DailyNoteEntity> AddAsync(
DateOnly day, string text, bool fromAgent = false, CancellationToken ct = default)
{
var nextOrder = await _context.DailyNotes
.Where(n => n.Date == day)
@@ -34,6 +40,7 @@ public sealed class DailyNoteRepository
Date = day,
Text = text,
SortOrder = nextOrder + 1,
FromAgent = fromAgent,
CreatedAt = DateTime.UtcNow,
};
_context.DailyNotes.Add(note);
@@ -17,10 +17,20 @@ public sealed class ListRepository
public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
{
NormalizeWorkingDir(entity);
if (entity.SortOrder == 0) entity.SortOrder = await NextSortOrderAsync(ct);
_context.Lists.Add(entity);
await _context.SaveChangesAsync(ct);
}
/// <summary>One past the highest <c>sort_order</c> in use — across <b>both</b> tables, because
/// the sidebar interleaves folders and top-level lists in a single order sequence.</summary>
private async Task<int> NextSortOrderAsync(CancellationToken ct = default)
{
var maxList = await _context.Lists.MaxAsync(l => (int?)l.SortOrder, ct) ?? -1;
var maxFolder = await _context.ListFolders.MaxAsync(f => (int?)f.SortOrder, ct) ?? -1;
return Math.Max(maxList, maxFolder) + 1;
}
public async Task UpdateAsync(ListEntity entity, CancellationToken ct = default)
{
NormalizeWorkingDir(entity);
@@ -56,18 +66,92 @@ public sealed class ListRepository
return await _context.Lists.OrderBy(l => l.SortOrder).ThenBy(l => l.CreatedAt).ToListAsync(ct);
}
public async Task ReorderAsync(IReadOnlyList<string> orderedListIds, CancellationToken ct = default)
/// <summary>
/// Writes the sidebar's visual order as one sequence spanning both tables: folders and lists
/// share the <c>sort_order</c> number space, which is what lets a top-level list sit between
/// two folders. A folder's own children follow it in the sequence, so ordering folders by
/// <c>sort_order</c> and lists by <c>sort_order</c> independently still reproduces the sidebar.
/// </summary>
public async Task ReorderSidebarAsync(IReadOnlyList<(string Id, bool IsFolder)> ordered, CancellationToken ct = default)
{
var idSet = orderedListIds.ToHashSet();
var entities = await _context.Lists.Where(l => idSet.Contains(l.Id)).ToListAsync(ct);
for (int i = 0; i < orderedListIds.Count; i++)
var listIds = ordered.Where(o => !o.IsFolder).Select(o => o.Id).ToHashSet();
var folderIds = ordered.Where(o => o.IsFolder).Select(o => o.Id).ToHashSet();
var lists = await _context.Lists.Where(l => listIds.Contains(l.Id)).ToListAsync(ct);
var folders = await _context.ListFolders.Where(f => folderIds.Contains(f.Id)).ToListAsync(ct);
for (int i = 0; i < ordered.Count; i++)
{
var e = entities.FirstOrDefault(x => x.Id == orderedListIds[i]);
if (e is not null) e.SortOrder = i;
var (id, isFolder) = ordered[i];
if (isFolder)
{
if (folders.FirstOrDefault(f => f.Id == id) is { } folder) folder.SortOrder = i;
}
else if (lists.FirstOrDefault(l => l.Id == id) is { } list) list.SortOrder = i;
}
await _context.SaveChangesAsync(ct);
}
// ---- Sidebar folders (list_folders) ----
// Folders live here rather than in their own repository because they exist only as a grouping
// of these lists — every caller that touches one already holds a ListRepository.
public async Task<List<ListFolderEntity>> GetFoldersAsync(CancellationToken ct = default)
{
return await _context.ListFolders
.OrderBy(f => f.SortOrder).ThenBy(f => f.CreatedAt)
.ToListAsync(ct);
}
public async Task AddFolderAsync(ListFolderEntity folder, CancellationToken ct = default)
{
folder.SortOrder = await NextSortOrderAsync(ct);
_context.ListFolders.Add(folder);
await _context.SaveChangesAsync(ct);
}
/// <summary>Patches just the editable fields — a whole-entity Update would need
/// <c>CreatedAt</c>/<c>SortOrder</c>/<c>IsCollapsed</c> carried along and silently reset
/// whichever the caller forgot (see the SetConfigAsync note in CLAUDE.md).</summary>
public async Task RenameFolderAsync(string folderId, string name, string? icon, string? color, CancellationToken ct = default)
{
await _context.ListFolders
.Where(f => f.Id == folderId)
.ExecuteUpdateAsync(s => s
.SetProperty(f => f.Name, name)
.SetProperty(f => f.Icon, icon)
.SetProperty(f => f.Color, color), ct);
}
/// <summary>Deletes the folder only — its lists survive and fall back to the top level via
/// the ON DELETE SET NULL on <c>lists.folder_id</c>.</summary>
public async Task DeleteFolderAsync(string folderId, CancellationToken ct = default)
{
await _context.ListFolders.Where(f => f.Id == folderId).ExecuteDeleteAsync(ct);
}
public async Task SetFolderCollapsedAsync(string folderId, bool collapsed, CancellationToken ct = default)
{
await _context.ListFolders
.Where(f => f.Id == folderId)
.ExecuteUpdateAsync(s => s.SetProperty(f => f.IsCollapsed, collapsed), ct);
}
/// <summary>Moves a folder under <paramref name="parentFolderId"/>, or to the top level when null.</summary>
public async Task SetFolderParentAsync(string folderId, string? parentFolderId, CancellationToken ct = default)
{
await _context.ListFolders
.Where(f => f.Id == folderId)
.ExecuteUpdateAsync(s => s.SetProperty(f => f.ParentFolderId, parentFolderId), ct);
}
/// <summary>Moves a list into <paramref name="folderId"/>, or back to the top level when null.</summary>
public async Task SetListFolderAsync(string listId, string? folderId, CancellationToken ct = default)
{
await _context.Lists
.Where(l => l.Id == listId)
.ExecuteUpdateAsync(s => s.SetProperty(l => l.FolderId, folderId), ct);
}
public async Task<ListConfigEntity?> GetConfigAsync(string listId, CancellationToken ct = default)
{
return await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == listId, ct);
@@ -90,6 +174,8 @@ public sealed class ListRepository
existing.VerifyCommand = config.VerifyCommand;
existing.PermissionMode = config.PermissionMode;
existing.TicketProjectId = config.TicketProjectId;
existing.PowerUseAllowed = config.PowerUseAllowed;
existing.BaseBranch = config.BaseBranch;
}
await _context.SaveChangesAsync(ct);
}
@@ -0,0 +1,39 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Repositories;
/// <summary>Reads and writes <c>mission_control_sessions</c>.</summary>
public sealed class MissionControlSessionRepository
{
private readonly ClaudeDoDbContext _context;
public MissionControlSessionRepository(ClaudeDoDbContext context) => _context = context;
/// <summary>Inserts the session unless it is already recorded — a resumed task session keeps
/// its original row and start time.</summary>
public async Task RecordAsync(MissionControlSessionEntity session, CancellationToken ct = default)
{
if (await _context.MissionControlSessions.AnyAsync(s => s.SessionId == session.SessionId, ct)) return;
_context.MissionControlSessions.Add(session);
await _context.SaveChangesAsync(ct);
}
/// <summary>Sets the session's title; a no-op for an unknown id. <see cref="RecordAsync"/> never overwrites it.</summary>
public async Task RenameAsync(string sessionId, string title, CancellationToken ct = default) =>
await _context.MissionControlSessions.Where(s => s.SessionId == sessionId)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.Title, title), ct);
/// <summary>The recorded session, or null.</summary>
public async Task<MissionControlSessionEntity?> GetAsync(string sessionId, CancellationToken ct = default) =>
await _context.MissionControlSessions.AsNoTracking().FirstOrDefaultAsync(s => s.SessionId == sessionId, ct);
/// <summary>Sessions started since <paramref name="sinceUtc"/>, newest first, at most <paramref name="max"/>.</summary>
public async Task<IReadOnlyList<MissionControlSessionEntity>> ListRecentAsync(
DateTime sinceUtc, int max, CancellationToken ct = default) =>
await _context.MissionControlSessions.AsNoTracking()
.Where(s => s.StartedAt >= sinceUtc)
.OrderByDescending(s => s.StartedAt)
.Take(max)
.ToListAsync(ct);
}
@@ -34,7 +34,10 @@ public sealed class TaskRepository
var tracked = _context.Tasks.Local.FirstOrDefault(t => t.Id == entity.Id);
if (tracked is not null && !ReferenceEquals(tracked, entity))
_context.Entry(tracked).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
_context.Tasks.Update(entity);
// Entry().State instead of Update(): Update marks the whole loaded graph Modified, which
// for the all-key task_dependencies rows means an UPDATE with nothing to set. Dependency
// edges are written only by TaskStateService.SetDependsOnAsync.
_context.Entry(entity).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
await _context.SaveChangesAsync(ct);
}
@@ -46,17 +49,20 @@ public sealed class TaskRepository
public async Task<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)
{
return await _context.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId, ct);
return await _context.Tasks.AsNoTracking().Include(t => t.Dependencies)
.FirstOrDefaultAsync(t => t.Id == taskId, ct);
}
public async Task<TaskEntity?> GetByNumberAsync(int number, CancellationToken ct = default)
{
return await _context.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Number == number, ct);
return await _context.Tasks.AsNoTracking().Include(t => t.Dependencies)
.FirstOrDefaultAsync(t => t.Number == number, ct);
}
public async Task<List<TaskEntity>> GetByListIdAsync(string listId, CancellationToken ct = default)
{
return await _context.Tasks
.Include(t => t.Dependencies)
.Where(t => t.ListId == listId)
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
.ToListAsync(ct);
@@ -36,7 +36,7 @@ public sealed class WorktreeRepository
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
}
public async Task SetMergedAsync(string taskId, string mergeCommit, CancellationToken ct = default)
public async Task SetMergedAsync(string taskId, string? mergeCommit, CancellationToken ct = default)
{
await _context.Worktrees
.Where(w => w.TaskId == taskId)
@@ -45,6 +45,14 @@ public sealed class WorktreeRepository
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
}
/// <summary>Stores the draft PR an approve in a company repo created or reused.</summary>
public async Task SetPullRequestUrlAsync(string taskId, string url, CancellationToken ct = default)
{
await _context.Worktrees
.Where(w => w.TaskId == taskId)
.ExecuteUpdateAsync(s => s.SetProperty(w => w.PullRequestUrl, url), ct);
}
/// <summary>Records a successful auto-rebase: the branch's fork point moves to the rebased-onto
/// commit and its head moves to the new tip <c>git rebase</c> produced.</summary>
public async Task SetRebasedAsync(string taskId, string baseCommit, string headCommit, CancellationToken ct = default)
+44 -9
View File
@@ -13,7 +13,20 @@ namespace ClaudeDo.Data.Wire;
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
public record WorkerBuildInfoDto(string? BuildSha);
/// <summary>A Worker operation running right now (verify, merge, rebase, worktree creation).
/// <paramref name="Title"/> is the task title, or the list name for a batch verify.</summary>
public record RunningOperationDto(
string Id, string Kind, string? TaskId, int? TaskNumber, string? ListId, string Title, DateTime StartedAt);
/// <summary>Stable <see cref="RunningOperationDto.Kind"/> tokens, localized by the UI.</summary>
public static class OperationKinds
{
public const string VerifyMerge = "verify-merge";
public const string VerifyBatch = "verify-batch";
public const string Merging = "merging";
public const string Rebasing = "rebasing";
public const string CreatingWorktree = "creating-worktree";
}
public record AppSettingsDto(
string DefaultClaudeInstructions,
@@ -38,7 +51,16 @@ public record AppSettingsDto(
int UsageThrottleFiveHourHardPct = 65,
int UsageThrottleSevenDaySoftPct = 50,
int UsageThrottleSevenDayHardPct = 65,
bool AutoContinueOnUsageLimit = false);
bool AutoContinueOnUsageLimit = false,
// PowerUse master switch — Settings → Execution. Only takes effect for a list that also set
// PowerUseAllowed.
bool PowerUseEnabled = false,
int ListHandlerMaxHandoffs = 4,
// PowerUse time window (`HH:mm`, local, null = round the clock) + the 5h-usage percentage at
// which a PowerUse handler session should hand off. Same Settings tab.
string? PowerUseWindowStart = null,
string? PowerUseWindowEnd = null,
int PowerUseHandoffPct = 95);
public record ModelPresetDto(string Model, string Effort, int MaxTurns);
@@ -65,7 +87,9 @@ public record WorktreeOverviewDto(
WorktreeState State,
string? DiffStat,
DateTime CreatedAt,
bool PathExistsOnDisk);
bool PathExistsOnDisk,
int TaskNumber = 0,
bool ContentInMain = false);
public record ForceRemoveResultDto(bool Removed, string? Reason);
@@ -73,7 +97,7 @@ public record PlanningMergeConflictStateDto(string PlanningTaskId, string Subtas
public record PendingQuestionDto(string TaskId, string QuestionId, string Question);
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage, string? PullRequestUrl = null);
public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount);
@@ -84,7 +108,8 @@ public record MergePreviewDto(
int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null);
public record MergeTargetsDto(
string DefaultBranch, IReadOnlyList<string> LocalBranches, string DefaultCommitMessage);
string DefaultBranch, IReadOnlyList<string> LocalBranches, string DefaultCommitMessage,
bool TargetDirty = false, bool IsCompanyRepo = false);
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
@@ -92,16 +117,19 @@ public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<Merg
public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs);
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false, string? Icon = null);
public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false, string? Icon = null, string? Color = null);
// TicketProjectId is tri-state on purpose: a caller that doesn't own the field (anything but the
// list-settings modal) must not be able to clear it by omission — SetConfigAsync copies the entity
// verbatim. Its scale: null = keep stored, <= 0 = clear the link, > 0 = set it.
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null);
// PowerUseAllowed is nullable for the same reason: null = keep stored, false = revoke the opt-in.
// A non-nullable bool here would let any caller that omits it silently revoke PowerUse for the list.
// BaseBranch: null = keep stored, "" = clear, value = set.
public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool? PowerUseAllowed = null, string? BaseBranch = null);
public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? PermissionMode = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null);
public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null, string? VerifyCommand = null, string? PermissionMode = null, int? TicketProjectId = null, bool PowerUseAllowed = false, string? BaseBranch = null);
public record SeedResultDto(int Copied, int Skipped);
@@ -207,7 +235,14 @@ public sealed record LaunchSpec(
IReadOnlyList<string> Args,
IReadOnlyDictionary<string, string> Env);
public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet);
// A handler round's still-open (Queued/Running) task, for the Mission Control "abandon run"
// action -- confirms with a count, then cancels each one before closing the pane.
public record HandlerOpenTaskDto(string Id, string Title);
public record TicketSettingsDto(string? ApiBaseUrl, bool TokenSet, string? FallbackListId = null);
public record TicketConnectionDto(bool Ok, string? UserName, IReadOnlyList<string> Scopes, string? Error);
public record AzureDevOpsSettingsDto(bool TokenSet);
// Repo = the company repo the test ran against (first list whose origin matches), DefaultBranch = its ADO default.
public record AzureDevOpsConnectionDto(bool Ok, string? Repo, string? DefaultBranch, string? Error);
public record TicketProjectDto(int Id, string Title, string DepartmentName);
public record TicketImportResultDto(int Examined, int Created);
@@ -1,5 +1,6 @@
using System.Text.Json;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -23,11 +24,12 @@ public sealed class ClaudeAuthCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var lookup = await _lookup.ResolveAsync(ctx, ct);
if (lookup.Resolved is null)
{
return CheckResult.Unknown(Id, Severity, "checks.claudeAuth.title",
"The claude CLI was not found — login could not be checked.");
loc?["checks.claudeAuth.notFound"] ?? "The claude CLI was not found — login could not be checked.");
}
var (fileName, arguments) = ProcessCommand.For(lookup.Resolved, "auth", "status", "--json");
@@ -36,19 +38,19 @@ public sealed class ClaudeAuthCheck : IEnvironmentCheck
if (exitCode != 0)
{
return CheckResult.Unknown(Id, Severity, "checks.claudeAuth.title",
"Login status could not be determined.", output);
loc?["checks.claudeAuth.undetermined"] ?? "Login status could not be determined.", output);
}
var loggedIn = TryParseLoggedIn(output);
if (loggedIn is null)
{
return CheckResult.Unknown(Id, Severity, "checks.claudeAuth.title",
"Login status could not be determined.", output);
loc?["checks.claudeAuth.undetermined"] ?? "Login status could not be determined.", output);
}
return loggedIn.Value
? CheckResult.Ok(Id, Severity, "checks.claudeAuth.title", "Logged in.")
: CheckResult.Fail(Id, Severity, "checks.claudeAuth.title", "Not logged in.", "checks.claudeAuth.hint");
? CheckResult.Ok(Id, Severity, "checks.claudeAuth.title", loc?["checks.claudeAuth.loggedIn"] ?? "Logged in.")
: CheckResult.Fail(Id, Severity, "checks.claudeAuth.title", loc?["checks.claudeAuth.notLoggedIn"] ?? "Not logged in.", "checks.claudeAuth.hint");
}
private static bool? TryParseLoggedIn(string output)
@@ -1,6 +1,7 @@
using System.IO;
using ClaudeDo.Data.Environment;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -21,20 +22,21 @@ public sealed class ClaudeCliCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var lookup = await _lookup.ResolveAsync(ctx, ct);
if (lookup.Resolved is null)
{
var searched = string.Join(", ", SearchedLocations());
return CheckResult.Fail(Id, Severity, "checks.claudeCli.title",
$"'{ctx.ClaudeBin}' was not found on PATH. Searched: {searched}.",
loc?.Get("checks.claudeCli.notFound", ctx.ClaudeBin, searched) ?? $"'{ctx.ClaudeBin}' was not found on PATH. Searched: {searched}.",
"checks.claudeCli.hint");
}
if (lookup.ExitCode != 0)
{
return CheckResult.Fail(Id, Severity, "checks.claudeCli.title",
$"'{lookup.Resolved.Path}' exited with code {lookup.ExitCode}.",
loc?.Get("checks.claudeCli.exitCode", lookup.Resolved.Path, lookup.ExitCode) ?? $"'{lookup.Resolved.Path}' exited with code {lookup.ExitCode}.",
"checks.claudeCli.hint", detail: lookup.Output);
}
@@ -42,7 +44,8 @@ public sealed class ClaudeCliCheck : IEnvironmentCheck
var detail = lookup.Resolved.IsShim
? "Resolved via a shim (.cmd/.bat/.ps1), not a native .exe."
: null;
return CheckResult.Ok(Id, Severity, "checks.claudeCli.title", $"{lookup.Resolved.Path} — {version}", detail);
return CheckResult.Ok(Id, Severity, "checks.claudeCli.title",
loc?.Get("checks.claudeCli.resolvedFormat", lookup.Resolved.Path, version) ?? $"{lookup.Resolved.Path} — {version}", detail);
}
private IEnumerable<string> SearchedLocations()
@@ -1,4 +1,5 @@
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -27,28 +28,29 @@ public sealed class ClaudeVersionCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var lookup = await _lookup.ResolveAsync(ctx, ct);
if (lookup.Resolved is null)
{
return CheckResult.Unknown(Id, Severity, "checks.claudeVersion.title",
"The claude CLI was not found — version could not be checked.");
loc?["checks.claudeVersion.notFound"] ?? "The claude CLI was not found — version could not be checked.");
}
if (lookup.ExitCode != 0 || lookup.ParsedVersion is null)
{
return CheckResult.Unknown(Id, Severity, "checks.claudeVersion.title",
$"Could not parse a version from '{lookup.Output.Trim()}'.");
loc?.Get("checks.claudeVersion.parseFailed", lookup.Output.Trim()) ?? $"Could not parse a version from '{lookup.Output.Trim()}'.");
}
if (lookup.ParsedVersion < MinimumVersion)
{
return CheckResult.Fail(Id, Severity, "checks.claudeVersion.title",
$"claude {lookup.ParsedVersion} is older than the required {MinimumVersion}.",
loc?.Get("checks.claudeVersion.tooOld", lookup.ParsedVersion, MinimumVersion) ?? $"claude {lookup.ParsedVersion} is older than the required {MinimumVersion}.",
"checks.claudeVersion.hint");
}
return CheckResult.Ok(Id, Severity, "checks.claudeVersion.title",
$"claude {lookup.ParsedVersion} meets the minimum ({MinimumVersion}).");
loc?.Get("checks.claudeVersion.ok", lookup.ParsedVersion, MinimumVersion) ?? $"claude {lookup.ParsedVersion} meets the minimum ({MinimumVersion}).");
}
}
+8 -3
View File
@@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
using ClaudeDo.Data.Environment;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Core.Interfaces;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -27,10 +28,12 @@ public sealed class GitCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var resolved = ExecutableResolver.Resolve("git", _pathOverride, _pathExtOverride);
if (resolved is null)
{
return CheckResult.Fail(Id, Severity, "checks.git.title", "git was not found on PATH.",
return CheckResult.Fail(Id, Severity, "checks.git.title",
loc?["checks.git.notFound"] ?? "git was not found on PATH.",
"checks.git.hint", "https://git-scm.com/downloads");
}
@@ -39,11 +42,13 @@ public sealed class GitCheck : IEnvironmentCheck
if (exitCode != 0)
{
return CheckResult.Fail(Id, Severity, "checks.git.title", $"'{resolved.Path}' exited with code {exitCode}.",
return CheckResult.Fail(Id, Severity, "checks.git.title",
loc?.Get("checks.git.exitCode", resolved.Path, exitCode) ?? $"'{resolved.Path}' exited with code {exitCode}.",
"checks.git.hint", "https://git-scm.com/downloads", output);
}
var version = VersionPattern.Match(output) is { Success: true } match ? match.Value : output.Trim();
return CheckResult.Ok(Id, Severity, "checks.git.title", $"{resolved.Path} — {version}");
return CheckResult.Ok(Id, Severity, "checks.git.title",
loc?.Get("checks.git.resolvedFormat", resolved.Path, version) ?? $"{resolved.Path} — {version}");
}
}
@@ -1,6 +1,7 @@
using ClaudeDo.Data.Environment;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Core.Interfaces;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -25,10 +26,12 @@ public sealed class GitIdentityCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var resolved = ExecutableResolver.Resolve("git", _pathOverride, _pathExtOverride);
if (resolved is null)
{
return CheckResult.Unknown(Id, Severity, "checks.gitIdentity.title", "git was not found — identity could not be checked.");
return CheckResult.Unknown(Id, Severity, "checks.gitIdentity.title",
loc?["checks.gitIdentity.notFound"] ?? "git was not found — identity could not be checked.");
}
var name = await ReadConfigAsync(resolved, "user.name", ct);
@@ -48,7 +51,8 @@ public sealed class GitIdentityCheck : IEnvironmentCheck
_ => "user.email",
};
return CheckResult.Fail(Id, Severity, "checks.gitIdentity.title", $"Missing git identity: {missing}.", "checks.gitIdentity.hint");
return CheckResult.Fail(Id, Severity, "checks.gitIdentity.title",
loc?.Get("checks.gitIdentity.missing", missing) ?? $"Missing git identity: {missing}.", "checks.gitIdentity.hint");
}
private async Task<string> ReadConfigAsync(ResolvedExecutable resolved, string key, CancellationToken ct)
@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -28,11 +29,12 @@ public sealed class PermissionModeAutoCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var lookup = await _lookup.ResolveAsync(ctx, ct);
if (lookup.Resolved is null)
{
return CheckResult.Unknown(Id, Severity, "checks.permissionModeAuto.title",
"The claude CLI was not found — permission-mode support could not be checked.");
loc?["checks.permissionModeAuto.notFound"] ?? "The claude CLI was not found — permission-mode support could not be checked.");
}
var (fileName, arguments) = ProcessCommand.For(lookup.Resolved, "--help");
@@ -41,17 +43,17 @@ public sealed class PermissionModeAutoCheck : IEnvironmentCheck
if (exitCode != 0 || string.IsNullOrWhiteSpace(output))
{
return CheckResult.Unknown(Id, Severity, "checks.permissionModeAuto.title",
"Could not determine whether --permission-mode auto is supported.");
loc?["checks.permissionModeAuto.undetermined"] ?? "Could not determine whether --permission-mode auto is supported.");
}
if (AutoIsListed(output))
{
return CheckResult.Ok(Id, Severity, "checks.permissionModeAuto.title",
"--permission-mode auto is recognized by this CLI.");
loc?["checks.permissionModeAuto.ok"] ?? "--permission-mode auto is recognized by this CLI.");
}
return CheckResult.Fail(Id, Severity, "checks.permissionModeAuto.title",
"This claude CLI version does not list 'auto' as a --permission-mode choice.",
loc?["checks.permissionModeAuto.notListed"] ?? "This claude CLI version does not list 'auto' as a --permission-mode choice.",
"checks.permissionModeAuto.hint");
}
+9 -3
View File
@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Sockets;
using ClaudeDo.Installer.Checks.Interfaces;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -19,6 +20,7 @@ public sealed class PortCheck : IEnvironmentCheck
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
var ports = new (string Label, int Port)[]
{
("SignalR", ctx.SignalRPort),
@@ -39,7 +41,9 @@ public sealed class PortCheck : IEnvironmentCheck
continue;
}
blocked.Add(owner is null ? $"{label} port {port} is in use." : $"{label} port {port} is in use by '{owner}'.");
blocked.Add(owner is null
? loc?.Get("checks.ports.inUse", label, port) ?? $"{label} port {port} is in use."
: loc?.Get("checks.ports.inUseBy", label, port, owner) ?? $"{label} port {port} is in use by '{owner}'.");
}
if (blocked.Count > 0)
@@ -49,10 +53,12 @@ public sealed class PortCheck : IEnvironmentCheck
if (ownWorker.Count > 0)
{
return CheckResult.Ok(Id, Severity, "checks.ports.title", $"In use by the running ClaudeDo Worker: {string.Join(", ", ownWorker)}.");
var ownWorkerList = string.Join(", ", ownWorker);
return CheckResult.Ok(Id, Severity, "checks.ports.title",
loc?.Get("checks.ports.inUseByOwnWorker", ownWorkerList) ?? $"In use by the running ClaudeDo Worker: {ownWorkerList}.");
}
return CheckResult.Ok(Id, Severity, "checks.ports.title", "Ports are available.");
return CheckResult.Ok(Id, Severity, "checks.ports.title", loc?["checks.ports.available"] ?? "Ports are available.");
}
private static bool IsFree(int port)
@@ -1,6 +1,7 @@
using System.IO;
using ClaudeDo.Data;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
namespace ClaudeDo.Installer.Checks;
@@ -14,17 +15,19 @@ public sealed class WriteAccessCheck : IEnvironmentCheck
public Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
{
var loc = TrExtension.Localizer;
foreach (var target in new[] { ctx.InstallDirectory, Paths.AppDataRoot() })
{
var error = TryWrite(target);
if (error is not null)
{
return Task.FromResult(CheckResult.Fail(Id, Severity, "checks.writeAccess.title",
$"Cannot write to '{target}': {error}", "checks.writeAccess.hint"));
loc?.Get("checks.writeAccess.cannotWrite", target, error) ?? $"Cannot write to '{target}': {error}", "checks.writeAccess.hint"));
}
}
return Task.FromResult(CheckResult.Ok(Id, Severity, "checks.writeAccess.title", "Install directory and data directory are writable."));
return Task.FromResult(CheckResult.Ok(Id, Severity, "checks.writeAccess.title",
loc?["checks.writeAccess.writable"] ?? "Install directory and data directory are writable."));
}
private static string? TryWrite(string path)
+226 -79
View File
@@ -11,11 +11,13 @@
"categoryGeneral": "Allgemein",
"categoryExecution": "Ausführung",
"categoryPrime": "Prime Claude",
"categoryPrimeHint": "Geplante Claude-Läufe: Ping, My Day füllen oder ein eigener Prompt, zu festen Zeiten.",
"categoryWorktrees": "Worktrees",
"categoryFiles": "Dateien & Prompts",
"categorySkills": "Session Skills",
"categoryReports": "Berichte",
"categoryTickets": "Ticketsystem"
"categoryTickets": "Externe Dienste",
"categorySsh": "SSH-Workspaces"
},
"repoHint": {
"text": "Wirkt erst mit verknüpftem Repo.",
@@ -27,6 +29,7 @@
"model": "Modell",
"maxTurns": "Max. Durchläufe",
"effort": "Effort",
"effortHint": "Denkaufwand, den das Modell anwendet. Höher kostet mehr und denkt tiefer.",
"modelPresets": "Vorgaben pro Modell",
"modelPresetsHint": "Effort und Durchlauf-Budget, mit denen eine Sitzung unter diesem Modell läuft. Overrides auf Listen- und Aufgabenebene gewinnen weiterhin.",
"maxTurnsCeiling": "Max. Durchläufe – Obergrenze",
@@ -36,13 +39,23 @@
"maxParallelExecutions": "Max. parallele Ausführungen",
"maxParallelExecutionsHint": "Wie viele Aufgaben aus der Warteschlange der Worker gleichzeitig ausführt.",
"usageGateHeading": "Usage-Limit-Stopp",
"usageGateHint": "Ab dieser Auslastung holt die Queue keine neuen Tasks mehr; laufende Tasks laufen zu Ende. 0 schaltet den Stopp ab.",
"usageGateHint": "Ab dieser Auslastung holt die Warteschlange keine neuen Aufgaben mehr; laufende Aufgaben laufen zu Ende. 0 schaltet den Stopp ab.",
"usageGateFiveHourPct": "5-Stunden-Fenster (%)",
"usageGateSevenDayPct": "7-Tage-Fenster (%)",
"autoContinueOnUsageLimit": "Nach Limit-Reset fortsetzen",
"autoContinueOnUsageLimitHint": "Ist ein Run am Nutzungslimit abgebrochen, wird er nach Ablauf des 5-Stunden-Fensters automatisch fortgesetzt. Weckt außerdem die pausierte Queue genau zum Reset-Zeitpunkt statt erst beim nächsten Backstop-Check.",
"autoContinueOnUsageLimitHint": "Ist ein Run am Nutzungslimit abgebrochen, wird er nach Ablauf des 5-Stunden-Fensters automatisch fortgesetzt. Weckt außerdem die pausierte Warteschlange genau zum Reset-Zeitpunkt statt erst beim nächsten Backstop-Check.",
"listHandlerMaxHandoffs": "Handoff-Budget des List-Handlers",
"listHandlerMaxHandoffsHint": "Wie viele Wait/Merge-Handoffs ein „Let Claude handle it\"-Run verketten darf. 4 entspricht zwei Merge-Runden. Für Läufe, die stundenlang durchlaufen sollen, höher setzen; der Wert begrenzt eine entlaufene Kette und darf daher nicht 0 sein.",
"powerUseHeading": "PowerUse",
"powerUse": "PowerUse aktivieren",
"powerUseHint": "Das restliche Fenster ausnutzen: ein List-Handler-Run fragt nichts, löst Konflikte selbst, sammelt neue Arbeit und handoffet weiter, solange sein Budget reicht. Wirkt nur auf Listen, die PowerUse in ihren Listen-Einstellungen erlaubt haben.",
"powerUseWindow": "Nur zwischen",
"powerUseWindowPlaceholder": "HH:mm",
"powerUseHandoffPct": "Übergabe bei (% von 5h)",
"powerUseWindowHint": "Beide Zeiten leer = rund um die Uhr. Das Fenster wird bei jedem Start einer Handler-Phase geprüft — eine Kette, die über das Ende hinausläuft, macht ohne PowerUse weiter.",
"powerUseWindowInvalid": "PowerUse-Zeiten müssen im Format HH:mm sein oder leer bleiben.",
"throttleStagesHeading": "Throttle-Stufen",
"throttleStagesHint": "Die Parallelität wird vor dem harten Gate begrenzt: soft auf 2 Slots, hard auf 1. Eingestellt wird das durch Ziehen der Marker an den Gauges im Usage-Monitor. Der Monitor schließt diese Einstellungen — vorher speichern, wenn du etwas geändert hast.",
"throttleStagesHint": "Die Parallelität wird vor dem harten Limit-Stopp begrenzt: soft auf 2 Slots, hard auf 1. Eingestellt wird das durch Ziehen der Marker an den Gauges im Usage-Monitor. Der Monitor schließt diese Einstellungen — vorher speichern, wenn du etwas geändert hast.",
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
"openUsageMonitor": "Usage-Monitor öffnen...",
"reportExcludedPaths": "Bericht: ausgeschlossene Pfade (einer pro Zeile)",
@@ -85,6 +98,7 @@
"retryPrompt": "Wiederholung",
"dailyPrepPrompt": "Tagesplanung",
"weeklyReportPrompt": "Wochenbericht",
"powerUsePrompt": "PowerUse-Addendum",
"viewPrompt": "Ansehen",
"customizedSection": "ANGEPASST",
"customizedHint": "Diese Prompts weichen vom mitgelieferten Standard ab und werden unverändert beibehalten. Zurücksetzen, um künftige Standard-Verbesserungen zu erhalten.",
@@ -99,10 +113,11 @@
"actionPing": "Ping",
"actionFillMyDay": "Mein Tag füllen",
"actionCustom": "Eigener Prompt",
"actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.",
"actionImportTickets": "Tickets + Tag planen",
"actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Tickets + Tag planen“ importiert erst deine zugewiesenen Tickets in die passenden Listen und startet dann die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.",
"customPromptLabel": "Prompt",
"customPromptPlaceholder": "Der vollständige Prompt für diesen Zeitplan (Pflichtfeld)",
"maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“.",
"maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“ oder „Tickets + Tag planen“.",
"promptOverrideLabel": "Prompt-Zusatz",
"promptOverridePlaceholder": "Zusätzliche Anweisungen, die für diesen Zeitplan an den Prime-Prompt angehängt werden (optional)",
"dayMo": "Mo",
@@ -134,12 +149,32 @@
"saveButton": "Konfiguration speichern"
},
"tickets": {
"ticketSection": "TICKETSYSTEM",
"azdoSection": "AZURE DEVOPS",
"azdoToken": "Personal Access Token (Scope: Code Read & Write)",
"azdoHint": "Gilt in Firmen-Repos (origin auf dem Azure DevOps Server): Fetch vor dem Task-Start, Push und Draft-PR beim Genehmigen.",
"baseUrl": "API-Base-URL",
"baseUrlHint": "Leer lassen, um die Ticket-Anbindung komplett zu deaktivieren.",
"token": "Personal Access Token",
"tokenSet": "gespeichert",
"testConnection": "Verbindung testen",
"clearToken": "Token entfernen"
"clearToken": "Token entfernen",
"fallbackList": "Fallback-Liste",
"fallbackListHint": "Ticket-Items, die Claude keiner Liste zuordnen kann, landen hier. Keine = sie werden übersprungen.",
"fallbackNone": "(keine)"
},
"ssh": {
"hint": "Server, auf denen Mission Control eine interaktive Claude-Session starten kann. Host ist ein Alias aus ~/.ssh/config (User, Key und Port kommen von dort); Claude muss auf dem Server installiert und eingeloggt sein.",
"name": "Name",
"host": "Host-Alias",
"remotePath": "Pfad auf dem Server (/… oder ~/…)",
"add": "Workspace hinzufügen",
"remove": "Entfernen",
"invalid": {
"name": "SSH-Workspace: Name fehlt.",
"host": "SSH-Workspace „{0}“: Host muss ein ssh-Alias sein (Buchstaben, Ziffern, . _ @ -, nicht mit - beginnend).",
"path": "SSH-Workspace „{0}“: Pfad muss mit / oder ~ beginnen und darf kein ' enthalten."
}
},
"inherit": {
"inheritedFromList": "geerbt · Liste",
@@ -177,7 +212,6 @@
"listSettingsTip": "Listeneinstellungen",
"addPlaceholder": "Aufgabe hinzufügen…",
"enterKey": "ENTER",
"notesPinnedRow": "Notizen (Tagesnotizen)",
"clearDayTip": "Tag leeren",
"planMyDayTip": "Meinen Tag planen",
"overdue": "ÜBERFÄLLIG",
@@ -189,6 +223,7 @@
"ctxMarkAs": "Markieren als",
"ctxMarkDone": "Erledigt",
"ctxMarkCancelled": "Abgebrochen",
"ctxMarkIdle": "Offen",
"ctxOpenConPtySession": "Interaktive Sitzung öffnen",
"ctxOpenWorktree": "Worktree öffnen",
"ctxOpenPlanningSession": "Planungssitzung öffnen",
@@ -209,6 +244,7 @@
"ctxRefineTask": "Aufgabe verfeinern",
"ctxPlanning": "Planung",
"ctxSchedule": "Zeitplan",
"ctxMoveToList": "In Liste verschieben",
"ctxDeleteTask": "Aufgabe löschen",
"reasonAlreadyRunning": "Aufgabe läuft bereits",
"reasonAlreadyQueued": "Bereits in der Warteschlange",
@@ -223,12 +259,13 @@
"reasonIsPlanningParent": "Teil eines Plans",
"reasonIsChildTask": "Teilaufgaben haben keine eigene Planungssitzung",
"reasonPlanAlreadyFinalized": "Plan ist bereits finalisiert",
"reasonNoWorktree": "Kein Worktree für diesen Task",
"reasonNoWorktree": "Kein Worktree für diese Aufgabe",
"cancel": "Abbrechen",
"removeFromQueueTip": "Aus Warteschlange entfernen",
"toggleSubtasksTip": "Unteraufgaben ein-/ausklappen",
"agentSuggestedTip": "Vom Agenten vorgeschlagen",
"interactiveChipTip": "Interaktive Sitzung offen — klicken, um im Mission Control dorthin zu springen",
"waitingOnTip": "Wartet auf {0}",
"scheduleTitle": "Aufgabe planen",
"scheduleWhen": "WANN",
"scheduleConfirm": "Planen",
@@ -250,24 +287,28 @@
"contextOpenFindings": "Findings-Ordner öffnen",
"contextImportTickets": "Aus Ticketsystem aktualisieren",
"contextSettings": "Einstellungen…",
"contextFolderSettings": "Ordner-Einstellungen…",
"contextRemoveFromFolder": "Aus Ordner herausnehmen",
"newList": "Neue Liste",
"addReposTip": "Repos als Listen hinzufügen",
"noRepoBannerText": "Noch kein Repo verknüpft. ClaudeDo führt Tasks in einem Git-Repo aus.",
"noRepoBannerText": "Noch kein Repo verknüpft. ClaudeDo führt Aufgaben in einem Git-Repo aus.",
"noRepoBannerButton": "Repo importieren",
"importTicketsResult": "{0} Tickets geprüft, {1} neue Tasks."
},
"details": {
"deleteTaskTip": "Aufgabe löschen",
"killSessionTip": "Laufende Sitzung beenden",
"killSessionTip": "Laufende Sitzung hart beenden",
"sendToQueue": "In Warteschlange einreihen",
"sendToQueueTip": "Diese Aufgabe in die Warteschlange einreihen, damit Claude sie beim nächsten Lauf übernimmt",
"copyTaskIdTip": "Aufgaben-ID kopieren",
"starTip": "Favorit",
"agentSettingsTip": "Agent-Einstellungen",
"agentSettingsHeading": "Agent-Einstellungen (Überschreibungen)",
"waitingOnPrefix": "Wartet auf",
"copyFormattedTip": "Titel, Beschreibung und offene Schritte kopieren",
"toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten",
"planDay": "Tag planen",
"importTicketsAndPlanDay": "Tickets + Tag planen",
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
"roadblockReply": {
"placeholder": "Auf den Roadblock antworten…",
@@ -290,7 +331,41 @@
"steps": "Schritte",
"files": "Dateien"
},
"emptyState": "Wähle eine Aufgabe aus, um Details zu sehen"
"emptyState": "Wähle eine Aufgabe aus, um Details zu sehen",
"roadblockLabel": "ROADBLOCK",
"stepsCard": {
"preview": "Vorschau",
"edit": "Bearbeiten",
"addStepPlaceholder": "Schritt hinzufügen…"
},
"workConsole": {
"continue": "Fortsetzen",
"resetAndRetry": "Zurücksetzen & erneut versuchen",
"tabOutput": "Ausgabe",
"tabGit": "Git",
"tabSession": "Sitzung",
"outcomeLabel": "ERGEBNIS",
"reviewLabel": "REVIEW",
"mergeLabel": "MERGE",
"targetBranch": "Zielbranch",
"openDiff": "Diff öffnen",
"worktree": "Worktree",
"reviewCombinedDiff": "Kombinierten Diff prüfen",
"openDiffHint": "Diff öffnen, um Mergen freizuschalten",
"targetDirty": "Ziel-Arbeitsverzeichnis hat uncommittete Änderungen — beim Mergen werden sie gestasht und danach wiederhergestellt.",
"approveMerge": "Genehmigen & Mergen",
"createDraftPr": "Draft-PR erstellen",
"updatePr": "PR aktualisieren",
"openPr": "Pull Request öffnen",
"companyTargetHint": "Firmen-Repo — Genehmigen pusht den Branch und erstellt einen Draft-PR auf Azure DevOps.",
"park": "Parken",
"parkTip": "Zurückstellen — zurück auf Leerlauf, Worktree bleibt erhalten",
"cancel": "Abbrechen",
"resetDiscardBranch": "Zurücksetzen (Branch verwerfen)…",
"submitForReviewHint": "Von Hand daran gearbeitet? Den Worktree zum Review einreichen, um ihn zu mergen.",
"submitForReview": "Zum Review einreichen",
"outcomesLabel": "ERGEBNISSE"
}
},
"agent": {
"openWorktreeTip": "Worktree im Datei-Explorer öffnen — benötigt einen aktiven Worktree",
@@ -299,8 +374,11 @@
},
"notes": {
"today": "Heute",
"add": "Hinzufügen",
"newNotePlaceholder": "Neue Notiz…"
"yesterday": "Gestern",
"newNotePlaceholder": "Neue Notiz…",
"emptyHint": "Noch nichts notiert. Tippen, Enter, fertig.",
"deleteTip": "Notiz löschen",
"fromAgentTip": "Von Claude notiert"
},
"session": {
"chipLive": "LIVE",
@@ -320,18 +398,43 @@
"empty": "Keine laufenden Aufgaben",
"settings": "Einstellungen",
"activity": "Aktivität",
"runningNow": "Läuft gerade",
"opVerifyMerge": "Prüfung nach Merge",
"opVerifyBatch": "Verify (Batch)",
"opMerging": "Merge",
"opRebasing": "Rebase",
"opCreatingWorktree": "Worktree wird erstellt",
"operationsRefreshFailed": "Laufende Operationen konnten nicht geladen werden: {0}",
"blocked": "Blockiert",
"running": "Läuft",
"review": "Im Review",
"verifying": "Wird geprüft",
"failed": "Fehlgeschlagen",
"roadblock": "Blockade gemeldet",
"openSession": "Sitzung öffnen",
"homeFolder": "Home (Schnellsitzung)",
"sshTarget": "{0} (SSH)",
"browseForFolder": "Ordner wählen…",
"recentSessions": "Letzte Sitzungen",
"recentSessionsEmpty": "Keine Sitzungen in den letzten 14 Tagen",
"resumeSession": "Fortsetzen",
"showBrief": "Brief",
"renameSession": "Doppelklick zum Umbenennen",
"renameFailed": "Sitzung konnte nicht umbenannt werden: {0}",
"handoffSession": "Übergabe",
"briefOpenFailed": "Brief konnte nicht geöffnet werden: {0}",
"focusMode": "Fokus",
"overviewMode": "Übersicht",
"closeSession": "Sitzung beenden",
"columns": "Spalten",
"columnsAuto": "Auto",
"closeSession": "Sitzung hart beenden",
"closeSessionConfirm": "Laufende Sitzung beenden? Der Claude-Prozess wird abgebrochen.",
"conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}",
"conptyStarting": "Sitzung wird gestartet…",
"conptyExited": "Sitzung beendet (Code {0}).",
"usageWarningNotDelivered": "Die Listen-Handler-Sitzung konnte nicht vor dem Usage-Limit gewarnt werden.",
"terminalSendFailed": "Konnte nicht an die Terminal-Sitzung gesendet werden.",
"deferredHandoffTitleSuffix": "warte auf Limit-Zurücksetzen ({0})",
"mergeHelperTitle": "Merge-Helfer",
"mergeHelperHandoffTitleSuffix": " (Übergabe)",
"mergeHelperTaskTitle": "Listen-Handler: {0}",
@@ -347,6 +450,10 @@
"enqueueAlreadyOpen": "Kann nicht in die Warteschlange gestellt werden — dieser Task hat eine offene interaktive Sitzung.",
"enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}",
"queueRefreshFailed": "Warteschlange konnte nicht aktualisiert werden: {0}",
"abandonRun": "Lauf abbrechen",
"abandonRunTip": "Die noch offenen Tasks dieses Laufs abbrechen und die Sitzung schließen",
"abandonRunConfirm": "{0} offene Aufgabe(n) abbrechen und diese Sitzung schließen? Aufgaben im Review oder bereits erledigt bleiben unangetastet.",
"abandonRunFailed": "Offene Tasks konnten nicht abgebrochen werden: {0}",
"question": {
"title": "Claude fragt nach",
"placeholder": "Antwort eingeben…",
@@ -380,24 +487,41 @@
"body": "ClaudeDo kann den Hintergrund-Worker nicht erreichen. Normalerweise wird er bei der Anmeldung automatisch gestartet. Du kannst ihn jetzt starten oder neu installieren, falls das Problem bestehen bleibt.",
"dismiss": "Ausblenden",
"rerunInstaller": "Installer erneut ausführen",
"startWorker": "Worker starten"
"startWorker": "Worker starten",
"workerNotFound": "Worker-Programm nicht gefunden. Installation prüfen.",
"installerNotFound": "Installer nicht gefunden. Installation prüfen.",
"startFailed": "Start fehlgeschlagen: {0}"
},
"listSettings": {
"title": "LISTENEINSTELLUNGEN",
"folderTitle": "ORDNER",
"deleteList": "Liste löschen",
"deleteFolder": "Ordner löschen",
"createAsFolder": "Stattdessen einen Ordner anlegen",
"createAsFolderHint": "Ein Ordner gruppiert nur Listen in der Seitenleiste — er enthält keine Aufgaben und führt nichts aus. Listen per Drag-and-Drop hineinziehen.",
"sectionGeneral": "ALLGEMEIN",
"name": "Name",
"color": "Farbe",
"colorHint": "Färbt den Punkt einer Liste bzw. Namen und Führungslinie eines Ordners. Der leere Ring stellt den Standard wieder her.",
"colorCustom": "Eigene",
"workingDirectory": "Arbeitsverzeichnis",
"workingDirectoryPlaceholder": "(keines)",
"baseBranch": "Basis-Branch",
"baseBranchPlaceholder": "(ausgecheckter Branch)",
"baseBranchHint": "Neue Task-Branches starten hier, und freigegebene Arbeit wird hierhin gemergt, z.B. dev. Leer = der gerade ausgecheckte Branch.",
"browse": "Durchsuchen...",
"defaultCommitType": "Standard-Commit-Typ",
"manualList": "Manuelle Liste (Erinnerungen)",
"manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.",
"findingsTracked": "Findings-Ordner .claudedo einchecken",
"findingsTrackedHint": "Aus: der Ordner bleibt über .git/info/exclude aus git heraus. An: Findings reisen mit dem Repo.",
"powerUseAllowed": "PowerUse für diese Liste erlauben",
"powerUseAllowedHint": "Erlaubt einem List-Handler-Run auf dieser Liste, unbeaufsichtigt zu laufen, sobald PowerUse in Einstellungen → Ausführung eingeschaltet ist: er fragt nichts, löst Konflikte selbst und sammelt neue Arbeit. Beide Schalter sind nötig — ein global aktives PowerUse ändert an einer Liste ohne Opt-in also nichts.",
"sectionIcon": "ICON",
"icon": "Listen-Icon",
"iconHint": "Wird in der Seitenleiste neben dem Listennamen angezeigt.",
"iconSearchWatermark": "Alle Icons durchsuchen (Englisch, z.B. rocket, car, bug)",
"iconSearchTruncated": "Die ersten 200 Treffer werden angezeigt — Suche verfeinern.",
"sectionAgent": "AGENT",
"resetAgentSettings": "Agent-Einstellungen zurücksetzen",
"sectionVerify": "VERIFIKATION",
@@ -414,7 +538,9 @@
"targetBranch": "Ziel-Branch",
"removeWorktree": "Worktree nach dem Mergen entfernen",
"commitMessage": "Commit-Nachricht",
"conflictedFiles": "Konfliktdateien:"
"conflictedFiles": "Konfliktdateien:",
"stashAndMerge": "Änderungen stashen und mergen",
"resolveConflicts": "Konflikte lösen"
},
"diff": {
"title": "DIFF",
@@ -446,6 +572,8 @@
"resolve": "Lösen",
"phantom": "Phantom",
"phantomTooltip": "Verzeichnis fehlt auf der Festplatte",
"mergedExternal": "gemergt (extern)",
"mergedExternalTooltip": "Die Commits sind schon im Default-Branch (lokaler origin-Stand, ohne Fetch), z.B. per Squash-PR. \"Abgeschlossene aufräumen\" entfernt ihn.",
"ctxShowDiff": "Diff anzeigen",
"ctxOpenInExplorer": "Im Explorer öffnen",
"ctxJumpToTask": "Zur Aufgabe springen",
@@ -470,6 +598,8 @@
"windowTitle": "Claude machen lassen",
"scopeList": "Liste: {0}",
"scopeReview": "Alle Listen — Tasks im Review",
"modelLabel": "Modell",
"modelTip": "Modell für die Triage-Session (Dedupe & Verfeinern). Spätere Merge-Phasen laufen immer auf Opus.",
"confirm": "Claude machen lassen",
"cancel": "Abbrechen",
"selectAll": "Alle auswählen",
@@ -497,21 +627,27 @@
"regenerate": "Neu erstellen",
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“."
},
"cheatSheets": {
"title": "CHEAT SHEETS",
"windowTitle": "Cheat Sheets",
"pin": "Anpinnen",
"pinTooltip": "Fenster über den anderen halten",
"missing": "Cheat Sheet {0} nicht gefunden."
},
"usageMonitor": {
"title": "USAGE MONITOR",
"windowTitle": "Usage Monitor",
"noGauges": "Noch keine Nutzungslimits gemeldet.",
"refresh": "Jetzt aktualisieren",
"refreshHint": "Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.",
"refreshHint": "Jetzt aktualisieren — Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.",
"staleFormat": "Werte veraltet (Stand {0})",
"staleGateHint": "Das Gate greift in diesem Zustand nicht.",
"staleGateHint": "Der Limit-Stopp greift in diesem Zustand nicht.",
"gateBlockedFormat": "Warteschlange pausiert — {0}",
"throttleFormat": "Warteschlange gedrosselt: {0}/{1} Slots ({2})",
"resetIn": "Reset in {0} ({1})",
"dragHint": "Marker ziehen oder Wert unten eintragen.",
"legendSoft": "Soft · 2 Slots",
"legendHard": "Hard · 1 Slot",
"legendGate": "Gate · Pause",
"legendGate": "Limit-Stopp · Pause",
"gaugeSession": "Session (5 Std.)",
"gaugeWeeklyAll": "Woche (alle Modelle)",
"gaugeWeeklyScopedFormat": "Woche ({0})",
@@ -531,7 +667,7 @@
"columnOtherOut": "ÜBRIGES OUT",
"columnOtherCache": "ÜBRIGES CACHE",
"columnShare": "ANTEIL",
"columnTask": "TASK",
"columnTask": "AUFGABE",
"columnList": "LISTE",
"columnModelShort": "MODELL",
"columnRuns": "RUNS",
@@ -635,35 +771,61 @@
"checks": {
"git": {
"title": "Git",
"hint": "Installiere Git und stelle sicher, dass es im PATH liegt."
"hint": "Installiere Git von https://git-scm.com/downloads und starte den Installer neu.",
"notFound": "git wurde nicht im PATH gefunden.",
"exitCode": "'{0}' wurde mit Exitcode {1} beendet.",
"resolvedFormat": "{0} — {1}"
},
"gitIdentity": {
"title": "Git-Identität",
"hint": "Lege deine Git-Identität fest: git config --global user.name \"...\" und user.email \"...\"."
"hint": "Führe aus: git config --global user.name \"Dein Name\" und git config --global user.email \"du@example.com\"",
"notFound": "git wurde nicht gefunden — Identität konnte nicht geprüft werden.",
"missing": "Fehlende Git-Identität: {0}."
},
"ports": {
"title": "Ports",
"hint": "Gib den Port frei oder ändere ihn nach der Installation auf der Worker-Seite."
"hint": "Ändere die SignalR-/MCP-Ports auf der Dienste-Seite, oder beende den blockierenden Prozess.",
"inUse": "{0}-Port {1} ist belegt.",
"inUseBy": "{0}-Port {1} ist belegt von '{2}'.",
"inUseByOwnWorker": "Belegt vom laufenden ClaudeDo Worker: {0}.",
"available": "Ports sind verfügbar."
},
"writeAccess": {
"title": "Schreibzugriff",
"hint": "Wähle ein anderes Installationsverzeichnis oder starte den Installer als Administrator."
"hint": "Wähle ein anderes Installationsverzeichnis oder starte den Installer mit ausreichenden Rechten (z. B. als Administrator).",
"cannotWrite": "Kein Schreibzugriff auf '{0}': {1}",
"writable": "Installations- und Datenverzeichnis sind beschreibbar."
},
"claudeCli": {
"title": "Claude CLI",
"hint": "Installiere die Claude CLI und stelle sicher, dass sie im PATH liegt."
"hint": "Installiere die Claude CLI (npm install -g @anthropic-ai/claude-code) und starte danach dein Terminal/deine Anmeldesitzung neu, damit der aktualisierte PATH greift.",
"notFound": "'{0}' wurde nicht im PATH gefunden. Durchsucht: {1}.",
"exitCode": "'{0}' wurde mit Exitcode {1} beendet.",
"resolvedFormat": "{0} — {1}"
},
"claudeVersion": {
"title": "Claude-CLI-Version",
"hint": "Aktualisiere die Claude CLI auf eine neuere Version."
"hint": "Aktualisiere die CLI: npm update -g @anthropic-ai/claude-code",
"notFound": "Die claude-CLI wurde nicht gefunden — Version konnte nicht geprüft werden.",
"parseFailed": "Konnte keine Version aus '{0}' auslesen.",
"tooOld": "claude {0} ist älter als die erforderliche Version {1}.",
"ok": "claude {0} erfüllt die Mindestanforderung ({1})."
},
"claudeAuth": {
"title": "Claude-Anmeldung",
"hint": "Führe 'claude auth login' aus, um dich anzumelden."
"title": "Claude-CLI-Login",
"hint": "Starte 'claude' einmal und schließe den Login-Vorgang ab.",
"notFound": "Die claude-CLI wurde nicht gefunden — Login konnte nicht geprüft werden.",
"undetermined": "Login-Status konnte nicht ermittelt werden.",
"notLoggedIn": "Nicht angemeldet.",
"loggedIn": "Angemeldet."
},
"permissionModeAuto": {
"title": "Berechtigungsmodus",
"hint": "Aktualisiere die Claude CLI auf eine Version, die --permission-mode auto unterstützt."
"hint": "Aktualisiere die CLI: npm update -g @anthropic-ai/claude-code. ClaudeDo funktioniert auch im Modus acceptEdits/default, dann mit mehr Rückfragen.",
"notFound": "Die claude-CLI wurde nicht gefunden — Unterstützung für permission-mode konnte nicht geprüft werden.",
"undetermined": "Konnte nicht ermitteln, ob --permission-mode auto unterstützt wird.",
"ok": "--permission-mode auto wird von dieser CLI erkannt.",
"notListed": "Diese claude-CLI-Version listet 'auto' nicht als --permission-mode-Option."
}
},
"planning": {
@@ -691,6 +853,9 @@
"abort": "Merge abbrechen"
},
"controls": {
"markdown": {
"copy": "Befehl kopieren"
},
"datePicker": {
"today": "Heute",
"tomorrow": "Morgen",
@@ -700,6 +865,14 @@
"done": "Fertig"
}
},
"dialog": {
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"delete": "Löschen",
"ok": "OK",
"confirmTitle": "Bestätigen",
"errorTitle": "Fehler"
},
"shell": {
"menu": {
"help": "Hilfe",
@@ -707,9 +880,18 @@
"repositories": "Repositories",
"checkForUpdates": "Nach Updates suchen",
"restartWorker": "Worker neu starten",
"updateClaudeCli": "Claude CLI aktualisieren",
"shutdown": "Beenden",
"worktrees": "Worktrees…",
"weeklyReport": "Wochenbericht…",
"usageMonitor": "Usage Monitor…",
"cheatSheets": "Cheat Sheets",
"cheatSheetGit": "Git…",
"cheatSheetCommits": "Conventional Commits…",
"cheatSheetClaude": "Claude CLI…",
"cheatSheetClaudeConfig": "Claude-Konfiguration…",
"cheatSheetDotnet": "dotnet CLI…",
"cheatSheetPowershell": "PowerShell…",
"about": "Über…",
"addRepos": "Repos als Listen hinzufügen…"
},
@@ -718,9 +900,6 @@
"updateNow": "Jetzt aktualisieren",
"dismiss": "Ausblenden"
},
"staleWorker": {
"message": "Der Worker läuft auf einem älteren Stand als der gemergte main-Branch dieses Repos — Neustart nötig, damit gemergte Änderungen wirken."
},
"externalMerge": {
"banner": "Eine Claude-Session löst gerade einen Merge-Konflikt auf — bitte keine Dateien im Repository bearbeiten.",
"open": "Resolver öffnen"
@@ -728,22 +907,22 @@
},
"vm": {
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
"shell": { "restartingWorker": "Worker wird neu gestartet…", "unexpectedError": "Unerwarteter Fehler: {0}" },
"shell": { "unexpectedError": "Unerwarteter Fehler: {0}", "updatingClaudeCli": "Claude CLI wird aktualisiert…", "shutdownConfirm": "Dadurch wird der ClaudeDo-Worker beendet (laufende Task-Runs und interaktive Sitzungen brechen ab) und die App geschlossen. Fortfahren?" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } },
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" },
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv", "waiting": "Wartet" },
"failureReason": { "maxTurns": "Turn-Limit erreicht", "timeout": "Zeitüberschreitung", "usageLimit": "Nutzungslimit erreicht", "error": "Fehler", "cancelled": "Abgebrochen", "unknown": "Grund unbekannt" },
"failureReasonTooltip": { "maxTurns": "Turn-Limit erreicht ({0}/{1} Turns) — der Worktree ist meist brauchbar; Task fortsetzen statt zurücksetzen." },
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
"queue": { "baseDirtyWarning": "Achtung: Das Repo dieser Liste hat uncommittete Änderungen ({0} geändert, {1} untracked) — ein neuer Worktree startet vom letzten Commit und enthält sie nicht." },
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "cancelReviewFailed": "Prüfung abbrechen fehlgeschlagen: {0}", "sendToQueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "queuePlanBlockedInteractive": "Plan kann nicht in die Warteschlange gestellt werden — {0} hat eine offene interaktive Sitzung und muss zuerst geschlossen werden.", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar.", "quickClaudeNoWorkingDir": "Für diese Liste ist kein Arbeitsverzeichnis konfiguriert.", "quickClaudeDirMissing": "Arbeitsverzeichnis existiert nicht mehr: {0}", "setStatusFailed": "Status konnte nicht aktualisiert werden: {0}", "cancelFailed": "Abbrechen fehlgeschlagen: {0}", "rejectToQueueFailed": "Zurückweisen in die Warteschlange fehlgeschlagen: {0}", "rejectToIdleFailed": "Zurückweisen fehlgeschlagen: {0}", "deleteTaskConfirm": "\"{0}\" löschen? Dies kann nicht rückgängig gemacht werden.", "deleteTaskFailed": "Löschen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}", "removeFromQueueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "statusPill": "{0} laufend · {1} Prüfung", "openTaskOne": "1 offene Aufgabe", "openTasks": "{0} offene Aufgaben", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "cancelReviewFailed": "Prüfung abbrechen fehlgeschlagen: {0}", "sendToQueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "queuePlanBlockedInteractive": "Plan kann nicht in die Warteschlange gestellt werden — {0} hat eine offene interaktive Sitzung und muss zuerst geschlossen werden.", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar.", "quickClaudeNoWorkingDir": "Für diese Liste ist kein Arbeitsverzeichnis konfiguriert.", "quickClaudeDirMissing": "Arbeitsverzeichnis existiert nicht mehr: {0}", "setStatusFailed": "Status konnte nicht aktualisiert werden: {0}", "cancelFailed": "Abbrechen fehlgeschlagen: {0}", "rejectToQueueFailed": "Zurückweisen in die Warteschlange fehlgeschlagen: {0}", "rejectToIdleFailed": "Zurückweisen fehlgeschlagen: {0}", "deleteTaskConfirm": "\"{0}\" löschen? Dies kann nicht rückgängig gemacht werden.", "deleteTaskFailed": "Löschen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}", "removeFromQueueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien).", "buildFailed": "Kombinierte Vorschau konnte nicht erstellt werden: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "dirtyTarget": "Merge abgelehnt — das Ziel-Arbeitsverzeichnis hat uncommittete Änderungen.", "targetDirty": "Das Ziel-Arbeitsverzeichnis hat bereits uncommittete Änderungen — stashen und mergen?", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
"onlineInbox": { "workerOffline": "Worker offline — Konfiguration kann nicht geladen werden.", "saved": "Konfiguration gespeichert.", "saveFailed": "Speichern fehlgeschlagen: {0}", "signedIn": "Erfolgreich angemeldet.", "signedInNoRole": "Angemeldet, aber diesem Konto fehlt die Rolle 'user' in Zitadel — die Online-Synchronisierung wird abgelehnt, bis die Rolle im ClaudeDo-Projekt zugewiesen wird.", "signInFailed": "Anmeldung fehlgeschlagen: {0}", "signedOut": "Abgemeldet.", "signOutFailed": "Abmeldung fehlgeschlagen: {0}" },
"tickets": { "connectionOk": "Verbunden als {0}.", "connectionMissingScopes": "Verbunden als {0}, aber es fehlen Scopes: {1}", "tokenCleared": "Token entfernt.", "workerOffline": "Worker nicht erreichbar." },
"tickets": { "azdoConnectionOk": "Verbunden — Repo {0}, Default-Branch {1}.", "connectionOk": "Verbunden als {0}.", "connectionMissingScopes": "Verbunden als {0}, aber es fehlen Scopes: {1}", "tokenCleared": "Token entfernt.", "workerOffline": "Worker nicht erreichbar." },
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "error": "Fehler: {0}" },
"usageMonitor": {
"loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}",
@@ -759,18 +938,19 @@
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchDone": "{0} gemergt, {1} zu lösen.", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
"listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —" },
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
"listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —", "loadFailed": "Listeneinstellungen konnten nicht geladen werden: {0}", "ticketProjectsLoadFailed": "Ticket-Projekte konnten nicht geladen werden: {0}", "confirmDeleteFolder": "Ordner \"{0}\" löschen? Die Listen und Unterordner darin bleiben erhalten und wandern auf die oberste Ebene.", "confirmDeleteList": "Liste \"{0}\" und alle ihre Aufgaben löschen? Das kann nicht rückgängig gemacht werden.", "deleteBlockedPlanning": "Diese Liste hat Planungssitzungen mit Kind-Aufgaben. Erst diese verwerfen, dann die Liste löschen.", "workingDirMissing": "Dieser Ordner existiert nicht.", "workingDirNotRepo": "Dieser Ordner ist noch kein Git-Repository. Führe darin zuerst `git init` aus, oder leere das Arbeitsverzeichnis, um dies als manuelle Liste zu speichern." },
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "dirtyTarget": "Merge abgelehnt — das Ziel-Arbeitsverzeichnis hat uncommittete Änderungen.", "stashAndMergeConfirm": "Das Ziel-Arbeitsverzeichnis hat uncommittete Änderungen.\nStashen, mergen und danach wiederherstellen?", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
"mergeSection": { "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
"about": { "openPathFailed": "Pfad konnte nicht geöffnet werden: {0}" },
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste", "findingsNotFound": "Für diese Liste gibt es noch keine Findings.", "findingsOpenFailed": "Findings-Ordner konnte nicht geöffnet werden: {0}", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
"repoImport": { "loadFailed": "Gespeicherte Ordner konnten nicht geladen werden: {0}", "saveFailed": "Ordner konnten nicht gespeichert werden: {0}" }
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "smartNotes": "Notizen", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste", "findingsNotFound": "Für diese Liste gibt es noch keine Findings.", "findingsOpenFailed": "Findings-Ordner konnte nicht geöffnet werden: {0}", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
"repoImport": { "loadFailed": "Gespeicherte Ordner konnten nicht geladen werden: {0}", "saveFailed": "Ordner konnten nicht gespeichert werden: {0}", "createButton": "{0} Liste(n) erstellen" }
},
"ops": {
"stalledHint": "Läuft weiter — Worker antwortet noch",
"merge": {
"merging": "Wird zusammengeführt…",
"verifying": "Verify-Kommando der Liste läuft… ({0})"
"verifying": "Verify-Kommando der Liste läuft… ({0})",
"publishing": "Push und Draft-PR laufen…"
},
"diff": {
"parsing": "Diff wird geparst…"
@@ -804,7 +984,8 @@
},
"reports": {
"generatingWeekly": "Bericht wird erstellt…",
"runningDailyPrep": "Tagesvorbereitung läuft…"
"runningDailyPrep": "Tagesvorbereitung läuft…",
"runningTicketImport": "Tickets werden importiert, danach Tagesplanung…"
},
"planning": {
"finalizing": "Plan wird finalisiert…",
@@ -832,39 +1013,5 @@
"lastError": "Letzter Fehler: {0}",
"throttled": "Gedrosselt: {0}/{1} Slots ({2})"
}
},
"checks": {
"git": {
"title": "Git",
"hint": "Installiere Git von https://git-scm.com/downloads und starte den Installer neu."
},
"gitIdentity": {
"title": "Git-Identität",
"hint": "Führe aus: git config --global user.name \"Dein Name\" und git config --global user.email \"du@example.com\""
},
"ports": {
"title": "Ports",
"hint": "Ändere die SignalR-/MCP-Ports auf der Dienste-Seite, oder beende den blockierenden Prozess."
},
"writeAccess": {
"title": "Schreibzugriff",
"hint": "Wähle ein anderes Installationsverzeichnis oder starte den Installer mit ausreichenden Rechten (z. B. als Administrator)."
},
"claudeCli": {
"title": "Claude CLI",
"hint": "Installiere die Claude CLI (npm install -g @anthropic-ai/claude-code) und starte danach dein Terminal/deine Anmeldesitzung neu, damit der aktualisierte PATH greift."
},
"claudeVersion": {
"title": "Claude-CLI-Version",
"hint": "Aktualisiere die CLI: npm update -g @anthropic-ai/claude-code"
},
"claudeAuth": {
"title": "Claude-CLI-Login",
"hint": "Starte 'claude' einmal und schließe den Login-Vorgang ab."
},
"permissionModeAuto": {
"title": "Unterstützung für --permission-mode auto",
"hint": "Aktualisiere die CLI: npm update -g @anthropic-ai/claude-code. ClaudeDo funktioniert auch im Modus acceptEdits/default, dann mit mehr Rückfragen."
}
}
}
+216 -69
View File
@@ -11,11 +11,13 @@
"categoryGeneral": "General",
"categoryExecution": "Execution",
"categoryPrime": "Prime Claude",
"categoryPrimeHint": "Scheduled Claude runs: Ping, Fill My Day, or a custom prompt, at fixed times.",
"categoryWorktrees": "Worktrees",
"categoryFiles": "Files & Prompts",
"categorySkills": "Session Skills",
"categoryReports": "Reports",
"categoryTickets": "Ticket system"
"categoryTickets": "External services",
"categorySsh": "SSH workspaces"
},
"repoHint": {
"text": "Only takes effect once a repo is linked.",
@@ -27,6 +29,7 @@
"model": "Model",
"maxTurns": "Max turns",
"effort": "Effort",
"effortHint": "Reasoning effort the model applies. Higher costs more and reasons deeper.",
"modelPresets": "Per-model defaults",
"modelPresetsHint": "Reasoning effort and turn budget used when a session runs under that model. List- and task-level max-turn overrides still win.",
"maxTurnsCeiling": "Max turns ceiling",
@@ -41,6 +44,16 @@
"usageGateSevenDayPct": "7-day window (%)",
"autoContinueOnUsageLimit": "Continue on session limit reset",
"autoContinueOnUsageLimitHint": "When a run stops because it hit the usage limit, automatically continue it once the 5-hour window resets. Also wakes the paused queue exactly at reset time instead of waiting for the next backstop check.",
"listHandlerMaxHandoffs": "List-handler handoff budget",
"listHandlerMaxHandoffsHint": "How many wait/merge handoffs one \"Let Claude handle it\" run may chain. 4 means two merge rounds. Raise it for runs meant to keep going for hours; it bounds a runaway chain, so it cannot be 0.",
"powerUseHeading": "PowerUse",
"powerUse": "Enable PowerUse",
"powerUseHint": "Spend the rest of the window: a list-handler run asks nothing, resolves conflicts itself, harvests new work, and keeps handing off for as long as its budget allows. Only affects lists that allowed PowerUse in their list settings.",
"powerUseWindow": "Only between",
"powerUseWindowPlaceholder": "HH:mm",
"powerUseHandoffPct": "Hand off at (% of 5h)",
"powerUseWindowHint": "Leave both times empty for round the clock. A window is checked whenever a handler phase starts, so a chain that runs past the end time continues without PowerUse.",
"powerUseWindowInvalid": "PowerUse window times must be HH:mm, or left blank.",
"throttleStagesHeading": "Throttle stages",
"throttleStagesHint": "Parallelism is capped before the hard gate: soft caps at 2 slots, hard at 1. Edited by dragging the markers on the usage-monitor gauges. Opening the monitor closes these settings — save first if you changed anything.",
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
@@ -85,6 +98,7 @@
"retryPrompt": "Retry",
"dailyPrepPrompt": "Daily prep",
"weeklyReportPrompt": "Weekly report",
"powerUsePrompt": "PowerUse addendum",
"viewPrompt": "View",
"customizedSection": "CUSTOMIZED",
"customizedHint": "These prompts differ from the bundled default and are kept as-is. Reset to pick up future default improvements.",
@@ -99,10 +113,11 @@
"actionPing": "Ping",
"actionFillMyDay": "Fill My Day",
"actionCustom": "Custom prompt",
"actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.",
"actionImportTickets": "Tickets + plan day",
"actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Tickets + plan day first imports your assigned tickets into the matching lists, then runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.",
"customPromptLabel": "Prompt",
"customPromptPlaceholder": "The full prompt to run for this schedule (required)",
"maxTasksDisabledHint": "Only applies to Fill My Day schedules.",
"maxTasksDisabledHint": "Only applies to Fill My Day and Tickets + plan day schedules.",
"promptOverrideLabel": "Prompt addition",
"promptOverridePlaceholder": "Extra instructions appended to the daily prep prompt for this schedule (optional)",
"dayMo": "Mo",
@@ -134,12 +149,32 @@
"saveButton": "Save config"
},
"tickets": {
"ticketSection": "TICKET SYSTEM",
"azdoSection": "AZURE DEVOPS",
"azdoToken": "Personal access token (scope: Code read & write)",
"azdoHint": "Used in company repos (origin on the Azure DevOps Server): fetch before a task starts, push and draft PR on approve.",
"baseUrl": "API base URL",
"baseUrlHint": "Leave empty to disable the ticket integration entirely.",
"token": "Personal access token",
"tokenSet": "stored",
"testConnection": "Test connection",
"clearToken": "Remove token"
"clearToken": "Remove token",
"fallbackList": "Fallback list",
"fallbackListHint": "Imported ticket items Claude can't match to a list land here. None = they are skipped.",
"fallbackNone": "(none)"
},
"ssh": {
"hint": "Servers you can start an interactive Claude session on from Mission Control. Host is an alias from ~/.ssh/config (user, key and port come from there); Claude must be installed and logged in on the server.",
"name": "Name",
"host": "Host alias",
"remotePath": "Remote path (/… or ~/…)",
"add": "Add workspace",
"remove": "Remove",
"invalid": {
"name": "SSH workspace: name is required.",
"host": "SSH workspace \"{0}\": host must be an ssh alias (letters, digits, . _ @ -, not starting with -).",
"path": "SSH workspace \"{0}\": remote path must start with / or ~ and must not contain '."
}
},
"inherit": {
"inheritedFromList": "inherited · List",
@@ -177,7 +212,6 @@
"listSettingsTip": "List settings",
"addPlaceholder": "Add a task…",
"enterKey": "ENTER",
"notesPinnedRow": "Notes (daily notes)",
"clearDayTip": "Clear day",
"planMyDayTip": "Plan My Day",
"overdue": "OVERDUE",
@@ -189,6 +223,7 @@
"ctxMarkAs": "Mark as",
"ctxMarkDone": "Done",
"ctxMarkCancelled": "Cancelled",
"ctxMarkIdle": "Idle",
"ctxOpenConPtySession": "Open interactive session",
"ctxOpenWorktree": "Open worktree",
"ctxOpenPlanningSession": "Open planning Session",
@@ -209,6 +244,7 @@
"ctxRefineTask": "Refine task",
"ctxPlanning": "Planning",
"ctxSchedule": "Schedule",
"ctxMoveToList": "Move to list",
"ctxDeleteTask": "Delete task",
"reasonAlreadyRunning": "Task is already running",
"reasonAlreadyQueued": "Already in the queue",
@@ -229,6 +265,7 @@
"toggleSubtasksTip": "Expand / collapse subtasks",
"agentSuggestedTip": "Suggested by the agent",
"interactiveChipTip": "Interactive session open — click to jump to it in Mission Control",
"waitingOnTip": "Waiting on {0}",
"scheduleTitle": "Schedule task",
"scheduleWhen": "WHEN",
"scheduleConfirm": "Schedule",
@@ -250,6 +287,8 @@
"contextOpenFindings": "Open findings folder",
"contextImportTickets": "Update from ticket system",
"contextSettings": "Settings…",
"contextFolderSettings": "Folder settings…",
"contextRemoveFromFolder": "Move out of folder",
"newList": "New list",
"addReposTip": "Add repos as lists",
"noRepoBannerText": "No repo linked yet. ClaudeDo runs tasks in a git repo.",
@@ -265,9 +304,11 @@
"starTip": "Star",
"agentSettingsTip": "Agent settings",
"agentSettingsHeading": "Agent settings (overrides)",
"waitingOnPrefix": "Waiting on",
"copyFormattedTip": "Copy title, description and open steps",
"toggleEditPreviewTip": "Toggle edit/preview",
"planDay": "Plan day",
"importTicketsAndPlanDay": "Tickets + plan day",
"prepEmpty": "No prep run today yet — click Plan day",
"roadblockReply": {
"placeholder": "Reply to the roadblock…",
@@ -290,7 +331,41 @@
"steps": "Steps",
"files": "Files"
},
"emptyState": "Select a task to see its details"
"emptyState": "Select a task to see its details",
"roadblockLabel": "ROADBLOCK",
"stepsCard": {
"preview": "Preview",
"edit": "Edit",
"addStepPlaceholder": "Add step…"
},
"workConsole": {
"continue": "Continue",
"resetAndRetry": "Reset & Retry",
"tabOutput": "Output",
"tabGit": "Git",
"tabSession": "Session",
"outcomeLabel": "OUTCOME",
"reviewLabel": "REVIEW",
"mergeLabel": "MERGE",
"targetBranch": "Target branch",
"openDiff": "Open Diff",
"worktree": "Worktree",
"reviewCombinedDiff": "Review Combined Diff",
"openDiffHint": "Open the diff to enable merge",
"targetDirty": "Target working directory has uncommitted changes — merging will stash and restore them.",
"approveMerge": "Approve & Merge",
"createDraftPr": "Create draft PR",
"updatePr": "Update PR",
"openPr": "Open pull request",
"companyTargetHint": "Company repo — approve pushes the branch and opens a draft PR on Azure DevOps.",
"park": "Park",
"parkTip": "Set aside — back to Idle, keeps the worktree",
"cancel": "Cancel",
"resetDiscardBranch": "Reset (discard branch)…",
"submitForReviewHint": "Worked on this by hand? Submit the worktree for review to merge it.",
"submitForReview": "Submit for review",
"outcomesLabel": "OUTCOMES"
}
},
"agent": {
"openWorktreeTip": "Open worktree in file explorer — requires an active worktree",
@@ -299,8 +374,11 @@
},
"notes": {
"today": "Today",
"add": "Add",
"newNotePlaceholder": "New note…"
"yesterday": "Yesterday",
"newNotePlaceholder": "New note…",
"emptyHint": "Nothing noted yet. Type, hit Enter, done.",
"deleteTip": "Delete note",
"fromAgentTip": "Written by Claude"
},
"session": {
"chipLive": "LIVE",
@@ -320,18 +398,43 @@
"empty": "No running tasks",
"settings": "Settings",
"activity": "Activity",
"runningNow": "Running now",
"opVerifyMerge": "Verifying after merge",
"opVerifyBatch": "Verify (batch)",
"opMerging": "Merging",
"opRebasing": "Rebasing",
"opCreatingWorktree": "Creating worktree",
"operationsRefreshFailed": "Couldn't load running operations: {0}",
"blocked": "Blocked",
"running": "Running",
"review": "In review",
"verifying": "Verifying",
"failed": "Failed",
"roadblock": "Roadblock reported",
"openSession": "Open session",
"homeFolder": "Home (quick session)",
"sshTarget": "{0} (SSH)",
"browseForFolder": "Choose folder…",
"recentSessions": "Recent sessions",
"recentSessionsEmpty": "No sessions in the last 14 days",
"resumeSession": "Resume",
"showBrief": "Brief",
"renameSession": "Double-click to rename",
"renameFailed": "Couldn't rename the session: {0}",
"handoffSession": "Handoff",
"briefOpenFailed": "Couldn't open the brief: {0}",
"focusMode": "Focus",
"overviewMode": "Overview",
"columns": "Columns",
"columnsAuto": "Auto",
"closeSession": "Kill session",
"closeSessionConfirm": "End the running session? The Claude process will be aborted.",
"conptyLaunchFailed": "Couldn't open interactive session: {0}",
"conptyStarting": "Starting session…",
"conptyExited": "Session ended (code {0}).",
"usageWarningNotDelivered": "Couldn't warn the list-handler session about the usage limit.",
"terminalSendFailed": "Couldn't send that to the terminal session.",
"deferredHandoffTitleSuffix": "waiting for limit reset ({0})",
"mergeHelperTitle": "Merge Helper",
"mergeHelperHandoffTitleSuffix": " (Handoff)",
"mergeHelperTaskTitle": "List handler: {0}",
@@ -347,6 +450,10 @@
"enqueueAlreadyOpen": "Can't queue — this task has an open interactive session.",
"enqueueFailed": "Queue failed: {0}",
"queueRefreshFailed": "Couldn't refresh the queue: {0}",
"abandonRun": "Abandon run",
"abandonRunTip": "Cancel this run's still-open tasks and close the session",
"abandonRunConfirm": "Cancel {0} open task(s) and close this session? Tasks already in review or done are not affected.",
"abandonRunFailed": "Couldn't cancel the open tasks: {0}",
"question": {
"title": "Claude is asking",
"placeholder": "Type your answer…",
@@ -380,24 +487,41 @@
"body": "ClaudeDo can't reach the background worker. It is normally started automatically at logon. You can start it now, or reinstall if the problem persists.",
"dismiss": "Dismiss",
"rerunInstaller": "Rerun Installer",
"startWorker": "Start Worker"
"startWorker": "Start Worker",
"workerNotFound": "Worker executable not found. Check the installation.",
"installerNotFound": "Installer not found. Check the installation.",
"startFailed": "Start failed: {0}"
},
"listSettings": {
"title": "LIST SETTINGS",
"folderTitle": "FOLDER",
"deleteList": "Delete list",
"deleteFolder": "Delete folder",
"createAsFolder": "Create a folder instead",
"createAsFolderHint": "A folder just groups lists in the sidebar — it holds no tasks and runs nothing. Drag lists onto it to fill it.",
"sectionGeneral": "GENERAL",
"name": "Name",
"color": "Colour",
"colorHint": "Tints the dot of a list, or the name and guide line of a folder. The empty ring restores the default.",
"colorCustom": "Custom",
"workingDirectory": "Working directory",
"workingDirectoryPlaceholder": "(none)",
"baseBranch": "Base branch",
"baseBranchPlaceholder": "(checked-out branch)",
"baseBranchHint": "New task branches start here and approved work merges back here, e.g. dev. Leave empty to use whatever branch is checked out.",
"browse": "Browse...",
"defaultCommitType": "Default commit type",
"manualList": "Manual list (reminders)",
"manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.",
"findingsTracked": "Commit the .claudedo findings folder",
"findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.",
"powerUseAllowed": "Allow PowerUse for this list",
"powerUseAllowedHint": "Lets a list-handler run on this list go unattended when PowerUse is switched on in Settings → Execution: it asks nothing, resolves conflicts itself, and harvests new work. Both switches are needed, so leaving PowerUse on globally never changes a list that hasn't opted in.",
"sectionIcon": "ICON",
"icon": "List icon",
"iconHint": "Shown next to the list name in the sidebar.",
"iconSearchWatermark": "Search all icons (English, e.g. rocket, car, bug)",
"iconSearchTruncated": "Showing the first 200 matches — refine the search.",
"sectionAgent": "AGENT",
"resetAgentSettings": "Reset agent settings",
"sectionVerify": "VERIFICATION",
@@ -414,7 +538,9 @@
"targetBranch": "Target branch",
"removeWorktree": "Remove worktree after merge",
"commitMessage": "Commit message",
"conflictedFiles": "Conflicted files:"
"conflictedFiles": "Conflicted files:",
"stashAndMerge": "Stash changes and merge",
"resolveConflicts": "Resolve conflicts"
},
"diff": {
"title": "DIFF",
@@ -446,6 +572,8 @@
"resolve": "Resolve",
"phantom": "phantom",
"phantomTooltip": "Directory missing on disk",
"mergedExternal": "merged (external)",
"mergedExternalTooltip": "Its commits are already in the default branch (local origin ref, not fetched) — e.g. a squash PR. \"Cleanup finished\" removes it.",
"ctxShowDiff": "Show diff",
"ctxOpenInExplorer": "Open in Explorer",
"ctxJumpToTask": "Jump to task",
@@ -470,6 +598,8 @@
"windowTitle": "Let Claude handle it",
"scopeList": "List: {0}",
"scopeReview": "All lists — tasks waiting for review",
"modelLabel": "Model",
"modelTip": "Model for the triage session (dedupe & refine). Later merge phases always run on opus.",
"confirm": "Let Claude handle it",
"cancel": "Cancel",
"selectAll": "Select all",
@@ -497,12 +627,18 @@
"regenerate": "Regenerate",
"emptyStateHint": "No report for this range yet. Click “Generate”."
},
"cheatSheets": {
"title": "CHEAT SHEETS",
"windowTitle": "Cheat Sheets",
"pin": "Pin",
"pinTooltip": "Keep this window on top of the others",
"missing": "Cheat sheet {0} not found."
},
"usageMonitor": {
"title": "USAGE MONITOR",
"windowTitle": "Usage Monitor",
"noGauges": "No usage limits reported yet.",
"refresh": "Refresh now",
"refreshHint": "Polled every 5 min while a task runs, otherwise every 15 min.",
"refreshHint": "Refresh now — polled every 5 min while a task runs, otherwise every 15 min.",
"staleFormat": "Values stale (as of {0})",
"staleGateHint": "The gate does not apply while values are stale.",
"gateBlockedFormat": "Queue paused — {0}",
@@ -635,35 +771,61 @@
"checks": {
"git": {
"title": "Git",
"hint": "Install Git and make sure it is on PATH."
"hint": "Install Git from https://git-scm.com/downloads, then restart the installer.",
"notFound": "git was not found on PATH.",
"exitCode": "'{0}' exited with code {1}.",
"resolvedFormat": "{0} — {1}"
},
"gitIdentity": {
"title": "Git Identity",
"hint": "Set your git identity: git config --global user.name \"...\" and user.email \"...\"."
"hint": "Run: git config --global user.name \"Your Name\" and git config --global user.email \"you@example.com\"",
"notFound": "git was not found — identity could not be checked.",
"missing": "Missing git identity: {0}."
},
"ports": {
"title": "Ports",
"hint": "Free the port, or change it on the Worker page after install."
"hint": "Change the SignalR/MCP ports on the Service page, or stop the process using them.",
"inUse": "{0} port {1} is in use.",
"inUseBy": "{0} port {1} is in use by '{2}'.",
"inUseByOwnWorker": "In use by the running ClaudeDo Worker: {0}.",
"available": "Ports are available."
},
"writeAccess": {
"title": "Write Access",
"hint": "Choose a different install directory, or run the installer as administrator."
"hint": "Choose a different install directory, or run the installer with sufficient permissions (e.g. as administrator).",
"cannotWrite": "Cannot write to '{0}': {1}",
"writable": "Install directory and data directory are writable."
},
"claudeCli": {
"title": "Claude CLI",
"hint": "Install the Claude CLI and make sure it is on PATH."
"hint": "Install the Claude CLI (npm install -g @anthropic-ai/claude-code), then restart your terminal/login session so the updated PATH takes effect.",
"notFound": "'{0}' was not found on PATH. Searched: {1}.",
"exitCode": "'{0}' exited with code {1}.",
"resolvedFormat": "{0} — {1}"
},
"claudeVersion": {
"title": "Claude CLI Version",
"hint": "Update the Claude CLI to a newer version."
"hint": "Update the CLI: npm update -g @anthropic-ai/claude-code",
"notFound": "The claude CLI was not found — version could not be checked.",
"parseFailed": "Could not parse a version from '{0}'.",
"tooOld": "claude {0} is older than the required {1}.",
"ok": "claude {0} meets the minimum ({1})."
},
"claudeAuth": {
"title": "Claude Login",
"hint": "Run 'claude auth login' to sign in."
"title": "Claude CLI Login",
"hint": "Run 'claude' once and complete the login flow.",
"notFound": "The claude CLI was not found — login could not be checked.",
"undetermined": "Login status could not be determined.",
"notLoggedIn": "Not logged in.",
"loggedIn": "Logged in."
},
"permissionModeAuto": {
"title": "Permission Mode",
"hint": "Update the Claude CLI to a version that supports --permission-mode auto."
"hint": "Update the CLI: npm update -g @anthropic-ai/claude-code. ClaudeDo still works in acceptEdits/default mode, with more prompts.",
"notFound": "The claude CLI was not found — permission-mode support could not be checked.",
"undetermined": "Could not determine whether --permission-mode auto is supported.",
"ok": "--permission-mode auto is recognized by this CLI.",
"notListed": "This claude CLI version does not list 'auto' as a --permission-mode choice."
}
},
"planning": {
@@ -691,6 +853,9 @@
"abort": "Abort merge"
},
"controls": {
"markdown": {
"copy": "Copy command"
},
"datePicker": {
"today": "Today",
"tomorrow": "Tomorrow",
@@ -700,6 +865,14 @@
"done": "Done"
}
},
"dialog": {
"cancel": "Cancel",
"confirm": "Confirm",
"delete": "Delete",
"ok": "OK",
"confirmTitle": "Confirm",
"errorTitle": "Error"
},
"shell": {
"menu": {
"help": "Help",
@@ -707,9 +880,18 @@
"repositories": "Repositories",
"checkForUpdates": "Check for updates",
"restartWorker": "Restart worker",
"updateClaudeCli": "Update Claude CLI",
"shutdown": "Shutdown",
"worktrees": "Worktrees…",
"weeklyReport": "Weekly Report…",
"usageMonitor": "Usage Monitor…",
"cheatSheets": "Cheat Sheets",
"cheatSheetGit": "Git…",
"cheatSheetCommits": "Conventional Commits…",
"cheatSheetClaude": "Claude CLI…",
"cheatSheetClaudeConfig": "Claude configuration…",
"cheatSheetDotnet": "dotnet CLI…",
"cheatSheetPowershell": "PowerShell…",
"about": "About…",
"addRepos": "Add repos as lists…"
},
@@ -718,9 +900,6 @@
"updateNow": "Update now",
"dismiss": "Dismiss"
},
"staleWorker": {
"message": "The worker is running an older build than this repo's merged main — restart it so your merged changes take effect."
},
"externalMerge": {
"banner": "A Claude session is resolving a merge conflict — don't edit files in the repository.",
"open": "Open resolver"
@@ -728,22 +907,22 @@
},
"vm": {
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
"shell": { "restartingWorker": "Restarting worker…", "unexpectedError": "Unexpected error: {0}" },
"shell": { "unexpectedError": "Unexpected error: {0}", "updatingClaudeCli": "Updating Claude CLI…", "shutdownConfirm": "This stops the ClaudeDo worker (any running task or interactive session is interrupted) and closes the app. Continue?" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } },
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" },
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive", "waiting": "Waiting" },
"failureReason": { "maxTurns": "Turn limit reached", "timeout": "Timed out", "usageLimit": "Usage limit reached", "error": "Error", "cancelled": "Cancelled", "unknown": "Unknown reason" },
"failureReasonTooltip": { "maxTurns": "Turn limit reached ({0}/{1} turns) — the worktree is usually fine; continue the task instead of resetting it." },
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
"queue": { "baseDirtyWarning": "Heads up: this list's repo has uncommitted changes ({0} modified, {1} untracked) — a new worktree starts from the last commit and won't include them." },
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "cancelReviewFailed": "Cancel review failed: {0}", "sendToQueueFailed": "Send to queue failed: {0}", "queuePlanBlockedInteractive": "Can't queue the plan — {0} has an open interactive session and must be closed first.", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available.", "quickClaudeNoWorkingDir": "This list has no working directory configured.", "quickClaudeDirMissing": "Working directory no longer exists: {0}", "setStatusFailed": "Failed to update status: {0}", "cancelFailed": "Cancel failed: {0}", "rejectToQueueFailed": "Reject to queue failed: {0}", "rejectToIdleFailed": "Reject failed: {0}", "deleteTaskConfirm": "Delete \"{0}\"? This cannot be undone.", "deleteTaskFailed": "Delete failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}", "removeFromQueueFailed": "Remove from queue failed: {0}", "openWorktreeFailed": "Couldn't open worktree: {0}" },
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "statusPill": "{0} running · {1} review", "openTaskOne": "1 open task", "openTasks": "{0} open tasks", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "cancelReviewFailed": "Cancel review failed: {0}", "sendToQueueFailed": "Send to queue failed: {0}", "queuePlanBlockedInteractive": "Can't queue the plan — {0} has an open interactive session and must be closed first.", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available.", "quickClaudeNoWorkingDir": "This list has no working directory configured.", "quickClaudeDirMissing": "Working directory no longer exists: {0}", "setStatusFailed": "Failed to update status: {0}", "cancelFailed": "Cancel failed: {0}", "rejectToQueueFailed": "Reject to queue failed: {0}", "rejectToIdleFailed": "Reject failed: {0}", "deleteTaskConfirm": "Delete \"{0}\"? This cannot be undone.", "deleteTaskFailed": "Delete failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}", "removeFromQueueFailed": "Remove from queue failed: {0}", "openWorktreeFailed": "Couldn't open worktree: {0}" },
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files).", "buildFailed": "Could not build combined preview: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "dirtyTarget": "Merge refused — the target working directory has uncommitted changes.", "targetDirty": "The target working directory already has uncommitted changes — stash them and merge?", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
"onlineInbox": { "workerOffline": "Worker offline — cannot load config.", "saved": "Config saved.", "saveFailed": "Save failed: {0}", "signedIn": "Signed in successfully.", "signedInNoRole": "Signed in, but this account is missing the 'user' role in Zitadel — online sync will be rejected until the role is granted in the ClaudeDo project.", "signInFailed": "Sign-in failed: {0}", "signedOut": "Signed out.", "signOutFailed": "Sign-out failed: {0}" },
"tickets": { "connectionOk": "Connected as {0}.", "connectionMissingScopes": "Connected as {0}, but missing scopes: {1}", "tokenCleared": "Token removed.", "workerOffline": "Worker not reachable." },
"tickets": { "azdoConnectionOk": "Connected — repo {0}, default branch {1}.", "connectionOk": "Connected as {0}.", "connectionMissingScopes": "Connected as {0}, but missing scopes: {1}", "tokenCleared": "Token removed.", "workerOffline": "Worker not reachable." },
"weeklyReport": { "invalidRange": "Invalid date range.", "error": "Error: {0}" },
"usageMonitor": {
"loadFailed": "Couldn't load usage data: {0}",
@@ -759,18 +938,19 @@
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchDone": "Merged {0}, {1} need resolution.", "openInExplorerFailed": "Couldn't open path: {0}" },
"listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —" },
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
"listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —", "loadFailed": "Couldn't load list settings: {0}", "ticketProjectsLoadFailed": "Couldn't load ticket projects: {0}", "confirmDeleteFolder": "Delete folder \"{0}\"? The lists and subfolders inside it are kept and move back to the top level.", "confirmDeleteList": "Delete list \"{0}\" and all its tasks? This cannot be undone.", "deleteBlockedPlanning": "This list has planning sessions with child tasks. Discard those first, then delete the list.", "workingDirMissing": "This folder doesn't exist.", "workingDirNotRepo": "This folder isn't a git repository yet. Run `git init` in it first, or clear the working directory to save this as a manual list." },
"detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "dirtyTarget": "Merge refused — the target working directory has uncommitted changes.", "stashAndMergeConfirm": "The target working directory has uncommitted changes.\nStash them, merge, and restore them afterwards?", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
"mergeSection": { "openWorktreeFailed": "Couldn't open worktree: {0}" },
"about": { "openPathFailed": "Couldn't open path: {0}" },
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list", "findingsNotFound": "No findings yet for this list.", "findingsOpenFailed": "Couldn't open findings folder: {0}", "openInExplorerFailed": "Couldn't open path: {0}" },
"repoImport": { "loadFailed": "Couldn't load remembered folders: {0}", "saveFailed": "Couldn't save folders: {0}" }
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "smartNotes": "Notes", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list", "findingsNotFound": "No findings yet for this list.", "findingsOpenFailed": "Couldn't open findings folder: {0}", "openInExplorerFailed": "Couldn't open path: {0}" },
"repoImport": { "loadFailed": "Couldn't load remembered folders: {0}", "saveFailed": "Couldn't save folders: {0}", "createButton": "Create {0} list(s)" }
},
"ops": {
"stalledHint": "Still running — worker hasn't responded in a while",
"merge": {
"merging": "Merging…",
"verifying": "Running the list's verify command… ({0})"
"verifying": "Running the list's verify command… ({0})",
"publishing": "Pushing and opening the draft PR…"
},
"diff": {
"parsing": "Parsing diff…"
@@ -804,7 +984,8 @@
},
"reports": {
"generatingWeekly": "Generating report…",
"runningDailyPrep": "Running daily prep…"
"runningDailyPrep": "Running daily prep…",
"runningTicketImport": "Importing tickets, then planning the day…"
},
"planning": {
"finalizing": "Finalizing plan…",
@@ -832,39 +1013,5 @@
"lastError": "Last error: {0}",
"throttled": "Throttled: {0}/{1} slots ({2})"
}
},
"checks": {
"git": {
"title": "Git",
"hint": "Install Git from https://git-scm.com/downloads, then restart the installer."
},
"gitIdentity": {
"title": "Git identity",
"hint": "Run: git config --global user.name \"Your Name\" and git config --global user.email \"you@example.com\""
},
"ports": {
"title": "Ports",
"hint": "Change the SignalR/MCP ports on the Service page, or stop the process using them."
},
"writeAccess": {
"title": "Write access",
"hint": "Choose a different install directory, or run the installer with sufficient permissions (e.g. as administrator)."
},
"claudeCli": {
"title": "Claude CLI",
"hint": "Install the Claude CLI (npm install -g @anthropic-ai/claude-code), then restart your terminal/login session so the updated PATH takes effect."
},
"claudeVersion": {
"title": "Claude CLI version",
"hint": "Update the CLI: npm update -g @anthropic-ai/claude-code"
},
"claudeAuth": {
"title": "Claude CLI login",
"hint": "Run 'claude' once and complete the login flow."
},
"permissionModeAuto": {
"title": "--permission-mode auto support",
"hint": "Update the CLI: npm update -g @anthropic-ai/claude-code. ClaudeDo still works in acceptEdits/default mode, with more prompts."
}
}
}
+14 -4
View File
@@ -5,20 +5,30 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui;
public sealed record AccentPreset(string Name, string Accent, string Dim, string Soft, string Glow);
/// <summary>An accent theme; <paramref name="Swatches"/> holds one hex per <c>SwatchNBrush</c> token (index 0 = Swatch1).</summary>
public sealed record AccentPreset(string Name, string Accent, string Dim, string Soft, string Glow, string[] Swatches);
public static class AccentPresets
{
// Hue 88 — moss/sage green (original)
public static readonly AccentPreset Moss = new("moss", "#FF7C9166", "#FF64785A", "#FF3E4B39", "#387C9166");
public static readonly AccentPreset Moss = new("moss", "#FF7C9166", "#FF64785A", "#FF3E4B39", "#387C9166",
["#7C9166", "#D4A574", "#C87060", "#D9C27A", "#6FB3A8", "#7FA7C9", "#A38BC9", "#C77FA0", "#8B9D7A", "#B58B6A", "#5C78B8", "#9AA0A6"]);
// Hue ~40 — warm earthy brown/terra
public static readonly AccentPreset Peat = new("peat", "#FF9A7B5C", "#FF7F6449", "#FF4D3C2C", "#389A7B5C");
public static readonly AccentPreset Peat = new("peat", "#FF9A7B5C", "#FF7F6449", "#FF4D3C2C", "#389A7B5C",
["#9A7B5C", "#D9A066", "#C4604F", "#DBBF6A", "#7FA890", "#7C9AB8", "#A98BB8", "#C98A8A", "#A39A6E", "#B8825A", "#6E80AE", "#A09A90"]);
// Hue ~180 — cool teal/sea-green
public static readonly AccentPreset Sea = new("sea", "#FF5B8F8C", "#FF4A7573", "#FF263D3C", "#385B8F8C");
public static readonly AccentPreset Sea = new("sea", "#FF5B8F8C", "#FF4A7573", "#FF263D3C", "#385B8F8C",
["#5B8F8C", "#C9A87C", "#BF7468", "#CBC27F", "#4FA396", "#5FA8C8", "#8E8BCC", "#B87FA5", "#7C9A8E", "#A8907A", "#5C78B8", "#8FA0A8"]);
public static readonly IReadOnlyList<AccentPreset> All = [Moss, Peat, Sea];
public static AccentPreset Default => Moss;
/// <summary>Number of <c>SwatchNBrush</c> tokens every preset must define.</summary>
public const int SwatchCount = 12;
/// <summary>Swatch keys ("Swatch1".."SwatchN") as stored on lists and folders.</summary>
public static readonly string[] SwatchKeys = Enumerable.Range(1, SwatchCount).Select(i => $"Swatch{i}").ToArray();
public static AccentPreset Find(string? name) =>
All.FirstOrDefault(p => p.Name == name) ?? Default;
}
+3
View File
@@ -16,6 +16,9 @@ public sealed class AppSettings
public string DiffViewMode { get; set; } = "unified";
public bool DiffWrapLines { get; set; }
/// <summary>Mission Control overview column count; 0 = auto (derived from the pane count).</summary>
public int MissionControlColumns { get; set; }
private static readonly string DefaultConfigPath = Paths.Expand("~/.claudeDo/ui.config.json");
/// Where this instance persists. Instance-level (not static) so tests can redirect it —
@@ -0,0 +1,63 @@
# Claude CLI
## Session starten
- `claude` — interaktive Session im aktuellen Verzeichnis
- `claude "<prompt>"` — Session mit erstem Prompt starten
- `claude -c` — letzte Session in diesem Verzeichnis fortsetzen
- `claude -r` — Session aus einer Auswahlliste fortsetzen
- `claude -r <session-id>` — bestimmte Session fortsetzen
- `claude --fork-session -r <id>` — Session unter neuer ID fortsetzen
- `claude -n "<name>"` — Anzeigename für die Session setzen
## Modell und Aufwand
- `claude --model sonnet` — Alias oder voller Name (`fable`, `opus`, `sonnet`, `claude-opus-5`)
- `claude --effort high` — Denkaufwand: `low`, `medium`, `high`, `xhigh`, `max`
- `claude --fallback-model sonnet` — Ausweichmodell bei Überlast (nur mit `-p`)
## Nicht interaktiv
- `claude -p "<prompt>"` — antworten und beenden
- `claude -p --output-format json` — ein JSON-Ergebnis
- `claude -p --output-format stream-json` — Streaming-JSON für Skripte
- `claude -p --include-partial-messages` — Teil-Chunks mitstreamen
- `claude -p --max-budget-usd 5` — Kostendeckel für den Lauf
- `claude -p --json-schema '<schema>'` — strukturierte Ausgabe validieren
## Berechtigungen und Tools
- `claude --permission-mode <mode>` — `acceptEdits`, `auto`, `plan`, `manual`, `dontAsk`, `bypassPermissions`
- `claude --allowed-tools "Bash(git *)" Edit` — nur diese Tools erlauben
- `claude --disallowed-tools "Bash(rm *)"` — diese Tools sperren
- `claude --tools "Bash,Edit,Read"` — Built-in-Toolset festlegen
- `claude --add-dir <pfad>` — weiteres Arbeitsverzeichnis freigeben
- `claude --restricted` — ohne befehlsausführende Tools und ohne lokale Settings
## Kontext und Konfiguration
- `claude --append-system-prompt "<text>"` — Systemprompt ergänzen
- `claude --system-prompt "<text>"` — Systemprompt ersetzen
- `claude --settings <datei|json>` — zusätzliche Settings laden
- `claude --agents '<json>'` — eigene Subagents für diese Session
- `claude --mcp-config <datei>` — MCP-Server aus JSON laden
- `claude --strict-mcp-config` — nur diese MCP-Server, alle anderen ignorieren
- `claude --safe-mode` — alle Anpassungen aus, zum Fehlersuchen
- `claude --bare` — Minimalmodus ohne Hooks, Plugins, CLAUDE.md
## Hintergrund-Sessions
- `claude --bg "<prompt>"` — im Hintergrund starten, gibt die Kurz-ID aus
- `claude agents` — laufende Hintergrund-Sessions auflisten
- `claude attach <id>` — Hintergrund-Session in dieses Terminal holen
- `claude logs <id>` — letzte Ausgabe einer Hintergrund-Session
- `claude stop <id>` — Session stoppen, Verlauf bleibt
- `claude rm <id>` — Session löschen
## Worktree
- `claude -w` — neuen Git-Worktree für diese Session anlegen
- `claude -w <name>` — Worktree mit eigenem Namen
## Verwaltung
- `claude doctor` — Installation prüfen
- `claude update` — auf neue Version aktualisieren
- `claude mcp` — MCP-Server konfigurieren
- `claude plugin` — Plugins verwalten
- `claude auth` — Authentifizierung verwalten
- `claude setup-token` — Langzeit-Token einrichten
- `claude -d api,hooks` — Debugmodus mit Kategoriefilter
- `claude -v` — Version ausgeben
@@ -0,0 +1,48 @@
# Claude Konfiguration
## Dateien und Orte
- `~/.claude/settings.json` — globale Settings für alle Projekte
- `.claude/settings.json` — Projekt-Settings, eingecheckt fürs Team
- `.claude/settings.local.json` — persönliche Projekt-Settings, nicht eingecheckt
- `Rangfolge: managed > local > projekt > global` — spezifischer gewinnt
- `/config` — Settings interaktiv in der Session ansehen und ändern
- `claude doctor` — Konfiguration auf Fehler prüfen
## CLAUDE.md
- `~/.claude/CLAUDE.md` — globale Anweisungen für alle Projekte
- `CLAUDE.md` — im Projekt-Root, wird bei Start automatisch geladen
- `<unterordner>/CLAUDE.md` — wird geladen, sobald dort gearbeitet wird
- `@pfad/zur/datei.md` — Import einer weiteren Datei in eine CLAUDE.md
- `# <notiz>` — in der Session: Notiz in die CLAUDE.md übernehmen
- `/init` — CLAUDE.md für ein Projekt generieren lassen
## Berechtigungen
- `"permissions": { "allow": [...], "deny": [...], "ask": [...] }` — Struktur in settings.json
- `Bash(git *)` — Bash-Befehle per Muster erlauben oder sperren
- `Edit(src/**)` — Dateizugriff per Glob einschränken
- `WebFetch(domain:example.com)` — Webzugriff auf eine Domain begrenzen
- `mcp__<server>__<tool>` — Regel für ein einzelnes MCP-Tool
- `/permissions` — Regeln in der Session ansehen und ergänzen
## MCP-Server
- `claude mcp add <name> -- <befehl>` — lokalen stdio-Server anlegen
- `claude mcp add --transport http <name> <url>` — HTTP-Server anlegen
- `claude mcp add --scope project <name> ...` — in `.mcp.json` fürs Team
- `claude mcp list` — konfigurierte Server anzeigen
- `claude mcp remove <name>` — Server entfernen
- `/mcp` — Serverstatus in der Session, OAuth-Login
## Hooks
- `"hooks": { "PreToolUse": [...] }` — Shell-Befehle an Lifecycle-Events hängen
- `PreToolUse / PostToolUse` — vor und nach jedem Tool-Aufruf
- `UserPromptSubmit` — beim Absenden eines Prompts
- `SessionStart / Stop` — bei Sessionbeginn und -ende
- `Exit-Code 2` — blockiert die Aktion, stderr geht an Claude
- `/hooks` — Hooks interaktiv verwalten
## Erweiterungen
- `.claude/commands/<name>.md` — eigener Slash-Command `/name`
- `.claude/skills/<name>/SKILL.md` — Skill, wird bei passender Aufgabe geladen
- `.claude/agents/<name>.md` — eigener Subagent-Typ
- `~/.claude/...` — dieselben Ordner global statt pro Projekt
- `claude plugin` — Plugins installieren und verwalten
@@ -0,0 +1,34 @@
# Conventional Commits
## Format
- `<type>(<scope>): <beschreibung>` — Grundform, Scope optional
- `feat(auth): add login endpoint` — Beispiel mit Scope
- `fix: prevent crash on empty input` — Beispiel ohne Scope
- `imperativ, klein, kein Punkt am Ende` — Stil der Beschreibung
## Types
- `feat` — neues Feature
- `fix` — Bugfix
- `refactor` — Umbau ohne Verhaltensänderung
- `perf` — Performance-Verbesserung
- `docs` — nur Dokumentation
- `test` — Tests hinzufügen oder korrigieren
- `style` — Formatierung, kein Code-Effekt
- `build` — Build-System oder Dependencies
- `ci` — CI-Konfiguration
- `chore` — Sonstiges ohne src- oder Test-Änderung
## Breaking Changes
- `feat(api)!: remove legacy endpoint` — `!` markiert einen Breaking Change
- `BREAKING CHANGE: <erklärung>` — alternativ als Footer im Commit-Body
## Body und Footer
- `Leerzeile nach der Betreffzeile` — dann optionaler Body mit dem Warum
- `Refs: #123` — Footer für Ticket-Referenzen
- `Reverts: <hash>` — Footer für Reverts
## Warum das Ganze
- `feat` — löst ein Minor-Release aus (SemVer)
- `fix` — löst ein Patch-Release aus (SemVer)
- `!` oder `BREAKING CHANGE` — löst ein Major-Release aus
- `git log --oneline --grep "^feat"` — Verlauf wird filterbar
@@ -0,0 +1,49 @@
# dotnet CLI
## Projekt anlegen
- `dotnet new list` — verfügbare Vorlagen anzeigen
- `dotnet new console -n <name>` — Projekt aus Vorlage anlegen
- `dotnet new sln` — Solution anlegen
- `dotnet sln add <projekt>` — Projekt in die Solution aufnehmen
## Bauen
- `dotnet restore` — NuGet-Pakete auflösen
- `dotnet build <projekt> -c Release` — Release bauen
- `dotnet build --no-restore` — ohne Restore, wenn Pakete aktuell sind
- `dotnet build -v minimal` — knappere Ausgabe
- `dotnet clean -c Release` — Build-Ausgabe löschen
## Laufen lassen
- `dotnet run --project <projekt> -c Release` — starten
- `dotnet run --project <projekt> -- <args>` — Argumente an die App
- `dotnet watch --project <projekt> run` — bei Dateiänderung neu starten
## Testen
- `dotnet test <projekt> -c Release --no-build` — Tests ohne Neubau
- `dotnet test --filter "FullyQualifiedName~<name>"` — nur passende Tests
- `dotnet test --filter "FullyQualifiedName!~<name>"` — Tests ausschließen
- `dotnet test --logger "console;verbosity=detailed"` — Details pro Test
- `dotnet test --blame-hang-timeout 60s` — hängende Tests aufspüren
## Pakete
- `dotnet add <projekt> package <name>` — Paket hinzufügen
- `dotnet add <projekt> package <name> --version 1.2.3` — feste Version
- `dotnet remove <projekt> package <name>` — Paket entfernen
- `dotnet list <projekt> package --outdated` — veraltete Pakete
- `dotnet nuget locals all --clear` — NuGet-Caches leeren
## EF Core Migrationen
- `dotnet ef migrations add <name> -p <daten-projekt>` — Migration anlegen
- `dotnet ef migrations list -p <daten-projekt>` — Migrationen anzeigen
- `dotnet ef migrations remove -p <daten-projekt>` — letzte Migration zurücknehmen
- `dotnet ef database update -p <daten-projekt>` — Schema anwenden
## Veröffentlichen
- `dotnet publish <projekt> -c Release -r win-x64 --self-contained` — eigenständiges Build
- `dotnet publish <projekt> -c Release -p:PublishSingleFile=true` — eine Exe
## Diagnose
- `dotnet --info` — SDKs, Runtimes, RID
- `dotnet --list-sdks` — installierte SDKs
- `dotnet format <projekt>` — Formatierung anwenden
- `dotnet build -p:TreatWarningsAsErrors=true` — Warnungen als Fehler
+80
View File
@@ -0,0 +1,80 @@
# Git
## Start
- `git clone <url>` — Repository klonen
- `git init` — neues Repository anlegen
- `git remote add origin <url>` — Remote verbinden
## Stand ansehen
- `git status -sb` — Kurzstatus mit Branch-Zeile
- `git diff` — nicht gestagete Änderungen
- `git diff --staged` — gestagete Änderungen
- `git diff main...HEAD` — alles, was dieser Branch gegenüber main bringt
- `git diff --stat` — nur Datei- und Zeilenzähler
- `git log --oneline --graph --decorate -20` — kompakter Verlauf
- `git log -p -- <pfad>` — Verlauf einer Datei mit Diffs
- `git show <commit>` — ein Commit im Detail
## Branch
- `git switch <branch>` — Branch wechseln
- `git switch -c <name>` — Branch anlegen und wechseln
- `git switch -` — zurück zum vorherigen Branch
- `git branch -vv` — Branches mit Upstream und letztem Commit
- `git branch -d <name>` — gemergten Branch löschen (`-D` erzwingt)
## Committen
- `git add <pfad>` — Änderungen stagen
- `git add -p` — Änderungen hunk-weise stagen
- `git commit -m "<text>"` — gestagete Änderungen committen
- `git commit -- <pfad>` — nur diese Pfade committen
- `git commit --amend --no-edit` — letzten Commit um die Staging-Area erweitern
- `git commit --fixup <commit>` — Fixup für späteres Autosquash
## Remote
- `git fetch --all --prune` — alles holen, gelöschte Remote-Branches aufräumen
- `git pull --rebase` — holen und eigene Commits obendrauf setzen
- `git push -u origin <name>` — pushen und Upstream setzen
- `git push --force-with-lease` — sicheres Force-Push, nie plain `--force`
## Rebase und Merge
- `git rebase main` — Branch auf aktuellen main setzen
- `git rebase -i --autosquash main` — interaktiv aufräumen
- `git rebase --continue` — nach gelöstem Konflikt weiter
- `git rebase --abort` — Rebase abbrechen, Ausgangszustand zurück
- `git merge --no-ff <branch>` — Merge-Commit erzwingen
- `git merge --abort` — laufenden Merge abbrechen
- `git cherry-pick <commit>` — einzelnen Commit übernehmen
## Konflikte
- `git diff --name-only --diff-filter=U` — konfliktbehaftete Dateien
- `git checkout --ours <pfad>` — eigene Seite übernehmen
- `git checkout --theirs <pfad>` — fremde Seite übernehmen
- `git mergetool` — konfiguriertes Merge-Tool starten
## Rückgängig
- `git restore <pfad>` — Datei auf HEAD zurücksetzen, Änderungen weg
- `git restore --staged <pfad>` — unstagen, Änderungen behalten
- `git reset --soft HEAD~1` — letzten Commit auflösen, Änderungen gestaged behalten
- `git reset --hard <commit>` — alles verwerfen, nicht rückholbar
- `git revert <commit>` — Commit durch Gegen-Commit aufheben
- `git reflog` — Referenzverlauf, um verlorene Commits wiederzufinden
## Stash
- `git stash push -m "<text>"` — Änderungen wegpacken
- `git stash push -u` — inklusive unversionierter Dateien
- `git stash list` — Stashes anzeigen
- `git stash pop` — letzten Stash zurückholen und entfernen
- `git stash apply <stash>` — anwenden, aber behalten
## Worktrees
- `git worktree list` — alle Worktrees
- `git worktree add <pfad> <branch>` — Worktree für bestehenden Branch
- `git worktree add -b <name> <pfad>` — Worktree mit neuem Branch
- `git worktree remove <pfad>` — Worktree entfernen
- `git worktree prune` — verwaiste Einträge aufräumen
## Suchen
- `git grep -n "<text>"` — im Arbeitsbaum suchen
- `git log -S"<text>"` — Commits, die den Text einfügen oder entfernen
- `git blame -L 40,60 <pfad>` — Zeilenbereich zuordnen
- `git bisect start` — binäre Suche nach dem Fehler-Commit

Some files were not shown because too many files have changed in this diff Show More