Files
ClaudeDo/src/ClaudeDo.Data/CLAUDE.md
T
mika kuns e8e2fcec3f docs: update for task-numbers features (Slice 5)
Document task-number allocation in Data layer (TaskEntity.Number,
next_task_number counter, invariants, insert paths).

Update Worker docs to clarify TaskIdResolver wiring (#123 → GUID lookup),
Number in MCP payloads, and correct the 'Two hard conventions' statement
(only the first is test-enforced).

Bump external-mcp.md verified-against commit to 38af549 (Slice 4 merge)
and add new sections on task ID resolution and numbering.

Add Slice 4 visual verification items to open.md (row/detail number display,
worker-log messages).

Verified against:
- src/ClaudeDo.Data/TaskNumberAllocator.cs
- src/ClaudeDo.Data/Repositories/TaskRepository.cs (AddAsync line 20, CreateChildAsync line 276)
- src/ClaudeDo.Worker/External/TaskIdResolver.cs
- src/ClaudeDo.Worker/External/ExternalMcpService.cs (TaskDto/TaskRefDto DTOs)
- tests/ClaudeDo.Worker.Tests/External/ExternalMcpToolSchemaTests.cs (only first convention test-enforced)
2026-08-11 14:26:22 +02:00

12 KiB
Raw Blame History

ClaudeDo.Data

Shared data layer: models, repositories, SQLite infrastructure, and git operations.

Models

  • TaskEntity — Id, ListId, Title, Description, Status, PlanningPhase, BlockedByTaskId (FK to predecessor in a chain), DependsOnTaskId (FK to a user/MCP-declared predecessor, distinct from BlockedByTaskId), ScheduledFor, Result, ReviewFeedback, LogPath, timestamps, CommitType, Model / SystemPrompt / AgentPath / MaxTurns (nullable overrides), IsStarred, IsMyDay, IsManual, Notes, ParentTaskId, PlanningSessionId / PlanningSessionToken / PlanningFinalizedAt, CreatedBy, HandlerBaseCommit / HandlerHeadCommit, InteractiveSessionId, Number (global immutable human-readable alias).
    • Status / PlanningPhase / BlockedByTaskId / DependsOnTaskId 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.
    • 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.
    • Number (INTEGER NOT NULL, unique index idx_tasks_number) — a global, monotonically increasing display alias for tasks. Never reused: deleting a task leaves a gap in numbering, ensuring that a task number that appears in a log or report always points to the same task (if it exists). Allocated by TaskNumberAllocator.AddWithNumberAsync on every insert via a persistent counter (app_settings.next_task_number). Why not MAX(number) + 1: deleting the highest-numbered task would free its number for reuse, breaking the immutability guarantee. The allocator uses an UPDATE…RETURNING statement to claim numbers atomically, retrying up to 5 times if a uniqueness collision occurs (the counter advances on each attempt, so retries always allocate fresh numbers). ⚠️ Tests use EnsureCreated, which bypasses migrations — the backfill needs a migration that runs Migrate() explicitly (not tested by default; see worker-task-pipeline notes).
    • 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.
  • 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.
  • 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.
NextTaskNumber next_task_number 1 Persistent counter for TaskEntity.Number allocation. Incremented atomically in TaskNumberAllocator.AddWithNumberAsync on every task insert. Monotonic only: the counter itself advances across all tasks (no per-list numbering), and it never resets. A deleted task leaves a gap.

All four usage percentages are clamped 0..100 by AppSettingsRepository.UpdateAsync. Gate/throttle semantics → usage-monitoring.

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.
  • ModelRegistryNormalizeAlias 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 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. ⚠️ Status-mutation primitives (MarkRunningAsync/MarkDoneAsync/MarkFailedAsync/FlipAllRunningToFailedAsync) are internal — only the Worker's TaskStateService may call them. CreateChildAsync produces children with Status=Idle, PlanningPhase=None. Task-number allocation: AddAsync (line 20) and CreateChildAsync (line 276) are the two sole task-creation paths; both call TaskNumberAllocator.AddWithNumberAsync to allocate a number atomically during the same transaction as the insert. Every other creation path (UI, MCP add_task, batch_add_tasks, Online Inbox sync, merge-helper handler tasks, planning children) routes through one of these two.
  • ListRepository — CRUD, GetConfigAsync/SetConfigAsync (upsert)/DeleteConfigAsync for list_config
  • WorktreeRepository — CRUD, UpdateHeadAsync, SetStateAsync, SetMergedAsync (atomically sets State=Merged and stamps MergeCommit in one update — the only writer of MergeCommit)
  • TaskAttachmentRepositoryAddAsync, 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; 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 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 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 (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), blocked_by_task_id (FK to tasks.id, ON DELETE SET NULL), and depends_on_task_id (same FK shape, but a separate column — see Worker/CLAUDE.md → Status Model for why it isn't unified with blocked_by_task_id).

Conventions

  • 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