cc-os/plugins/os-doc-hygiene/openspec/changes/add-deterministic-core/tasks.md

5.9 KiB

Tasks: Add Deterministic Core

Execution shape: Group 1 (build-spike) is a sequential gate — it MUST complete and be visually confirmed in a real session before any other group starts. Once it lands, Groups 2 (scanner) and 3 (state store) are independent and run in parallel (no shared state). Group 4 (reminder hook) is sequential after Group 3 — it reads and writes last_reminded through the state store. Group 5 (cross-seam verification) runs last. Model routing: all scripts are deterministic (no model); per CLAUDE.md, write them with Haiku.

1. Build-Spike — SessionStart banner (SEQUENTIAL GATE, do FIRST)

  • 1.1 Author a trivial hooks/hooks.json at the plugin root wiring a SessionStart hook (matcher: startup|resume) that emits a fixed systemMessage banner, with timeout ≤ 5s and exit 0.
  • 1.2 Acceptance criterion (implementation-time): start a real Claude Code session with the plugin enabled and visually confirm the banner renders to the user. The rest of this change does not start until this is confirmed.
  • 1.3 Record the confirmed hook entry format (plugin-root hooks/hooks.json, nested hooks array, systemMessage field) so Group 4 reuses it. This is the first CC hook in the collection — capture the working shape.

2. Scanner (doc-scanner) — parallel with Group 3 after the gate

  • 2.1 Implement a Scanner class with injected filesystem/root and scope globs; walk scoped files under the resolved project root. No model.
  • 2.2 Implement the exclusion pipeline, applied before signal computation and short-circuiting: (a) dir-prune scan.excluded_dirs (build, vendor, archive, graphify-out, .dochygiene) at the directory level, including self-exclusion of .dochygiene/; (b) .dochygiene-ignore pattern match; (c) hygiene: frozen frontmatter; (d) append-only-log detection.
  • 2.3 Implement deterministic per-path signal computation (objective facts only: broken references, version skew, edit-recency vs git churn, location, append-only growth, archive-to-live ratio, frontmatter markers).
  • 2.4 Emit the intermediate artifact (shortlist + per-path signals) as structured JSON with a correct exit code. Do NOT emit entries, category, op, token_estimate, or a machine report.
  • 2.5 Tests — exclusions (invariant #9): fixtures proving a hygiene: frozen file, a .dochygiene-ignore-matched file, and an append-only log (positive + negative fixture) are each present in the tree and absent from the shortlist; and that .dochygiene/ and excluded dirs are never scanned.
  • 2.6 Tests — signals: one fixture per signal type asserts the expected signal appears on the right path with no classification attached.

3. State store (state-store) — parallel with Group 2 after the gate

  • 3.1 Implement resolve_project_root(start_dir) — nearest ancestor .git, fallback cwd — as a pure function over injected filesystem.
  • 3.2 Implement a StateStore class confining all writes to <project_root>/.dochygiene/; create the dir as needed; maintain no global index; never open or edit .gitignore.
  • 3.3 Implement lifecycle timestamp read/write (last_check, last_clean, last_reminded) in state.json; absent timestamps read as unset.
  • 3.4 Implement atomic writes (write-temp → fsync → os.replace) for state.json and report files.
  • 3.5 Implement report rollover: delete any prior .md + .json report pair before writing the new pair; exactly one pair retained (invariant #4).
  • 3.6 Tests — root resolution (invariant #3): git-root case, cwd-fallback case, and that no path outside the resolved root is written; assert .gitignore is never edited.
  • 3.7 Tests — atomic write & rollover (invariants #3, #4): torn-write safety (reader sees old-or-new, never partial); two sequential report writes leave exactly one latest pair on disk.
  • 3.8 Tests — timestamps: round-trip read/write with an injected clock; absent timestamp reads as unset.

4. Reminder hook (session-reminder) — SEQUENTIAL after Group 3

  • 4.1 Replace the spike's fixed banner: implement a Reminder decision pure over (last_check, last_reminded, now, threshold) per design D5 — silent when fresh; banner when stale and not snoozed; snooze keyed on calendar day via last_reminded.
  • 4.2 Implement the reminder script: read state via the Group 3 store with an injected clock; on banner, emit systemMessage (naming the slash command) and write last_reminded = now; on silent, write nothing; always exit 0; treat missing/unreadable state as never-checked (stale). No scan, no model (invariant #1).
  • 4.3 Update hooks/hooks.json to invoke the reminder script (reusing the confirmed entry format from 1.3), keeping timeout ≤ 5s.
  • 4.4 Tests — injected clock (invariants #1, #2): fresh → silent; stale + never-reminded → banner; stale + reminded same calendar day → silent (covers repeated startup/resume within a day); stale + reminded prior day → banner; assert no file is mutated except last_reminded, and only on banner.

5. Cross-seam verification & wiring

  • 5.1 Confirm no seam imports or invokes a model and the scanner makes no network/API call (invariant #6 review).
  • 5.2 Confirm the scanner output is the intermediate artifact (no entries, no report) and references the frozen report-schema shapes only as consumed.
  • 5.3 Generate a CONTEXT.md for scripts/ (progressive disclosure) indexing the reminder, scanner, and state-store modules.
  • 5.4 Run openspec validate add-deterministic-core --strict and confirm it passes.