fix:Improve Claude Mds

This commit is contained in:
Mika Kuns
2026-08-05 16:46:02 +02:00
parent f6cb8250bb
commit d15aa27707
9 changed files with 307 additions and 272 deletions
+69 -28
View File
@@ -4,52 +4,93 @@ 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, IsManual (reminder only the user can do — automation skips it), Notes, ParentTaskId, PlanningSessionId, PlanningSessionToken, PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit (nullable; review range for a worktree-less "list handler" host task — Mission Control's "Let Claude handle it" — which commits straight into the list's working dir instead of a per-task worktree: `HandlerBaseCommit` is stamped to the list repo's HEAD when the host task is created, `HandlerHeadCommit` when it's submitted for review; the Worker's `SubmitTaskForReview`/`GetTaskDiff` and the Ui's `DetailsIslandViewModel`/`MergeSectionViewModel` fall back to this pair whenever `Worktree` is null). Legacy values `Manual`/`Planning`/`Planned`/`Draft`/`Waiting` were retired; existing rows backfill automatically via the `RetireLegacyTaskStatus` migration.
- **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.
- 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).
- 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 with list), Model, SystemPrompt, AgentPath, MaxTurns, SessionSkills, VerifyCommand (all nullable). `VerifyCommand` is an optional post-merge gate (see `ClaudeDo.Worker/CLAUDE.md` → TaskMergeService): null/blank = today's behavior, no gate.
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, MergeCommit (nullable; SHA of the merge commit this worktree's branch produced on the target branch, stamped by `TaskMergeService` the moment a merge/continue-merge succeeds — the only thing that makes `revert_merge` possible without heuristically searching `git log`; null for any worktree merged before this field existed), State (Active|Merged|Discarded|Kept)
- **TaskRunEntity** — per-run record (session_id, turns, result, structured output, exit code, log path, nullable `Model` — the model the run actually executed with, column `model`; token fields `TokensIn`/`TokensOut`/`CacheReadTokens`/`CacheWriteTokens`, columns `tokens_in`/`tokens_out`/`cache_read_tokens`/`cache_write_tokens` — populated from the session transcript, not the stream-json event, as a per-run delta against prior runs on the same `session_id`; see `ClaudeDo.Worker/CLAUDE.md` → Execution History)
- **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) , `ModelPresets` (string?, JSON array of `ModelPreset` rows, column `model_presets``AppSettingsRepository.GetAsync` backfills shipping defaults into this column on the first read after it's null, so it's never null once a run has started), `MaxTurnsCeiling` (int, default 80, column `max_turns_ceiling` — hard ceiling every resolved max-turns value (task/list/global) is clamped to before a run starts; `AppSettingsRepository.UpdateAsync` clamps it to a minimum of 1), `UsageGateFiveHourPct` / `UsageGateSevenDayPct` (int, defaults 80/90, columns `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` — percentage of the 5h/7d Claude usage window at which the autonomous queue pauses; `0` = gate off; `AppSettingsRepository.UpdateAsync` clamps both to 0..100), and `UsageThrottleSoftPct` / `UsageThrottleHardPct` (int, defaults 50/65, columns `usage_throttle_soft_pct` / `usage_throttle_hard_pct` — intermediate staging thresholds below the hard gate above, at which the queue's effective parallelism steps down to 2 then 1 slot; see `Worker/CLAUDE.md``UsageThrottle`; `0` = that stage off; also clamped 0..100). `DefaultMaxTurns` default was lowered from 100 to 40 (entity default + `AddMaxTurnsCeiling` migration backfill on the seeded row).
- **ModelPresets** / **ModelPreset** — per-model run defaults (`Model`, `Effort`, `MaxTurns`): one row per `ModelRegistry.Aliases` entry, supplying the **global** effort and max-turns defaults. `Parse`/`Serialize` normalize (unknown models dropped, missing aliases filled from `Defaults`, effort validated, turns clamped 1200) and never throw — a malformed settings row must not stop a run. `For(presets, model, fallbackMaxTurns = 30)` always returns a usable row: `model` is resolved through `ModelRegistry.TryNormalizeAlias` first, so a full CLI model id (e.g. `claude-sonnet-4-6`) still hits its alias's preset row instead of missing every lookup and falling through; only a model that normalizes to nothing recognized falls back to a synthesized row (`EffortRegistry.DefaultLevel` + `fallbackMaxTurns` — callers pass `AppSettings.DefaultMaxTurns` here so that setting has a real effect instead of a hardcoded number). Ship defaults: haiku medium/20, sonnet high/30, opus high/40, fable high/25.
- **ModelRegistry.TryNormalizeAlias** — non-throwing counterpart to `NormalizeAlias` for the run path: exact alias match, then substring match against a full model id, else `null`. Never throws, unlike `NormalizeAlias` (which stays the strict, throwing validator for `add_task`/planning model input).
- **EffortRegistry** — the `--effort` levels (`low|medium|high|xhigh|max`) + `NormalizeLevel` (blank → null = don't pass the flag)
- **SubtaskEntity**, **AppSettingsEntity**, **AgentInfo** — existing helpers / settings / record for scanned agent files
- **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. |
| `UsageThrottleSoftPct` / `UsageThrottleHardPct` | `usage_throttle_*_pct` | 50 / 65 | Staged parallelism below the hard gate; `0` = that stage off. |
| `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 1200) 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`, `SetMergedAsync` (atomically sets State=Merged and stamps MergeCommit in one update — the only writer of MergeCommit)
- **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), revert (`RevertMergeCommitAsync``git revert --no-edit -m 1 <sha>`, reverts a merge commit against its first parent; `RevertAbortAsync`; `IsMidRevertAsync``REVERT_HEAD` presence, mirrors `IsMidMergeAsync`'s `MERGE_HEAD`), `PreviewMergeAsync` (non-destructive mergeability check via `git merge-tree --write-tree`), `CountChangedFilesAsync`, rev-parse, is-git-repo. Revert never resets/rewrites — it always produces a new commit, because the working directory it operates on is shared with other concurrent sessions.
**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. Migration `AddModelPresetsAndManualFlag` added `app_settings.model_presets` plus the `is_manual` flags on `tasks` and `lists`. Migration `AddHandlerCommitRange` added the nullable `handler_base_commit`/`handler_head_commit` columns to `tasks`. Migration `AddUsageGateAndRunModel` added `app_settings.usage_gate_five_hour_pct`/`usage_gate_seven_day_pct` (defaults 80/90) and the nullable `task_runs.model` column. Migration `AddWorktreeMergeCommit` added the nullable `worktrees.merge_commit` column. Migration `AddVerifyCommand` added the nullable `list_config.verify_command` column. Migration `AddMaxTurnsCeiling` added `app_settings.max_turns_ceiling` (default 80) and lowered `default_max_turns`'s default/seeded value from 100 to 40. Migration `AddRunCacheTokens` added the nullable `task_runs.cache_read_tokens`/`cache_write_tokens` columns. Migration `AddUsageThrottleThresholds` added `app_settings.usage_throttle_soft_pct`/`usage_throttle_hard_pct` (defaults 50/65). `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: `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