Compare commits
326
Commits
v2.3.1
...
e10634bdc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e10634bdc4 | ||
|
|
549aee097b | ||
|
|
af0f318bf0 | ||
|
|
b06bbc213d | ||
|
|
9437121f3a | ||
|
|
6dff72f27c | ||
|
|
15958b992d | ||
|
|
df9e85a28b | ||
|
|
7eeb8f5086 | ||
|
|
b0c23a2bf6 | ||
|
|
4e44251d54 | ||
|
|
2f3f9387c4 | ||
|
|
51371cf678 | ||
|
|
54b179fb0b | ||
|
|
c6d1fff8b1 | ||
|
|
231b063751 | ||
|
|
1f8f3efc72 | ||
|
|
ac586797ce | ||
|
|
fb29e8a871 | ||
|
|
f7fa8292e0 | ||
|
|
66630d5ce2 | ||
|
|
1e383e1c1f | ||
|
|
3acb1cba8f | ||
|
|
cf80fe3cd6 | ||
|
|
a7a3545e2b | ||
|
|
14e0cffa25 | ||
|
|
861ba12f7f | ||
|
|
c1184adc92 | ||
|
|
e299421f76 | ||
|
|
ffc60c4101 | ||
|
|
c792765ed3 | ||
|
|
e2d987e976 | ||
|
|
eeee9591aa | ||
|
|
59827757f0 | ||
|
|
ac099dd1a8 | ||
|
|
ebbee6005d | ||
|
|
cba7d01c31 | ||
|
|
3cd0f0879e | ||
|
|
f62dbb9239 | ||
|
|
6e9a7cea87 | ||
|
|
8acf9f8d7d | ||
|
|
292d17173e | ||
|
|
482d7ec332 | ||
|
|
315bea7cf9 | ||
|
|
c4e4e0976a | ||
|
|
028ac57398 | ||
|
|
a7ff1b3a2f | ||
|
|
d5730a8405 | ||
|
|
17e6cd6118 | ||
|
|
4a7b00ed53 | ||
|
|
f3e6655fc2 | ||
|
|
bf19e84e76 | ||
|
|
32ef88526e | ||
|
|
30efbcbf2f | ||
|
|
96723f4582 | ||
|
|
967359d6e7 | ||
|
|
ab888e5291 | ||
|
|
7337312eca | ||
|
|
c07225f530 | ||
|
|
ff52fb9eb1 | ||
|
|
109e85da83 | ||
|
|
4a28bfe82e | ||
|
|
27d50b81ff | ||
|
|
166021049a | ||
|
|
a78cc526a6 | ||
|
|
4310f88ebf | ||
|
|
b97f55bfb6 | ||
|
|
bac8387069 | ||
|
|
d1ed29f19f | ||
|
|
8dbdfb3b89 | ||
|
|
ddf68d66cc | ||
|
|
bd83cac57d | ||
|
|
b2940bf1ff | ||
|
|
7f03f04268 | ||
|
|
cc90600f72 | ||
|
|
003e7b2b78 | ||
|
|
c90f93e57f | ||
|
|
4d9ceefee2 | ||
|
|
176ba78e11 | ||
|
|
57c61a2043 | ||
|
|
79f90a9a8e | ||
|
|
efee14780b | ||
|
|
42a234b46e | ||
|
|
774f9d3d13 | ||
|
|
ed7cd7dcf6 | ||
|
|
d84607f796 | ||
|
|
6118698d8c | ||
|
|
1a988ff4fc | ||
|
|
9e2a15e421 | ||
|
|
1649291ef0 | ||
|
|
58741c2bd6 | ||
|
|
7affb4c204 | ||
|
|
0d1e3b9a6f | ||
|
|
aac84e48b7 | ||
|
|
0878bae19e | ||
|
|
20bce9b424 | ||
|
|
5d1d2d89d0 | ||
|
|
0358bcf8b2 | ||
|
|
c6213d77c4 | ||
|
|
e59f6c2438 | ||
|
|
46e177eb59 | ||
|
|
abac5e5150 | ||
|
|
7240b39f16 | ||
|
|
4a60c2bfa8 | ||
|
|
03c4ed4607 | ||
|
|
f3e16412f4 | ||
|
|
db4177db83 | ||
|
|
8bc7bc0c4f | ||
|
|
af16830060 | ||
|
|
b54a133c16 | ||
|
|
8247a749a0 | ||
|
|
f8c48e2ed7 | ||
|
|
3e07536ee9 | ||
|
|
6980aae77e | ||
|
|
75561930d7 | ||
|
|
b66ce580de | ||
|
|
70322c203c | ||
|
|
db1775fb69 | ||
|
|
76dfbc87eb | ||
|
|
7cfe280a23 | ||
|
|
e38fcec8cc | ||
|
|
423ab7f2a9 | ||
|
|
2ad9bdd851 | ||
|
|
46c664a03f | ||
|
|
445242cd7d | ||
|
|
86f962ee7d | ||
|
|
68aa2f5f4b | ||
|
|
76b748060c | ||
|
|
175f160956 | ||
|
|
cfd2936c25 | ||
|
|
3462ca1355 | ||
|
|
e717c901b2 | ||
|
|
0f187d8e82 | ||
|
|
f106c890b3 | ||
|
|
56f7d64f07 | ||
|
|
091aca521f | ||
|
|
730ecb1abc | ||
|
|
b6ecbb13f5 | ||
|
|
b5a8d58e62 | ||
|
|
b6791265d1 | ||
|
|
e113987d2e | ||
|
|
4309c4cb08 | ||
|
|
41c2de5ab0 | ||
|
|
a35103418d | ||
|
|
1b0ecbe361 | ||
|
|
83ca1fc6dc | ||
|
|
09e0772673 | ||
|
|
7e1b1177de | ||
|
|
b3a8373c70 | ||
|
|
e7c9ef891f | ||
|
|
ae6e95d2c0 | ||
|
|
8bb1014e54 | ||
|
|
66d2dae316 | ||
|
|
4988a42620 | ||
|
|
7c4bce63d3 | ||
|
|
ad2d91f658 | ||
|
|
cdfd0614dd | ||
|
|
1d258a3e2c | ||
|
|
23794ed21b | ||
|
|
0ce5bad9c1 | ||
|
|
f143d5fc18 | ||
|
|
487e75c031 | ||
|
|
583c98f2b7 | ||
|
|
adc0eaebb1 | ||
|
|
c25300526e | ||
|
|
49f78d2b8d | ||
|
|
a6af90ff0d | ||
|
|
d1df54cf5c | ||
|
|
be378ae0ea | ||
|
|
23b282573c | ||
|
|
6887b1d08d | ||
|
|
860201017c | ||
|
|
df16989435 | ||
|
|
d43b5fcefc | ||
|
|
29bd1b5069 | ||
|
|
a7d95a000a | ||
|
|
beef57fba2 | ||
|
|
a768bc4163 | ||
|
|
ecba12997a | ||
|
|
bee06bc307 | ||
|
|
4ef01274f9 | ||
|
|
8c251c78b1 | ||
|
|
2975f90f9d | ||
|
|
95eba1d5e7 | ||
|
|
663b09e0a0 | ||
|
|
5cc1ec98c0 | ||
|
|
05be07b28c | ||
|
|
d743a9d0e9 | ||
|
|
45bc324402 | ||
|
|
7fa43b5737 | ||
|
|
30bc12b0ac | ||
|
|
8b26e23d73 | ||
|
|
e88f9d01e6 | ||
|
|
dce42a9c22 | ||
|
|
bdee731376 | ||
|
|
d15aa27707 | ||
|
|
2700c3d817 | ||
|
|
f6cb8250bb | ||
|
|
79a1403834 | ||
|
|
91eb2996c8 | ||
|
|
19003e68b6 | ||
|
|
2a217336e8 | ||
|
|
18fb483788 | ||
|
|
f381ef28fd | ||
|
|
ca184511ae | ||
|
|
fe9a9596bd | ||
|
|
b153869216 | ||
|
|
2ebdadff08 | ||
|
|
b1e4e543dd | ||
|
|
a201d3f43d | ||
|
|
7d3d6d7b54 | ||
|
|
08ac8bf7b1 | ||
|
|
3ea48ff76d | ||
|
|
f02b7d6ee5 | ||
|
|
0e0438d3f2 | ||
|
|
83ea429b8a | ||
|
|
032debc780 | ||
|
|
f822c50102 | ||
|
|
7dcbbf484e | ||
|
|
5872666f31 | ||
|
|
cf9dd1cc94 | ||
|
|
677a4c1853 | ||
|
|
338fc3903d | ||
|
|
57326f1d3b | ||
|
|
8103006e26 | ||
|
|
5115cfc288 | ||
|
|
1e88fefbda | ||
|
|
7661129202 | ||
|
|
7cbd4e66ae | ||
|
|
6e2158d157 | ||
|
|
d4cd202460 | ||
|
|
519ea5a8e0 | ||
|
|
1aaa40b894 | ||
|
|
3e7126b3f2 | ||
|
|
d2ca7fb500 | ||
|
|
10e561f336 | ||
|
|
32c019bd5d | ||
|
|
65db1cdefa | ||
|
|
5ba0b63e03 | ||
|
|
42c70fb28c | ||
|
|
c9ba1e2645 | ||
|
|
394febadeb | ||
|
|
1ee21b560d | ||
|
|
8f8c2a65b2 | ||
|
|
6c5acd09b9 | ||
|
|
a7b88098ab | ||
|
|
ee84a75bd7 | ||
|
|
1cc247a590 | ||
|
|
c871f35513 | ||
|
|
3972ce50a6 | ||
|
|
8ed6e08710 | ||
|
|
194ce58a72 | ||
|
|
b38b0857dd | ||
|
|
334cf1e1d2 | ||
|
|
b126a21c57 | ||
|
|
b1efcdce87 | ||
|
|
e926f4db81 | ||
|
|
20d17c6887 | ||
|
|
bedd2defbb | ||
|
|
8d7ba1e314 | ||
|
|
48de816051 | ||
|
|
cc3013a76e | ||
|
|
eee87f086c | ||
|
|
9804ecefff | ||
|
|
7d29ec0725 | ||
|
|
524836ffe8 | ||
|
|
47e2734357 | ||
|
|
37da35b903 | ||
|
|
6a2a40cd46 | ||
|
|
01cd47ffec | ||
|
|
3a07b319f9 | ||
|
|
c07c1f70a8 | ||
|
|
52c2186999 | ||
|
|
47edab5907 | ||
|
|
87de53e052 | ||
|
|
c82ea2eea1 | ||
|
|
7d636c60dd | ||
|
|
0b6792620c | ||
|
|
fd50a4fb7c | ||
|
|
342a061d94 | ||
|
|
63d8b5c28d | ||
|
|
ab56644ddc | ||
|
|
71050e2634 | ||
|
|
ef7645c6b1 | ||
|
|
92ce7a4a77 | ||
|
|
4dc4fe27e3 | ||
|
|
0ee30bee03 | ||
|
|
b9e0721875 | ||
|
|
92eb654f9b | ||
|
|
724814f770 | ||
|
|
b5464fc533 | ||
|
|
44cdad386c | ||
|
|
58f8b11dbb | ||
|
|
e653677487 | ||
|
|
994e94c2af | ||
|
|
db447f36da | ||
|
|
df2fcd8def | ||
|
|
17ef99bc9b | ||
|
|
c4425d6499 | ||
|
|
be6ccb2c17 | ||
|
|
57d433276e | ||
|
|
785ebe55e4 | ||
|
|
e1807fd53b | ||
|
|
149e2adadb | ||
|
|
d569313598 | ||
|
|
0a3c25840f | ||
|
|
7d6cb2bd3e | ||
|
|
7c8a9dd61b | ||
|
|
3fbbd7ab93 | ||
|
|
24f999facd | ||
|
|
3a648b7d77 | ||
|
|
fde9615b34 | ||
|
|
c93a20f20c | ||
|
|
1466d0fbab | ||
|
|
6c8de4aef3 | ||
|
|
7f9f0ca128 | ||
|
|
edd2774d86 | ||
|
|
62fc5aaa5a | ||
|
|
023e136c53 | ||
|
|
4877802bcd | ||
|
|
40eb979924 | ||
|
|
e3bacc3143 | ||
|
|
cc823ec4f6 | ||
|
|
ec10b06848 | ||
|
|
81cf94187f | ||
|
|
538b4ede09 |
+210
-1
@@ -1,5 +1,214 @@
|
||||
# Changelog
|
||||
|
||||
## v2.8.0 — 2026-08-06
|
||||
|
||||
### Features
|
||||
|
||||
- persist interactive session id so a closed/aborted ConPTY session can be resumed (1a988ff)
|
||||
- repo scan discovers nested repos in subfolders (7affb4c)
|
||||
- warn when the running worker predates the selected repo's merged HEAD (8bc7bc0)
|
||||
- add treatWaitingForChildrenAsBusy to wait_for_task_change (af16830)
|
||||
- warn on near-duplicate titles in add_task/batch_add_tasks (3e07536)
|
||||
- add Diagnose section to SettingsWindow (7e1b117)
|
||||
- raise wait_for_task_change timeout, expose queue slot state (d43b5fc)
|
||||
- add get_effective_run_config MCP tool (a768bc4)
|
||||
- hand off to a fresh session after Phase 2 (4ef0127)
|
||||
- add copy-last-40-lines button to log visualizer (8c251c7)
|
||||
- make max-turns ceiling editable in General tab (2975f90)
|
||||
- add SystemCheckPage with blocking-error gating (5cc1ec9)
|
||||
- pull in preflight check implementations as prerequisite for SystemCheckPage (05be07b)
|
||||
- add Claude CLI preflight checks (found, version, login, auto-mode) (d743a9d)
|
||||
- add Git, GitIdentity, Port, and WriteAccess preflight checks (45bc324)
|
||||
- add check abstraction and EnvironmentCheckService (8b26e23)
|
||||
- clamp max-turns to a configurable ceiling (08ac8bf)
|
||||
- make overview panes and queue strip individually resizable (3ea48ff)
|
||||
- add reading-discipline section to default system prompt (f02b7d6)
|
||||
|
||||
### Fixes
|
||||
|
||||
- honor RunCancellationRegistry.Register's return value at both dispatch sites (109e85d)
|
||||
- treat an immediate ConPTY exit as a start failure (4a28bfe)
|
||||
- surface merge-drain cancel rejection in task-row quick cancel (27d50b8)
|
||||
- close interactive-session gate bypasses in reset-and-retry and plan queueing (1660210)
|
||||
- surface worker-offline failures when deleting a task (a78cc52)
|
||||
- bound the ConPTY launch-gate wait so a hung launch can't freeze other panes (4310f88)
|
||||
- reparse-check repo-scan root folder and move scan off UI thread (b97f55b)
|
||||
- serialize ConPTY env launch and close open-path dedupe races (176ba78)
|
||||
- gate Mission Control submit-for-review, add retry, fix event leak (57c61a2)
|
||||
- block cancelling a task while its unit merge is draining (79f90a9)
|
||||
- filter the transliterated 'fuer' stopword, not the untransliterated 'fur' (efee147)
|
||||
- claim Running before creating run resources (774f9d3)
|
||||
- gate queueing on an open interactive ConPTY session (d84607f)
|
||||
- route details-pane task delete through worker to advance blocked parents (9e2a15e)
|
||||
- advance planning chains and parents on stale-Running recovery (58741c2)
|
||||
- keep ConPTY sessions alive across Mission Control layout rebuilds (aac84e4)
|
||||
- suppress auto-open conflict resolver during MCP-driven merges (5d1d2d8)
|
||||
- release the ShellLink COM object so reading a .lnk doesn't lock it (c6213d7)
|
||||
- surface empty review ranges and blocked children over MCP (e59f6c2)
|
||||
- clean up three review leftovers from the list-handler run (b54a133)
|
||||
- make list_tasks/batch_get_tasks lean by default (f8c48e2)
|
||||
- restore ForegroundHelper.AllowAny call before wt.exe launch (46c664a)
|
||||
- skip rewriting the autostart shortcut when already current (445242c)
|
||||
- remove duplicate list-settings context menu entry (68aa2f5)
|
||||
- read the persisted MCP port on update instead of the wizard default (3462ca1)
|
||||
- show the MCP registration step in the progress list (e717c90)
|
||||
- refresh the run session id live, close the stale handoff pane (f106c89)
|
||||
- stop NumericUpDown from writing null into non-nullable settings (56f7d64)
|
||||
- apply the verify gate to worktree-less approvals and report it everywhere (091aca5)
|
||||
- recover the planning session id from the on-disk transcript (b679126)
|
||||
- make Files-tab system prompts view-only, drop external-editor edit path (6887b1d)
|
||||
- broadcast TaskUpdated on queue-claimed task start (8602010)
|
||||
- remove duplicate Let-Claude entry point and icon collision (df16989)
|
||||
- filter EF Core/ASP.NET Core noise out of log ring buffer (29bd1b5)
|
||||
- clarify worktree commits aren't auto-commits (a7d95a0)
|
||||
- resolve claude CLI shims (.cmd/.bat) not just .exe on PATH (e88f9d0)
|
||||
- stop 429s with an activity-dependent poll cadence + manual refresh (2700c3d)
|
||||
- pass FakeTranscriptUsageReader to TaskRunner in FailureDiagnosisTests (f6cb825)
|
||||
- stop on-disk prompt overrides from freezing forever (b153869)
|
||||
- surface the real reason a Claude run failed instead of a generic exit-code message (2ebdadf)
|
||||
- record real raw token usage per run, not the uncached remainder (7d3d6d7)
|
||||
|
||||
### Performance
|
||||
|
||||
- stop echoing task description from writing MCP tools (ecba129)
|
||||
|
||||
### Documentation
|
||||
|
||||
- document the ConPTY env race and open-path dedupe fixes (4d9ceef)
|
||||
- reflect claim-before-create ordering in worker-task-pipeline (42a234b)
|
||||
- document the queueing gate on open ConPTY sessions (ed7cd7d)
|
||||
- document interactive session id persistence and resume precedence (6118698)
|
||||
- document stale-Running chain/parent advance fix (1649291)
|
||||
- document the kill-on-detach gotcha and ConPtyPaneHost reparenting (0d1e3b9)
|
||||
- point both drift checks at the merged state (0878bae)
|
||||
- point external-mcp drift check at the merged state (03c4ed4)
|
||||
- evaluate planning-chain fork-base options, recommend fork-from-predecessor (db4177d)
|
||||
- record the verify-gate reach and the NumericUpDown null trap (0f187d8)
|
||||
- mark the usage-monitor pass done and drop the light-theme checks (730ecb1)
|
||||
- record the findings from the 2026-08-06 visual pass (b6ecbb1)
|
||||
- re-verify the 2026-07-24 findings and drop the fixed ones (b5a8d58)
|
||||
- drop the verified 2026-07-27 visual-pass block, keep its design notes (e113987)
|
||||
- mark the MaxTurnsCeiling editor as shipped (4309c4c)
|
||||
- drop the stale reserved-slot comment now that Claude Help Me shipped (41c2de5)
|
||||
- document Environment Checks + note unmerged prerequisite gap (a6af90f)
|
||||
- break growing enumerations into one entry per line (beef57f)
|
||||
- root-cause --permission-mode auto + CLI/.NET preflight research (dce42a9)
|
||||
- rename ConPTY session UI text to interactive session (b1e4e54)
|
||||
- update for v2.7.0 (0e0438d)
|
||||
|
||||
## v2.7.0 — 2026-08-05
|
||||
|
||||
### Features
|
||||
|
||||
- add usage monitor modal with gauges and model/task usage analysis (8103006)
|
||||
- add usage pill to footer and mission control header (7cbd4e6)
|
||||
- expose usage/model-usage hub surface and persist run model (d4cd202)
|
||||
- add roadblock reply field to the ROADBLOCK card (d2ca7fb)
|
||||
- record merge commit SHA and add revert_merge tool (10e561f)
|
||||
- let CreateChildTask set maxTurns on child tasks (65db1cd)
|
||||
- add post-merge verification gate for list merges (c9ba1e2)
|
||||
- add preview_merge and preview_merge_set MCP tools (394feba)
|
||||
- add splitting, turns-preflight, merge-order rules to list-handler prompt (8ed6e08)
|
||||
- add wait_for_task_change MCP tool (194ce58)
|
||||
- render task descriptions into the list-handler brief (b38b085)
|
||||
- add TranscriptUsageReader for per-model token usage (e926f4d)
|
||||
- add OAuth usage client + poller (20d17c6)
|
||||
- add usage gate thresholds and task run model column (bedd2de)
|
||||
- give the list handler its own review task (c07c1f7)
|
||||
- clear focus from textboxes on Escape in the main window (52c2186)
|
||||
- show newest log entries first in Log Visualizer (47edab5)
|
||||
|
||||
### Fixes
|
||||
|
||||
- install a real localizer in UsagePillViewModelTests (677a4c1)
|
||||
- avoid int CommandParameter cast crash in usage monitor presets (57326f1)
|
||||
- transport ConPTY task brief via file, not CLI argument (3972ce5)
|
||||
- drop notification for the removed CanPickUpInTerminal property (eee87f0)
|
||||
- normalize model alias before preset lookup, wire DefaultMaxTurns as fallback (7d636c6)
|
||||
- only ask list-handler dedupe questions when a candidate exists (fd50a4f)
|
||||
|
||||
### Refactoring
|
||||
|
||||
- remove pick-up-in-terminal, keep ConPTY as the single session entry (87de53e)
|
||||
|
||||
### Documentation
|
||||
|
||||
- mark the 2026-08-05 list-handler run complete (032debc)
|
||||
- document usage monitor & gate across CLAUDE.md files (cf9dd1c)
|
||||
- update for v2.6.0 (342a061)
|
||||
|
||||
## v2.6.0 — 2026-08-04
|
||||
|
||||
### Fixes
|
||||
|
||||
- unbreak the update path and cache the download (63d8b5c)
|
||||
- clear task selection when switching lists (ab56644)
|
||||
|
||||
### Documentation
|
||||
|
||||
- update for v2.5.0 (71050e2)
|
||||
|
||||
## v2.5.0 — 2026-07-29
|
||||
|
||||
### Features
|
||||
|
||||
- move a task to another list via drag & drop (92ce7a4)
|
||||
- sidebar shows queued+running, no auto-monitor seeding (44cdad3)
|
||||
- expose maxParallelExecutions in get_app_settings (e653677)
|
||||
- accent color presets in Settings → General (149e2ad)
|
||||
- allow Done via update_task_status with worktree guard (d569313)
|
||||
- add Let Claude handle it broom button to tasks header (7d6cb2b)
|
||||
|
||||
### Fixes
|
||||
|
||||
- stop swallowing mutating hub call failures (b5464fc)
|
||||
- pin LogRingBuffer clock in Does_not_throw_when_detached (58f8b11)
|
||||
- propagate HubException from ApproveReviewAsync so blocked merges surface errors (e1807fd)
|
||||
|
||||
### Documentation
|
||||
|
||||
- update for v2.4.0 (3fbbd7a)
|
||||
|
||||
## v2.4.0 — 2026-07-27
|
||||
|
||||
### Features
|
||||
|
||||
- mark tasks and lists as manual (3a648b7)
|
||||
- per-model effort and turn presets (fde9615)
|
||||
- Interactive chip for tasks with an open ConPTY session (c93a20f)
|
||||
- spinners for ConPTY session start and task refine (1466d0f)
|
||||
- five-phase list-handler prompt with dedupe and enhance (e3bacc3)
|
||||
- allow update_task_status to set Cancelled (cc823ec)
|
||||
|
||||
### Fixes
|
||||
|
||||
- bind search focus to Ctrl+K instead of OemQuestion (7f9f0ca)
|
||||
- persist title edits from the detail pane (edd2774)
|
||||
|
||||
### Refactoring
|
||||
|
||||
- make the list-handler launch spec single-list and single-repo (4877802)
|
||||
- scope "Let Claude handle it" to a single list (40eb979)
|
||||
|
||||
### Documentation
|
||||
|
||||
- record the manual-task, effort-preset and chip/spinner changes (24f999f)
|
||||
- document the list-handler selection modal (62fc5aa)
|
||||
- describe the list-scoped five-phase handler (023e136)
|
||||
- implementation plan for the per-list task handler (ec10b06)
|
||||
- per-list task handler with read/dedupe/enhance/run/merge (81cf941)
|
||||
- update for v2.3.1 (538b4ed)
|
||||
|
||||
## v2.3.1 — 2026-07-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- retry stashing app/worker through transient file locks on update (9591841)
|
||||
|
||||
### Documentation
|
||||
|
||||
- update for v2.3.0 (9349386)
|
||||
|
||||
## v2.3.0 — 2026-07-24
|
||||
|
||||
### Features
|
||||
@@ -469,7 +678,7 @@
|
||||
- initialize Localizer at app startup from config/OS culture (f529a5f)
|
||||
- add Language preference and Save() to AppSettings (6a85d82)
|
||||
- add Avalonia loc:Tr markup extension and LocalizedString (35ad171)
|
||||
- seed en.json and wire locale copy to app output (3c40bb5)
|
||||
- seed en.json and wire locale copy to app output (3c40bb5e)
|
||||
- add CultureResolver for OS-culture mapping (d95d55e)
|
||||
- add Localizer with fallback chain and change event (d22b50e)
|
||||
- add LocaleStore folder discovery (a83a0c4)
|
||||
|
||||
@@ -11,10 +11,13 @@ Two-process system communicating over SignalR (`127.0.0.1:47821`):
|
||||
- **ClaudeDo.Data** — SQLite data layer, repositories, models, GitService
|
||||
- **ClaudeDo.Worker** — ASP.NET Core hosted service, task queue, Claude CLI runner
|
||||
- **ClaudeDo.Localization** — `locales/en.json` + `locales/de.json` and the lookup service
|
||||
- **ClaudeDo.Releases** — Gitea release client (`IReleaseClient`), used by the Ui update check and the Installer
|
||||
- **ClaudeDo.Installer** — WPF (`UseWPF`) setup app; install/update/uninstall step pipeline
|
||||
- **tests/** — six xUnit projects (Worker, Data, Ui, Localization, Installer, Releases); Worker.Tests run real SQLite and real git
|
||||
|
||||
Each project has its own `CLAUDE.md` — those are the living per-project docs.
|
||||
Per-project `CLAUDE.md` files exist for **App, Data, Installer, Ui, Worker, and Worker.Tests** —
|
||||
those are the living per-project docs. Localization, Releases, and the other five test projects
|
||||
have none; this file plus the code is all there is for them.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -39,7 +42,7 @@ Each project has its own `CLAUDE.md` — those are the living per-project docs.
|
||||
- EF Core migrations manage schema (Migrations/ folder in ClaudeDo.Data)
|
||||
- `IDbContextFactory<ClaudeDoDbContext>` used by singleton consumers (e.g. Worker)
|
||||
- Entity configuration via `IEntityTypeConfiguration<T>` in Configuration/ folder
|
||||
- Task status flow: Idle | Queued -> Running -> WaitingForReview -> Done | Failed | Cancelled. A task that spawns/has children passes through WaitingForChildren first, then surfaces for review once every child is terminal — this is the single parent model for both planning and improvement parents (planning/improvement *children* themselves go straight to Done, only the parent is reviewed). From review you can approve, reject-rerun (Queued, resumes the session with feedback), reject-park (Idle), or cancel. Approve is the single review+merge action: a childless task merges its own worktree then Done (conflicts keep it in WaitingForReview); a task with children drives the unit merge (parent worktree if any + each Done child in order, with conflict continue/abort). Tasks with no active worktree (sandbox run) approve straight to Done. In the detail pane, **Approve & Merge is gated behind opening the diff**: when there is something to inspect (worktree diff / merged range / children combined diff) the button stays disabled until the diff or combined-diff viewer has been opened once, and re-locks per run (any state change resets it); tasks with nothing to inspect are never gated. The row-level quick-approve in the task list is an intentional bypass.
|
||||
- Task status flow: `Idle | Queued -> Running -> WaitingForReview -> Done | Failed | Cancelled`; a task with children passes through `WaitingForChildren` first. **Approve is the single review+merge action** (no separate "Merge all"), and in the detail pane it's gated behind opening the diff. Full transition table → `src/ClaudeDo.Worker/CLAUDE.md`; merge/review/gate mechanics → `docs/explore-notes/review-merge.md`.
|
||||
- Worktree state flow: Active -> Merged | Discarded | Kept
|
||||
- The queue picker claims tasks by `Status=Queued` (with `BlockedByTaskId IS NULL`); the legacy tag system was removed
|
||||
- Interfaces live in an `Interfaces/` subfolder beside their consumers (namespace unchanged)
|
||||
@@ -84,4 +87,4 @@ dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
- `docs/improvement-plan.md` — improvement snapshot from 2026-04-13 (historical)
|
||||
- `docs/prompts-inventory.md`, `docs/mailbox-proposal.md` — reference material (mailbox integration is parked)
|
||||
- `CHANGELOG.md` — Keep a Changelog format, maintained on release
|
||||
- `docs/explore-notes/` — distilled maps of complex subsystems from deep exploration (detail too fine for a CLAUDE.md). **Before** deep-exploring a subsystem, check for a matching note first; **after** a deep explore, distill durable findings back and bump its "verified against" commit. Always verify against current code before trusting. See `docs/explore-notes/README.md`.
|
||||
- `docs/explore-notes/` — distilled maps of complex subsystems (detail too fine for a CLAUDE.md, read on demand). **Before** deep-exploring a subsystem, check for a matching note first; **after** a deep explore, distill durable findings back and bump its "verified against" commit. Always verify against current code before trusting. See `docs/explore-notes/README.md`. Current notes: `worker-task-pipeline`, `usage-monitoring`, `external-mcp`, `review-merge`, `conpty-sessions`, `installer-preflight`.
|
||||
|
||||
@@ -1,106 +1,429 @@
|
||||
# ClaudeDo
|
||||
|
||||
A desktop task management app that executes tasks autonomously via [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) in isolated git worktrees.
|
||||
A Windows desktop app that turns your to-do list into a work queue for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
||||
|
||||
Queue up coding tasks, and ClaudeDo picks them up one by one — each running in its own worktree so your main branch stays clean.
|
||||
Write down what you want done. ClaudeDo picks the task up, runs Claude in an isolated git
|
||||
worktree, and hands you back a diff to review. Your main branch is never touched until you
|
||||
approve.
|
||||
|
||||
## Architecture
|
||||
It looks and feels like a normal task app — lists, My Day, stars, due dates — except every
|
||||
task can also be *executed*.
|
||||
|
||||
Two-process system communicating over SignalR:
|
||||
---
|
||||
|
||||
| Project | Role |
|
||||
|---|---|
|
||||
| **ClaudeDo.App** | Avalonia desktop entry point, DI container setup |
|
||||
| **ClaudeDo.Ui** | Views, ViewModels, SignalR client (MVVM) |
|
||||
| **ClaudeDo.Data** | SQLite data layer, repositories, models, GitService |
|
||||
| **ClaudeDo.Worker** | ASP.NET Core hosted service, task queue, Claude CLI runner |
|
||||
## Contents
|
||||
|
||||
```
|
||||
┌────────────────┐ SignalR ┌────────────────┐
|
||||
│ ClaudeDo.App │◄───────────►│ ClaudeDo.Worker │
|
||||
│ (Avalonia) │ 127.0.0.1 │ (ASP.NET Core) │
|
||||
│ │ :47821 │ │
|
||||
│ ┌────────────┐│ │ ┌────────────┐ │
|
||||
│ │ Ui ││ │ │ TaskQueue │ │
|
||||
│ │(ViewModels)││ │ │ Claude CLI │ │
|
||||
│ └────────────┘│ │ └────────────┘ │
|
||||
└───────┬────────┘ └───────┬────────┘
|
||||
│ │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ ClaudeDo.Data │
|
||||
│ (SQLite) │
|
||||
└───────────────┘
|
||||
```
|
||||
- [Is this for you?](#is-this-for-you)
|
||||
- [Requirements](#requirements)
|
||||
- [Install](#install)
|
||||
- [The window](#the-window)
|
||||
- [The core loop](#the-core-loop)
|
||||
- [Lists and repositories](#lists-and-repositories)
|
||||
- [Writing a task](#writing-a-task)
|
||||
- [Ways to run a task](#ways-to-run-a-task)
|
||||
- [Watching work happen](#watching-work-happen)
|
||||
- [Reviewing and merging](#reviewing-and-merging)
|
||||
- [Resolving conflicts](#resolving-conflicts)
|
||||
- [Worktrees](#worktrees)
|
||||
- [Staying inside your usage limits](#staying-inside-your-usage-limits)
|
||||
- [Your daily rhythm](#your-daily-rhythm)
|
||||
- [Settings](#settings)
|
||||
- [Letting Claude drive ClaudeDo](#letting-claude-drive-claudedo)
|
||||
- [Updates](#updates)
|
||||
- [Where your data lives](#where-your-data-lives)
|
||||
- [Keyboard shortcuts](#keyboard-shortcuts)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [For developers](#for-developers)
|
||||
|
||||
## Tech Stack
|
||||
---
|
||||
|
||||
- .NET 8.0
|
||||
- Avalonia 12.0.0 (Fluent theme)
|
||||
- SQLite (WAL mode) via Entity Framework Core (EF Core + Migrations)
|
||||
- SignalR for real-time IPC between UI and Worker
|
||||
- CommunityToolkit.Mvvm for source-generated MVVM
|
||||
- Git worktrees for task isolation
|
||||
## Is this for you?
|
||||
|
||||
## Prerequisites
|
||||
ClaudeDo is built for one person running many small-to-medium coding jobs across several
|
||||
repositories, mostly unattended.
|
||||
|
||||
- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
||||
It fits when you:
|
||||
|
||||
- have a backlog of contained changes ("rename this", "add that endpoint", "fix this bug")
|
||||
- want them worked on while you do something else
|
||||
- still want to read every diff before it lands on `main`
|
||||
|
||||
It is *not* a CI system, not a team tool, and not a chat window. There are no pull
|
||||
requests, no reviewers but you, and no cloud component (unless you deliberately turn on the
|
||||
optional online inbox).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10/11
|
||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), installed and signed in
|
||||
(ClaudeDo runs `claude` as *you* — it never handles your credentials)
|
||||
- Git
|
||||
- [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) — the installer
|
||||
itself needs it, so install it first if the installer refuses to start
|
||||
|
||||
## Getting Started
|
||||
## Install
|
||||
|
||||
Run `ClaudeDo.Installer.exe`. It downloads the current release, writes its config, and
|
||||
starts the background worker.
|
||||
|
||||
The wizard asks for:
|
||||
|
||||
| Page | What it decides |
|
||||
|---|---|
|
||||
| **Welcome** | Install folder, and whether Claude may manage your tasks via MCP (see [below](#letting-claude-drive-claudedo)) |
|
||||
| **Data paths** | Where the database, logs, sandboxes and worktrees live |
|
||||
| **Worker** | Port, path to the `claude` binary, and whether the worker starts at logon |
|
||||
|
||||
Re-running the installer later gives you **Update**, **Repair** and **Uninstall** instead
|
||||
of the full wizard. Uninstall asks separately before deleting your tasks and settings.
|
||||
|
||||
ClaudeDo has two parts: the window you see, and a background **worker** that does the
|
||||
actual running. The worker starts at logon and keeps going even when the window is closed —
|
||||
so a long task finishes whether you are watching or not.
|
||||
|
||||
## The window
|
||||
|
||||
Three panes ("islands"), plus a footer:
|
||||
|
||||
```
|
||||
┌──────────────┬────────────────────────────┬───────────────────────────┐
|
||||
│ LISTS │ TASKS │ DETAILS │
|
||||
│ │ │ │
|
||||
│ My Day │ ▸ Add a task… ENTER │ Fix login redirect │
|
||||
│ Important │ │ ───────────────────── │
|
||||
│ Planned │ OVERDUE │ Steps ▢ ▢ ▣ │
|
||||
│ │ ● Fix login redirect │ Details (markdown) │
|
||||
│ Queue 3 │ │ Files (drop here) │
|
||||
│ Running 1 │ TASKS │ │
|
||||
│ Review 2 │ ● Add CSV export RUNNING │ Worktree · Diff · Merge │
|
||||
│ │ ● Bump deps QUEUED │ │
|
||||
│ MY LISTS │ ● Write changelog │ Output │ Git │ Session │
|
||||
│ LagerApp 7 │ ● Call the tax guy MANUAL │ ┌─────────────────────┐ │
|
||||
│ LogX 2 │ │ │ live Claude output │ │
|
||||
│ ClaudeDo 12 │ │ └─────────────────────┘ │
|
||||
├──────────────┴────────────────────────────┴───────────────────────────┤
|
||||
│ ● Online 5h 41% · 7d 22% worker log line… logs │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Lists** (left) — smart lists, virtual work lists, and your own lists (one per repo)
|
||||
- **Tasks** (middle) — the selected list, grouped into Overdue / Tasks / Completed
|
||||
- **Details** (right) — everything about the selected task, including its live output
|
||||
- **Footer** — worker connection, usage pill, and the latest worker log line (clickable)
|
||||
|
||||
On a narrow window the side panes fold away automatically.
|
||||
|
||||
The grid icon in the title bar opens **Mission Control**, a separate window for watching
|
||||
several running sessions at once.
|
||||
|
||||
## The core loop
|
||||
|
||||
```
|
||||
write it down → queue it → Claude runs it → you review → merge
|
||||
(Idle) (Queued) (Running) (Waiting for (Done)
|
||||
Review)
|
||||
```
|
||||
|
||||
1. **Capture.** Type into the add box. That is it — a task starts out as a plain reminder.
|
||||
2. **Queue.** Right-click → *Send to queue*. The worker claims queued tasks in order, as
|
||||
many at a time as *Max parallel executions* allows.
|
||||
3. **Run.** ClaudeDo creates a git worktree off your repo (branch `claudedo/<id>`), runs
|
||||
Claude there with your task as the prompt, and streams the output into the detail pane.
|
||||
4. **Review.** On success the task lands in **Waiting for Review** with a committed
|
||||
worktree and a diff. Nothing has touched your working copy.
|
||||
5. **Merge.** *Approve & merge* merges the branch into the target you pick, then marks the
|
||||
task Done. Or reject it with feedback, park it, or cancel it.
|
||||
|
||||
A task that fails is marked **Failed** and keeps its worktree and its log, so you can look
|
||||
at what happened and either continue the same session or reset and retry from scratch.
|
||||
|
||||
## Lists and repositories
|
||||
|
||||
A list becomes *runnable* by giving it a **working directory** — a git repo. Tasks in that
|
||||
list get worktrees off that repo; tasks in a list without a working directory can still be
|
||||
run, but in a scratch sandbox with no git.
|
||||
|
||||
- **Add repos as lists** (`Repositories` menu, or the folder icon) scans folders you point
|
||||
it at and creates one list per git repo it finds. This is the fastest way to get started.
|
||||
- **List settings** (right-click a list) sets the name, working directory, default commit
|
||||
type, the agent defaults for that list, and an optional verify command.
|
||||
- **Manual list** — mark a list as reminders-only. New tasks in it start out manual, so
|
||||
automation never touches them. Good for a "Errands" or "Phone calls" list.
|
||||
- Drag a task onto another list to move it. ClaudeDo refuses to move a running task and
|
||||
warns you when the two lists point at different repos.
|
||||
|
||||
**Smart lists** are always there:
|
||||
|
||||
| List | Contains |
|
||||
|---|---|
|
||||
| **My Day** | What you (or the daily prep) picked for today, plus a pinned Notes row |
|
||||
| **Important** | Starred tasks |
|
||||
| **Planned** | Everything with a date |
|
||||
| **Queue / Running / Review** | Live work — waiting, in flight, and awaiting your review |
|
||||
|
||||
## Writing a task
|
||||
|
||||
The detail pane is where a one-line reminder becomes a brief Claude can act on:
|
||||
|
||||
- **Title + Details** — markdown, with an edit/preview toggle. This is the prompt.
|
||||
- **Steps** — a checklist. Handy for you, and included when you copy the task out.
|
||||
- **Files** — drag files onto the pane (or use *Add file…*) to attach reference material.
|
||||
Attachments are handed to Claude as read-only paths, not pasted into the prompt.
|
||||
- **Star / Schedule / Add to My Day** — the usual task-app affordances.
|
||||
- **Agent settings** — per-task overrides for model, turn budget, extra system prompt,
|
||||
agent file and skills. Anything you do not override shows an *inherited* badge pointing at
|
||||
the list or global default.
|
||||
- **Refine** — hand the task to Claude to sharpen it: it rewrites the title and details into
|
||||
a proper brief. Useful when you captured something in three words.
|
||||
- **Mark as manual** — a MANUAL badge; the queue, the daily prep and every automation skip
|
||||
it. Use it for things only you can do.
|
||||
|
||||
## Ways to run a task
|
||||
|
||||
| Way | What it does | Use it when |
|
||||
|---|---|---|
|
||||
| **Send to queue** | Worker picks it up in order | The normal path |
|
||||
| **Run now** | Skips the queue, starts immediately | You want this one first |
|
||||
| **Continue** | Resumes the last session with a follow-up | "Almost right, now also…" |
|
||||
| **Reset & retry** | Throws the worktree away, re-queues from scratch | The run went sideways |
|
||||
| **Open ConPTY session** | Opens the *real* Claude terminal for this task in Mission Control | You want to drive it yourself, with the task as the starting brief |
|
||||
| **Open planning session** | An interactive session whose job is to break the task into subtasks | The work is too big for one run |
|
||||
| **Let Claude handle it** | Hands a whole list over in one go | You have a pile of small tasks |
|
||||
|
||||
### Planning sessions
|
||||
|
||||
A planning session is a conversation whose output is *structure*, not code. Claude creates
|
||||
child tasks under the parent. You then:
|
||||
|
||||
1. **Finalize plan** — children are chained (each waits for the previous one) but stay Idle.
|
||||
2. **Queue plan** — when you are happy with the list, this queues them all.
|
||||
3. The parent waits in **Waiting for Subtasks** until every child is terminal, then surfaces
|
||||
for review once — you review and merge the *whole unit*, not each child.
|
||||
|
||||
A parked planning session is remembered; the next launch offers to resume, finalize or
|
||||
discard it.
|
||||
|
||||
### "Let Claude handle it"
|
||||
|
||||
Right-click a list → *Let Claude handle it*. You get a checkbox list of that list's open
|
||||
tasks. Confirm, and one Claude session works the selection end to end: it reads them,
|
||||
de-duplicates overlaps, enhances thin descriptions, queues the work, and merges the results.
|
||||
It shows up as its own MANUAL task so you can see what a given run covered, and you review
|
||||
the combined diff when it is finished.
|
||||
|
||||
## Watching work happen
|
||||
|
||||
- **Detail pane, three tabs** — *Output* (live Claude stream), *Git* (worktree, diff,
|
||||
merge), *Session* (result, token/turn counts, subtask outcomes).
|
||||
- **Mission Control** — one tile per running or interactive session, in Focus or Overview
|
||||
mode. ConPTY tiles are the real Claude TUI, embedded: keyboard, colors, `/` commands, all
|
||||
of it. *New session* opens an ad-hoc one that belongs to no task.
|
||||
- **Roadblocks** — when a run gets stuck it reports a roadblock instead of silently failing.
|
||||
The Session tab shows it as its own card with a reply box: answer the question and the
|
||||
same session picks up where it stopped.
|
||||
- **Claude is asking** — an interactive session can put a question in front of you mid-run;
|
||||
answer it inline in Mission Control and the run continues.
|
||||
- **Footer log strip** — the worker's latest event. Failures of your own actions flash here
|
||||
too, instead of vanishing. Click it for the last 30 minutes of worker logs, with a
|
||||
warnings-and-errors filter.
|
||||
|
||||
## Reviewing and merging
|
||||
|
||||
When a task reaches **Waiting for Review** you get four actions:
|
||||
|
||||
| Action | Result |
|
||||
|---|---|
|
||||
| **Approve & merge** | Merges the work into the target branch, then Done |
|
||||
| **Reject** | Asks for feedback and re-runs the same session with it |
|
||||
| **Park** | Back to Idle so you can rewrite the task yourself |
|
||||
| **Cancel** | Done with it; the worktree stays until you clean it up |
|
||||
|
||||
**You have to look before you approve.** When there is a diff to inspect, *Approve & merge*
|
||||
stays disabled until you have opened the diff viewer once. It re-locks after any new run.
|
||||
(The quick-approve on the task row itself is a deliberate bypass for when you already know.)
|
||||
|
||||
The diff viewer shows a file tree on the left and the diff on the right, and can show a
|
||||
dirty worktree, a branch against its base, or a commit range. For a parent with children,
|
||||
*Review combined diff* shows per-subtask diffs plus a combined preview of the whole unit.
|
||||
|
||||
**Verify command** (optional, per list) — a command that must exit 0 after a merge lands
|
||||
before the task is allowed to reach Done. Typically a build or a test run. If it fails, the
|
||||
merge stays (nothing is rewritten behind your back) but the task is held out of Done and the
|
||||
failure output is reported.
|
||||
|
||||
## Resolving conflicts
|
||||
|
||||
If a merge conflicts, ClaudeDo opens a three-pane merge editor in-app:
|
||||
|
||||
```
|
||||
┌──────────────────┬──────────────────┬──────────────────┐
|
||||
│ MAIN │ RESULT │ INCOMING │
|
||||
│ merge target │ (editable) │ task branch │
|
||||
│ │ ▐ │ │
|
||||
│ › │ ← you build │ ‹ │
|
||||
│ │ the result │ │
|
||||
└──────────────────┴──────────────────┴──────────────────┘
|
||||
```
|
||||
|
||||
- Whole files, syntax-highlighted, scrolling in sync
|
||||
- Each conflict starts *empty*. The gutter arrows toggle each side in or out — take main,
|
||||
incoming, both (in the order you click), or neither
|
||||
- Only conflict regions are editable in the middle pane; type your own resolution if
|
||||
neither side is right
|
||||
- `F8` / `Shift+F8` jump between conflicts; the ruler beside the result pane maps every
|
||||
conflict in the file
|
||||
- A counter tells you how many files still need attention. *Resolve & continue* is disabled
|
||||
until they are all done, and *Abort merge* always leaves the target branch as it was
|
||||
|
||||
Binary conflicts cannot be resolved here — ClaudeDo says so and lists the paths.
|
||||
|
||||
## Worktrees
|
||||
|
||||
Every run gets its own worktree, so parallel tasks never fight over your checkout.
|
||||
|
||||
The **Worktrees** overview (per list or global) lists them with state, diff size, age and
|
||||
outcome. From there you can show a diff, jump to the task, merge, batch-merge a selection
|
||||
into one target, mark one *Kept* or *Discarded*, or force-remove a leftover. Worktrees whose
|
||||
directory is gone from disk are flagged as *phantom*.
|
||||
|
||||
Finished worktrees are auto-cleaned after a number of days you choose in Settings.
|
||||
|
||||
## Staying inside your usage limits
|
||||
|
||||
The footer pill shows your Claude usage: `5h 41% · 7d 22%`. Click it for the **Usage
|
||||
Monitor**:
|
||||
|
||||
- Gauges for the 5-hour session window and the 7-day windows, each with a reset countdown
|
||||
- **Models** — token usage per model over a range you pick, split into ClaudeDo's own
|
||||
consumption and everything else
|
||||
- **Tasks** — your biggest consumers, with run counts and totals
|
||||
|
||||
And the part that matters when you are asleep: the **usage limit stop**. Set a percentage
|
||||
per window in Settings, and once usage reaches it the queue simply stops picking up new
|
||||
tasks. Running tasks finish normally, and the queue resumes by itself as the window rolls
|
||||
over. `0` turns a stop off. Manual actions — *Run now*, *Continue*, interactive and planning
|
||||
sessions — are never blocked.
|
||||
|
||||
## Your daily rhythm
|
||||
|
||||
- **My Day** — the day's shortlist. *Clear day* empties it.
|
||||
- **Prime Claude** (Settings → Prime Claude) — schedules for the days and times you pick.
|
||||
At that time ClaudeDo runs a short prep session: Claude looks at your idle tasks, picks an
|
||||
effort-aware subset up to your daily cap, and fills My Day. It also warms your usage
|
||||
window for the day. Only runs while ClaudeDo is open; if you start the app within 30
|
||||
minutes of a scheduled time it fires right away. *Plan day* runs it on demand.
|
||||
- **Notes** — a pinned row in My Day: dated bullet notes with a day navigator, for the
|
||||
thoughts that are not tasks.
|
||||
- **Weekly report** (`Help` menu) — pick a range (defaults to "since your standup weekday")
|
||||
and Claude writes up what actually happened, from your task and session history. Paths you
|
||||
do not want in reports can be excluded in Settings.
|
||||
|
||||
## Settings
|
||||
|
||||
Most settings exist at three levels — **global → list → task** — and the more specific one
|
||||
wins. Overridden fields carry an *override* badge with a one-click reset; inherited ones say
|
||||
where they come from.
|
||||
|
||||
**General**
|
||||
- Default instructions applied to every task
|
||||
- Default model, and a **per-model table** of reasoning effort and turn budget — so `haiku`
|
||||
can be cheap and short while `opus` gets room to think
|
||||
- Permission mode for autonomous runs
|
||||
- Max parallel executions
|
||||
- Usage limit stops (see above)
|
||||
- Session skills applied to every task
|
||||
- Report exclusions, standup weekday
|
||||
- Accent color (Moss / Peat / Sea) and language (English / German)
|
||||
|
||||
**Worktrees** — sibling or central placement, auto-cleanup age, and the manual cleanup and
|
||||
force-remove-all buttons.
|
||||
|
||||
**Files** — open the prompt templates (system, planning, retry, daily prep, weekly report)
|
||||
in your editor, and restore the bundled default agent files.
|
||||
|
||||
**Skills** — install a skill from a git URL, update it, remove it. Skills selected globally,
|
||||
per list and per task combine.
|
||||
|
||||
**Prime Claude** — the schedules and the daily task cap.
|
||||
|
||||
**Online Inbox** (optional, off by default) — mirrors your idle backlog to a server so you
|
||||
can capture tasks from your phone. Disabled means zero network traffic. Enabling it needs a
|
||||
URL plus a sign-in; the refresh token is stored encrypted on your machine, never in a config
|
||||
file.
|
||||
|
||||
## Letting Claude drive ClaudeDo
|
||||
|
||||
If you allowed it during install, ClaudeDo registers itself as an MCP server with the Claude
|
||||
CLI. Any Claude session on your machine can then read and manage your tasks: list and create
|
||||
tasks, add subtasks, queue and cancel runs, read logs and diffs, merge, review, manage lists
|
||||
and per-list config.
|
||||
|
||||
Concretely, you can sit in a normal Claude Code session and say "put these five things in
|
||||
the ClaudeDo backlog for the LagerApp list", or "check whether the CSV export task is done".
|
||||
|
||||
The MCP surface is deliberately narrow on the dangerous end: it can start and observe work,
|
||||
but it cannot rewrite your app settings, and a task with an active worktree cannot be forced
|
||||
to Done behind your back.
|
||||
|
||||
## Updates
|
||||
|
||||
ClaudeDo checks for new releases and shows a banner when one is out. *Update now* relaunches
|
||||
the installer, which stops the worker, swaps the binaries and starts it again. Your database,
|
||||
worktrees and settings are preserved. You can also check manually via `Help → Check for
|
||||
updates`.
|
||||
|
||||
## Where your data lives
|
||||
|
||||
Everything is under `%USERPROFILE%\.todo-app`:
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `todo.db` | Your tasks, lists, runs and worktree records (SQLite) |
|
||||
| `ui.config.json` | Window and UI settings |
|
||||
| `worker.config.json` | Worker settings — paths, port, worktree strategy |
|
||||
| `logs/` | Worker and task logs |
|
||||
| `agents/` | Your agent definition files |
|
||||
| `attachments/` | Files you attached to tasks |
|
||||
|
||||
`Help → About` links straight to these folders. Nothing leaves your machine unless you turn
|
||||
on the online inbox.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `Ctrl+K` | Focus search |
|
||||
| `Ctrl+N` | Focus the add-task box |
|
||||
| `Enter` | Add the task |
|
||||
| `Esc` | Leave the current text field / close a dialog |
|
||||
| `F8` / `Shift+F8` | Next / previous conflict (merge editor) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Worker not reachable"** — the background worker is not running. The dialog offers to
|
||||
start it; `Worker → Restart worker` does the same later. If it keeps happening, re-run the
|
||||
installer and choose *Repair*.
|
||||
|
||||
**A task failed immediately** — usually the `claude` CLI is not signed in, or the list's
|
||||
working directory is not a git repo. Check the Output tab and the footer log.
|
||||
|
||||
**A task is stuck in Running after a crash** — the worker sweeps orphaned runs to Failed on
|
||||
startup. Restart the worker.
|
||||
|
||||
**Nothing is being picked up** — check the usage pill: if a usage stop is active the queue is
|
||||
paused on purpose. Also check whether the tasks are MANUAL, blocked by a predecessor, or
|
||||
scheduled for later.
|
||||
|
||||
## For developers
|
||||
|
||||
Architecture, build commands and per-project docs live in `CLAUDE.md` (root and one per
|
||||
project under `src/`). Short version: .NET 8, Avalonia UI, SQLite via EF Core, and a
|
||||
SignalR-connected worker process on `127.0.0.1:47821`.
|
||||
|
||||
```bash
|
||||
# Build
|
||||
dotnet build src/ClaudeDo.App
|
||||
dotnet build src/ClaudeDo.Worker
|
||||
|
||||
# Run tests
|
||||
dotnet test tests/ClaudeDo.Worker.Tests
|
||||
|
||||
# Run the app
|
||||
dotnet run --project src/ClaudeDo.App
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Create a task in the UI and tag it with **"agent"** to mark it for automated execution.
|
||||
2. The Worker picks up queued tasks and runs each one via Claude CLI in an isolated git worktree.
|
||||
3. When done, the worktree can be merged, kept for review, or discarded.
|
||||
|
||||
**Task status flow:** `Manual | Queued → Running → Done | Failed`
|
||||
|
||||
**Worktree state flow:** `Active → Merged | Discarded | Kept`
|
||||
|
||||
## Configuration
|
||||
|
||||
All data and config lives under `~/.todo-app/`:
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `todo.db` | SQLite database |
|
||||
| `ui.config.json` | UI settings |
|
||||
| `worker.config.json` | Worker settings (worktree strategy, etc.) |
|
||||
| `logs/` | Application logs |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
ClaudeDo.slnx
|
||||
├── src/
|
||||
│ ├── ClaudeDo.App/ # Desktop entry point
|
||||
│ ├── ClaudeDo.Ui/ # Views & ViewModels
|
||||
│ ├── ClaudeDo.Data/ # Data access layer
|
||||
│ └── ClaudeDo.Worker/ # Background task runner
|
||||
├── tests/
|
||||
│ └── ClaudeDo.Worker.Tests/
|
||||
├── schema/
|
||||
│ └── schema.sql # Database schema
|
||||
└── docs/
|
||||
├── plan.md # Architecture & design spec
|
||||
├── open.md # Verification checklist & backlog
|
||||
└── improvement-plan.md # Prioritized improvements
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -10,6 +10,17 @@ These sit **between** the CLAUDE.md files and the code:
|
||||
too fine-grained for a CLAUDE.md but stable enough to be worth caching. Read on demand.
|
||||
- **code** — the only source of truth.
|
||||
|
||||
## Index
|
||||
|
||||
| Note | Covers |
|
||||
|---|---|
|
||||
| [worker-task-pipeline](worker-task-pipeline.md) | `TaskRunner` end-to-end: config resolution, worktree, CLI invocation, streaming, commit |
|
||||
| [usage-monitoring](usage-monitoring.md) | OAuth usage endpoint, gate, throttle, per-run token accounting, usage pill/modal |
|
||||
| [external-mcp](external-mcp.md) | The `claudedo` MCP tool surface + its two test-enforced conventions |
|
||||
| [review-merge](review-merge.md) | Approve=merge-unit, verify gate, `MergeCommit`/revert, diff stack, conflict resolver |
|
||||
| [conpty-sessions](conpty-sessions.md) | Interactive/planning/list-handler launch specs + the arg-flattening gotcha |
|
||||
| [installer-preflight](installer-preflight.md) | CLI version/login/auto-mode research, the `ExecutableResolver`/shim root cause, and the Installer's `Checks/`+`SystemCheckPage` implementation status |
|
||||
|
||||
## Rules
|
||||
|
||||
- **Only stable structure.** Flows, responsibilities, entry points, invariants, relative
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
# ConPTY interactive sessions & launch specs
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `8dbdfb3` (2026-08-06).
|
||||
> Drift check: `git log --oneline bdee731..HEAD -- src/ClaudeDo.Worker/Planning src/ClaudeDo.Worker/Hub src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/Views/InteractiveTerminalView.axaml`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
Covers `InteractiveLaunchSpecService` and the four kinds of embedded ConPTY session the UI
|
||||
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
|
||||
|
||||
| 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` |
|
||||
| List handler | `GetMergeHelperLaunchSpec` | Effort from list config; `--permission-mode auto` (unattended) |
|
||||
|
||||
## ⚠️ Gotcha: never pass task free-text as a CLI argument
|
||||
|
||||
**No ConPTY path ever passes task free-text (title / description / brief) as a CLI argument.**
|
||||
Every one of them writes it to a file first and hands `claude` a single-line kickoff pointing at
|
||||
that file, exposed via `--add-dir`.
|
||||
|
||||
Two independent reasons:
|
||||
|
||||
1. The ConPTY host flattens `Args` into **one command line** to spawn the process, and `claude`
|
||||
re-splits that line on whitespace. Any token starting with `-` in real task text (e.g. `->`,
|
||||
`--abort`) is then misread as an unknown option.
|
||||
2. A raw multi-line positional prompt truncates at its **first newline** regardless.
|
||||
|
||||
A fresh task session's brief lives at `~/.todo-app/task-sessions/<taskId>/brief.md`
|
||||
(`InteractiveLaunchSpecService.BuildFreshTaskArgsAsync`). A task with neither title nor
|
||||
description skips the file **and** the positional arg entirely — it still gets `--session-id`.
|
||||
|
||||
## 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.
|
||||
|
||||
`--model` is deliberately **NOT** forced on an interactive session — the user can still switch
|
||||
models in the TUI.
|
||||
|
||||
### ⚠️ Gotcha: no directory arg may end in a separator
|
||||
|
||||
`PtyTerminalSession` hands `Args` to `TerminalControl.Args`, which the library flattens into ONE
|
||||
Windows command line with each token quoted. Windows argv rules read `\"` as an *escaped* quote,
|
||||
so a token like `"C:\repo\"` never closes and every following argument is swallowed by the
|
||||
preceding **variadic** flag. A list working dir stored as `C:\Dev\Repos\Bandel.Hub\` therefore
|
||||
fed `--add-dir` the repo, `--append-system-prompt-file`, its value **and** the positional kickoff:
|
||||
the CLI warned `brief.md is not a directory` and the session opened with no prompt at all
|
||||
(2026-08-06). `BuildForMergeHelperAsync`/`BuildForMergeHelperHandoffAsync` now run the repo
|
||||
through `TrimTrailingSeparator`; session dirs the worker builds never carry one.
|
||||
|
||||
Diagnosing this from code or a PowerShell repro is a dead end — PowerShell quotes correctly, so
|
||||
every repro passes. Read the real command line instead:
|
||||
`Get-CimInstance Win32_Process -Filter "Name = 'claude.exe'"`.
|
||||
|
||||
## Resuming a task session (`TaskEntity.InteractiveSessionId`)
|
||||
|
||||
`claude --session-id <uuid>` lets the caller pre-assign a conversation's session id instead of
|
||||
waiting for the CLI to generate one. `BuildForTaskAsync` uses this so a closed or aborted
|
||||
interactive task session can be resumed even if it never got far enough to write anything to its
|
||||
own transcript:
|
||||
|
||||
1. **Resume check.** If the task isn't on a freshly (re)created worktree, `BuildForTaskAsync`
|
||||
picks a session to resume with `task.InteractiveSessionId ?? run?.SessionId` — this task's own
|
||||
last *interactive* conversation takes precedence over the latest *autonomous* run's session,
|
||||
since they're distinct conversations even against the same worktree. A task that has only ever
|
||||
run autonomously still resumes into that run's session the first time it's opened interactively
|
||||
(this is the pre-existing behavior `run?.SessionId` alone used to provide).
|
||||
2. **Fresh path.** If neither is available (never run any way, or `isFreshWorktree`), a new
|
||||
`Guid.NewGuid()` is generated and persisted to `TaskEntity.InteractiveSessionId` via
|
||||
`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`).
|
||||
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
|
||||
`InteractiveSessionId` with the new one.
|
||||
|
||||
Net effect: reopening an interactive session for a task (pane closed, process killed, whatever)
|
||||
resumes the same claude conversation, because the id was committed to the DB before the previous
|
||||
launch even started.
|
||||
|
||||
## List handler ("Let Claude handle it")
|
||||
|
||||
`BuildForMergeHelperAsync` uses `--permission-mode auto` so it runs unattended. The
|
||||
`--allowedTools` allowlist is the security boundary:
|
||||
`mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill`.
|
||||
|
||||
`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)).
|
||||
|
||||
### The host task and its commit range
|
||||
|
||||
The handler run **owns a real ClaudeDo task**, created by `CreateMergeHelperTask` (hub) →
|
||||
`InteractiveLaunchSpecService.CreateMergeHelperTaskAsync`, called by the UI *before* it opens
|
||||
the tile:
|
||||
|
||||
- One new task per run in that list, `Idle` + `IsManual=true` (never queued).
|
||||
- Title/description localized via `missionControl.mergeHelperTaskTitle` /
|
||||
`mergeHelperTaskDescriptionHeader`.
|
||||
- `TaskEntity.HandlerBaseCommit` stamped to the list repo's current HEAD.
|
||||
|
||||
The host task **never gets a worktree of its own** — the handler commits straight into the
|
||||
list's working dir and merges the tasks it handles itself. Consequences:
|
||||
|
||||
- `SubmitTaskForReview` branches on whether the task has a `WorktreeEntity`: with one, it
|
||||
commits the worktree; without one, it stamps `HandlerHeadCommit` to the list repo's current
|
||||
HEAD. Both paths then flip the task `Idle`/`Failed` → `WaitingForReview`.
|
||||
- `GetTaskDiff` and the UI's `DetailsIslandViewModel` / `MergeSectionViewModel` fall back to the
|
||||
`HandlerBaseCommit`..`HandlerHeadCommit` range whenever `Worktree` is null.
|
||||
|
||||
### UI flow
|
||||
|
||||
`MergeHelperSelectionModalViewModel` — checkbox picker over one list's non-terminal, non-manual
|
||||
tasks, pre-ticking the actionable ones. **List-scoped only** (`Configure(listId, listName)`, no
|
||||
global scope). Opened from the list row's context menu, which is hidden when the list has no
|
||||
working dir.
|
||||
|
||||
On confirm: `ListsIslandViewModel` raises `LetClaudeHandleRequested` → shell →
|
||||
`MissionControlViewModel.OpenMergeHelperConPtySessionAsync`, which calls
|
||||
`CreateMergeHelperTaskAsync` and then opens a **task-based** tile (deduped by `TaskId` like
|
||||
`OpenConPtySessionAsync`, **not** `CreateAdHoc`) running the five-phase handler prompt.
|
||||
|
||||
## Tile lifecycle
|
||||
|
||||
`ConPtyPaneViewModel` resolves its own launch spec — the ctor takes a descriptor **factory**,
|
||||
the host wires handlers and then calls `Start()`. So the tile appears **immediately** with its
|
||||
spinner while the worker is still preparing the worktree. A failed launch keeps the tile with an
|
||||
inline error banner instead of the tile never appearing.
|
||||
|
||||
`SubmitForReviewCommand.CanExecute` also gates on `Terminal.IsStarting` / `StartError` /
|
||||
`HasExited` (not just `IsTaskBased`) — a starting or dead pane can't offer a review it would only
|
||||
have the worker reject, and `MissionControlViewModel.OnPaneSubmitForReview` sets the pane's
|
||||
`IsSubmitPending` flag for the duration of the round trip so a rapid double-click can't race two
|
||||
`SubmitTaskForReviewAsync` calls. A failed launch also offers `RetryCommand` (visible whenever
|
||||
`HasExited && StartError != null`) — it swaps in a fresh `InteractiveTerminalViewModel` and calls
|
||||
`Start()` again on the **same** pane/`TaskId` dedupe slot, since `PtyTerminalSession` throws on a
|
||||
second `StartAsync` call and can't be restarted in place.
|
||||
|
||||
### ⚠️ Gotcha: the terminal library kills its child on visual-tree detach
|
||||
|
||||
`Iciclecreek.Avalonia.Terminal`'s `TerminalView.OnDetachedFromLogicalTree` calls
|
||||
`CleanupProcess()` (kills the PTY child) unless `BeginReparent()` suppressed it — and Mission
|
||||
Control detaches pane views routinely (`RebuildOverviewGrid` recreates everything on any pane
|
||||
add/remove/column change; focus-mode tab switches re-present content). Two-part defense (since
|
||||
`aac84e4`):
|
||||
|
||||
1. `PtyTerminalSession.StartAsync` puts the control in **permanent reparent mode** right after
|
||||
`LaunchProcess()` — `EndReparent` is deliberately never called. Teardown is explicit only:
|
||||
`ConPtyPaneViewModel.Dispose` → `Terminal.Kill()` (pane close, VM disposal via DI on exit).
|
||||
2. `ConPtyPaneHost` (the DataTemplate content for a pane) reparents **one long-lived
|
||||
`ConPtyPaneView` per pane VM** (`ConditionalWeakTable`, view pins its own `DataContext`)
|
||||
instead of letting the template instantiate a fresh view — a fresh view would render a dead,
|
||||
empty terminal because the running session is bound to the original `TerminalControl`.
|
||||
Hosts only steal the view while `IsEffectivelyVisible`; the layout toggle posts a reclaim
|
||||
pass (`MissionControlView.ReclaimVisiblePaneHosts`) so the now-visible layout re-steals.
|
||||
|
||||
`Ellipse.spinner` (IslandStyles) is the shared indeterminate spinner — used for a starting pane
|
||||
(`InteractiveTerminalViewModel.IsStarting`) and in place of the refine button while
|
||||
`TaskRowViewModel.IsRefining`.
|
||||
|
||||
### ⚠️ Gotcha: env-var launch race across sessions
|
||||
|
||||
`PtyTerminalSession.StartAsync` applies `TerminalLaunchDescriptor.Env` via
|
||||
`Environment.SetEnvironmentVariable` onto the **whole UI process** (Porta.Pty has no per-launch
|
||||
env seam — it always inherits the calling process's environment), then calls
|
||||
`TerminalControl.LaunchProcess()`. Two sessions starting back-to-back (e.g. planning sessions for
|
||||
two different tasks) could interleave: task B's `SetEnvironmentVariable` calls could land between
|
||||
task A's env-set and its `LaunchProcess()` fork, so task A's `claude` process inherits B's env
|
||||
(e.g. `CLAUDEDO_PLANNING_TOKEN`) and fails its own MCP auth. Fixed by serializing the
|
||||
set-env-then-launch critical section behind a process-wide `static SemaphoreSlim(1,1)` in
|
||||
`PtyTerminalSession`. Env leakage onto the whole process *after* a launch has forked remains a
|
||||
documented limitation — only the fork-time race is closed.
|
||||
|
||||
### ⚠️ Gotcha: open-path dedupe races
|
||||
|
||||
`MissionControlViewModel.OpenConPtySessionAsync` / `OpenPlanningConPtySessionAsync` dedupe by
|
||||
`TaskId` against `ConPtySessions`, but the check ran before an **awaited** DB title lookup and
|
||||
only `AddConPtyPane` registers the pane — two rapid invocations for the same task (e.g. a
|
||||
double-click) could both pass the dedupe check before either pane existed, opening two panes.
|
||||
`OpenMergeHelperConPtySessionAsync` was worse: it awaits `CreateMergeHelperTaskAsync` (which mints
|
||||
a brand-new task id every call) *before* any `TaskId` dedupe is even possible, so a double-trigger
|
||||
always minted two host tasks in the DB.
|
||||
|
||||
Fixed with synchronous, pre-await claims: `_pendingTaskOpens` (shared by the two `TaskId`-keyed
|
||||
open paths) and `_pendingMergeHelperLists` (keyed by `listId`, guarding the whole method since
|
||||
there's no `TaskId` yet to dedupe on) are `HashSet<string>` fields checked-and-added at method
|
||||
entry, before any `await`, and released in a `finally`. A second overlapping call for the same key
|
||||
bails out immediately instead of racing past the collection-based dedupe.
|
||||
|
||||
## Focus / key handling
|
||||
|
||||
`InteractiveTerminalView` lives in `MissionControlWindow`, so the `FocusClearing` Escape handler
|
||||
(scoped to `MainWindow` via `AddClassHandler<MainWindow>`) never runs there — **Escape always
|
||||
reaches the PTY**. See the note in `src/ClaudeDo.Ui/CLAUDE.md`.
|
||||
|
||||
`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.
|
||||
|
||||
### Queueing is gated on an open session (UI-only, since `d84607f`)
|
||||
|
||||
A task-based session leaves the row `Idle` (sessions never write `Status`), so nothing on the
|
||||
worker side distinguishes it from a plain idle task. `TaskRowViewModel.CanSendToQueue` and
|
||||
`MissionControlViewModel.EnqueueTaskAsync` (drag-to-queue onto the Command Center window) both
|
||||
check for an open session before queueing — the row via `HasInteractiveSession`, the drag path via
|
||||
`ConPtySessions.Any(s => s.TaskId == taskId)` (Mission Control's own authoritative pane list,
|
||||
since the mirrored bool on the row could lag). Queueing a task open in a hand-driven ConPTY pane
|
||||
would otherwise let the picker spawn an autonomous `claude` process into the same worktree the
|
||||
user is editing. Both enqueue paths (`TasksIslandViewModel.SendToQueueAsync` and
|
||||
`MissionControlViewModel.EnqueueTaskAsync`) also route through `IWorkerClient.SetTaskStatusAsync`
|
||||
(hub `SetTaskStatus` → `TaskStateService.EnqueueAsync`) instead of a raw EF write, so the
|
||||
manual/draft-child guards apply on both paths too.
|
||||
|
||||
## System-prompt matrix (autonomous vs. interactive)
|
||||
|
||||
Autonomous and interactive sessions do **not** share a system prompt. Per start path:
|
||||
|
||||
| Start path | Entry point | System prompt |
|
||||
|---|---|---|
|
||||
| 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`) |
|
||||
| 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>` |
|
||||
| List handler ("Let Claude handle it"), triage | `InteractiveLaunchSpecService.BuildForMergeHelperAsync` | `--append-system-prompt-file <path>` (`PromptKind.MergeHelperTriage` — phases 0–2 only), always fresh — this path never resumes |
|
||||
| List handler, post-handoff | `InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync` | `--append-system-prompt-file <path>` (`PromptKind.MergeHelperExecute` — phases 3–5 only), fresh session dir, same handler task id |
|
||||
|
||||
So every interactive resume (task session and planning) drops the system prompt entirely — it's
|
||||
not that they inherit the autonomous one, it's that **no** `claude` process on any resume path
|
||||
ever passes `--append-system-prompt(-file)`.
|
||||
|
||||
### Does `--resume` bring back a prior `--append-system-prompt`? No.
|
||||
|
||||
Checked by reading real session transcripts (`~/.claude/projects/<cwd>/<sessionId>.jsonl`) for
|
||||
several autonomous ClaudeDo task runs, including ones with multiple invocations (initial run +
|
||||
`ContinueAsync`/retry on the same session id, confirmed via that project's `task_runs` history).
|
||||
Grepped for the `PromptKind.System` default text ("You are completing one well-defined task
|
||||
autonomously...") and for any `"type":"system"` entry or `message.role == "system"` anywhere in
|
||||
those files: the prompt text only ever showed up as ordinary tool-result content (e.g. a task
|
||||
that happened to read `PromptFiles.cs`'s own source), never as a persisted system/config entry.
|
||||
No session transcript — autonomous or interactive — carries a system-role message or a
|
||||
per-session record of the CLI flags it was launched with; there is no sidecar file next to the
|
||||
`.jsonl` either. The system prompt is purely a per-process request parameter the CLI builds fresh
|
||||
from that invocation's own flags, never replayed from a resumed session's history. This matches
|
||||
why `TaskRunner.ContinueAsync` (autonomous) explicitly re-resolves and re-passes
|
||||
`--append-system-prompt` on every continue instead of relying on `--resume` to carry it —
|
||||
if inheritance worked, that re-resolution would be redundant.
|
||||
|
||||
**Conclusion: no leak.** An interactive resume (task or planning) does not pick up the autonomous
|
||||
run's `--append-system-prompt` text — including the "commit your work" / `CLAUDEDO_BLOCKED`
|
||||
instructions from `PromptKind.System`. It simply runs with the `claude` CLI's own baseline system
|
||||
prompt, same as every other path in this table that passes no system-prompt flag. No code change
|
||||
needed here.
|
||||
|
||||
## Related hub methods
|
||||
|
||||
`GetInteractiveLaunchSpec`, `GetAdHocLaunchSpec`, `GetMergeHelperLaunchSpec`,
|
||||
`CreateMergeHelperTask`, `SubmitTaskForReview`.
|
||||
|
||||
Planning sessions: `StartPlanningSession`, `ResumePlanningSession`, `DiscardPlanningSession`,
|
||||
`FinalizePlanningSession`, `QueuePlanningSubtasks`, `GetPendingDraftCount`,
|
||||
`GetPlanningAggregate`, `BuildPlanningIntegrationBranch`.
|
||||
@@ -0,0 +1,221 @@
|
||||
# External MCP tool surface
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `20bce9b` (2026-08-06).
|
||||
> Drift check: `git log --oneline 20bce9b..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
|
||||
Claude sessions. Registered explicitly in `Program.cs`'s external app via `.WithTools<T>()`.
|
||||
Server name `claudedo`, registered globally by the installer's `RegisterMcpStep`, so callers
|
||||
need no `--mcp-config`.
|
||||
|
||||
**Scope boundary:** these tools cover *starting* and *observing* sessions plus task/list CRUD
|
||||
and git/merge operations. They deliberately do **not** expose multi-turn control, planning
|
||||
session internals, or app-settings writes. Auth via an optional `X-ClaudeDo-Key` header.
|
||||
|
||||
## Hard conventions (enforced by tests)
|
||||
|
||||
1. **Every optional/filter parameter needs a C# default value** (e.g. `string? status = null`).
|
||||
The MCP schema only marks a parameter optional when it has one — nullability alone does
|
||||
not do it. `ExternalMcpToolSchemaTests` guards this by reflection.
|
||||
2. **No tool returns bare `Task` or a nullable payload directly.** An MCP client cannot tell
|
||||
an empty/omitted response apart from a dropped one.
|
||||
- *Write* tools return a small confirmation record — `{ ok/deleted/removed/reset/started:
|
||||
true, <id>, ... }` (`DeleteListResult`, `RunTaskNowResult`, `ResetFailedTaskResult`,
|
||||
`RemoveAttachmentResult`; `SetListConfigResult` / `SetTaskConfigResult` additionally echo
|
||||
the resulting config so the caller can see which fields were set vs. cleared to null).
|
||||
- *Read* tools that may have nothing to return use an explicit `Found` / `Available` flag
|
||||
alongside the nullable payload (`TaskConfigResult`, `BatchGetTaskResult`, `TaskLogResult`).
|
||||
- The same flag-alongside-nullable-payload idiom also covers "which of two shapes did you
|
||||
get": `ListTasks`/`BatchGetTasks` take `includeDescription` (default `false`) and return
|
||||
`ListTasksResult`/`BatchGetTaskResult`, where exactly one of the lean (`TaskRefDto`) and
|
||||
full (`TaskDto`, incl. Description/Result) fields is populated per the flag — keeps a
|
||||
list of verbosely-described tasks from blowing past the response size limit by default.
|
||||
3. **Description style is documented in `McpToolDocs`** (same folder) and shared boilerplate
|
||||
lives there as `const` strings. Rules: the first sentence says what the tool does *and* when
|
||||
to reach for it (MCP clients rank tools by that text, so the trigger must not sit behind
|
||||
return-shape prose); parameters are documented with `[Description]` **on the parameter**, not
|
||||
in the tool description; result fields appear only where the caller must branch on them
|
||||
before calling (`isEmpty`, `truncated`, `conflicts`, `available`); no design rationale or
|
||||
"since this feature was introduced" history. Not test-enforced — review it in PRs.
|
||||
4. `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so
|
||||
`InvalidOperationException` / `ArgumentException` messages survive as `McpException` —
|
||||
otherwise the SDK's catch-all replaces any non-`McpException` with a generic
|
||||
*"An error occurred invoking 'X'."*
|
||||
|
||||
## Tool classes
|
||||
|
||||
### `ExternalMcpService` — task CRUD, execution, git
|
||||
|
||||
Task: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`,
|
||||
`UpdateTaskStatus`, `ReviewTask`, `RunTaskNow`, `ContinueTask`, `CancelTask`, `DeleteTask`.
|
||||
(`GetTaskStatusValues` was removed — a whole tool entry for static reference text. `GetTask`'s
|
||||
description is now the canonical place for what each status means.)
|
||||
|
||||
Worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ContinueMerge`, `AbortMerge`,
|
||||
`PreviewMerge`, `PreviewMergeSet`, `RevertMerge`, `ListWorktrees`, `CleanupTaskWorktree`.
|
||||
|
||||
Daily prep: `GetDailyPrepCandidates`, `SetMyDay`.
|
||||
|
||||
### Other classes
|
||||
|
||||
| Class | Tools |
|
||||
|---|---|
|
||||
| `BatchMcpTools` | `BatchGetTasks`, `BatchAddTasks`, `BatchUpdateTaskStatus`, `BatchCancelTasks`, `BatchDeleteTasks`, `BatchSetMyDay`, `BatchCleanupTaskWorktrees` |
|
||||
| `ListMcpTools` | `CreateList`, `UpdateList`, `DeleteList` |
|
||||
| `ConfigMcpTools` | `GetListConfig`, `SetListConfig`, `GetTaskConfig`, `SetTaskConfig`, `GetEffectiveRunConfig` |
|
||||
| `RunHistoryMcpTools` | `ListRuns`, `GetRun`, `GetTaskLog` |
|
||||
| `AgentMcpTools` | `ListAgents` |
|
||||
| `LifecycleMcpTools` | `ResetFailedTask` |
|
||||
| `AppSettingsMcpTools` | `GetAppSettings` (read-only) |
|
||||
| `TaskWaitMcpTools` | `WaitForTaskChange` |
|
||||
| `QueueStateMcpTools` | `GetQueueState` |
|
||||
| `AttachmentMcpTools` | `AddTaskAttachment`, `ListTaskAttachments`, `RemoveTaskAttachment` |
|
||||
|
||||
## Per-tool behaviour worth knowing
|
||||
|
||||
**`ListTasks`** — `includeDescription=false` (default) returns lean `TaskRefDto` references in
|
||||
`tasks` (`tasksFull` null); `includeDescription=true` returns full `TaskDto`s (incl.
|
||||
Description/Result) in `tasksFull` instead (`tasks` null). Filtering by `createdBy`/`status`
|
||||
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.
|
||||
|
||||
**`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
|
||||
overlaps strongly with the new one. Cheap word-overlap heuristic (`ExternalMcpService`'s
|
||||
`FindPossibleDuplicatesAsync`/`NormalizeTitleWords`), no embeddings/LLM call, no blocking —
|
||||
the caller just gets a heads-up to relay. `BatchAddTasks` carries the same field per item.
|
||||
|
||||
**`ReviewTask`** — `approve` / `reject_rerun` / `reject_park` / `cancel` for a
|
||||
`WaitingForReview` task. Approve is review+merge exactly like the hub's `ApproveReview`: unit
|
||||
merge for parents, worktree merge into optional `targetBranch` for childless tasks. Conflicts
|
||||
are reported in `ReviewTaskResult`.
|
||||
- A parent's approve also returns `emptyChildren`: the `Done` children about to be unit-merged
|
||||
whose own review range contributed nothing (computed the same way as `PreviewMerge`'s
|
||||
`isEmpty`, before the merge starts so it reflects what's about to be approved). Surfaces a
|
||||
child that reported `CLAUDEDO_BLOCKED` and committed no code — previously that child reached
|
||||
`Done` and merged silently with `changedFileCount: 0`, indistinguishable from a small-but-real
|
||||
change. `TaskRefDto.roadblockCount` (on every task-returning tool, stamped by `TaskRunner` from
|
||||
`result.Blocks.Count`) is the MCP-visible signal for *why* a child is empty.
|
||||
|
||||
**`PreviewMerge`** — non-destructive `git merge-tree --write-tree` mergeability check for one
|
||||
task's worktree branch against `targetBranch` (default: the repo's current branch). Returns
|
||||
status / conflictFiles / changedFileCount / `behind` / `isEmpty`. Unlike
|
||||
`TaskMergeService.PreviewAsync`'s silent *"unavailable"*, this **throws a clear error** when the
|
||||
task has neither an active worktree nor a handler commit range, or the list's working dir is
|
||||
missing.
|
||||
- `isEmpty` = the review range contributed nothing — zero files changed against the worktree's
|
||||
base commit, or (for a worktree-less list-handler host task) `HandlerBaseCommit ==
|
||||
HandlerHeadCommit`. Distinguishes a genuinely empty branch from one that merely made a small
|
||||
change (`changedFileCount: 0` alone reads as "tiny", not "nothing to review") — the gap that
|
||||
let two blocked planning children reach `Done` with unmerged empty branches unnoticed.
|
||||
- A worktree-less handler task has no separate branch to `merge-tree`-preview (its commits
|
||||
already sit in `list.WorkingDir`) — `PreviewMergeCoreAsync` falls back to a synthetic `clean`
|
||||
preview over its own `HandlerBaseCommit..HandlerHeadCommit` diff-stat instead of throwing
|
||||
"has no worktree".
|
||||
|
||||
**`PreviewMergeSet`** — same preview for a batch (each entry also carries `isEmpty`), plus a
|
||||
file→tasks overlap report built from each task's own diff-stat. ⚠️ That overlap report is a
|
||||
**same-file-name hint only** — it is blind to cross-file collisions (e.g. the CS0103 case that
|
||||
motivated it). A task that fails to preview gets `error` set and is excluded from the overlap
|
||||
instead of aborting the batch.
|
||||
|
||||
**`RevertMerge`** — undoes a previously merged task's merge commit on `targetBranch` via
|
||||
`git revert -m 1`. Always a **new commit**, never a reset/rewrite, because the target working
|
||||
directory is shared with other concurrent sessions.
|
||||
- Requires the task to be `Done` with a `Merged` worktree carrying a recorded
|
||||
`WorktreeEntity.MergeCommit`. A task merged before that field existed has none and is
|
||||
**refused rather than guessed** via `git log`.
|
||||
- On success the task returns to `WaitingForReview` and the worktree moves to `Kept` — not
|
||||
`Active` (its directory/branch are typically already gone from the original merge's cleanup)
|
||||
and not `Merged`/`Discarded` (`WorktreeMaintenanceService` sweeps those).
|
||||
- A conflicting revert is aborted immediately; no half-resolved state is left in the tree.
|
||||
|
||||
**`BatchMcpTools`** — best-effort loops over the `ExternalMcpService` single-entity methods.
|
||||
**Sequential**, because the scoped `DbContext` is not thread-safe. Merge/review stay
|
||||
single-task. Every tool returns a per-item result array (`{ id/index, ok, error?, … }`) — a
|
||||
failing item never aborts the rest — and rejects batches over **100 items**.
|
||||
`BatchGetTasks` mirrors `ListTasks`'s `includeDescription` flag (default `false`): a found item's
|
||||
`BatchGetTaskResult` carries `task` (lean) or `taskFull` (full), never both.
|
||||
|
||||
**`GetTaskLog`** — latest run's log, tail-capped at 256 KB.
|
||||
|
||||
**`WaitForTaskChange(taskIds, timeoutSeconds = 60, treatWaitingForChildrenAsBusy = false)`** —
|
||||
blocks until any given task leaves `Queued`/`Running`, or times out. Returns immediately for a
|
||||
task already outside those two (unknown ids reported as status `"NotFound"`, also immediate).
|
||||
- Implemented as an **async DB poll** (short-lived `DbContext` per check, 500 ms delay, no
|
||||
held connection, no busy loop) rather than hooking `HubBroadcaster` — deliberately isolated
|
||||
so it can't regress the existing broadcast callers.
|
||||
- `timeoutSeconds` is clamped server-side to `TaskWaitMcpTools.MaxTimeoutSeconds` (900 s),
|
||||
comfortably under the `MCP_TOOL_TIMEOUT` (930 s) every ClaudeDo-owned claude launcher sets —
|
||||
`ClaudeProcess` for headless queue runs, `InteractiveLaunchSpecService` for every embedded
|
||||
ConPTY session (list handler, planning, interactive resume) — so the tool reports
|
||||
`timedOut: true` instead of racing the client's own abort. A caller running claude with a
|
||||
different (or default: 60 s) `MCP_TOOL_TIMEOUT` will still see its own client-side timeout
|
||||
fire first; the server has no way to detect or compensate for that.
|
||||
- **`treatWaitingForChildrenAsBusy` pitfall (default `false`, backward-compatible):** a planning
|
||||
parent with children goes `Running` → `WaitingForChildren` while children are still working,
|
||||
and by default that already counts as "changed" (it's outside `Queued`/`Running`) — so waiting
|
||||
on a parent returns immediately even though the unit isn't done. Set the flag to keep polling
|
||||
through `WaitingForChildren`; the call then only reports changed once the parent reaches
|
||||
`WaitingForReview` or a terminal status. Does not list or watch the parent's children —
|
||||
callers still need their own ids for that.
|
||||
- Replaced the list handler's old "sleep + poll `get_task` in a loop" Phase 3 instruction.
|
||||
|
||||
**`GetQueueState()`** — read-only snapshot so a caller doesn't have to infer queue state from
|
||||
`maxParallelExecutions` or repeated `wait_for_task_change` rounds:
|
||||
`{ configuredSlots, effectiveSlots, activeSlots: [{ slot, taskId, startedAt }], waitingTaskIds }`.
|
||||
- `configuredSlots`/`effectiveSlots` reuse `QueueService.GetSlotCountsAsync` — the same
|
||||
configured-vs-throttled computation `QueueService.ExecuteAsync` uses each tick (see
|
||||
[usage-monitoring](usage-monitoring.md) for the throttle staging) — so this tool can't drift
|
||||
from the queue's actual refill decision.
|
||||
- `activeSlots` reuses `QueueService.GetActive()` (already the source for the Hub's `GetActive`):
|
||||
`slot` is `"queue"` for a normal queue slot or `"override"` for the single
|
||||
`run_task_now`/`continue_task` slot.
|
||||
- `waitingTaskIds` is a fresh read-only query mirroring `QueuePicker.ClaimNextAsync`'s
|
||||
eligibility filter and order (`Queued`, unblocked, non-manual, due, `sort_order` then
|
||||
`created_at`) — it does not claim or mutate anything.
|
||||
|
||||
**`AttachmentMcpTools`** — re-attaching the same `fileName` overwrites. Add/remove refuse on a
|
||||
`Running` task.
|
||||
|
||||
**`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`
|
||||
helper in the same file.
|
||||
|
||||
**`SetMyDay`** — sets `IsMyDay` (+ optional `SortOrder`). A server-side cap-guard rejects
|
||||
turning on MyDay beyond `DailyPrepMaxTasks` open (Idle) MyDay tasks.
|
||||
|
||||
**`GetEffectiveRunConfig`** — read-only report of what a task will *actually* run with (model,
|
||||
max turns, effort, permission mode, agent path, whether a system prompt is set, skill names),
|
||||
each with its source (`task`/`list`/`preset`/`global`); max turns additionally reports the raw
|
||||
requested value and whether it was clamped to `AppSettings.MaxTurnsCeiling`. Unlike
|
||||
`GetAppSettings`/`GetTaskConfig` (raw, possibly-unused config values), this goes through the same
|
||||
`EffectiveRunConfigResolver.Resolve` that `TaskRunner` itself runs with — see
|
||||
[worker-task-pipeline](./worker-task-pipeline.md)'s model/effort/max-turns section — so it can't
|
||||
drift from the real run. Reads (not writes) `AppSettingsRepository.GetAsync`, which backfills
|
||||
`model_presets` on first read after a null column; that backfill is pre-existing shared behavior,
|
||||
not a new side effect introduced by this tool.
|
||||
|
||||
## Model / max-turns on task creation
|
||||
|
||||
Task-generating tools (`AddTask`, planning `CreateChildTask`, `SuggestImprovement`) accept an
|
||||
optional `model`, alias-validated via `ModelRegistry.NormalizeAlias` (`haiku`/`sonnet`/`opus`,
|
||||
blank = inherit), so Claude can assign the cheapest capable model at creation time — the
|
||||
planning/system/improvement prompts instruct it to do so, using
|
||||
`ModelRegistry.ByCostAscending` as the cost order.
|
||||
|
||||
Planning's `CreateChildTask` **additionally** accepts an optional `maxTurns` (positive int;
|
||||
`0`/negative rejected with `ArgumentException`, null = inherit list/global default) so the
|
||||
planner can raise the turn budget for a subtask it knows will run long.
|
||||
`SuggestImprovement` and `AddTask` do **not** expose it.
|
||||
@@ -0,0 +1,252 @@
|
||||
# Installer preflight: CLI version gate & environment checks
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `bdee731` (2026-08-05).
|
||||
> Drift check: `git log --oneline bdee731..HEAD -- src/ClaudeDo.Worker/Lifecycle/ClaudeCliPreflight.cs src/ClaudeDo.Worker/ClaudeDo.Worker.csproj src/ClaudeDo.App/ClaudeDo.App.csproj .gitea/workflows/release.yml src/ClaudeDo.Installer/Checks src/ClaudeDo.Data/Environment/ExecutableResolver.cs`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
## Implementation status (as of 2026-08-05)
|
||||
|
||||
The research below (§1–5) led to an implementation, but it is **not on `main` yet** — it exists
|
||||
on two unmerged task branches:
|
||||
|
||||
- `claudedo/06aca9b3afec4b939f59b627bfe21737` — `src/ClaudeDo.Installer/Checks/*`
|
||||
(`GitCheck`, `GitIdentityCheck`, `WriteAccessCheck`, `PortCheck`, `ClaudeCliCheck`,
|
||||
`ClaudeVersionCheck`, `ClaudeAuthCheck`, `PermissionModeAutoCheck`, `EnvironmentCheckService`,
|
||||
`ClaudeCliLookup`) plus `SystemCheckPage` (the Fresh-Install wizard page hosting them) and its
|
||||
own copy of `src/ClaudeDo.Data/Environment/ExecutableResolver.cs`.
|
||||
- `claudedo/40272c0bb3b14562b59c022d09c382b6` — the original `ExecutableResolver.cs`, plus wiring
|
||||
it into `ClaudeDo.Worker`'s `ClaudeCliPreflight` and `ClaudeProcess` (the actual root-cause fix:
|
||||
both used to spawn `claude` with `UseShellExecute = false` and no `.cmd`/`.bat` shim resolution,
|
||||
so an npm-installed `claude.cmd` was invisible to the Worker even though it worked in a shell).
|
||||
|
||||
The two branches were authored independently and each vendored its own copy of
|
||||
`ExecutableResolver.cs` (identical except `06aca9b3` adds a `FallbackDirectories()` diagnostic
|
||||
helper); merging both cleanly requires picking one copy, not literally running `git merge` twice.
|
||||
Two planned follow-up tasks — a "Claude Help Me" button and a Config-mode Diagnose section —
|
||||
never got past a blocked first step, precisely because this prerequisite work wasn't on `main`
|
||||
when they ran. See `Environment Checks` in `src/ClaudeDo.Installer/CLAUDE.md` and `docs/open.md`
|
||||
for the current gap and the manual verification checklist.
|
||||
|
||||
What's confirmed as **matching the research below**: `ClaudeVersionCheck.MinimumVersion` is
|
||||
`2.1.220`, exactly the "verified-floor, not a proven minimum" constant from §3. `ClaudeAuthCheck`
|
||||
uses `claude auth status --json` exactly as recommended in §4, parsing only the `loggedIn` field.
|
||||
`PermissionModeAutoCheck` is the static "is `auto` listed in `--help`" check recommended in §2 —
|
||||
real org/model/plan eligibility is deliberately **not** checked, matching the recommendation not
|
||||
to build that (a real task run surfaces a startup rejection fast enough on its own). No .NET
|
||||
Desktop Runtime check was implemented as an `IEnvironmentCheck` (§5's registry-key detection
|
||||
remains a documented-but-unbuilt option, not currently gating anything).
|
||||
|
||||
---
|
||||
|
||||
Pure research, no code changed. Answers the five questions from the "Root Cause
|
||||
`--permission-mode auto`" task. Sources: the locally installed CLI (`claude --version` /
|
||||
`--help`), the official docs at `code.claude.com` (fetched 2026-08-05), and this repo's own
|
||||
csproj/workflow files.
|
||||
|
||||
## 1. Why can `--permission-mode auto` fail?
|
||||
|
||||
**It is very rarely a CLI-version problem.** Per the official permission-modes doc
|
||||
(`code.claude.com/docs/en/permission-modes#eliminate-prompts-with-auto-mode`), auto mode
|
||||
requires **all** of:
|
||||
|
||||
- **Plan**: any plan (Free/Pro/Max/Team/Enterprise) qualifies in principle.
|
||||
- **Organization**: on Team/Enterprise, on by default; an admin can disable it org-wide via
|
||||
`permissions.disableAutoMode: "disable"` in managed settings. When disabled this way, the CLI
|
||||
**"rejects `--permission-mode auto` at startup"** (verbatim from the doc) — not a runtime
|
||||
fallback, a hard reject.
|
||||
- **Model**: on the Anthropic API / Claude Platform on AWS — Opus 4.6+, Sonnet 4.6+, or Fable 5.
|
||||
On Bedrock / Vertex / Foundry / signed-in gateway sessions — only Sonnet 5, Opus 4.7+, Fable 5.
|
||||
Older models (Sonnet 4.5, Opus 4.5, Haiku, claude-3-*) are **not supported on any provider**.
|
||||
A per-org `availableModels` restriction that only allows an old model would silently make
|
||||
auto mode unavailable even on a Team/Enterprise org that hasn't touched `disableAutoMode`.
|
||||
- **Provider opt-in (historical)**: on Bedrock/Vertex/Foundry/gateway, CLI **v2.1.158–v2.1.206**
|
||||
required `CLAUDE_CODE_ENABLE_AUTO_MODE=1`; **v2.1.207** removed that requirement. This does
|
||||
**not** apply to a normal claude.ai / Console (Anthropic API) login — only to those four
|
||||
provider types.
|
||||
|
||||
A separate, easy-to-hit trap: `defaultMode: "auto"` in **project or local** settings
|
||||
(`.claude/settings.json`, `.claude/settings.local.json`) is silently **ignored** since v2.1.142
|
||||
— it must live in `~/.claude/settings.json` (user scope). A repo that ships `defaultMode: auto`
|
||||
in its own `.claude/settings.json` will start in Manual mode with **no error at all**. This
|
||||
doesn't apply to ClaudeDo's case (it passes `--permission-mode auto` as an explicit CLI flag,
|
||||
not via a checked-in settings file), but is worth knowing if the failure mode was "silently
|
||||
starts in Manual" rather than "CLI errors out".
|
||||
|
||||
Quoted from the docs, verbatim: *"If Claude Code reports auto mode as unavailable, one of these
|
||||
requirements is unmet; this is not a transient outage."*
|
||||
|
||||
**Not verifiable**: which of the above actually hit the colleague — we have no diagnostic from
|
||||
their machine (no `claude auth status --json` output, no `claude --version`, no org name). Do
|
||||
not guess; if this recurs, capture `claude auth status --json` and `claude --version` from the
|
||||
affected machine before further debugging.
|
||||
|
||||
## 2. How to reliably detect whether `auto` is supported
|
||||
|
||||
**Static, cheap, always safe (no API call):**
|
||||
- `claude --version` — semver string, e.g. `2.1.220 (Claude Code)`.
|
||||
- `claude --help` — the `--permission-mode <mode>` line lists the accepted enum. Confirmed by
|
||||
testing an invalid value locally:
|
||||
```
|
||||
$ claude -p "test" --permission-mode bogus
|
||||
error: option '--permission-mode <mode>' argument 'bogus' is invalid. Allowed choices are
|
||||
acceptEdits, auto, bypassPermissions, manual, dontAsk, plan.
|
||||
```
|
||||
This is validated by the CLI's arg parser (Commander.js) **before any network call** — exits
|
||||
1 immediately. So checking that `auto` is one of the listed choices is a legitimate, free,
|
||||
fast static check — but it only proves the *flag* is recognized, **not** that auto mode is
|
||||
actually usable (org/model/plan gating happens later, at session start, not at arg-parse time).
|
||||
- `claude auth status --json` — cheap, local/fast, **does not send a prompt**. Returns:
|
||||
```json
|
||||
{ "loggedIn": true, "authMethod": "claude.ai", "apiProvider": "firstParty",
|
||||
"email": "...", "orgId": "...", "orgName": "...", "subscriptionType": "team" }
|
||||
```
|
||||
This is the answer to question 4 (see below) and also gives `subscriptionType`/`orgName` —
|
||||
useful context but **still not a direct "is auto mode eligible" answer** (doesn't report the
|
||||
active model or `disableAutoMode` policy).
|
||||
|
||||
**No cheap subcommand exists for full eligibility.** Checked `claude auto-mode --help`: it only
|
||||
has `config` (effective auto-mode *rule* config — allow/deny lists, not eligibility),
|
||||
`defaults` (same, shipped defaults), `critique`, and `reset`. None report plan/model/org
|
||||
eligibility. `claude doctor` (non-interactive) does **not** report auto-mode eligibility either
|
||||
— it explicitly says *"For a full setup checkup that can also fix issues, run `/doctor` in a
|
||||
session"*; the in-session `/doctor` slash command is the one the docs say proposes
|
||||
`defaultMode: auto` when eligible, but that requires an interactive session, not a scriptable
|
||||
preflight.
|
||||
|
||||
**Dynamic (real probe) is the only way to fully confirm eligibility**, and per the docs an
|
||||
org-disabled or otherwise-ineligible account **rejects at startup** (fast, before any model
|
||||
turn) — so a probe doesn't have to be a full expensive run. A minimal probe such as
|
||||
`claude -p "ok" --permission-mode auto --max-turns 1 --output-format json` would fail fast on
|
||||
ineligibility (reject at startup) but still costs one real turn + tokens on the success path,
|
||||
and still requires a working prompt/response round trip on the happy path. **Recommendation**:
|
||||
don't build this into an automated preflight; a static version+flag check plus `auth status`
|
||||
covers the reliably-detectable ground, and a real first task run will surface an auto-mode
|
||||
rejection immediately and cheaply (fails at startup, not mid-task) if it's actually unavailable.
|
||||
**Implemented as:** `PermissionModeAutoCheck` (Warning) — the static flag-listed check only.
|
||||
|
||||
## 3. Minimum CLI version for the flags ClaudeDo uses
|
||||
|
||||
Flags used (from `ClaudeArgsBuilder` per `src/ClaudeDo.Worker/CLAUDE.md`): `--permission-mode
|
||||
auto`, `--effort`, `--agents`, `--json-schema`, `--append-system-prompt`, `--output-format
|
||||
stream-json --verbose`, `--resume`, and (installer) `claude mcp add --transport http --scope
|
||||
user`.
|
||||
|
||||
**Not verifiable precisely.** All eight of these are foundational, long-established flags.
|
||||
The official changelog (`raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md`)
|
||||
only retains roughly the last ~40 entries (oldest visible: `2.1.181`); auto mode itself, and
|
||||
`--json-schema`/`--resume`/`mcp add --transport` all clearly predate that window (the earliest
|
||||
found reference is a *bug fix* mentioning "sessions created before v2.1.85" for `--resume`,
|
||||
implying `--resume` existed well before 2.1.85). There is no accessible source that pins an
|
||||
"introduced in vX" date for any of these eight flags — guessing one would violate the task's
|
||||
explicit instruction not to invent a source-less root cause.
|
||||
|
||||
What **is** sourced, from the docs fetched 2026-08-05:
|
||||
- `--json-schema` + invalid-schema handling: before v2.1.205, an invalid schema was silently
|
||||
ignored (returned unstructured text); v2.1.205 made it a hard `Error: --json-schema is not a
|
||||
valid JSON Schema` exit. Not a "does it exist" gate, but changes error-handling behavior
|
||||
ClaudeDo might currently rely on failing loudly.
|
||||
- `--output-format stream-json --verbose` + `system/init.capabilities` array requires v2.1.205+
|
||||
(absent before). Not currently consumed by ClaudeDo per the Worker CLAUDE.md's stream handling
|
||||
description, so not a hard requirement today.
|
||||
- `system/init.mcp_server_errors` field requires v2.1.219+. Not currently consumed by ClaudeDo.
|
||||
- Manual-mode label/`manual` alias requires v2.1.200+ — irrelevant, ClaudeDo passes `auto`
|
||||
explicitly, never `manual`.
|
||||
- Auto mode's Bedrock/Vertex/Foundry/gateway opt-in-env-var requirement was removed in v2.1.207
|
||||
— irrelevant for a direct claude.ai/Console login (this machine: `apiProvider: "firstParty"`).
|
||||
|
||||
**Decided constant** (see below) is therefore **the newest version we can positively confirm
|
||||
works end-to-end on this machine** (`2.1.220`), not a proven theoretical minimum — because no
|
||||
lower true minimum is derivable from available sources without guessing.
|
||||
**Implemented as:** `ClaudeVersionCheck.MinimumVersion = new Version(2, 1, 220)` (Error).
|
||||
|
||||
## 4. Detecting "CLI is logged in" without sending a prompt
|
||||
|
||||
`claude auth status --json` (confirmed working, instant, no API/model call):
|
||||
```json
|
||||
{ "loggedIn": true, "authMethod": "claude.ai", "apiProvider": "firstParty",
|
||||
"email": "...", "orgId": "...", "orgName": "...", "subscriptionType": "team" }
|
||||
```
|
||||
This is the CLI's own maintained answer — cheaper and more robust than parsing
|
||||
`.credentials.json` directly (format may be internal/undocumented and is a hard-blocked file
|
||||
for this task's own tooling; the docs page confirms it lives at
|
||||
`%USERPROFILE%\.claude\.credentials.json` on Windows but say nothing about its schema being a
|
||||
stable public contract). `claude auth status --text` is available for a human-readable variant;
|
||||
`--json` is the default and the right one for a preflight to parse.
|
||||
**Implemented as:** `ClaudeAuthCheck` (Error) — parses only the `loggedIn` boolean; any other
|
||||
field, or a non-zero exit code, or unparseable JSON, becomes `Unknown` rather than `Failed`.
|
||||
|
||||
## 5. .NET runtimes required by the published `app\` / `worker\` artifacts
|
||||
|
||||
From `.gitea/workflows/release.yml` (the only build/publish pipeline in this repo) and the two
|
||||
csproj files:
|
||||
|
||||
- **`ClaudeDo.App`** (`net8.0`, Avalonia, `WinExe`) — published via
|
||||
`dotnet publish ... -r win-x64 --self-contained true`. **Self-contained**: bundles its own
|
||||
.NET 8 runtime. **No .NET runtime needs to be pre-installed** on the target machine for the
|
||||
app itself.
|
||||
- **`ClaudeDo.Worker`** (`net8.0`, `Microsoft.NET.Sdk.Web`, ASP.NET Core) — same treatment:
|
||||
`-r win-x64 --self-contained true`. Also fully self-contained; no ASP.NET Core runtime needs
|
||||
to be pre-installed.
|
||||
- **`ClaudeDo.Installer`** (`net8.0-windows`, WPF) is the one exception: published
|
||||
`--self-contained false -p:PublishSingleFile=true` — **framework-dependent**. The csproj
|
||||
comment explains why: *"the WPF runtime pack isn't distributed for cross-compile on Linux CI,
|
||||
which made self-contained bundles crash on startup with AV in the apphost."* The target
|
||||
machine **must** have the **.NET 8 Desktop Runtime (x64)** installed before running
|
||||
`ClaudeDo.Installer.exe` — this is the actual runtime-preflight gap, not the app/worker.
|
||||
|
||||
**How to check on a target machine**:
|
||||
- `dotnet --list-runtimes` — look for a `Microsoft.WindowsDesktop.App 8.0.x` line (Desktop
|
||||
Runtime, required by the installer). Requires the `dotnet` CLI itself to be on PATH; not
|
||||
guaranteed present on a fresh machine that never installed the SDK, only the runtime — in
|
||||
that case `dotnet` may not exist at all even though the runtime DLLs do.
|
||||
- Registry fallback (works even without the `dotnet` CLI on PATH):
|
||||
`HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App` —
|
||||
each installed version is a subkey/value here. This is the standard documented detection
|
||||
mechanism for .NET Desktop Runtime presence on Windows and is what most installer-detection
|
||||
tooling (e.g. Squirrel, WiX bundles) uses instead of shelling out to `dotnet`.
|
||||
- **Not independently verified in this task**: the exact registry key shape wasn't inspected
|
||||
live (would require reading `HKLM\SOFTWARE\dotnet\...` on this machine, which is standard
|
||||
.NET installer-detection convention, but out of scope to screenshot/dump here since the task
|
||||
is docs-only and this is a well-documented, non-project-specific Windows convention).
|
||||
**Not implemented** as an `IEnvironmentCheck` — none of the shipped checks verify the Desktop
|
||||
Runtime; if the Installer itself is running at all, .NET 8 Desktop Runtime is implicitly
|
||||
present (framework-dependent publish would otherwise fail to launch).
|
||||
|
||||
## Beschlossene Konstanten
|
||||
|
||||
| Constant | Value | Confidence |
|
||||
|---|---|---|
|
||||
| Minimum CLI version | `2.1.220` | **Verified-floor, not a proven minimum.** This is the newest version confirmed installed and working end-to-end on a dev machine for every flag ClaudeDo uses. No lower true minimum could be sourced (see §3) — treat any lower value as a guess. |
|
||||
| Credentials file path | `%USERPROFILE%\.claude\.credentials.json` (Windows) | Sourced from official docs; content/schema not inspected (hard-blocked secrets file). |
|
||||
| Login-check command | `claude auth status --json` | Verified locally, instant, no model call. Fields: `loggedIn`, `authMethod`, `apiProvider`, `email`, `orgId`, `orgName`, `subscriptionType`. |
|
||||
| App/Worker runtime requirement | **None** — self-contained win-x64 publish | Sourced from `.gitea/workflows/release.yml`. |
|
||||
| Installer runtime requirement | **.NET 8 Desktop Runtime (x64)** must be pre-installed | Sourced from `.gitea/workflows/release.yml` comment + `ClaudeDo.Installer.csproj`. |
|
||||
|
||||
## Erkennungsstrategie pro Check
|
||||
|
||||
| Check | Type | Command | Expected pass output | Implemented as |
|
||||
|---|---|---|---|---|
|
||||
| git present + version | static | `git --version` (via `ExecutableResolver`) | resolves, exit 0 | `GitCheck` (Error) |
|
||||
| git identity set | static | `git config --get user.name` / `user.email` | both non-empty | `GitIdentityCheck` (Warning) |
|
||||
| install dir + data dir writable | static | probe-file write/delete | succeeds | `WriteAccessCheck` (Error) |
|
||||
| SignalR/ExternalMcp ports free | static | `TcpListener` bind probe + owning-process lookup | free, or owned by running `ClaudeDo.Worker` | `PortCheck` (Warning) |
|
||||
| CLI present + version | static | `claude --version` | `X.Y.Z (Claude Code)`; parse and compare `X.Y.Z >= 2.1.220` | `ClaudeCliCheck` (Error) / `ClaudeVersionCheck` (Error) |
|
||||
| `auto` recognized as a flag value | static | `claude --help` (or trigger the parse error path) | `--permission-mode <mode>` help text lists `auto` among the choices | `PermissionModeAutoCheck` (Warning) |
|
||||
| CLI logged in | static/cheap | `claude auth status --json` | exit 0, `loggedIn: true` | `ClaudeAuthCheck` (Error) |
|
||||
| Auto mode actually eligible (org/model/plan) | **not statically detectable** | none exists | N/A — see §2; don't build this, let a real task run surface a fast startup rejection instead | not implemented (by design) |
|
||||
| .NET Desktop Runtime present (installer only) | static | `dotnet --list-runtimes` (if `dotnet` on PATH) or registry `HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App` | a `Microsoft.WindowsDesktop.App 8.0.x` entry exists | not implemented (see §5) |
|
||||
| App/Worker runtime present | **not needed** | — | self-contained, nothing to check | not implemented (not needed) |
|
||||
|
||||
## Not verifiable (explicit)
|
||||
|
||||
- The actual root cause on the colleague's machine — no diagnostic data was captured from it.
|
||||
- Exact stderr/exit-code wording the CLI prints when auto mode is rejected at startup for an
|
||||
ineligible org/model (docs state the *behavior* — "rejects `--permission-mode auto` at
|
||||
startup" — but not the literal message; this machine's account is eligible, so it couldn't be
|
||||
reproduced locally).
|
||||
- A true (not just "newest confirmed") minimum SemVer for `--effort`, `--agents`,
|
||||
`--json-schema`, `--append-system-prompt`, `--resume`, `claude mcp add --transport http
|
||||
--scope user` — all predate the retrievable changelog window.
|
||||
- Live inspection of the `HKLM\SOFTWARE\dotnet\Setup\InstalledVersions` registry shape on this
|
||||
machine (documented Windows convention, not independently screenshotted here).
|
||||
@@ -0,0 +1,239 @@
|
||||
# Review, merge & conflict resolution
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against `2f3f938` (2026-08-07), which finished the diff-viewer rework:
|
||||
> 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.
|
||||
|
||||
Covers the review→merge path: `TaskStateService` review transitions, `TaskMergeService`,
|
||||
`PlanningMergeOrchestrator`, the post-merge verify gate, and the UI conflict resolver.
|
||||
|
||||
## Approve = merge the whole unit
|
||||
|
||||
`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`.
|
||||
- **No active worktree** (sandbox run) → straight to `Done`.
|
||||
|
||||
Review transitions all live in `TaskStateService`: `SubmitForReviewAsync`,
|
||||
`SubmitForChildrenAsync`, `ApproveReviewAsync`, `RejectToQueueAsync`, `RejectToIdleAsync`,
|
||||
`ClearReviewFeedbackAsync`.
|
||||
|
||||
`ReviewFeedback` (nullable string on `TaskEntity`) is the reviewer's rejection comment: set by
|
||||
`RejectToQueueAsync`, consumed and cleared by `QueueService` on the next re-run, where it
|
||||
becomes the next-turn prompt of the resumed Claude session.
|
||||
|
||||
## Unified parent model
|
||||
|
||||
Every parent — planning **or** improvement — flows
|
||||
`… → WaitingForChildren → WaitingForReview → Done`, advanced by the single
|
||||
`TaskStateService.TryAdvanceParentAsync`. It surfaces any `WaitingForChildren` parent for
|
||||
review once all children are terminal; failed/cancelled children are **annotated on the
|
||||
result, not wedged**.
|
||||
|
||||
- A planning parent enters `WaitingForChildren` at `FinalizePlanningAsync` (or
|
||||
`WaitingForReview` directly if it has no children).
|
||||
- An improvement parent enters it from `TaskRunner.HandleSuccess` when its run spawned children.
|
||||
- Planning/improvement **children** go straight to `Done` — no individual review. Only the
|
||||
parent is reviewed.
|
||||
|
||||
A child that hits a roadblock (fails, or reports `CLAUDEDO_BLOCKED` roadblocks) does **not**
|
||||
advance the parent — the parent stays in `WaitingForChildren` until every child is terminal.
|
||||
The UI surfaces blocked children on the parent's Session tab (`ChildOutcomes` + a "children
|
||||
need attention" band) so the roadblock is visible without forcing a transition.
|
||||
|
||||
A blocked planning/improvement child still goes straight to `Done` per the unified parent model
|
||||
above — it committed nothing, but nothing prevents its (empty) branch from being unit-merged
|
||||
like any other `Done` child once the parent is approved. The MCP surface has no UI equivalent of
|
||||
`ChildOutcomes`, so `review_task`'s approve on a parent additionally returns `emptyChildren` (the
|
||||
`Done` children whose review range is empty) and every task-returning tool exposes
|
||||
`TaskRefDto.roadblockCount` → [external-mcp.md](external-mcp.md) → `ReviewTask`/`PreviewMerge`.
|
||||
An empty branch is still mergeable by design (some tasks — e.g. an audit — legitimately produce
|
||||
no diff); this is a visibility fix, not a merge gate.
|
||||
|
||||
## Cancel is blocked while a unit merge is draining
|
||||
|
||||
`ApproveReview` on a task with children awaits `PlanningMergeOrchestrator.StartAsync` /
|
||||
`DrainAsync` synchronously — the parent sits in `WaitingForReview` for the whole (potentially
|
||||
minutes-long, one-child-at-a-time) drain. Without a guard, a concurrent `CancelReview` (UI or
|
||||
`update_task_status`/`cancel_task` via MCP) could flip the parent to `Cancelled` mid-drain while
|
||||
the orchestrator kept merging children's worktrees onto the target branch; `FinalizeParentDoneAsync`
|
||||
then finds the parent no longer `WaitingForReview` and gives up, leaving the merged children's
|
||||
diffs stranded with no rollback.
|
||||
|
||||
`TaskStateService.CancelAsync` now rejects with a `TransitionResult` reason whenever
|
||||
`PlanningMergeOrchestrator.HasActiveMerge(taskId)` is true for the task being cancelled, before any
|
||||
DB write. Cycle note: `TaskStateService` can't take a direct constructor dependency on
|
||||
`PlanningMergeOrchestrator` (which itself depends on `ITaskStateService`), so it takes a lazily-resolved
|
||||
`Func<IActiveMergeState>` instead — same cycle-breaking shape as the existing `Func<ITaskStateService>`
|
||||
handed to `PlanningChainCoordinator`. `IActiveMergeState` (`Planning/Interfaces/`) is `PlanningMergeOrchestrator`'s
|
||||
only public surface `TaskStateService` needs.
|
||||
|
||||
The UI mirrors this as polish: `DetailsIslandViewModel.IsMergeDraining` (set on
|
||||
`PlanningMergeStartedEvent`, cleared on `PlanningMergeAborted`/`PlanningCompleted` for the bound
|
||||
task) gates `CancelReviewCommand`'s `CanExecute` — same shape as `WorktreesOverviewModalViewModel.IsMerging`
|
||||
gating `CanMergeAll`. The worker-side guard remains the actual correctness fix; the button gate
|
||||
just avoids inviting a click the worker would reject. `CancelReviewAsync`'s catch now raises
|
||||
`ErrorReported` (→ shell `FlashFooterError`) instead of swallowing the rejection silently.
|
||||
|
||||
## Post-merge verify gate
|
||||
|
||||
A list can set `ListConfigEntity.VerifyCommand` (List Settings modal → Verification).
|
||||
Null/blank (the default) = **no gate**, behavior bit-identical to before the feature existed.
|
||||
|
||||
When set, `TaskMergeService` runs it via `VerifyCommandRunner` (`cmd.exe /c <command>`,
|
||||
10-minute fixed timeout, output tail-captured) in `list.WorkingDir` right after a successful
|
||||
`MergeNoFfAsync` / `ContinueMergeAsync` **and** worktree cleanup, but **before** the task is
|
||||
allowed to reach `Done`.
|
||||
|
||||
| Outcome | Effect |
|
||||
|---|---|
|
||||
| Exit 0 | Unchanged flow — worktree `Merged`, task `Done` if it was `WaitingForReview`. |
|
||||
| Non-zero exit or timeout | The git merge is **deliberately left in place** (no auto-revert — that's a separate, unbuilt feature). The worktree is still marked `Merged` (it's already gone from disk when `removeWorktree` was requested), but the task stays out of `Done`. |
|
||||
|
||||
On failure `MergeResult.Status` comes back `TaskMergeService.StatusVerifyFailed`
|
||||
(`"verify_failed"`) with an output excerpt in `ErrorMessage`. This flows through
|
||||
`MergeResultDto` (hub) and `ReviewTaskResult` (`review_task`) unchanged, because both already
|
||||
treat any non-`blocked`/`conflict` status generically. All three UI merge entry points handle it
|
||||
explicitly (detail-pane Approve, merge modal, worktrees batch) — a generic fallback there showed
|
||||
the raw status string instead of the failure.
|
||||
|
||||
**Worktree-less approvals are gated too.** A task with no active `WorktreeEntity` — a sandbox run,
|
||||
or a list-handler task that commits straight into `list.WorkingDir` — skips the merge entirely,
|
||||
but `ApproveAndMergeAsync` still runs the verify command (same per-repo gate, working dir =
|
||||
`list.WorkingDir`) before the task may reach `Done`. Without that, the run that lands the most on
|
||||
the target branch at once would be the one run nothing checks.
|
||||
|
||||
**Serialization:** a process-wide `ConcurrentDictionary<string, SemaphoreSlim>` keyed by
|
||||
`list.WorkingDir` serializes `MergeAsync` / `ContinueMergeAsync` (git ops + verify) per repo,
|
||||
so a verify run can't be interrupted by a second merge landing in the same working dir
|
||||
mid-build.
|
||||
|
||||
## `MergeCommit` and revert
|
||||
|
||||
`WorktreeEntity.MergeCommit` (nullable) is the SHA of the merge commit this worktree's branch
|
||||
produced on the target branch. Stamped by `TaskMergeService` the moment a merge/continue-merge
|
||||
succeeds, written **only** by `WorktreeRepository.SetMergedAsync` (which atomically sets
|
||||
`State=Merged` and stamps the SHA in one update).
|
||||
|
||||
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.
|
||||
|
||||
## Review gate in the UI
|
||||
|
||||
**Approve & Merge is gated behind opening the diff.** When there is something to inspect
|
||||
(worktree diff / merged range / children combined diff), the button stays disabled until the
|
||||
diff or combined-diff viewer has been opened once. The gate **re-locks per run** — any state
|
||||
change resets it. Tasks with nothing to inspect are never gated.
|
||||
|
||||
The row-level quick-approve in the task list is an **intentional bypass**.
|
||||
|
||||
Implementation: `MergeSectionViewModel` owns merge-target selection, the mergeability
|
||||
indicator (`MergePreviewPresenter` over `PreviewMergeAsync`), and `OpenDiffAsync` /
|
||||
`ReviewCombinedDiffCommand` — both build a `DiffViewerViewModel`, call `ShowDiffViewer`, and
|
||||
fire the `DiffViewed` callback. `HasReviewableDiff` reports whether anything is inspectable
|
||||
and feeds the gate.
|
||||
|
||||
## Conflict resolver (in-app Rider-style 3-pane merge editor)
|
||||
|
||||
`ConflictResolverViewModel` + `Views/Conflicts/ConflictResolverView`. Handles **both**
|
||||
single-task and planning unit-merge conflicts.
|
||||
|
||||
### Model
|
||||
|
||||
Single-task mode starts the conflict merge, then parses each conflicted file into stable and
|
||||
conflict `MergeFileSegment`s via the worker's `GetMergeConflictDocuments`. Types live in
|
||||
`ConflictModels`: `MergeFile` / `MergeFileSegment` / `MergeConflictBlock`.
|
||||
|
||||
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),
|
||||
`Current` / `Next` / `Previous` (focused-conflict nav), a per-file `PositionText` readout,
|
||||
per-block `AcceptOurs/Theirs/Both/Base` + `MergeFile.Compose`, and `CanContinue` gated on
|
||||
**every file resolved + no binary**. Each file is written via `WriteConflictResolution`.
|
||||
|
||||
**Planning mode** via `OpenForPlanningAsync(parentId, subtaskId)` loads the current subtask's
|
||||
mid-merge conflicts **without re-starting the merge** and routes continue/abort to
|
||||
`ContinuePlanningMerge` / `AbortPlanningMerge`, so a unit-merge conflict re-opens the editor
|
||||
per subtask via the `PlanningMergeConflict` broadcast.
|
||||
|
||||
**Except when an MCP session is driving the merge.** `PlanningMergeOrchestrator.StartAsync` takes
|
||||
an `externallyDriven` bool (default `false`; `ExternalMcpService.review_task`'s parent-with-children
|
||||
path passes `true`, since a running Claude session — not the UI — will resolve conflicts via
|
||||
`continue_merge`/`abort_merge`). The flag rides along on the `PlanningMergeConflict` broadcast
|
||||
(4th arg); `IslandsShellViewModel.OnPlanningMergeConflict` only auto-opens the resolver when it's
|
||||
`false` — otherwise it shows a persistent, non-auto-dismissing banner
|
||||
(`IsExternalMergeBannerVisible`) with a manual "Open resolver" button, cleared on
|
||||
`PlanningMergeAborted`/`PlanningCompleted`. This exists because two parties (a human and the
|
||||
driving session) could otherwise end up editing the same shared checkout at once.
|
||||
`PlanningMergeOrchestrator.GetActiveExternalConflictsAsync` (hub: `GetActiveExternalPlanningMergeConflicts`)
|
||||
re-derives this state by checking `GitService.IsMidMergeAsync` rather than trusting the in-memory
|
||||
flag alone, so a UI restart mid-merge (or a stale entry left behind if something resolved the
|
||||
repo outside the normal Continue/Abort path) can't show a phantom banner — the Ui calls it on
|
||||
`ConnectionRestoredEvent`. The childless single-task conflict path
|
||||
(`TaskMergeService.ApproveAndMergeAsync`) has no equivalent broadcast — it only sends the generic
|
||||
`TaskUpdated` — so it never auto-opened the resolver and needed no change.
|
||||
|
||||
### View
|
||||
|
||||
Three **AvaloniaEdit** panes showing the whole file: MAIN/ours (read-only) | editable Result |
|
||||
INCOMING/theirs (read-only). TextMate highlighting by extension (theme `StyleInclude` in
|
||||
`App.axaml`).
|
||||
|
||||
- A code-behind `IBackgroundRenderer` tints each conflict block (unresolved/resolved) across
|
||||
panes. Tints live in `Tokens.axaml` (`Merge*TintBrush`).
|
||||
- An `IReadOnlySectionProvider` + `TextAnchor` regions keep **only conflict spans** editable in
|
||||
Result; edits flow back to the block.
|
||||
- Each unresolved conflict starts **EMPTY** (a thin marker bar).
|
||||
- The between-pane gutter controls **toggle** each side in/out of the result: `›`/`‹` add
|
||||
MAIN/INCOMING in click order (first pick on top), clicking again removes that side — so a
|
||||
conflict can take main, incoming, both, or **neither**.
|
||||
- `FilesSummary` shows how many files still have conflicts. The three panes share a
|
||||
proportional synced vertical scroll.
|
||||
- A conflict overview ruler right of the Result pane (`ConflictMap`) maps every conflict in the
|
||||
file proportionally; click a tick to jump. Useful for long files.
|
||||
|
||||
### Entry points
|
||||
|
||||
Review **Approve** on conflict, and the **Merge** button in the Diff window (a conflicting
|
||||
`MergeTask` hands off via `RequestConflictResolution`).
|
||||
|
||||
## Hub methods
|
||||
|
||||
- Review/merge: `ApproveReview(taskId, targetBranch) -> MergeResultDto`,
|
||||
`ContinuePlanningMerge` / `AbortPlanningMerge`, `PreviewMerge(taskId, targetBranch) ->
|
||||
MergePreviewDto`, `RejectReviewToQueue`, `RejectReviewToIdle`, `CancelReview`, `MergeTask`,
|
||||
`GetMergeTargets`
|
||||
- Single-task conflict resolver: `StartConflictMerge`, `GetMergeConflictDocuments`,
|
||||
`WriteConflictResolution`, `ContinueConflictMerge`, `AbortConflictMerge` — note the
|
||||
service-level `TaskMergeService.ContinueMergeAsync` / `AbortMergeAsync` keep their own names.
|
||||
- Broadcast events: `PlanningMergeStarted`, `PlanningSubtaskMerged`, `PlanningMergeConflict`,
|
||||
`PlanningMergeAborted`, `PlanningCompleted`
|
||||
|
||||
## Diff stack (UI)
|
||||
|
||||
`UnifiedDiffParser` (static) parses `git diff` output into `DiffFileViewModel`s, detecting
|
||||
added/deleted/renamed/binary files and per-line numbers. `DiffModels.cs` holds the shared types
|
||||
(`DiffLineViewModel`, `DiffFileViewModel`, `DiffLineKind`, `DiffFileStatus`, `SubtaskDiffRow`,
|
||||
`DiffTreeNodeViewModel`, `DiffTree`).
|
||||
|
||||
`DiffViewerViewModel` is one unified read-only viewer with two modes:
|
||||
- **Files** — dirty worktree / branch-vs-base / commit-range. Loads via `GitService`, folder
|
||||
file-tree left + per-file diff pane right, Merge button for a live branch source.
|
||||
- **Planning** — per-subtask diffs via `GetPlanningAggregateAsync`, subtask list left + one
|
||||
editor per file right (`PlanningFiles`), combined integration-branch toggle.
|
||||
|
||||
Both modes render through `DiffAlignment` (pure — pairs diff lines into side-by-side rows and
|
||||
computes word-diff spans) and `DiffTextView` (AvaloniaEdit + TextMate highlighting keyed off the
|
||||
file extension, unified/split layout, optional line wrap, synced scrolling). Files mode hosts one
|
||||
`DiffTextView` for the selected file; Planning mode hosts one per file in `PlanningFiles` so each
|
||||
gets its own grammar (one editor can only carry one TextMate grammar). The split/wrap toggles
|
||||
persist to `ui.config.json` via `AppSettings` and reach the per-file editors in Planning mode
|
||||
too.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Usage monitoring, gate & throttle
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `f6cb825` (2026-08-05), plus the uncommitted per-bucket-throttle /
|
||||
> draggable-gauge change of 2026-08-06 (this note already describes that newer state).
|
||||
> Drift check: `git log --oneline f6cb825..HEAD -- src/ClaudeDo.Worker/Usage src/ClaudeDo.Worker/Queue src/ClaudeDo.Ui/ViewModels/UsagePillViewModel.cs`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
Covers `src/ClaudeDo.Worker/Usage/`, the queue's throttle/gate integration, per-run token
|
||||
accounting, and the UI surfaces (usage pill + usage monitor modal).
|
||||
|
||||
## Data source
|
||||
|
||||
`GET https://api.anthropic.com/api/oauth/usage` — an **undocumented** Anthropic endpoint,
|
||||
authenticated with the Bearer access token Claude Code itself keeps fresh at
|
||||
`~/.claude/.credentials.json`. ClaudeDo reads that token, never refreshes it, never logs it.
|
||||
|
||||
Because the endpoint is undocumented and can change without notice, **every consumer
|
||||
fails open**. That is the single most important invariant here.
|
||||
|
||||
## Components (`Usage/`)
|
||||
|
||||
| Type | Role |
|
||||
|---|---|
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `TranscriptUsageReader` | Aggregates token usage from Claude Code transcripts. |
|
||||
|
||||
Interfaces in `Usage/Interfaces/`: `IUsageClient`, `ITranscriptUsageReader`, `IUsageGate`.
|
||||
|
||||
## The gate (hard pause)
|
||||
|
||||
Thresholds: `AppSettings.UsageGateFiveHourPct` / `UsageGateSevenDayPct` (defaults 80/90).
|
||||
Blocked once `five_hour >= UsageGateFiveHourPct` **or** `seven_day >= UsageGateSevenDayPct`
|
||||
(`>=`, not `>`). Threshold `0` = that bucket never gates.
|
||||
|
||||
What it pauses: **only the queue's slot-fill loop** — new queued tasks don't start.
|
||||
Unaffected: already-running runs, `RunNow`, `ContinueTask`, interactive ConPTY sessions,
|
||||
planning sessions, daily prep (all bypass the queue).
|
||||
|
||||
**Fail-open**: no snapshot yet, a failed last poll, or an app-settings read error all
|
||||
resolve to not-blocked.
|
||||
|
||||
There is **no persistent pause state**. Recovery is just the queue's 30 s backstop timer
|
||||
(`queue_backstop_interval_ms`) re-evaluating the gate on its own once usage drops back
|
||||
under the threshold. A blocked↔free transition is logged and broadcast (`WorkerLog`, Warn
|
||||
on block / Info on resume) exactly **once per change**, not every tick.
|
||||
|
||||
## The throttle (staged parallelism)
|
||||
|
||||
`UsageThrottle.EffectiveSlots(configuredSlots, fiveHourPct, fiveHourThresholds, sevenDayPct,
|
||||
sevenDayThresholds)` — pure static, no state. `UsageThresholds(SoftPct, HardPct, GatePct)` is the
|
||||
per-bucket triple (same file).
|
||||
|
||||
Thresholds are **per bucket** (`usage_throttle_five_hour_{soft,hard}_pct` /
|
||||
`usage_throttle_seven_day_{soft,hard}_pct`, defaults 50/65 each) because the 5h and 7d windows fill
|
||||
at very different rates. Each bucket is staged independently and the **strictest** bucket wins —
|
||||
not "whichever is more utilized", so a bucket that is lower but tightly configured can be the one
|
||||
that throttles:
|
||||
|
||||
| Utilization (per bucket) | That bucket's slots |
|
||||
|---|---|
|
||||
| below soft | full configured `max_parallel_executions` |
|
||||
| `>= softPct` | capped at 2 |
|
||||
| `>= hardPct` | capped at 1 |
|
||||
| `>= gatePct` | 0 — same hard block as `UsageGate` |
|
||||
|
||||
A threshold of `0` disables that stage for that bucket, and a bucket with no reading (null) never
|
||||
throttles. The `0` return is deliberately kept in sync with `UsageGate`'s hard block because both
|
||||
read the same gate thresholds — change one, change both.
|
||||
|
||||
Only **new** slot fills are affected; a run already occupying a slot when the stage tightens
|
||||
runs to completion. Same fail-open policy: no snapshot means no throttling.
|
||||
|
||||
The effective stage (configured vs. effective slots + the decisive bucket, `ThrottleBucket`
|
||||
= `"five_hour"` / `"seven_day"`) rides along on `UsageSnapshotDto` purely for UI display.
|
||||
It does **not** change what the gate gates on.
|
||||
|
||||
## Queue integration (`Queue/QueueService`)
|
||||
|
||||
Per loop tick:
|
||||
|
||||
1. `GetEffectiveMaxParallelAsync` reads `AppSettings.MaxParallelExecutions` and steps it
|
||||
down via `UsageThrottle.EffectiveSlots` against the current `UsageState` snapshot.
|
||||
A missing/failed snapshot fails open to the configured value. A **stage change** (not
|
||||
every tick) logs once.
|
||||
2. Separately, `IUsageGate.EvaluateAsync` — if blocked, the slot-fill loop is skipped
|
||||
entirely for that tick.
|
||||
|
||||
## Per-run token accounting
|
||||
|
||||
`task_runs` stores four raw token fields: `tokens_in` / `tokens_out` /
|
||||
`cache_read_tokens` / `cache_write_tokens`.
|
||||
|
||||
These are **not** read from the stream-json `result` event's `usage.input_tokens` — that is
|
||||
only the uncached remainder of a single API call and undercounts the real prompt size by
|
||||
orders of magnitude once caching kicks in.
|
||||
|
||||
Instead `TaskRunner.ApplyUsageAsync` calls
|
||||
`ITranscriptUsageReader.ReadSessionTotalsAsync(sessionId)` — the session transcript's
|
||||
cumulative raw totals across every assistant message, located by `{sessionId}.jsonl` — and
|
||||
stores the **delta** against prior `task_runs` rows sharing the same `session_id`, so a
|
||||
`--resume`'d run doesn't double-count turns already billed to an earlier run.
|
||||
|
||||
A missing/unreadable transcript leaves all four fields `null`; it never fails the run.
|
||||
|
||||
### `TranscriptUsageReader` details
|
||||
|
||||
Reads `~/.claude/projects/**/*.jsonl`, aggregating by date / model / scope (ClaudeDo vs
|
||||
Other), deduped by `requestId`, with a per-file length+mtime cache.
|
||||
|
||||
`ReadAsync` **skips any file whose mtime predates the window start minus one day** — it cannot hold
|
||||
a record inside the range, and the full history is large (measured 2026-08-06: 501 files / 230 MB /
|
||||
77k lines ≈ 1.7 s to parse cold; a 7-day range touches ~190 files / ~106 MB). The one-day slack
|
||||
absorbs local-vs-UTC skew between mtime and record timestamps. `ReadSessionTotalsAsync` is
|
||||
unaffected — it looks up a single `{sessionId}.jsonl`.
|
||||
|
||||
`<synthetic>`-model lines are skipped **everywhere** — they are not real API calls.
|
||||
|
||||
## UI surfaces
|
||||
|
||||
- **`UsagePillViewModel`** — one shared instance backs the `UsagePill` control in both the
|
||||
footer and the Mission Control header. Loads via `GetUsageSnapshotAsync`, updates live off
|
||||
`IWorkerClient.UsageUpdatedEvent`. Dot state priority is mutually exclusive:
|
||||
**blocked > stale > warn > normal**. `IsThrottled` (effective slots below configured, and
|
||||
not gate-blocked) adds a tooltip line naming effective/configured slots + decisive bucket.
|
||||
The pill's click handler (`IslandsShellViewModel.OpenUsageMonitor`) **shows the window before
|
||||
loading** (`BeginLoad`) — awaiting the load first made the pill feel like a dead click, because
|
||||
the first `GetModelUsage` per worker process scans the whole transcript history.
|
||||
- **Draggable stage markers** — each of the two real gauges carries three markers (soft/hard/gate).
|
||||
`UsageGaugeBar` (`Views/Controls`) draws them against its own width and does the pointer work;
|
||||
the math is a pure static, `UsageThresholdDrag` (in the modal VM's file), which keeps
|
||||
soft ≤ hard ≤ gate and treats a neighbour of `0` as off. Release fires the row's
|
||||
`CommitCommand` → read-modify-write via `GetAppSettings` + `UpdateAppSettings`, so only the
|
||||
dragged bucket's three fields change. Plan-dependent `weekly_scoped` gauges are read-only.
|
||||
- **Legend = numeric editor.** Under each adjustable bar sit three legend rows whose colour swatches
|
||||
match the markers (soft `TextDimBrush`, hard `StatusReviewBrush`, gate `StatusErrorBrush`), each
|
||||
with a `NumericUpDown`. `NumericUpDown` has no commit command, so the box's `Tag`
|
||||
(`soft`/`hard`/`gate`) plus two code-behind handlers (`LostFocus`, Enter) call the row's
|
||||
`CommitSoft`/`CommitHard`/`CommitGate` command. Those run the typed value through the **same**
|
||||
`UsageThresholdDrag.Apply` clamp as a drag, so a box can't invert the order and only the edited
|
||||
stage moves. ⚠️ The `KeepLastNumber` converter is mandatory on those bindings — see the
|
||||
`NumericUpDown` null gotcha in `src/ClaudeDo.Ui/CLAUDE.md`.
|
||||
Rows are updated **in place** on each snapshot (keyed by limit kind) so a poll landing mid-drag
|
||||
doesn't replace the bound instance.
|
||||
- **`UsageMonitorModalViewModel`** — opened from the pill. Renders one gauge **per row** in
|
||||
`UsageSnapshotDto.Limits` — deliberately **dynamic**, because the `seven_day_opus` /
|
||||
`seven_day_sonnet`-style buckets the raw API returns are plan-dependent and come back
|
||||
`null` on plans that don't have them; a fixed gauge layout would break. Also shows model
|
||||
usage (`GetModelUsageAsync`, ClaudeDo-vs-Other split per model) and top-task usage
|
||||
(`GetTaskUsageAsync`) over a 7d/30d preset or custom range.
|
||||
|
||||
## Hub surface
|
||||
|
||||
- `GetUsageSnapshot() -> UsageSnapshotDto` — percentages/limits/`FetchedAtUtc` are null and
|
||||
`IsStale=true` when no snapshot has landed yet. `IsStale` also trips on a failed last poll
|
||||
or a snapshot older than 3× `usage_poll_interval_seconds`.
|
||||
- `GetModelUsage(from, to)` — thin wrapper over `ITranscriptUsageReader.ReadAsync`.
|
||||
- `GetTaskUsage(from, to)` — top consumers from `task_runs` joined to task/list, grouped per
|
||||
task. Null token columns count as **0**, never drop the row. `Model` comes from that task's
|
||||
most recent run. Sorted by total tokens descending, capped at 100.
|
||||
- `UsageUpdated` event carries the same `UsageSnapshotDto`.
|
||||
|
||||
## Settings columns
|
||||
|
||||
`app_settings`: `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` (80/90),
|
||||
`usage_throttle_five_hour_{soft,hard}_pct` / `usage_throttle_seven_day_{soft,hard}_pct` (50/65 per
|
||||
bucket). All six clamped 0..100 by `AppSettingsRepository.UpdateAsync`, which does **not** enforce
|
||||
soft ≤ hard ≤ gate — the ordering is a UI-side drag constraint, and an out-of-order stored config
|
||||
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
|
||||
gauge-only — `SettingsModalViewModel` therefore carries them load→save verbatim so saving Settings
|
||||
can't reset a dragged value.
|
||||
@@ -1,6 +1,6 @@
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `896d4b5` (2026-07-23).
|
||||
> Drift check: `git log --oneline 896d4b5..HEAD -- src/ClaudeDo.Worker`
|
||||
> Last verified against commit `cc90600` (2026-08-06).
|
||||
> Drift check: `git log --oneline cc90600..HEAD -- src/ClaudeDo.Worker`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
# Worker: Task Execution Pipeline
|
||||
@@ -28,9 +28,11 @@ How a task moves Queued → Running → terminal, across `src/ClaudeDo.Worker`
|
||||
|
||||
5. **Run Preparation** — `TaskRunner.RunAsync()` (Runner/TaskRunner.cs)
|
||||
- Loads task, list config, subtasks, attachments from the DB.
|
||||
- `StartRunningAsync()` (only if not pre-claimed): atomic claim to Running, **before any
|
||||
resource is created**. A rejected claim (task already Running) bails out immediately —
|
||||
no worktree, no MCP token file. Broadcasts TaskStarted.
|
||||
- `PrepareRunDirectoryAsync()`: worktree (via WorktreeManager) if the list has a WorkingDir,
|
||||
else sandbox. Generates a per-run MCP token, writes MCP config to disk.
|
||||
- `StartRunningAsync()` (only if not pre-claimed): atomic Queued → Running. Broadcasts TaskStarted.
|
||||
|
||||
6. **Claude Execution** — `TaskRunner.RunOnceAsync()` (Runner/TaskRunner.cs)
|
||||
- Creates a TaskRunEntity, points the task at the run's log path.
|
||||
@@ -50,9 +52,47 @@ How a task moves Queued → Running → terminal, across `src/ClaudeDo.Worker`
|
||||
- **Done** CompleteAsync (Running → Done) — top-level success.
|
||||
- **WaitingForReview** SubmitForReviewAsync (Running → WaitingForReview) — review gate.
|
||||
- **WaitingForChildren** SubmitForChildrenAsync (Running → WaitingForChildren) — blocks on children.
|
||||
Advances to WaitingForReview via `TryAdvanceParentAsync` once every remaining child is
|
||||
terminal (Done/Failed/Cancelled) — including zero children left, e.g. after the last child
|
||||
is deleted (`WorkerHub.DeleteTask` / `ExternalMcpService.DeleteTask` both call it).
|
||||
- **Failed** FailAsync (Running/Queued → Failed).
|
||||
- **Cancelled** CancelAsync (Running/Queued/WaitingForReview/WaitingForChildren → Cancelled).
|
||||
|
||||
## Model, effort & max-turns resolution
|
||||
|
||||
*(section added at commit `f6cb825`, 2026-08-05; resolver extraction added same day)*
|
||||
|
||||
The resolution below lives in `Runner/EffectiveRunConfigResolver.Resolve` (not inlined in
|
||||
`TaskRunner` anymore) so `TaskRunner.ResolveConfigAsync` and the read-only
|
||||
`get_effective_run_config` MCP tool (`External/ConfigMcpTools.cs`) share one codepath and can't
|
||||
report different numbers for the same task. The tool additionally surfaces, per field, whether
|
||||
it came from the task/list/preset/global layer, and — for max turns — the raw requested value
|
||||
plus whether it was clamped.
|
||||
|
||||
Step 6 builds the CLI args. Model and turn budget resolve like this:
|
||||
|
||||
1. **Effective model** — task override → list config → `AppSettings.DefaultModel`.
|
||||
2. **Preset row** — `ModelPresets.For(global.ModelPresets, model, global.DefaultMaxTurns)`.
|
||||
The model string is resolved through `ModelRegistry.TryNormalizeAlias` **first**, so a full
|
||||
CLI model id (e.g. `claude-sonnet-4-6`, not just the bare `sonnet`/`opus`/`haiku`/`fable`
|
||||
aliases) still hits its alias's preset row instead of missing every lookup. Only a model
|
||||
that normalizes to nothing recognized falls back to a synthesized row using
|
||||
`AppSettings.DefaultMaxTurns` — **never a hardcoded number, and it never throws**: an
|
||||
unknown model must not block a run.
|
||||
3. The preset supplies `--effort` and the **global** max-turns default. Task/list `MaxTurns`
|
||||
overrides still win over it.
|
||||
4. **Ceiling clamp** — `TaskRunner.ResolveMaxTurns` hard-clamps the resolved value to
|
||||
`AppSettings.MaxTurnsCeiling` (default 80). An override above the ceiling still starts, just
|
||||
capped, and a Warn logs the task id + requested + effective value.
|
||||
|
||||
⚠️ **Trap:** if `app_settings.model_presets` is somehow null, the fallback path decides the turn
|
||||
budget — which is why `AppSettingsRepository.GetAsync` backfills shipping defaults on the first
|
||||
read after null. Ship preset turns are low (haiku 20, sonnet 30, opus 40, fable 25), so a task
|
||||
that genuinely needs a long run must set its own `MaxTurns`.
|
||||
|
||||
Prompt composition: `TaskPromptComposer.Compose` injects attachment **absolute paths** as a
|
||||
read-only "## Reference files" section.
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
**Queue/**
|
||||
@@ -73,14 +113,18 @@ How a task moves Queued → Running → terminal, across `src/ClaudeDo.Worker`
|
||||
- `TaskStateService` — all task status transitions; guards preconditions; signals queue/hub.
|
||||
|
||||
**Lifecycle/** (startup recovery)
|
||||
- `StaleTaskRecovery` — tasks stuck Running after a crash/restart → Failed.
|
||||
- `StaleTaskRecovery` — tasks stuck Running after a crash/restart → Failed. The underlying
|
||||
`TaskStateService.RecoverStaleRunningAsync` bulk-flips Running→Failed, then re-runs the same
|
||||
chain/parent-advance side effects as a normal `FailAsync` (per recovered id, best-effort) so a
|
||||
crash mid-chain-child or mid-improvement-child doesn't leave a successor blocked forever or a
|
||||
`WaitingForChildren` parent wedged.
|
||||
- `OrphanRecovery` — dequeues children whose parent is no longer planning (stays attached).
|
||||
- `AttachmentOrphanRecovery` — cleans orphaned attachment files.
|
||||
- `TaskResetService` — manual reset to Idle.
|
||||
- `TaskMergeService` — conflict resolution for worktree merges.
|
||||
|
||||
**Hub/**
|
||||
- `HubBroadcaster` — single SignalR broadcast point (TaskStarted/TaskUpdated/TaskMessage/RunCreated…).
|
||||
- `HubBroadcaster` — single SignalR broadcast point (TaskStarted/TaskUpdated/TaskMessage/WorktreeUpdated…).
|
||||
- `WorkerHub` — SignalR hub + client methods.
|
||||
|
||||
**Agents/**
|
||||
@@ -113,6 +157,15 @@ Program.cs (DI setup)
|
||||
- **Slot limit** — respects MaxParallelExecutions; a backstop timer wakes even if a Wake() is missed.
|
||||
- **Pre-claimed tasks** — the dispatcher pre-claims via the picker; the override slot
|
||||
(RunNow/ContinueTask) must call StartRunningAsync if a task is not pre-claimed.
|
||||
- **Claim before create** — `TaskRunner.RunAsync`'s unclaimed path calls `StartRunningAsync`
|
||||
*before* `PrepareRunDirectoryAsync`. RunNow racing the picker for the same Queued row used to
|
||||
create the worktree first and only claim afterwards, so the losing dispatch could hit
|
||||
WorktreeManager's branch-collision self-heal and force-remove the winner's live worktree
|
||||
mid-run. `OverrideSlotService.RunNow` also fast-rejects a task already Running in the DB
|
||||
(defense in depth; the picker's atomic SQL claim is the real arbiter either way).
|
||||
`RunCancellationRegistry.Register` refuses (and logs) a second registration for the same task
|
||||
id instead of silently overwriting the first, so a losing dispatch's cleanup can't unregister
|
||||
the winner's CTS out from under it.
|
||||
- **Terminal writes** — use `CancellationToken.None`; a task is never left Running after crash/cancel.
|
||||
- **Per-run MCP tokens** — each run gets a unique token scoping tool access; unregistered on end.
|
||||
- **Auto-retry** — one automatic retry if a session exists and the first run failed.
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# Handoff — List-handler run on list "Claude do", 2026-08-05
|
||||
|
||||
> **✅ COMPLETED 2026-08-05 (follow-up session).** Everything in §1 and §2 is merged and `main`
|
||||
> verified green. Read §0 below before §1–§8 — §5's diagnosis turned out to be **wrong** and the
|
||||
> rest is now history. Still nothing pushed.
|
||||
|
||||
Repo: `C:\Private\ClaudeDo` · List id: `5f973815-050a-4136-94f0-1506a5d4560a` · Branch: `main` (nothing pushed)
|
||||
|
||||
---
|
||||
|
||||
## 0. What the follow-up session did (and what §5 got wrong)
|
||||
|
||||
**All merged, `main` green after every step** (Worker 811/811, Data 143/143, Ui 292/292,
|
||||
Localization 16/16, builds 0 new warnings):
|
||||
|
||||
| Merged | Task | Merge commit |
|
||||
|---|---|---|
|
||||
| §1 #1 | `9e307199` revert_merge + merge-SHA persistence | `3e7126b` |
|
||||
| §1 #2 | `0b2fbb48` post-merge verification gate | (conflict-resolved) |
|
||||
| §1 #3 | `8c1c2130` roadblock reply box | `519ea5a` |
|
||||
| §2 #42 | `c1df5b9a` Hub surface for usage | `6e2158d` |
|
||||
| §2 #43 | `f74b44d9` Usage pill | `7661129` |
|
||||
| §2 #45 | `06068810` gate thresholds in settings | `5115cfc` |
|
||||
| §2 #44 | `82488d2a` Usage Monitor modal | `338fc39` |
|
||||
| §2 #46 | `9c8cffe0` docs | `5872666` |
|
||||
| parent | `439a4daf` Usage Monitor unit | approved → Done (empty unit merge; all children already `Merged`) |
|
||||
|
||||
Plus one hand-fix on `main`: `677a4c1` — `UsagePillViewModelTests` never set `Loc.Current`
|
||||
(defaults to a key-echo localizer) and only passed because another test class happened to
|
||||
install a real `Localizer` first; #44's new tests changed the ordering and broke it on `main`.
|
||||
Classic "both branches green, `main` red".
|
||||
|
||||
### §5 is wrong — `"exited with code 1 and no result"` is NOT (only) a CLI crash
|
||||
|
||||
It is a **catch-all** hiding at least three causes. The truth is in the run log's last NDJSON
|
||||
line (`{"type":"result", …}` → `terminal_reason` / `errors` / `result`):
|
||||
|
||||
- **`max_turns`** — what actually killed #40 and #42. `app_settings.model_presets` is `NULL`, so
|
||||
`ModelPresets.Parse` falls back to the shipping defaults, and **sonnet's default is 30 turns**.
|
||||
`AppSettings.DefaultMaxTurns` (100) is only the fallback for an *unrecognized* alias, so it
|
||||
never applies. Every task without an explicit `maxTurns` override got 30 turns.
|
||||
→ Fix used here: `set_task_config(taskId, model="sonnet", maxTurns=200)` before queueing.
|
||||
- **`api_error`** + `"You've hit your session limit · resets 1pm (Europe/Berlin)"` — the account's
|
||||
5-hour limit, which took out #43's first attempt. Nothing to fix; wait for the reset, re-queue.
|
||||
- Genuine process death — the case §5 describes.
|
||||
|
||||
§5's *operational* advice still holds and is what saved #42: **never `reset_failed_task`** on one
|
||||
of these; check the worktree, build/test it, then set the task `Queued` so the agent resumes its
|
||||
own session and commits. #42's worktree held a complete green implementation (773/773).
|
||||
|
||||
Follow-up tasks: `ca6e55c0` (surface the real failure reason) is new; the turn-budget/presets side
|
||||
is already covered by the Idle task `2de2f008` (`b0317ec7` fixed only the unknown-alias half).
|
||||
|
||||
### Still open
|
||||
|
||||
- **Visual passes** (nobody has looked at these in a running app): usage pill in the footer *and*
|
||||
the Mission Control header; Usage Monitor modal (gauges, tables, stale/blocked bands, dark/light);
|
||||
the roadblock reply box; the verify-command field in the List Settings modal. See `docs/open.md`.
|
||||
- **`~/.todo-app/prompts/planning.md` still shadows the compiled default** (§7.1) — unchanged.
|
||||
- **`wait_for_task_change` is merged but not in the running Worker**, so it isn't callable over MCP
|
||||
until the Worker is restarted. §3's sqlite poll was used instead.
|
||||
- One rough edge in the verify gate: if the verify command fails, the worktree has already been
|
||||
removed and its state set `Merged` while the task stays `WaitingForReview` — re-approving is then
|
||||
refused. Recovery is `update_task_status(..., "Done")` once `main` is fixed.
|
||||
|
||||
Predecessor session ran the five-phase list handler over 11 briefed tasks and, along the way,
|
||||
absorbed the 9-child "Usage Monitor" unit. **Phases 0–3 are complete for the brief.** What is
|
||||
left is Phase 4 (review + merge) for three tasks, plus the Usage chain.
|
||||
|
||||
---
|
||||
|
||||
## 1. Do this first — three brief tasks sit in WaitingForReview
|
||||
|
||||
Merge in **this order** (the order was chosen with the user and matters):
|
||||
|
||||
| # | Task | Id | Note |
|
||||
|---|------|----|------|
|
||||
| 1 | Feat: Merge zurücknehmen — Merge-Commit festhalten + `revert_merge` | `9e307199-2eca-4eb7-9057-d2c675cc57ca` | Migration + new tool. Merge **before** #2 |
|
||||
| 2 | Feat: Verifikations-Gate nach dem Merge | `0b2fbb48-d44c-4155-8c21-d3464c0bd5c2` | Depends on #1's merge-SHA persistence; both edit `TaskMergeService.cs` |
|
||||
| 3 | Feat: Antwortfeld auf der Roadblock-Karte | `8c1c213004574c4fad6beb75b84b70d7` | UI + localization (en **and** de) |
|
||||
|
||||
For each one:
|
||||
|
||||
1. `get_task_diff(taskId, stat=true)`, then the full diff if non-trivial. Sanity-check against
|
||||
the task description (they are long and precise — the acceptance criteria are the checklist).
|
||||
2. `review_task(taskId, decision="approve", leaveConflictsInTree=true)`.
|
||||
3. On conflict: open the files under the returned `repoPath`, resolve keeping **both** sides'
|
||||
intent, then `continue_merge(taskId)`. Conflicts are expected and normal here.
|
||||
4. **After every merge, verify `main`** (see §4). This is non-negotiable — see §5.
|
||||
|
||||
Expected conflicts: `TaskMergeService.cs` between #1 and #2; `src/ClaudeDo.Worker/CLAUDE.md`
|
||||
and `src/ClaudeDo.Data/CLAUDE.md` in nearly every merge (doc bullet lists — trivial, keep both
|
||||
sides' entries).
|
||||
|
||||
## 2. Then the Usage Monitor unit
|
||||
|
||||
Parent `439a4daf166f4ab5b0fa415693d2c80d` ("Usage Monitor hinzufügen") is `WaitingForChildren`
|
||||
and has **no worktree of its own**. It has 9 children. Four are merged, one is in flight, four
|
||||
are Idle.
|
||||
|
||||
| Child | Id | State |
|
||||
|---|---|---|
|
||||
| #38 Data: Usage-Gate-Schwellen + Modell-Spalte | `c1c999b6-b800-4b6b-a821-fbc028c15772` | merged `b1efcdc` |
|
||||
| #39 Worker: OAuth-Usage-Client + Poller | `f657e316-ad72-4f45-8036-460841fc8997` | merged `b126a21` |
|
||||
| #40 Worker: UsageGate | `06a7cc32-6ab7-4758-98f4-bee77149b2bf` | merged `1ee21b5` |
|
||||
| #41 Worker: TranscriptUsageReader | `840fdb98-1c0e-4219-8062-c8769233fc14` | merged `334cf1e` |
|
||||
| #42 Worker: Hub-Surface für Usage | `c1df5b9a-b911-4fe8-aab4-5876d9d85793` | **re-queued, in flight — read §5 before touching** |
|
||||
| #43 UI: Usage-Pill | `f74b44d9-7e48-4bfe-9d89-075e194d1fc9` | Idle — queue once #42 is merged |
|
||||
| #45 UI: Gate-Schwellen im Settings-Modal | `06068810-5b5c-4635-80dd-62eeba89fb8c` | Idle — queue once #42 is merged (parallel with #43) |
|
||||
| #44 UI: Usage-Monitor-Modal | `82488d2a-8ff7-41b8-b791-367959a8f827` | Idle — needs #42 **and** #43 merged |
|
||||
| #46 Docs: Usage Monitor | `9c8cffe0-8f7b-401e-a4f0-33b937047082` | Idle — last, after everything is merged |
|
||||
|
||||
**The chain is strictly serial and you must respect it.** Every child forks from `main`, and each
|
||||
one's own description hard-requires the earlier ones. Queueing them all at once is exactly what
|
||||
produced the original roadblock: #40 ran, found its prerequisite types only on unmerged sibling
|
||||
branches, and returned `Done` having written **zero** code. So: merge a child → then queue the
|
||||
next → verify `main` → repeat.
|
||||
|
||||
When the last child is merged the parent surfaces for review by itself; approve it to close the
|
||||
unit (it has no worktree, so it approves straight to Done).
|
||||
|
||||
## 3. Cheap status polling — important
|
||||
|
||||
`list_tasks` and `batch_get_tasks` return full descriptions and **blow the token limit** on this
|
||||
list (`list_tasks` over 52 tasks = ~206,000 chars; that is literally one of the bugs this run
|
||||
fixed). Do not poll with them. Poll the DB read-only instead:
|
||||
|
||||
```bash
|
||||
PYTHONIOENCODING=utf-8 python - <<'EOF'
|
||||
import sqlite3
|
||||
c=sqlite3.connect("file:C:/Users/mika.kuns/.todo-app/todo.db?mode=ro",uri=True)
|
||||
for i,s in c.execute("select id,status from tasks"):
|
||||
print(s, i)
|
||||
EOF
|
||||
```
|
||||
|
||||
`list_worktrees` is also compact and safe. **New this run:** `wait_for_task_change(taskIds,
|
||||
timeoutSeconds)` is now merged and is the proper primitive — it returns as soon as any listed
|
||||
task leaves Queued/Running (server-clamped to 170 s). Prefer it over sleeping.
|
||||
|
||||
## 4. Verify main after every merge
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
For the UI/localization task (#3 above, and children #43–#45) also:
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
`.slnx` needs .NET 9 — build individual csproj files, `-c Release` (a running Worker locks Debug).
|
||||
|
||||
Baseline as of this handoff: Worker **753/753**, Data **143/143**, build 0 warnings.
|
||||
|
||||
## 5. The trap that cost this run the most time
|
||||
|
||||
Two children "failed" with `"Claude exited with code 1 and no result"`. **That is a CLI crash,
|
||||
not bad code.** In both cases the worktree held complete work that built with 0 warnings and
|
||||
passed the full suite (#40: 732/732, #42: 766/766) — the run just died before the auto-commit.
|
||||
|
||||
- **Never `reset_failed_task` on such a task** — it discards the worktree and destroys the work.
|
||||
- Instead: `cd` into the worktree, `git status`, build + test it. If green, set the task
|
||||
`Queued`. The worktree is preserved and the agent resumes its own session (`--resume`),
|
||||
finds its work and commits it. That is how #40 was recovered.
|
||||
- #42 is mid-recovery right now via exactly this route. If it failed again, verify its worktree
|
||||
(`C:\Private\.claudedo-worktrees\claude-do\c1df5b9a-b911-4fe8-aab4-5876d9d85793`) before
|
||||
doing anything destructive.
|
||||
|
||||
Second trap: git merges cleanly and the **compiler** still breaks. It happened again this run —
|
||||
`Usage/UsageModels.cs` was an add/add conflict, and `src/ClaudeDo.Worker/CLAUDE.md` merged
|
||||
"cleanly" into a file with the `Usage/` folder documented **twice**. Always read what a clean
|
||||
merge produced, and always run §4.
|
||||
|
||||
## 6. Phase 0–3 decisions already made (do not redo)
|
||||
|
||||
Dedupe: four candidate pairs examined, **nothing cancelled**. Decisions:
|
||||
|
||||
- `05827da5` ↔ `81e37801` — kept both, and `05827da5` was **re-scoped**: its "lean status query"
|
||||
half was removed because `81e37801`'s wait tool covers it. `05827da5` now owns only the
|
||||
brief-description rendering. Both are merged.
|
||||
- `a76d9547` ↔ `99732497` — kept both, `99732497` merged first. Done.
|
||||
- `0b2fbb48` ↔ `9e307199` — kept both, `9e307199` merges first. **This is item #1/#2 in §1.**
|
||||
- `20c78c95` ↔ `a76d9547`(b) — kept both, different actors. Done.
|
||||
|
||||
Phase 2: all 11 tasks carry acceptance criteria, real file+line references and out-of-scope
|
||||
sections. Three that were one-liners were researched and rewritten after asking the user
|
||||
(ConPTY fix approach, maxTurns-only scope, roadblock reply-box design).
|
||||
|
||||
Run config: `maxParallelExecutions` = **3**. No list config exists, so effective max turns was
|
||||
the global **100**; it was raised to **200** per-task on the five heaviest via `set_task_config`.
|
||||
`0b2fbb48`, `9e307199` and `8c1c2130` still carry that override.
|
||||
|
||||
## 7. Open follow-ups worth new tasks
|
||||
|
||||
1. **`~/.todo-app/prompts/planning.md` shadows the planning prompt.** `PromptFiles.EnsureExists`
|
||||
only writes a default when the file is absent, and that file exists (dated Jun 2). The
|
||||
`maxTurns` guidance merged in `65db1cd` therefore **does not reach real planning sessions**
|
||||
until that file is updated by hand. `system.md` and `agent.md` are shadowed too.
|
||||
`merge-helper-system.md`/`merge-helper-initial.md` do **not** exist, so this run's handler
|
||||
prompt changes are live.
|
||||
2. **MCP task DTOs expose no parent/child link.** The 9-child Usage unit had to be reconstructed
|
||||
from `sortOrder` and creation timestamps. `get_task`/`list_tasks` should return
|
||||
`parentTaskId` / `blockedByTaskId`.
|
||||
3. **Visual verification open** on: the ConPTY fix (open a tile on a task whose description
|
||||
contains `->`), and — once merged — the roadblock reply box and the verify-gate field in the
|
||||
list settings modal.
|
||||
4. **`"exited with code 1 and no result"` is too common.** Three runs died that way today, two
|
||||
with finished work. Worth investigating whether the auto-commit step can be made to survive
|
||||
a late CLI crash.
|
||||
5. Nothing has been **pushed**. `main` is 22 commits ahead of `8d7ba1e`.
|
||||
|
||||
## 8. Rules this session operated under
|
||||
|
||||
- Drive merges through the MCP tools. Never raw `git merge` / `reset` / `checkout`.
|
||||
- Hand-resolve only markers the tools left behind, then `continue_merge`. When committing by
|
||||
hand is unavoidable, stage **explicit paths** — never `git add -A`: the main checkout is
|
||||
shared with other sessions.
|
||||
- For a parent/children unit merge, pass the **parent** id to `continue_merge` / `abort_merge`.
|
||||
- Ask the user on anything ambiguous, risky, or destructive.
|
||||
- Never `delete_task` to dedupe — `Cancelled` keeps it visible and resettable.
|
||||
+207
-24
@@ -1,42 +1,225 @@
|
||||
# ClaudeDo — Offene Punkte
|
||||
|
||||
Stand: 2026-07-24. Diese Datei listet die **aktiv verifizierten Findings** aus der Verifikations-Session vom 2026-07-24. Die laufende Verifikations-Checkliste (Pass/Fail je Abschnitt, inkl. noch offener Abschnitte §7–§11 und Kanten) lebt in `docs/verification-handoff.md`. Erledigtes steht in den Commits/im Code, nicht hier.
|
||||
Stand: 2026-08-06. Der Findings-Block von 2026-07-24 wurde am 2026-08-06 **gegen den Code
|
||||
nachverifiziert** (statisch, nicht in der laufenden App); alles, was inzwischen gefixt ist, ist
|
||||
hier entfernt — Erledigtes steht in den Commits/im Code, nicht hier. Die alte Verifikations-
|
||||
Checkliste lebt in `docs/verification-handoff.md`.
|
||||
|
||||
---
|
||||
|
||||
## Bugs (verifiziert 2026-07-24)
|
||||
## Bugs (offen)
|
||||
|
||||
- **OUTCOME-Karte rendert rohes Structured-Output-JSON:** `TaskMonitorViewModel.ApplyOutcome` (ClaudeDo.Ui) setzt bei Tasks ohne Roadblock-Marker `SessionOutcome = result` wörtlich (Zeile ~261). Der Worker legt in `task.Result` das rohe `{"summary":…,"files_changed":[…]}` ab (die lesbare Fassung steht in `task_runs.resultMarkdown`), also zeigt die Detail-Insel OUTCOME als JSON-Blob statt als Text. Fix: (a) UI parst ein JSON-Result und zeigt `summary`, oder (b) Worker schreibt `summary`/`resultMarkdown` statt des JSON in `task.Result`. Verifiziert am Task `verif §1 diff matrix`. Deckt sich mit Memory `worker_testing_findings`.
|
||||
- **Approve & Merge schluckt einen „blocked"-Merge still:** `DetailsIslandViewModel.ApproveReviewAsync` reagiert nur auf `result.Status == "conflict"` (öffnet den Resolver); bei `"blocked"` (z.B. Ziel-Working-Tree hat uncommittete getrackte Änderungen) und anderen Nicht-`merged`/Nicht-`conflict`-Status passiert **nichts** — kein Footer-Fehler, kein Dialog (der `catch` greift nur bei Exceptions, „blocked" ist aber ein normaler Rückgabewert mit `ErrorMessage`). User sieht „Klick tut nichts". Doppelt verifiziert (Konflikt-Approve UND sauberer additiver Approve `verif §1b`, beide bei dirty `main`-Checkout still). Fix: bei `blocked`/unerwartetem Status `result.ErrorMessage` via `ShowErrorAsync`/`FlashFooterError` surfacen. Verstößt gegen `feedback_ui_error_surfacing`. (Der Konflikt-Resolver + 3-Pane-Editor funktionieren, sobald der Ziel-Tree sauber ist.)
|
||||
- **„Waiting for Improvements" für Planning-Parents (Terminologie):** `taskStatus.waitingForChildren` = „Waiting for Improvements" (en.json:504), `agentStatus.children` dito (503), `childOutcomesLabel` = „IMPROVEMENTS" (191). Seit dem unified-parent-Modell gilt `WaitingForChildren` für Planning **und** Improvement-Parents — „Improvements" ist für einen Planning-Parent falsch. Auf neutrales „Waiting for Subtasks"/„Subtasks" umstellen (en **und** de — Localization.Tests-Parität). Verifiziert §3.
|
||||
- **Kind-Rows aktualisieren nach Parent-Planning-Transitionen nicht live:** Parent-getriebene Änderungen propagieren nicht auf die Kind-`TaskRowViewModel`s ohne Listen-Reload. (a) Nach „Finalize planning" bleiben Kind-Rows auf „Draft" (`IsDraft`) statt „Planned" (`IsPlanned`) — der geänderte `ParentFinalized` kommt nicht über das Parent-`TaskUpdated`-Broadcast an. (b) Nach „Discard" bleiben die (in der DB gelöschten) Draft-Kind-Rows sichtbar, bis die Liste neu geladen wird. Gemeinsame Ursache: die Kinderliste/-zustände werden bei Parent-Transitionen nicht live neu aufgelöst. Doppelt verifiziert §3 (Finalize **und** Discard, 2026-07-24).
|
||||
- **AskUser-Frage erscheint NUR in Mission Control, nicht in der Task-Detail-Insel:** Der `ask_user`-Banner (Frage + Antwort-Eingabe) lebt ausschließlich in `MonitorPaneView` (Mission Control). Die Detail-Insel (`DetailsIslandView`) streamt zwar den Live-Log eines laufenden Tasks, zeigt aber **kein** Frage-Banner — ein Nutzer, der nur die Detailansicht offen hat, sieht nicht, dass der Run auf eine Antwort wartet, und läuft nach 3 min in den Timeout-Fallback. Verifiziert §7 (User schaute Detailansicht, Frage war unsichtbar; erst in Mission Control sichtbar). Fix: Frage-Banner + Inline-Antwort auch in der Detail-Insel für den gebundenen laufenden Task surfacen (VM-Zustand liegt bereits in `TaskMonitorViewModel`, müsste für die Detail-Insel repliziert/geteilt werden). **Von Mika bei der §7-Verifikation ausdrücklich gewünscht** („vermisse diese Interaktion in der Detail-Ansicht"). §7.
|
||||
- **„New session"-Button (Mission Control) unsichtbar — Icon.Plus ist Strich-Only:** `Icon.Plus` = `M12 5v14M5 12h14` (IslandStyles.axaml:61) ist reine Linien-Geometrie ohne Fläche; im `<PathIcon>` (MissionControlView.axaml:38, füllt Geometrie) rendert sie **unsichtbar** → der Ad-hoc-„New session"-Button erscheint leer und ist nicht auffindbar (Feature + VM `OpenAdHocConPtySessionAsync` + Tests funktionieren, nur das Icon fehlt sichtbar). Fix: Icon.Plus als gefüllte Geometrie authoren ODER als gestricheltes `Path` rendern (Icon-Gotcha in CLAUDE.md). **Andere `Icon.Plus`-Verwendungen mitprüfen.** Verifiziert §5.
|
||||
- **Verwaiste git-Worktrees sind für die App unsichtbar** (gefunden 2026-08-06). In `C:\Private\ClaudeDo` waren nach dem Cleanup 22 git-registrierte Worktrees + 26 `claudedo/*`-Branches vorhanden, die ClaudeDo-DB kannte davon nur zwei. Die Worktrees-Übersicht listet ausschließlich Zeilen aus `worktrees`, also kann der Nutzer diese Reste nicht über die App entfernen — und es gibt keinen Sweep dafür (`OrphanRecovery` räumt nur Task-Zeilen auf, keine Worktrees). Wunsch: entweder ein Startup-Abgleich `git worktree list` ↔ DB, der Unbekannte als „untracked" in die Übersicht aufnimmt, oder mindestens eine Warnung mit Anzahl.
|
||||
|
||||
## UX / Nits (verifiziert 2026-07-24)
|
||||
## UX / Nits (offen)
|
||||
|
||||
- **„Resume planning session" ist grundsätzlich kaputt (Session-Id wird nie erfasst) + Fehler wird verschluckt:** Im UnfinishedPlanning-Modal macht **Resume** nichts. Ursache: `PlanningSessionManager.ResumeAsync` (PlanningSessionManager.cs:238) bricht mit `InvalidOperationException("No Claude session ID captured yet; cannot resume.")` ab, wenn `task.PlanningSessionId` leer ist — und der Setter `TaskRepository.UpdatePlanningSessionIdAsync` (TaskRepository.cs:322) hat **keinen einzigen Aufrufer** im Worker, d.h. `planning_session_id` wird nie befüllt (verifiziert an `verif §3 resume-discard`: `planning_session_id=NULL`, Token gesetzt, Drafts angelegt). Resume kann also **nie** erfolgreich sein. Zusätzlich verschluckt `TasksIslandViewModel.ResumePlanningSessionAsync` den ganzen Block in einem leeren `catch { }` (~Zeile 870) → der Button ist ein **stummer No-op**, kein Footer-Fehler, kein Dialog (verstößt gegen `feedback_ui_error_surfacing`). Hintergrund: die wt-Planning-Session ist interaktiv; ClaudeDo erfasst die claude-Session-Id dort nicht (analog zur bewusst nicht persistierten ConPTY-Session-Id, §5). Fix-Optionen: (a) beim Planning-Start/-Lauf die claude-Session-Id erfassen und via `UpdatePlanningSessionIdAsync` persistieren, damit Resume echt resumen kann; ODER (b) Resume entfernen/deaktivieren, wenn keine Session-Id vorliegt; in JEDEM Fall den Fehler statt des leeren `catch` surfacen. Verifiziert §3. §3.
|
||||
- **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 (App-Neustart nötig, um die „erster-Drop"-Bedingung wiederherzustellen). 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 (die genaue Exception-Message fehlt, weil `DropStatus` nur `{fileName}: {ex.Message}` zeigt). Verifiziert §9 (einmalig beobachtet). §9.
|
||||
- **Agent-Settings-Gear weicht vom übrigen Settings-Icon ab:** Der Agent-Settings-Flyout-Button (`TaskHeaderBar.axaml:68`) rendert ein Unicode-Glyph `⚙` (`<TextBlock Text="⚙">`), während die Listen-Nav (`ListsIslandView.axaml:56`) und die Listen-Settings (`TasksIslandView.axaml:47`) das gefüllte `Icon.Settings`-PathIcon (Gear-StreamGeometry, IslandStyles.axaml:110) nutzen → optisch ein anderes Zahnrad. Angleichen: den `⚙`-TextBlock durch `<PathIcon Data="{StaticResource Icon.Settings}" .../>` ersetzen. Verifiziert §8 (User-Sichtprüfung). §8.
|
||||
- **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"). Verifiziert §8 (frischer Zustand vor ponytail-Install). Kleiner Empty-State-Text ergänzen.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
- **Diff-Viewer: reiner Rename schwach dargestellt:** Rename korrekt erkannt (`UnifiedDiffParser` → `DiffFileStatus.Renamed`, Badge `StatusCode="R"`), aber der File-Tree zeigt nur den neuen Namen + „+0 −0" ohne „alt → neu"-Pfad; liest sich wie „keine Änderung". Optional: alten Pfad + „renamed"-Label anzeigen. §1.
|
||||
- **Header-TurnsText zeigt `0/max` für abgeschlossene Runs:** `TurnsText => {Turns}/{EffectiveMaxTurns}` — `Turns` wird beim Laden eines terminalen Tasks nicht aus `task_runs.turnCount` restauriert (nur live gefüllt). Kosmetisch. §1.
|
||||
- **Conflict-Resolver: Continue/Merge-Button klickbar trotz offener Konflikte:** Gate greift funktional (merged erst wenn alle Konflikte in allen Dateien gelöst), aber der Button ist nicht disabled/gegraut → früher Klick ist ein stummer No-op. Besser: disabled bis `AllResolved`, oder Hinweis „N Konflikte in M Dateien offen". §4.
|
||||
- **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". §4.
|
||||
- **Planning-aktiver Parent zeigt weiter „Idle":** Parent in `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber der Row-Status-Chip zeigt „Idle"; der `PlanningBadge` ersetzt das nicht sichtbar → liest sich wie ein normaler Idle-Task. Wunsch: klarer „Planning/Draft aktiv"-Zustand, der Idle überschreibt. (Kinder zeigen korrekt „Draft".) §3.
|
||||
- **Blocked-by-Kette nicht sichtbar:** Nach Finalize ist die sequentielle Kette korrekt gesetzt (child[i] blocked-by child[i-1]), aber die UI stellt die Reihenfolge/Abhängigkeit nicht dar. Wunsch: Kette visualisieren (z.B. „wartet auf <Vorgänger>"), nicht nur der „waiting"-Chip nach dem Queueen. §3.
|
||||
- **Dequeue-„X" fehlt auf wartenden (blockierten) Kettengliedern:** `CanRemoveFromQueue = IsQueued || HasQueuedSubtasks`, `IsQueued` verlangt leeres `blocked_by`. Ein gequeuetes, aber blockiertes Kind (`IsWaiting`) bekommt daher kein Remove-from-queue-X — nur Parent + erstes (entsperrtes) Kind. §3.
|
||||
- **„Open ConPTY session" erneut = Prompt wird neu gesendet, kein Resume:** Da die ConPTY-Session-Id bewusst nicht persistiert wird, startet ein erneutes „Open ConPTY session" auf demselben Task eine frische Session und sendet den Task-Prompt erneut (re-runt die Arbeit im selben Worktree) statt zu resumen. So designt, aber UX-Falle — evtl. „Resume"-Affordance oder Re-Open-Warnung. §5.
|
||||
## Feature-Wünsche
|
||||
|
||||
## Feature-Wünsche (aus der Session)
|
||||
- **Conflict-Resolver: farbliches Hervorheben eingefügter Zeilen im Result-Pane** (grüner „flow" der übernommenen Zeilen).
|
||||
- **Die Handler-Session soll „Submit for review" selbst auslösen können** (Sichtprüfung 2026-08-06). Aktuell ist der Abschluss eines List-Handler-Laufs ein reiner Handgriff über die Schaltfläche in der Mission-Control-Kachel — die Session selbst hat kein Werkzeug dafür, obwohl sie am besten weiß, wann sie fertig ist. Wunsch: ein MCP-Tool auf der In-Task-Oberfläche, das denselben Pfad wie der Button nimmt.
|
||||
- **Handoff-Kachel besser beschriften** (Sichtprüfung 2026-08-06). Die zweite Kachel heißt `Merge Helper — <Liste> (Handoff)` bzw. „(Übergabe)" (`missionControl.mergeHelperHandoffTitleSuffix`, en.json/de.json:297). „Handoff" ist internes Vokabular und sagt nicht, was die Session tut. Besser etwas, das die Rolle nennt (Ausführen + Mergen der verbliebenen Tasks, Phasen 3-5).
|
||||
|
||||
- **Conflict-Resolver: farbliches Hervorheben eingefügter Zeilen im Result-Pane** (grüner „flow" der übernommenen Zeilen). §4.
|
||||
- Als ClaudeDo-Tasks in Liste „Claude do" erfasst: **Approve erzwingt Diff/Review vor Merge** (koppelt den blocked-Merge-Silent-Fail-Fix) und **interaktive Planning-Session über embedded ConPTY statt externem wt-Fenster** (koppelt den Planning-Permission-Prompt).
|
||||
## Design-Entscheidungen (27.07.-Batch, Sichtprüfung am 2026-08-06 abgeschlossen)
|
||||
|
||||
Der Sichtprüfungs-Block dieses Batches (Ctrl+K/`#`, Titel-Edit, ConPTY-/Refine-Spinner,
|
||||
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.
|
||||
- `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)
|
||||
|
||||
- **`--permission-mode auto` + Modell `haiku` → Writes werden denied:** Kontrolliert verifiziert (CLI 2.1.207): unter dem Default-Mode `auto` bekommt **sonnet** Writes auto-approved (`permission_denials:[]`), **haiku** wird `denied` (`permission_denials:[Write]`, keine Datei) — eine haiku-Task macht unter `auto` still nichts und landet ohne Änderung in `WaitingForReview`. Normalbetrieb (Default = sonnet) nicht betroffen. KEINE CLI-Regression, sondern modellabhängiges `auto`-Verhalten. Optionen falls es nervt: haiku aus der Auswahl nehmen, ODER Runner auf `acceptEdits`/`bypassPermissions` (modell-unabhängig). Mika: erstmal beobachten. Siehe Memory `auto_permission_haiku_footgun`.
|
||||
- ~~**`QueueServiceTests.UsageGate_TransitionLogging_FiresOncePerChange` ist zeitbasiert flaky**~~ — **am 2026-08-06 gefixt**: der feste `Task.Delay(200)` ist durch die schon im selben File vorhandene `AssertStableCountAsync` ersetzt (pollt bis der erste Backstop-Tick geloggt hat, wartet dann eine Karenzzeit und stellt sicher, dass kein weiterer Tick nachlegt). Historischer Befund zur Einordnung: Schlägt reproduzierbar fehl (`Expected 1, Actual 0` Warn-Log-Aufrufe), sowohl solo (`--filter`) als auch im Vollauf, auf einem sauberen `git worktree add` gegen `main` (bdee731) — also **kein** durch diese Abschluss-Session verursachter Regress (die Session hat keine `.cs`-Datei angefasst). Ursache: der Test verlässt sich auf einen festen `Task.Delay(200)`, um mehrere 50-ms-Backstop-Ticks abzuwarten (Kommentar im Test: „Several backstop ticks (50ms interval) all observe the same blocked state"); auf einer stark ausgelasteten Maschine (hier: viele parallele ClaudeDo-Worktrees/Builds) reicht das Fenster nicht immer. Zum Vergleich: derselbe Test lief in einer zweiten, isolierten Verifikation (Scratch-Merge für den Environment-Checks-Task) sauber durch (876/876). Fix wäre ein Poll-basiertes Warten statt fixem Sleep — aber außerhalb des Scopes dieser Doku/Verifikations-Session (keine Code-Änderung angefasst).
|
||||
|
||||
---
|
||||
|
||||
## Offene Verifikation (2026-07-27)
|
||||
|
||||
- **List handler (2026-07-27)** — visual pass: the Broom button is gone from the lists footer,
|
||||
the context-menu item appears only on lists with a working dir, and the selection dialog has no
|
||||
LIST column. (Der real-Claude-Smoke-Run der fünf Phasen ist am 2026-07-29 gelaufen — 6/6 sauber
|
||||
gemerged; nur die Sichtprüfung ist noch offen.)
|
||||
|
||||
## Offene Verifikation (2026-08-05)
|
||||
|
||||
- **List handler owns a task (2026-08-05)** — **visuell verifiziert am 2026-08-06** (Lauf über die
|
||||
Liste `ClaudeDoTests` mit 4 Tasks, davon 2 absichtliche Dubletten): genau ein neuer Task, Mission-
|
||||
Control-Tile task-basiert mit „Submit for review", Dedupe hat die Dublette erkannt,
|
||||
Beschreibungen wurden angereichert, Submit → `WaitingForReview`, und die Diff-Karte zeigte alle
|
||||
drei Dateien des Laufs. Korrektur zur Erwartung oben: der Task-Chip liest sich **„Interactive"**,
|
||||
nicht `Idle` — das ist korrekt so (`HasInteractiveSession` überschreibt den Status-Chip, solange
|
||||
die ConPTY-Session läuft). Die drei dabei gefundenen Punkte stehen unter „Bugs" bzw.
|
||||
„Feature-Wünsche".
|
||||
- **Roadblock reply field (2026-08-05)** — **Kernfunktion am 2026-08-06 verifiziert**: auf einem
|
||||
Task mit gemeldetem Roadblock erscheint das Antwortfeld unter dem Roadblock-Text, die Antwort
|
||||
setzt die Session fort, und der Task landet danach sauber auf `WaitingForReview`. Dabei ist der
|
||||
Bug oben aufgefallen (Feld deaktiviert, solange der Task seit vor dem Lauf offen ist). Layout der
|
||||
Roadblock-Karte ist in Ordnung. **Noch offen:** Enter-zum-Senden, und dass ein „Override-Slot
|
||||
besetzt"-Fehler im Footer-Strip landet (nicht als Modal) mit dem getippten Text weiterhin im
|
||||
Feld. Design-Entscheidung dazu: der Review-Bereich lebt als zwei nullable Spalten
|
||||
direkt auf `TaskEntity` (kein Phantom-`WorktreeEntity`), damit `list_worktrees`/die
|
||||
Worktrees-Übersicht ihn nie sehen.
|
||||
|
||||
- **Post-merge verify gate (2026-08-05)** — build + unit tests all green (incl. real-process
|
||||
`VerifyCommandRunner` exit-code/output/timeout tests and `TaskMergeService` success/failure/
|
||||
timeout paths via a fake runner), but **not visually verified**: open a list's Settings modal,
|
||||
confirm the new "VERIFICATION" section renders below Agent with a settable/clearable
|
||||
`VerifyCommand` field; approve a task on a list with a failing command configured and confirm
|
||||
the footer/error surfacing (`ShowErrorAsync`) actually shows the verify failure message instead
|
||||
of silently looking like nothing happened. Also no real-build smoke test (a real `dotnet build`/
|
||||
`dotnet test` invocation as the configured command) — only fast synthetic commands (`exit N`,
|
||||
`ping` for timeout) were exercised.
|
||||
|
||||
## Offene Verifikation (2026-08-06, Fix-Batch aus der Sichtprüfung)
|
||||
|
||||
Fünf Findings der Sichtprüfung sind gefixt, Build + Tests grün, aber **noch nicht in der App
|
||||
nachgeprüft** (der laufende Build ist älter — erst nach Neuinstallation testbar):
|
||||
|
||||
- **NumericUpDown-Leeren wirft nicht mehr:** neuer `KeepLastNumberConverter` bildet beim
|
||||
ConvertBack `null` auf `BindingOperations.DoNothing` ab, an allen acht nicht-nullbaren
|
||||
NumericUpDowns im Settings-Modal verdrahtet. Prüfen: Wert löschen und neu tippen — keine
|
||||
Exception, alter Wert bleibt stehen, bis eine echte Zahl kommt. Damit ist auch der offene Rest
|
||||
von „MaxTurnsCeiling ändern → Hinweistexte ziehen mit" nachholbar.
|
||||
- **Verify-Gate greift jetzt auch ohne Worktree:** Approve auf einem List-Handler-Task mit
|
||||
fehlschlagendem `VerifyCommand` muss `verify_failed` liefern und den Task aus `Done` halten
|
||||
(vorher ging er kommentarlos auf `Done`).
|
||||
- **`verify_failed` im Merge-Modal und in der Worktrees-Übersicht:** Modal zeigt statt „Unknown
|
||||
status" den echten Fehlertext und schließt sich **nicht** automatisch; die Batch-Übersicht
|
||||
zeigt `VerifyFailed` und markiert die Zeile trotzdem als `Merged` (der Merge ist ja gelandet).
|
||||
- **Roadblock-Reply/Continue live:** Task offen lassen, während er in den Roadblock läuft — das
|
||||
Antwortfeld und Continue müssen **ohne** erneutes Öffnen aktiv werden.
|
||||
- **Cleanup-Log:** ein Worktree-Cleanup über veraltete DB-Zeilen darf keine WARN-Flut mehr im
|
||||
Footer erzeugen (die „schon erledigt"-Fälle gehen auf Debug).
|
||||
- **Handoff schließt die alte Kachel:** beim Übergang von Phase 2 auf Phase 3 muss die alte
|
||||
Merge-Helper-Kachel verschwinden und die neue an ihrer Stelle stehen — keine leere Geister-Pane
|
||||
mehr. (Entscheidung Mika 2026-08-06: automatisch schließen statt Output erhalten.)
|
||||
|
||||
## Offene Verifikation (2026-08-06, Resume planning session)
|
||||
|
||||
`planning_session_id` wurde nie befüllt (der Setter hatte keinen Aufrufer), weil die interaktive
|
||||
Planning-Session ihre claude-Session-Id nie zurückmeldet — Resume konnte deshalb nie
|
||||
funktionieren. `PlanningSessionManager.ResumeAsync` liest die Id jetzt beim ersten Resume aus dem
|
||||
Transkript, das Claude Code unter `~/.claude/projects/<encodiertes cwd>/<sessionId>.jsonl` für den
|
||||
Planning-Worktree ablegt (`PlanningTranscriptLocator`), und persistiert sie. Unit-Tests grün,
|
||||
**nicht visuell verifiziert**:
|
||||
|
||||
- Planning-Session starten, Fenster/Pane schließen, Task erneut öffnen → „Resume" muss die
|
||||
ConPTY-Pane mit der **fortgesetzten** Unterhaltung öffnen (nicht mit leerem Verlauf).
|
||||
- Ohne Transkript (z. B. Session nie wirklich gestartet): Fehlermeldung „No Claude session
|
||||
transcript found…" landet sichtbar im Footer-Error-Strip (`Terminal.StartError` →
|
||||
`ErrorReported`), nicht still.
|
||||
- **Risiko:** das Verzeichnis-Encoding (`~/.claude/projects/`, jedes Nicht-Alphanumerische wird
|
||||
`-`) ist undokumentiertes CLI-Verhalten — ändert es sich, findet der Locator nichts und Resume
|
||||
meldet sauber „cannot resume" (fail-safe, kein falscher Resume).
|
||||
|
||||
## Offene Verifikation (2026-08-05, Usage Monitor)
|
||||
|
||||
> **Kein Light-Theme.** `App.axaml:6` setzt `RequestedThemeVariant="Dark"` fest, es gibt keinen
|
||||
> Umschalter und `Tokens.axaml` kennt keine Light-Variante. „Dark/Light"-Checks sind deshalb
|
||||
> überall aus dieser Datei entfernt — sie waren nie erfüllbar.
|
||||
|
||||
- **Visueller Pass Usage-Pill** (Footer **und** Mission-Control-Header) — **am 2026-08-06
|
||||
verifiziert**: Text/Tooltip lesbar, Dot-Zustände plausibel, Pill in Footer und
|
||||
Mission-Control-Header identisch.
|
||||
- **Visueller Pass Usage-Monitor-Modal** — **am 2026-08-06 verifiziert**: Gauges (dynamisch aus
|
||||
`limits[]`), Info-Bänder (der Throttle-Hinweis „Queue throttled: 1/3 slots (7d)" stand real an),
|
||||
7d/30d-Presets + Custom-Range. Die Analyse-Tabellen sind ausbaufähig → siehe „UX / Nits".
|
||||
- **E2E Gate**: das Gate greift real, sobald ein Bucket (`five_hour`/`seven_day`) die
|
||||
konfigurierte Schwelle reißt — Queue-Nachschub pausiert, laufende Runs/`RunNow`/ConPTY/
|
||||
Planning/Prime bleiben unberührt — und die Queue nimmt nach dem Reset selbstständig wieder
|
||||
auf (30s-Backstop, kein persistenter Pause-Zustand).
|
||||
- **Risiko:** der Usage-Endpoint (`GET https://api.anthropic.com/api/oauth/usage`) ist
|
||||
undokumentiert und kann sich ändern; bei Ausfall/Formatänderung ist das Gate wirkungslos
|
||||
(fail-open by design — kein Blocker, aber der Schutz fällt dann aus, ohne dass es auffällt).
|
||||
|
||||
### Nachtrag 2026-08-05: 429-Fix (Poll-Kadenz + Refresh-Button)
|
||||
|
||||
Der 60s-Poll lief in 429s. Neu: aktivitätsabhängige Kadenz (5 Min. solange ein Task `Running`
|
||||
ist, sonst 15 Min.), 429-Backoff mit `Retry-After`, und ein „Jetzt aktualisieren"-Button im
|
||||
Usage-Monitor-Modal (`RefreshUsage` → `UsageMonitorService.RefreshNowAsync`, 10s-Cooldown).
|
||||
Unit-Tests grün, **offen**:
|
||||
|
||||
- **Visueller Pass Refresh-Button** im Modal — **am 2026-08-06 verifiziert** (Button, Hinweiszeile
|
||||
„Polled every 5 min while a task runs, otherwise every 15 min.").
|
||||
- **E2E:** über ≥20 Min. mit und ohne laufenden Task beobachten, dass keine 429s mehr im
|
||||
Worker-Log auftauchen und die Pill trotzdem aktuell bleibt.
|
||||
- **Beachten:** die Pill wird jetzt erst nach 3× 15 Min. als `stale` markiert — ein echter
|
||||
Endpoint-Ausfall fällt vorher nur über `LastError` auf (der `IsStale` sofort setzt).
|
||||
|
||||
## Offene Verifikation (2026-08-05, Max-Turns-Ceiling)
|
||||
|
||||
Build + unit tests grün (`ResolveMaxTurns`-Klemmung, Repository-Backfill von `model_presets`,
|
||||
Migration `AddMaxTurnsCeiling` gegen eine Scratch-DB angewendet), aber **nicht visuell
|
||||
verifiziert**:
|
||||
|
||||
- Agent-Settings-Editor (Task **und** Liste): Max-Turns-Feld auf einen Wert über der Ceiling
|
||||
(Default 80) setzen, Hinweistext unter dem `NumericUpDown` erscheint ("Runs are capped at
|
||||
{N} turns…").
|
||||
- Settings → Allgemein → Vorgaben pro Modell: eine Zeile über 80 setzen, derselbe Hinweistext
|
||||
erscheint unter der Zeile.
|
||||
- `MaxTurnsCeiling` ist seit `2975f90` selbst editierbar (Settings → Allgemein,
|
||||
`SettingsModalView.axaml`) — Feld prüfen: Wert ändern, speichern, Hinweistexte oben ziehen mit.
|
||||
|
||||
## Offene Verifikation (2026-08-05, Environment Checks / SystemCheckPage)
|
||||
|
||||
Checks + SystemCheckPage (`claudedo/06aca9b3…`) und das ExecutableResolver-Wiring im Worker
|
||||
(`claudedo/40272c0b…`) sind seit 2026-08-06 auf `main` gemerged; die frühere „erst mergen"-
|
||||
Voraussetzung ist erledigt. Details → `installer-preflight` in `docs/explore-notes/README.md`
|
||||
und der Abschnitt „Environment Checks" in `src/ClaudeDo.Installer/CLAUDE.md`.
|
||||
|
||||
**Update (2026-08-06):** beide Folge-Features sind jetzt implementiert und auf `main`.
|
||||
|
||||
- Diagnose-Sektion (Config-Modus/`SettingsWindow`): `Pages/DiagnosePage/` + geteilte
|
||||
`Checks/CheckListViewModel.cs`/`Checks/CheckListView.xaml` (auch von `SystemCheckPage`
|
||||
genutzt, keine zweite Implementierung). Unit-getestet
|
||||
(`tests/ClaudeDo.Installer.Tests/Pages/DiagnosePage/DiagnosePageViewModelTests.cs`).
|
||||
- „Claude Help Me"-Button: `Core/ClaudeHelpLauncher.cs` + `SystemCheckPageViewModel`/-View,
|
||||
unit-getestet (`tests/ClaudeDo.Installer.Tests/Core/ClaudeHelpLauncherTests.cs`).
|
||||
|
||||
Beides **nicht visuell verifiziert**:
|
||||
|
||||
- [x] Help-Me-Button ist deaktiviert, wenn `claude-cli` nicht `Ok` ist oder `claude-auth`
|
||||
`Failed` ist (bleibt aktiv bei `Unknown`), mit erklärendem Tooltip — unit-getestet.
|
||||
- [ ] „Claude Help Me" öffnet tatsächlich ein Terminal mit laufender Claude-Session, und die
|
||||
Session hat den Diagnose-Report gelesen — **nicht verifiziert** (der eigentliche
|
||||
Terminal-Start/`wt.exe`-Zusammenspiel und die Session-Qualität sind nur über die
|
||||
injizierte `IProcessLauncher`-Fake getestet, nie mit einem echten Terminal/CLI).
|
||||
- [ ] Platzierung des Help-Me-Buttons: er sitzt seit dem Merge der Diagnose-Sektion in einer
|
||||
**eigenen Zeile unter** dem geteilten Check-Listen-Footer (der reservierte Slot *im*
|
||||
Footer entfiel mit der Extraktion nach `CheckListView.xaml`). Optisch prüfen, ob das
|
||||
so bleiben soll oder ob der Button in den geteilten Footer gehört.
|
||||
- [ ] Diagnose-Sektion im Config-Modus: Öffnen von SettingsWindow löst keinen Prüflauf aus, Klick
|
||||
auf „Erneut prüfen" schon; zeigt die echten installierten Pfade/Ports (nicht die
|
||||
InstallContext-Defaults), und der laufende Worker auf dem konfigurierten SignalR-Port gilt
|
||||
nicht als Konflikt — **unit-verifiziert, visueller Durchlauf noch offen.**
|
||||
|
||||
Weitere Punkte, gebaut + unit-getestet auf `main`, aber **nicht visuell verifiziert**:
|
||||
|
||||
- [ ] SystemCheckPage: Layout, Icon-/Farbwirkung der vier Status (Ok grün / Warnung orange /
|
||||
Fehler rot / Unbekannt grau — `StatusGreenBrush`/`StatusOrangeBrush`/`StatusRedBrush`/
|
||||
`StatusGrayBrush`), Lesbarkeit der Hint-Texte, DE und EN.
|
||||
- [ ] Weiter-Button gesperrt bei einem echten blockierenden Fehler (z. B. `claude` nicht im
|
||||
PATH → `claude-cli` Error/Failed), und der Grund ist in der Zusammenfassungszeile
|
||||
sichtbar (nennt den/die blockierenden Check(s) namentlich).
|
||||
- [ ] „Erneut prüfen" wechselt einen Status live (z. B. git-Identity setzen → Warnung
|
||||
verschwindet), ohne dass ein zweiter paralleler Lauf startet, wenn währenddessen erneut
|
||||
geklickt wird.
|
||||
- [ ] Update-Modus zeigt die SystemCheckPage **nicht** (Wizard bleibt Welcome + Install).
|
||||
- [ ] Auf einem Rechner mit npm-installiertem `claude.cmd`: `claude-cli`-Check findet es
|
||||
(Detail-Text „Resolved via a shim…"), und ein Task läuft im Worker durch (bestätigt, dass
|
||||
`ClaudeProcess`/`ClaudeCliPreflight` den Shim über `cmd.exe /c` tatsächlich startet, nicht
|
||||
nur, dass der Check ihn findet).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,938 @@
|
||||
# Per-List Task Handler 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:** Make "Let Claude handle it" list-scoped only, and turn its prompt into a five-phase run — read all tasks, dedupe, enhance, queue, review+merge.
|
||||
|
||||
**Architecture:** Four independent commits. Two touch only leaf code (the MCP status tool, the prompt templates). One strips the global UI entry point. The last is an atomic sweep that makes `listId` non-nullable end to end and collapses the launch spec to a single repo — atomic because a half-flipped signature chain leaves nullable warnings scattered across a commit boundary.
|
||||
|
||||
**Tech Stack:** .NET 8, xUnit, Avalonia 12, EF Core + SQLite, CommunityToolkit.Mvvm.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-07-27-list-handler-design.md`
|
||||
|
||||
**Build note:** `dotnet build ClaudeDo.slnx` needs .NET 9 — build individual csproj with `-c Release` (a running Worker locks `Debug` output).
|
||||
|
||||
**Staging note:** the checkout is shared with parallel sessions. Always `git add -- <exact paths>` and `git commit -- <exact paths>`. Never `git add -A`, never a bare `git commit`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `update_task_status` accepts `Cancelled`
|
||||
|
||||
Dedupe needs to retire an **Idle** duplicate. Today nothing can: `UpdateTaskStatus` allows only
|
||||
`Idle`/`Queued`, `cancel_task` only cancels a *running* task, and `review_task(decision="cancel")`
|
||||
requires WaitingForReview/Running/Queued. `TaskStateService.CancelAsync` already owns the
|
||||
transition and its side effects.
|
||||
|
||||
`BatchMcpTools.BatchUpdateTaskStatus` delegates to this same method, so batch cancel comes free.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/External/ExternalMcpService.cs:264-300`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Append inside the `ExternalMcpServiceTests` class. `SeedTaskAsync` does not exist in this class —
|
||||
seed inline the way the existing tests do, via `_lists` / `_tasks`.
|
||||
|
||||
```csharp
|
||||
private async Task<TaskEntity> SeedPlainTaskAsync(TaskStatus status)
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||
var task = new TaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t",
|
||||
Status = status, CreatedAt = DateTime.UtcNow, CommitType = "chore",
|
||||
};
|
||||
await _tasks.AddAsync(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTaskStatus_Cancelled_CancelsAnIdleTask()
|
||||
{
|
||||
var task = await SeedPlainTaskAsync(TaskStatus.Idle);
|
||||
var queue = CreateQueue();
|
||||
var sut = BuildSut(queue);
|
||||
|
||||
var dto = await sut.UpdateTaskStatus(task.Id, "Cancelled", CancellationToken.None);
|
||||
|
||||
Assert.Equal("Cancelled", dto.Status);
|
||||
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Cancelled, loaded!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTaskStatus_Done_StillRejected()
|
||||
{
|
||||
var task = await SeedPlainTaskAsync(TaskStatus.Idle);
|
||||
var queue = CreateQueue();
|
||||
var sut = BuildSut(queue);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.UpdateTaskStatus(task.Id, "Done", CancellationToken.None));
|
||||
Assert.Contains("not settable externally", ex.Message);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~ExternalMcpServiceTests.UpdateTaskStatus"
|
||||
```
|
||||
|
||||
Expected: `UpdateTaskStatus_Cancelled_CancelsAnIdleTask` FAILS with
|
||||
`Status 'Cancelled' is not settable externally.`; `UpdateTaskStatus_Done_StillRejected` passes.
|
||||
|
||||
- [ ] **Step 3: Add the `Cancelled` branch**
|
||||
|
||||
In `ExternalMcpService.UpdateTaskStatus`, insert between the `Queued` case and `default`:
|
||||
|
||||
```csharp
|
||||
case TaskStatus.Cancelled:
|
||||
var cancelResult = await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken);
|
||||
if (!cancelResult.Ok)
|
||||
throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
|
||||
break;
|
||||
```
|
||||
|
||||
Then update the `[McpServerTool, Description(...)]` text directly above the method — it currently
|
||||
claims only Idle and Queued are permitted. Replace the whole attribute with:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description(
|
||||
"Update a task's status. Only 'Idle', 'Queued' and 'Cancelled' are permitted externally — " +
|
||||
"use run_task_now for execution control, and review_task to act on a WaitingForReview task. " +
|
||||
"Settable: Idle (reset to editable), Queued (enqueue for execution), " +
|
||||
"Cancelled (retire the task without deleting it; it can be reset to Idle later). " +
|
||||
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.")]
|
||||
```
|
||||
|
||||
Also fix the `default` branch message, which still points at `cancel_task`:
|
||||
|
||||
```csharp
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Status '{target}' is not settable externally. Use run_task_now or review_task.");
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~ExternalMcpServiceTests"
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 5: Run the MCP schema test**
|
||||
|
||||
`ExternalMcpToolSchemaTests` asserts over tool descriptions and may pin the old text.
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~ExternalMcpToolSchemaTests"
|
||||
```
|
||||
|
||||
Expected: PASS. If it fails on the changed description, update the assertion to match the new
|
||||
text — do not revert the description.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add -- src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||
git commit -m "feat(worker): allow update_task_status to set Cancelled" -- src/ClaudeDo.Worker/External/ExternalMcpService.cs tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs
|
||||
```
|
||||
|
||||
(If Step 5 required a schema-test edit, add that path to both commands too.)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Five-phase helper prompt
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/PromptFiles.cs:231-276` (`MergeHelperDefault`, `MergeHelperInitialDefault`)
|
||||
- Test: `tests/ClaudeDo.Data.Tests/PromptFilesTests.cs:54-80`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Replace the existing `DefaultFor_merge_helper_is_non_empty_and_mentions_the_merge_tools` test with
|
||||
the two below, and keep the other merge-helper tests as they are.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void DefaultFor_merge_helper_covers_all_five_phases()
|
||||
{
|
||||
var d = PromptFiles.DefaultFor(PromptKind.MergeHelper);
|
||||
Assert.False(string.IsNullOrWhiteSpace(d));
|
||||
Assert.Contains("Phase 0", d);
|
||||
Assert.Contains("Phase 1", d);
|
||||
Assert.Contains("Phase 2", d);
|
||||
Assert.Contains("Phase 3", d);
|
||||
Assert.Contains("Phase 4", d);
|
||||
Assert.Contains("Phase 5", d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultFor_merge_helper_names_the_tools_each_phase_needs()
|
||||
{
|
||||
var d = PromptFiles.DefaultFor(PromptKind.MergeHelper);
|
||||
Assert.Contains("batch_get_tasks", d); // phase 0
|
||||
Assert.Contains("update_task", d); // phase 1 + 2
|
||||
Assert.Contains("get_app_settings", d); // phase 3
|
||||
Assert.Contains("update_task_status", d); // phase 3
|
||||
Assert.Contains("review_task", d); // phase 4
|
||||
Assert.Contains("continue_merge", d); // phase 4
|
||||
Assert.DoesNotContain("run_task_now(", d); // single override slot — must not batch-start
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~PromptFilesTests"
|
||||
```
|
||||
|
||||
Expected: both new tests FAIL (no "Phase 0", no `batch_get_tasks`).
|
||||
|
||||
- [ ] **Step 3: Replace `MergeHelperDefault`**
|
||||
|
||||
Replace the whole `private const string MergeHelperDefault = """ … """;` block with:
|
||||
|
||||
```csharp
|
||||
private const string MergeHelperDefault = """
|
||||
You are the ClaudeDo list handler, running as an interactive session with the user watching. Ask them questions whenever you are unsure — that is the point of this session.
|
||||
|
||||
Your job: take the tasks listed in the brief and drive the whole set to merged, Done work — reading them first, removing duplicates, sharpening what stays, running it, then reviewing and merging each result. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
||||
|
||||
Work the five phases in order. Do not start a phase before the previous one is finished.
|
||||
|
||||
## Phase 0 — Read everything
|
||||
Call batch_get_tasks with every id from the brief and read each task's title, description, status and parent/child links. Do not act on any single task before you have read them all — Phase 1 needs the whole set in view.
|
||||
|
||||
## Phase 1 — Dedupe
|
||||
Compare the tasks pairwise for overlap: same goal stated twice, one task fully contained in another, two tasks that would edit the same thing for the same reason.
|
||||
|
||||
Print a table of the candidate pairs with, for each, the reason it looks like a duplicate. Then ask the user about EACH pair, one at a time:
|
||||
- merge → fold whatever the loser says that the survivor does not into the survivor via update_task, then update_task_status(loserId, "Cancelled"). Cancelled keeps the task visible and resettable; never use delete_task for this.
|
||||
- keep both → note why and move on.
|
||||
|
||||
Cancel nothing without an explicit answer. If there are no duplicates, say so and go on.
|
||||
|
||||
## Phase 2 — Enhance for execution
|
||||
Each surviving task is about to be run by an autonomous agent with no further input. Sharpen it so that run can succeed. For each task, rewrite title and description to carry:
|
||||
- concrete acceptance criteria — what must be true when it is done,
|
||||
- the files and areas actually involved, found with Read/Grep/Glob in the repo. Do not guess paths; look them up.
|
||||
- what is explicitly out of scope.
|
||||
|
||||
Write it back with update_task (title, description and commitType are the settable fields).
|
||||
|
||||
Rules: do not change what the user asked for, and do not invent requirements. You are making the existing intent precise, not adding to it. If a task is too vague to sharpen without guessing, ASK instead of guessing. Report a short before/after per task.
|
||||
|
||||
## Phase 3 — Run
|
||||
Do NOT use run_task_now for a batch — there is a single override slot and the second call fails with "override slot busy".
|
||||
|
||||
Read get_app_settings and tell the user how many parallel execution slots are configured (maxParallelExecutions). If it is 1, say plainly that the tasks will execute one after another and that the value is changeable in ClaudeDo's settings.
|
||||
|
||||
Then, for each surviving task:
|
||||
- Idle or Failed → update_task_status(id, "Queued"). For a Failed task ask first whether to reset_failed_task and re-queue it, or skip it.
|
||||
- Queued → leave it; it is already waiting for a slot.
|
||||
- Running or WaitingForChildren → leave it; only poll.
|
||||
- WaitingForReview → leave it; it goes straight to Phase 4.
|
||||
|
||||
Poll get_task until every task has left Queued and Running — WaitingForReview on success, Failed on error. Report progress as tasks land; do not poll silently for minutes.
|
||||
|
||||
## Phase 4 — Review and merge
|
||||
One task at a time, in the order the brief lists them.
|
||||
|
||||
1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title and description.
|
||||
2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
|
||||
3. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||
- Clean merge → the task is Done; move on.
|
||||
- Conflict (markers left in the working tree, repoPath returned) → resolve it.
|
||||
|
||||
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.
|
||||
|
||||
Resolving a conflict:
|
||||
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you — fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
|
||||
- For a task WITH children (a unit merge), pass the PARENT task id to continue_merge / abort_merge.
|
||||
- If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing.
|
||||
- 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.
|
||||
|
||||
Rules for the whole session:
|
||||
- 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.
|
||||
- Ask the user for anything ambiguous, risky, or destructive.
|
||||
|
||||
## Phase 5 — Summary
|
||||
Print one line per task from the original brief:
|
||||
title — dedupe action (kept / merged into X / cancelled as duplicate of X) — enhanced (yes/no) — final status — merge commit (if any) — conflicts resolved (if any).
|
||||
|
||||
Then list anything you skipped or left for the user and why, and any follow-ups worth turning into new tasks.
|
||||
""";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Replace `MergeHelperInitialDefault`**
|
||||
|
||||
The scope is now always one list with one repo, so the header states it once and the task lines
|
||||
drop the constant `list:` / `repo:` fields.
|
||||
|
||||
```csharp
|
||||
private const string MergeHelperInitialDefault = """
|
||||
# List handler brief
|
||||
|
||||
Scope: {scope}
|
||||
Repo: {repo}
|
||||
|
||||
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
||||
|
||||
{tasks}
|
||||
|
||||
When every task is handled, print the summary.
|
||||
""";
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the `{repo}` token test**
|
||||
|
||||
`{repo}` is a new token — Task 4 will pass it. Add to `PromptFilesTests`:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void DefaultFor_merge_helper_initial_has_repo_token()
|
||||
{
|
||||
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial);
|
||||
Assert.Contains("{repo}", d);
|
||||
}
|
||||
```
|
||||
|
||||
The existing `RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks` test passes only
|
||||
`scope` and `tasks`. `RenderTemplate` leaves unknown tokens alone, so its two `Assert.Contains`
|
||||
still hold and its `Assert.DoesNotContain("{scope}", outp)` still holds. Leave it unchanged.
|
||||
|
||||
- [ ] **Step 6: Run the tests to verify they pass**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~PromptFilesTests"
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add -- src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||
git commit -m "feat(data): five-phase list-handler prompt with dedupe and enhance" -- src/ClaudeDo.Data/PromptFiles.cs tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Drop the global entry point and the LIST column
|
||||
|
||||
This removes every caller that passes a null `listId`, clearing the way for Task 4's signature
|
||||
sweep. Types stay nullable here; only callers and UI go.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:103-113`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml:184,206-210`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs:14-53`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml:41-72`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json`, `src/ClaudeDo.Localization/locales/de.json`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs`
|
||||
|
||||
- [ ] **Step 1: Update the dialog tests to the list-only API**
|
||||
|
||||
`Configure` becomes `Configure(string listId, string listName)` and `IsGlobal` and `ListName` are
|
||||
gone. Rewrite the affected tests. `Load_ExcludesTerminalStatuses_AndTicksActionableByDefault`,
|
||||
`CanConfirm_FollowsRowSelection` and `Confirm_ReturnsSelectedIds_InRowOrder` all used
|
||||
`Configure(null, null)` to see every seeded task — point them at `"L1"` instead, which holds all
|
||||
eight seeded statuses (`t-other-list` lives in `L2` and drops out).
|
||||
|
||||
Replace the four tests below; leave `Load_NoCandidates_HasTasksFalse_CannotConfirm` untouched.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Load_ExcludesTerminalStatuses_AndTicksActionableByDefault()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L1", "Work");
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.DoesNotContain(vm.Tasks, t => t.Id is "t-done" or "t-cancelled");
|
||||
Assert.DoesNotContain(vm.Tasks, t => t.Id == "t-other-list");
|
||||
Assert.Equal(6, vm.Tasks.Count);
|
||||
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-queued").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-review").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-failed").IsSelected);
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "t-running").IsSelected);
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "t-children").IsSelected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_PerListScope_FiltersToThatList()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L2", "Home");
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.Single(vm.Tasks);
|
||||
Assert.Equal("t-other-list", vm.Tasks[0].Id);
|
||||
Assert.Contains("Home", vm.ScopeLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanConfirm_FollowsRowSelection()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L1", "Work");
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.True(vm.CanConfirm);
|
||||
|
||||
vm.SelectNoneCommand.Execute(null);
|
||||
Assert.False(vm.CanConfirm);
|
||||
Assert.All(vm.Tasks, t => Assert.False(t.IsSelected));
|
||||
|
||||
vm.Tasks[0].IsSelected = true; // single row re-enables via PropertyChanged hook
|
||||
Assert.True(vm.CanConfirm);
|
||||
|
||||
vm.SelectAllCommand.Execute(null);
|
||||
Assert.All(vm.Tasks, t => Assert.True(t.IsSelected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Confirm_ReturnsSelectedIds_InRowOrder()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L1", "Work");
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.SelectNoneCommand.Execute(null);
|
||||
vm.Tasks.Single(t => t.Id == "t-review").IsSelected = true;
|
||||
vm.Tasks.Single(t => t.Id == "t-idle").IsSelected = true;
|
||||
|
||||
var closed = false;
|
||||
vm.CloseAction = () => closed = true;
|
||||
vm.ConfirmCommand.Execute(null);
|
||||
|
||||
var result = await vm.Result.Task;
|
||||
Assert.NotNull(result);
|
||||
// Row order (SortOrder): t-idle was seeded before t-review.
|
||||
Assert.Equal(new[] { "t-idle", "t-review" }, result);
|
||||
Assert.True(closed);
|
||||
}
|
||||
```
|
||||
|
||||
Also change `Cancel_ReturnsNull`'s `vm.Configure(null, null);` to `vm.Configure("L1", "Work");`.
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~MergeHelperSelectionModalViewModelTests"
|
||||
```
|
||||
|
||||
Expected: FAIL — the project does not compile, because `Configure(string, string)` does not exist
|
||||
yet and `IsGlobal` was removed from an assertion that still compiles against it. Compilation
|
||||
failure is the expected "red" here.
|
||||
|
||||
- [ ] **Step 3: Make the dialog VM list-only**
|
||||
|
||||
In `MergeHelperSelectionModalViewModel.cs`:
|
||||
|
||||
Remove the `ListName` property from `MergeHelperTaskRowViewModel`:
|
||||
|
||||
```csharp
|
||||
public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string StatusText { get; init; }
|
||||
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
}
|
||||
```
|
||||
|
||||
Change the field to non-nullable, drop `IsGlobal`, and make `Configure` list-only:
|
||||
|
||||
```csharp
|
||||
private string _listId = "";
|
||||
```
|
||||
|
||||
```csharp
|
||||
[ObservableProperty] private string _scopeLabel = "";
|
||||
|
||||
public bool HasTasks => Tasks.Count > 0;
|
||||
```
|
||||
|
||||
```csharp
|
||||
public void Configure(string listId, string listName)
|
||||
{
|
||||
_listId = listId;
|
||||
ScopeLabel = Loc.T("modals.mergeHelper.scopeList", listName);
|
||||
}
|
||||
```
|
||||
|
||||
In `LoadAsync`, the list filter is now unconditional and `ListName` is no longer selected:
|
||||
|
||||
```csharp
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var candidates = await ctx.Tasks.AsNoTracking()
|
||||
.Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled)
|
||||
.Where(t => t.ListId == _listId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.Select(t => new { t.Id, t.Title, t.Status })
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var c in candidates)
|
||||
{
|
||||
var row = new MergeHelperTaskRowViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
StatusText = c.Status.ToString(),
|
||||
IsSelected = IsTickedByDefault(c.Status),
|
||||
};
|
||||
row.PropertyChanged += OnRowChanged;
|
||||
Tasks.Add(row);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Drop the LIST column from the dialog view**
|
||||
|
||||
In `MergeHelperSelectionModal.axaml`, change both `ColumnDefinitions="32,*,120,120"` (lines 41 and
|
||||
57) to `ColumnDefinitions="32,*,120"`, and delete the two `Grid.Column="3"` elements — the header
|
||||
`TextBlock` bound to `modals.mergeHelper.columnList` (lines 45-46) and the row `TextBlock` bound to
|
||||
`ListName` (lines 68-71).
|
||||
|
||||
- [ ] **Step 5: Remove the global command and the Broom button**
|
||||
|
||||
In `ListsIslandViewModel.cs`, delete the whole `LetClaudeHandleAllAsync` method including its
|
||||
`[RelayCommand]` attribute (lines 103-113). Leave `LetClaudeHandleListAsync` and the
|
||||
`MergeHelperRequest` record as they are — Task 4 changes those.
|
||||
|
||||
In `ListsIslandView.axaml`, revert the button row to two columns:
|
||||
|
||||
```xml
|
||||
<!-- New list + import row -->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
|
||||
```
|
||||
|
||||
and delete the whole `<Button Grid.Column="2" … LetClaudeHandleAllCommand … />` element
|
||||
(lines 206-210) including its `<PathIcon>` child.
|
||||
|
||||
- [ ] **Step 6: Remove the three dead localization keys**
|
||||
|
||||
Delete from **both** `src/ClaudeDo.Localization/locales/en.json` and
|
||||
`src/ClaudeDo.Localization/locales/de.json`:
|
||||
|
||||
- `lists.letClaudeAllTip` (and the trailing comma on the preceding key, so the object stays valid JSON)
|
||||
- `modals.mergeHelper.scopeAll`
|
||||
- `modals.mergeHelper.columnList` (and the trailing comma on the preceding key)
|
||||
|
||||
Keep `lists.contextLetClaude` and `modals.mergeHelper.scopeList`.
|
||||
|
||||
- [ ] **Step 7: Build and run the tests**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
Expected: build succeeds, all tests pass. The localization parity test is the one that catches a
|
||||
key removed from only one of the two JSON files.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add -- src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs
|
||||
git commit -m "refactor(ui): scope \"Let Claude handle it\" to a single list" -- src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Modals/MergeHelperSelectionModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/MergeHelperSelectionModal.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/MergeHelperSelectionModalViewModelTests.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Non-nullable `listId` and a single-repo launch spec
|
||||
|
||||
One atomic commit across Worker and Ui. Splitting it would leave one side passing `string?` into a
|
||||
`string` parameter — nullable warnings strewn across a commit boundary, and a launch spec that
|
||||
still carries a dead multi-repo path.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs:36`
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs:166-253`
|
||||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:682-687`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:90`
|
||||
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs:525-526`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs:325-357`
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:20`
|
||||
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs:103`
|
||||
- Modify: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:78`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs:363-472`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs:409-463`
|
||||
|
||||
- [ ] **Step 1: Rewrite the launch-spec tests**
|
||||
|
||||
In `InteractiveLaunchSpecServiceTests.cs`, replace the four merge-helper `[Fact]`s (from
|
||||
`BuildForMergeHelperAsync_EmptyTaskIds_ThrowsInvalidOperation` through
|
||||
`BuildForMergeHelperAsync_WithListId_UsesListWorkingDirAsCwdAndListScope`) with these five. Keep
|
||||
the `_mergeHelperSessionDirs` field and `TrackSessionDir` helper above them exactly as they are.
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_EmptyTaskIds_ThrowsInvalidOperation()
|
||||
{
|
||||
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||
var svc = BuildService();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_ListWithoutExistingWorkingDir_ThrowsInvalidOperation()
|
||||
{
|
||||
var listId = await SeedListAsync(workingDir: Path.Combine(_tempDir, "gone"));
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.WaitingForReview);
|
||||
|
||||
var svc = BuildService();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, CancellationToken.None));
|
||||
Assert.Contains("working directory", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_UnknownList_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(taskId, listId, TaskStatus.Idle);
|
||||
|
||||
var svc = BuildService();
|
||||
await Assert.ThrowsAsync<KeyNotFoundException>(
|
||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_BuildsListScopedSpecWithSingleRepo()
|
||||
{
|
||||
var repo = Path.Combine(_tempDir, "repoOnly");
|
||||
Directory.CreateDirectory(repo);
|
||||
|
||||
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||
var t1 = Guid.NewGuid().ToString();
|
||||
var t2 = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||
|
||||
var svc = BuildService();
|
||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
||||
var sessionDir = TrackSessionDir(spec);
|
||||
|
||||
Assert.Equal(repo, spec.Cwd);
|
||||
Assert.Equal(_claudeStubPath, spec.Exe);
|
||||
|
||||
var args = spec.Args.ToList();
|
||||
|
||||
var pmIdx = args.IndexOf("--permission-mode");
|
||||
Assert.True(pmIdx >= 0);
|
||||
Assert.Equal("default", args[pmIdx + 1]);
|
||||
|
||||
var atIdx = args.IndexOf("--allowedTools");
|
||||
Assert.Equal("mcp__claudedo__*,Read,Grep,Glob,Edit,Bash,WebFetch,WebSearch,Skill", args[atIdx + 1]);
|
||||
|
||||
// --add-dir: session dir + the list's single repo dir
|
||||
var addIdx = args.IndexOf("--add-dir");
|
||||
var appendIdx = args.IndexOf("--append-system-prompt-file");
|
||||
var addDirs = args.GetRange(addIdx + 1, appendIdx - addIdx - 1);
|
||||
Assert.Equal(new[] { sessionDir, repo }, addDirs);
|
||||
|
||||
var systemPromptPath = args[appendIdx + 1];
|
||||
Assert.Equal(Path.Combine(sessionDir, "system-prompt.md"), systemPromptPath);
|
||||
Assert.True(File.Exists(systemPromptPath));
|
||||
|
||||
// kickoff is the LAST arg (positional), single line, points at brief.md
|
||||
var kickoff = args[^1];
|
||||
var briefPath = Path.Combine(sessionDir, "brief.md");
|
||||
Assert.Contains(briefPath, kickoff);
|
||||
Assert.DoesNotContain('\n', kickoff);
|
||||
|
||||
Assert.Equal("200000", spec.Env["MCP_TOOL_TIMEOUT"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildForMergeHelperAsync_BriefNamesListRepoAndEveryTask()
|
||||
{
|
||||
var repo = Path.Combine(_tempDir, "repoBrief");
|
||||
Directory.CreateDirectory(repo);
|
||||
|
||||
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||
var t1 = Guid.NewGuid().ToString();
|
||||
var t2 = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||
|
||||
var svc = BuildService();
|
||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, CancellationToken.None);
|
||||
var sessionDir = TrackSessionDir(spec);
|
||||
|
||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||
Assert.Contains("Scope: List: Alpha", brief);
|
||||
Assert.Contains($"Repo: {repo}", brief);
|
||||
Assert.Contains("First task", brief);
|
||||
Assert.Contains("Second task", brief);
|
||||
Assert.Contains(t1, brief);
|
||||
Assert.Contains(t2, brief);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the launch-spec tests to verify they fail**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~InteractiveLaunchSpecServiceTests.BuildForMergeHelper"
|
||||
```
|
||||
|
||||
Expected: FAIL. `BuildForMergeHelperAsync_UnknownList_Throws` fails because the current code
|
||||
resolves the repo from the tasks and never validates the list; the brief test fails on the missing
|
||||
`Repo:` line.
|
||||
|
||||
- [ ] **Step 3: Rewrite `BuildForMergeHelperAsync`**
|
||||
|
||||
Replace the method body (`InteractiveLaunchSpecService.cs:166-253`) with:
|
||||
|
||||
```csharp
|
||||
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct)
|
||||
{
|
||||
if (taskIds.Count == 0)
|
||||
throw new InvalidOperationException("No tasks selected for the list handler.");
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var taskRepo = new TaskRepository(ctx);
|
||||
var listRepo = new ListRepository(ctx);
|
||||
|
||||
var list = await listRepo.GetByIdAsync(listId, ct)
|
||||
?? throw new KeyNotFoundException($"List not found: {listId}");
|
||||
|
||||
var repoDir = list.WorkingDir;
|
||||
if (string.IsNullOrEmpty(repoDir) || !Directory.Exists(repoDir))
|
||||
throw new InvalidOperationException($"list '{list.Name}' has no existing working directory");
|
||||
|
||||
var briefLines = new List<string>();
|
||||
foreach (var id in taskIds)
|
||||
{
|
||||
var task = await taskRepo.GetByIdAsync(id, ct)
|
||||
?? throw new KeyNotFoundException($"Task not found: {id}");
|
||||
briefLines.Add($"- [{task.Status}] {task.Title} (id: {task.Id})");
|
||||
}
|
||||
|
||||
var sessionDir = Path.Combine(Paths.AppDataRoot(), "merge-helper-sessions", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(sessionDir);
|
||||
|
||||
var systemPromptPath = Path.Combine(sessionDir, "system-prompt.md");
|
||||
await File.WriteAllTextAsync(systemPromptPath, PromptFiles.ReadOrDefault(PromptKind.MergeHelper), ct);
|
||||
|
||||
var briefPath = Path.Combine(sessionDir, "brief.md");
|
||||
await File.WriteAllTextAsync(briefPath, PromptFiles.Render(PromptKind.MergeHelperInitial,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["scope"] = $"List: {list.Name}",
|
||||
["repo"] = repoDir,
|
||||
["tasks"] = string.Join("\n", briefLines),
|
||||
}), ct);
|
||||
|
||||
var resolvedClaude = WindowsTerminalLauncher.Resolve(_claudePath)
|
||||
?? throw new InvalidOperationException($"claude executable not found: {_claudePath}");
|
||||
|
||||
// Mirrors WindowsTerminalLauncher.BuildPlanningStartArgs ordering: variadic flags
|
||||
// (--allowedTools, --add-dir) first, then a single-value flag, then the single-line
|
||||
// positional kickoff LAST — a multi-line positional prompt truncates at the first
|
||||
// newline, so the full multi-line brief travels via the file exposed through --add-dir.
|
||||
var args = new List<string>
|
||||
{
|
||||
"--permission-mode", "default",
|
||||
"--allowedTools", MergeHelperAllowedTools,
|
||||
"--add-dir", sessionDir, repoDir,
|
||||
"--append-system-prompt-file", systemPromptPath,
|
||||
$"Read the file {briefPath} first. It lists the tasks you must handle and their status. " +
|
||||
"After reading it, begin the session as your instructions describe.",
|
||||
};
|
||||
|
||||
var env = new Dictionary<string, string>
|
||||
{
|
||||
["MCP_TOOL_TIMEOUT"] = "200000",
|
||||
};
|
||||
|
||||
return new LaunchSpec(cwd: repoDir, resolvedClaude, args, env);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Flip the interface and hub signatures**
|
||||
|
||||
`src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs:36`:
|
||||
|
||||
```csharp
|
||||
Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct);
|
||||
```
|
||||
|
||||
`src/ClaudeDo.Worker/Hub/WorkerHub.cs:682`:
|
||||
|
||||
```csharp
|
||||
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId) => HubGuard(() =>
|
||||
```
|
||||
|
||||
(leave the method body as it is).
|
||||
|
||||
- [ ] **Step 5: Run the Worker tests**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release \
|
||||
--filter "FullyQualifiedName~InteractiveLaunchSpecServiceTests"
|
||||
```
|
||||
|
||||
Expected: build succeeds, all pass. `TasksIslandViewModelPlanningTests.cs:78` holds a fake
|
||||
implementing `IWorkerClient` — its signature is flipped in Step 7; if the Worker.Tests build fails
|
||||
there, do Step 7 first and re-run.
|
||||
|
||||
- [ ] **Step 6: Update the Mission Control tests**
|
||||
|
||||
In `MissionControlViewModelTests.cs`, the four `OpenMergeHelperConPtySessionAsync` calls pass
|
||||
`null` as the list id. Replace `null` with `"L1"` on lines 421, 435, 436 and 461, and change the
|
||||
`ThrowingMergeHelperLaunchSpecWorker` override signature at line 411 to:
|
||||
|
||||
```csharp
|
||||
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||
```
|
||||
|
||||
The list-title lookup in `OpenMergeHelperConPtySessionAsync` is wrapped in a `try/catch` and falls
|
||||
back to the plain title, so an unseeded `"L1"` is harmless.
|
||||
|
||||
- [ ] **Step 7: Flip the UI signatures**
|
||||
|
||||
`src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:90`:
|
||||
|
||||
```csharp
|
||||
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default);
|
||||
```
|
||||
|
||||
`src/ClaudeDo.Ui/Services/WorkerClient.cs:525`:
|
||||
|
||||
```csharp
|
||||
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
||||
```
|
||||
|
||||
`tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs:103` and
|
||||
`tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs:78` — same parameter change
|
||||
(`string? listId` → `string listId`), bodies unchanged.
|
||||
|
||||
`src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs:20`:
|
||||
|
||||
```csharp
|
||||
/// <summary>Confirmed handler run: the scope list and the ordered selected task ids.</summary>
|
||||
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds);
|
||||
```
|
||||
|
||||
`src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs:325-341` — the `listId is not null` guard is
|
||||
now dead:
|
||||
|
||||
```csharp
|
||||
// List-handler session over a hand-picked set of tasks ("Let Claude handle it").
|
||||
// Ad-hoc style: no owning task, never deduped — every run opens a fresh pane.
|
||||
public async System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
||||
{
|
||||
if (taskIds is not { Count: > 0 }) return;
|
||||
|
||||
var title = Loc.T("missionControl.mergeHelperTitle");
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var list = await ctx.Lists.AsNoTracking().FirstOrDefaultAsync(l => l.Id == listId);
|
||||
if (list?.Name is { Length: > 0 } name) title = $"{title} — {name}";
|
||||
}
|
||||
catch { /* best-effort title lookup */ }
|
||||
```
|
||||
|
||||
Leave the rest of the method (the `try` block that fetches the spec and adds the pane) unchanged.
|
||||
|
||||
- [ ] **Step 8: Build everything and run the full suite**
|
||||
|
||||
```bash
|
||||
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
|
||||
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
Expected: both builds succeed with no `CS8600`/`CS8604` nullability warnings on the touched files,
|
||||
and every test passes.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add -- src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs
|
||||
git commit -m "refactor(worker): make the list-handler launch spec single-list and single-repo" -- src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Runner/Interfaces/IInteractiveLaunchSpecService.cs src/ClaudeDo.Worker/Hub/WorkerHub.cs src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs src/ClaudeDo.Ui/ViewModels/MissionControlViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update the project docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/CLAUDE.md`
|
||||
- Modify: `src/ClaudeDo.Worker/CLAUDE.md`
|
||||
- Modify: `docs/open.md`
|
||||
|
||||
- [ ] **Step 1: Check what the CLAUDE.md files claim**
|
||||
|
||||
```bash
|
||||
grep -n "merge.helper\|Let Claude handle\|MergeHelper" src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Correct any stale claim**
|
||||
|
||||
Where those files describe the merge helper as having a global scope, or describe the prompt as
|
||||
run-and-merge only, update them to: list-scoped only, single repo, five phases (read, dedupe,
|
||||
enhance, queue, review+merge). Note in `src/ClaudeDo.Worker/CLAUDE.md` that
|
||||
`update_task_status` now also accepts `Cancelled`. Do not restructure the files beyond that.
|
||||
|
||||
- [ ] **Step 3: Add the open verification items**
|
||||
|
||||
Append to the open-items section of `docs/open.md`:
|
||||
|
||||
```markdown
|
||||
- **List handler (2026-07-27)** — visual pass: the Broom button is gone from the lists footer,
|
||||
the context-menu item appears only on lists with a working dir, and the selection dialog has no
|
||||
LIST column. Plus a real-Claude smoke run of the five phases (dedupe questions, enhancements
|
||||
landing in task descriptions, queued execution, merges).
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -- src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||
git commit -m "docs: describe the list-scoped five-phase handler" -- src/ClaudeDo.Ui/CLAUDE.md src/ClaudeDo.Worker/CLAUDE.md docs/open.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification left to the user
|
||||
|
||||
None of this can be confirmed from tests alone:
|
||||
|
||||
- The lists footer no longer shows the Broom button, and the row context menu still offers
|
||||
"Let Claude handle it" for lists with a working dir.
|
||||
- The selection dialog shows TASK and STATUS only, and the scope line reads `List: <name>`.
|
||||
- A real ConPTY run: Phase 1 asks about duplicates, Phase 2's enhancements are visible in the task
|
||||
descriptions afterwards, Phase 3 reports the slot count and the tasks execute, Phase 4 merges or
|
||||
hands off to conflict resolution.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,939 @@
|
||||
# Handler-Run Links 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:** A "Let Claude handle it" run records which tasks it processed, shows them as a list on the handler task's detail pane, and wears a HANDLER badge instead of MANUAL.
|
||||
|
||||
**Architecture:** One new nullable column `TaskEntity.HandlerTaskId` (1:n, last run wins) stamped at handler-task creation from the selection the UI already passes down. The badge is a display-only computed property on `TaskRowViewModel`, driven by the existing `HandlerBaseCommit`. The panel reuses `ChildOutcomeRowViewModel` and the existing refresh path.
|
||||
|
||||
**Tech Stack:** .NET 8, EF Core (SQLite), Avalonia 12 + CommunityToolkit.Mvvm, xUnit.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-07-handler-run-links-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Modified:**
|
||||
- `src/ClaudeDo.Data/Models/TaskEntity.cs` — new `HandlerTaskId` property
|
||||
- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — column mapping + index
|
||||
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — `SetHandlerTaskIdAsync`
|
||||
- `src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs` — stamp after creating the handler task
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs` — `HandlerBaseCommit`, `IsHandlerRun`, `HandlerBadge`, `ManualBadge` precedence
|
||||
- `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml` — HANDLER badge border
|
||||
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `HandlerBadgeBrush` + `Border.badge.handler`
|
||||
- `src/ClaudeDo.Localization/locales/en.json` + `de.json` — `tasks.badgeHandler`, `tasks.handlerTip`
|
||||
- `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `HandledTasks` collection, loader, clear, refresh
|
||||
- `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` — HANDLED TASKS panel
|
||||
- `src/ClaudeDo.Data/CLAUDE.md`, `src/ClaudeDo.Ui/CLAUDE.md`, `docs/explore-notes/conpty-sessions.md` — docs
|
||||
|
||||
**Created:**
|
||||
- `src/ClaudeDo.Data/Migrations/<timestamp>_AddHandlerTaskId.cs` (+ Designer, + snapshot update) — generated
|
||||
- `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryHandlerLinkTests.cs`
|
||||
- `tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowViewModelHandlerBadgeTests.cs`
|
||||
- `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandHandledTasksTests.cs`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Data — `HandlerTaskId` column and migration
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs:60-61`
|
||||
- Modify: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs:96-97` and `:127-131`
|
||||
- Create: `src/ClaudeDo.Data/Migrations/<timestamp>_AddHandlerTaskId.cs` (generated)
|
||||
|
||||
- [ ] **Step 1: Add the property**
|
||||
|
||||
In `src/ClaudeDo.Data/Models/TaskEntity.cs`, directly after the existing `HandlerHeadCommit` line (`public string? HandlerHeadCommit { get; set; }`), add:
|
||||
|
||||
```csharp
|
||||
|
||||
// Id of the "list handler" run task that processed this task ("Let Claude handle it").
|
||||
// 1:n and last-run-wins -- a second handler run over the same task overwrites it. Deliberately
|
||||
// NOT ParentTaskId: that is the planning-child relation and drives the indented tree rendering.
|
||||
// No FK: a deleted handler task must not cascade into the tasks it merely touched.
|
||||
public string? HandlerTaskId { get; set; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Map the column and index it**
|
||||
|
||||
In `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`, after the line
|
||||
`builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");` add:
|
||||
|
||||
```csharp
|
||||
builder.Property(t => t.HandlerTaskId).HasColumnName("handler_task_id");
|
||||
```
|
||||
|
||||
At the end of `Configure`, after the line
|
||||
`builder.HasIndex(t => t.BlockedByTaskId).HasDatabaseName("idx_tasks_blocked_by");` add:
|
||||
|
||||
```csharp
|
||||
builder.HasIndex(t => t.HandlerTaskId).HasDatabaseName("idx_tasks_handler_task_id");
|
||||
```
|
||||
|
||||
Do **not** add a `HasOne`/`HasForeignKey` relationship — the column is intentionally FK-less.
|
||||
|
||||
- [ ] **Step 3: Generate the migration**
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add AddHandlerTaskId --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||||
```
|
||||
|
||||
Expected: creates `src/ClaudeDo.Data/Migrations/<timestamp>_AddHandlerTaskId.cs` + `.Designer.cs` and updates `ClaudeDoDbContextModelSnapshot.cs`. The `Up` method must contain exactly one `AddColumn<string>(name: "handler_task_id", table: "tasks", nullable: true)` and one `CreateIndex(name: "idx_tasks_handler_task_id", table: "tasks", column: "handler_task_id")`. If it contains anything else, another agent's uncommitted model change leaked in — delete the migration, coordinate, retry.
|
||||
|
||||
If `dotnet ef` is unavailable, hand-author the migration + Designer mirroring
|
||||
`src/ClaudeDo.Data/Migrations/20260806111454_AddInteractiveSessionId.cs`, and add
|
||||
`Property<string>("HandlerTaskId").HasColumnType("TEXT").HasColumnName("handler_task_id");`
|
||||
plus the index to the `TaskEntity` builder in `ClaudeDoDbContextModelSnapshot.cs`.
|
||||
|
||||
- [ ] **Step 4: Build**
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj -c Release`
|
||||
Expected: `Build succeeded`, 0 errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs src/ClaudeDo.Data/Migrations
|
||||
git commit -- src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs src/ClaudeDo.Data/Migrations -m "feat(data): add handler_task_id to link handled tasks to their handler run"
|
||||
```
|
||||
|
||||
⚠️ Always commit with explicit paths (`git commit -- <paths>`), never a bare `git commit` — the
|
||||
main checkout is shared with concurrent sessions.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Data — `SetHandlerTaskIdAsync` repository method
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs` (after `SetHandlerHeadCommitAsync`, currently `:394-403`)
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryHandlerLinkTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryHandlerLinkTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Repositories;
|
||||
|
||||
/// Covers the handler-run link: SetHandlerTaskIdAsync stamps the tasks a "Let Claude handle it"
|
||||
/// run processed, so the handler task's detail pane can list them after the run.
|
||||
public sealed class TaskRepositoryHandlerLinkTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
private readonly ClaudeDoDbContext _ctx;
|
||||
private readonly TaskRepository _tasks;
|
||||
private readonly ListRepository _lists;
|
||||
|
||||
public TaskRepositoryHandlerLinkTests()
|
||||
{
|
||||
_ctx = _db.CreateContext();
|
||||
_tasks = new TaskRepository(_ctx);
|
||||
_lists = new ListRepository(_ctx);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ctx.Dispose();
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
private async Task<string> CreateListAsync()
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
await _lists.AddAsync(new ListEntity
|
||||
{
|
||||
Id = listId,
|
||||
Name = "Test List",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
return listId;
|
||||
}
|
||||
|
||||
private async Task<string> AddTaskAsync(string listId)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString();
|
||||
await _tasks.AddAsync(new TaskEntity
|
||||
{
|
||||
Id = id,
|
||||
ListId = listId,
|
||||
Title = "T",
|
||||
Status = TaskStatus.Idle,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetHandlerTaskIdAsync_StampsAllGivenTasks()
|
||||
{
|
||||
var listId = await CreateListAsync();
|
||||
var a = await AddTaskAsync(listId);
|
||||
var b = await AddTaskAsync(listId);
|
||||
var handlerId = await AddTaskAsync(listId);
|
||||
|
||||
var affected = await _tasks.SetHandlerTaskIdAsync(new[] { a, b }, handlerId);
|
||||
|
||||
Assert.Equal(2, affected);
|
||||
Assert.Equal(handlerId, (await _tasks.GetByIdAsync(a))!.HandlerTaskId);
|
||||
Assert.Equal(handlerId, (await _tasks.GetByIdAsync(b))!.HandlerTaskId);
|
||||
Assert.Null((await _tasks.GetByIdAsync(handlerId))!.HandlerTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetHandlerTaskIdAsync_IgnoresUnknownIds()
|
||||
{
|
||||
var listId = await CreateListAsync();
|
||||
var a = await AddTaskAsync(listId);
|
||||
var handlerId = await AddTaskAsync(listId);
|
||||
|
||||
var affected = await _tasks.SetHandlerTaskIdAsync(
|
||||
new[] { a, "does-not-exist" }, handlerId);
|
||||
|
||||
Assert.Equal(1, affected);
|
||||
Assert.Equal(handlerId, (await _tasks.GetByIdAsync(a))!.HandlerTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetHandlerTaskIdAsync_SecondRunOverwrites()
|
||||
{
|
||||
var listId = await CreateListAsync();
|
||||
var a = await AddTaskAsync(listId);
|
||||
var firstHandler = await AddTaskAsync(listId);
|
||||
var secondHandler = await AddTaskAsync(listId);
|
||||
|
||||
await _tasks.SetHandlerTaskIdAsync(new[] { a }, firstHandler);
|
||||
await _tasks.SetHandlerTaskIdAsync(new[] { a }, secondHandler);
|
||||
|
||||
Assert.Equal(secondHandler, (await _tasks.GetByIdAsync(a))!.HandlerTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetHandlerTaskIdAsync_EmptyList_IsNoOp()
|
||||
{
|
||||
var listId = await CreateListAsync();
|
||||
var handlerId = await AddTaskAsync(listId);
|
||||
|
||||
var affected = await _tasks.SetHandlerTaskIdAsync(Array.Empty<string>(), handlerId);
|
||||
|
||||
Assert.Equal(0, affected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~TaskRepositoryHandlerLinkTests"`
|
||||
Expected: compile error — `TaskRepository` does not contain a definition for `SetHandlerTaskIdAsync`.
|
||||
|
||||
- [ ] **Step 3: Implement the method**
|
||||
|
||||
In `src/ClaudeDo.Data/Repositories/TaskRepository.cs`, directly after `SetHandlerHeadCommitAsync`, add:
|
||||
|
||||
```csharp
|
||||
// Links the tasks a "list handler" run processed back to the handler's own task, so the
|
||||
// handler's detail pane can list them after the run. Stamped from the user's selection at
|
||||
// creation time -- that way tasks the handler later cancels as duplicates stay visible.
|
||||
// Unknown ids are silently skipped. Returns the number of rows actually stamped.
|
||||
public async Task<int> SetHandlerTaskIdAsync(
|
||||
IReadOnlyList<string> taskIds,
|
||||
string handlerTaskId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (taskIds.Count == 0) return 0;
|
||||
|
||||
var ids = taskIds.Where(id => id != handlerTaskId).Distinct().ToList();
|
||||
if (ids.Count == 0) return 0;
|
||||
|
||||
return await _context.Tasks
|
||||
.Where(t => ids.Contains(t.Id))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.HandlerTaskId, handlerTaskId), ct);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~TaskRepositoryHandlerLinkTests"`
|
||||
Expected: `Passed! - Failed: 0, Passed: 4`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryHandlerLinkTests.cs
|
||||
git commit -- src/ClaudeDo.Data/Repositories/TaskRepository.cs tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryHandlerLinkTests.cs -m "feat(data): add SetHandlerTaskIdAsync to stamp handled tasks"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Worker — stamp the selection when the handler task is created
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs:409-450`
|
||||
- Test: `tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs` (append a `[Fact]` in the `── CreateMergeHelperTaskAsync ──` region, currently starting at `:747`)
|
||||
|
||||
Note: `CreateMergeHelperTaskAsync` already receives `IReadOnlyList<string> taskIds` — the UI →
|
||||
`IWorkerClient` → `WorkerHub` chain needs **no** change.
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Append to `tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs` inside the same test class, after the existing `CreateMergeHelperTaskAsync_CreatesIdleManualTask_StampsHandlerBaseCommit` test:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task CreateMergeHelperTaskAsync_StampsHandlerTaskIdOnSelectedTasks()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
var listId = await SeedListAsync(workingDir: repo.RepoDir, name: "Alpha");
|
||||
var t1 = Guid.NewGuid().ToString();
|
||||
var t2 = Guid.NewGuid().ToString();
|
||||
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||
|
||||
var svc = BuildService();
|
||||
var handlerId = await svc.CreateMergeHelperTaskAsync(
|
||||
new[] { t1, t2 }, listId, "List handler: Alpha", "Tasks handled by this run:", CancellationToken.None);
|
||||
|
||||
using var readCtx = _db.CreateContext();
|
||||
var tasks = new TaskRepository(readCtx);
|
||||
Assert.Equal(handlerId, (await tasks.GetByIdAsync(t1))!.HandlerTaskId);
|
||||
Assert.Equal(handlerId, (await tasks.GetByIdAsync(t2))!.HandlerTaskId);
|
||||
// The handler never links to itself.
|
||||
Assert.Null((await tasks.GetByIdAsync(handlerId))!.HandlerTaskId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~CreateMergeHelperTaskAsync_StampsHandlerTaskIdOnSelectedTasks"`
|
||||
Expected: FAIL — `Assert.Equal() Failure: Values differ … Actual: null`.
|
||||
|
||||
- [ ] **Step 3: Stamp the selection**
|
||||
|
||||
In `src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs`, in `CreateMergeHelperTaskAsync`, replace:
|
||||
|
||||
```csharp
|
||||
await taskRepo.AddAsync(handlerTask, ct);
|
||||
|
||||
return handlerTask.Id;
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
await taskRepo.AddAsync(handlerTask, ct);
|
||||
|
||||
// Link the selection back to this run BEFORE the session starts: the handler cancels
|
||||
// duplicates in phase 1, and those still belong in the "what was this run supposed to do"
|
||||
// list. Stamping later (e.g. at handoff) would lose them.
|
||||
await taskRepo.SetHandlerTaskIdAsync(taskIds, handlerTask.Id, ct);
|
||||
|
||||
return handlerTask.Id;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter "FullyQualifiedName~CreateMergeHelperTaskAsync"`
|
||||
Expected: `Passed! - Failed: 0` (all five `CreateMergeHelperTaskAsync` tests).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs
|
||||
git commit -- src/ClaudeDo.Worker/Runner/InteractiveLaunchSpecService.cs tests/ClaudeDo.Worker.Tests/Runner/InteractiveLaunchSpecServiceTests.cs -m "feat(handler): link the selected tasks to the handler run task"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Ui — HANDLER badge instead of MANUAL
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs:40,51,234-240,308-329`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml:141-144`
|
||||
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml:114-118` and `:987-990`
|
||||
- Modify: `src/ClaudeDo.Localization/locales/en.json:163-164`, `src/ClaudeDo.Localization/locales/de.json:163-164`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowViewModelHandlerBadgeTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowViewModelHandlerBadgeTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
/// A "list handler" host task is IsManual=true so automation skips it, but MANUAL reads wrong on
|
||||
/// it -- the HANDLER badge must win and MANUAL must disappear.
|
||||
public class TaskRowViewModelHandlerBadgeTests
|
||||
{
|
||||
[Fact]
|
||||
public void HandlerTask_ShowsHandlerBadge_AndSuppressesManualBadge()
|
||||
{
|
||||
var row = new TaskRowViewModel { Id = "t1" };
|
||||
row.IsManual = true;
|
||||
row.HandlerBaseCommit = "base123";
|
||||
|
||||
Assert.True(row.IsHandlerRun);
|
||||
Assert.NotNull(row.HandlerBadge);
|
||||
Assert.Null(row.ManualBadge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlainManualTask_StillShowsManualBadge()
|
||||
{
|
||||
var row = new TaskRowViewModel { Id = "t2" };
|
||||
row.IsManual = true;
|
||||
|
||||
Assert.False(row.IsHandlerRun);
|
||||
Assert.Null(row.HandlerBadge);
|
||||
Assert.NotNull(row.ManualBadge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateFromEntity_MirrorsHandlerBaseCommit()
|
||||
{
|
||||
var row = new TaskRowViewModel { Id = "t3" };
|
||||
row.UpdateFromEntity(new TaskEntity
|
||||
{
|
||||
Id = "t3",
|
||||
ListId = "l1",
|
||||
Title = "List handler: Alpha",
|
||||
Status = TaskStatus.Idle,
|
||||
IsManual = true,
|
||||
HandlerBaseCommit = "base123",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
Assert.Equal("base123", row.HandlerBaseCommit);
|
||||
Assert.True(row.IsHandlerRun);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "FullyQualifiedName~TaskRowViewModelHandlerBadgeTests"`
|
||||
Expected: compile error — `TaskRowViewModel` has no `HandlerBaseCommit` / `IsHandlerRun` / `HandlerBadge`.
|
||||
|
||||
- [ ] **Step 3: Add the properties**
|
||||
|
||||
In `src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs`, after the `_isManual` field
|
||||
declaration (`[ObservableProperty] private bool _isManual;`), add:
|
||||
|
||||
```csharp
|
||||
// Mirror of TaskEntity.HandlerBaseCommit -- non-null marks this row as a "list handler" run
|
||||
// host task ("Let Claude handle it"), which wears HANDLER instead of MANUAL.
|
||||
[ObservableProperty] private string? _handlerBaseCommit;
|
||||
```
|
||||
|
||||
Replace the `ManualBadge` line (currently `public string? ManualBadge => IsManual ? Loc.T("tasks.badgeManual") : null;`) with:
|
||||
|
||||
```csharp
|
||||
public bool IsHandlerRun => !string.IsNullOrEmpty(HandlerBaseCommit);
|
||||
|
||||
public string? HandlerBadge => IsHandlerRun ? Loc.T("tasks.badgeHandler") : null;
|
||||
|
||||
// HANDLER outranks MANUAL: a handler host task is IsManual only so automation skips it, and
|
||||
// "MANUAL" would read as a hand-written reminder. The two badges never show together.
|
||||
public bool ShowManualBadge => IsManual && !IsHandlerRun;
|
||||
|
||||
public string? ManualBadge => ShowManualBadge ? Loc.T("tasks.badgeManual") : null;
|
||||
```
|
||||
|
||||
Add a change hook next to the existing `OnIsManualChanged` partial method:
|
||||
|
||||
```csharp
|
||||
partial void OnHandlerBaseCommitChanged(string? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsHandlerRun));
|
||||
OnPropertyChanged(nameof(HandlerBadge));
|
||||
OnPropertyChanged(nameof(ShowManualBadge));
|
||||
OnPropertyChanged(nameof(ManualBadge));
|
||||
}
|
||||
```
|
||||
|
||||
Inside the existing `OnIsManualChanged`, next to the existing `OnPropertyChanged(nameof(ManualBadge));` line, add:
|
||||
|
||||
```csharp
|
||||
OnPropertyChanged(nameof(ShowManualBadge));
|
||||
```
|
||||
|
||||
In `UpdateFromEntity`, after the line `IsManual = t.IsManual;` add:
|
||||
|
||||
```csharp
|
||||
HandlerBaseCommit = t.HandlerBaseCommit;
|
||||
```
|
||||
|
||||
Also add `HandlerBadge` to `RefreshLocalized`, next to the existing `PlanningBadge` line:
|
||||
|
||||
```csharp
|
||||
OnPropertyChanged(nameof(HandlerBadge));
|
||||
OnPropertyChanged(nameof(ManualBadge));
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the locale keys**
|
||||
|
||||
In `src/ClaudeDo.Localization/locales/en.json`, after `"manualTip": ...` (line 164) add:
|
||||
|
||||
```json
|
||||
"badgeHandler": "HANDLER",
|
||||
"handlerTip": "Handler run — see the tasks it processed in the detail pane",
|
||||
```
|
||||
|
||||
In `src/ClaudeDo.Localization/locales/de.json`, after `"manualTip": ...` (line 164) add:
|
||||
|
||||
```json
|
||||
"badgeHandler": "HANDLER",
|
||||
"handlerTip": "Handler-Run — die bearbeiteten Tasks stehen im Detailbereich",
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the badge style and brush**
|
||||
|
||||
In `src/ClaudeDo.Ui/Design/IslandStyles.axaml`, after the line
|
||||
`<SolidColorBrush x:Key="ManualBadgeBrush" Color="{StaticResource TextFaintColor}"/>` add:
|
||||
|
||||
```xml
|
||||
<SolidColorBrush x:Key="HandlerBadgeBrush" Color="{StaticResource PeatSoftColor}"/>
|
||||
```
|
||||
|
||||
After the existing `Border.badge.manual` style block add:
|
||||
|
||||
```xml
|
||||
<!-- handler → peat: a "Let Claude handle it" run host, not a hand-written reminder -->
|
||||
<Style Selector="Border.badge.handler">
|
||||
<Setter Property="Background" Value="{DynamicResource HandlerBadgeBrush}"/>
|
||||
</Style>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Render the badge**
|
||||
|
||||
In `src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml`, replace the manual badge block (lines 141-144):
|
||||
|
||||
```xml
|
||||
<Border Classes="badge manual" IsVisible="{Binding IsManual}"
|
||||
ToolTip.Tip="{loc:Tr tasks.manualTip}">
|
||||
<TextBlock Text="{Binding ManualBadge}"/>
|
||||
</Border>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```xml
|
||||
<Border Classes="badge manual" IsVisible="{Binding ShowManualBadge}"
|
||||
ToolTip.Tip="{loc:Tr tasks.manualTip}">
|
||||
<TextBlock Text="{Binding ManualBadge}"/>
|
||||
</Border>
|
||||
<Border Classes="badge handler" IsVisible="{Binding IsHandlerRun}"
|
||||
ToolTip.Tip="{loc:Tr tasks.handlerTip}">
|
||||
<TextBlock Text="{Binding HandlerBadge}"/>
|
||||
</Border>
|
||||
```
|
||||
|
||||
Only the `IsVisible` binding changed on the manual border (`IsManual` → `ShowManualBadge`); the
|
||||
handler border is new. No converter is needed — `ShowManualBadge` is already a `bool`.
|
||||
|
||||
- [ ] **Step 7: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "FullyQualifiedName~TaskRowViewModelHandlerBadgeTests"`
|
||||
Expected: `Passed! - Failed: 0, Passed: 3`.
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
|
||||
Expected: `Passed! - Failed: 0` (en/de key parity).
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: `Build succeeded` — this compiles the AXAML.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowViewModelHandlerBadgeTests.cs
|
||||
git commit -- src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowViewModelHandlerBadgeTests.cs -m "feat(ui): show a HANDLER badge on list-handler run tasks"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Ui — "HANDLED TASKS" panel on the handler's detail pane
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:248-255`, `:581-584`, `:685`, `:814-833`
|
||||
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml:414-434`
|
||||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandHandledTasksTests.cs` (create)
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandHandledTasksTests.cs`:
|
||||
|
||||
```csharp
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
/// Covers the handler-run link: binding a "list handler" host task lists every task stamped with
|
||||
/// its id, including ones the handler cancelled as duplicates.
|
||||
public class DetailsIslandHandledTasksTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public DetailsIslandHandledTasksTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_details_handled_test_{Guid.NewGuid():N}.db");
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private sealed class NullServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
|
||||
private sealed class StubNotesApi : ClaudeDo.Ui.Services.Interfaces.INotesApi
|
||||
{
|
||||
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) =>
|
||||
Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) =>
|
||||
Task.FromResult<DailyNoteDto?>(null);
|
||||
public Task UpdateAsync(string id, string text) => Task.CompletedTask;
|
||||
public Task DeleteAsync(string id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class FakeWorkerClient : StubWorkerClient
|
||||
{
|
||||
public override bool IsConnected => true;
|
||||
}
|
||||
|
||||
private DetailsIslandViewModel BuildVm()
|
||||
{
|
||||
var factory = new TestDbFactory(NewContext);
|
||||
return new DetailsIslandViewModel(
|
||||
factory, new FakeWorkerClient(), new NullServiceProvider(), new StubNotesApi(), new MergeCoordinator());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bind_HandlerTask_ListsHandledTasksWithTheirStatus()
|
||||
{
|
||||
const string listId = "list-1";
|
||||
const string handlerId = "handler-task-1";
|
||||
|
||||
await using (var ctx = NewContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", WorkingDir = @"C:\repo", CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = handlerId, ListId = listId, Title = "List handler: L",
|
||||
Status = TaskStatus.WaitingForReview, IsManual = true,
|
||||
HandlerBaseCommit = "base123", HandlerHeadCommit = "head456",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
ctx.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = "done-1", ListId = listId, Title = "Merged task",
|
||||
Status = TaskStatus.Done, HandlerTaskId = handlerId,
|
||||
SortOrder = 0, CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
ctx.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = "dupe-1", ListId = listId, Title = "Duplicate the handler cancelled",
|
||||
Status = TaskStatus.Cancelled, HandlerTaskId = handlerId,
|
||||
SortOrder = 1, CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
ctx.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = "unrelated-1", ListId = listId, Title = "Not part of the run",
|
||||
Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var vm = BuildVm();
|
||||
vm.Bind(new TaskRowViewModel { Id = handlerId, Status = TaskStatus.WaitingForReview });
|
||||
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
while (DateTime.UtcNow < deadline && vm.HandledTasks.Count == 0)
|
||||
await Task.Delay(20);
|
||||
|
||||
Assert.Equal(2, vm.HandledTasks.Count);
|
||||
Assert.True(vm.HasHandledTasks);
|
||||
Assert.Equal("Merged task", vm.HandledTasks[0].Title);
|
||||
Assert.Equal(TaskStatus.Done, vm.HandledTasks[0].Status);
|
||||
Assert.Equal(TaskStatus.Cancelled, vm.HandledTasks[1].Status);
|
||||
Assert.DoesNotContain(vm.HandledTasks, r => r.Id == "unrelated-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bind_PlainTask_HasNoHandledTasks()
|
||||
{
|
||||
const string listId = "list-1";
|
||||
const string taskId = "plain-1";
|
||||
|
||||
await using (var ctx = NewContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = taskId, ListId = listId, Title = "Plain",
|
||||
Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var vm = BuildVm();
|
||||
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.Idle });
|
||||
await Task.Delay(300);
|
||||
|
||||
Assert.Empty(vm.HandledTasks);
|
||||
Assert.False(vm.HasHandledTasks);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "FullyQualifiedName~DetailsIslandHandledTasksTests"`
|
||||
Expected: compile error — `DetailsIslandViewModel` has no `HandledTasks` / `HasHandledTasks`.
|
||||
|
||||
- [ ] **Step 3: Add the collection**
|
||||
|
||||
In `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`, after the line
|
||||
`public ObservableCollection<ChildOutcomeRowViewModel> ChildOutcomes { get; } = new();` add:
|
||||
|
||||
```csharp
|
||||
// Tasks a "list handler" run processed ("Let Claude handle it"), linked via
|
||||
// TaskEntity.HandlerTaskId. Separate from ChildOutcomes on purpose: that collection is the
|
||||
// planning/improvement parent's children and feeds the merge card's combined diff, which a
|
||||
// handler run must not touch (it commits straight to the list's working dir).
|
||||
public ObservableCollection<ChildOutcomeRowViewModel> HandledTasks { get; } = new();
|
||||
```
|
||||
|
||||
After the line `public bool HasChildOutcomes => ChildOutcomes.Count > 0;` add:
|
||||
|
||||
```csharp
|
||||
public bool HasHandledTasks => HandledTasks.Count > 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Clear it on rebind**
|
||||
|
||||
In the same file, in the rebind reset block, after the line `ChildOutcomes.Clear();` add:
|
||||
|
||||
```csharp
|
||||
HandledTasks.Clear();
|
||||
```
|
||||
|
||||
and after `OnPropertyChanged(nameof(HasChildOutcomes));` in that same block add:
|
||||
|
||||
```csharp
|
||||
OnPropertyChanged(nameof(HasHandledTasks));
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Load it on bind**
|
||||
|
||||
In the same file, directly after the line `await LoadChildOutcomesAsync(row.Id, ct);` add:
|
||||
|
||||
```csharp
|
||||
await LoadHandledTasksAsync(row.Id, ct);
|
||||
```
|
||||
|
||||
Then add the loader immediately after the closing brace of `LoadChildOutcomesAsync`:
|
||||
|
||||
```csharp
|
||||
// Tasks stamped with this handler run's id. Ordered like the task list itself so the panel
|
||||
// reads in the same order the user picked them.
|
||||
private async System.Threading.Tasks.Task LoadHandledTasksAsync(string handlerTaskId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var handled = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Worktree)
|
||||
.Where(t => t.HandlerTaskId == handlerTaskId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (handled.Count == 0) return;
|
||||
|
||||
HandledTasks.Clear();
|
||||
foreach (var h in handled)
|
||||
HandledTasks.Add(new ChildOutcomeRowViewModel
|
||||
{
|
||||
Id = h.Id,
|
||||
Title = h.Title,
|
||||
Status = h.Status,
|
||||
RoadblockCount = h.RoadblockCount,
|
||||
WorktreeState = h.Worktree?.State ?? ClaudeDo.Data.Models.WorktreeState.Active,
|
||||
});
|
||||
OnPropertyChanged(nameof(HasHandledTasks));
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Keep the rows live**
|
||||
|
||||
In the same file, in `RefreshChildOutcomeAsync`, replace:
|
||||
|
||||
```csharp
|
||||
var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId);
|
||||
if (row is null) return;
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
// The same refresh serves both lists: a planning parent's children and a handler run's
|
||||
// handled tasks. Only one of them can hold a given id.
|
||||
var row = ChildOutcomes.FirstOrDefault(c => c.Id == childTaskId)
|
||||
?? HandledTasks.FirstOrDefault(c => c.Id == childTaskId);
|
||||
if (row is null) return;
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Render the panel**
|
||||
|
||||
In `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`, directly after the closing
|
||||
`</StackPanel>` of the existing `<!-- Child outcomes -->` block, add:
|
||||
|
||||
```xml
|
||||
<!-- Handled tasks (list handler run) -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasHandledTasks}">
|
||||
<TextBlock Classes="section-label" Text="HANDLED TASKS" />
|
||||
<ItemsControl ItemsSource="{Binding HandledTasks}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ChildOutcomeRowViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,2">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Title}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding RoadblockText}"
|
||||
IsVisible="{Binding HasRoadblock}"
|
||||
Foreground="#E0A030"
|
||||
Margin="8,0" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding StatusLabel}"
|
||||
Opacity="0.75" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run the tests to verify they pass**
|
||||
|
||||
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter "FullyQualifiedName~DetailsIslandHandledTasksTests"`
|
||||
Expected: `Passed! - Failed: 0, Passed: 2`.
|
||||
|
||||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||
Expected: `Build succeeded`.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandHandledTasksTests.cs
|
||||
git commit -- src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandHandledTasksTests.cs -m "feat(ui): list the tasks a handler run processed on its detail pane"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Full verification and docs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeDo.Data/CLAUDE.md` (TaskEntity field list)
|
||||
- Modify: `src/ClaudeDo.Ui/CLAUDE.md` (TaskRowViewModel + DetailsIslandViewModel bullets)
|
||||
- Modify: `docs/explore-notes/conpty-sessions.md` (list handler → host task section)
|
||||
|
||||
- [ ] **Step 1: Run every affected test project**
|
||||
|
||||
```bash
|
||||
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj -c Release
|
||||
dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
Expected: `Failed: 0` in all four. If a hand-rolled fake in a test project fails to compile,
|
||||
it is one of the known `IWorkerClient`/ViewModel-ctor fakes — update it; do not skip the test.
|
||||
|
||||
- [ ] **Step 2: Update `src/ClaudeDo.Data/CLAUDE.md`**
|
||||
|
||||
In the `TaskEntity` bullet, append `HandlerTaskId` to the field enumeration (after
|
||||
`HandlerBaseCommit / HandlerHeadCommit`), and add a sub-bullet under the existing
|
||||
`HandlerBaseCommit`/`HandlerHeadCommit` sub-bullet:
|
||||
|
||||
```markdown
|
||||
- `HandlerTaskId` = back-link from a task to the **list handler run** that processed it (1:n, last run wins, no FK). Stamped from the user's selection when the handler task is created, so tasks the handler later cancels as duplicates stay listed. Deliberately not `ParentTaskId` — that is the planning-child relation and drives the indented tree.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `src/ClaudeDo.Ui/CLAUDE.md`**
|
||||
|
||||
In the `DetailsIslandViewModel` bullet, after the `ChildOutcomes` mention, add
|
||||
`, plus `HandledTasks` (tasks a list-handler run processed, via `HandlerTaskId`)`.
|
||||
|
||||
In the `TaskRowViewModel` sentence, after the `IsManual` clause, add
|
||||
`, `IsHandlerRun` (→ HANDLER badge, which outranks MANUAL)`.
|
||||
|
||||
- [ ] **Step 4: Update `docs/explore-notes/conpty-sessions.md`**
|
||||
|
||||
In the "The host task and its commit range" section, add after the existing description:
|
||||
|
||||
```markdown
|
||||
`CreateMergeHelperTaskAsync` also stamps `TaskEntity.HandlerTaskId` on every selected task
|
||||
(`TaskRepository.SetHandlerTaskIdAsync`) before the session starts, so the handler task's detail
|
||||
pane can list what the run was meant to process — including tasks phase 1 cancels as duplicates.
|
||||
The handler never links to itself.
|
||||
```
|
||||
|
||||
Bump that note's "verified against" commit line to the current HEAD.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeDo.Data/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md docs/explore-notes/conpty-sessions.md
|
||||
git commit -- src/ClaudeDo.Data/CLAUDE.md src/ClaudeDo.Ui/CLAUDE.md docs/explore-notes/conpty-sessions.md -m "docs(handler): document the handler-run task link"
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Report the visual-verification gap**
|
||||
|
||||
The build and tests cannot confirm any of this renders correctly. Explicitly hand these to Mika:
|
||||
|
||||
1. HANDLER badge colour and legibility on a handler task row (light **and** dark theme), and that MANUAL is gone from that row while still present on a normal manual reminder.
|
||||
2. The HANDLED TASKS panel on the handler task's Session tab: position relative to OUTCOMES, spacing, and behaviour with ~20 handled tasks (scroll).
|
||||
3. That a real "Let Claude handle it" run over a multi-task selection produces a populated panel after the run, including a phase-1-cancelled duplicate.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ the engine.
|
||||
| A1 | Diff viewing | `DiffModalViewModel` (worktree + commit-range), `WorktreeModalViewModel` (file-tree + per-file), `PlanningDiffViewModel` (per-subtask + integration) | `UnifiedDiffParser`, `DiffLinesView` (good) |
|
||||
| A2 | Agent-config editing | `ListSettingsModalViewModel` (list scope), `AgentSettingsSectionViewModel` (task scope); global lives in `SettingsModalViewModel` | `InheritanceResolver`, `InheritedBadge` (good) |
|
||||
| A3 | Worktree actions | `WorktreesOverviewModalViewModel` per-row cmds (Merge/Discard/Keep/ForceRemove/ShowDiff/Jump) vs `MergeSectionViewModel` (Merge/OpenDiff) | same `IWorkerClient` calls |
|
||||
| A4 | Merge display | `AgentStripView` re-displays `MergeSectionViewModel` state | — |
|
||||
| A4 | Merge display | ~~`AgentStripView` re-displays `MergeSectionViewModel` state~~ — resolved 2026-08-06: `AgentStripView` was dead (never bound after the task-detail redesign); deleted instead of unified | — |
|
||||
|
||||
### Bucket B — entry-point sprawl (one backend, many hand-wired doors)
|
||||
|
||||
@@ -43,7 +43,7 @@ the engine.
|
||||
|---|---|---|---|
|
||||
| B1 | Conflict-resolution seam | 5 copies of `Func<string,string,Task>? RequestConflictResolution` | `WorktreesOverviewModalViewModel.cs:83`, `DiffModalViewModel.cs:75`, `MergeModalViewModel.cs:33`, `MergeSectionViewModel.cs:51`, `DetailsIslandViewModel.cs:347` (delegates). Threaded through `MainWindow.axaml.cs:81`, `IslandsShellViewModel.cs:49/202`, `DiffModalViewModel.cs:103`, `MergeSectionViewModel.cs:159` |
|
||||
| B2 | Diff (open) | 3–4 | MergeSection "Open Diff", TaskHeaderBar "Review Merged Diff", WorktreesOverview "Show Diff", Planning "Review Combined" |
|
||||
| B3 | List Settings dialog | 3 | Lists context menu, Tasks header button, shell bridge `IslandsShellViewModel.cs:190-194` |
|
||||
| B3 | List Settings dialog | 2 (was 3 — Lists context menu entry removed 2026-08-06) | Tasks header button, double-click on a list row, shell bridge `IslandsShellViewModel.cs:190-194` |
|
||||
| B4 | Worktrees Overview | 2–3 | Repos menu (global), Lists context menu (per-list) |
|
||||
| B5 | Repo Import | 2 | Repos menu, Lists footer button |
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# "Let Claude handle it" — per-list handler with read / dedupe / enhance / run / merge
|
||||
|
||||
Date: 2026-07-27
|
||||
Supersedes parts of: `2026-07-24-merge-helper-design.md` (global scope, run-then-merge-only prompt)
|
||||
|
||||
## 1. Problem
|
||||
|
||||
The merge helper shipped in v2.3.0 with two entry points — a per-list context-menu item and a
|
||||
global footer Broom button — and a prompt that only runs and merges the selected tasks.
|
||||
|
||||
Two things are wrong with that:
|
||||
|
||||
- **The global scope is unwanted.** A run spanning several lists spans several repos, which
|
||||
makes `cwd`, the `--add-dir` set and the merge order ambiguous for no benefit. The user
|
||||
works one list (= one repo) at a time.
|
||||
- **The helper starts too late.** It takes the task list as given: it never reads the tasks as
|
||||
a set, so duplicates run twice and produce conflicting worktrees, and vague tasks go into an
|
||||
autonomous run under-specified and come back wrong.
|
||||
|
||||
## 2. Goal
|
||||
|
||||
One entry point, on a user list. It opens an interactive ConPTY session that takes the selected
|
||||
tasks through five phases: read them all, dedupe them, sharpen them for autonomous execution,
|
||||
run them, then review and merge each worktree.
|
||||
|
||||
Non-goals: no change to the autonomous queue path, no change to the ConPTY tile plumbing,
|
||||
no rename of the `MergeHelper*` identifiers (the user-facing label stays "Let Claude handle it").
|
||||
|
||||
## 3. Scope becomes list-only
|
||||
|
||||
`listId` becomes non-nullable across the whole chain:
|
||||
|
||||
| Layer | Change |
|
||||
|---|---|
|
||||
| `ListsIslandViewModel` | `MergeHelperRequest(string ListId, …)`; `LetClaudeHandleAllAsync` deleted |
|
||||
| `IslandsShellViewModel:241` | unchanged (already forwards `req.ListId`) |
|
||||
| `MissionControlViewModel:327` | `OpenMergeHelperConPtySessionAsync(string listId, …)` |
|
||||
| `IWorkerClient:90` / `WorkerClient:525` | `GetMergeHelperLaunchSpecAsync(taskIds, string listId, ct)` |
|
||||
| `WorkerHub:682` | `GetMergeHelperLaunchSpec(string[] taskIds, string listId)` |
|
||||
| `IInteractiveLaunchSpecService:36` | `BuildForMergeHelperAsync(taskIds, string listId, ct)` |
|
||||
|
||||
Deleted UI surface:
|
||||
|
||||
- Broom button `ListsIslandView.axaml:205-210` and `LetClaudeHandleAllCommand`.
|
||||
- `IsGlobal` on `MergeHelperSelectionModalViewModel` and the LIST column
|
||||
(`MergeHelperSelectionModal.axaml:71`) — with a single list the column is constant.
|
||||
- Localization keys `lists.letClaudeAllTip`, `modals.mergeHelper.scopeAll`,
|
||||
`modals.mergeHelper.columnList` (en + de, parity test enforces both).
|
||||
|
||||
`Configure(string listId, string listName)` loses its nullable overload; `ScopeLabel` always
|
||||
renders `modals.mergeHelper.scopeList`.
|
||||
|
||||
### 3.1 Single repo in the launch spec
|
||||
|
||||
`BuildForMergeHelperAsync` currently collects a distinct `repoDirs` set across the selected
|
||||
tasks and picks `cwd` per scope. With a list scope every task shares the list's `WorkingDir`,
|
||||
so this collapses to:
|
||||
|
||||
- Load the list; throw `InvalidOperationException` if it has no existing `WorkingDir`.
|
||||
- `cwd` = that directory; `--add-dir` = the session dir + that one directory.
|
||||
- The per-task brief line drops the now-constant `list:` and `repo:` fields.
|
||||
|
||||
The context-menu item is already hidden when `WorkingDir` is empty, so the throw is a guard,
|
||||
not a normal path.
|
||||
|
||||
### 3.2 Entry point visibility
|
||||
|
||||
The item stays in the list row's context menu, next to "List settings", "Worktrees overview",
|
||||
"Open in Explorer" and "Open in Terminal". That is the established place for list-scoped
|
||||
actions; a second always-visible button in the row would break the pattern.
|
||||
|
||||
## 4. Worker: `Cancelled` becomes externally settable
|
||||
|
||||
`ExternalMcpService.UpdateTaskStatus` accepts only `Idle` and `Queued` and throws
|
||||
`"Status '{target}' is not settable externally. Use run_task_now or cancel_task."` for the
|
||||
rest — but neither escape hatch reaches an **Idle** task: `cancel_task` only cancels a
|
||||
*running* task, and `review_task(decision="cancel")` requires WaitingForReview/Running/Queued.
|
||||
The dedupe phase needs exactly that: retire an Idle duplicate without destroying it.
|
||||
|
||||
Add to the switch:
|
||||
|
||||
```csharp
|
||||
case TaskStatus.Cancelled:
|
||||
var cancelResult = await _state.CancelAsync(taskId, DateTime.UtcNow, cancellationToken);
|
||||
if (!cancelResult.Ok)
|
||||
throw new InvalidOperationException(cancelResult.Reason ?? "Cannot cancel task.");
|
||||
break;
|
||||
```
|
||||
|
||||
`TaskStateService.CancelAsync` (`State/TaskStateService.cs:244`) already owns the transition
|
||||
and its worktree/parent side effects. The existing error message in `UpdateTaskStatus` already
|
||||
lists `Cancelled` as valid, so this also removes a lie. A cancelled task stays visible and can
|
||||
be reset to Idle — nothing is lost, unlike `delete_task`.
|
||||
|
||||
## 5. The five-phase prompt
|
||||
|
||||
`PromptFiles.MergeHelperDefault` is rewritten. The session stays interactive and the helper is
|
||||
told to ask whenever unsure — that is the point of a watched ConPTY session.
|
||||
|
||||
### Phase 0 — Read
|
||||
|
||||
`batch_get_tasks` over every id in the brief before touching anything: title, description,
|
||||
status, parent/child links. The helper must hold the whole set in mind before acting on any
|
||||
single task.
|
||||
|
||||
### Phase 1 — Dedupe
|
||||
|
||||
Compare the tasks pairwise for overlap. Emit a table of candidate pairs with the reason each
|
||||
pair looks like a duplicate, then **ask per pair**:
|
||||
|
||||
- merge → fold the loser's unique content into the survivor via `update_task`, then
|
||||
`update_task_status(loserId, "Cancelled")`;
|
||||
- keep both → note why and move on.
|
||||
|
||||
Nothing is cancelled without an explicit answer.
|
||||
|
||||
### Phase 2 — Enhance
|
||||
|
||||
For each surviving task, sharpen title and description for autonomous execution:
|
||||
|
||||
- concrete acceptance criteria,
|
||||
- the files/areas actually involved — grounded in the repo via Read/Grep/Glob, not guessed,
|
||||
- explicit out-of-scope.
|
||||
|
||||
Write back with `update_task` (title / description / commitType are the settable fields; it
|
||||
refuses while Running, which cannot happen this early). Rules: do not change intent, do not
|
||||
invent requirements. A task too vague to sharpen safely gets a question, not a guess.
|
||||
|
||||
### Phase 3 — Run
|
||||
|
||||
`run_task_now` cannot be used for a batch: `OverrideSlotService.StartInSlot`
|
||||
(`Queue/OverrideSlotService.cs:65-70`) holds a single slot and throws `"override slot busy"`
|
||||
on the second concurrent call. The queue picker is the only parallel path.
|
||||
|
||||
So: read `get_app_settings`, tell the user how many parallel slots are configured
|
||||
(`MaxParallelExecutions`, default 1 — `AppSettingsEntity.cs:14`), then
|
||||
`update_task_status(id, "Queued")` for every surviving task, then poll `get_task` until each
|
||||
has left Queued/Running — `WaitingForReview` on success, `Failed` on error. Announcing the
|
||||
slot count up front stops the user wondering why "run them all" executes one at a time.
|
||||
|
||||
A task already `Running` or `WaitingForChildren` when the session starts is not re-queued, only
|
||||
polled. A task already `WaitingForReview` skips straight to Phase 4.
|
||||
|
||||
### Phase 4 — Review and merge
|
||||
|
||||
Sequential, in list order. A task that came back `Failed` needs human judgement — ask whether
|
||||
to `reset_failed_task` and re-queue it, or skip it. Otherwise, unchanged from the shipped
|
||||
prompt: `get_task_diff` (stat first,
|
||||
full diff when non-trivial), sanity-check against the task's intent, ask before merging
|
||||
anything that looks wrong, then `review_task(taskId, decision="approve",
|
||||
leaveConflictsInTree=true)` and the conflict loop (`continue_merge` / `abort_merge`, parent id
|
||||
for unit merges, hand-resolution only where MCP cannot reach, always
|
||||
`git commit -- <paths>` and never `git add -A` because the checkout is shared).
|
||||
|
||||
New in this phase: the prompt states that because Phase 3 branches all fork from the same
|
||||
base, **conflicts are the normal case, not an exception** — resolve them rather than bailing
|
||||
out of the run.
|
||||
|
||||
### Phase 5 — Summary
|
||||
|
||||
One line per task: title — dedupe action — enhanced? — final status — merge commit —
|
||||
conflicts resolved. Then anything skipped and why, then follow-ups.
|
||||
|
||||
### Brief template
|
||||
|
||||
`MergeHelperInitialDefault` names the list and repo once in the header and drops the per-task
|
||||
`list:`/`repo:` fields, leaving `- [{status}] {title} (id: {id})`. Descriptions stay out of the
|
||||
brief; Phase 0 fetches them.
|
||||
|
||||
## 6. Testing
|
||||
|
||||
| Test | Change |
|
||||
|---|---|
|
||||
| `MergeHelperSelectionModalViewModelTests` | drop `IsGlobal`, list-scoped `Configure` |
|
||||
| `InteractiveLaunchSpecServiceTests` | three `null`-listId cases → non-null; drop the two-repo global-scope test; add "list without WorkingDir throws" |
|
||||
| `MissionControlViewModelTests` | four `OpenMergeHelperConPtySessionAsync(null, …)` calls |
|
||||
| `StubWorkerClient`, `TasksIslandViewModelPlanningTests` fake | signature |
|
||||
| `PromptFilesTests` | assert the five phase markers in the default prompt |
|
||||
| `Localization.Tests` | parity after removing three keys |
|
||||
| new: `ExternalMcpService` / worker test | `UpdateTaskStatus(id, "Cancelled")` cancels an Idle task; unknown status still throws |
|
||||
|
||||
No test spawns the real `claude` CLI — the prompt content is asserted as text, the session
|
||||
itself is a manual smoke step.
|
||||
|
||||
## 7. Verification left to the user
|
||||
|
||||
- The list context menu shows "Let Claude handle it" only for lists with a working dir, and
|
||||
the Broom button is gone from the footer row.
|
||||
- The selection dialog has no LIST column and reads "List: <name>".
|
||||
- A real ConPTY run: dedupe questions appear, enhancements land in the task descriptions,
|
||||
queued tasks execute, merges complete or hand off to conflict resolution.
|
||||
@@ -0,0 +1,235 @@
|
||||
# Planning-Chain Children: Fork Base Commit (Design Options, No Implementation)
|
||||
|
||||
**Date:** 2026-08-06
|
||||
**Status:** Options evaluated, recommendation given — decision pending (Mika)
|
||||
**Scope:** Analysis only. No production code changed by this document.
|
||||
|
||||
## Problem
|
||||
|
||||
The planning chain gives **ordering**, not **code inheritance**. `PlanningChainCoordinator.SetupChainAsync`
|
||||
(`src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs:36-72`) links children via
|
||||
`BlockedByTaskId` (child[i] blocks on child[i-1]) so they run one at a time. But each child's
|
||||
worktree is still created from **main's current HEAD**, not from the predecessor's branch, so
|
||||
child N+1 never sees child N's (unmerged) work.
|
||||
|
||||
**Observed failure (unit `44241dcb`, "Installer: Environment-Checks", 2026-08-05):** 7 of 9
|
||||
children succeeded; the two that depended on a sibling's output could not:
|
||||
|
||||
- `4e196058` ("Claude Help Me" button) — reported: *"the task spec assumes an existing
|
||||
SystemCheckPage/EnvironmentCheckReport/ExecutableResolver, but that code only exists on an
|
||||
unmerged sibling branch (claudedo/06aca9b3...). A fast-forward merge of that branch was
|
||||
attempted ... but was denied twice by the auto-mode permission classifier. No implementation
|
||||
work was done."*
|
||||
- `c22cdd06` (Diagnose section) — *"Blocked before implementation could start."*
|
||||
|
||||
Both correctly reported `CLAUDEDO_BLOCKED` and committed nothing (`ahead: 0`). A second-order
|
||||
symptom of the same root cause: `ExecutableResolver.cs` was independently created **twice**
|
||||
(`40272c0b`, `06aca9b3`, near-identical) and collided as an add/add conflict at unit-merge time.
|
||||
|
||||
## Ist-Zustand (verified against commit `816f247`, 2026-08-06)
|
||||
|
||||
### Where the base commit is chosen
|
||||
|
||||
`WorktreeManager.ResolveBaseCommitAsync` — `src/ClaudeDo.Worker/Runner/WorktreeManager.cs:191-207`:
|
||||
|
||||
```csharp
|
||||
private async Task<string> ResolveBaseCommitAsync(TaskEntity task, string workingDir, CancellationToken ct)
|
||||
{
|
||||
if (task.ParentTaskId is not null)
|
||||
{
|
||||
var parent = ...;
|
||||
if (parent is not null && parent.PlanningPhase == PlanningPhase.None)
|
||||
{
|
||||
var parentWt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.ParentTaskId, ct);
|
||||
var parentHead = parentWt?.HeadCommit ?? parentWt?.BaseCommit;
|
||||
if (parentHead is not null)
|
||||
return parentHead;
|
||||
}
|
||||
}
|
||||
return await _git.RevParseHeadAsync(workingDir, ct); // planning children land here
|
||||
}
|
||||
```
|
||||
|
||||
**A "don't fork from main" mechanism already exists** — but only for *improvement* children
|
||||
(a non-planning parent's own follow-up subtasks), which base off `task.ParentTaskId`'s worktree
|
||||
HEAD. The guard `parent.PlanningPhase == PlanningPhase.None` explicitly **excludes** planning
|
||||
children: their `ParentTaskId` points at the planning parent (which has no worktree of its own),
|
||||
not at a sibling, so this branch never fires for them and they fall through to `RevParseHeadAsync`
|
||||
= main HEAD. This is a deliberate exclusion in the existing code, not an oversight — it simply
|
||||
never anticipated that a planning child's *predecessor in the chain* (not its parent) might be
|
||||
the thing to inherit from.
|
||||
|
||||
Called from `WorktreeManager.CreateAsync` (`:35`), invoked by `TaskRunner` (`Runner/TaskRunner.cs:309`)
|
||||
at the moment a task transitions to `Running` — i.e. worktree creation happens per-run, not once
|
||||
at plan-finalize time.
|
||||
|
||||
### Children already run strictly sequentially — this is not a new constraint
|
||||
|
||||
`SetupChainAsync` sets `BlockedByTaskId` on every child but the first
|
||||
(`Planning/PlanningChainCoordinator.cs:61-69`); the queue picker only claims rows with
|
||||
`BlockedByTaskId IS NULL`. `OnChildFinishedAsync` (`:92-120`) unblocks the successor **only**
|
||||
after the predecessor reaches `Done` (and cascades cancellation down the chain on
|
||||
Failed/Cancelled). `OverrideSlotService.RunNow` (`Queue/OverrideSlotService.cs:32-42`) is the one
|
||||
path that bypasses this — it calls `StartRunningAsync` directly with no `BlockedByTaskId` check,
|
||||
so a user can manually force a later child to run out of turn.
|
||||
|
||||
Net: under the normal (queue-driven) path, by the time child N+1's worktree is created, child N
|
||||
is already terminal. Forking N+1 from N's branch tip instead of main's HEAD does **not** introduce
|
||||
any new concurrency constraint on the normal path — the sequencing already exists. It only matters
|
||||
for the `RunNow` bypass edge case (see Option 1 below).
|
||||
|
||||
### The merge side already treats the unit as a sequential chain, twice over
|
||||
|
||||
`PlanningMergeOrchestrator.DrainAsync` (`Planning/PlanningMergeOrchestrator.cs:183-229`) merges
|
||||
`Done` children into `targetBranch` **one at a time, in `SortOrder`**, via
|
||||
`TaskMergeService.MergeAsync`; the first conflict pauses the whole drain
|
||||
(`PlanningMergeConflict`, state kept for `ContinueAsync`/`AbortAsync`), and the first
|
||||
non-conflict failure **aborts the drain outright** — remaining children are never merged and the
|
||||
parent never reaches `Done` (`:208-215`).
|
||||
|
||||
`PlanningAggregator.BuildIntegrationBranchAsync` (`Planning/PlanningAggregator.cs:81-126`) — used
|
||||
for the pre-approve combined-diff preview — does the same thing again: builds a scratch
|
||||
integration branch off `targetBranch` and `MergeNoFfAsync`s each child's branch in, in
|
||||
`SortOrder`, stopping at the first conflict.
|
||||
|
||||
So **both** the actual unit-merge and its preview already model the child set as an ordered
|
||||
sequence where one bad link can stall everything downstream. The only place in the whole pipeline
|
||||
that still treats planning children as N independent forks of main is worktree creation.
|
||||
|
||||
## Options evaluated
|
||||
|
||||
### Option 1 — child forks from the predecessor's branch
|
||||
|
||||
Extend `ResolveBaseCommitAsync` (or a planning-specific sibling of it) to also handle planning
|
||||
children: look up the chain predecessor via `BlockedByTaskId` (not `ParentTaskId` — that points at
|
||||
the planning parent, which has no worktree) and use its `WorktreeEntity.HeadCommit ?? BaseCommit`
|
||||
the same way the improvement-child path already does.
|
||||
|
||||
**What actually breaks, given the Ist-Zustand above:**
|
||||
|
||||
- *Not* a new concurrency constraint (see above) — the chain already serializes execution.
|
||||
- *Not* a new "one failure blocks everyone" behavior at merge time — `DrainAsync` already aborts
|
||||
the whole drain on the first non-conflict failure, and the integration-branch preview already
|
||||
stops at the first conflict. Option 1 brings execution-time behavior in line with what merge-time
|
||||
behavior already is, rather than introducing a new failure mode.
|
||||
- **Genuinely new:** child N+1's branch now contains N's commits as ancestors. When N is later
|
||||
merged into `targetBranch` with `--no-ff` and N+1 is merged afterward, N+1's diff against N's
|
||||
content is empty (identical trees) — merges cleanly as a no-op for that slice, verified by the
|
||||
same mechanism `DrainAsync` already uses. No new conflict class is introduced; if anything this
|
||||
*removes* one: the `ExecutableResolver.cs` add/add collision would not have occurred, since N+1
|
||||
would start from a tree that already has the file.
|
||||
- **Genuinely new edge case:** `OverrideSlotService.RunNow` on a chain member whose predecessor
|
||||
hasn't produced a `WorktreeEntity`/`HeadCommit` yet. Needs an explicit fallback to main HEAD —
|
||||
the same `?? ` pattern the improvement-child branch already uses for a predecessor with no
|
||||
`HeadCommit` (never committed) covers most of this; a predecessor that was never even *run* needs
|
||||
the same fallback-to-`RevParseHeadAsync` the code already falls through to today.
|
||||
- **Genuinely new edge case:** a discarded/failed predecessor's worktree row still carries its last
|
||||
`HeadCommit`/`BaseCommit` (the row isn't deleted on discard, only `State` flips) — same as the
|
||||
improvement-child path already tolerates, so no new handling needed there.
|
||||
- Children still go straight to `Done` with no individual review (Unified Parent Model), so there
|
||||
is no "reject and rewrite a middle child after a successor already forked from it" scenario to
|
||||
worry about — that action doesn't exist in the current state machine.
|
||||
|
||||
**Price:** deeper branch chains (cosmetic — they're squashed away by the sequential merge/prune
|
||||
regardless); one new fallback path for the `RunNow` bypass. Meaningfully smaller than it first
|
||||
looks, because it extends an existing pattern (improvement children) into a lane whose execution
|
||||
and merge sides are *already* sequential — it only fixes the one place that wasn't.
|
||||
|
||||
### Option 2 — merge predecessor to main immediately on success, before the successor starts
|
||||
|
||||
Keeps children independent worktrees (forked from main-as-it-is-now, updated after each merge),
|
||||
but requires a real merge to `main` per child, outside of Approve.
|
||||
|
||||
**Breaks:**
|
||||
|
||||
- Directly contradicts the documented invariant "**Approve is the single review+merge action**"
|
||||
(`src/ClaudeDo.Worker/CLAUDE.md` → Status Model; `docs/explore-notes/review-merge.md` → "Approve
|
||||
= merge the whole unit"). Unreviewed work would land on `main` automatically, mid-chain, before
|
||||
the parent — or the user — ever sees it.
|
||||
- Requires calling `TaskMergeService.MergeAsync` from `OnChildFinishedAsync` (state-transition
|
||||
code), duplicating what `PlanningMergeOrchestrator` already owns, and doing it against a
|
||||
`VerifyCommand`-gated repo N times instead of once — if the gate is configured, a mid-chain
|
||||
verify failure now has to be handled somewhere there's currently no error path for it (parent is
|
||||
still `WaitingForChildren`, not under merge orchestration yet).
|
||||
- Unavoidably touches `TaskStateService`/status-transition semantics, which this task's scope
|
||||
explicitly excludes ("Die Status-Logik oder `TaskStateService` anfassen" is out of scope) — this
|
||||
option cannot be implemented without doing exactly that.
|
||||
|
||||
**Verdict:** rejected outright — it isn't just costly, it conflicts with a stated architectural
|
||||
invariant and this task's own non-goals.
|
||||
|
||||
### Option 3 — child may merge the predecessor's branch into its own worktree if it needs to
|
||||
|
||||
Least invasive to the base-commit mechanism; delegates the decision to the agent at runtime.
|
||||
|
||||
**Breaks:**
|
||||
|
||||
- This is *exactly* what the blocked child in the observed incident already tried, and it was
|
||||
denied twice by the auto-mode permission classifier. The real blocker isn't a missing
|
||||
capability, it's that `git merge` trips the classifier's "risky/hard-to-reverse" heuristic
|
||||
regardless of target — even though a merge confined to the task's own isolated worktree (never
|
||||
touching the shared `workingDir`) is materially lower-risk than a merge on a shared checkout.
|
||||
Fixing this option means carving a narrower permission rule (allow `git merge` only inside a
|
||||
path under the task's own worktree root), which is a security-policy change, not a merge-model
|
||||
change.
|
||||
- Even with permission granted, it still relies on the agent (a) noticing it's missing a
|
||||
prerequisite, (b) correctly identifying which sibling branch has it, and (c) successfully
|
||||
resolving any conflict that surfaces — three separate failure points per occurrence, decided
|
||||
fresh by an LLM each time rather than encoded once in the pipeline.
|
||||
- Doesn't help children that fail *before* they'd even think to look (e.g., a child whose first
|
||||
action is reading a file that doesn't exist yet has no signal to act on).
|
||||
|
||||
**Verdict:** possible as a narrow permission-scope fix layered *on top of* Option 1 (so an agent
|
||||
that still needs something from further back than its immediate predecessor isn't stuck), but not
|
||||
a substitute for it — on its own it reproduces the exact failure mode from the incident, just with
|
||||
one fewer denial.
|
||||
|
||||
### Option 4 — change nothing; require independent children at planning time
|
||||
|
||||
Zero code changes; pushes the constraint into plan quality.
|
||||
|
||||
**Breaks:**
|
||||
|
||||
- No enforcement exists today (`PlanningSessionManager` doesn't validate structural independence
|
||||
between proposed subtasks), and reliably detecting "child B depends on code child A will write"
|
||||
from a plan draft is itself an unsolved code-review problem — not something a prompt tweak
|
||||
guarantees.
|
||||
- A shared-foundation-plus-N-consumers decomposition (exactly the `44241dcb` shape: environment
|
||||
checks feeding two UI consumers) is often the *correct* decomposition, not a planning mistake.
|
||||
Banning it either forces over-merging into one giant child (defeating the purpose of splitting)
|
||||
or requires the planner to reject a structurally sound plan.
|
||||
- Reproduces the observed failure verbatim under the same conditions: nothing about this option
|
||||
would have caught `44241dcb` before it shipped two dead children.
|
||||
|
||||
**Verdict:** cheapest to write down, but doesn't fix the problem — it relocates it to "the plan
|
||||
looked independent but wasn't," which is what already happened.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Option 1**, with the `RunNow`-bypass fallback described above, and with Option 3's narrower
|
||||
permission-scope fix as an optional follow-up (not a prerequisite) for cases where a child needs
|
||||
something from further back in the chain than its immediate predecessor.
|
||||
|
||||
**Why:** Option 1 is not a new mechanism — it's closing the one gap in a pattern that already
|
||||
exists twice in this codebase: `WorktreeManager.ResolveBaseCommitAsync` already forks improvement
|
||||
children from their parent's HEAD instead of main, and both `PlanningMergeOrchestrator.DrainAsync`
|
||||
and `PlanningAggregator.BuildIntegrationBranchAsync` already treat the child set as an ordered
|
||||
merge sequence where the first failure stalls everything after it. Planning-child worktree
|
||||
creation is the outlier, not the rule. Extending the existing predecessor-lookup pattern (keyed
|
||||
off `BlockedByTaskId` instead of `ParentTaskId` for this lane) makes the "sees predecessor's work"
|
||||
guarantee hold everywhere the chain already implies it, without touching `TaskStateService`, the
|
||||
review model, or introducing a merge-time failure mode that doesn't already exist. Options 2 and 4
|
||||
either conflict with a stated invariant/this task's own non-goals, or fail to address the incident
|
||||
at all; Option 3 alone reproduces the incident's exact failure.
|
||||
|
||||
**Price to pay knowingly:** a chain member that never got a chance to run (no `WorktreeEntity` yet)
|
||||
needs an explicit main-HEAD fallback when its successor is forced via `RunNow` — a few lines,
|
||||
mirroring the null-coalescing fallback the improvement-child path already has. No other new failure
|
||||
surface was found.
|
||||
|
||||
## Explicitly out of scope (per task)
|
||||
|
||||
- Implementing Option 1 or any other option — Mika decides first.
|
||||
- Any change to `TaskStateService` or status-transition logic.
|
||||
- Changing blocked-child visibility/behavior — that's task `001ee94a`, running in parallel; it only
|
||||
touches MCP visibility, no production code overlap with this document.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Diff Viewer: Side-by-Side, Syntax Highlighting, Word Diff
|
||||
|
||||
Date: 2026-08-07
|
||||
Status: approved (design), not implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The diff viewer renders every change as a flat unified stream. Reading what actually changed
|
||||
inside a modified line means mentally aligning a `−` row with a `+` row several rows below it.
|
||||
The user reads diffs faster side by side.
|
||||
|
||||
Three gaps, all in the same surface:
|
||||
|
||||
1. No side-by-side mode.
|
||||
2. No syntax highlighting — the merge editor (`ConflictResolverView`) already has it via
|
||||
TextMate, the diff viewer does not.
|
||||
3. No intra-line (word) highlighting, so a one-character change looks like a whole-line rewrite.
|
||||
|
||||
## Current state
|
||||
|
||||
| Concern | Where |
|
||||
|---|---|
|
||||
| Parsing | `src/ClaudeDo.Ui/ViewModels/Modals/UnifiedDiffParser.cs` → `DiffFileViewModel.Lines` |
|
||||
| Models | `src/ClaudeDo.Ui/ViewModels/Modals/DiffModels.cs` (`DiffLineViewModel{Kind,OldNo,NewNo,Text}`, `DiffLineKind{Add,Del,Ctx,File}`) |
|
||||
| Rendering | `src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml` — non-virtualized `ItemsControl`, one `Border`+`Grid`+4 `TextBlock`s per line, `TextWrapping="NoWrap"` |
|
||||
| Host | `src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml` — Files mode (line 154, `SelectedFile.Lines`) and Planning mode (line 162, flattened `DiffLines` across all files) |
|
||||
| Highlighting reference | `src/ClaudeDo.Ui/Views/Conflicts/ConflictResolverView.axaml.cs:80-83` — `RegistryOptions(ThemeName.DarkPlus)` + `InstallTextMate` + `SetGrammar` by file extension |
|
||||
|
||||
`Avalonia.AvaloniaEdit`, `AvaloniaEdit.TextMate` and `TextMateSharp.Grammars` are already
|
||||
referenced in `ClaudeDo.Ui.csproj`.
|
||||
|
||||
## Decision
|
||||
|
||||
Replace `DiffLinesView` with an AvaloniaEdit-based control. TextMate highlighting is bound to
|
||||
the `TextEditor` control; it cannot be lifted into `TextBlock` inlines without reimplementing
|
||||
the scope→brush layer that `AvaloniaEdit.TextMate` already provides. Building split/wrap/word
|
||||
diff on the `TextBlock` model first and swapping the renderer later would be throwaway work.
|
||||
|
||||
Side effect worth having: AvaloniaEdit virtualizes, which removes the current non-virtualized
|
||||
`ItemsControl` as a scaling limit on large diffs.
|
||||
|
||||
Rejected: keeping the `ItemsControl` and hand-rolling highlighting from `TextMateSharp`
|
||||
tokenization — same output, materially more code, and a second highlighting path to maintain
|
||||
alongside the merge editor's.
|
||||
|
||||
## Layout semantics
|
||||
|
||||
Left pane is the **old** state, right pane is the **new** state — each side carries the complete
|
||||
version of the hunk, not "removals here, additions there".
|
||||
|
||||
```
|
||||
LEFT (old) RIGHT (new)
|
||||
12 public void Save() 12 public void Save()
|
||||
13 var x = 1; 13 var x = 2; ← word diff on `1` / `2`
|
||||
14 Log("old"); · (filler)
|
||||
· (filler) 14 Log("new");
|
||||
15 } 15 }
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. `DiffAlignment` (new, pure)
|
||||
|
||||
`src/ClaudeDo.Ui/ViewModels/Modals/DiffAlignment.cs`
|
||||
|
||||
Turns `IReadOnlyList<DiffLineViewModel>` into a render-ready `AlignedDiff`. No Avalonia types,
|
||||
fully unit-testable.
|
||||
|
||||
```csharp
|
||||
public enum AlignedSide { Ctx, Del, Add, Filler, Gap }
|
||||
|
||||
public readonly record struct TextSpan(int Start, int Length);
|
||||
|
||||
public sealed record SplitRow(
|
||||
AlignedSide LeftKind, int? OldNo, string LeftText, IReadOnlyList<TextSpan> LeftSpans,
|
||||
AlignedSide RightKind, int? NewNo, string RightText, IReadOnlyList<TextSpan> RightSpans);
|
||||
|
||||
public sealed record UnifiedRow(
|
||||
AlignedSide Kind, int? OldNo, int? NewNo, string Text, IReadOnlyList<TextSpan> Spans);
|
||||
|
||||
public sealed record AlignedDiff(
|
||||
IReadOnlyList<SplitRow> SplitRows, string LeftText, string RightText,
|
||||
IReadOnlyList<UnifiedRow> UnifiedRows, string UnifiedText);
|
||||
```
|
||||
|
||||
Row index `i` maps to document line `i + 1` in the corresponding text. That mapping is the
|
||||
contract the margin and both renderers depend on.
|
||||
|
||||
**Pairing.** Walk the lines. A `Ctx` run emits rows with the same text on both sides. A change
|
||||
block (a `Del` run followed by an `Add` run) pairs index-wise up to `min(delCount, addCount)`;
|
||||
the overhang gets `Filler` rows on the opposite side.
|
||||
|
||||
**Gaps.** The parser drops `@@` headers, so a skipped region shows up as a jump in `OldNo`/`NewNo`
|
||||
between consecutive lines. `DiffAlignment` detects that jump and inserts a `Gap` row on both
|
||||
sides. The parser is not touched.
|
||||
|
||||
**Word diff.** Only for `(Del, Add)` rows that are paired 1:1. Tokenize each side into runs of
|
||||
word characters / whitespace / single punctuation, run an LCS over the tokens, and emit the
|
||||
changed token runs as character spans per side.
|
||||
|
||||
Two guards, both `const` and both covered by tests:
|
||||
- Skip when either side exceeds `MaxWordDiffChars = 2000` — LCS cost, and such lines are
|
||||
unreadable as word diffs anyway.
|
||||
- Skip when token similarity is below `MinWordDiffSimilarity = 0.5` (common tokens / max token
|
||||
count). Below that the two lines are unrelated rewrites and per-word tinting is noise.
|
||||
|
||||
### 2. `DiffTextView` (new control, replaces `DiffLinesView`)
|
||||
|
||||
`src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml` + `.axaml.cs`
|
||||
|
||||
Styled properties:
|
||||
|
||||
| Property | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `File` | `DiffFileViewModel?` | Source; the control aligns it and caches the `AlignedDiff` per file instance |
|
||||
| `Mode` | `DiffViewMode` (`Unified`\|`Split`) | Layout |
|
||||
| `WrapLines` | `bool` | Bound to each editor's `WordWrap` |
|
||||
|
||||
Two `TextEditor`s in a two-column grid, both `IsReadOnly=true`, `ShowLineNumbers=false`.
|
||||
`Unified` mode collapses the right editor and spans the left one across both columns, feeding
|
||||
it `UnifiedText`. `Split` mode shows both, fed `LeftText` / `RightText`.
|
||||
|
||||
Per editor:
|
||||
- **TextMate**: one shared `RegistryOptions(ThemeName.DarkPlus)`; grammar resolved from
|
||||
`File.Path`'s extension via `GetLanguageByExtension` → `GetScopeByLanguageId` → `SetGrammar`,
|
||||
exactly as `ConflictResolverView.ApplyGrammar` does. No extension match → no grammar, plain text.
|
||||
- **`DiffLineNumberMargin : AbstractMargin`** — draws line numbers from the row list. Split: old
|
||||
numbers left, new numbers right. Unified: two number columns in one margin. `Filler` and `Gap`
|
||||
rows draw nothing.
|
||||
- **`DiffLineBackgroundRenderer : IBackgroundRenderer`** — full-width tint per visual line by
|
||||
row kind: add / del / filler / gap / ctx.
|
||||
- **`WordDiffRenderer : IBackgroundRenderer`** — stronger tint over the changed spans, via
|
||||
`BackgroundGeometryBuilder` at `lineStartOffset + span.Start`.
|
||||
|
||||
Both renderers resolve rows through a single `Func<int, RowInfo?>` keyed by document line.
|
||||
|
||||
**Highlighting on fragments.** Only hunks are in the document, not whole files, so TextMate's
|
||||
line-by-line state can be wrong at a fragment boundary (a line inside a block comment may be
|
||||
highlighted as code). Accepted — the same is true of every fragment-based diff viewer.
|
||||
|
||||
**Colors.** Line tints stay the existing low-alpha `RunningTintBrush` / `ErrorTintBrush` so
|
||||
syntax foregrounds remain legible. The per-line foreground recolor from `DiffLinesView`
|
||||
(green/red text) is dropped — syntax colors take over. `Filler` gets a new dim token brush,
|
||||
`Gap` renders as a dim `⋯` separator row.
|
||||
|
||||
**Scroll sync** (split only), modeled on `ConflictResolverView.HookScrollSync` — find each
|
||||
editor's descendant `ScrollViewer`, guard re-entry with a `_syncing` flag:
|
||||
- `WrapLines = false`: sync `Offset.Y` directly. Line heights match, so alignment is exact.
|
||||
- `WrapLines = true`: line heights diverge. Sync on the first visible document line instead
|
||||
(`ScrollToLine`), which keeps the top of the viewport aligned and lets rows drift downward.
|
||||
|
||||
### 3. Host changes
|
||||
|
||||
`DiffViewerView.axaml`:
|
||||
- Header gains a segmented Unified/Split toggle and a wrap toggle.
|
||||
- Files mode: `DiffLinesView Lines="{Binding SelectedFile.Lines}"` → `DiffTextView File="{Binding SelectedFile}"`.
|
||||
- Planning mode: the flattened single-stream `DiffLines` view is replaced by an `ItemsControl`
|
||||
over the subtask's parsed files, each item a file header plus its own `DiffTextView`. One
|
||||
editor can only carry one grammar, so per-file editors are required for highlighting to work
|
||||
at all here. `DiffViewerViewModel` exposes the parsed per-file list for the selected subtask;
|
||||
`DiffLines` and `UnifiedDiffParser.Flatten` lose their last consumer and are removed.
|
||||
|
||||
`DiffLinesView.axaml` + `.axaml.cs` are deleted once both usages are migrated.
|
||||
|
||||
### 4. Persistence
|
||||
|
||||
`src/ClaudeDo.Ui/AppSettings.cs` (`~/.todo-app/ui.config.json`) already holds UI-only
|
||||
preferences (`Language`, `AccentPreset`) with plain `Load()`/`Save()`. Two properties are added
|
||||
there:
|
||||
|
||||
```csharp
|
||||
public string DiffViewMode { get; set; } = "unified"; // "unified" | "split"
|
||||
public bool DiffWrapLines { get; set; }
|
||||
```
|
||||
|
||||
`DiffViewerViewModel` takes the injected `AppSettings`, seeds its toggles on open and calls
|
||||
`Save()` when either changes. No database column, no EF migration, no hub method — a view
|
||||
preference does not belong in `AppSettingsEntity`.
|
||||
|
||||
### 5. Localization
|
||||
|
||||
New keys in both `locales/en.json` and `locales/de.json` (Localization.Tests enforces parity):
|
||||
`diff.view.unified`, `diff.view.split`, `diff.view.wrap`.
|
||||
|
||||
## Testing
|
||||
|
||||
`tests/ClaudeDo.Ui.Tests` — `DiffAlignment` is pure and carries the logic worth testing:
|
||||
|
||||
- Context-only diff → identical rows on both sides, no fillers.
|
||||
- Equal-size change block → 1:1 pairing, no fillers.
|
||||
- Unequal change block (3 del / 5 add) → 3 paired rows + 2 right-side rows with left fillers.
|
||||
- Add-only and delete-only blocks → fillers on the opposite side throughout.
|
||||
- Non-contiguous line numbers → exactly one `Gap` row inserted.
|
||||
- Word diff: single-token change yields one span per side at the right offsets.
|
||||
- Word diff skipped above `MaxWordDiffChars` and below `MinWordDiffSimilarity`.
|
||||
- Row index ↔ document line mapping holds for both `SplitRows` and `UnifiedRows`.
|
||||
- Binary file and empty-content file → empty `AlignedDiff`, no crash.
|
||||
|
||||
`AppSettings` round-trip: persisted mode and wrap survive `Save()`/`Load()`.
|
||||
|
||||
Rendering (margin, both renderers, scroll sync, TextMate colors) is not unit-testable here and
|
||||
is an explicit manual visual pass — see Open items.
|
||||
|
||||
## Known limitations
|
||||
|
||||
1. **Wrap + split drift.** With wrap on, the two panes align at the top of the viewport but rows
|
||||
drift apart further down. Per-line vertical alignment as VS Code does it is out of scope.
|
||||
2. **Fragment highlighting.** See above — highlighting state can be wrong at hunk boundaries.
|
||||
3. **Editors per file in Planning mode.** A subtask touching many files instantiates one editor
|
||||
per file. Same cost as viewing those files individually in Files mode; not capped. If it
|
||||
proves slow, the fix is lazy instantiation on expand, not a silent truncation.
|
||||
|
||||
## Open items (manual verification)
|
||||
|
||||
- Visual pass on both modes: tints legible over DarkPlus syntax colors; line numbers aligned;
|
||||
filler and gap rows readable.
|
||||
- Scroll sync with wrap off (exact) and wrap on (top-anchored).
|
||||
- Planning mode with a multi-file subtask.
|
||||
- Toggle state survives an app restart.
|
||||
|
||||
## Docs to update on completion
|
||||
|
||||
- `src/ClaudeDo.Ui/CLAUDE.md` — Views/Controls list and the "Diff & Conflicts" section still
|
||||
name `DiffLinesView`.
|
||||
- `docs/explore-notes/review-merge.md` — diff stack description + "verified against" commit.
|
||||
@@ -0,0 +1,169 @@
|
||||
# Handler-Run: Verknüpfung zu den behandelten Tasks
|
||||
|
||||
**Date:** 2026-08-07
|
||||
**Status:** Design approved (Mika), implementation pending
|
||||
**Verified against:** commit `c792765`
|
||||
|
||||
## Problem
|
||||
|
||||
Ein "Let Claude handle it"-Run besitzt seit 2026-08-05 einen echten Task (`IsManual=true`,
|
||||
`HandlerBaseCommit`/`HandlerHeadCommit`, Diff über Commit-Range). Was fehlt: **welche Tasks der Run
|
||||
behandelt hat, ist nirgends persistiert.** Die Auswahl lebt nur in der ConPTY-Session und im
|
||||
Transcript; `HandoffMcpTools.HandoffListHandler` (`src/ClaudeDo.Worker/External/HandoffMcpTools.cs:28-45`)
|
||||
bekommt `survivingTaskIds` als flüchtige Liste.
|
||||
|
||||
Folge: Nachdem ein Run durch ist und der Diff sichtbar wird, lässt sich nicht mehr nachvollziehen,
|
||||
*was alles gemacht werden sollte* und *welcher Task was produziert hat*. Duplikate, die der Handler
|
||||
in Phase 1 gecancelt hat, verschwinden vollständig aus dem Blickfeld.
|
||||
|
||||
Zweitens zeigt der Handler-Task in der Liste das Badge **MANUAL**, weil er `IsManual=true` setzt —
|
||||
irreführend, denn es ist kein manueller Reminder.
|
||||
|
||||
## Ist-Zustand
|
||||
|
||||
### Es gibt kein Task-Kind
|
||||
|
||||
`TaskEntity` hat **kein `Kind`/`Type`-Enum**. Task-"Arten" sind heute Feld-Kombinationen:
|
||||
|
||||
| Feld | Bedeutung |
|
||||
|---|---|
|
||||
| `IsManual` | manueller Reminder — Queue/Daily-Prep/Refine überspringen ihn |
|
||||
| `ParentTaskId` | Kind einer Planning-/Improvement-Session |
|
||||
| `PlanningPhase` | Planning-Parent |
|
||||
| `BlockedByTaskId` | Kettenglied, Queue-Picker überspringt es |
|
||||
| `HandlerBaseCommit` | worktree-loser List-Handler-Host (`src/ClaudeDo.Data/Models/TaskEntity.cs:60-61`) |
|
||||
|
||||
Ein Handler-Task ist also allein durch `HandlerBaseCommit != null` identifiziert.
|
||||
|
||||
### `ParentTaskId` ist belegt
|
||||
|
||||
`TaskRepository.CreateChildAsync` (`src/ClaudeDo.Data/Repositories/TaskRepository.cs:306`) setzt es
|
||||
für Planning-Kinder; `TaskRowViewModel.IsChild`/`ShowAsChild`
|
||||
(`src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs:59,66`) hängen daran und rücken die Zeile
|
||||
im Baum ein. Ein Recycling für Handler→behandelte Tasks würde die Auswahl optisch unter den Handler
|
||||
schieben und mit echten Planning-Kindern kollidieren.
|
||||
|
||||
### Badge-Infrastruktur existiert
|
||||
|
||||
`TaskRowView.axaml:129-144` rendert DRAFT / PLANNED / PLANNING / MANUAL über
|
||||
`Border Classes="badge <variant>"`. Basis-Style und Varianten liegen in
|
||||
`src/ClaudeDo.Ui/Design/IslandStyles.axaml:963-990`, die Brushes als theme-fähige Tokens in
|
||||
`Tokens.axaml`. Loc-Keys: `tasks.badgeManual`, `tasks.manualTip` (en.json:163-164).
|
||||
|
||||
### Kinder-Panel existiert
|
||||
|
||||
`DetailsIslandViewModel.LoadChildOutcomesAsync`
|
||||
(`src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:704-748`) lädt
|
||||
`Where(t => t.ParentTaskId == parentTaskId)` in `ChildOutcomes` (`:248`) und rendert pro Zeile
|
||||
Id/Titel/Status/RoadblockCount/WorktreeState via `ChildOutcomeRowViewModel`; Refresh läuft über
|
||||
`TaskUpdated`/`WorktreeUpdated` (`:814-833`).
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Frage | Entscheidung | Begründung |
|
||||
|---|---|---|
|
||||
| Neues `TaskKind`-Enum? | **Nein** | Es gäbe kein Enum zu erweitern — es wäre das erste überhaupt, inkl. Migration und Rückwirkung auf Queue/Filter/UI. Der Bedarf ist eine Beziehung, kein Typ. |
|
||||
| `ParentTaskId` wiederverwenden? | **Nein** | belegt durch Planning-Kinder, kollidiert mit Einrückungs-Logik |
|
||||
| 1:n oder n:m? | **1:n**, eine nullable Spalte | Historie "welcher Run hat den Task mal berührt" bringt nichts, wenn ohnehin der letzte Run derjenige ist, dessen Diff man ansieht. Join-Tabelle = doppelter Code für einen Randfall. |
|
||||
| Wann stempeln? | **Beim Anlegen des Handler-Tasks** | Die UI kennt die Auswahl bereits. Erfasst auch die Tasks, die der Handler in Phase 1 als Duplikat cancelt — genau das "was sollte alles gemacht werden". Ein Stempeln erst in `handoff_list_handler` würde Dedupe-Verlierer verlieren und bei Abbruch vor Phase 2 gar nichts verknüpfen. |
|
||||
| Umfang der Anzeige | **Nur Liste + Endstatus** | Kein Phasen-Protokoll, kein Per-Task-Diff im Panel — der Diff hängt ohnehin am jeweiligen Task. |
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Daten
|
||||
|
||||
Neue nullable Spalte auf `TaskEntity`:
|
||||
|
||||
```csharp
|
||||
/// <summary>Id des Handler-Task-Runs, der diesen Task behandelt hat (null = keiner).</summary>
|
||||
public string? HandlerTaskId { get; set; }
|
||||
```
|
||||
|
||||
Konfiguration in `TaskEntityConfiguration`: `HasIndex(t => t.HandlerTaskId)`, kein FK-Constraint
|
||||
(konsistent mit `BlockedByTaskId`-Handhabung; ein gelöschter Handler-Task soll die behandelten Tasks
|
||||
nicht kaskadierend anfassen). EF-Core-Migration `AddHandlerTaskId`.
|
||||
|
||||
Ein zweiter Run über dieselben Tasks überschreibt die Zuordnung — gewollt (1:n).
|
||||
|
||||
### 2. Schreiben
|
||||
|
||||
Die Auswahl wird durchgereicht: UI → `IWorkerClient.CreateMergeHelperTaskAsync` →
|
||||
`WorkerHub.CreateMergeHelperTask` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs:827-838`) →
|
||||
`InteractiveLaunchSpecService.CreateMergeHelperTaskAsync`. Nach dem Anlegen des Handler-Tasks setzt
|
||||
eine neue Repository-Methode die Zuordnung in einem Batch-Update:
|
||||
|
||||
```csharp
|
||||
Task<int> SetHandlerTaskIdAsync(IReadOnlyList<string> taskIds, string handlerTaskId, CancellationToken ct);
|
||||
```
|
||||
|
||||
Der Handler-Task selbst bekommt **kein** `HandlerTaskId` (kein Selbstbezug). Unbekannte Ids werden
|
||||
still übersprungen.
|
||||
|
||||
### 3. Badge
|
||||
|
||||
`TaskRowViewModel`:
|
||||
|
||||
```csharp
|
||||
public bool IsHandlerRun => !string.IsNullOrEmpty(HandlerBaseCommit);
|
||||
public string? HandlerBadge => IsHandlerRun ? Loc.T("tasks.badgeHandler") : null;
|
||||
public string? ManualBadge => IsManual && !IsHandlerRun ? Loc.T("tasks.badgeManual") : null;
|
||||
```
|
||||
|
||||
`HandlerBaseCommit` muss dafür auf das Row-ViewModel und in dessen Mapping aufgenommen werden.
|
||||
HANDLER hat Vorrang vor MANUAL — beide Badges nie gleichzeitig.
|
||||
|
||||
In `TaskRowView.axaml` analog zu `:141-144` ein `Border Classes="badge handler"` mit
|
||||
`ToolTip.Tip="{loc:Tr tasks.handlerTip}"`. In `IslandStyles.axaml` eine `.badge.handler`-Variante
|
||||
mit `{DynamicResource HandlerBadgeBrush}`, Token in `Tokens.axaml` für Light und Dark.
|
||||
|
||||
Neue Loc-Keys in en.json **und** de.json (Parität ist testgeprüft):
|
||||
|
||||
- `tasks.badgeHandler` — "HANDLER" / "HANDLER"
|
||||
- `tasks.handlerTip` — "Handler run — lists the tasks it processed" / "Handler-Run — listet die
|
||||
Tasks, die er bearbeitet hat"
|
||||
|
||||
### 4. Anzeige
|
||||
|
||||
Im Detail-Bereich eines Handler-Tasks eine Liste der behandelten Tasks, parallel zum bestehenden
|
||||
Kinder-Panel:
|
||||
|
||||
- Neue Collection `HandledTasks` auf `DetailsIslandViewModel`, befüllt von `LoadHandledTasksAsync`
|
||||
mit `Where(t => t.HandlerTaskId == taskId)`, sortiert wie die Kinder-Liste.
|
||||
- Zeilen wiederverwenden `ChildOutcomeRowViewModel` (Id, Titel, Status, RoadblockCount,
|
||||
WorktreeState) — keine neue Row-Klasse.
|
||||
- Refresh über dieselben `TaskUpdated`-Events wie `ChildOutcomes`; der bestehende
|
||||
`RefreshChildOutcomeAsync`-Pfad (`:814-833`) wird um die zweite Collection erweitert.
|
||||
- Sichtbar nur wenn `HandledTasks.Count > 0`.
|
||||
- **Keine Klick-Interaktion** — das bestehende `ChildOutcomes`-Template ist eine reine Anzeige
|
||||
(Titel / Roadblock / Status, kein Tapped-Handler). Die neue Liste bleibt identisch; "zum Task
|
||||
springen" wäre neues Verhalten und ist hier nicht enthalten.
|
||||
|
||||
### 5. Fehlerfälle
|
||||
|
||||
- Handler-Task gelöscht → `HandlerTaskId` der behandelten Tasks zeigt ins Leere; die Tasks bleiben
|
||||
normal nutzbar, das Panel existiert schlicht nicht mehr. Kein Cleanup nötig.
|
||||
- Behandelter Task gelöscht → verschwindet aus der Liste (Query läuft live gegen die Tasks).
|
||||
- Leere Auswahl → kein Stempeln, Panel bleibt unsichtbar.
|
||||
|
||||
## Tests
|
||||
|
||||
| Ebene | Test |
|
||||
|---|---|
|
||||
| Data | `SetHandlerTaskIdAsync` stempelt alle übergebenen Ids, ignoriert unbekannte, überschreibt eine vorhandene Zuordnung |
|
||||
| Worker | `CreateMergeHelperTaskAsync` stempelt die übergebene Auswahl und **nicht** den Handler-Task selbst |
|
||||
| Ui | `TaskRowViewModel`: HANDLER schlägt MANUAL (`IsManual=true` + `HandlerBaseCommit` gesetzt → nur HANDLER) |
|
||||
| Ui | `DetailsIslandViewModel`: `HandledTasks` lädt nach `HandlerTaskId`, aktualisiert sich auf `TaskUpdated` |
|
||||
| Localization | Parität en/de — deckt der bestehende Test automatisch ab |
|
||||
|
||||
## Bewusst nicht enthalten
|
||||
|
||||
- Kein `TaskKind`-Enum.
|
||||
- Keine n:m-Historie über mehrere Runs.
|
||||
- Kein Phasen-Protokoll (Dedupe-Begründungen, Umformulierungen) — nur das Ergebnis.
|
||||
- Kein Per-Task-Diff im Panel; der Diff bleibt am jeweiligen Task.
|
||||
- Kein Badge auf den *behandelten* Tasks.
|
||||
|
||||
## Offen
|
||||
|
||||
- **Sichtprüfung durch Mika:** Badge-Farbe im Light- und Dark-Theme, Position des Panels im
|
||||
Detail-Bereich, Verhalten bei vielen behandelten Tasks (Scroll).
|
||||
@@ -0,0 +1,173 @@
|
||||
# UI-Reaktivität und Listen-Performance
|
||||
|
||||
**Datum:** 2026-08-07
|
||||
**Status:** Design freigegeben, Implementierung offen
|
||||
|
||||
## Problem
|
||||
|
||||
Zwei Symptome, die als eines gemeldet wurden:
|
||||
|
||||
1. **Stale UI.** Ein Task bleibt in der Liste auf `Queued` stehen, obwohl der Worker ihn längst auf `Running` gesetzt hat. Ebenso tauchen extern angelegte Tasks (Online-Inbox, List-Handler) erst nach einem manuellen Neuladen auf. Modals und Overlays zeigen den Stand vom Öffnungszeitpunkt.
|
||||
2. **Langsames Laden.** Eine Liste mit ~125 erledigten Tasks braucht 1–2 Sekunden zum Öffnen. Ziel sind Listen mit bis zu ~1000 erledigten Tasks.
|
||||
|
||||
## Analyse
|
||||
|
||||
### Reaktivität: es fehlt kein Event, es fehlt die Selbstheilung
|
||||
|
||||
Der Broadcast-Pfad ist im Grundsatz korrekt: der Worker schreibt in die DB, committet, und sendet danach eine ID über SignalR (`HubBroadcaster`); die UI lädt die Entity frisch nach. WAL-Sichtbarkeit und EF-Change-Tracking wurden als Ursache **ausgeschlossen** — die UI nutzt `IDbContextFactory` mit kurzlebigen Kontexten, und WAL-Reader sehen Commits sofort.
|
||||
|
||||
Das eigentliche Problem: **ein einziger verlorener Event ist permanent.** Der einzige Reconcile-Trigger ist heute `ConnectionRestoredEvent`, also ein Verbindungsabbruch. Es gibt drei Wege, auf denen ein Update verloren geht:
|
||||
|
||||
| # | Loch | Ort |
|
||||
|---|---|---|
|
||||
| 1 | Blankes `catch { }` um den gesamten Delta-Pfad. Eine einzige transiente Exception (z.B. `SQLITE_BUSY`) lässt die Zeile dauerhaft auf dem alten Stand — ohne Log, ohne Retry. | `src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs:224` |
|
||||
| 2 | `QueuePicker.ClaimNextAsync` committet `status='running'` sofort. Wirft danach etwas in `RunInSlotAsync` oder im ungeschützten Setup-Block von `TaskRunner.ContinueAsync` (Zeilen 218–238 liegen außerhalb jedes `try`), fängt der Catch das ab und **loggt nur** — kein `FailAsync`, kein Broadcast. DB sagt Running, die UI erfährt es nie. | `src/ClaudeDo.Worker/Queue/QueueService.cs:349-352` |
|
||||
| 3 | DB-Writes ohne Broadcast. | `src/ClaudeDo.Worker/Runner/WorktreeManager.cs:103`, `src/ClaudeDo.Worker/Online/OnlineSyncService.cs:131` |
|
||||
|
||||
**Korrektur (2026-08-07, bei der Umsetzung gefunden):** `InteractiveLaunchSpecService.cs:447` stand hier ursprünglich als drittes Loch. Das war falsch. Die Service-Methode broadcastet zwar selbst nicht, aber ihr einziger Produktions-Aufrufer `WorkerHub.CreateMergeHelperTask` (`src/ClaudeDo.Worker/Hub/WorkerHub.cs:818`) sendet direkt danach `TaskUpdated` — seit Commit `c07c1f7` vom 2026-08-05, abgesichert durch `MergeHelperTaskHubTests.CreateMergeHelperTask_CreatesIdleManualTask_StampsBaseCommit_Broadcasts`. Der ursprüngliche Befund hatte den DB-Write gesehen, aber den Aufrufer nicht geprüft. Ein Broadcast im Service wäre ein Duplikat gewesen.
|
||||
|
||||
Dazu zwei kleinere Befunde:
|
||||
|
||||
- **Race im Delta-Pfad.** `OnWorkerTaskUpdated` ist `async void` und hängt an *zwei* Events (`TaskUpdatedEvent` und `WorktreeUpdatedEvent`, `TasksIslandViewModel.cs:117-118`). Der Full-Reload-Zweig ist per `_loadCts` gegen Überholen abgesichert, der Delta-Zweig nicht — ein älterer Read kann einen neueren überschreiben.
|
||||
- ~~**Kein Busy-Timeout konfiguriert.**~~ **Widerlegt (2026-08-07, empirisch geprüft).** Die Vermutung war, die blanken Connection-Strings (`src/ClaudeDo.App/Program.cs:95`, `src/ClaudeDo.Worker/Program.cs:61`) ließen einen `SQLITE_BUSY` sofort durchschlagen. Das stimmt nicht: Microsoft.Data.Sqlite 8.0.11 setzt `DefaultTimeout` **von sich aus auf 30 Sekunden**, mit oder ohne das Keyword — gemessen an `SqliteConnectionStringBuilder("Data Source=x.db").DefaultTimeout` → `30`, ebenso `SqliteConnection.DefaultTimeout` und `SqliteCommand.CommandTimeout`. Ein Contention-Test (Writer hält 2s, zweiter Writer parallel) zeigt, dass der zweite wartet und nach ~2030 ms durchkommt, statt zu werfen. Der ursprünglich dafür gemachte Commit `f62dbb9` war ein No-op mit irreführendem Kommentar und wurde mit `ac58679` zurückgenommen. Die tatsächliche Absicherung gegen transiente Lesefehler leistet der Retry im Delta-Pfad, nicht ein Timeout.
|
||||
- **`RunCreated` ist ein totes Event.** Wird in `TaskRunner.cs:358` gesendet, hat aber keinen einzigen Abonnenten in der UI.
|
||||
|
||||
### Performance: der Engpass ist das Rendering, nicht die Datenbank
|
||||
|
||||
SQLite ist hier **nicht** der Engpass, und ein DB-Wechsel würde nichts verbessern. Die Kosten verteilen sich so:
|
||||
|
||||
| Posten | bei 125 Zeilen |
|
||||
|---|---|
|
||||
| SQLite-Read, 125 Zeilen × ~30 Spalten, 2 Joins | < 1 ms |
|
||||
| EF-Materialisierung | ~1–5 ms |
|
||||
| **Avalonia baut ~8.500 Controls mit Bindings** | **~1.000–1.500 ms** |
|
||||
|
||||
Eine `TaskRowView` erzeugt **~68 Controls eager**: ~50 für Struktur und Inhalt plus 18 `MenuItem`-Deklarationen des inline deklarierten ContextMenus (`TaskRowView.axaml:35-85`). Die Liste ist **nicht virtualisiert**: drei `ItemsControl` (Overdue/Open/Completed) liegen in einem gemeinsamen `ScrollViewer` ohne `ItemsPanel`-Override (`TasksIslandView.axaml:100,119,152`). `ItemsControl` nutzt per Default ein normales `StackPanel`, und der gemeinsame `ScrollViewer` gibt allen dreien unbegrenzte Höhe — deshalb würde auch ein bloßes Setzen von `VirtualizingStackPanel` nichts bewirken.
|
||||
|
||||
Zeilenhöhe ~68px, verfügbare Listenhöhe auf 2560×1440 ~1250px → **~19 Zeilen gleichzeitig sichtbar**.
|
||||
|
||||
| | Controls | geschätzt |
|
||||
|---|---|---|
|
||||
| heute, 125 Tasks | ~8.500 | 1–2 s |
|
||||
| heute, 1000 Tasks | ~68.000 | ~10 s+ |
|
||||
| virtualisiert (19 sichtbar + Overscan ≈ 25 Zeilen) | ~1.700 | ~250 ms |
|
||||
| + ContextMenu lazy | ~1.250 | ~180 ms |
|
||||
|
||||
Entscheidend: die virtualisierten Werte sind **konstant** und gelten für 125 wie für 10.000 Zeilen.
|
||||
|
||||
Nebenbefund: `LoadForList` (`TasksIslandViewModel.cs:305-309`) hat **kein `.Where()` vor `ToListAsync()`** — es lädt die komplette `tasks`-Tabelle aller Listen mit zwei Joins und filtert danach in C#. Die vorhandenen Indizes (`idx_tasks_list_id`, `idx_tasks_status`) werden dadurch nie genutzt. Bei der heutigen DB-Größe unkritisch, aber es skaliert mit der DB-Gesamtgröße statt mit der Listengröße.
|
||||
|
||||
### Drag & Drop: architektonisch virtualisierungsfähig
|
||||
|
||||
Die Task-Liste nutzt **kein** Avalonia-`DragDrop` pro Item, sondern ein eigenes Ghost-Drag:
|
||||
|
||||
- Vier Pointer-Handler hängen **zentral** an der `TasksIslandView` (`TasksIslandView.axaml.cs:40-43`, Tunnel-Routing) — keine pro-Zeile registrierten Handler, die beim Container-Recycling leaken könnten.
|
||||
- Das Ziel wird per `InputHitTest` live ermittelt (`TasksIslandView.axaml.cs:304-329`) — kein Index-Hack, recycling-sicher.
|
||||
- Drop-Hints sind reine ViewModel-Properties (`TaskRowViewModel.cs:25-26`, `DropHintAbove`/`DropHintBelow`).
|
||||
- Der Ghost ist ein `RenderTargetBitmap`-Snapshot in einem separaten Topmost-Fenster (`Views/Controls/TaskDragController.cs`), losgelöst vom Visual Tree.
|
||||
|
||||
Anzupassen sind:
|
||||
|
||||
- `FindNextInSameSection` und `SectionFor` (`TasksIslandViewModel.cs:366-374`, `:622-628`) iterieren per `IndexOf` über die drei UI-Collections. Die flache Master-Collection `Items` existiert bereits (`TasksIslandViewModel.cs:55`) und ist korrekt sortiert — das ist der Grund, warum das Flatten bezahlbar ist.
|
||||
- **Auto-Scroll beim Ziehen fehlt komplett.** Fällt heute weniger auf, weil alle Container realisiert sind; bei einer virtualisierten Liste ist es Pflicht.
|
||||
- **Bestehender Bug:** Zieht man über eine Gruppengrenze, prüft `ReorderAsync` (`TasksIslandViewModel.cs:560-563`) zwar die Sektion, verschiebt bei ungleichen Sektionen aber nur `Items` ohne anschließendes `Regroup()`. `SortOrder` landet in der DB, die UI zeigt nichts.
|
||||
- **Kein Präzedenzfall:** im gesamten `ClaudeDo.Ui`-Projekt existiert kein `VirtualizingStackPanel` und kein `ItemsRepeater`.
|
||||
|
||||
### Drag-Optik
|
||||
|
||||
Heute laufen zwei Darstellungen derselben Zeile gleichzeitig: der Bitmap-Ghost am Cursor **und** die Originalzeile mit `Opacity 0.55`, `scale(1.03)`, BoxShadow und Accent-Rand (`IslandStyles.axaml:441-446`). `scale(1.03)` ändert kein Layout und überlappt daher die Nachbarzeilen. Das Feedback für das Drop-Ziel ist der ganz normale `:pointerover`-Hover (`IslandStyles.axaml:433-435`, setzt nur `BorderBrush`) — dasselbe Signal wie beim harmlosen Drüberfahren; einen eigenen `drop-target`-Style gibt es nur für die Lists-Island (`Border.list-item.drop-target`). Zusätzlich laufen `BrushTransition` (0.12s) und `ThicknessTransition` auf `Margin` (0.15s) auch während des Drags mit, was die Rückmeldung verschwimmen lässt.
|
||||
|
||||
## Verworfene Alternativen
|
||||
|
||||
| Alternative | Warum verworfen |
|
||||
|---|---|
|
||||
| **SQLite ersetzen** | Kein Engpass. Der DB-Read liegt unter 1 ms; die Zeit steckt zu >95% im Aufbau des Visual Tree. Monatelange Arbeit für null messbaren Gewinn. |
|
||||
| **Nur Task-Titel laden, Rest lazy beim Öffnen** | Richtige Intuition, falsche Ebene. Spart Bytes aus einer lokalen Datei, die in unter 1 ms gelesen wird. Die 125 Zeilen werden weiterhin als 125 vollständige `TaskRowView` gebaut. Um wirklich zu sparen, müsste das Zeilen-Template entkernt werden — also genau die Chips und Icons entfallen, wegen derer die Liste nützlich ist. Als *Zusatz* (schlanke Projektion für Speicher/Materialisierung) sinnvoll, als Hauptmaßnahme nicht. |
|
||||
| **Completed einklappen + „mehr laden"** | Billig und sofort wirksam, aber aufgeklappt mit 1000 Zeilen hängt es wieder. Bleibt als **Fallback**, falls der Virtualisierungs-Spike scheitert. |
|
||||
| **Completed archivieren** | Löst das Problem durch Vermeidung; der Wunsch war ausdrücklich, 1000 erledigte Tasks sehen zu können. |
|
||||
| **`Revision`-Spalte / Change-Feed** | Strukturell sauber (verlorene Events wären egal, Race gelöst), aber Migration plus Anpassung jedes Schreibpfads. Für eine Single-User-Desktop-App mit lokaler DB Overkill; der Reconcile-Tick erreicht dasselbe Ziel deutlich billiger. Bleibt als Eskalation, falls Phase 3 in der Praxis nicht reicht. |
|
||||
| **Nur die Löcher stopfen (ohne Reconcile)** | Behebt die bekannten Fälle, lässt die Architektur „ein verlorener Event = permanent stale" aber intakt. Das nächste Loch kommt mit dem nächsten Feature. |
|
||||
|
||||
## Design
|
||||
|
||||
### Phase 1 — Reaktivitäts-Löcher schließen
|
||||
|
||||
Unabhängig von Phase 2 und 3, kann sofort starten.
|
||||
|
||||
| Fix | Ort |
|
||||
|---|---|
|
||||
| `catch { }` ersetzen durch Log + einmaligen Retry. **Kein** Footer-Error — das ist ein Hintergrund-Refresh, keine Nutzeraktion. | `TasksIslandViewModel.cs:224` |
|
||||
| Catch-Block ruft `_state.FailAsync` (das selbst broadcastet), statt nur zu loggen; `OperationCanceledException` bleibt ausgenommen | `QueueService.cs:349-352` |
|
||||
| `WorktreeUpdated` nach dem Insert broadcasten | `Runner/WorktreeManager.cs:103` |
|
||||
| `TaskUpdated` nach dem Insert broadcasten | `Online/OnlineSyncService.cs:131` |
|
||||
| Monotone Sequenznummer pro TaskId im Delta-Pfad; Ergebnisse mit veralteter Sequenz verwerfen | `OnWorkerTaskUpdated` |
|
||||
| `RunCreated` ersatzlos entfernen (totes Event ohne Abonnent) | `HubBroadcaster`, `TaskRunner.cs:358` |
|
||||
|
||||
Der ungeschützte Setup-Block in `TaskRunner.ContinueAsync` (Zeilen 218–238) wird **nicht** separat umgebaut: sobald `RunInSlotAsync` im Fehlerfall `FailAsync` ruft, ist jede dort geworfene Exception abgedeckt — der Task landet auf `Failed` und der Broadcast erfolgt. Ein zweiter Schutzwall wäre doppelt.
|
||||
|
||||
### Phase 2 — Flache virtualisierte Liste
|
||||
|
||||
**Datenmodell.** `Regroup()` erzeugt statt drei Collections **eine** `Rows`-Collection vom Union-Typ (`HeaderRow` | `TaskRowViewModel`), abgeleitet aus der bereits vorhandenen flachen `Items`. Gruppenüberschriften werden zu regulären Einträgen:
|
||||
|
||||
```
|
||||
Rows
|
||||
[0] HeaderRow "Überfällig (3)"
|
||||
[1] TaskRow …
|
||||
[4] HeaderRow "Offen (12)"
|
||||
…
|
||||
[17] HeaderRow "Erledigt (125)"
|
||||
…
|
||||
```
|
||||
|
||||
**View.** Eine `ListBox` mit `VirtualizingStackPanel` und einem DataTemplate-Selector (Header / Task) ersetzt die drei `ItemsControl` und den umschließenden `ScrollViewer`.
|
||||
|
||||
**Drag.** `FindNextInSameSection`, `SectionFor` und `ReorderAsync` rechnen gegen `Items` statt gegen die UI-Collections. Auto-Scroll beim Ziehen an den Listenrand wird neu gebaut. Der Cross-Section-Reorder-Bug wird im selben Zug behoben, da die Sektionsgrenzen in der flachen Struktur ohnehin explizit modelliert werden müssen.
|
||||
|
||||
**Zeilenkosten.** Das ContextMenu wird bei `ContextRequested` im Code-Behind aufgebaut statt als 18 `MenuItem`s pro Template-Instanz.
|
||||
|
||||
**Query.** `.Where()` wandert vor `ToListAsync()`, damit die Query mit der Listengröße statt der DB-Gesamtgröße skaliert. Erfordert Umbau von `ITaskListFilter` von In-Memory-Prädikaten (`Matches(TaskEntity)`) auf `IQueryable`-Expressions.
|
||||
|
||||
**Drag-Optik (Variante A).** Ghost folgt dem Cursor; die Originalzeile kollabiert zu einer leeren, gestrichelten Lücke, die beim Ziehen an die jeweilige Zielposition mitwandert.
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Fix login bug │
|
||||
├──────────────────────┤
|
||||
│ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ │ ← leerer Slot, wandert mit
|
||||
├──────────────────────┤
|
||||
│ Rebase branch │
|
||||
└──────────────────────┘
|
||||
┌────────────────┐
|
||||
│ Update deps │ ← Ghost am Cursor
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
Begleitend: `scale(1.03)` und BoxShadow auf der gezogenen Zeile entfallen; der normale `:pointerover`-Hover wird während eines aktiven Drags per `dragging`-Klasse am Container unterdrückt; Transitions sind während des Drags aus.
|
||||
|
||||
### Phase 3 — Reconcile-Tick
|
||||
|
||||
Setzt Phase 2 voraus: auf einer Liste, die 1–2 s zum Laden braucht, würde ein periodischer Abgleich alles verschlimmern.
|
||||
|
||||
Ein Timer (3–5 s) gleicht die **sichtbaren** Rows gegen die lokale SQLite ab und **patcht ausschließlich Properties** — er baut nie Zeilen neu und löst nie einen `LoadForList` aus. Bei einer lokalen DB und wenigen Dutzend sichtbaren Zeilen ist das ein einzelner indizierter Query.
|
||||
|
||||
Damit ist jeder verlorene Event nach spätestens einem Tick geheilt, unabhängig davon, wo er fehlte. Derselbe Tick versorgt zusätzlich die langlebigen Overlays: Worktrees-Overview, LogVisualizer und die MergeHelper-Auswahl.
|
||||
|
||||
Kurzlebige Modals (Settings, ListSettings, RepoImport, WeeklyReport, ConflictResolver) bleiben bewusst statisch — ein Dialog, der sich unter den Fingern des Nutzers ändert, ist schlechter als einer, der den Stand vom Öffnen zeigt.
|
||||
|
||||
## Tests
|
||||
|
||||
- **Worker.Tests:** Broadcast-Assertions über einen Fake-Broadcaster auf allen in Phase 1 gefixten Pfaden — insbesondere, dass der Fehlerfall in `RunInSlotAsync` einen Broadcast auslöst.
|
||||
- **Ui.Tests:** Der Reconcile-Tick patcht abweichende Properties; ein Ergebnis mit veralteter Sequenznummer wird verworfen; `Regroup()` erzeugt die korrekte `Rows`-Folge inklusive Header-Positionen und -Zählern.
|
||||
- **Nicht automatisiert testbar:** Ladezeit und Drag-Optik. Beides erfordert eine manuelle Gegenmessung mit einer großen Liste durch den Nutzer.
|
||||
|
||||
Keine Tests, die die echte `claude`-CLI starten.
|
||||
|
||||
## Risiken
|
||||
|
||||
1. **Kein Virtualisierungs-Präzedenzfall im Projekt.** Das HitTest-basierte Custom-Drag ist theoretisch tragfähig, aber nie gegen recycelte Container verifiziert. **Die erste Aufgabe in Phase 2 ist ein Spike**, kein Umbau: eine virtualisierte Liste mit dem bestehenden Drag, inklusive Recycling während eines aktiven Drags und Auto-Scroll. Scheitert der Spike, ist der Fallback „Completed einklappen + nachladen".
|
||||
2. **Variable Zeilenhöhen.** 68 px im Normalfall, 90–110 px bei zweizeiligem Titel oder mehreren Badges. `VirtualizingStackPanel` beherrscht das, aber die Scrollbar springt dabei gern, weil die Gesamthöhe geschätzt wird. Muss im Spike mitgeprüft werden.
|
||||
3. **`ITaskListFilter`-Umbau.** Die Umstellung auf `IQueryable`-Expressions berührt die Filter-Registry (`src/ClaudeDo.Data/Filtering/`) und damit auch die virtuellen Listen. Kann bei Bedarf aus Phase 2 herausgelöst und nachgezogen werden — der Performance-Gewinn liegt heute ohnehin fast vollständig beim Rendering.
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
Phase 1 → Phase 2 (Spike zuerst) → Phase 3. Phase 1 ist unabhängig und kann parallel oder vorab laufen.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Usage-Optimierung — Befunde und Messmethodik
|
||||
|
||||
Stand: 2026-08-05. Ausgangsfrage: Wie lassen sich autonome ClaudeDo-Agents
|
||||
token-effizient betreiben? Abrechnung läuft über **Abo-Session-Limits, nicht über
|
||||
die API** — relevant ist roher Token-Verbrauch gegen 5h-/7d-Fenster, nicht Geld.
|
||||
|
||||
Dieses Dokument ist bewusst kompakt gehalten. Was eine Session früh in den Prefix
|
||||
lädt, wird mit jeder weiteren Nachricht multipliziert (siehe Befund 5) — ein
|
||||
30k-Handover-Dokument würde das Problem reproduzieren, das es beschreibt.
|
||||
|
||||
---
|
||||
|
||||
## 1. Verbrauchsstruktur
|
||||
|
||||
Gemessen über alle Transcripts unter `C:\Users\mika.kuns\.claude\projects`.
|
||||
|
||||
| Posten | Roh-Token | Anteil |
|
||||
|---|---:|---:|
|
||||
| Cache-Read | 3.118.025.644 | 95,6 % |
|
||||
| Cache-Write | 140.783.227 | 4,3 % |
|
||||
| Output | 22.466.702 | 0,7 % |
|
||||
| Input frisch | 141.496 | 0,0 % |
|
||||
|
||||
**Kontext-Resend = 99,3 % des Rohverbrauchs.** Auch unter API-Preisgewichtung
|
||||
(read 0,1× / write 1,25× / out 5×) bleibt es bei 81,3 %. Die Rangfolge ist gegen
|
||||
jede plausible Gewichtung robust.
|
||||
|
||||
Verstärkung: ~3,6 Mio einzigartiger Inhalt → 1,59 Mrd abgerechnete Prompt-Token
|
||||
in ClaudeDo-Sessions. **Jeder Kontext-Token wird im Schnitt ~425× erneut
|
||||
abgerechnet.**
|
||||
|
||||
## 2. Scope-Split — wer verbraucht
|
||||
|
||||
| Scope | Roh-Token | Anteil |
|
||||
|---|---:|---:|
|
||||
| INTERAKTIV (Mikas eigene Sessions) | 2.690.028.785 | **81,6 %** |
|
||||
| AGENT (ClaudeDo-Runs) | 606.222.055 | **18,4 %** |
|
||||
|
||||
Interaktiv nach Modell: opus-4-8 33,0 % · opus-5 28,5 % · sonnet-5 14,0 % ·
|
||||
fable-5 5,2 %. Agent-Runs fahren überwiegend sonnet-5 — der Default greift, die
|
||||
Modelldisziplin auf Agent-Seite ist in Ordnung.
|
||||
|
||||
**Konsequenz:** ClaudeDo-Optimierung adressiert maximal 18 % des Limits. Der
|
||||
größere Block sind die eigenen Opus-Sessions.
|
||||
|
||||
## 3. Session-Länge ist der Treiber
|
||||
|
||||
Verbrauch ≈ Nachrichten × Ø-Kontext, und der Kontext wächst mit den Nachrichten →
|
||||
**quadratisch**. Eine Session in k Teile schneiden bringt grob 1/k.
|
||||
|
||||
Interaktiv: **Top 20 von 255 Sessions = 55,9 % des Verbrauchs.**
|
||||
|
||||
| Roh-Token | Msgs | Ø Kontext | Modell |
|
||||
|---:|---:|---:|---|
|
||||
| 199.516.097 | 716 | 278.653 | opus-4-8 |
|
||||
| 138.020.247 | 440 | 313.682 | opus-5 |
|
||||
| 123.586.522 | 463 | 266.925 | opus-4-8 |
|
||||
|
||||
## 4. Was den Kontext füllt
|
||||
|
||||
| Tool | Anteil (Agent) | Anteil (interaktiv) |
|
||||
|---|---:|---:|
|
||||
| Read | 59,8 % | 68,2 % |
|
||||
| Bash | 16,3 % | 15,4 % |
|
||||
| Grep | 8,3 % | 5,2 % |
|
||||
|
||||
Read ohne `offset`/`limit`: **57 % (Agent) / 62 % (interaktiv)**.
|
||||
Re-Reads derselben Datei in derselben Session: 18 % der Read-Calls.
|
||||
Subagent-Nutzung: nur 40 Calls bei 1.325 Reads (Agent-Seite) — die
|
||||
Kontext-Firewall ist praktisch ungenutzt. Interaktiv sind Subagent-Sessions
|
||||
dagegen 20 % des Verbrauchs.
|
||||
|
||||
Teuerste Read-Ziele sind God-Files: `TasksIslandViewModel.cs` (1121 Z.),
|
||||
`ExternalMcpService.cs` (1002 Z.), `WorkerHub.cs` (979 Z.).
|
||||
|
||||
## 5. Prefix-Größe × Nachrichtenzahl schlägt alles
|
||||
|
||||
Fallstudie an der Analyse-Session selbst (136 Msgs, 43.492.835 Roh-Token):
|
||||
|
||||
```
|
||||
msg 1 : 39.914
|
||||
msg 3 : 273.822 (+233.908) <-- claude-api-Skill geladen
|
||||
msg 136: 380.327
|
||||
```
|
||||
|
||||
Der Skill in Nachricht 3 wurde danach 136× mitgelesen: **31,8 Mio Token = 73 %
|
||||
der Session**. Zum Vergleich: **alle tool_results zusammen = 21.600 Token
|
||||
(0,05 %)**.
|
||||
|
||||
Discovery ist praktisch gratis, wenn man aggregiert statt Dateien dumpt. Teuer ist
|
||||
ausschließlich, was früh und groß in den Prefix wandert. Derselbe Block in
|
||||
Nachricht 120 geladen hätte ein Zwanzigstel gekostet.
|
||||
|
||||
## 6. Amortisation von Planungssessions
|
||||
|
||||
| Posten | Token |
|
||||
|---|---:|
|
||||
| Analyse-Session gesamt | 43,5 Mio |
|
||||
| davon vermeidbarer Prefix-Ballast | −31,8 Mio |
|
||||
| echte Planungsleistung | ≈ 11,7 Mio |
|
||||
|
||||
Gegenrechnung: **10 von 37 Tasks brauchten einen Retry (27 %)**, 13 von 54 Runs
|
||||
sind Wiederholungen. Ein Agent-Run liegt im Schnitt bei ~11 Mio Roh-Token — ein
|
||||
vermiedener Retry spart also grob eine ganze Planungssession. Dazu ersparte
|
||||
Rediscovery: ein Fund von ~8k Token, den ein Agent in Turn 10 eines 60-Turn-Runs
|
||||
selbst machen müsste, wird danach ~50× mitgelesen = ~400k pro Fund.
|
||||
|
||||
**Fazit: gründliche Planung rechnet sich — aber nur bei schlankem Prefix.**
|
||||
|
||||
---
|
||||
|
||||
## Was NICHT hilft (geprüft und verworfen)
|
||||
|
||||
- **`--effort` senken.** Thinking ist 12.536 Token über alle interaktiven
|
||||
Sessions = **0,00 %** des Verbrauchs. Effort zu senken spart nichts und kostet
|
||||
Qualität. Endgültig erledigt.
|
||||
- **Skill-Trigger entschärfen.** Der `claude-api`-Skill kostete in einer Session
|
||||
73 % — feuerte aber nur **2× in 3 Wochen** (2026-07-14, 2026-08-05). Erwarteter
|
||||
Nutzen vernachlässigbar, Risiko (Antworten aus veraltetem Prior) real. Er hat
|
||||
sich in dieser Session sogar bezahlt gemacht: der Hinweis, dass `input_tokens`
|
||||
nur der uncached Rest ist, hat den Accounting-Bug aufgedeckt.
|
||||
- **God-Files splitten.** Eigenes Risiko; Read-Disziplin entschärft das Symptom
|
||||
billiger.
|
||||
|
||||
## Offene Hebel — noch nicht untersucht
|
||||
|
||||
- Interaktive Session-Hygiene: ab wann lohnt `/clear`, messbar an Ø-Kontext?
|
||||
- Modellrouting interaktiv (opus-4-8 + opus-5 = 61,5 % des Accounts).
|
||||
- Subagent-Ökonomie: Firewall-Nutzen gegen Eigenverbrauch (20 % interaktiv).
|
||||
- MCP-Tool-Definitionen im Prefix: Umfang bei ~200 deferred Tools nicht gemessen.
|
||||
- Kalibrierung der tatsächlichen Limit-Gewichtung gegen die Rohtoken-Zahlen
|
||||
(braucht `/usage`-Ausgabe; der OAuth-Endpoint ist für Claude nicht zugänglich,
|
||||
weil `~/.claude/.credentials.json` hart blockiert ist).
|
||||
|
||||
---
|
||||
|
||||
## Messmethodik (reproduzierbar)
|
||||
|
||||
Datenquelle: `C:\Users\mika.kuns\.claude\projects\**\*.jsonl`, ein Record je
|
||||
Message, Verbrauch in `message.usage`.
|
||||
|
||||
```python
|
||||
raw = (u.get('input_tokens',0) + u.get('output_tokens',0)
|
||||
+ u.get('cache_read_input_tokens',0) + u.get('cache_creation_input_tokens',0))
|
||||
```
|
||||
|
||||
Kontextgröße einer Message = `input_tokens + cache_read + cache_write` (ohne
|
||||
Output). Der Verlauf über die Session zeigt Prefix-Sprünge.
|
||||
|
||||
### Fallstricke
|
||||
|
||||
1. **Scope-Filter.** NICHT nach Pfadname `claudedo` filtern — das zieht
|
||||
interaktive Sessions im ClaudeDo-Repo mit rein und verfälscht den Split massiv
|
||||
(53 % statt korrekt 18,4 %). Agent-Runs erkennt man an `claudedo-worktrees`
|
||||
oder `sandbox` im Projektordner, so wie es `TranscriptUsageReader` macht.
|
||||
2. **`<synthetic>`-Messages** überspringen — keine echten API-Calls.
|
||||
3. **`task_runs.tokens_in` ist unbrauchbar** — liest nur `input_tokens`, also den
|
||||
uncached Rest. Lag bei einer 79-Mio-Token-Session bei 200 (Faktor ~400.000).
|
||||
`tokens_out` zusätzlich 3,8× zu niedrig. Siehe Task `38394081`.
|
||||
4. **In Bash absolute Pfade verwenden** — `$HOME`/`~` zeigt auf dieser Maschine
|
||||
auf das P:-Laufwerk, nicht auf `C:\Users\mika.kuns`.
|
||||
5. **DB nie direkt lesen** während die App läuft — Kopie ziehen (`todo.db` +
|
||||
`todo.db-wal`).
|
||||
6. **Grundrate prüfen, bevor ein Hebel empfohlen wird.** Ein teurer Einzelfall ist
|
||||
kein Muster (siehe Skill-Trigger oben).
|
||||
|
||||
---
|
||||
|
||||
## Abgeleitete Tasks (Liste „Claude do")
|
||||
|
||||
Empfohlene Reihenfolge:
|
||||
|
||||
| # | ID | Titel |
|
||||
|---|---|---|
|
||||
| 1 | `1b599d67` | Prompt-Dateien frieren Default ein — `SuggestImprovement`/`AskUser` tot |
|
||||
| 2 | `38394081` | Limit-Verbrauch pro Run sichtbar machen (Cache-Token in `task_runs`) |
|
||||
| 3 | `0d0aa8b0` | Read-Disziplin + Explorer-Subagent im System-Prompt |
|
||||
| 4 | `2de2f008` | `max_turns` deckeln (Ceiling, Defaults, UI-Warnung) |
|
||||
| 5 | `87105f5e` | UsageGate: Parallelität stufenweise drosseln |
|
||||
|
||||
(1) zuerst, weil es ein echter Funktionsbug ist und Voraussetzung dafür, dass (3)
|
||||
den Nutzer überhaupt erreicht. (2) als Nächstes, weil ohne korrektes Accounting
|
||||
nichts messbar ist.
|
||||
|
||||
### Ist-Zustand zum Zeitpunkt der Analyse
|
||||
|
||||
- `app_settings`: `default_model` = sonnet, `default_max_turns` = 100,
|
||||
`max_parallel_executions` = 3, `model_presets` = **null** (fällt auf Defaults
|
||||
zurück: haiku 20 / sonnet 30 / opus 40 / fable 25 — greifen faktisch nie).
|
||||
- 15 Tasks überschreiben `max_turns` nach oben: 10× auf 100, 5× auf 200.
|
||||
- UsageGate existiert bereits: 5h @ 80 %, 7d @ 90 %
|
||||
(Migration `20260805074906_AddUsageGateAndRunModel`, dieselbe Migration hat auch
|
||||
die `model`-Spalte auf `task_runs` gebracht).
|
||||
- `~/.todo-app/prompts/system.md` stammt vom 2026-06-04 und weicht vom
|
||||
`SystemDefault` in `PromptFiles.cs` ab; `agent.md` und `planning.md` dort sind
|
||||
verwaiste Reste eines alten Namensschemas.
|
||||
@@ -22,6 +22,7 @@
|
||||
<converters:BoolToItalicConverter x:Key="BoolToItalic"/>
|
||||
<converters:BoolToDraftOpacityConverter x:Key="BoolToDraftOpacity"/>
|
||||
<converters:LogKindForegroundConverter x:Key="LogKindForeground"/>
|
||||
<converters:KeepLastNumberConverter x:Key="KeepLastNumber"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
@@ -3,6 +3,9 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using ClaudeDo.Ui;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using ClaudeDo.Ui.Views;
|
||||
@@ -22,6 +25,8 @@ public partial class App : Application
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
if (_services?.GetService<AppSettings>() is { } settings)
|
||||
AccentPresetService.Apply(AccentPresets.Find(settings.AccentPreset));
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
@@ -37,9 +42,22 @@ public partial class App : Application
|
||||
// modeless Mission Control window is still open.
|
||||
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||
|
||||
var shell = services.GetRequiredService<IslandsShellViewModel>();
|
||||
|
||||
// Last-resort backstop: an exception escaping a dispatcher job kills the process, and
|
||||
// ClaudeDo hosts third-party UI (the ConPTY terminal control) whose async void key and
|
||||
// render handlers have done exactly that — one bad keystroke took the whole app down,
|
||||
// losing every open session. Swallowing is the lesser evil here: the failing job is
|
||||
// already dead either way, and the user still gets told via the footer error strip.
|
||||
Dispatcher.UIThread.UnhandledException += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
shell.FlashFooterError(Loc.T("vm.shell.unexpectedError", e.Exception.Message));
|
||||
};
|
||||
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = services.GetRequiredService<IslandsShellViewModel>(),
|
||||
DataContext = shell,
|
||||
};
|
||||
|
||||
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
|
||||
|
||||
@@ -8,19 +8,13 @@ Desktop entry point for the ClaudeDo application. Configures DI, initializes the
|
||||
- `App.axaml` / `App.axaml.cs` — Avalonia application lifecycle, main window creation, static `ServiceProvider` accessor
|
||||
- `ViewLocator.cs` — reflection-based IDataTemplate that maps ViewModels to Views by naming convention
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Avalonia 12.0.0 (Desktop, Fluent theme, Inter fonts)
|
||||
- CommunityToolkit.Mvvm 8.4.1
|
||||
- Microsoft.Extensions.DependencyInjection 8.0.1
|
||||
- Microsoft.AspNetCore.SignalR.Client 8.0.11
|
||||
- Microsoft.Data.Sqlite 8.0.11
|
||||
- Project references: ClaudeDo.Data, ClaudeDo.Ui
|
||||
Project references: `ClaudeDo.Data`, `ClaudeDo.Ui`. Package versions are in the `.csproj` — see
|
||||
the root CLAUDE.md for the tech stack.
|
||||
|
||||
## DI Registration Pattern
|
||||
|
||||
- **Singletons**: `IDbContextFactory`, all Repositories, GitService, WorkerClient, `IReleaseClient`, `UpdateCheckService`, `IPrimeScheduleApi`/`WorkerPrimeScheduleApi`, `INotesApi`/`WorkerNotesApi`, `InstallerLocator` / `WorkerLocator`, the island VMs (`ListsIslandViewModel`, `TasksIslandViewModel`, `DetailsIslandViewModel`) and `IslandsShellViewModel` (the window's DataContext)
|
||||
- **Transients**: modal VMs (`SettingsModalViewModel`, `MergeModalViewModel`, `ListSettingsModalViewModel`, `RepoImportModalViewModel`, `WeeklyReportModalViewModel`, `DiffViewerViewModel`, `WorktreesOverviewModalViewModel`, `PrimeClaudeTabViewModel`), several exposed as `Func<T>` factories for on-demand dialog creation (`Func<DiffViewerViewModel>` for the diff viewer); `ConflictResolverViewModel` via a `Func<string, ConflictResolverViewModel>` factory keyed by taskId (singleton factory, handed to `IslandsShellViewModel.ConflictResolverFactory`)
|
||||
- **Singletons** — `IDbContextFactory`, all repositories, `GitService`, `WorkerClient`, `IReleaseClient`, `UpdateCheckService`, `IPrimeScheduleApi`, `INotesApi`, `InstallerLocator`/`WorkerLocator`, the three island VMs, and `IslandsShellViewModel` (the window's DataContext)
|
||||
- **Transients** — modal VMs, several exposed as `Func<T>` factories for on-demand dialog creation (e.g. `Func<DiffViewerViewModel>`). `ConflictResolverViewModel` uses a `Func<string, ConflictResolverViewModel>` factory keyed by taskId (singleton factory, handed to `IslandsShellViewModel.ConflictResolverFactory`).
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ sealed class Program
|
||||
sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
|
||||
sc.AddTransient<WeeklyReportModalViewModel>();
|
||||
sc.AddTransient<Func<WeeklyReportModalViewModel>>(sp => () => sp.GetRequiredService<WeeklyReportModalViewModel>());
|
||||
sc.AddTransient<UsageMonitorModalViewModel>();
|
||||
sc.AddTransient<Func<UsageMonitorModalViewModel>>(sp => () => sp.GetRequiredService<UsageMonitorModalViewModel>());
|
||||
sc.AddSingleton<Func<string, ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel>>(sp =>
|
||||
taskId => new ClaudeDo.Ui.ViewModels.Conflicts.ConflictResolverViewModel(
|
||||
sp.GetRequiredService<IWorkerClient>(), taskId));
|
||||
@@ -159,10 +161,13 @@ sealed class Program
|
||||
sp,
|
||||
sp.GetRequiredService<INotesApi>(),
|
||||
sp.GetRequiredService<IMergeCoordinator>()));
|
||||
sc.AddSingleton<UsagePillViewModel>(sp =>
|
||||
new UsagePillViewModel(sp.GetRequiredService<IWorkerClient>()));
|
||||
sc.AddSingleton<MissionControlViewModel>(sp =>
|
||||
new MissionControlViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp.GetRequiredService<IWorkerClient>()));
|
||||
sp.GetRequiredService<IWorkerClient>(),
|
||||
sp.GetRequiredService<UsagePillViewModel>()));
|
||||
sc.AddSingleton<IslandsShellViewModel>(sp =>
|
||||
{
|
||||
var shell = ActivatorUtilities.CreateInstance<IslandsShellViewModel>(sp);
|
||||
|
||||
+81
-26
@@ -4,49 +4,104 @@ Shared data layer: models, repositories, SQLite infrastructure, and git operatio
|
||||
|
||||
## Models
|
||||
|
||||
- **TaskEntity** — Id, ListId, Title, Description, Status (`Idle|Queued|Running|WaitingForChildren|WaitingForReview|Done|Failed|Cancelled`), PlanningPhase (`None|Active|Finalized` — parent-only), BlockedByTaskId (nullable FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback (nullable; reviewer's rejection comment, consumed and cleared by the runner on the next re-run), LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy. Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
|
||||
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
|
||||
- **ListConfigEntity** — ListId (PK, 1:1 with list), Model, SystemPrompt, AgentPath, MaxTurns (all nullable)
|
||||
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
|
||||
- **TaskRunEntity** — per-run record (session_id, tokens, turns, result, structured output, exit code, log path)
|
||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||||
- **DailyNoteEntity** — Id, Date (DateOnly), Text, SortOrder, CreatedAt → table `daily_notes`
|
||||
- **WeekReportEntity** — Id, StartDate/EndDate (DateOnly), Markdown, GeneratedAt → table `week_reports`, unique index on (start_date, end_date)
|
||||
- **TaskAttachmentEntity** — Id, TaskId (FK to tasks, ON DELETE CASCADE), FileName, ByteSize, CreatedAt → table `task_attachments`
|
||||
- **AppSettingsEntity** also carries `ReportExcludedPaths` (string?, JSON array of excluded path prefixes, column `report_excluded_paths`), `StandupWeekday` (int DayOfWeek, default Wednesday, column `standup_weekday`), and `DailyPrepMaxTasks` (int, default 5, column `daily_prep_max_tasks` — hard cap on how many open tasks the daily-prep / "Prime Claude" feature may place in MyDay)
|
||||
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
|
||||
- **TaskEntity** — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId.
|
||||
- Status / PlanningPhase / BlockedByTaskId semantics + allowed transitions: `ClaudeDo.Worker/CLAUDE.md` → Status Model.
|
||||
- `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).
|
||||
- Legacy status values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill via the `RetireLegacyTaskStatus` migration.
|
||||
- **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 (all nullable). `VerifyCommand` is an optional post-merge gate; null/blank = no gate → [review-merge](../../docs/explore-notes/review-merge.md).
|
||||
- **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).
|
||||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, column `days_of_week`), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on selected weekdays; no date range.
|
||||
- **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`
|
||||
- **SubtaskEntity**, **AgentInfo** — subtasks / record for scanned agent files
|
||||
|
||||
### AppSettingsEntity
|
||||
|
||||
Beyond the basics it carries:
|
||||
|
||||
| Property | Column | Default | Note |
|
||||
|---|---|---|---|
|
||||
| `DefaultMaxTurns` | `default_max_turns` | **40** | Lowered from 100; `AddMaxTurnsCeiling` backfilled the seeded row. |
|
||||
| `MaxTurnsCeiling` | `max_turns_ceiling` | 80 | Hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run. `UpdateAsync` clamps to min 1. |
|
||||
| `ModelPresets` | `model_presets` | seeded | JSON array of `ModelPreset` rows. ⚠️ `AppSettingsRepository.GetAsync` **backfills shipping defaults on the first read after it's null**, so it's never null once a run has started. |
|
||||
| `UsageGateFiveHourPct` / `UsageGateSevenDayPct` | `usage_gate_*_pct` | 80 / 90 | Queue pause thresholds; `0` = off. |
|
||||
| `UsageThrottle{FiveHour,SevenDay}{Soft,Hard}Pct` | `usage_throttle_{five_hour,seven_day}_{soft,hard}_pct` | 50 / 65 per bucket | Staged parallelism below the hard gate, **per bucket**; `0` = that stage off. Edited by dragging the usage-monitor gauges. |
|
||||
| `DailyPrepMaxTasks` | `daily_prep_max_tasks` | 5 | Hard cap on MyDay tasks the daily prep may place. |
|
||||
| `ReportExcludedPaths` | `report_excluded_paths` | null | JSON array of excluded path prefixes. |
|
||||
| `StandupWeekday` | `standup_weekday` | Wednesday | int `DayOfWeek`. |
|
||||
|
||||
All four usage percentages are clamped 0..100 by `AppSettingsRepository.UpdateAsync`.
|
||||
Gate/throttle semantics → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md).
|
||||
|
||||
### 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).
|
||||
- **ModelRegistry** — `NormalizeAlias` is the strict, **throwing** validator for `add_task`/planning model input. `TryNormalizeAlias` is the non-throwing counterpart for the run path (exact alias match, then substring match against a full model id, else `null`). `ByCostAscending` = the cost order the prompts use.
|
||||
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag).
|
||||
|
||||
## Repositories
|
||||
|
||||
All repositories use EF Core LINQ queries via `ClaudeDoDbContext`. The atomic `Queued -> Running` claim lives in the Worker's `QueuePicker` (uses `FromSqlRaw`), not here.
|
||||
All use EF Core LINQ via `ClaudeDoDbContext`. The atomic `Queued → Running` claim lives in the
|
||||
Worker's `QueuePicker` (`FromSqlRaw`), **not** here.
|
||||
|
||||
- **TaskRepository** — CRUD, planning helpers (`CreateChildAsync`, `SetPlanningStartedAsync`, `DiscardPlanningAsync`, `UpdateChildAsync`), `UpdateAgentSettingsAsync` (model / system-prompt / agent-path overrides). Status-mutation primitives `MarkRunningAsync` / `MarkDoneAsync` / `MarkFailedAsync` / `FlipAllRunningToFailedAsync` are `internal` and called only by `TaskStateService` in the worker. `CreateChildAsync` produces children with `Status=Idle, PlanningPhase=None`; once their parent's `PlanningPhase` becomes `Finalized`, the chain coordinator queues them.
|
||||
- **ListRepository** — CRUD, `GetConfigAsync` / `SetConfigAsync` (upsert) / `DeleteConfigAsync` for `list_config`
|
||||
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
|
||||
- **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||
- **DailyNoteRepository** — `ListByDayAsync`, `ListBetweenAsync`, `AddAsync`, `UpdateAsync`, `DeleteAsync`
|
||||
- **WeekReportRepository** — `GetByRangeAsync`, `UpsertAsync`
|
||||
- **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`.
|
||||
- **ListRepository** — CRUD, `GetConfigAsync`/`SetConfigAsync` (upsert)/`DeleteConfigAsync` for `list_config`
|
||||
- **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)`, `DeleteAllForTaskAsync`
|
||||
- **DailyNoteRepository**, **WeekReportRepository**, **TaskRunRepository**, **SubtaskRepository**, **AppSettingsRepository**
|
||||
|
||||
`TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment
|
||||
dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- **ClaudeDoDbContext** — EF Core DbContext; configured with WAL mode and foreign keys via `UseSqlite` options
|
||||
- **IDbContextFactory<ClaudeDoDbContext>** — registered in DI; used by singleton consumers (e.g. Worker hosted service)
|
||||
- **Paths** — expands `~` and `%USERPROFILE%`, resolves relative paths. App root: `~/.todo-app`
|
||||
- **ClaudeDoDbContext** — EF Core DbContext; WAL mode + foreign keys via `UseSqlite` options
|
||||
- **IDbContextFactory\<ClaudeDoDbContext\>** — registered in DI; used by singleton consumers (e.g. the Worker hosted service)
|
||||
- **Paths** — expands `~` and `%USERPROFILE%`, resolves relative paths. App root `~/.todo-app`
|
||||
- **AppSettings** — loads `~/.todo-app/ui.config.json` (DbPath, SignalRUrl)
|
||||
- **AttachmentStore** — dependency-free file store; default root `~/.todo-app/attachments/<taskId>/`. `SaveAsync` enforces a 5 MB cap and path-traversal/containment guard. Also exposes `DeleteFile`, `DeleteTaskDir`, `TaskDir`, `Root`, and `EnumerateTaskIds` (used by the worker orphan sweep). Attachment files live outside git worktrees intentionally.
|
||||
- **AttachmentStore** — dependency-free file store, default root `~/.todo-app/attachments/<taskId>/`. `SaveAsync` enforces a 5 MB cap and a path-traversal/containment guard. Also `DeleteFile`, `DeleteTaskDir`, `TaskDir`, `Root`, `EnumerateTaskIds` (used by the worker orphan sweep). Attachment files live **outside** git worktrees intentionally.
|
||||
|
||||
## Git
|
||||
|
||||
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Worktree ops (add — serialized to avoid a commondir race —, remove, prune, list paths for branch), branch ops (current, list local, checkout, delete), staging/commit (status porcelain, add-all, add-path, commit via stdin), diffs (working tree, branch vs base, commit range `base..head` — used to show a merged task's diff after the worktree is gone —, per-file, diff-stat, committed files, has-changes), merge (ff-only, no-ff, abort, mid-merge detection, conflicted files), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
|
||||
**GitService** — async wrapper around the git CLI (`ProcessStartInfo`, no shell):
|
||||
|
||||
- Worktrees: add (**serialized** to avoid a commondir race), remove, prune, list paths for branch
|
||||
- Branches: current, list local, checkout, delete
|
||||
- Staging/commit: status porcelain, add-all, add-path, commit via stdin
|
||||
- Diffs: working tree, branch vs base, commit range `base..head` (shows a merged task's diff after the worktree is gone), per-file, diff-stat, committed files, has-changes
|
||||
- Merge: ff-only, no-ff, abort, mid-merge detection (`MERGE_HEAD`), conflicted files
|
||||
- Revert: `RevertMergeCommitAsync` (`git revert --no-edit -m 1 <sha>`), `RevertAbortAsync`, `IsMidRevertAsync` (`REVERT_HEAD`, mirrors `IsMidMergeAsync`)
|
||||
- `PreviewMergeAsync` (non-destructive check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo
|
||||
|
||||
⚠️ **Revert never resets or rewrites** — it always produces a new commit, because the working
|
||||
directory it operates on is shared with other concurrent sessions.
|
||||
|
||||
## Schema
|
||||
|
||||
Tables: `lists`, `tasks`, `worktrees`, `list_config`, `task_runs`, `subtasks`, `app_settings`, `prime_schedules`, `daily_notes`, `week_reports`, `task_attachments`. Managed by EF Core migrations in the `Migrations/` folder. The `tasks` table holds `status`, `planning_phase` (default `none`), and `blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`). Migration `WeeklyReport` added `daily_notes`, `week_reports`, and the two new `app_settings` columns. Migration `DailyPrepMaxTasks` added the `daily_prep_max_tasks` column to `app_settings` (no new tables). Migration `AddTaskAttachments` created the `task_attachments` table. `TaskRepository.DeleteAsync` and `ListRepository.DeleteAsync` also delete the on-disk attachment dir(s) via an optional `AttachmentStore` ctor param (defaults to the production store).
|
||||
Tables (one per line so parallel migrations don't collide on the same line):
|
||||
- `lists`
|
||||
- `tasks`
|
||||
- `worktrees`
|
||||
- `list_config`
|
||||
- `task_runs`
|
||||
- `subtasks`
|
||||
- `app_settings`
|
||||
- `prime_schedules`
|
||||
- `daily_notes`
|
||||
- `week_reports`
|
||||
- `task_attachments`
|
||||
|
||||
Managed by EF Core migrations in `Migrations/` — **`ls Migrations/` is the authoritative history**;
|
||||
don't maintain a changelog here. `tasks` holds `status`, `planning_phase` (default `none`), and
|
||||
`blocked_by_task_id` (FK to `tasks.id`, `ON DELETE SET NULL`).
|
||||
|
||||
## Conventions
|
||||
|
||||
- Enum <-> string mapping via EF Core `ValueConverter` (configured in `IEntityTypeConfiguration<T>`)
|
||||
- Entity configurations live in the `Configuration/` folder
|
||||
- Enum ↔ string mapping via EF Core `ValueConverter`, configured in `IEntityTypeConfiguration<T>`
|
||||
- Entity configurations live in `Configuration/`
|
||||
- Primary keys are `init`-only strings (GUIDs assigned at creation)
|
||||
- All methods are async with CancellationToken where applicable
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ClaudeDo.Worker" />
|
||||
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||
<InternalsVisibleTo Include="ClaudeDo.Data.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -18,10 +18,13 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
builder.Property(s => s.DefaultModel)
|
||||
.HasColumnName("default_model").IsRequired().HasDefaultValue("sonnet");
|
||||
builder.Property(s => s.DefaultMaxTurns)
|
||||
.HasColumnName("default_max_turns").IsRequired().HasDefaultValue(30);
|
||||
.HasColumnName("default_max_turns").IsRequired().HasDefaultValue(40);
|
||||
builder.Property(s => s.DefaultPermissionMode)
|
||||
.HasColumnName("default_permission_mode").IsRequired().HasDefaultValue("bypassPermissions");
|
||||
|
||||
builder.Property(s => s.MaxTurnsCeiling)
|
||||
.HasColumnName("max_turns_ceiling").IsRequired().HasDefaultValue(80);
|
||||
|
||||
builder.Property(s => s.MaxParallelExecutions)
|
||||
.HasColumnName("max_parallel_executions").IsRequired().HasDefaultValue(1);
|
||||
|
||||
@@ -45,6 +48,21 @@ public class AppSettingsEntityConfiguration : IEntityTypeConfiguration<AppSettin
|
||||
.HasColumnName("daily_prep_max_tasks").IsRequired().HasDefaultValue(5);
|
||||
|
||||
builder.Property(s => s.SessionSkills).HasColumnName("session_skills");
|
||||
builder.Property(s => s.ModelPresets).HasColumnName("model_presets");
|
||||
|
||||
builder.Property(s => s.UsageGateFiveHourPct)
|
||||
.HasColumnName("usage_gate_five_hour_pct").IsRequired().HasDefaultValue(80);
|
||||
builder.Property(s => s.UsageGateSevenDayPct)
|
||||
.HasColumnName("usage_gate_seven_day_pct").IsRequired().HasDefaultValue(90);
|
||||
|
||||
builder.Property(s => s.UsageThrottleFiveHourSoftPct)
|
||||
.HasColumnName("usage_throttle_five_hour_soft_pct").IsRequired().HasDefaultValue(50);
|
||||
builder.Property(s => s.UsageThrottleFiveHourHardPct)
|
||||
.HasColumnName("usage_throttle_five_hour_hard_pct").IsRequired().HasDefaultValue(65);
|
||||
builder.Property(s => s.UsageThrottleSevenDaySoftPct)
|
||||
.HasColumnName("usage_throttle_seven_day_soft_pct").IsRequired().HasDefaultValue(50);
|
||||
builder.Property(s => s.UsageThrottleSevenDayHardPct)
|
||||
.HasColumnName("usage_throttle_seven_day_hard_pct").IsRequired().HasDefaultValue(65);
|
||||
|
||||
builder.HasData(new AppSettingsEntity { Id = AppSettingsEntity.SingletonId });
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@ public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfig
|
||||
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
||||
builder.Property(c => c.MaxTurns).HasColumnName("max_turns");
|
||||
builder.Property(c => c.SessionSkills).HasColumnName("session_skills");
|
||||
builder.Property(c => c.VerifyCommand).HasColumnName("verify_command");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
||||
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
||||
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
|
||||
builder.Property(l => l.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||
builder.Property(l => l.IsManual).HasColumnName("is_manual").IsRequired().HasDefaultValue(false);
|
||||
|
||||
builder.HasIndex(l => l.SortOrder).HasDatabaseName("idx_lists_sort");
|
||||
|
||||
|
||||
@@ -89,9 +89,13 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
||||
builder.Property(t => t.MaxTurns).HasColumnName("max_turns");
|
||||
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.Notes).HasColumnName("notes");
|
||||
builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0);
|
||||
builder.Property(t => t.SessionSkills).HasColumnName("session_skills");
|
||||
builder.Property(t => t.HandlerBaseCommit).HasColumnName("handler_base_commit");
|
||||
builder.Property(t => t.HandlerHeadCommit).HasColumnName("handler_head_commit");
|
||||
builder.Property(t => t.InteractiveSessionId).HasColumnName("interactive_session_id");
|
||||
|
||||
builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id");
|
||||
builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id");
|
||||
|
||||
@@ -24,9 +24,12 @@ public class TaskRunEntityConfiguration : IEntityTypeConfiguration<TaskRunEntity
|
||||
builder.Property(r => r.TurnCount).HasColumnName("turn_count");
|
||||
builder.Property(r => r.TokensIn).HasColumnName("tokens_in");
|
||||
builder.Property(r => r.TokensOut).HasColumnName("tokens_out");
|
||||
builder.Property(r => r.CacheReadTokens).HasColumnName("cache_read_tokens");
|
||||
builder.Property(r => r.CacheWriteTokens).HasColumnName("cache_write_tokens");
|
||||
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
||||
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
||||
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
||||
builder.Property(r => r.Model).HasColumnName("model");
|
||||
|
||||
builder.HasOne(r => r.Task)
|
||||
.WithMany(t => t.Runs)
|
||||
|
||||
@@ -35,6 +35,7 @@ public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEnti
|
||||
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
||||
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.State).HasColumnName("state").IsRequired()
|
||||
.HasDefaultValue(WorktreeState.Active)
|
||||
.HasConversion(StateConverter);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using SysEnvironment = System.Environment;
|
||||
|
||||
namespace ClaudeDo.Data.Environment;
|
||||
|
||||
public sealed record ResolvedExecutable(string Path, bool IsShim);
|
||||
|
||||
public sealed record ShimStartInfo(string FileName, string Arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a command the way Windows' CreateProcess/PATH search does, but also finds
|
||||
/// non-.exe shims (.cmd/.bat/.ps1) that UseShellExecute=false alone would miss.
|
||||
/// </summary>
|
||||
public static class ExecutableResolver
|
||||
{
|
||||
private const string DefaultPathExt = ".COM;.EXE;.BAT;.CMD";
|
||||
|
||||
// Known npm/claude install locations to try when PATH search comes up empty.
|
||||
private static readonly string[] FallbackDirectoryTemplates =
|
||||
{
|
||||
"%APPDATA%\\npm",
|
||||
"%LOCALAPPDATA%\\Programs\\claude",
|
||||
"%USERPROFILE%\\.local\\bin",
|
||||
};
|
||||
|
||||
public static ResolvedExecutable? Resolve(string command, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
var pathExts = ParsePathExt(pathExtOverride);
|
||||
|
||||
if (LooksLikePath(command))
|
||||
{
|
||||
return ResolveAsPath(command, pathExts);
|
||||
}
|
||||
|
||||
var directories = ParsePath(pathOverride);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var resolved = ResolveInDirectory(dir, command, pathExts);
|
||||
if (resolved is not null) return resolved;
|
||||
}
|
||||
|
||||
foreach (var template in FallbackDirectoryTemplates)
|
||||
{
|
||||
var dir = SysEnvironment.ExpandEnvironmentVariables(template);
|
||||
var resolved = ResolveInDirectory(dir, command, pathExts);
|
||||
if (resolved is not null) return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Expanded fallback directories tried when PATH search comes up empty (for diagnostics).</summary>
|
||||
public static IReadOnlyList<string> FallbackDirectories() =>
|
||||
FallbackDirectoryTemplates.Select(SysEnvironment.ExpandEnvironmentVariables).ToList();
|
||||
|
||||
public static ShimStartInfo BuildShimStartInfo(string shimPath, IReadOnlyList<string> arguments)
|
||||
{
|
||||
var parts = new List<string> { "/c", Quote(shimPath) };
|
||||
parts.AddRange(arguments.Select(Quote));
|
||||
return new ShimStartInfo("cmd.exe", string.Join(' ', parts));
|
||||
}
|
||||
|
||||
private static bool LooksLikePath(string command) =>
|
||||
command.Contains(Path.DirectorySeparatorChar) || command.Contains(Path.AltDirectorySeparatorChar);
|
||||
|
||||
private static ResolvedExecutable? ResolveAsPath(string command, IReadOnlyList<string> pathExts)
|
||||
{
|
||||
if (File.Exists(command)) return new ResolvedExecutable(command, IsShimExtension(Path.GetExtension(command)));
|
||||
|
||||
if (Path.HasExtension(command)) return null;
|
||||
|
||||
foreach (var ext in pathExts)
|
||||
{
|
||||
var candidate = command + ext;
|
||||
if (File.Exists(candidate)) return new ResolvedExecutable(candidate, IsShimExtension(ext));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ResolvedExecutable? ResolveInDirectory(string directory, string command, IReadOnlyList<string> pathExts)
|
||||
{
|
||||
if (!Directory.Exists(directory)) return null;
|
||||
|
||||
if (Path.HasExtension(command))
|
||||
{
|
||||
var candidate = Path.Combine(directory, command);
|
||||
return File.Exists(candidate) ? new ResolvedExecutable(candidate, IsShimExtension(Path.GetExtension(candidate))) : null;
|
||||
}
|
||||
|
||||
foreach (var ext in pathExts)
|
||||
{
|
||||
var candidate = Path.Combine(directory, command + ext);
|
||||
if (File.Exists(candidate)) return new ResolvedExecutable(candidate, IsShimExtension(ext));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsShimExtension(string extension) =>
|
||||
!extension.Equals(".exe", StringComparison.OrdinalIgnoreCase)
|
||||
&& !extension.Equals(".com", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static IReadOnlyList<string> ParsePathExt(string? pathExtOverride)
|
||||
{
|
||||
var raw = pathExtOverride ?? SysEnvironment.GetEnvironmentVariable("PATHEXT") ?? DefaultPathExt;
|
||||
return raw.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ParsePath(string? pathOverride)
|
||||
{
|
||||
var raw = pathOverride ?? SysEnvironment.GetEnvironmentVariable("PATH") ?? "";
|
||||
return raw.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value;
|
||||
}
|
||||
@@ -26,6 +26,22 @@ public sealed class GitService
|
||||
return stdout.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if <paramref name="ancestorSha"/> is an ancestor of (or equal to) <paramref name="descendantSha"/>,
|
||||
/// via `git merge-base --is-ancestor`. Null means the answer can't be determined (e.g. the commit is
|
||||
/// unknown in this repo) — callers must treat that as "unknown", never as "not an ancestor".
|
||||
/// </summary>
|
||||
public async Task<bool?> IsAncestorAsync(string repoDir, string ancestorSha, string descendantSha, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, _) = await RunGitAsync(repoDir, ["merge-base", "--is-ancestor", ancestorSha, descendantSha], ct);
|
||||
return exitCode switch
|
||||
{
|
||||
0 => true,
|
||||
1 => false,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task WorktreeAddAsync(string repoDir, string branchName, string worktreePath, string baseCommit, CancellationToken ct = default)
|
||||
{
|
||||
await WorktreeAddGate.WaitAsync(ct);
|
||||
@@ -280,6 +296,36 @@ public sealed class GitService
|
||||
throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
public async Task<bool> IsMidRevertAsync(string repoDir, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["rev-parse", "--git-dir"], ct);
|
||||
if (exitCode != 0) return false;
|
||||
var gitDir = stdout.Trim();
|
||||
if (!Path.IsPathRooted(gitDir))
|
||||
gitDir = Path.Combine(repoDir, gitDir);
|
||||
return File.Exists(Path.Combine(gitDir, "REVERT_HEAD"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts a single commit with `-m 1` (diff against its first parent) — the form needed to
|
||||
/// revert a merge commit. On success this creates a new commit with the inverse changes;
|
||||
/// the original commit and all history stay intact (no rewrite, no reset).
|
||||
/// </summary>
|
||||
public async Task<(int ExitCode, string Stderr)> RevertMergeCommitAsync(
|
||||
string repoDir, string mergeCommitSha, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir,
|
||||
["-c", "merge.conflictStyle=diff3", "revert", "--no-edit", "-m", "1", mergeCommitSha], ct);
|
||||
return (exitCode, stderr);
|
||||
}
|
||||
|
||||
public async Task RevertAbortAsync(string repoDir, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["revert", "--abort"], ct);
|
||||
if (exitCode != 0)
|
||||
throw new InvalidOperationException($"git revert --abort failed (exit {exitCode}): {stderr}");
|
||||
}
|
||||
|
||||
public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
|
||||
{
|
||||
var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
|
||||
|
||||
+802
@@ -0,0 +1,802 @@
|
||||
// <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("20260727114206_AddModelPresetsAndManualFlag")]
|
||||
partial class AddModelPresetsAndManualFlag
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<DateTime?>("FinishedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("finished_at");
|
||||
|
||||
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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
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<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>("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.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,57 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddModelPresetsAndManualFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_manual",
|
||||
table: "tasks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "is_manual",
|
||||
table: "lists",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "model_presets",
|
||||
table: "app_settings",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "model_presets",
|
||||
value: null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_manual",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "is_manual",
|
||||
table: "lists");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "model_presets",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+810
@@ -0,0 +1,810 @@
|
||||
// <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("20260805065217_AddHandlerCommitRange")]
|
||||
partial class AddHandlerCommitRange
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("prompt");
|
||||
|
||||
b.Property<string>("ResultMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("result_markdown");
|
||||
|
||||
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<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>("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.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,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHandlerCommitRange : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "handler_base_commit",
|
||||
table: "tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "handler_head_commit",
|
||||
table: "tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "handler_base_commit",
|
||||
table: "tasks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "handler_head_commit",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
+828
@@ -0,0 +1,828 @@
|
||||
// <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("20260805074906_AddUsageGateAndRunModel")]
|
||||
partial class AddUsageGateAndRunModel
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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<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<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>("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.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,57 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUsageGateAndRunModel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "model",
|
||||
table: "task_runs",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_gate_five_hour_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 80);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_gate_seven_day_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 90);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "usage_gate_five_hour_pct", "usage_gate_seven_day_pct" },
|
||||
values: new object[] { 80, 90 });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "model",
|
||||
table: "task_runs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_gate_five_hour_pct",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_gate_seven_day_pct",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
// <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("20260805090016_AddVerifyCommand")]
|
||||
partial class AddVerifyCommand
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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<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<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>("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.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,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddVerifyCommand : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "verify_command",
|
||||
table: "list_config",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "verify_command",
|
||||
table: "list_config");
|
||||
}
|
||||
}
|
||||
}
|
||||
+832
@@ -0,0 +1,832 @@
|
||||
// <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("20260805091910_AddWorktreeMergeCommit")]
|
||||
partial class AddWorktreeMergeCommit
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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<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<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.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,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWorktreeMergeCommit : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "merge_commit",
|
||||
table: "worktrees",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "merge_commit",
|
||||
table: "worktrees");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,843 @@
|
||||
// <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("20260805132052_AddMaxTurnsCeiling")]
|
||||
partial class AddMaxTurnsCeiling
|
||||
{
|
||||
/// <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<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<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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 40,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
MaxTurnsCeiling = 80,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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<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<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.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,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxTurnsCeiling : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "default_max_turns",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 40,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldDefaultValue: 30);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "max_turns_ceiling",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 80);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "default_max_turns", "max_turns_ceiling" },
|
||||
values: new object[] { 40, 80 });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "max_turns_ceiling",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "default_max_turns",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 30,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldDefaultValue: 40);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
column: "default_max_turns",
|
||||
value: 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
// <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("20260805132801_AddRunCacheTokens")]
|
||||
partial class AddRunCacheTokens
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<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<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<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.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,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRunCacheTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "cache_read_tokens",
|
||||
table: "task_runs",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "cache_write_tokens",
|
||||
table: "task_runs",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "cache_read_tokens",
|
||||
table: "task_runs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "cache_write_tokens",
|
||||
table: "task_runs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+850
@@ -0,0 +1,850 @@
|
||||
// <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("20260805133429_AddUsageThrottleThresholds")]
|
||||
partial class AddUsageThrottleThresholds
|
||||
{
|
||||
/// <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<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(30)
|
||||
.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<string>("ModelPresets")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model_presets");
|
||||
|
||||
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>("UsageThrottleHardPct")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(65)
|
||||
.HasColumnName("usage_throttle_hard_pct");
|
||||
|
||||
b.Property<int>("UsageThrottleSoftPct")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(50)
|
||||
.HasColumnName("usage_throttle_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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
UsageThrottleHardPct = 65,
|
||||
UsageThrottleSoftPct = 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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<string>("ErrorMarkdown")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("error_markdown");
|
||||
|
||||
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<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<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.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,47 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUsageThrottleThresholds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_throttle_hard_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 65);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_throttle_soft_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 50);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "app_settings",
|
||||
keyColumn: "id",
|
||||
keyValue: 1,
|
||||
columns: new[] { "usage_throttle_hard_pct", "usage_throttle_soft_pct" },
|
||||
values: new object[] { 65, 50 });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_throttle_hard_pct",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_throttle_soft_pct",
|
||||
table: "app_settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+869
@@ -0,0 +1,869 @@
|
||||
// <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("20260806111454_AddInteractiveSessionId")]
|
||||
partial class AddInteractiveSessionId
|
||||
{
|
||||
/// <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<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<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>("UsageThrottleHardPct")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(65)
|
||||
.HasColumnName("usage_throttle_hard_pct");
|
||||
|
||||
b.Property<int>("UsageThrottleSoftPct")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(50)
|
||||
.HasColumnName("usage_throttle_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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 40,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
MaxTurnsCeiling = 80,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
UsageThrottleHardPct = 65,
|
||||
UsageThrottleSoftPct = 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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<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<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<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.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,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddInteractiveSessionId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "interactive_session_id",
|
||||
table: "tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "interactive_session_id",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
+883
@@ -0,0 +1,883 @@
|
||||
// <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("20260806141710_SplitUsageThrottlePerBucket")]
|
||||
partial class SplitUsageThrottlePerBucket
|
||||
{
|
||||
/// <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<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<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,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 40,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
MaxTurnsCeiling = 80,
|
||||
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<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>("SessionSkills")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("session_skills");
|
||||
|
||||
b.Property<string>("SystemPrompt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
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>("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<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>("Description")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("description");
|
||||
|
||||
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<string>("ParentTaskId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("parent_task_id");
|
||||
|
||||
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>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BlockedByTaskId")
|
||||
.HasDatabaseName("idx_tasks_blocked_by");
|
||||
|
||||
b.HasIndex("ListId")
|
||||
.HasDatabaseName("idx_tasks_list_id");
|
||||
|
||||
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<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<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<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.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,70 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ClaudeDo.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SplitUsageThrottlePerBucket : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "usage_throttle_soft_pct",
|
||||
table: "app_settings",
|
||||
newName: "usage_throttle_seven_day_soft_pct");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "usage_throttle_hard_pct",
|
||||
table: "app_settings",
|
||||
newName: "usage_throttle_seven_day_hard_pct");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_throttle_five_hour_hard_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 65);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "usage_throttle_five_hour_soft_pct",
|
||||
table: "app_settings",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 50);
|
||||
|
||||
// The old single soft/hard pair was compared against whichever bucket was more utilized,
|
||||
// so carrying it into BOTH buckets keeps an existing install behaving exactly as before
|
||||
// the split — the rename above already preserved it for the 7d side.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE app_settings
|
||||
SET usage_throttle_five_hour_soft_pct = usage_throttle_seven_day_soft_pct,
|
||||
usage_throttle_five_hour_hard_pct = usage_throttle_seven_day_hard_pct;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_throttle_five_hour_hard_pct",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "usage_throttle_five_hour_soft_pct",
|
||||
table: "app_settings");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "usage_throttle_seven_day_soft_pct",
|
||||
table: "app_settings",
|
||||
newName: "usage_throttle_soft_pct");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "usage_throttle_seven_day_hard_pct",
|
||||
table: "app_settings",
|
||||
newName: "usage_throttle_hard_pct");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace ClaudeDo.Data.Migrations
|
||||
b.Property<int>("DefaultMaxTurns")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30)
|
||||
.HasDefaultValue(40)
|
||||
.HasColumnName("default_max_turns");
|
||||
|
||||
b.Property<string>("DefaultModel")
|
||||
@@ -66,6 +66,16 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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<string>("RepoImportFolders")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("repo_import_folders");
|
||||
@@ -84,6 +94,42 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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")
|
||||
@@ -113,11 +159,18 @@ namespace ClaudeDo.Data.Migrations
|
||||
Id = 1,
|
||||
DailyPrepMaxTasks = 5,
|
||||
DefaultClaudeInstructions = "",
|
||||
DefaultMaxTurns = 100,
|
||||
DefaultMaxTurns = 40,
|
||||
DefaultModel = "sonnet",
|
||||
DefaultPermissionMode = "auto",
|
||||
MaxParallelExecutions = 1,
|
||||
MaxTurnsCeiling = 80,
|
||||
StandupWeekday = 3,
|
||||
UsageGateFiveHourPct = 80,
|
||||
UsageGateSevenDayPct = 90,
|
||||
UsageThrottleFiveHourHardPct = 65,
|
||||
UsageThrottleFiveHourSoftPct = 50,
|
||||
UsageThrottleSevenDayHardPct = 65,
|
||||
UsageThrottleSevenDaySoftPct = 50,
|
||||
WorktreeAutoCleanupDays = 7,
|
||||
WorktreeAutoCleanupEnabled = false,
|
||||
WorktreeStrategy = "sibling"
|
||||
@@ -180,6 +233,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("system_prompt");
|
||||
|
||||
b.Property<string>("VerifyCommand")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("verify_command");
|
||||
|
||||
b.HasKey("ListId");
|
||||
|
||||
b.ToTable("list_config", (string)null);
|
||||
@@ -202,6 +259,12 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasDefaultValue("chore")
|
||||
.HasColumnName("default_commit_type");
|
||||
|
||||
b.Property<bool>("IsManual")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_manual");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
@@ -406,6 +469,24 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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")
|
||||
@@ -534,6 +615,14 @@ namespace ClaudeDo.Data.Migrations
|
||||
.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");
|
||||
@@ -556,6 +645,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_path");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("model");
|
||||
|
||||
b.Property<string>("Prompt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
@@ -665,6 +758,10 @@ namespace ClaudeDo.Data.Migrations
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("head_commit");
|
||||
|
||||
b.Property<string>("MergeCommit")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("merge_commit");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
|
||||
@@ -8,9 +8,13 @@ public sealed class AppSettingsEntity
|
||||
|
||||
public string DefaultClaudeInstructions { get; set; } = string.Empty;
|
||||
public string DefaultModel { get; set; } = "sonnet";
|
||||
public int DefaultMaxTurns { get; set; } = 100;
|
||||
public int DefaultMaxTurns { get; set; } = 40;
|
||||
public string DefaultPermissionMode { get; set; } = "auto";
|
||||
|
||||
// Hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run
|
||||
// starts. Guards against runaway sessions regardless of what a task/list override requests.
|
||||
public int MaxTurnsCeiling { get; set; } = 80;
|
||||
|
||||
public int MaxParallelExecutions { get; set; } = 1;
|
||||
|
||||
public string WorktreeStrategy { get; set; } = "sibling";
|
||||
@@ -30,4 +34,20 @@ public sealed class AppSettingsEntity
|
||||
|
||||
// JSON array of session skill names applied by default to new tasks.
|
||||
public string? SessionSkills { get; set; }
|
||||
|
||||
// JSON array of ModelPreset rows (model → effort + max turns). Supplies the global effort and
|
||||
// turn defaults per model; list-/task-level max-turns overrides still win. Null = ship defaults.
|
||||
public string? ModelPresets { get; set; }
|
||||
|
||||
// Percentage of the 5h/7d Claude usage window at which the autonomous queue pauses. 0 = gate off.
|
||||
public int UsageGateFiveHourPct { get; set; } = 80;
|
||||
public int UsageGateSevenDayPct { get; set; } = 90;
|
||||
|
||||
// Percentage at which the queue starts throttling parallelism ahead of the hard gate above.
|
||||
// Tracked per bucket, because the 5h and 7d windows fill at very different rates — soft caps
|
||||
// parallelism at 2 slots, hard at 1. 0 = that stage off for that bucket.
|
||||
public int UsageThrottleFiveHourSoftPct { get; set; } = 50;
|
||||
public int UsageThrottleFiveHourHardPct { get; set; } = 65;
|
||||
public int UsageThrottleSevenDaySoftPct { get; set; } = 50;
|
||||
public int UsageThrottleSevenDayHardPct { get; set; } = 65;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The reasoning-effort levels the claude CLI accepts for <c>--effort</c>.
|
||||
/// </summary>
|
||||
public static class EffortRegistry
|
||||
{
|
||||
public static readonly IReadOnlyList<string> Levels = new[] { "low", "medium", "high", "xhigh", "max" };
|
||||
|
||||
public const string DefaultLevel = "high";
|
||||
|
||||
/// <summary>
|
||||
/// Validate an effort level from external input. Null/blank → null (don't pass --effort).
|
||||
/// Returns the canonical lowercase level; throws on an unknown value.
|
||||
/// </summary>
|
||||
public static string? NormalizeLevel(string? effort)
|
||||
{
|
||||
var e = effort?.Trim();
|
||||
if (string.IsNullOrEmpty(e)) return null;
|
||||
foreach (var level in Levels)
|
||||
if (string.Equals(level, e, StringComparison.OrdinalIgnoreCase))
|
||||
return level;
|
||||
throw new ArgumentException($"Unknown effort '{effort}'. Allowed: {string.Join(", ", Levels)}.");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public sealed class ListConfigEntity
|
||||
public string? AgentPath { get; set; }
|
||||
public int? MaxTurns { get; set; }
|
||||
public string? SessionSkills { get; set; }
|
||||
public string? VerifyCommand { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public ListEntity List { get; set; } = null!;
|
||||
|
||||
@@ -8,6 +8,9 @@ public sealed class ListEntity
|
||||
public string? WorkingDir { get; set; }
|
||||
public string DefaultCommitType { get; set; } = CommitTypeRegistry.DefaultType;
|
||||
public int SortOrder { get; set; }
|
||||
// A manual list holds reminders rather than Claude work: new tasks created here start out
|
||||
// manual (see TaskEntity.IsManual).
|
||||
public bool IsManual { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public ListConfigEntity? Config { get; set; }
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ClaudeDo.Data.Models;
|
||||
|
||||
/// <summary>Per-model run defaults: which reasoning effort and turn budget a session gets when
|
||||
/// it runs under <paramref name="Model"/> and nothing more specific overrides it.</summary>
|
||||
public sealed record ModelPreset(string Model, string Effort, int MaxTurns);
|
||||
|
||||
/// <summary>
|
||||
/// Serialization + lookup for <see cref="AppSettingsEntity.ModelPresets"/> (a JSON array on the
|
||||
/// singleton settings row). One row per <see cref="ModelRegistry.Aliases"/> entry; the row supplies
|
||||
/// the *global* effort and max-turns defaults, which list- and task-level overrides still beat.
|
||||
/// </summary>
|
||||
public static class ModelPresets
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>Shipping defaults — one row per known model alias.</summary>
|
||||
public static IReadOnlyList<ModelPreset> Defaults { get; } = new[]
|
||||
{
|
||||
new ModelPreset("haiku", "medium", 20),
|
||||
new ModelPreset("sonnet", "high", 30),
|
||||
new ModelPreset("opus", "high", 40),
|
||||
new ModelPreset("fable", "high", 25),
|
||||
};
|
||||
|
||||
public static string SerializeDefaults() => Serialize(Defaults);
|
||||
|
||||
public static string Serialize(IEnumerable<ModelPreset> presets)
|
||||
=> JsonSerializer.Serialize(Normalize(presets), Json);
|
||||
|
||||
/// <summary>Parses the stored JSON, falling back to <see cref="Defaults"/> for anything absent
|
||||
/// or malformed — a bad settings row must never stop a run from starting.</summary>
|
||||
public static IReadOnlyList<ModelPreset> Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Defaults;
|
||||
try
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<List<ModelPreset>>(json, Json);
|
||||
return parsed is { Count: > 0 } ? Normalize(parsed) : Defaults;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Defaults;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The preset for <paramref name="model"/>, or the default row for it. Never null so
|
||||
/// callers don't have to special-case an unconfigured or unknown model. <paramref name="model"/>
|
||||
/// is resolved through <see cref="ModelRegistry.TryNormalizeAlias"/> first, so a full CLI model
|
||||
/// id (e.g. "claude-sonnet-4-6") still hits its alias's preset row instead of falling through.
|
||||
/// If <paramref name="model"/> normalizes to nothing recognized, <paramref name="fallbackMaxTurns"/>
|
||||
/// (typically <c>AppSettings.DefaultMaxTurns</c>) is used instead of a hardcoded number, so a
|
||||
/// truly unknown model is no harsher than the configured global default.</summary>
|
||||
public static ModelPreset For(IReadOnlyList<ModelPreset> presets, string? model, int fallbackMaxTurns = 30)
|
||||
{
|
||||
var alias = ModelRegistry.TryNormalizeAlias(model) ?? (model ?? ModelRegistry.DefaultAlias).Trim();
|
||||
foreach (var p in presets)
|
||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||
return p;
|
||||
foreach (var p in Defaults)
|
||||
if (string.Equals(p.Model, alias, StringComparison.OrdinalIgnoreCase))
|
||||
return p;
|
||||
return new ModelPreset(alias, EffortRegistry.DefaultLevel, fallbackMaxTurns);
|
||||
}
|
||||
|
||||
public static ModelPreset For(string? json, string? model, int fallbackMaxTurns = 30)
|
||||
=> For(Parse(json), model, fallbackMaxTurns);
|
||||
|
||||
// One row per known alias, canonical order, clamped values — so an edited or partial list
|
||||
// still round-trips into something every consumer can rely on.
|
||||
private static IReadOnlyList<ModelPreset> Normalize(IEnumerable<ModelPreset> presets)
|
||||
{
|
||||
var byModel = new Dictionary<string, ModelPreset>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in presets)
|
||||
{
|
||||
if (p is null || string.IsNullOrWhiteSpace(p.Model)) continue;
|
||||
byModel[p.Model.Trim()] = p;
|
||||
}
|
||||
|
||||
var result = new List<ModelPreset>();
|
||||
foreach (var fallback in Defaults)
|
||||
{
|
||||
var source = byModel.TryGetValue(fallback.Model, out var found) ? found : fallback;
|
||||
var effort = TryNormalizeEffort(source.Effort) ?? fallback.Effort;
|
||||
var turns = source.MaxTurns is >= 1 and <= 200 ? source.MaxTurns : fallback.MaxTurns;
|
||||
result.Add(new ModelPreset(fallback.Model, effort, turns));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? TryNormalizeEffort(string? effort)
|
||||
{
|
||||
try { return EffortRegistry.NormalizeLevel(effort); }
|
||||
catch (ArgumentException) { return null; }
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ namespace ClaudeDo.Data.Models;
|
||||
|
||||
public static class ModelRegistry
|
||||
{
|
||||
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku" };
|
||||
public static readonly IReadOnlyList<string> Aliases = new[] { "sonnet", "opus", "haiku", "fable" };
|
||||
|
||||
/// <summary>Model aliases ordered cheapest → most capable. Single source for prompt cost guidance.</summary>
|
||||
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus" };
|
||||
public static readonly IReadOnlyList<string> ByCostAscending = new[] { "haiku", "sonnet", "opus", "fable" };
|
||||
|
||||
public const string DefaultAlias = "sonnet";
|
||||
public const string PlanningAlias = "opus";
|
||||
@@ -26,4 +26,22 @@ public static class ModelRegistry
|
||||
return alias;
|
||||
throw new ArgumentException($"Unknown model '{model}'. Allowed: {string.Join(", ", Aliases)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort alias lookup for run-path model resolution, where a full CLI model id
|
||||
/// (e.g. "claude-sonnet-4-6") is just as likely as a bare alias and a run must never be
|
||||
/// blocked by an unrecognized one. Null/blank/unrecognized → null; never throws.
|
||||
/// </summary>
|
||||
public static string? TryNormalizeAlias(string? model)
|
||||
{
|
||||
var m = model?.Trim();
|
||||
if (string.IsNullOrEmpty(m)) return null;
|
||||
foreach (var alias in Aliases)
|
||||
if (string.Equals(alias, m, StringComparison.OrdinalIgnoreCase))
|
||||
return alias;
|
||||
foreach (var alias in Aliases)
|
||||
if (m.Contains(alias, StringComparison.OrdinalIgnoreCase))
|
||||
return alias;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,29 @@ public sealed class TaskEntity
|
||||
public int? MaxTurns { get; set; }
|
||||
public bool IsStarred { get; set; }
|
||||
public bool IsMyDay { get; set; }
|
||||
// Manual = a reminder only the user can do. Automation skips it (queue picker, daily prep,
|
||||
// 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; }
|
||||
public string? Notes { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public string? SessionSkills { get; set; }
|
||||
|
||||
// Review range for a worktree-less task hosting an interactive "list handler" run
|
||||
// (Mission Control's "Let Claude handle it"): the handler commits its own changes
|
||||
// straight to the list's working dir, so there is no per-task worktree to diff. These
|
||||
// capture the repo's HEAD at session start / submit-for-review instead, so the normal
|
||||
// diff/get_task_diff paths can show `HandlerBaseCommit..HandlerHeadCommit` over the
|
||||
// list's working dir exactly like a merged task's commit-range diff.
|
||||
public string? HandlerBaseCommit { get; set; }
|
||||
public string? HandlerHeadCommit { get; set; }
|
||||
|
||||
// The claude session id an embedded ConPTY interactive task session is (or was last)
|
||||
// running under -- generated up front and persisted before launch so a closed/aborted
|
||||
// session can be resumed even if the process never got past startup. Cleared implicitly
|
||||
// whenever the task's worktree is recreated (a fresh worktree has nothing to resume into).
|
||||
public string? InteractiveSessionId { get; set; }
|
||||
|
||||
public string? ParentTaskId { get; set; }
|
||||
public string? PlanningSessionId { get; set; }
|
||||
public string? PlanningSessionToken { get; set; }
|
||||
|
||||
@@ -15,9 +15,12 @@ public sealed class TaskRunEntity
|
||||
public int? TurnCount { get; set; }
|
||||
public int? TokensIn { get; set; }
|
||||
public int? TokensOut { get; set; }
|
||||
public int? CacheReadTokens { get; set; }
|
||||
public int? CacheWriteTokens { get; set; }
|
||||
public string? LogPath { get; set; }
|
||||
public DateTime? StartedAt { get; set; }
|
||||
public DateTime? FinishedAt { get; set; }
|
||||
public string? Model { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public TaskEntity Task { get; set; } = null!;
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class WorktreeEntity
|
||||
public required string BaseCommit { get; set; }
|
||||
public string? HeadCommit { get; set; }
|
||||
public string? DiffStat { get; set; }
|
||||
public string? MergeCommit { get; set; }
|
||||
public WorktreeState State { get; set; } = WorktreeState.Active;
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
|
||||
|
||||
@@ -11,16 +11,16 @@ public static class Paths
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Path must not be empty.", nameof(path));
|
||||
|
||||
var expanded = Environment.ExpandEnvironmentVariables(path);
|
||||
var expanded = System.Environment.ExpandEnvironmentVariables(path);
|
||||
|
||||
if (expanded.StartsWith("~", StringComparison.Ordinal))
|
||||
{
|
||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
|
||||
expanded = home + expanded[1..];
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(expanded))
|
||||
expanded = Path.GetFullPath(expanded, baseDir ?? Environment.CurrentDirectory);
|
||||
expanded = Path.GetFullPath(expanded, baseDir ?? System.Environment.CurrentDirectory);
|
||||
|
||||
return Path.GetFullPath(expanded);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,187 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelper, MergeHelperInitial }
|
||||
public enum PromptKind { System, Planning, PlanningInitial, Retry, DailyPrep, WeeklyReport, ImprovementChild, Refine, MergeHelperTriage, MergeHelperExecute, MergeHelperInitial, MergeHelperHandoff }
|
||||
|
||||
/// <summary>
|
||||
/// How a prompt kind's on-disk override (if any) relates to the bundled default.
|
||||
/// </summary>
|
||||
public enum PromptFileState
|
||||
{
|
||||
/// <summary>No override file — the bundled default is in effect.</summary>
|
||||
Missing,
|
||||
/// <summary>File exists and is byte-for-byte (normalized) the current default.</summary>
|
||||
MatchesCurrentDefault,
|
||||
/// <summary>File exists, differs from today's default, but was recorded as an unedited copy of a past default — safe to reconcile away.</summary>
|
||||
MatchesKnownPastDefault,
|
||||
/// <summary>File exists and diverges from the default with no matching recorded hash — a real user customization.</summary>
|
||||
Edited
|
||||
}
|
||||
|
||||
public static class PromptFiles
|
||||
{
|
||||
public static string Root => Path.Combine(Paths.AppDataRoot(), "prompts");
|
||||
|
||||
public static string PathFor(PromptKind kind) => kind switch
|
||||
public static string PathFor(PromptKind kind, string? root = null) =>
|
||||
Path.Combine(root ?? Root, FileNameFor(kind));
|
||||
|
||||
private static string FileNameFor(PromptKind kind) => kind switch
|
||||
{
|
||||
PromptKind.System => Path.Combine(Root, "system.md"),
|
||||
PromptKind.Planning => Path.Combine(Root, "planning-system.md"),
|
||||
PromptKind.PlanningInitial => Path.Combine(Root, "planning-initial.md"),
|
||||
PromptKind.Retry => Path.Combine(Root, "retry.md"),
|
||||
PromptKind.DailyPrep => Path.Combine(Root, "daily-prep.md"),
|
||||
PromptKind.WeeklyReport => Path.Combine(Root, "weekly-report.md"),
|
||||
PromptKind.ImprovementChild => Path.Combine(Root, "improvement-child.md"),
|
||||
PromptKind.Refine => Path.Combine(Root, "refine.md"),
|
||||
PromptKind.MergeHelper => Path.Combine(Root, "merge-helper-system.md"),
|
||||
PromptKind.MergeHelperInitial => Path.Combine(Root, "merge-helper-initial.md"),
|
||||
PromptKind.System => "system.md",
|
||||
PromptKind.Planning => "planning-system.md",
|
||||
PromptKind.PlanningInitial => "planning-initial.md",
|
||||
PromptKind.Retry => "retry.md",
|
||||
PromptKind.DailyPrep => "daily-prep.md",
|
||||
PromptKind.WeeklyReport => "weekly-report.md",
|
||||
PromptKind.ImprovementChild => "improvement-child.md",
|
||||
PromptKind.Refine => "refine.md",
|
||||
PromptKind.MergeHelperTriage => "merge-helper-triage.md",
|
||||
PromptKind.MergeHelperExecute => "merge-helper-execute.md",
|
||||
PromptKind.MergeHelperInitial => "merge-helper-initial.md",
|
||||
PromptKind.MergeHelperHandoff => "merge-helper-handoff.md",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
|
||||
public static void EnsureExists(PromptKind kind)
|
||||
/// <summary>Classify an override file against the bundled default and the recorded default-hash log.</summary>
|
||||
public static PromptFileState Classify(PromptKind kind, string? root = null)
|
||||
{
|
||||
Directory.CreateDirectory(Root);
|
||||
var path = PathFor(kind);
|
||||
if (File.Exists(path)) return;
|
||||
File.WriteAllText(path, DefaultFor(kind));
|
||||
var effectiveRoot = root ?? Root;
|
||||
var path = PathFor(kind, effectiveRoot);
|
||||
if (!File.Exists(path)) return PromptFileState.Missing;
|
||||
|
||||
var normalized = Normalize(File.ReadAllText(path));
|
||||
if (normalized == Normalize(DefaultFor(kind))) return PromptFileState.MatchesCurrentDefault;
|
||||
|
||||
var hashes = LoadDefaultHashes(effectiveRoot);
|
||||
if (hashes.TryGetValue(kind.ToString(), out var recorded) && recorded == HashOf(normalized))
|
||||
return PromptFileState.MatchesKnownPastDefault;
|
||||
|
||||
return PromptFileState.Edited;
|
||||
}
|
||||
|
||||
public static string? ReadOrNull(PromptKind kind)
|
||||
/// <summary>Write an explicit override. If the content matches today's default, its hash is recorded so a
|
||||
/// later default change can reconcile it away automatically instead of freezing it forever.</summary>
|
||||
public static void Save(PromptKind kind, string content, string? root = null)
|
||||
{
|
||||
var path = PathFor(kind);
|
||||
var effectiveRoot = root ?? Root;
|
||||
Directory.CreateDirectory(effectiveRoot);
|
||||
File.WriteAllText(PathFor(kind, effectiveRoot), content);
|
||||
|
||||
var normalized = Normalize(content);
|
||||
var hashes = LoadDefaultHashes(effectiveRoot);
|
||||
if (normalized == Normalize(DefaultFor(kind)))
|
||||
hashes[kind.ToString()] = HashOf(normalized);
|
||||
else
|
||||
hashes.Remove(kind.ToString());
|
||||
SaveDefaultHashes(effectiveRoot, hashes);
|
||||
}
|
||||
|
||||
/// <summary>Delete the override file (if any) so the bundled default takes effect again.</summary>
|
||||
public static void ResetToDefault(PromptKind kind, string? root = null)
|
||||
{
|
||||
var effectiveRoot = root ?? Root;
|
||||
var path = PathFor(kind, effectiveRoot);
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
|
||||
var hashes = LoadDefaultHashes(effectiveRoot);
|
||||
if (hashes.Remove(kind.ToString())) SaveDefaultHashes(effectiveRoot, hashes);
|
||||
}
|
||||
|
||||
/// <summary>Startup sweep: any override file that only ever matched a past (now superseded) default,
|
||||
/// and was never actually edited, is dropped so the current default reaches real runs again.</summary>
|
||||
public static void ReconcileStaleDefaults(string? root = null)
|
||||
{
|
||||
var effectiveRoot = root ?? Root;
|
||||
foreach (var kind in Enum.GetValues<PromptKind>())
|
||||
if (Classify(kind, effectiveRoot) == PromptFileState.MatchesKnownPastDefault)
|
||||
ResetToDefault(kind, effectiveRoot);
|
||||
}
|
||||
|
||||
/// <summary>Startup sweep: moves any *.md file under the prompts root that doesn't match a known
|
||||
/// <see cref="PromptKind"/> path (leftovers from a retired naming scheme) into a "_orphans" subfolder.
|
||||
/// Never deletes them outright. Returns the destination paths of files it moved.</summary>
|
||||
public static IReadOnlyList<string> QuarantineOrphans(string? root = null)
|
||||
{
|
||||
var effectiveRoot = root ?? Root;
|
||||
if (!Directory.Exists(effectiveRoot)) return Array.Empty<string>();
|
||||
|
||||
var known = Enum.GetValues<PromptKind>()
|
||||
.Select(k => PathFor(k, effectiveRoot))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var moved = new List<string>();
|
||||
foreach (var file in Directory.EnumerateFiles(effectiveRoot, "*.md", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
if (known.Contains(file)) continue;
|
||||
|
||||
var orphansDir = Path.Combine(effectiveRoot, "_orphans");
|
||||
Directory.CreateDirectory(orphansDir);
|
||||
var dest = Path.Combine(orphansDir, Path.GetFileName(file));
|
||||
if (File.Exists(dest))
|
||||
dest = Path.Combine(orphansDir,
|
||||
$"{Path.GetFileNameWithoutExtension(file)}-{HashOf(file)[..8]}{Path.GetExtension(file)}");
|
||||
File.Move(file, dest);
|
||||
moved.Add(dest);
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
/// <summary>Compact, non-LCS diff (common prefix/suffix trimmed, differing middle shown +/-) between the
|
||||
/// bundled default and the on-disk override, for surfacing a customization in the Files settings tab.</summary>
|
||||
public static string DiffAgainstDefault(PromptKind kind, string? root = null)
|
||||
{
|
||||
var effectiveRoot = root ?? Root;
|
||||
var defaultLines = Normalize(DefaultFor(kind)).Split('\n');
|
||||
var fileLines = Normalize(ReadOrNull(kind, effectiveRoot) ?? "").Split('\n');
|
||||
|
||||
var prefix = 0;
|
||||
while (prefix < defaultLines.Length && prefix < fileLines.Length && defaultLines[prefix] == fileLines[prefix])
|
||||
prefix++;
|
||||
var suffix = 0;
|
||||
while (suffix < defaultLines.Length - prefix && suffix < fileLines.Length - prefix &&
|
||||
defaultLines[^(suffix + 1)] == fileLines[^(suffix + 1)])
|
||||
suffix++;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (var i = prefix; i < defaultLines.Length - suffix; i++) sb.AppendLine("- " + defaultLines[i]);
|
||||
for (var i = prefix; i < fileLines.Length - suffix; i++) sb.AppendLine("+ " + fileLines[i]);
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
internal static string Normalize(string s) => s.Replace("\r\n", "\n").Trim();
|
||||
|
||||
internal static string HashOf(string content) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content)));
|
||||
|
||||
private static string DefaultsHashPath(string root) => Path.Combine(root, ".defaults.json");
|
||||
|
||||
private static Dictionary<string, string> LoadDefaultHashes(string root)
|
||||
{
|
||||
var path = DefaultsHashPath(root);
|
||||
if (!File.Exists(path)) return new();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path)) ?? new();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveDefaultHashes(string root, Dictionary<string, string> hashes)
|
||||
{
|
||||
Directory.CreateDirectory(root);
|
||||
File.WriteAllText(DefaultsHashPath(root), JsonSerializer.Serialize(hashes));
|
||||
}
|
||||
|
||||
public static string? ReadOrNull(PromptKind kind, string? root = null)
|
||||
{
|
||||
var path = PathFor(kind, root ?? Root);
|
||||
if (!File.Exists(path)) return null;
|
||||
var content = File.ReadAllText(path).Trim();
|
||||
return string.IsNullOrEmpty(content) ? null : content;
|
||||
@@ -46,14 +194,16 @@ public static class PromptFiles
|
||||
public static string Render(PromptKind kind, IReadOnlyDictionary<string, string> values)
|
||||
=> RenderTemplate(ReadOrDefault(kind), values);
|
||||
|
||||
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.</summary>
|
||||
private static readonly Regex TokenPattern = new(@"\{(\w+)\}", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Replace only the given {name} tokens; any other braces pass through untouched.
|
||||
/// Single pass over the template, so a token appearing inside a substituted VALUE is never
|
||||
/// re-substituted. That matters because the values are user-authored task titles and
|
||||
/// descriptions: a sharpened description mentioning a literal {repo} must survive verbatim,
|
||||
/// and it must not depend on the caller happening to order its dictionary correctly.</summary>
|
||||
public static string RenderTemplate(string template, IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
var sb = new StringBuilder(template);
|
||||
foreach (var (key, val) in values)
|
||||
sb.Replace("{" + key + "}", val);
|
||||
return sb.ToString();
|
||||
}
|
||||
=> TokenPattern.Replace(template, m =>
|
||||
values.TryGetValue(m.Groups[1].Value, out var val) ? val : m.Value);
|
||||
|
||||
public static string DefaultFor(PromptKind kind) => kind switch
|
||||
{
|
||||
@@ -65,8 +215,10 @@ public static class PromptFiles
|
||||
PromptKind.WeeklyReport => WeeklyReportDefault,
|
||||
PromptKind.ImprovementChild => ImprovementChildDefault,
|
||||
PromptKind.Refine => RefineDefault,
|
||||
PromptKind.MergeHelper => MergeHelperDefault,
|
||||
PromptKind.MergeHelperTriage => MergeHelperTriageDefault,
|
||||
PromptKind.MergeHelperExecute => MergeHelperExecuteDefault,
|
||||
PromptKind.MergeHelperInitial => MergeHelperInitialDefault,
|
||||
PromptKind.MergeHelperHandoff => MergeHelperHandoffDefault,
|
||||
_ => ""
|
||||
};
|
||||
|
||||
@@ -91,6 +243,10 @@ public static class PromptFiles
|
||||
trivial/mechanical work, 'sonnet' for normal coding, 'opus' only for genuinely
|
||||
complex work (cheapest to most capable: haiku < sonnet < opus).
|
||||
|
||||
This tool only exists for a standalone top-level task. A child task or a task in a
|
||||
planning chain does not have it — improvements are one layer deep. If you don't have
|
||||
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.
|
||||
@@ -98,10 +254,30 @@ public static class PromptFiles
|
||||
just restate the code.
|
||||
- Validate only at real boundaries (user input, external APIs).
|
||||
|
||||
## Reading efficiently
|
||||
- Locate before reading: use Grep/Glob to find the relevant spot instead of
|
||||
opening files to look around.
|
||||
- Read narrowly: pass `offset`/`limit` for the relevant section. Read a whole file
|
||||
only when you already know you need all of it — for files over ~400 lines that's
|
||||
rarely the case.
|
||||
- Don't re-read: check whether the content is already in context before reading it
|
||||
again.
|
||||
- For orientation questions ("where is X", "how does Y work", "which files touch
|
||||
Z"), dispatch an exploration subagent instead of reading broadly yourself — the
|
||||
file dump stays in its context, only the summary comes back. Skip the subagent
|
||||
for a single targeted read or edit; the overhead isn't worth it there.
|
||||
|
||||
## Finishing
|
||||
- Before claiming done, verify: run the build and relevant tests, confirm they
|
||||
pass, and report what you ran. If you couldn't verify something, say so plainly.
|
||||
- Make focused commits using the repository's existing commit-message convention.
|
||||
You are on this task's own branch in its own worktree — a commit here is the
|
||||
deliverable, not an unrequested auto-commit; a rule against auto-committing
|
||||
protects `main` and shared checkouts, neither of which is this. Still never
|
||||
push, never commit on `main`, and never `git add -A` or a bare `git commit`
|
||||
in a checkout other sessions share.
|
||||
- Report the real outcome, including the commit SHA if you made one. Don't say
|
||||
no commit was made when there was.
|
||||
|
||||
## Safety
|
||||
- Never force-push, hard-reset, or delete branches/files beyond the task's scope
|
||||
@@ -140,7 +316,10 @@ public static class PromptFiles
|
||||
- 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 using the repository's commit-message convention.
|
||||
- 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.
|
||||
""";
|
||||
|
||||
private const string PlanningSystemDefault = """
|
||||
@@ -154,8 +333,13 @@ public static class PromptFiles
|
||||
until the user has approved the design.
|
||||
|
||||
You can ONLY shape this task's plan — you cannot edit files or touch other tasks.
|
||||
The tools available to you are: CreateChildTask, ListChildTasks, UpdateChildTask,
|
||||
DeleteChildTask, UpdatePlanningTask, and Finalize. Use nothing else.
|
||||
To shape it you have the ClaudeDo planning tools: CreateChildTask, ListChildTasks,
|
||||
UpdateChildTask, DeleteChildTask, UpdatePlanningTask and Finalize. You also have
|
||||
Skill (needed for the brainstorming skill above), Read, Grep, Glob, WebFetch and
|
||||
WebSearch. You do NOT have Write, Edit or Bash.
|
||||
|
||||
Use Read/Grep/Glob to ground the plan in the actual repo — a subtask that names the
|
||||
real files and symbols involved runs far better than one written from guesswork.
|
||||
|
||||
Once the design is approved, create the child tasks with CreateChildTask, then
|
||||
call Finalize. Keep each subtask concrete and self-contained with a clear
|
||||
@@ -168,6 +352,12 @@ public static class PromptFiles
|
||||
- sonnet — normal coding work; the sensible default when unsure.
|
||||
- opus — only for genuinely complex, cross-cutting, or hard-to-debug work.
|
||||
Do not default everything to opus — most subtasks are haiku or sonnet.
|
||||
|
||||
Only for a subtask you can tell will need noticeably more turns than the
|
||||
default budget (a large or sprawling piece of work), also pass CreateChildTask's
|
||||
`maxTurns` argument with a generous turn count, so the run doesn't die mid-work
|
||||
at the turn limit. Leave `maxTurns` null for everything else — it inherits the
|
||||
list/global default.
|
||||
""";
|
||||
|
||||
private const string PlanningInitialDefault = """
|
||||
@@ -224,27 +414,92 @@ public static class PromptFiles
|
||||
not already present in the current subtasks above.
|
||||
|
||||
Use ONLY these tools: mcp__claudedo__get_task, mcp__claudedo__update_task,
|
||||
mcp__claudedo__add_subtask, and read-only Read/Grep/Glob. When you have updated the
|
||||
task, stop.
|
||||
mcp__claudedo__add_subtask, and — only when a repository is available — read-only
|
||||
Read/Grep/Glob. When you have updated the task, stop.
|
||||
""";
|
||||
|
||||
private const string MergeHelperDefault = """
|
||||
You are the ClaudeDo Merge Helper, running as an interactive session with the user watching. Ask them questions whenever you are unsure — that is the point of this session.
|
||||
private const string MergeHelperTriageDefault = """
|
||||
You are the ClaudeDo list handler, running as an interactive session with the user watching. Work autonomously and decide things yourself by default. Ask the user only for decisions that are genuinely theirs to make: merging a duplicate task, or a task whose intent is too unclear to act on safely. Everything else, decide and keep moving.
|
||||
|
||||
Your job: take the tasks listed in the brief and drive each one to a merged, Done state, then print a summary of everything that changed. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it lists each task's id, title, status, list and repo.
|
||||
Your job: take the tasks listed in the brief and get the set ready to run — reading them all first, removing duplicates, then sharpening what stays. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
||||
|
||||
Handle the tasks in the order listed, one at a time. For each task, act on its current status:
|
||||
- Idle or Queued: run it with run_task_now, then poll get_task until it leaves Running (it lands in WaitingForReview on success, or Failed).
|
||||
- Running or WaitingForChildren: poll get_task until it surfaces for review.
|
||||
- Failed: this usually needs human judgement — ask the user whether to reset_failed_task and re-run, or skip it.
|
||||
- WaitingForReview: review, then merge it (below).
|
||||
A second session takes over after you to run, review and merge these tasks. Your deliverable is a clean, sharpened set of task descriptions — you never run or merge anything yourself, and you touch no git state.
|
||||
|
||||
Reviewing and merging a WaitingForReview task:
|
||||
1. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title/description.
|
||||
Work the three phases in order. Do not start a phase before the previous one is finished.
|
||||
|
||||
## Phase 0 — Read everything
|
||||
The brief is the primary source: it already lists every task's title, id, status and full description. Read it in full before acting. Only call batch_get_tasks if you need something the brief does not carry for a specific task, e.g. parent/child links. Do not act on any single task before you have read them all — Phase 1 needs the whole set in view.
|
||||
|
||||
## Phase 1 — Dedupe
|
||||
Compare the tasks pairwise for overlap: same goal stated twice, one task fully contained in another, two tasks that would edit the same thing for the same reason.
|
||||
|
||||
If no pair looks like a duplicate, say so in one sentence and move straight to Phase 2 — do not ask the user to confirm the absence of duplicates.
|
||||
|
||||
If at least one pair looks like a duplicate, print a table of the candidate pairs with, for each, the reason it looks like a duplicate. Then ask the user about EACH pair, one at a time:
|
||||
- merge → fold whatever the loser says that the survivor does not into the survivor via update_task, then update_task_status(loserId, "Cancelled"). Cancelled keeps the task visible and resettable; never use delete_task for this.
|
||||
- keep both → note why and move on.
|
||||
|
||||
Cancel nothing without an explicit answer.
|
||||
|
||||
## Phase 2 — Enhance for execution
|
||||
Each surviving task is about to be run by an autonomous agent with no further input. Sharpen it so that run can succeed. For each task, rewrite title and description to carry:
|
||||
- concrete acceptance criteria — what must be true when it is done,
|
||||
- the files and areas actually involved, found with Read/Grep/Glob in the repo. Do not guess paths; look them up.
|
||||
- what is explicitly out of scope.
|
||||
|
||||
Write it back with update_task (title, description and commitType are the settable fields).
|
||||
|
||||
Rules: do not change what the user asked for, and do not invent requirements. You are making the existing intent precise, not adding to it. If a task is too vague to sharpen without guessing, ASK instead of guessing. Report a short before/after per task.
|
||||
|
||||
If a task visibly bundles several independent features, or has a blocker that is not resolved by anything in its own description, do not force it into one description. Propose splitting it to the user; if they agree, create the pieces with add_task/add_subtask and only move the pieces the user confirmed into "surviving tasks" for the phases below. Split only what the task already asks for — the "do not invent requirements" rule still applies.
|
||||
|
||||
## Handoff
|
||||
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 with this session's task id and the surviving task ids, in the order you intend them to run. That opens a fresh session to carry out the run/review/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.
|
||||
""";
|
||||
|
||||
private const string MergeHelperExecuteDefault = """
|
||||
You are the ClaudeDo list handler, running as an interactive session with the user watching. Work autonomously and decide things yourself by default. Ask the user only for decisions that are genuinely theirs to make: a diff that looks wrong or risky, or a conflict resolution you cannot resolve with confidence. Everything else, decide and keep moving.
|
||||
|
||||
Your job: take the tasks listed in the brief and drive them to merged, Done work — running them, then reviewing and merging each result. You act through the mcp__claudedo__* tools. Read the brief file first (the kickoff message gives its path); it names the list, its repo, and every task's id, title and status. All tasks belong to that one list and one repo.
|
||||
|
||||
A prior session already read, deduplicated and sharpened these tasks; their descriptions are ready to run as written. Start at Phase 3.
|
||||
|
||||
Work the three phases in order. Do not start a phase before the previous one is finished.
|
||||
|
||||
## Phase 3 — Run
|
||||
Do NOT use run_task_now for a batch — there is a single override slot and the second call fails with "Override slot busy. Try again later.".
|
||||
|
||||
Read get_app_settings and tell the user how many parallel execution slots are configured (maxParallelExecutions). If it is 1, say plainly that the tasks will execute one after another and that the value is changeable in ClaudeDo's settings.
|
||||
|
||||
For each surviving task, call get_effective_run_config(taskId) and report the max-turns it will ACTUALLY run with. Do not derive that from task/list/preset values yourself — the resolved value is clamped to a global ceiling, so a raw task or list setting can be higher than what runs. The tool reports the effective value, its source, the raw requested value and whether it was clamped. If a task looks substantial (several files, or one you just split off above) but its effective turns look low, say so and ask before queuing it; set_task_config raises it per task, though the ceiling still applies.
|
||||
|
||||
Then, for each surviving task:
|
||||
- Idle or Failed → update_task_status(id, "Queued"). For a Failed task ask first whether to reset_failed_task and re-queue it, or skip it.
|
||||
- Queued → leave it; it is already waiting for a slot.
|
||||
- Running or WaitingForChildren → leave it; the wait below covers it.
|
||||
- WaitingForReview → leave it; it goes straight to Phase 4.
|
||||
|
||||
Then wait with wait_for_task_change instead of sleeping and polling get_task yourself. Pass the ids of every task not yet in WaitingForReview or a terminal status — Queued, Running and WaitingForChildren alike — and set treatWaitingForChildrenAsBusy=true. Without that flag a task with children returns the moment it goes Running → WaitingForChildren, while its children are still working, and you would walk into Phase 4 with unfinished work. Use timeoutSeconds 900: the server clamps there anyway, and ClaudeDo's launchers already raise MCP_TOOL_TIMEOUT above it, so one long wait costs one turn where six short ones cost six.
|
||||
|
||||
It returns as soon as a task reaches WaitingForReview or fails, or reports timedOut if none did. Report progress as tasks land, then call it again with whatever ids are still outstanding until none remain.
|
||||
|
||||
## Phase 4 — Review and merge
|
||||
Before merging anything, call preview_merge_set with every surviving task's id (the same targetBranch you are about to merge into). It tells you, per task, whether a clean merge-tree preview is even possible (status/conflictFiles/changedFileCount/behind) and which files more than one of the tasks changed (overlaps). Read the overlaps: a file two tasks both touch is where a same-branch collision could happen. This is a HINT, not proof — it only catches same-file overlap, not a cross-file break (e.g. one task deletes a symbol another task's file still references), and a clean preview never guarantees the result compiles or passes tests. Use it to decide merge order and to know which pairs to look at extra carefully in step 1 below; it does not replace reading the diffs.
|
||||
|
||||
One task at a time. Default to the order the brief lists them. If the overlap check above (or the file lists you gathered in Phase 2) 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. Inspect the change with get_task_diff (stat first, then the full diff if it is non-trivial) and sanity-check it against the task's title and description. If preview_merge_set flagged this task in an overlap, also skim the diff of the other task(s) sharing that file.
|
||||
2. If the change looks wrong, incomplete, or risky, STOP and ask the user before merging — offer reject_rerun (with feedback) or skip.
|
||||
3. Otherwise merge it with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||
3. Otherwise merge with review_task(taskId, decision="approve", leaveConflictsInTree=true).
|
||||
- Clean merge → the task is Done; move on.
|
||||
- Conflict (markers left in the working tree, repoPath returned) → resolve it (below).
|
||||
- Conflict (markers left in the working tree, repoPath returned) → resolve 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.
|
||||
|
||||
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.
|
||||
|
||||
Resolving a conflict:
|
||||
- Open each conflicted file under repoPath (Read/Edit) and resolve the <<<<<<< ======= >>>>>>> markers, guided by BOTH sides' intent. Then call continue_merge(taskId). If markers remain it tells you — fix and call again. Use abort_merge(taskId) to cancel a merge you cannot safely resolve.
|
||||
@@ -252,28 +507,44 @@ public static class PromptFiles
|
||||
- If a resolution is non-obvious, ambiguous, or might drop someone's work, ASK THE USER before continuing.
|
||||
- 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.
|
||||
|
||||
Rules:
|
||||
Rules for the whole session:
|
||||
- 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.
|
||||
- Ask the user for anything ambiguous, risky, or destructive.
|
||||
|
||||
When every task is handled, print a SUMMARY:
|
||||
- one line per task: title — final status — merge commit (if any) — conflicts resolved (if any)
|
||||
- anything you skipped or left for the user, and why
|
||||
- suggested follow-ups, if any.
|
||||
## Phase 5 — Summary
|
||||
Print one line per task from the brief:
|
||||
title — final status — merge commit (if any) — conflicts resolved (if any).
|
||||
|
||||
Then list anything you skipped or left for the user and why, and any follow-ups worth turning into new tasks.
|
||||
""";
|
||||
|
||||
private const string MergeHelperInitialDefault = """
|
||||
# Merge Helper brief
|
||||
# List handler brief
|
||||
|
||||
Scope: {scope}
|
||||
Repo: {repo}
|
||||
|
||||
Handle the following tasks, in order. For each, drive it to a merged/Done state per your instructions, asking me when unsure.
|
||||
Handle the following tasks. Work Phases 0–5 as your instructions describe, asking me whenever you are unsure.
|
||||
|
||||
{tasks}
|
||||
|
||||
When every task is handled, print the summary.
|
||||
""";
|
||||
|
||||
private const string MergeHelperHandoffDefault = """
|
||||
# List handler handoff
|
||||
|
||||
Scope: {scope}
|
||||
Repo: {repo}
|
||||
|
||||
A prior session already read, deduped and enhanced this list's tasks. Pick up at phase 3
|
||||
for the tasks below — their descriptions are already sharpened.
|
||||
|
||||
{tasks}
|
||||
|
||||
Start with phase 3 (run), continuing through review/merge and the summary as your
|
||||
instructions describe.
|
||||
""";
|
||||
|
||||
private const string WeeklyReportDefault = """
|
||||
You are generating a concise weekly standup report for a software developer,
|
||||
covering {start} to {end}.
|
||||
|
||||
@@ -14,25 +14,45 @@ public sealed class AppSettingsRepository
|
||||
{
|
||||
var row = await _context.AppSettings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
|
||||
if (row is not null) return row;
|
||||
if (row is null)
|
||||
{
|
||||
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId, ModelPresets = ModelPresets.SerializeDefaults() };
|
||||
_context.AppSettings.Add(row);
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync(ct);
|
||||
_context.Entry(row).State = EntityState.Detached;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// Concurrent process already inserted the singleton — discard our attempt and re-read.
|
||||
_context.Entry(row).State = EntityState.Detached;
|
||||
row = await _context.AppSettings.AsNoTracking()
|
||||
.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// First read after upgrading from a null model_presets column: persist the shipping
|
||||
// defaults so the Settings UI shows real, editable rows instead of a code-only fallback.
|
||||
if (row.ModelPresets is null)
|
||||
row.ModelPresets = await BackfillModelPresetsAsync(ct);
|
||||
|
||||
row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId };
|
||||
_context.AppSettings.Add(row);
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync(ct);
|
||||
_context.Entry(row).State = EntityState.Detached;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// Concurrent process already inserted the singleton — discard our attempt and re-read.
|
||||
_context.Entry(row).State = EntityState.Detached;
|
||||
row = await _context.AppSettings.AsNoTracking()
|
||||
.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId, ct);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private async Task<string> BackfillModelPresetsAsync(CancellationToken ct)
|
||||
{
|
||||
var defaults = ModelPresets.SerializeDefaults();
|
||||
var tracked = await GetOrCreateTrackedRowAsync(ct);
|
||||
if (tracked.ModelPresets is null)
|
||||
{
|
||||
tracked.ModelPresets = defaults;
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
private async Task<AppSettingsEntity> GetOrCreateTrackedRowAsync(CancellationToken ct)
|
||||
{
|
||||
var row = await _context.AppSettings
|
||||
@@ -52,6 +72,7 @@ public sealed class AppSettingsRepository
|
||||
row.DefaultClaudeInstructions = updated.DefaultClaudeInstructions ?? string.Empty;
|
||||
row.DefaultModel = string.IsNullOrWhiteSpace(updated.DefaultModel) ? "sonnet" : updated.DefaultModel;
|
||||
row.DefaultMaxTurns = updated.DefaultMaxTurns;
|
||||
row.MaxTurnsCeiling = updated.MaxTurnsCeiling < 1 ? 1 : updated.MaxTurnsCeiling;
|
||||
row.DefaultPermissionMode = string.IsNullOrWhiteSpace(updated.DefaultPermissionMode)
|
||||
? "auto" : updated.DefaultPermissionMode;
|
||||
row.MaxParallelExecutions = updated.MaxParallelExecutions < 1 ? 1 : updated.MaxParallelExecutions;
|
||||
@@ -65,6 +86,12 @@ public sealed class AppSettingsRepository
|
||||
row.StandupWeekday = updated.StandupWeekday;
|
||||
row.DailyPrepMaxTasks = updated.DailyPrepMaxTasks < 1 ? 1 : updated.DailyPrepMaxTasks;
|
||||
row.SessionSkills = string.IsNullOrWhiteSpace(updated.SessionSkills) ? null : updated.SessionSkills;
|
||||
row.UsageGateFiveHourPct = Math.Clamp(updated.UsageGateFiveHourPct, 0, 100);
|
||||
row.UsageGateSevenDayPct = Math.Clamp(updated.UsageGateSevenDayPct, 0, 100);
|
||||
row.UsageThrottleFiveHourSoftPct = Math.Clamp(updated.UsageThrottleFiveHourSoftPct, 0, 100);
|
||||
row.UsageThrottleFiveHourHardPct = Math.Clamp(updated.UsageThrottleFiveHourHardPct, 0, 100);
|
||||
row.UsageThrottleSevenDaySoftPct = Math.Clamp(updated.UsageThrottleSevenDaySoftPct, 0, 100);
|
||||
row.UsageThrottleSevenDayHardPct = Math.Clamp(updated.UsageThrottleSevenDayHardPct, 0, 100);
|
||||
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ public sealed class ListRepository
|
||||
existing.AgentPath = config.AgentPath;
|
||||
existing.MaxTurns = config.MaxTurns;
|
||||
existing.SessionSkills = config.SessionSkills;
|
||||
existing.VerifyCommand = config.VerifyCommand;
|
||||
}
|
||||
await _context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,60 @@ public sealed class TaskRepository
|
||||
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
||||
=> GetByListIdAsync(listId, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ids of every descendant of <paramref name="taskId"/> (children, grandchildren, ...),
|
||||
/// walking the ParentTaskId chain breadth-first. Does not include the task itself.
|
||||
/// </summary>
|
||||
public async Task<List<string>> GetDescendantIdsAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<string>();
|
||||
// Guards against a corrupt parent chain (a self-parent or a cycle would loop forever).
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal) { taskId };
|
||||
var frontier = new List<string> { taskId };
|
||||
while (frontier.Count > 0)
|
||||
{
|
||||
var children = await _context.Tasks.AsNoTracking()
|
||||
.Where(t => t.ParentTaskId != null && frontier.Contains(t.ParentTaskId))
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(ct);
|
||||
var fresh = children.Where(seen.Add).ToList();
|
||||
if (fresh.Count == 0) break;
|
||||
result.AddRange(fresh);
|
||||
frontier = fresh;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a task (and every descendant, so a child never ends up in a different list than its
|
||||
/// parent) to <paramref name="targetListId"/>, appending it at the end of the target list's
|
||||
/// order. ListId is init-only, so the move goes through ExecuteUpdate rather than a tracked
|
||||
/// entity mutation.
|
||||
/// </summary>
|
||||
public async Task MoveToListAsync(string taskId, string targetListId, CancellationToken ct = default)
|
||||
{
|
||||
var exists = await _context.Tasks.AsNoTracking().AnyAsync(t => t.Id == taskId, ct);
|
||||
if (!exists)
|
||||
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
|
||||
var descendantIds = await GetDescendantIdsAsync(taskId, ct);
|
||||
var movedIds = new List<string> { taskId };
|
||||
movedIds.AddRange(descendantIds);
|
||||
|
||||
var maxSort = await _context.Tasks
|
||||
.Where(t => t.ListId == targetListId)
|
||||
.Select(t => (int?)t.SortOrder)
|
||||
.MaxAsync(ct);
|
||||
|
||||
await _context.Tasks
|
||||
.Where(t => movedIds.Contains(t.Id))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.ListId, targetListId), ct);
|
||||
|
||||
await _context.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SortOrder, (maxSort ?? -1) + 1), ct);
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Tasks
|
||||
@@ -222,8 +276,12 @@ public sealed class TaskRepository
|
||||
string? commitType,
|
||||
string? createdBy = null,
|
||||
string? model = null,
|
||||
int? maxTurns = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (maxTurns.HasValue && maxTurns.Value <= 0)
|
||||
throw new ArgumentException($"maxTurns must be positive, got {maxTurns.Value}.");
|
||||
|
||||
// AsNoTracking: SetPlanningStartedAsync mutates via ExecuteUpdate which
|
||||
// bypasses the change tracker; a tracked Find would return stale data.
|
||||
var parent = await _context.Tasks.AsNoTracking()
|
||||
@@ -249,6 +307,7 @@ public sealed class TaskRepository
|
||||
SortOrder = (maxSort ?? -1) + 1,
|
||||
CreatedBy = createdBy,
|
||||
Model = ModelRegistry.NormalizeAlias(model),
|
||||
MaxTurns = maxTurns,
|
||||
};
|
||||
_context.Tasks.Add(child);
|
||||
await _context.SaveChangesAsync(ct);
|
||||
@@ -330,6 +389,32 @@ public sealed class TaskRepository
|
||||
.SetProperty(t => t.PlanningSessionId, sessionId), ct);
|
||||
}
|
||||
|
||||
// Stamps the review range's head commit for a worktree-less "list handler" host task
|
||||
// (Mission Control's "Let Claude handle it") when its ConPTY session is submitted for review.
|
||||
public async Task SetHandlerHeadCommitAsync(
|
||||
string taskId,
|
||||
string headCommit,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _context.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.HandlerHeadCommit, headCommit), ct);
|
||||
}
|
||||
|
||||
// Persists the claude session id a fresh embedded ConPTY interactive task session will run
|
||||
// under, written BEFORE launch so a closed/aborted session still leaves a resumable id.
|
||||
public async Task SetInteractiveSessionIdAsync(
|
||||
string taskId,
|
||||
string? sessionId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await _context.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.InteractiveSessionId, sessionId), ct);
|
||||
}
|
||||
|
||||
public async Task<TaskEntity?> FindByPlanningTokenAsync(
|
||||
string token,
|
||||
CancellationToken ct = default)
|
||||
|
||||
@@ -36,6 +36,15 @@ public sealed class WorktreeRepository
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
||||
}
|
||||
|
||||
public async Task SetMergedAsync(string taskId, string mergeCommit, CancellationToken ct = default)
|
||||
{
|
||||
await _context.Worktrees
|
||||
.Where(w => w.TaskId == taskId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(w => w.State, WorktreeState.Merged)
|
||||
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
||||
|
||||
@@ -4,13 +4,18 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Checks.Interfaces;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
using ClaudeDo.Installer.Localization;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Releases;
|
||||
using ClaudeDo.Installer.Pages.DiagnosePage;
|
||||
using ClaudeDo.Installer.Pages.InstallPage;
|
||||
using ClaudeDo.Installer.Pages.PathsPage;
|
||||
using ClaudeDo.Installer.Pages.ServicePage;
|
||||
using ClaudeDo.Installer.Pages.SystemCheckPage;
|
||||
using ClaudeDo.Installer.Pages.UiSettingsPage;
|
||||
using ClaudeDo.Installer.Pages.WelcomePage;
|
||||
using ClaudeDo.Installer.Steps;
|
||||
@@ -27,6 +32,13 @@ public partial class App : Application
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// The app relaunches us via ShellExecute without an explicit working directory, so we
|
||||
// inherit its CWD — which the Start Menu shortcut sets to <InstallDir>\app. A process's
|
||||
// current directory is locked by the OS, so we would block DownloadAndExtractStep's
|
||||
// Directory.Move of app\ against ourselves (fails on every attempt, reboot included).
|
||||
// Step out of the install tree before anything else touches it.
|
||||
try { Environment.CurrentDirectory = Path.GetTempPath(); } catch { /* best effort */ }
|
||||
|
||||
// --- Initialize localizer as early as possible so all windows can use {loc:Tr} ---
|
||||
var localesDir = Path.Combine(AppContext.BaseDirectory, "locales");
|
||||
var localeStore = LocaleStore.Load(localesDir);
|
||||
@@ -114,17 +126,45 @@ public partial class App : Application
|
||||
sc.AddSingleton<IReleaseClient>(sp => new ReleaseClient(sp.GetRequiredService<HttpClient>()));
|
||||
sc.AddSingleton<InstallModeDetector>();
|
||||
|
||||
// Environment checks — stateless, so their infrastructure is shared; ClaudeCliLookup is
|
||||
// rebuilt per EnvironmentCheckService instance so a re-check doesn't reuse a stale result.
|
||||
sc.AddSingleton<IProcessRunner, ProcessRunnerAdapter>();
|
||||
sc.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
||||
sc.AddSingleton<IPortOwnerResolver, NetstatPortOwnerResolver>();
|
||||
sc.AddSingleton<ClaudeHelpLauncher>();
|
||||
sc.AddTransient<Func<EnvironmentCheckService>>(sp => () =>
|
||||
{
|
||||
var processRunner = sp.GetRequiredService<IProcessRunner>();
|
||||
var claudeLookup = new ClaudeCliLookup(processRunner);
|
||||
return new EnvironmentCheckService(new IEnvironmentCheck[]
|
||||
{
|
||||
new GitCheck(processRunner),
|
||||
new GitIdentityCheck(processRunner),
|
||||
new PortCheck(sp.GetRequiredService<IPortOwnerResolver>()),
|
||||
new WriteAccessCheck(),
|
||||
new ClaudeCliCheck(claudeLookup),
|
||||
new ClaudeVersionCheck(claudeLookup),
|
||||
new ClaudeAuthCheck(claudeLookup),
|
||||
new PermissionModeAutoCheck(claudeLookup),
|
||||
});
|
||||
});
|
||||
|
||||
// Pages
|
||||
sc.AddSingleton<IInstallerPage, WelcomePageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage, SystemCheckPageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage, PathsPageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage, ServicePageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage, UiSettingsPageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage, InstallPageViewModel>();
|
||||
sc.AddSingleton<IInstallerPage>(sp => new DiagnosePageViewModel(
|
||||
sp.GetRequiredService<InstallContext>(),
|
||||
sp.GetRequiredService<Func<EnvironmentCheckService>>(),
|
||||
InstallerWorkerConfig.Load));
|
||||
|
||||
// Steps — execution order matters for the FreshInstall pipeline (IEnumerable<IInstallStep>).
|
||||
// Double-registered as both IInstallStep and concrete type so the Update pipeline
|
||||
// can pull them out individually via GetRequiredService<T>().
|
||||
sc.AddSingleton<DownloadAndExtractStep>();
|
||||
sc.AddSingleton(sp => new DownloadAndExtractStep(sp.GetRequiredService<IReleaseClient>()));
|
||||
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<DownloadAndExtractStep>());
|
||||
sc.AddSingleton<IInstallStep, WriteConfigStep>();
|
||||
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
|
||||
|
||||
@@ -21,20 +21,18 @@ Note: this is the one project where `System.Windows` is correct (WPF, not Avalon
|
||||
3. Open `WizardWindow` (FreshInstall / Update) or `SettingsWindow` (Config)
|
||||
|
||||
The installer does **not** self-update. Each release ships a stable-named
|
||||
`ClaudeDo.Installer.exe` asset (permanent URL
|
||||
`…/releases/latest/download/ClaudeDo.Installer.exe`); the installer never checks for or
|
||||
replaces itself on launch. The in-app "Update" button relaunches the on-disk installer to
|
||||
run the app update — the installer binary itself only changes when the user downloads a
|
||||
fresh copy. App-update detection is unaffected: `WriteInstallManifestStep` records
|
||||
`ctx.InstalledVersion` (the release tag from `DownloadAndExtractStep`), which
|
||||
`InstallModeDetector` compares against the latest tag.
|
||||
`ClaudeDo.Installer.exe` asset (permanent URL `…/releases/latest/download/ClaudeDo.Installer.exe`);
|
||||
the binary only changes when the user downloads a fresh copy. The in-app "Update" button relaunches
|
||||
the on-disk installer to run the *app* update. App-update detection is unaffected:
|
||||
`WriteInstallManifestStep` records `ctx.InstalledVersion` (the release tag from
|
||||
`DownloadAndExtractStep`), which `InstallModeDetector` compares against the latest tag.
|
||||
|
||||
## Modes (`Core/InstallerMode.cs`)
|
||||
|
||||
| Mode | Condition | Window |
|
||||
|---|---|---|
|
||||
| `FreshInstall` | No `install.json` | Full wizard (all pages) |
|
||||
| `Update` | `install.json` present + newer release available | Wizard — Welcome + Install pages only |
|
||||
| `FreshInstall` | No `install.json` | Full wizard: Welcome → **SystemCheck** → Paths → Service → UiSettings → Install |
|
||||
| `Update` | `install.json` present + newer release available | Wizard — Welcome + Install pages only (SystemCheck **not** shown) |
|
||||
| `Config` | Current version, or Gitea API unreachable | `SettingsWindow` (settings / repair / uninstall) |
|
||||
|
||||
## Install Pipelines
|
||||
@@ -62,7 +60,8 @@ Installer/
|
||||
ConfigModels, InstallerService, UninstallRunner, PageResolver,
|
||||
AutostartShortcut, ShortcutFactory, ProcessRunner, DarkTitleBar
|
||||
Interfaces/ — IInstallStep + StepResult/StepStatus/StepProgress, IInstallerPage
|
||||
Pages/ — WelcomePage, PathsPage, ServicePage, UiSettingsPage, InstallPage
|
||||
Checks/ — environment preflight checks, see "Environment Checks" below
|
||||
Pages/ — WelcomePage, SystemCheckPage, PathsPage, ServicePage, UiSettingsPage, InstallPage
|
||||
(each: ViewModel + View.xaml)
|
||||
Views/ — WizardWindow(+WizardViewModel), SettingsWindow(+SettingsViewModel)
|
||||
```
|
||||
@@ -76,12 +75,23 @@ claude mcp add --transport http --scope user claudedo http://127.0.0.1:{External
|
||||
```
|
||||
Non-fatal if `claude` CLI is missing or too old (prints the manual command). Server name: `claudedo`.
|
||||
|
||||
**`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`). Also migrates away from legacy mechanisms:
|
||||
**`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`); `AutostartShortcut.Install` skips the rewrite (and reports it) when the shortcut already points at the current worker exe, so update/repair runs don't touch it needlessly. Also migrates away from legacy mechanisms, unconditionally on every run (no cached "already migrated" flag — see the comment in the step):
|
||||
- Deletes legacy Windows service: `sc.exe stop/delete ClaudeDoWorker`
|
||||
- Deletes legacy scheduled task: `schtasks /Delete /TN ClaudeDoWorker`
|
||||
|
||||
No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works.
|
||||
|
||||
**`DownloadAndExtractStep`** — fetches `checksums.txt` first and only touches the install dir after
|
||||
the zip verifies. The zip is cached in `%TEMP%\ClaudeDo-download-cache` (ctor takes an override for
|
||||
tests) and reused on a retry when its SHA-256 still matches, so a failed attempt doesn't cost
|
||||
another full download. Cache is dropped after a successful install, a bad download is deleted
|
||||
immediately, other versions are pruned. `app\`/`worker\` are stashed to `*.bak` before extraction
|
||||
and restored if it fails.
|
||||
|
||||
### Gotcha: the installer must never run from inside the install dir
|
||||
|
||||
`App.OnStartup` sets `Environment.CurrentDirectory` to `%TEMP%`, and the UI passes an explicit `WorkingDirectory` when it relaunches us. A process's current directory is locked by Windows: inheriting the app's CWD (`<InstallDir>\app`, from the Start Menu shortcut's "start in") made the installer block its own `app` → `app.bak` rename, so every update failed with "Could not replace the existing files" — unaffected by retries or a reboot. Keep both guards.
|
||||
|
||||
## `InstallContext` Defaults
|
||||
|
||||
| Property | Default |
|
||||
@@ -115,3 +125,63 @@ No new service or scheduled task is created. Rationale: the worker must run in t
|
||||
| `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\ClaudeDo Worker.lnk` | Worker autostart |
|
||||
|
||||
The Apps & Features uninstall string and "Rerun Installer" both point at `<InstallDir>\uninstaller\ClaudeDo.Installer.exe` with no `/uninstall` flag — Config mode is detected from `install.json`.
|
||||
|
||||
## Environment Checks
|
||||
|
||||
`Checks/` holds one `IEnvironmentCheck` per concern, run in parallel by `EnvironmentCheckService.RunAllAsync`:
|
||||
|
||||
| Check | Severity | What it verifies |
|
||||
|---|---|---|
|
||||
| `GitCheck` | Error | `git` resolvable (via `ExecutableResolver`) and runs |
|
||||
| `WriteAccessCheck` | Error | install dir + `%APPDATA%` (or first existing parent) are writable |
|
||||
| `ClaudeCliCheck` | Error | `claude` resolvable on PATH, including npm `.cmd`/`.bat`/`.ps1` shims |
|
||||
| `ClaudeVersionCheck` | Error | resolved `claude --version` ≥ `ClaudeVersionCheck.MinimumVersion` (currently `2.1.220`) |
|
||||
| `ClaudeAuthCheck` | Error | `claude auth status --json` reports `loggedIn: true` (never sends a prompt) |
|
||||
| `GitIdentityCheck` | Warning | `git config user.name`/`user.email` are set |
|
||||
| `PortCheck` | Warning | `SignalRPort`/`ExternalMcpPort` are free, or already owned by a running `ClaudeDo.Worker` |
|
||||
| `PermissionModeAutoCheck` | Warning | CLI's `--help` still lists `auto` as a `--permission-mode` choice |
|
||||
|
||||
Each check returns a `CheckResult` with `CheckStatus` (`Ok` / `Failed` / `Unknown`). A check that
|
||||
throws is caught by `EnvironmentCheckService` and turned into `Unknown`, never a crash.
|
||||
|
||||
**Gating rule:** `EnvironmentCheckReport.HasBlockingError` is true only when a check with
|
||||
`Severity == Error` has `Status == Failed`. Warnings never block, and `Unknown` never blocks
|
||||
regardless of severity (an indeterminate result — e.g. the CLI not found, so version/auth/auto-mode
|
||||
can't be checked — must not strand the user; the underlying `Error`-severity check for the CLI
|
||||
itself, `ClaudeCliCheck`, is what blocks in that case).
|
||||
|
||||
The check-row rendering and the check-run logic (busy state, summary text, Recheck command) live
|
||||
in one place — `Checks/CheckListViewModel.cs` + `Checks/CheckListView.xaml` — composed by every
|
||||
page that hosts a check list, not duplicated per page.
|
||||
|
||||
`SystemCheckPage` (`Pages/SystemCheckPage/`) hosts the check list in the **FreshInstall** wizard
|
||||
only, registered via `PageResolver` at `Order = 1` (directly after `WelcomePage`); `WizardViewModel`
|
||||
filters it back out in `Update` mode along with Paths/Service/UiSettings. Checks run automatically
|
||||
on page entry (`LoadAsync`, guarded against double-entry). "Next" is disabled via
|
||||
`IInstallerPage.BlocksNavigation` (`IsRunning || HasBlockingError`) — `WizardViewModel.CanGoNext`
|
||||
subscribes to `PropertyChanged` on the current page so a live recheck can flip it back. A "Recheck"
|
||||
button re-runs `EnvironmentCheckService.RunAllAsync` (disabled while already running).
|
||||
|
||||
**"Claude Help Me" button** (`Core/ClaudeHelpLauncher.cs`) — a second button below the shared
|
||||
check-list footer, enabled only when `claude-cli` is `Ok` and `claude-auth` is not `Failed` (`Unknown`
|
||||
stays enabled — an indeterminate login state shouldn't block the one feature that could help
|
||||
diagnose it). `BuildReportAsync` renders all check results (Id/Severity/Status/Message table,
|
||||
plus the full `Detail` of any `Failed` check) and system info (OS, `dotnet --list-runtimes`,
|
||||
resolved `git`/`claude` messages, planned install dir/ports) into
|
||||
`%TEMP%\claudedo-setup-diagnose.md` — English and hardcoded (an AI assistant reads it, not the
|
||||
user) and deliberately excludes credentials/tokens/env-var dumps. `LaunchTerminal` then opens
|
||||
`wt.exe -d %TEMP% cmd.exe /k <claude> <prompt>` (or `cmd.exe /k <claude> <prompt>` if `wt.exe`
|
||||
isn't resolvable) via the injectable `IProcessLauncher`, pointing the initial prompt at that
|
||||
report file. Errors from either step surface as `ClaudeHelpError` on the page, never an
|
||||
exception. It reads the last report via `CheckListViewModel.LastReport`, so it re-evaluates on
|
||||
every check run.
|
||||
|
||||
`DiagnosePage` (`Pages/DiagnosePage/`) hosts the same `CheckListView` in `SettingsWindow` (Config
|
||||
mode only, `ShowInSettings = true` / `ShowInWizard = false`, `Order = 5` — after UiSettings).
|
||||
Nothing here blocks navigation and checks do **not** auto-run on load — only on a "Recheck" click.
|
||||
Unlike the wizard, its `InstallContext` is built from the **installed** configuration
|
||||
(`InstallerWorkerConfig.Load()` for `ClaudeBin`/`SignalRPort`, the shared `InstallContext` for
|
||||
`InstallDirectory`/`ExternalMcpPort`), refreshed on every `LoadAsync()` — not the wizard-default
|
||||
`InstallContext` the DI container hands out, which is only populated once a page's `ApplyAsync`
|
||||
(i.e. Save) runs. `DiagnosePage` intentionally has **no** "Claude Help Me" button — that one is
|
||||
wizard-only.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<UserControl x:Class="ClaudeDo.Installer.Checks.CheckListView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:ClaudeDo.Installer.Checks"
|
||||
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
|
||||
d:DataContext="{d:DesignInstance local:CheckListViewModel}"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Check list -->
|
||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:CheckRowViewModel}">
|
||||
<Border Margin="0,0,0,6" Padding="10,8"
|
||||
Background="{StaticResource IslandBgBrush}"
|
||||
BorderBrush="{StaticResource BorderSubtleBrush}"
|
||||
BorderThickness="1" CornerRadius="4">
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Ellipse Grid.Column="0" Width="10" Height="10" Margin="0,0,10,0"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{Binding StatusBrush}"/>
|
||||
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Message}" FontSize="12"
|
||||
Foreground="{StaticResource TextSecondaryBrush}"
|
||||
TextWrapping="Wrap" Margin="0,2,0,0"/>
|
||||
|
||||
<TextBlock Text="{Binding Hint}" FontSize="11"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
TextWrapping="Wrap" Margin="0,4,0,0"
|
||||
Visibility="{Binding Hint, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||
|
||||
<Button Content="{Binding HelpUrl}"
|
||||
HorizontalAlignment="Left" Margin="-16,4,0,0"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Foreground="{StaticResource AccentLightBrush}"
|
||||
Cursor="Hand"
|
||||
Command="{Binding DataContext.OpenHelpUrlCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
|
||||
CommandParameter="{Binding HelpUrl}"
|
||||
Visibility="{Binding HelpUrl, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Progress + summary -->
|
||||
<StackPanel Grid.Row="1" Margin="0,8,0,0">
|
||||
<ProgressBar IsIndeterminate="True" Margin="0,0,0,8"
|
||||
Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisConverter}}"/>
|
||||
<TextBlock Text="{Binding Summary}" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasBlockingError}" Value="True">
|
||||
<Setter Property="Foreground" Value="{StaticResource ErrorBrush}"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||
<Button Content="{loc:Tr installer.systemCheck.recheck}"
|
||||
Command="{Binding RunChecksCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public partial class CheckListView : UserControl
|
||||
{
|
||||
public CheckListView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Localization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public sealed class CheckRowViewModel
|
||||
{
|
||||
public CheckRowViewModel(CheckResult result)
|
||||
{
|
||||
var loc = TrExtension.Localizer;
|
||||
Status = result.Status;
|
||||
Severity = result.Severity;
|
||||
Title = loc?[result.TitleKey] ?? result.TitleKey;
|
||||
Message = result.Message;
|
||||
Hint = result.HintKey is not null ? loc?[result.HintKey] ?? result.HintKey : null;
|
||||
HelpUrl = result.HelpUrl;
|
||||
StatusBrush = ResolveBrush(Status, Severity);
|
||||
}
|
||||
|
||||
public CheckStatus Status { get; }
|
||||
public CheckSeverity Severity { get; }
|
||||
public string Title { get; }
|
||||
public string Message { get; }
|
||||
public string? Hint { get; }
|
||||
public string? HelpUrl { get; }
|
||||
public Brush StatusBrush { get; }
|
||||
|
||||
private static Brush ResolveBrush(CheckStatus status, CheckSeverity severity)
|
||||
{
|
||||
var key = status switch
|
||||
{
|
||||
CheckStatus.Ok => "StatusGreenBrush",
|
||||
CheckStatus.Unknown => "StatusGrayBrush",
|
||||
CheckStatus.Failed when severity == CheckSeverity.Error => "StatusRedBrush",
|
||||
CheckStatus.Failed => "StatusOrangeBrush",
|
||||
_ => "StatusGrayBrush",
|
||||
};
|
||||
return Application.Current?.Resources[key] as Brush ?? Brushes.Gray;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared presentation logic for the environment check list: running the check pass, tracking
|
||||
/// busy/summary state, and the row collection. Composed (not subclassed) by every page that
|
||||
/// hosts a check list — <see cref="Pages.SystemCheckPage.SystemCheckPageViewModel"/> and
|
||||
/// <see cref="Pages.DiagnosePage.DiagnosePageViewModel"/> — so the run logic exists exactly once.
|
||||
/// </summary>
|
||||
public partial class CheckListViewModel : ObservableObject
|
||||
{
|
||||
private readonly InstallContext _context;
|
||||
private readonly Func<EnvironmentCheckService> _checkServiceFactory;
|
||||
|
||||
public ObservableCollection<CheckRowViewModel> Rows { get; } = [];
|
||||
|
||||
/// <summary>Report of the most recent run, null before the first one. Hosts that need the raw
|
||||
/// results (the wizard's "Claude Help Me" button) read it instead of running the checks again.</summary>
|
||||
public EnvironmentCheckReport? LastReport { get; private set; }
|
||||
|
||||
[ObservableProperty] private bool _isRunning;
|
||||
[ObservableProperty] private bool _hasRun;
|
||||
[ObservableProperty] private bool _hasBlockingError;
|
||||
[ObservableProperty] private string _summary = string.Empty;
|
||||
|
||||
public CheckListViewModel(InstallContext context, Func<EnvironmentCheckService> checkServiceFactory)
|
||||
{
|
||||
_context = context;
|
||||
_checkServiceFactory = checkServiceFactory;
|
||||
}
|
||||
|
||||
partial void OnIsRunningChanged(bool value) => RunChecksCommand.NotifyCanExecuteChanged();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanRunChecks))]
|
||||
private async Task RunChecksAsync()
|
||||
{
|
||||
if (IsRunning) return;
|
||||
|
||||
IsRunning = true;
|
||||
Summary = TrExtension.Localizer?["installer.systemCheck.summary.running"] ?? "Checking your system…";
|
||||
|
||||
try
|
||||
{
|
||||
var service = _checkServiceFactory();
|
||||
var report = await service.RunAllAsync(_context, CancellationToken.None);
|
||||
|
||||
Rows.Clear();
|
||||
foreach (var result in report.Results)
|
||||
Rows.Add(new CheckRowViewModel(result));
|
||||
|
||||
LastReport = report;
|
||||
OnPropertyChanged(nameof(LastReport));
|
||||
HasBlockingError = report.HasBlockingError;
|
||||
Summary = BuildSummary(report);
|
||||
HasRun = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanRunChecks() => !IsRunning;
|
||||
|
||||
private static string BuildSummary(EnvironmentCheckReport report)
|
||||
{
|
||||
var loc = TrExtension.Localizer;
|
||||
|
||||
var blocking = report.Results
|
||||
.Where(r => r.Severity == CheckSeverity.Error && r.Status == CheckStatus.Failed)
|
||||
.ToList();
|
||||
if (blocking.Count > 0)
|
||||
{
|
||||
var names = string.Join(", ", blocking.Select(r => loc?[r.TitleKey] ?? r.TitleKey));
|
||||
return loc?.Get("installer.systemCheck.summary.blocking", blocking.Count, names)
|
||||
?? $"{blocking.Count} problem(s) must be fixed: {names}";
|
||||
}
|
||||
|
||||
var warnings = report.Results.Count(r => r.Severity == CheckSeverity.Warning && r.Status == CheckStatus.Failed);
|
||||
if (warnings > 0)
|
||||
{
|
||||
return loc?.Get("installer.systemCheck.summary.warnings", warnings)
|
||||
?? $"{warnings} warning(s) found.";
|
||||
}
|
||||
|
||||
return loc?["installer.systemCheck.summary.ok"] ?? "Everything looks good.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private static void OpenHelpUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) return;
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public enum CheckSeverity { Warning, Error }
|
||||
|
||||
public enum CheckStatus { Ok, Failed, Unknown }
|
||||
|
||||
public sealed record CheckResult(
|
||||
string Id,
|
||||
CheckSeverity Severity,
|
||||
CheckStatus Status,
|
||||
string TitleKey,
|
||||
string Message,
|
||||
string? HintKey,
|
||||
string? HelpUrl,
|
||||
string? Detail)
|
||||
{
|
||||
public static CheckResult Ok(string id, CheckSeverity severity, string titleKey, string message, string? detail = null) =>
|
||||
new(id, severity, CheckStatus.Ok, titleKey, message, HintKey: null, HelpUrl: null, detail);
|
||||
|
||||
public static CheckResult Fail(string id, CheckSeverity severity, string titleKey, string message, string? hintKey = null, string? helpUrl = null, string? detail = null) =>
|
||||
new(id, severity, CheckStatus.Failed, titleKey, message, hintKey, helpUrl, detail);
|
||||
|
||||
public static CheckResult Unknown(string id, CheckSeverity severity, string titleKey, string message, string? detail = null) =>
|
||||
new(id, severity, CheckStatus.Unknown, titleKey, message, HintKey: null, HelpUrl: null, detail);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Text.Json;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Checks login via `claude auth status --json` — never sends a prompt (costs tokens, can hang
|
||||
/// on a usage-limited account). See docs/explore-notes/installer-preflight.md §4.
|
||||
/// </summary>
|
||||
public sealed class ClaudeAuthCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "claude-auth";
|
||||
|
||||
private readonly ClaudeCliLookup _lookup;
|
||||
|
||||
public ClaudeAuthCheck(ClaudeCliLookup lookup)
|
||||
{
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Error;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
var (fileName, arguments) = ProcessCommand.For(lookup.Resolved, "auth", "status", "--json");
|
||||
var (exitCode, output) = await _lookup.ProcessRunner.RunAsync(fileName, arguments, null, ct);
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
return CheckResult.Unknown(Id, Severity, "checks.claudeAuth.title",
|
||||
"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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
private static bool? TryParseLoggedIn(string output)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(output);
|
||||
if (doc.RootElement.TryGetProperty("loggedIn", out var prop) &&
|
||||
prop.ValueKind is JsonValueKind.True or JsonValueKind.False)
|
||||
{
|
||||
return prop.GetBoolean();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// fall through to null — treated as Unknown
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Without the claude CLI, no task can ever run — blocking.</summary>
|
||||
public sealed class ClaudeCliCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "claude-cli";
|
||||
|
||||
private readonly ClaudeCliLookup _lookup;
|
||||
|
||||
public ClaudeCliCheck(ClaudeCliLookup lookup)
|
||||
{
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Error;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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}.",
|
||||
"checks.claudeCli.hint");
|
||||
}
|
||||
|
||||
if (lookup.ExitCode != 0)
|
||||
{
|
||||
return CheckResult.Fail(Id, Severity, "checks.claudeCli.title",
|
||||
$"'{lookup.Resolved.Path}' exited with code {lookup.ExitCode}.",
|
||||
"checks.claudeCli.hint", detail: lookup.Output);
|
||||
}
|
||||
|
||||
var version = lookup.ParsedVersion?.ToString() ?? lookup.Output.Trim();
|
||||
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);
|
||||
}
|
||||
|
||||
private IEnumerable<string> SearchedLocations()
|
||||
{
|
||||
var pathVar = _lookup.PathOverride ?? System.Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
var pathEntries = pathVar.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return pathEntries.Concat(ExecutableResolver.FallbackDirectories());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public sealed record ClaudeCliLookupResult(
|
||||
ResolvedExecutable? Resolved,
|
||||
int ExitCode,
|
||||
string Output,
|
||||
Version? ParsedVersion);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the claude CLI and runs `--version` at most once per install run, shared across
|
||||
/// ClaudeCliCheck, ClaudeVersionCheck, ClaudeAuthCheck, and PermissionModeAutoCheck.
|
||||
/// </summary>
|
||||
public sealed class ClaudeCliLookup
|
||||
{
|
||||
private static readonly Regex VersionPattern = new(@"\d+(\.\d+){1,3}", RegexOptions.Compiled);
|
||||
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
private ClaudeCliLookupResult? _cached;
|
||||
|
||||
public ClaudeCliLookup(IProcessRunner processRunner, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
ProcessRunner = processRunner;
|
||||
PathOverride = pathOverride;
|
||||
PathExtOverride = pathExtOverride;
|
||||
}
|
||||
|
||||
public IProcessRunner ProcessRunner { get; }
|
||||
public string? PathOverride { get; }
|
||||
public string? PathExtOverride { get; }
|
||||
|
||||
public async Task<ClaudeCliLookupResult> ResolveAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
if (_cached is not null) return _cached;
|
||||
|
||||
await _gate.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
_cached ??= await ResolveCoreAsync(ctx, ct);
|
||||
return _cached;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ClaudeCliLookupResult> ResolveCoreAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
var resolved = ExecutableResolver.Resolve(ctx.ClaudeBin, PathOverride, PathExtOverride);
|
||||
if (resolved is null)
|
||||
{
|
||||
return new ClaudeCliLookupResult(null, ExitCode: -1, Output: string.Empty, ParsedVersion: null);
|
||||
}
|
||||
|
||||
var (fileName, arguments) = ProcessCommand.For(resolved, "--version");
|
||||
var (exitCode, output) = await ProcessRunner.RunAsync(fileName, arguments, null, ct);
|
||||
var parsedVersion = exitCode == 0 ? ParseVersion(output) : null;
|
||||
|
||||
return new ClaudeCliLookupResult(resolved, exitCode, output, parsedVersion);
|
||||
}
|
||||
|
||||
private static Version? ParseVersion(string output) =>
|
||||
VersionPattern.Match(output) is { Success: true } match && Version.TryParse(match.Value, out var version)
|
||||
? version
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Gates the CLI flags ClaudeArgsBuilder relies on — blocking.</summary>
|
||||
public sealed class ClaudeVersionCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "claude-version";
|
||||
|
||||
/// <summary>
|
||||
/// Newest version confirmed to work end-to-end with every flag ClaudeDo uses (--permission-mode
|
||||
/// auto, --effort, --agents, --json-schema, --append-system-prompt, --output-format stream-json
|
||||
/// --verbose, --resume, mcp add --transport http --scope user). A verified floor, not a proven
|
||||
/// theoretical minimum — see docs/explore-notes/installer-preflight.md §3.
|
||||
/// </summary>
|
||||
public static readonly Version MinimumVersion = new(2, 1, 220);
|
||||
|
||||
private readonly ClaudeCliLookup _lookup;
|
||||
|
||||
public ClaudeVersionCheck(ClaudeCliLookup lookup)
|
||||
{
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Error;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
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()}'.");
|
||||
}
|
||||
|
||||
if (lookup.ParsedVersion < MinimumVersion)
|
||||
{
|
||||
return CheckResult.Fail(Id, Severity, "checks.claudeVersion.title",
|
||||
$"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}).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public sealed record EnvironmentCheckReport(IReadOnlyList<CheckResult> Results)
|
||||
{
|
||||
public bool HasBlockingError => Results.Any(r => r.Severity == CheckSeverity.Error && r.Status == CheckStatus.Failed);
|
||||
}
|
||||
|
||||
public sealed class EnvironmentCheckService
|
||||
{
|
||||
private readonly IReadOnlyList<IEnvironmentCheck> _checks;
|
||||
|
||||
public EnvironmentCheckService(IEnumerable<IEnvironmentCheck> checks)
|
||||
{
|
||||
_checks = checks.ToList();
|
||||
}
|
||||
|
||||
public async Task<EnvironmentCheckReport> RunAllAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
var tasks = _checks.Select(check => RunSafeAsync(check, ctx, ct)).ToArray();
|
||||
var results = await Task.WhenAll(tasks);
|
||||
return new EnvironmentCheckReport(results);
|
||||
}
|
||||
|
||||
private static async Task<CheckResult> RunSafeAsync(IEnvironmentCheck check, InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await check.RunAsync(ctx, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CheckResult.Unknown(check.Id, check.Severity, string.Empty, ex.Message, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Without git, no task can run (worktrees) — blocking.</summary>
|
||||
public sealed class GitCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "git";
|
||||
private static readonly Regex VersionPattern = new(@"\d+(\.\d+){1,3}", RegexOptions.Compiled);
|
||||
|
||||
private readonly IProcessRunner _processRunner;
|
||||
private readonly string? _pathOverride;
|
||||
private readonly string? _pathExtOverride;
|
||||
|
||||
public GitCheck(IProcessRunner processRunner, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
_processRunner = processRunner;
|
||||
_pathOverride = pathOverride;
|
||||
_pathExtOverride = pathExtOverride;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Error;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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.",
|
||||
"checks.git.hint", "https://git-scm.com/downloads");
|
||||
}
|
||||
|
||||
var (fileName, arguments) = ProcessCommand.For(resolved, "--version");
|
||||
var (exitCode, output) = await _processRunner.RunAsync(fileName, arguments, null, ct);
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
return CheckResult.Fail(Id, Severity, "checks.git.title", $"'{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}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Trivial to fix after the fact (git config --global), and the Claude help-me button can resolve it — a warning, not a blocker.</summary>
|
||||
public sealed class GitIdentityCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "git-identity";
|
||||
|
||||
private readonly IProcessRunner _processRunner;
|
||||
private readonly string? _pathOverride;
|
||||
private readonly string? _pathExtOverride;
|
||||
|
||||
public GitIdentityCheck(IProcessRunner processRunner, string? pathOverride = null, string? pathExtOverride = null)
|
||||
{
|
||||
_processRunner = processRunner;
|
||||
_pathOverride = pathOverride;
|
||||
_pathExtOverride = pathExtOverride;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Warning;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
var name = await ReadConfigAsync(resolved, "user.name", ct);
|
||||
var email = await ReadConfigAsync(resolved, "user.email", ct);
|
||||
var hasName = !string.IsNullOrWhiteSpace(name);
|
||||
var hasEmail = !string.IsNullOrWhiteSpace(email);
|
||||
|
||||
if (hasName && hasEmail)
|
||||
{
|
||||
return CheckResult.Ok(Id, Severity, "checks.gitIdentity.title", $"{name} <{email}>");
|
||||
}
|
||||
|
||||
var missing = (hasName, hasEmail) switch
|
||||
{
|
||||
(false, false) => "user.name, user.email",
|
||||
(false, true) => "user.name",
|
||||
_ => "user.email",
|
||||
};
|
||||
|
||||
return CheckResult.Fail(Id, Severity, "checks.gitIdentity.title", $"Missing git identity: {missing}.", "checks.gitIdentity.hint");
|
||||
}
|
||||
|
||||
private async Task<string> ReadConfigAsync(ResolvedExecutable resolved, string key, CancellationToken ct)
|
||||
{
|
||||
var (fileName, arguments) = ProcessCommand.For(resolved, "config", "--get", key);
|
||||
var (_, output) = await _processRunner.RunAsync(fileName, arguments, null, ct);
|
||||
return output.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public interface IEnvironmentCheck
|
||||
{
|
||||
string Id { get; }
|
||||
CheckSeverity Severity { get; }
|
||||
Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ClaudeDo.Installer.Checks.Interfaces;
|
||||
|
||||
public interface IPortOwnerResolver
|
||||
{
|
||||
Task<string?> FindOwningProcessNameAsync(int port, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using ClaudeDo.Installer.Checks.Interfaces;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
public sealed class NetstatPortOwnerResolver : IPortOwnerResolver
|
||||
{
|
||||
private readonly IProcessRunner _processRunner;
|
||||
|
||||
public NetstatPortOwnerResolver(IProcessRunner processRunner) => _processRunner = processRunner;
|
||||
|
||||
public async Task<string?> FindOwningProcessNameAsync(int port, CancellationToken ct)
|
||||
{
|
||||
var (exitCode, output) = await _processRunner.RunAsync("netstat.exe", "-ano -p TCP", null, ct);
|
||||
if (exitCode != 0) return null;
|
||||
|
||||
var suffix = $":{port}";
|
||||
foreach (var line in output.Split('\n'))
|
||||
{
|
||||
var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length < 5) continue;
|
||||
if (!parts[0].Equals("TCP", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!parts[1].EndsWith(suffix, StringComparison.Ordinal)) continue;
|
||||
if (!int.TryParse(parts[^1], out var pid)) continue;
|
||||
|
||||
try { return Process.GetProcessById(pid).ProcessName; }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Static-only check per docs/explore-notes/installer-preflight.md §2: real auto-mode eligibility
|
||||
/// (org/model/plan) has no cheap detectable signal, so this only confirms the CLI's argument
|
||||
/// parser still recognizes "auto" as a --permission-mode choice. A warning, not a blocker — a
|
||||
/// too-old CLI just degrades to acceptEdits/default with more prompts.
|
||||
/// </summary>
|
||||
public sealed class PermissionModeAutoCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "claude-permission-mode-auto";
|
||||
|
||||
private static readonly Regex AutoWord = new(@"\bauto\b", RegexOptions.Compiled);
|
||||
private const int SearchWindow = 300;
|
||||
|
||||
private readonly ClaudeCliLookup _lookup;
|
||||
|
||||
public PermissionModeAutoCheck(ClaudeCliLookup lookup)
|
||||
{
|
||||
_lookup = lookup;
|
||||
}
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Warning;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
var (fileName, arguments) = ProcessCommand.For(lookup.Resolved, "--help");
|
||||
var (exitCode, output) = await _lookup.ProcessRunner.RunAsync(fileName, arguments, null, ct);
|
||||
|
||||
if (exitCode != 0 || string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return CheckResult.Unknown(Id, Severity, "checks.permissionModeAuto.title",
|
||||
"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.");
|
||||
}
|
||||
|
||||
return CheckResult.Fail(Id, Severity, "checks.permissionModeAuto.title",
|
||||
"This claude CLI version does not list 'auto' as a --permission-mode choice.",
|
||||
"checks.permissionModeAuto.hint");
|
||||
}
|
||||
|
||||
private static bool AutoIsListed(string helpOutput)
|
||||
{
|
||||
var index = helpOutput.IndexOf("--permission-mode", StringComparison.Ordinal);
|
||||
if (index < 0) return false;
|
||||
|
||||
var windowEnd = Math.Min(helpOutput.Length, index + SearchWindow);
|
||||
return AutoWord.IsMatch(helpOutput[index..windowEnd]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using ClaudeDo.Installer.Checks.Interfaces;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Both ports are configurable in the installer settings, so a conflict is only a warning.</summary>
|
||||
public sealed class PortCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "ports";
|
||||
|
||||
private readonly IPortOwnerResolver _portOwnerResolver;
|
||||
|
||||
public PortCheck(IPortOwnerResolver portOwnerResolver) => _portOwnerResolver = portOwnerResolver;
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Warning;
|
||||
|
||||
public async Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
var ports = new (string Label, int Port)[]
|
||||
{
|
||||
("SignalR", ctx.SignalRPort),
|
||||
("External MCP", ctx.ExternalMcpPort),
|
||||
};
|
||||
|
||||
var blocked = new List<string>();
|
||||
var ownWorker = new List<string>();
|
||||
|
||||
foreach (var (label, port) in ports)
|
||||
{
|
||||
if (IsFree(port)) continue;
|
||||
|
||||
var owner = await _portOwnerResolver.FindOwningProcessNameAsync(port, ct);
|
||||
if (owner is not null && owner.Contains("ClaudeDo.Worker", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ownWorker.Add($"{label} ({port})");
|
||||
continue;
|
||||
}
|
||||
|
||||
blocked.Add(owner is null ? $"{label} port {port} is in use." : $"{label} port {port} is in use by '{owner}'.");
|
||||
}
|
||||
|
||||
if (blocked.Count > 0)
|
||||
{
|
||||
return CheckResult.Fail(Id, Severity, "checks.ports.title", string.Join(" ", blocked), "checks.ports.hint");
|
||||
}
|
||||
|
||||
if (ownWorker.Count > 0)
|
||||
{
|
||||
return CheckResult.Ok(Id, Severity, "checks.ports.title", $"In use by the running ClaudeDo Worker: {string.Join(", ", ownWorker)}.");
|
||||
}
|
||||
|
||||
return CheckResult.Ok(Id, Severity, "checks.ports.title", "Ports are available.");
|
||||
}
|
||||
|
||||
private static bool IsFree(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var listener = new TcpListener(IPAddress.Loopback, port);
|
||||
listener.Start();
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using ClaudeDo.Data.Environment;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Builds a (FileName, Arguments) pair for a resolved executable, routing shims through cmd.exe.</summary>
|
||||
internal static class ProcessCommand
|
||||
{
|
||||
public static (string FileName, string Arguments) For(ResolvedExecutable resolved, params string[] args)
|
||||
{
|
||||
if (resolved.IsShim)
|
||||
{
|
||||
var shim = ExecutableResolver.BuildShimStartInfo(resolved.Path, args);
|
||||
return (shim.FileName, shim.Arguments);
|
||||
}
|
||||
|
||||
return (resolved.Path, string.Join(' ', args.Select(Quote)));
|
||||
}
|
||||
|
||||
private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Checks;
|
||||
|
||||
/// <summary>Without write access nothing can be installed — blocking.</summary>
|
||||
public sealed class WriteAccessCheck : IEnvironmentCheck
|
||||
{
|
||||
public const string CheckId = "write-access";
|
||||
|
||||
public string Id => CheckId;
|
||||
public CheckSeverity Severity => CheckSeverity.Error;
|
||||
|
||||
public Task<CheckResult> RunAsync(InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(CheckResult.Ok(Id, Severity, "checks.writeAccess.title", "Install directory and data directory are writable."));
|
||||
}
|
||||
|
||||
private static string? TryWrite(string path)
|
||||
{
|
||||
var probeDir = FirstExistingParent(path);
|
||||
var probeFile = Path.Combine(probeDir, $".claudedo-write-check-{Guid.NewGuid():N}.tmp");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(probeFile, string.Empty);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(probeFile); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
private static string FirstExistingParent(string path)
|
||||
{
|
||||
var current = Path.GetFullPath(path);
|
||||
while (!Directory.Exists(current))
|
||||
{
|
||||
var parent = Path.GetDirectoryName(current);
|
||||
if (string.IsNullOrEmpty(parent) || parent == current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,26 @@ public static class AutostartShortcut
|
||||
|
||||
public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName);
|
||||
|
||||
public static void Install(string startupDir, string workerExe)
|
||||
/// <summary>Creates or updates the Startup shortcut. Returns false if it already pointed at <paramref name="workerExe"/> and was left untouched.</summary>
|
||||
public static bool Install(string startupDir, string workerExe)
|
||||
{
|
||||
Directory.CreateDirectory(startupDir);
|
||||
var shortcutPath = PathIn(startupDir);
|
||||
if (File.Exists(shortcutPath))
|
||||
{
|
||||
var existingTarget = ShortcutFactory.TryGetTarget(shortcutPath);
|
||||
if (existingTarget is not null && PathsEqual(existingTarget, workerExe))
|
||||
return false;
|
||||
}
|
||||
|
||||
var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir;
|
||||
ShortcutFactory.CreateShortcut(PathIn(startupDir), workerExe, workingDir, "ClaudeDo background worker");
|
||||
ShortcutFactory.CreateShortcut(shortcutPath, workerExe, workingDir, "ClaudeDo background worker");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool PathsEqual(string a, string b) =>
|
||||
string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static void Remove(string startupDir)
|
||||
{
|
||||
var path = PathIn(startupDir);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ClaudeDo.Data.Environment;
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Core;
|
||||
|
||||
public sealed record ClaudeHelpLaunchResult(bool Success, string? ErrorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Builds a setup-diagnose report from the environment check results and opens an interactive
|
||||
/// claude session pointed at it, so a stuck user can get live help finishing setup.
|
||||
/// </summary>
|
||||
public sealed class ClaudeHelpLauncher
|
||||
{
|
||||
public const string ReportFileName = "claudedo-setup-diagnose.md";
|
||||
|
||||
private readonly IProcessRunner _processRunner;
|
||||
private readonly IProcessLauncher _processLauncher;
|
||||
private readonly string? _pathOverride;
|
||||
private readonly string? _pathExtOverride;
|
||||
|
||||
public ClaudeHelpLauncher(
|
||||
IProcessRunner processRunner,
|
||||
IProcessLauncher processLauncher,
|
||||
string? pathOverride = null,
|
||||
string? pathExtOverride = null)
|
||||
{
|
||||
_processRunner = processRunner;
|
||||
_processLauncher = processLauncher;
|
||||
_pathOverride = pathOverride;
|
||||
_pathExtOverride = pathExtOverride;
|
||||
}
|
||||
|
||||
public async Task<string> BuildReportAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("# ClaudeDo setup diagnose");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(
|
||||
"ClaudeDo is a desktop task manager that runs the `claude` CLI autonomously in git " +
|
||||
"worktrees. The person you're talking to is stuck partway through the setup wizard. " +
|
||||
"Explain the likely cause of each failing check below in plain language, propose the " +
|
||||
"concrete command(s) to fix it, and ask a clarifying question if something here is " +
|
||||
"ambiguous.");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Check results");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Id | Severity | Status | Message |");
|
||||
sb.AppendLine("|---|---|---|---|");
|
||||
foreach (var result in report.Results)
|
||||
{
|
||||
sb.AppendLine($"| {result.Id} | {result.Severity} | {result.Status} | {EscapeCell(result.Message)} |");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
var failedDetails = report.Results
|
||||
.Where(r => r.Status == CheckStatus.Failed && !string.IsNullOrWhiteSpace(r.Detail))
|
||||
.ToList();
|
||||
if (failedDetails.Count > 0)
|
||||
{
|
||||
sb.AppendLine("## Details of failed checks");
|
||||
foreach (var result in failedDetails)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"### {result.Id}");
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine(result.Detail);
|
||||
sb.AppendLine("```");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("## System info");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"- Windows: {RuntimeInformation.OSDescription}");
|
||||
sb.AppendLine($"- Architecture: {RuntimeInformation.OSArchitecture}");
|
||||
sb.AppendLine($"- git: {FindMessage(report, GitCheck.CheckId)}");
|
||||
sb.AppendLine($"- claude: {FindMessage(report, ClaudeCliCheck.CheckId)}");
|
||||
sb.AppendLine($"- Planned install directory: {ctx.InstallDirectory}");
|
||||
sb.AppendLine($"- Planned ports: SignalR {ctx.SignalRPort}, MCP {ctx.ExternalMcpPort}");
|
||||
sb.AppendLine("- `dotnet --list-runtimes`:");
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine(await RunDotnetListRuntimesAsync(ct));
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
// No secrets: never include credentials.json content, tokens, or a raw environment
|
||||
// variable dump here — only resolved paths and version numbers, since this file exists
|
||||
// to be read by an AI assistant, not to stay private.
|
||||
sb.AppendLine(
|
||||
"No secrets are included above (no credentials file contents, no tokens, no raw " +
|
||||
"environment variable dump) — only resolved paths and version numbers.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Please start with the blocking errors above.");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<ClaudeHelpLaunchResult> LaunchAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct)
|
||||
{
|
||||
string reportPath;
|
||||
try
|
||||
{
|
||||
var content = await BuildReportAsync(report, ctx, ct);
|
||||
reportPath = Path.Combine(Path.GetTempPath(), ReportFileName);
|
||||
await File.WriteAllTextAsync(reportPath, content, ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ClaudeHelpLaunchResult(false, ex.Message);
|
||||
}
|
||||
|
||||
return LaunchTerminal(reportPath, ctx);
|
||||
}
|
||||
|
||||
public ClaudeHelpLaunchResult LaunchTerminal(string reportPath, InstallContext ctx)
|
||||
{
|
||||
var claude = ExecutableResolver.Resolve(ctx.ClaudeBin, _pathOverride, _pathExtOverride);
|
||||
if (claude is null)
|
||||
{
|
||||
return new ClaudeHelpLaunchResult(false, $"'{ctx.ClaudeBin}' was not found on PATH.");
|
||||
}
|
||||
|
||||
var tempDir = Path.GetTempPath();
|
||||
var promptText = $"Lies {reportPath} und hilf mir, mein ClaudeDo-Setup zum Laufen zu bringen.";
|
||||
var claudeCommand = $"{Quote(claude.Path)} {Quote(promptText)}";
|
||||
|
||||
var wt = ExecutableResolver.Resolve("wt", _pathOverride, _pathExtOverride);
|
||||
var startInfo = wt is not null
|
||||
? new ProcessStartInfo(wt.Path)
|
||||
{
|
||||
Arguments = $"-d {QuoteDirectory(tempDir)} cmd.exe /k {claudeCommand}",
|
||||
WorkingDirectory = tempDir,
|
||||
UseShellExecute = false,
|
||||
}
|
||||
: new ProcessStartInfo("cmd.exe")
|
||||
{
|
||||
Arguments = $"/k {claudeCommand}",
|
||||
WorkingDirectory = tempDir,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_processLauncher.Start(startInfo);
|
||||
return new ClaudeHelpLaunchResult(true, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ClaudeHelpLaunchResult(false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> RunDotnetListRuntimesAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (_, output) = await _processRunner.RunAsync("dotnet", "--list-runtimes", null, ct);
|
||||
return output.Trim();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"(could not run 'dotnet --list-runtimes': {ex.Message})";
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FindMessage(EnvironmentCheckReport report, string checkId) =>
|
||||
report.Results.FirstOrDefault(r => r.Id == checkId)?.Message;
|
||||
|
||||
private static string EscapeCell(string value) =>
|
||||
value.Replace("|", "\\|").Replace("\r", " ").Replace("\n", " ");
|
||||
|
||||
private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value;
|
||||
|
||||
private static string QuoteDirectory(string directory) => Quote(directory.TrimEnd('\\', '/'));
|
||||
}
|
||||
@@ -52,6 +52,9 @@ public sealed class InstallerWorkerConfig
|
||||
[JsonPropertyName("claude_bin")]
|
||||
public string ClaudeBin { get; set; } = "claude";
|
||||
|
||||
[JsonPropertyName("external_mcp_port")]
|
||||
public int ExternalMcpPort { get; set; } = 47_822;
|
||||
|
||||
private static readonly JsonSerializerOptions ReadOpts = new()
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
|
||||
@@ -13,4 +13,7 @@ public interface IInstallerPage
|
||||
Task LoadAsync();
|
||||
Task ApplyAsync();
|
||||
bool Validate();
|
||||
|
||||
/// <summary>True while this page wants "Next" disabled (e.g. a check run in progress or a blocking error).</summary>
|
||||
bool BlocksNavigation => false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
/// <summary>Starts a detached process without waiting for it to exit or capturing its output.</summary>
|
||||
public interface IProcessLauncher
|
||||
{
|
||||
void Start(ProcessStartInfo startInfo);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
public interface IProcessRunner
|
||||
{
|
||||
Task<(int ExitCode, string Output)> RunAsync(string fileName, string arguments, string? workingDirectory, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Core;
|
||||
|
||||
public sealed class ProcessLauncher : IProcessLauncher
|
||||
{
|
||||
public void Start(ProcessStartInfo startInfo) => Process.Start(startInfo);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user