@nurix/etna 0.19.92 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/bin/etna.js +0 -11
  2. package/data/catalog/rules/frontend.md +1 -1
  3. package/data/catalog/rules/general.md +3 -3
  4. package/data/catalog/rules/nomenclature.md +0 -4
  5. package/data/catalog/rules/pnpm.md +1 -1
  6. package/data/catalog/rules/testing.md +1 -0
  7. package/data/harness-index.json +1 -1
  8. package/data/harnesses/code/CLAUDE.md +14 -14
  9. package/data/harnesses/code/agents/bookkeeper.md +3 -0
  10. package/data/harnesses/code/agents/maintenance.md +3 -14
  11. package/data/harnesses/code/agents/performance-review.md +1 -1
  12. package/data/harnesses/code/codex/agents/performance-review.toml +1 -1
  13. package/data/harnesses/code/harness.json +1 -1
  14. package/data/harnesses/code/hooks/awareness.mjs +5 -169
  15. package/data/harnesses/code/hooks/compliance.mjs +21 -17
  16. package/data/harnesses/code/hooks/lib/ledger.mjs +36 -13
  17. package/data/harnesses/code/hooks/lib/sweep.mjs +172 -0
  18. package/data/harnesses/code/rules/frontend.md +1 -1
  19. package/data/harnesses/code/rules/general.md +3 -3
  20. package/data/harnesses/code/rules/nomenclature.md +0 -4
  21. package/data/harnesses/code/rules/pnpm.md +1 -1
  22. package/data/harnesses/code/rules/testing.md +1 -0
  23. package/data/harnesses/code/skills/boundary/SKILL.md +1 -1
  24. package/data/harnesses/code/skills/documentation/SKILL.md +1 -1
  25. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +1 -1
  26. package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +12 -8
  27. package/data/harnesses/code/skills/documentation/templates/plan.md +9 -16
  28. package/data/harnesses/code/skills/testing/SKILL.md +1 -2
  29. package/data/harnesses/delivery/agents/maintenance.md +3 -14
  30. package/data/harnesses/delivery/harness.json +1 -1
  31. package/data/harnesses/delivery/hooks/generate-ownership-index.mjs +3 -1
  32. package/data/harnesses/delivery/rules/nomenclature.md +0 -4
  33. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -1
  34. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +1 -1
  35. package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +12 -8
  36. package/data/harnesses/delivery/skills/documentation/templates/plan.md +9 -16
  37. package/data/harnesses/etna/agents/maintenance.md +3 -14
  38. package/data/harnesses/etna/agents/performance-review.md +1 -1
  39. package/data/harnesses/etna/codex/agents/performance-review.toml +1 -1
  40. package/data/harnesses/etna/harness.json +1 -1
  41. package/data/harnesses/etna/hooks/generate-ownership-index.mjs +3 -1
  42. package/data/harnesses/etna/rules/general.md +3 -3
  43. package/data/harnesses/etna/rules/nomenclature.md +0 -4
  44. package/data/harnesses/etna/rules/testing.md +1 -0
  45. package/data/harnesses/etna/skills/boundary/SKILL.md +1 -1
  46. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -1
  47. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +1 -1
  48. package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +12 -8
  49. package/data/harnesses/etna/skills/documentation/templates/plan.md +9 -16
  50. package/lib/codex/agents-md.mjs +1 -35
  51. package/lib/codex/hooks.mjs +0 -29
  52. package/lib/install/materialize.mjs +4 -34
  53. package/lib/install/plan.mjs +54 -10
  54. package/package.json +3 -3
  55. package/data/harnesses/code/hooks/check-bookkeeping-clean.mjs +0 -28
  56. package/data/harnesses/code/hooks/check-changelog-presence.mjs +0 -36
  57. package/data/harnesses/code/hooks/check-completion-integrity.mjs +0 -72
  58. package/data/harnesses/code/hooks/check-ownership.mjs +0 -55
  59. package/lib/codex/index.mjs +0 -63
  60. package/lib/codex/plugin-manifest.mjs +0 -42
  61. package/lib/codex/subagents.mjs +0 -26
package/bin/etna.js CHANGED
@@ -120,10 +120,6 @@ async function installHarness(harnessName, harnesses) {
120
120
  // (the `files` and `seeds` lists) is read by materializeHarness, which owns placement.
121
121
  const harnessMeta = readJson(join(bundledHarnessDir, 'harness.json'));
122
122
  const versionTag = harnessMeta.version ? ` v${harnessMeta.version}` : '';
123
- // The underlying plugin name — may differ from the harness name, and several harnesses may
124
- // share it. Used only for legacy marketplace-entry cleanup.
125
- const pluginName = harnessMeta.name || harnesses[harnessName]?.plugin || harnessName;
126
-
127
123
  const spin = p.spinner();
128
124
  spin.start(`Installing ${harnessName}${versionTag} into ${targetDir}`);
129
125
 
@@ -132,8 +128,6 @@ async function installHarness(harnessName, harnesses) {
132
128
  result = materializeHarness({
133
129
  bundledHarnessDir,
134
130
  targetDir,
135
- pluginName,
136
- projectHasGit: existsSync(join(targetDir, '.git')),
137
131
  });
138
132
  } catch (error) {
139
133
  spin.stop(`Could not install ${harnessName}`);
@@ -319,11 +313,6 @@ async function main(args) {
319
313
  process.exit(1);
320
314
  }
321
315
 
322
- const boot = p.spinner();
323
- boot.start('Starting up');
324
- await sleep(2000);
325
- boot.stop('Ready');
326
-
327
316
  // Transition alias: the `apollo` harness merged into `code` (the code harness is pre-baked —
328
317
  // it ships the docs scaffold as seeds and auto-baselines on first contact). Scripts and docs
329
318
  // that pin the old name keep working through the alias; it retires after one window.
@@ -29,7 +29,7 @@ Every package compiles under `strict: true` and `noUncheckedIndexedAccess: true`
29
29
 
30
30
  - **Never** `any` to silence the checker. Unknown value → type `unknown`, then narrow (type guard / `zod` parse).
31
31
  - **Never** weaken strictness: no `// @ts-nocheck`; `// @ts-expect-error` only with a same-line reason.
32
- - Infer locals and return types; annotate at boundaries (exports, props, contracts).
32
+ - Annotate at boundaries (exports, props, contracts).
33
33
 
34
34
  ### Generics
35
35
 
@@ -19,11 +19,11 @@ How a package's `src/` is laid out. A single-runtime package drops `server/` and
19
19
 
20
20
  **Never `core/`** — its membership test collapses to "is it important?", making it the queue for files nobody could place; a bare `lib/` holding everything non-visual fails the same way, which is what `shared/` prevents. A route's own nested `lib/` is private and is **never** hoisted, and **dependencies point down** — a shared folder importing one route folder is a defect.
21
21
 
22
- **A folder earns its place at ~4+ files, or by being a boundary somebody could delete whole.** Below that, inline it: a folder around one file is a longer filename, and a folder holding only another folder is cascade with nothing at it. Density — not depth, not root breadth — is what makes a tree read as sprawling. The boundary clause splits the one-file cases: a one-file route folder stays, a one-file component folder goes.
22
+ **A folder earns its place at ~4+ files, or by being a boundary somebody could delete whole.** Below that, inline it. The boundary clause splits the one-file cases: a one-file route folder stays, a one-file component folder goes.
23
23
 
24
- **Never judge dead code by a name grep** — a relative import never contains its own parent's path, so `grep "x/brand"` cannot see `from "./brand/Detail"`, and every folder imported only by its parent reads as dead. Test by transitive reachability from the real entry points, resolving specifiers as the bundler does (`.ts`, `.tsx`, `.css`, `/index.ts`; strip `?raw`/`?url`; map a `.js` specifier onto its `.ts` source), running the test tree as a **second, separate** entry set — reachable from tests alone means a suite pinning code nothing ships, and both go together. Ambient `.d.ts` files enter through tsconfig `include`, so a sweep always reports them dead. A retired mechanism spans more than source — grep the build config for the same concept before calling a removal complete. **Unreachable ≠ unwanted**: a routed surface rendering dummy data is reachable and still not product, and retiring it removes a route, which is a product decision.
24
+ **Never judge dead code by a name grep** — a relative import never contains its own parent's path, so every folder imported only by its parent reads as dead. Test by transitive reachability from the real entry points, resolving specifiers as the bundler does (`.ts`, `.tsx`, `.css`, `/index.ts`; strip `?raw`/`?url`; map a `.js` specifier onto its `.ts` source), with the test tree as a **second, separate** entry set — reachable from tests alone is a suite pinning code nothing ships. Ambient `.d.ts` files enter through tsconfig `include`, so a sweep always reports them dead; grep the build config for a retired mechanism before calling its removal complete. **Unreachable ≠ unwanted**: retiring a routed dummy surface removes a route — a product decision.
25
25
 
26
- **Reshape an existing tree largest-lever-first**, verifying typecheck plus the full suite between steps: purge unreachable files → collapse single-occupancy and pass-through folders → push domain-coupled components down → name the shared layer (the only step that adds a top-level name) → optionally add a path alias, which makes depth invisible at the import site and dissolves most "flatten it" instincts. **Delete and promote before you move** — a folder whose name misdescribes its contents just relocates the lie.
26
+ **Reshape an existing tree largest-lever-first**, verifying typecheck plus the full suite between steps: purge unreachable files → collapse single-occupancy and pass-through folders → push domain-coupled components down → name the shared layer (the only step that adds a top-level name) → optionally add a path alias (depth becomes invisible at the import site). **Delete and promote before you move** — a folder whose name misdescribes its contents just relocates the lie.
27
27
 
28
28
  ## Runtime Versions
29
29
 
@@ -20,11 +20,7 @@ Names state intent in full words — clarity over brevity; no abbreviations unle
20
20
 
21
21
  | Identifier | Shape | Example |
22
22
  | --- | --- | --- |
23
- | Variable | noun phrase | `paymentStatus` |
24
- | Function / method | verb phrase naming the action | `validateInput` |
25
23
  | Boolean | `is` / `has` / `can` / `should` prefix — **never** a bare adjective | `hasPermission` |
26
24
  | Array / collection | plural noun | `errorMessages` |
27
- | Class / component | singular noun phrase | `RequestHandler` |
28
25
  | Event handler | `handle` or `on` prefix | `handleSubmit`, `onPageLoad` |
29
26
  | Interface | `I` prefix acceptable; type aliases stay plain | `IConfigOptions`, `AuthResponse` |
30
- | Async function | verb phrase; `Async` suffix acceptable | `loadConfigAsync` |
@@ -1,6 +1,6 @@
1
1
  # pnpm — `@nurix/*` Workspaces: naming, boundaries, and consuming
2
2
 
3
- How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here.
3
+ How a `@nurix/*` repo is **named**, **structured**, and how its packages are **consumed** — every repo takes the workspace shape, single-package frontends included. Publishing and CLI design live in the `nustack` skill, not here — no `nustack` skill in the runtime ⇒ ask before crossing the publish boundary.
4
4
 
5
5
  ---
6
6
 
@@ -32,6 +32,7 @@ Rules for the plan regime between the plan gate and the verification gate:
32
32
  - **Precondition: the ledger exists.** A plan with no Test Obligations section is blocked at the gate — run the `testing` skill (plan-gate mode) on it first; an empty ledger passes vacuously and the gap surfaces only after the file is frozen.
33
33
  - **The verdict is never delegated** (the root policy file § Delegation), and where browser work is main-session-only (its § Runtime Notes), the driving is too. Fan-out may prepare fixtures or draft test bodies; the driving and the verdict stay inline.
34
34
  - **The gate needs no stronger tier than the session default** — a standard-tier session suffices whatever tier built the plan; the plan and its ledger carry all the state the gate needs, so gating in a fresh session loses nothing.
35
+ - **Browser driving is the gate's final rung.** Playwright / Chrome CDP drives run once, at the end, after suites and cheap oracles are green — never interleaved mid-loop; they are the most expensive oracle in time and tokens.
35
36
  - **Report what happened, including failures.** A gate that reports green while a suite is red is worse than no gate; state the failing test and its output.
36
37
  - **Abandonment does not erase debt.** Before `Status: Abandoned` freezes a plan whose code was committed, convert its undischarged rows into backlog items; uncommitted code owes nothing.
37
38
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "a1cf8eb35f57",
2
+ "fingerprint": "067206dbe15a",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -33,14 +33,13 @@ Three peer modes, then a verification gate and a Completion step — enter where
33
33
 
34
34
  > Brainstorm entry needs a **basis** — a reference document or a one-shot brain dump; drip-sized asks (a bug fix, one button) are too small and get redirected. A **pillar** is a recorded strategic principle from a past brainstorm: in-progress sessions live under `docs/research/`, harvested ones under `docs/history/brainstorms/`, harvested doctrine under `docs/architecture/`. The LLD is the feature doc itself — `docs/features/{feature}.md`, design intent first, extended in place at Completion.
35
35
 
36
- > **The lightweight lane:** a change touching one module, no cross-module contract surface, and under about a day's work skips the modes above entirely — the ad-hoc regime of `.claude/rules/testing.md` applies: make the change, pay its test immediately if it alters promised behavior, cover it with a changelog row.
36
+ > **The lightweight lane:** a change touching one module, **at most ~10 files, none on a contract-heavy surface** (db schema, wire envelopes, auth, events, env/config — the `boundary` skill's list), and under about **three days'** work skips the modes above entirely — the ad-hoc regime of `.claude/rules/testing.md` applies: make the change, pay its test immediately if it alters promised behavior, cover it with a changelog row. The predicate is mechanical; when in doubt, the `boundary` diff scan is the tiebreak, not self-assessment.
37
37
 
38
38
  **On Plan Completion:**
39
39
 
40
- 1. **The verification gate passes first** — user-triggered: when implementation completes, report it and offer the gate; never run it unbidden. It requires the plan's Test Obligations ledger to exist (proof the plan gates ran) and discharges every row, driving the real surface before pinning tests — `.claude/rules/testing.md`. A plan with outstanding obligations stays open, whatever the code does.
41
- 2. Freeze the plan — `Status: Complete`, fill the Completed date. The file is now frozen.
42
- 3. **Discharge the plan's Doc Obligations ledger — every row, into exactly one outcome**: **updated** (the doc now states what the code does), **owes nothing** (with the reason), or **blocked** (→ a backlog item, so the debt outlives the frozen plan). For each row's doc: flip `status: active` if it was `draft`, extend it in place with the how-it-works sections, and confirm its `owns:` globs against the actual code. A plan spanning three features discharges three rows — the ledger, not memory, says how many.
43
- 4. Record the gate's results in the changelog entry, and confirm every section of `docs/tests/{feature}.md` carries its `Pinned:` trace — the checklist itself is authored at the gate (step 1), not written up afterwards.
40
+ 1. **The verification gate passes first** **[override]** — user-triggered: when implementation completes, report it and offer the gate; never run it unbidden. This deliberately inverts the runtime's finish-the-task default: completion is the user's call. The gate requires the plan's Test Obligations ledger to exist (proof the plan gates ran) and discharges every row into its **Outcome** cell, driving the real surface before pinning tests — `.claude/rules/testing.md`. A plan with outstanding obligations stays open, whatever the code does.
41
+ 2. **Discharge, then freeze — in one edit.** Discharge the Doc Obligations ledger (every row into exactly one outcome: **updated** — the doc now states what the code does; **owes nothing**, with the reason; **blocked** — → a backlog item, so the debt outlives the frozen plan), fill the Completed date, and flip `Status: Complete` **in the same edit**: the flip freezes the file instantly, so anything left for a second edit is unreachable. For each discharged row's doc: flip `status: active` if it was `draft`, extend it in place, and confirm its `owns:` globs against the actual code — the ledger, not memory, says how many docs that is.
42
+ 3. Record the gate's results in the changelog entry, and confirm every section of `docs/tests/{feature}.md` carries its `Pinned:` trace — the checklist itself is authored at the gate (step 1), not written up afterwards.
44
43
 
45
44
  **On Brainstorm Close** (when a session ends, not when a plan completes): close + harvest is the skill's `stop` procedure — converge, a user-approved harvest manifest, apply, then a uselessness test; conclusions disperse by scope (architecture docs, decision records, backlog), the record moves from `docs/research/{topic}-brainstorm/` to `docs/history/brainstorms/{topic}/`, and the user decides whether the moved folder is deleted or committed.
46
45
 
@@ -52,7 +51,7 @@ These four mechanisms work in tandem to keep the repo self-updating and to preve
52
51
 
53
52
  ### Ownership — route before editing
54
53
 
55
- Which doc is canonical for which code paths is the **feature doc's own frontmatter**. A feature doc (flat in `docs/features/`) carries `owns:` (code-path globs — the only ownership key), `module:` (a bounded-context label, omitted when cross-cutting), `keywords:` (discovery), and `status:`. Code → doc ownership is the union of every feature doc's `owns:` globs, surfaced as the generated ownership index at session start. A module is tied to code by the `owns:` globs of the docs carrying its `module:` label — not a folder, not a separate map. Architecture docs describe cross-cutting patterns and own no files; operations docs describe procedures. Every doc carries `type:` — its genre, stamped by document generators or at authoring, read by tooling (UI grouping, protected-file mapping); the `maintenance` agent stamps it where missing. Like `last-reviewed:`, it is tooling metadata: preserve it on edits, never inject or narrate it.
54
+ Which doc is canonical for which code paths is the **feature doc's own frontmatter**. A feature doc (in `docs/features/`) carries `owns:` (code-path globs — the only ownership key), `module:` (a bounded-context label, omitted when cross-cutting), `keywords:` (discovery), and `status:`. Code → doc ownership is the union of every feature doc's `owns:` globs, surfaced as the generated ownership index at session start. A module is tied to code by the `owns:` globs of the docs carrying its `module:` label — not a folder, not a separate map. Architecture docs describe cross-cutting patterns and own no files; operations docs describe procedures. Every doc carries `type:` — its genre, stamped by document generators or at authoring, read by tooling (UI grouping, protected-file mapping); the `maintenance` agent stamps it where missing. Like `last-reviewed:`, `rev:`, and `slug:`, it is tooling metadata: preserve it on edits, never inject or narrate it.
56
55
 
57
56
  Before modifying code in an unfamiliar area:
58
57
 
@@ -65,11 +64,11 @@ The `lld-mode` agent checks for ownership conflicts during LLD research — feat
65
64
 
66
65
  Boundaries are **diagnosed, not documented** — the `boundary` skill derives them fresh from source on every run (the definition and mechanism catalog live in the skill), so nothing is persisted that could go stale.
67
66
 
68
- **Invoke the `boundary` skill after a plan is approved and before its first implementation step.** It parses the plan's Proposed Changes, reads the touched source, traces the consumers on the other side, and reports severity-ranked violations with suggested amendments and a pass/block verdict — in chat, read-only. Fold the amendments into the plan before coding. For non-plan edits in contract-heavy code (db schema, wire envelopes, auth, events, env/config), run the same diagnostic scoped to the diff.
67
+ **Invoke the `boundary` skill after a plan is approved and before its first implementation step.** It parses the plan's Proposed Changes, reads the touched source, traces the consumers on the other side, and reports severity-ranked violations with suggested amendments and a pass/block verdict — in chat, read-only except its one write: its own segment of the plan's **`Gates:`** line. That line is machine-read — the compliance hook blocks implementation edits inside the plan's scope while it reads `Boundary: pending` — so this gate is enforced, not ceremony. Fold the amendments into the plan before coding. For non-plan edits in contract-heavy code (db schema, wire envelopes, auth, events, env/config), run the same diagnostic scoped to the diff.
69
68
 
70
69
  ### Test obligations — specify before implementing
71
70
 
72
- **After `boundary` passes and its amendments are folded, invoke the `testing` skill (plan-gate mode)** — same plan gate, ordered second. It derives the tests the plan owes from its Proposed Changes and the owning LLD, and writes the **Test Obligations** ledger into the plan (one row per promised behavior: layer · target file · oracle · fixtures). It writes no test files — the ledger is what the verification gate discharges, and because it lives in the plan file it survives dispatch to an executor that sees only the plan. On a boundary **BLOCK** it does not run — re-plan first. Layer rules and the e2e altitude gate live in the skill.
71
+ **After `boundary` passes and its amendments are folded, invoke the `testing` skill (plan-gate mode)** — same plan gate, ordered second. It derives the tests the plan owes from its Proposed Changes and the owning LLD, and writes the **Test Obligations** ledger into the plan (one row per promised behavior: layer · target file · oracle · fixtures · an empty **Outcome** cell the verification gate fills). It writes no test files — the ledger is what the verification gate discharges, and because it lives in the plan file it survives dispatch to an executor that sees only the plan. On a boundary **BLOCK** it does not run — re-plan first. Layer rules and the e2e altitude gate live in the skill. **The Doc Obligations table is the author's, filled at the same gate** — a lookup against the ownership index, not a skill run; the hook blocks implementation edits while either section is missing.
73
72
 
74
73
  ### Verification — prove before completing
75
74
 
@@ -103,7 +102,7 @@ When recording a learning, ask: _is this true of this project, or true of this u
103
102
 
104
103
  ## Changelog
105
104
 
106
- The working session is the last holder of the refinement context that produced a change — asks, iterations, rejections, discoveries; the changelog captures it, and the `retro` agent distills it on demand into doc, skill, and pin feedback. When an entry is owed, its lifecycle, its granularity, and the migration-runbook genre are the `changelog` rule; format and required fields are the `documentation` skill's `templates/changelog-entry.md`. A plan reaching `Status: Complete` writes its entry as a **draft** immediately, while the context is fresh; **immutability begins at commit, not creation** — a committed entry is never edited, a correction is a new entry that links back.
105
+ The working session is the last holder of the refinement context that produced a change — asks, iterations, rejections, discoveries; the changelog captures it, and the `retro` agent distills it on demand into doc, skill, and pin feedback. Everything else — when an entry is owed, lifecycle, immutability, granularity, the migration-runbook genre — is the `changelog` rule; format and required fields are the `documentation` skill's `templates/changelog-entry.md`.
107
106
 
108
107
  ---
109
108
 
@@ -113,13 +112,13 @@ When a plan's implementation surfaces work that is important but outside the pla
113
112
 
114
113
  - Append it to `docs/history/backlog.md` under the appropriate priority (P0-P3)
115
114
  - Format: `- **[P1]** Description — discovered by [plan-name or agent-name]`
116
- - Do NOT ask permission. Just append and mention it in the reply.
115
+ - **[override]** Do NOT ask permission — append and mention it in the reply. This deliberately overrides the runtime's do-only-what-was-asked default: losing a discovered defect costs more than an unrequested backlog line.
117
116
 
118
117
  ---
119
118
 
120
119
  ## Delegation
121
120
 
122
- The single policy for sub-agent dispatch — when to fan out, at what tier, and how far to trust the result. Component-internal dispatch mechanics (the foreman loop, the brainstorm twin, batch classification) stay in their components and defer here for anything this section covers. No dispatch mechanism ⇒ inline.
121
+ The single policy for sub-agent dispatch — when to fan out, at what tier, and how far to trust the result. Component-internal dispatch mechanics (the brainstorm twin, batch classification) stay in their components and defer here for anything this section covers. No dispatch mechanism ⇒ inline.
123
122
 
124
123
  - **Delegate to keep bulk out of the main context.** Wide file reads, sweeps, broad research, audits, fixture prep — work whose raw tool output the main session doesn't need — defaults to a sub-agent: output a sub-agent reads dies with it, while output read inline is re-read and re-billed on every later turn. The hand-off is self-contained (the sub-agent sees none of the conversation), so it must still cost less than the work; run independent delegations in parallel.
125
124
  - **Small or judgment work stays inline.** A task finishable in a handful of tool calls is faster done than briefed. **Never delegate judgment by reflex** — a second opinion is for a high-stakes call or an explicit request.
@@ -133,13 +132,14 @@ The single policy for sub-agent dispatch — when to fan out, at what tier, and
133
132
 
134
133
  Defaults — ❌ prohibitions hold unless the user explicitly asks otherwise; ✅ and ⚠️ items are standing policy:
135
134
 
136
- - ✅ **Verification — batched at a gate, not continuous.** Only a plan defers tests; bug fixes and ad-hoc changes that alter promised behavior pay theirs immediately. The gate, regimes, frameworks, and placement are `.claude/rules/testing.md`; the procedure is the `testing` skill (gate mode) — the repo's existing runner and layout always win.
135
+ - ✅ **Verification** — batched at the gate, never continuous; the regimes and the gate are `.claude/rules/testing.md`, the procedure the `testing` skill (gate mode).
137
136
  - ❌ **Dependencies** – Never upgrade pinned packages without asking; security patches and test-tooling installs at `latest` (`testing.md`) are exempt.
138
137
  - ❌ **Compatibility** – Never maintain backward compatibility unless specified.
139
138
  - ❌ **Branches & Worktrees [override]** – Never self-create git branches or worktrees; commit straight to the checked-out branch. This replaces any built-in "branch first on the default branch" default. Exception: when the user asks for a pull request, create the branch the PR requires before committing its changes.
140
139
  - ❌ **Protected Files** – Once per session, if `docs/operations/protected-files.md` exists, read it and never modify any path it lists; never modify `AGENTS.md` (harness-regenerated) or `protected-files.md` itself.
141
140
  - ✅ **Browser Automation** – If the runtime provides browser automation, use it freely; no need to ask first. If it doesn't — or the executing agent can't hold the browser connection (see Runtime Notes) — return a browser-verification checklist (URLs, actions, expected outcomes) for the orchestrating session or the user to run.
142
- - ✅ **Bookkeeping** — hooks silently append doc-debt rows to `docs/operations/bookkeeping.md`; the commit hook blocks while any unresolved row **implicates a file the commit stages** — dispatch the `bookkeeper` agent to drain those rows. Rows about files the commit does not touch are whoever raised them's, and are not yours to drain; a row naming no path, a `needs-session:` row, an expired waiver, and a commit staging nothing block regardless. The queue is a queue, never an archive: a drained row is **deleted**, so a fully drained file holds no rows at all. **Every row is a cache of a condition, not a record of an event** — the session-start sweep re-tests each row against the repo and deletes the ones whose condition has died, `waived:` rows included, so the file empties itself as the work lands. What the sweep cannot re-test ages out instead: **a `waived:` row is unresolved once `expires=` passes**, which returns the wall and re-summons the bookkeeper to re-judge the reason. A waiver defers a finding; it never makes one immortal.
141
+ - ✅ **Bookkeeping** — hooks append doc-debt rows to `docs/operations/bookkeeping.md`; three walls read it at three widths (Stop = your session's rows · commit = rows implicating staged files · CI = all), each re-verifying conditions at gate time. Dispatch the `bookkeeper` for your rows; a drained row is **deleted**. The full grammar arrives in the deny message when a wall fires.
142
+ - ✅ **Session gate log** — a denial with no `resolved` line blocks Stop; resolution mechanics arrive in the block message. Never delete log lines.
143
143
  - ✅ **Cleanup removes the cause, not the record of it.** When the user asks for something to be deleted or cleaned up, the ask is for the underlying condition to be gone. Deleting the record while the condition survives is worse than doing nothing — it destroys the only evidence the debt exists. The order is fixed: **read what the record is masking** (a `waived:` row, an `UNBUILT:` marker, a `status:` flag, a lint suppression — each asserts something is fine; check whether that is still true), **fix the condition**, **then delete the record**. Reversing it backfires concretely: a deleted waiver's finding is re-appended by the detector as an `auto:` row, which *fails* the commit wall the `waived:` row passed.
144
144
  - ✅ **Delegation** — when to fan out, tiers, and trust are § Delegation.
145
145
  - ⚠️ **Plans** – Plans are created directly in `docs/history/plans/` (see Workflows). Once `Status: Complete` or `Status: Abandoned`, the file is frozen — never edited. Abandoning a plan whose code was committed first converts its undischarged Test Obligations into backlog items (the `testing` rule). If scope changes after completion, write a new dated plan that links back to the old one.
@@ -150,7 +150,7 @@ Defaults — ❌ prohibitions hold unless the user explicitly asks otherwise;
150
150
  ## File Structure
151
151
 
152
152
  - Add a file-path header comment at the top of each source file, in the file's native comment syntax — `// src/modules/example.js - Module for handling user authentication`. Place it below any shebang line; skip formats with no comment syntax (JSON, lockfiles) and generated files.
153
- - Add a 1-line description above each function whose purpose isn't fully evident from its name and signature: `// Extract JWT token from request based on endpoint type`. A comment that restates the name is noise. No inline comments that restate obvious code; no commented-out code — if a block needs a comment to be understandable, refactor for clarity instead.
153
+ - Add a 1-line description above each function whose purpose isn't fully evident from its name and signature: `// Extract JWT token from request based on endpoint type`. No commented-out code — refactor for clarity instead.
154
154
 
155
155
  ---
156
156
 
@@ -22,6 +22,8 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
22
22
  | `doc-lint` | fix the finding, or add `<!-- doc-lint-disable: id -->` with a reason beside it when the flagged text is deliberate |
23
23
  | `index-row-missing` | add the doc's row to `docs/README.md` |
24
24
  | `doc-implicated` | read the diff of the named source file (`git diff` / `git log -p`), and update the owning doc **only where the diff contradicts documented behavior**; if nothing documented changed, the row's resolution is deleting it |
25
+ | `package-rejected` | re-check the named manifest: the package no longer declared → delete the row; still declared → `needs-session:` — changing a package decision is the user's call, never yours |
26
+ | `frozen-record-edited` | re-check `git diff HEAD -- {path}`: clean again → delete the row; still modified → `needs-session:` — never revert another session's work |
25
27
 
26
28
  2. **`note:` rows — expand strictly from the stated context.** The one-line context after the area is everything the session chose to hand you; land it in the destination the row names (`lands-in:` an owning doc, the backlog, or memory), written in that document's genre.
27
29
  3. **A thin note, same machine only** — derive the transcript from the row's `sid=` tail: the session JSONL lives at `~/.claude/projects/{project-dir-slug}/{sid}.jsonl` (the slug is the project path with `/` → `-`). Search it for the row's file path or area and read only that window — never the whole file. The sid is best-effort and dead on other clones — expected; treat an unreadable or missing transcript as "no window".
@@ -30,6 +32,7 @@ Ground every edit in this ladder, in order. Never invent content; a fact you can
30
32
  ## Rules
31
33
 
32
34
  - **One drain at a time — check the claim board first.** Before touching the queue, read `.claude/state/claims.log` (tab-separated: timestamp · start/end · activity · session id). An open `bookkeeping-drain` claim — a `start` with no later `end` — that is under 20 minutes old and belongs to a session whose registry row (`.claude/state/sessions.tsv`) points at a recently-modified transcript means another drain is mid-flight: stop and report that instead of draining. Otherwise append your own `start` line before the pass and an `end` line after it, so a second bookkeeper can see you and a waiting session can see you finish. A stale claim (older, or holder gone) is yours to note and proceed past.
35
+ - **A sibling's in-flight rows are theirs.** A row whose named file another live session is actively editing (the claim board / presence banner shows who) resolves at that session's own close — leave it and note it in your report as left-in-flight.
33
36
  - **Never edit frozen records** — committed changelog entries, `Status: Complete|Abandoned` plans, outcome-stamped runbooks. A finding that seems to require one gets a `needs-session:` row instead.
34
37
  - **Every touched doc goes through the `documentation` skill's rules** — present tense, two-natures, no history narration in truth documents.
35
38
  - **Resolution = deletion.** A finished row is removed from the queue, not annotated. Only `waived:` conversions (a reason plus `expires=YYYY-MM-DD`) and `needs-session:` rows survive your pass.
@@ -28,9 +28,7 @@ Decide the mode once, up front, from the version marker — then run.
28
28
  | behind on minor/patch | **probe first**, below |
29
29
  | equal to installed | **audit only** (the default spine) |
30
30
 
31
- **The probe.** Run the Step B2 detection pass read-only: if any old-shape artifact in the fold table is present, **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
32
-
33
- > **The version digit is a hint; the fold table is the authority.** A shape change ships in a minor whenever the capability behind it is additive — `docs/history/ledger/` dissolving into `docs/history/changelog/` arrived in a minor, as did `docs/history/migrations/` — so "minor ⇒ nothing to reshape" silently strands dissolved-shape artifacts in the repo forever.
31
+ **The probe.** Run the Step B2 detection pass read-only: if any `*.bkp.*` file sits under `docs/` (a fold awaiting its pass), **or** any **scaffolded file** in the target tree is missing (`docs/README.md`, `docs/structure.md`, `docs/history/CHANGELOG.md`, `docs/history/backlog.md`, `docs/memory/main.md`), the shape *did* change ⇒ **baseline mode**. Only when the probe finds nothing ⇒ re-stamp the version (no reshape), then audit.
34
32
 
35
33
  > **Never probe on a missing folder.** Git does not track empty directories, so an unused folder is absent on every clone — reading that as drift puts the repo in baseline mode *forever*. Only the five scaffolded files above are shape signals; every folder in the target tree is created on first use.
36
34
  3. **Precondition for baseline mode:** a root `CLAUDE.md` must exist. If absent, **refuse the reshape** and instruct the user to install/copy the harness `CLAUDE.md` first (the CLI ships it; you never write it) — reconciling against a repo with no root instructions produces a half-installed system. Detect-and-instruct, not a hard gate on the audit.
@@ -166,21 +164,12 @@ Detecting and folding old-shape artifacts is what makes a baseline more than a f
166
164
 
167
165
  | If you find (old shape) | Fold to (current shape) |
168
166
  | --- | --- |
169
- | a persisted `docs/routing.md` ownership map | migrate each File-Pattern row's ownership into the owning feature doc's `owns:` frontmatter (Pattern → an `owns:` glob; Module column → the doc's `module:` label), then **delete `docs/routing.md`** |
170
- | feature docs carrying `owns`/`touches`/`depends_on` legacy frontmatter | collapse to the single `owns:` key (drop `touches`/`depends_on`; their paths fold into `owns:` if the doc genuinely owns them, else drop) |
171
- | a `docs/modules/{m}/` tree | move each module's `features/` flat into `docs/features/` with the module name as a `module:` tag on each doc; fold module-local `pillars/`/prose into `docs/architecture/`; delete the `docs/modules/` tree |
172
- | repo-wide `docs/pillars/` | cross-module doctrine → `docs/architecture/`; delete the folder |
173
- | `docs/boundaries/` (legacy persisted boundaries) | delete — boundary analysis is a plan-time diagnostic (the `boundary` skill reads contracts fresh from source); fold any human-authored knowledge (external consumers, intent, accepted exceptions) into `docs/architecture/` |
174
- | `docs/OWNERSHIP_INDEX.md` (legacy derived ownership index) | delete — ownership is the union of feature `owns:` frontmatter, surfaced by the generated index |
175
- | `docs/history/ledger/YYYY-MM.md` (the Refinement Ledger, now merged into the changelog) | split each `### ` block into its own `docs/history/changelog/YYYY-MM-DD-{three-words}.md` entry — content preserved **verbatim**, heading promoted to `#`, three words naming the work; append one row per entry to `docs/history/CHANGELOG.md`; delete the `docs/history/ledger/` folder. Records are moved, never rewritten |
176
- | `docs/operations/guardrails.md` | fold durable contract notes into `docs/architecture/` |
177
- | `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
178
- | `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
179
- | a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
180
167
  | `*.bkp.*` files under `docs/` (the installer's first-contact backups of the project's pre-existing files) | **fold the content, then delete the backup.** Classify each backup's content by nature and land it in the current shape — a backed-up changelog's rows into `docs/history/CHANGELOG.md` and per-entry files, a backed-up structure/readme's real claims into the scaffolded truth doc, a backed-up feature doc's content merged into its sibling (or adopted whole if the sibling is a pristine seed). Content is folded, never discarded silently; what cannot be classified goes to the backlog with the backup kept. When the fold lands, delete the `.bkp` file |
181
168
  | `*.bkp.*` files at the repo root or in config (`CLAUDE.bkp.md`, `.prettierrc.bkp.json`, …) | **never merge these.** Top-level config is harness-owned by design; list each one to the user as their call, and leave the file in place |
182
169
  | no `harness-version` in `structure.md` | stamp it (Step B4) |
183
170
 
171
+ **The support window: three past minor versions.** The fold table covers shapes at most three minors behind the installed harness — which today means the rows above, because every older dissolved construct (`docs/routing.md`, `docs/modules/`, pillars, the Refinement Ledger, the ADR genre, legacy brainstorm folders, …) predates the window. An older shape is **reported, never improvised**: name what the probe found, point the user at etna's dated migration runbooks (`docs/history/migrations/` in the forge), and recommend a fresh baseline over an archaeological fold — a repo more than three minors behind re-baselines rather than migrates.
172
+
184
173
  ### Step B3 — Present the reconcile plan, then execute
185
174
 
186
175
  Table: `| File | Action | From | To | Reason |` (actions: `Move`, `Merge into {target}`, `Delete`, `Keep`, `Create`, `Fold`). For a fresh baseline this is mostly `Create`; for an upgrade it is mostly `Fold`/`Move`. Ask once: **"Approve this reconcile?"** — you own this interaction (the `documentation` skill is a library and has none).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: performance-review
3
- description: Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. Merges the former `dep-health` and `review` agents so the package tree and the code that uses it are audited together.
3
+ description: Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. The package tree and the code that uses it are audited together.
4
4
  tools: Read, Glob, Grep, Bash
5
5
  model: opus
6
6
  ---
@@ -1,6 +1,6 @@
1
1
  # dropped Claude tools: Read, Glob, Grep, Bash
2
2
  name = "performance-review"
3
- description = "Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. Merges the former `dep-health` and `review` agents so the package tree and the code that uses it are audited together."
3
+ description = "Combined dependency + code health audit. Part A audits the dependency supply chain — security CVEs, outdated packages, license compliance, abandoned packages, unwanted transitive dependencies, install size. Part B audits code-level health — bundle size, tree-shaking issues, performance anti-patterns, code-quality flags. Run weekly or before releases. The package tree and the code that uses it are audited together."
4
4
  developer_instructions = '''
5
5
 
6
6
  # Performance-Review Agent
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "f07032c97db1"
18
+ "digest": "aa73ece3da8a"
19
19
  }
@@ -31,8 +31,10 @@ import {
31
31
  recordTouched,
32
32
  } from "./lib/state.mjs";
33
33
  import { readSafe, relIfInside, today, SOURCE_EXT, commandRoot } from "./lib/repo.mjs";
34
+ import { sweepResolvedRows, docLint, hasHeaderComment } from "./lib/sweep.mjs";
34
35
  import { listPlans } from "./lib/plans.mjs";
35
36
  import { frozenStatusAtHead } from "./lib/frozen.mjs";
37
+ import { undischargedDocRows } from "./lib/ledger.mjs";
36
38
  import { approvedPackages, rejectedPackages } from "./lib/approved.mjs";
37
39
  import {
38
40
  recordSession,
@@ -103,128 +105,6 @@ function openPlanLines(projectDir) {
103
105
  });
104
106
  }
105
107
 
106
- // --- the re-verification sweep ---------------------------------------------------------------
107
- //
108
- // THE QUEUE IS DERIVED, NOT ACCUMULATED. Every finding here is recomputable from the repo, so a row
109
- // is a cache of a condition rather than a record of an event — and a cache nothing invalidates is
110
- // an archive. Before any wall reads the queue, each row is re-tested against the repo as it is NOW,
111
- // and a row whose condition has died is deleted.
112
- //
113
- // THIS RUNS OVER `waived:` ROWS TOO, WHICH IS THE POINT. A waiver defers a finding; it does not
114
- // make the finding immortal. Without this, waiving was strictly WORSE than leaving a row alone —
115
- // the one existing resolver filtered `kind === "auto"`, so waiving a self-clearing row opted it out
116
- // of its own return path forever. The condition, not the row's kind, decides.
117
- //
118
- // A kind with no verifier here is not swept — it ages out through `expires=` instead. Add a
119
- // verifier whenever a finding becomes mechanically checkable; silence is the only failure mode
120
- // this file has, so a verifier that is unsure must return false and leave the row standing.
121
- const ROW_VERIFIERS = {
122
- // "<rel> lacks its file-path header comment" — dead once the header is there (or the file is gone).
123
- "header-missing": (projectDir, body) => {
124
- const m = /·\s+header-missing\s+·\s+(\S+)\s+lacks/.exec(body);
125
- if (!m) return false;
126
- const rel = m[1];
127
- if (!fs.existsSync(path.join(projectDir, rel))) return true;
128
- return hasHeaderComment(projectDir, rel);
129
- },
130
-
131
- // "owns: glob `<glob>` in <doc> matches no files" — dead once the glob matches something again.
132
- "dead-glob": (projectDir, body, ctx) => {
133
- const m = /`([^`]+)`/.exec(body);
134
- if (!m) return false;
135
- const re = globToRegExp(m[1]);
136
- return ctx.repoFiles.some((f) => re.test(f));
137
- },
138
-
139
- // "<rel> is a frozen record and carries uncommitted modifications" — dead once it is committed.
140
- // Immutability begins at commit, so the finding exists only while the edit is still in flight.
141
- "frozen-record-edited": (projectDir, body) => {
142
- const m = /·\s+frozen-record-edited\s+·\s+(\S+)\s+is a frozen record/.exec(body);
143
- if (!m) return false;
144
- const rel = m[1];
145
- if (!fs.existsSync(path.join(projectDir, rel))) return true;
146
- const res = spawnSync("git", ["diff", "--quiet", "HEAD", "--", rel], { cwd: projectDir, timeout: 5000 });
147
- // status 0 = no uncommitted modifications. A git failure leaves the row standing.
148
- return res.status === 0;
149
- },
150
-
151
- // "<src> changed; owning doc <doc> may need an update" — dead once the doc is newer than the
152
- // source that implicated it, which is the same freshness test the drift report already applies.
153
- "doc-implicated": (projectDir, body) => {
154
- const m = /·\s+doc-implicated\s+·\s+(\S+)\s+changed;\s+owning doc\s+(\S+?)\s+may need/.exec(body);
155
- if (!m) return false;
156
- const [, src, doc] = m;
157
- const mtime = (rel) => {
158
- try {
159
- return fs.statSync(path.join(projectDir, rel)).mtimeMs;
160
- } catch (e) {
161
- return null;
162
- }
163
- };
164
- const docTime = mtime(doc);
165
- const srcTime = mtime(src);
166
- if (docTime === null) return false; // the doc is gone — a real finding, not a resolved one
167
- if (srcTime === null) return true; // the source is gone; nothing implicates the doc any more
168
- return docTime >= srcTime;
169
- },
170
-
171
- // "<rel> has N doc-lint finding(s)" — dead once a re-run comes back clean (or the doc is gone).
172
- "doc-lint": (projectDir, body) => {
173
- const m = /·\s+doc-lint\s+·\s+(\S+)\s+has/.exec(body);
174
- if (!m) return false;
175
- if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
176
- return docLint(projectDir, m[1]).length === 0;
177
- },
178
-
179
- // "<rel> is not linked from docs/README.md" — dead once the index names it (or it is gone).
180
- "index-row-missing": (projectDir, body) => {
181
- const m = /·\s+index-row-missing\s+·\s+(\S+)\s+is not linked/.exec(body);
182
- if (!m) return false;
183
- if (!fs.existsSync(path.join(projectDir, m[1]))) return true;
184
- return readSafe(path.join(projectDir, "docs", "README.md")).includes(path.basename(m[1]));
185
- },
186
-
187
- // "<rel> declares `<name>` (package-rejected)" — dead once the manifest drops the package
188
- // or the table stops rejecting it.
189
- "package-rejected": (projectDir, body) => {
190
- const m = /·\s+package-rejected\s+·\s+(\S+)\s+declares\s+`([^`]+)`/.exec(body);
191
- if (!m) return false;
192
- const [, rel, name] = m;
193
- let manifest;
194
- try {
195
- manifest = JSON.parse(readSafe(path.join(projectDir, rel)));
196
- } catch (e) {
197
- return !fs.existsSync(path.join(projectDir, rel)); // gone ⇒ resolved; unparseable ⇒ standing
198
- }
199
- const declared = { ...manifest.dependencies, ...manifest.devDependencies };
200
- if (!(name in declared)) return true;
201
- return !rejectedPackages(projectDir).has(name);
202
- },
203
-
204
- // Retired detector kinds — the harness no longer raises these, so a standing row caches a
205
- // condition nothing can re-derive. Swept unconditionally — but only machine-raised rows: a
206
- // note: or waived: row whose free text merely mentions the phrase is a human's, not ours.
207
- "doc-placement": (projectDir, body, ctx, row) => row.kind === "auto",
208
- "boundary-scan-suggested": (projectDir, body, ctx, row) => row.kind === "auto",
209
- };
210
-
211
- // Delete every row — `auto:` or `waived:` — whose condition no longer holds. Returns the count.
212
- export function sweepResolvedRows(projectDir, repoFiles) {
213
- const ctx = { repoFiles: repoFiles || listRepoFiles(projectDir) };
214
- return resolveRows(projectDir, (row) => {
215
- if (row.kind === "needs-session") return false; // a human judgment, never swept mechanically
216
- for (const [kind, verify] of Object.entries(ROW_VERIFIERS)) {
217
- if (!row.body.includes(kind)) continue;
218
- try {
219
- return verify(projectDir, row.body, ctx, row);
220
- } catch (e) {
221
- return false; // never delete a row because a verifier threw
222
- }
223
- }
224
- return false;
225
- });
226
- }
227
-
228
108
  // Compute drift facts once; write the full report, return the one-line banner.
229
109
  function driftBanner(projectDir) {
230
110
  const repoFiles = listRepoFiles(projectDir);
@@ -245,37 +125,12 @@ function driftBanner(projectDir) {
245
125
  return !!m && waiverCovers(queueRows, "dead-glob", m[1], today());
246
126
  });
247
127
 
248
- // Docs older than the code they own (doc mtime < newest owned-file mtime) — one stat per
249
- // repo file, cached across docs, instead of docs × files stats.
250
- const mtimes = new Map();
251
- const mtimeOf = (rel) => {
252
- if (!mtimes.has(rel)) {
253
- try {
254
- mtimes.set(rel, fs.statSync(path.join(projectDir, rel)).mtimeMs);
255
- } catch (e) {
256
- mtimes.set(rel, null);
257
- }
258
- }
259
- return mtimes.get(rel);
260
- };
261
- const staleDocs = [];
262
- for (const entry of docs) {
263
- const docTime = mtimeOf(entry.doc);
264
- if (docTime === null) continue;
265
- const res = entry.owns.map((g) => globToRegExp(g));
266
- const stale = repoFiles.some((file) => res.some((re) => re.test(file)) && (mtimeOf(file) ?? 0) > docTime);
267
- if (stale) staleDocs.push(entry.doc);
268
- }
269
-
270
128
  const report = [
271
129
  `# Drift report — generated ${new Date().toISOString()}`,
272
130
  "",
273
131
  `## Dead owns: globs (${dead.length}, ${dead.length - unwaivedDead.length} waived)`,
274
132
  ...dead.map((d) => `- ${d.glob} → ${d.doc}${unwaivedDead.includes(d) ? "" : " (waived)"}`),
275
133
  "",
276
- `## Docs older than owned code (${staleDocs.length})`,
277
- ...staleDocs.map((d) => `- ${d}`),
278
- "",
279
134
  `## Bookkeeping queue (${counts.unresolved} unresolved: ${counts.auto} auto · ${counts.note} note · ${counts.needsSession} needs-session · ${counts.expiredWaived} expired-waived; ${counts.waived} waived total, ${swept} swept as resolved this session)`,
280
135
  ].join("\n");
281
136
  try {
@@ -284,14 +139,14 @@ function driftBanner(projectDir) {
284
139
  /* report is a convenience */
285
140
  }
286
141
 
287
- if (dead.length === 0 && staleDocs.length === 0 && counts.unresolved === 0) return null;
142
+ if (dead.length === 0 && counts.unresolved === 0) return null;
288
143
  const waivedNote = dead.length
289
144
  ? unwaivedDead.length === 0
290
145
  ? " (all waived)"
291
146
  : ` (${unwaivedDead.length} UNWAIVED)`
292
147
  : "";
293
148
  return (
294
- `Drift: ${dead.length} dead owns: glob(s)${waivedNote} · ${staleDocs.length} doc(s) older than owned code · ` +
149
+ `Drift: ${dead.length} dead owns: glob(s)${waivedNote} · ` +
295
150
  `bookkeeping: ${counts.unresolved} unresolved row(s) — full report: .claude/state/drift-report.md`
296
151
  );
297
152
  }
@@ -372,21 +227,6 @@ function sessionStart(projectDir, payload = {}) {
372
227
  // PostToolUse — the silent detector
373
228
  // ---------------------------------------------------------------------------
374
229
 
375
- // Spawn the installed doc-lint over one file; returns finding lines (empty when absent/clean).
376
- function docLint(projectDir, rel) {
377
- const script = path.join(projectDir, ".claude", "skills", "documentation", "scripts", "doc-lint.mjs");
378
- if (!fs.existsSync(script)) return [];
379
- const res = spawnSync("node", [script, rel], { cwd: projectDir, encoding: "utf8", timeout: 5000 });
380
- const out = `${res.stdout || ""}`;
381
- return out.split("\n").filter((line) => /:\d+ \[(block|warn)\]/.test(line));
382
- }
383
-
384
- // True when a source file's first lines carry its repo-relative path (the header-comment rule).
385
- function hasHeaderComment(projectDir, rel) {
386
- const head = readSafe(path.join(projectDir, rel)).split("\n").slice(0, 5).join("\n");
387
- return head.includes(rel) || head.includes(path.basename(rel));
388
- }
389
-
390
230
  function detectorForFile(projectDir, rel, meta, rows) {
391
231
  const owners = owningDocs(projectDir, rel);
392
232
  recordTouched(projectDir, rel, owners.map((o) => o.doc), meta.sessionId);
@@ -404,11 +244,7 @@ function detectorForFile(projectDir, rel, meta, rows) {
404
244
  if (rel.startsWith("docs/history/plans/")) {
405
245
  const planText = readSafe(path.join(projectDir, rel));
406
246
  resolveRows(projectDir, (row) => row.kind === "needs-session" && row.body.includes(rel));
407
- if (
408
- /^\*\*Status:\*\*\s*Complete\b/m.test(planText) &&
409
- /^##\s+Doc Obligations\b/m.test(planText) &&
410
- /^\s*-\s*\[ \]\s*Every Doc Obligations row discharged/m.test(planText)
411
- ) {
247
+ if (/^\*\*Status:\*\*\s*Complete\b/m.test(planText) && undischargedDocRows(planText).length > 0) {
412
248
  rows.push(
413
249
  formatNeedsSessionRow({
414
250
  date: meta.date,