@nurix/etna 0.19.57 → 0.19.59

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 (37) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/apollo/CLAUDE.md +1 -1
  3. package/data/harnesses/apollo/agents/bookkeeper.md +1 -1
  4. package/data/harnesses/apollo/agents/lld-mode.md +2 -0
  5. package/data/harnesses/apollo/codex/agents/bookkeeper.toml +1 -1
  6. package/data/harnesses/apollo/harness.json +2 -2
  7. package/data/harnesses/apollo/hooks/awareness.mjs +25 -0
  8. package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +6 -3
  9. package/data/harnesses/apollo/hooks/compliance.mjs +11 -0
  10. package/data/harnesses/apollo/hooks/lib/state.mjs +8 -1
  11. package/data/harnesses/apollo/skills/boundary/SKILL.md +4 -0
  12. package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +1 -1
  13. package/data/harnesses/apollo/skills/documentation/templates/plan.md +18 -4
  14. package/data/harnesses/code/CLAUDE.md +1 -1
  15. package/data/harnesses/code/agents/bookkeeper.md +1 -1
  16. package/data/harnesses/code/agents/lld-mode.md +2 -0
  17. package/data/harnesses/code/codex/agents/bookkeeper.toml +1 -1
  18. package/data/harnesses/code/harness.json +2 -2
  19. package/data/harnesses/code/hooks/awareness.mjs +25 -0
  20. package/data/harnesses/code/hooks/check-completion-integrity.mjs +6 -3
  21. package/data/harnesses/code/hooks/compliance.mjs +11 -0
  22. package/data/harnesses/code/hooks/lib/state.mjs +8 -1
  23. package/data/harnesses/code/skills/boundary/SKILL.md +4 -0
  24. package/data/harnesses/code/skills/documentation/templates/bookkeeping.md +1 -1
  25. package/data/harnesses/code/skills/documentation/templates/plan.md +18 -4
  26. package/data/harnesses/delivery/CLAUDE.md +1 -1
  27. package/data/harnesses/delivery/agents/lld-mode.md +2 -0
  28. package/data/harnesses/delivery/harness.json +2 -2
  29. package/data/harnesses/delivery/skills/documentation/templates/bookkeeping.md +1 -1
  30. package/data/harnesses/delivery/skills/documentation/templates/plan.md +18 -4
  31. package/data/harnesses/etna/CLAUDE.md +1 -1
  32. package/data/harnesses/etna/agents/lld-mode.md +2 -0
  33. package/data/harnesses/etna/harness.json +2 -2
  34. package/data/harnesses/etna/skills/boundary/SKILL.md +4 -0
  35. package/data/harnesses/etna/skills/documentation/templates/bookkeeping.md +1 -1
  36. package/data/harnesses/etna/skills/documentation/templates/plan.md +18 -4
  37. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "73e48dd4eae4",
2
+ "fingerprint": "44a8ddcfae78",
3
3
  "harnesses": {
4
4
  "apollo": {
5
5
  "plugin": "apollo",
@@ -39,7 +39,7 @@ Three peer modes, then a verification gate and a Completion step — enter where
39
39
 
40
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
41
  2. Freeze the plan — `Status: Complete`, fill the Completed date. The file is now frozen.
42
- 3. Flip the feature LLD to `status: active`; extend it in place with the how-it-works sections; confirm its `owns:` globs against the actual code.
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
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.
44
44
 
45
45
  **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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bookkeeper
3
- description: Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, marks rows resolved. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on "drain the bookkeeping queue", "dispatch the bookkeeper", "/bookkeeper".
3
+ description: Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, deletes each row it drains. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on "drain the bookkeeping queue", "dispatch the bookkeeper", "/bookkeeper".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash, Skill
5
5
  model: sonnet
6
6
  ---
@@ -33,6 +33,7 @@ This tells you what already exists, what's adjacent, how detailed the LLD should
33
33
  4. **Current state** — Understand how the system works today in the area being specified
34
34
  5. **Dependencies** — Map what this feature touches and what depends on it
35
35
  6. **Ownership conflicts** — Compare the new feature's intended code paths against existing feature docs' `owns:` globs. Report overlaps (two docs claiming the same paths at equal specificity).
36
+ 7. **Invalidation** — a different question from conflict, and the one that gets missed. A conflict is two docs claiming one glob; an invalidation is **one existing doc whose stated behavior this feature makes false** — it may own entirely different paths and still be wrong once this ships. For every doc whose territory this feature touches or whose described flow it changes, name the specific claim that stops being true.
36
37
 
37
38
  ## What You Return
38
39
 
@@ -44,6 +45,7 @@ Structure your findings as:
44
45
  - **Current behavior**: How the relevant part of the system works today
45
46
  - **Conventions**: Naming, structure, and patterns to follow based on existing code
46
47
  - **Ownership conflicts**: Existing feature docs whose `owns:` globs overlap with the proposed feature. Include the conflicting paths and the owning doc name.
48
+ - **Invalidated claims**: One row per existing doc this feature makes wrong — doc path, the claim, why it stops being true. This list seeds the plan's **Doc Obligations** ledger, so it is the research finding most likely to survive into an artifact; "none" is a real answer and says the feature is genuinely additive.
47
49
  - **Risks or concerns**: Anything the LLD should address or call out
48
50
 
49
51
  Use the documentation skill's style guide and conventions when structuring your findings — imperative voice, ASCII diagrams for flows, function signatures over implementations, concise.
@@ -1,6 +1,6 @@
1
1
  # dropped Claude tools: Read, Glob, Grep, Edit, Write, Bash, Skill
2
2
  name = "bookkeeper"
3
- description = "Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, marks rows resolved. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on \"drain the bookkeeping queue\", \"dispatch the bookkeeper\", \"/bookkeeper\"."
3
+ description = "Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, deletes each row it drains. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on \"drain the bookkeeping queue\", \"dispatch the bookkeeper\", \"/bookkeeper\"."
4
4
  developer_instructions = '''
5
5
 
6
6
  # Bookkeeper Agent
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "apollo",
3
3
  "name": "apollo",
4
- "version": "1.1.6",
4
+ "version": "1.2.1",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -15,5 +15,5 @@
15
15
  "docs/memory/main.md",
16
16
  ".mcp.json"
17
17
  ],
18
- "digest": "8e61d4b9ddcf"
18
+ "digest": "56f71396cdcf"
19
19
  }
@@ -23,6 +23,7 @@ import {
23
23
  readQueue,
24
24
  countRows,
25
25
  formatAutoRow,
26
+ formatNeedsSessionRow,
26
27
  appendRows,
27
28
  resolveRows,
28
29
  unexpiredWaivers,
@@ -245,6 +246,30 @@ function detectorForFile(projectDir, rel, meta, rows) {
245
246
  if (rel.endsWith(".md")) {
246
247
  // An edited doc resolves any doc-implicated row that pointed at it.
247
248
  resolveRows(projectDir, (row) => row.kind === "auto" && row.body.includes("doc-implicated") && row.body.includes(rel));
249
+
250
+ // A plan's Doc Obligations ledger names docs whose stated behavior the plan falsifies. Whether
251
+ // one is discharged is a judgment, never a mechanical fix — so it becomes a `needs-session:`
252
+ // row, the one kind the bookkeeper leaves untouched, rather than an `auto:` row a sub-agent
253
+ // would tick without reading the code. Raising it here rather than walling it at Stop is what
254
+ // gives it the queue's three existing walls (Stop, commit, CI) instead of a fourth special case.
255
+ // Resolve-then-append keeps exactly one row per plan, so re-editing never stacks duplicates.
256
+ if (rel.startsWith("docs/history/plans/")) {
257
+ const planText = readSafe(path.join(projectDir, rel));
258
+ resolveRows(projectDir, (row) => row.kind === "needs-session" && row.body.includes(rel));
259
+ if (
260
+ /^\*\*Status:\*\*\s*Complete\b/m.test(planText) &&
261
+ /^##\s+Doc Obligations\b/m.test(planText) &&
262
+ /^\s*-\s*\[ \]\s*Every Doc Obligations row discharged/m.test(planText)
263
+ ) {
264
+ rows.push(
265
+ formatNeedsSessionRow({
266
+ date: meta.date,
267
+ description: `${rel} reached Status: Complete with its Doc Obligations rows undischarged`,
268
+ refs: rel,
269
+ })
270
+ );
271
+ }
272
+ }
248
273
  if (rel.startsWith("docs/")) {
249
274
  const findings = docLint(projectDir, rel);
250
275
  if (findings.length) {
@@ -41,13 +41,16 @@ for (const file of plans) {
41
41
  const unchecked = (text.match(/^- \[ \]/gm) || []).length;
42
42
  if (unchecked > 0) fail(`${file} is Complete with ${unchecked} unchecked Done Criteria box(es)`);
43
43
 
44
- const spec = (/^\*\*Spec:\*\*\s*\[[^\]]*\]\(([^)]+)\)/m.exec(text) || [])[1];
45
- if (spec) {
44
+ // EVERY Spec line, not the first. A plan may implement against several feature docs, and a
45
+ // single-match parse silently exempts all but one — the same singular assumption the Doc
46
+ // Obligations ledger exists to break, reintroduced here where nothing would surface it.
47
+ const specs = [...text.matchAll(/^\*\*Spec:\*\*\s*\[[^\]]*\]\(([^)]+)\)/gm)].map((m) => m[1]);
48
+ for (const spec of specs) {
46
49
  const specPath = path.resolve(plansDir, spec);
47
50
  const specText = readSafe(specPath);
48
51
  const specStatus = (field(frontmatter(specText), "status") || "").toString();
49
52
  if (specText && specStatus !== "active") {
50
- fail(`${file} is Complete but its Spec LLD is status: ${specStatus || "unknown"} (expected active)`);
53
+ fail(`${file} is Complete but its Spec LLD ${spec} is status: ${specStatus || "unknown"} (expected active)`);
51
54
  }
52
55
  const feature = path.basename(specPath, ".md");
53
56
  if (!fs.existsSync(path.join(projectDir, "docs", "tests", `${feature}.md`))) {
@@ -75,6 +75,7 @@ function openGatedPlans(projectDir) {
75
75
  status,
76
76
  gates,
77
77
  hasLedger: /^##\s+Test Obligations\b/m.test(text),
78
+ hasDocLedger: /^##\s+Doc Obligations\b/m.test(text),
78
79
  });
79
80
  }
80
81
  return plans;
@@ -110,6 +111,12 @@ function fileGate(projectDir, rel, toolName) {
110
111
  `BLOCKED — plan gate. \`${plan.file}\` has no \`## Test Obligations\` section. Run the \`testing\` skill (plan-gate mode); its ledger is what the verification gate discharges.`
111
112
  );
112
113
  }
114
+ if (!plan.hasDocLedger) {
115
+ return decision(
116
+ "deny",
117
+ `BLOCKED — plan gate. \`${plan.file}\` has no \`## Doc Obligations\` section. Resolve the Proposed Changes paths against the ownership index and fill the table yourself — it is a lookup, not a skill run. "None" is a valid row when the plan touches only unowned paths.`
118
+ );
119
+ }
113
120
  }
114
121
  }
115
122
 
@@ -381,6 +388,10 @@ function stopGate(projectDir, payload) {
381
388
  };
382
389
  }
383
390
 
391
+ // An undischarged Doc Obligations ledger is not walled here — the detector raises it as a
392
+ // `needs-session:` row, which the queue wall above already blocks on, and which outlives this
393
+ // stop through the commit gate and CI. A second wall keyed to one checkbox would add a special
394
+ // case beside a general rule that already covers it.
384
395
  const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
385
396
  (rel) => !planHasChangelogEntry(projectDir, rel)
386
397
  );
@@ -14,7 +14,7 @@ export const QUEUE_PATH = ["docs", "operations", "bookkeeping.md"];
14
14
 
15
15
  export const QUEUE_HEADER = `# Bookkeeping
16
16
 
17
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A \`waived:\` row carries \`expires=\` and is PR-reviewable history.
17
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to \`waived:\`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the \`documentation\` skill's \`templates/bookkeeping.md\`. A \`waived:\` row carries \`expires=\` and is PR-reviewable history.
18
18
  `;
19
19
 
20
20
  export function queueFile(projectDir) {
@@ -63,6 +63,13 @@ export function formatAutoRow({ date, kind, description, action, sessionId }) {
63
63
  return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
64
64
  }
65
65
 
66
+ // Format one needs-session: row — a finding detected mechanically that only a session can resolve.
67
+ // It carries no `sid=`: unlike an auto: row it is never drained by the bookkeeper, so the session
68
+ // that raised it is not the one that must answer it.
69
+ export function formatNeedsSessionRow({ date, description, refs }) {
70
+ return `- needs-session: ${date} · ${description} → main session resolves | refs=${refs}`;
71
+ }
72
+
66
73
  // Append rows to the queue, creating the file with its genre header when absent.
67
74
  // Dedupes on the row text before the `| sid=` tail, so a re-fired detector never stacks duplicates.
68
75
  export function appendRows(projectDir, rowLines) {
@@ -57,8 +57,12 @@ The packs are a floor, not a fence: report any contract that passes the ships-gr
57
57
 
58
58
  ## Suggested plan amendments
59
59
  ## Missing plan subsections
60
+ ## Docs the plan's ledger missed
61
+ | Feature doc | Owned path in the touch list | In Doc Obligations? |
60
62
  ```
61
63
 
64
+ **The doc cross-check is free and you are the only one who can make it.** You already hold the touch list; resolving those paths against the ownership index and comparing the result to the plan's **Doc Obligations** rows costs one lookup. The plan's author wrote that ledger, so it is the one gate artifact nobody independently checked — a `None` that contradicts your touch list is the failure this section exists to catch. Report the gap; never edit the ledger yourself.
65
+
62
66
  **BLOCK** = any CRITICAL. HIGH findings yield **PASS WITH AMENDMENTS** — fold the amendments into the plan before coding. Order amendments sequencing-first: many violations dissolve by reordering steps (expand-then-contract, migrate-before-code) rather than changing what ships — prefer that fix, drawn from the entry's safe-change sequence.
63
67
 
64
68
  6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps these exact forms — implementation edits stay blocked while the segment reads `pending`. A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
@@ -7,7 +7,7 @@ The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:`
7
7
  ```
8
8
  # Bookkeeping
9
9
 
10
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A `waived:` row carries `expires=` and is PR-reviewable history.
10
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
11
 
12
12
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
13
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -4,14 +4,16 @@ Record. Use for: every implementation plan. Plans are created here directly with
4
4
 
5
5
  The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `testing` skill (plan-gate mode) writes a **Test Obligations** section into the plan at the same gate. Keep that structure stable — section headings and bullet shape are load-bearing for the parsers, and the Test Obligations table is the ledger the verification gate discharges before the plan may reach `Status: Complete`.
6
6
 
7
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. Never hand-edit the line to skip a gate.
7
+ **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
+
9
+ The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
8
10
 
9
11
  ```
10
12
  # {Feature Name}
11
13
 
12
14
  **Status:** Draft | In Progress | Complete | Abandoned
13
- **Gates:** Boundary: pending · Test-plan: pending
14
- **Spec:** [docs/features/{feature}.md](../../features/{feature}.md)
15
+ **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
16
+ **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
15
17
  **Started:** YYYY-MM-DD
16
18
  **Completed:** YYYY-MM-DD <!-- fill when status flips to Complete -->
17
19
 
@@ -44,6 +46,18 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
44
46
  - List which cross-cutting contracts this plan affects (db schema, wire envelope, auth, events, env/config, …)
45
47
  - "None" is a valid answer and means the `boundary` check will pass cleanly
46
48
 
49
+ ## Doc Obligations
50
+
51
+ <!-- One row per feature doc whose `owns:` globs intersect the Proposed Changes above — resolve
52
+ them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
+ Amendable while the plan is open; discharged at Completion. -->
54
+
55
+ | Doc | Claim that changes | `owns:` still accurate? |
56
+ | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
58
+
59
+ **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
+
47
61
  ## Implementation Steps
48
62
 
49
63
  1. Concrete, ordered, testable
@@ -60,7 +74,7 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
60
74
 
61
75
  - [ ] Implementation complete
62
76
  - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
63
- - [ ] Affected docs updated (use the injected ownership index to find them)
77
+ - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
64
78
  - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
65
79
  - [ ] Plan marked `Status: Complete`, **Completed:** date filled
66
80
 
@@ -37,7 +37,7 @@ Three peer modes, then a verification gate and a Completion step — enter where
37
37
 
38
38
  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.
39
39
  2. Freeze the plan — `Status: Complete`, fill the Completed date. The file is now frozen.
40
- 3. Flip the feature LLD to `status: active`; extend it in place with the how-it-works sections; confirm its `owns:` globs against the actual code.
40
+ 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.
41
41
  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.
42
42
 
43
43
  **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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bookkeeper
3
- description: Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, marks rows resolved. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on "drain the bookkeeping queue", "dispatch the bookkeeper", "/bookkeeper".
3
+ description: Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, deletes each row it drains. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on "drain the bookkeeping queue", "dispatch the bookkeeper", "/bookkeeper".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash, Skill
5
5
  model: sonnet
6
6
  ---
@@ -33,6 +33,7 @@ This tells you what already exists, what's adjacent, how detailed the LLD should
33
33
  4. **Current state** — Understand how the system works today in the area being specified
34
34
  5. **Dependencies** — Map what this feature touches and what depends on it
35
35
  6. **Ownership conflicts** — Compare the new feature's intended code paths against existing feature docs' `owns:` globs. Report overlaps (two docs claiming the same paths at equal specificity).
36
+ 7. **Invalidation** — a different question from conflict, and the one that gets missed. A conflict is two docs claiming one glob; an invalidation is **one existing doc whose stated behavior this feature makes false** — it may own entirely different paths and still be wrong once this ships. For every doc whose territory this feature touches or whose described flow it changes, name the specific claim that stops being true.
36
37
 
37
38
  ## What You Return
38
39
 
@@ -44,6 +45,7 @@ Structure your findings as:
44
45
  - **Current behavior**: How the relevant part of the system works today
45
46
  - **Conventions**: Naming, structure, and patterns to follow based on existing code
46
47
  - **Ownership conflicts**: Existing feature docs whose `owns:` globs overlap with the proposed feature. Include the conflicting paths and the owning doc name.
48
+ - **Invalidated claims**: One row per existing doc this feature makes wrong — doc path, the claim, why it stops being true. This list seeds the plan's **Doc Obligations** ledger, so it is the research finding most likely to survive into an artifact; "none" is a real answer and says the feature is genuinely additive.
47
49
  - **Risks or concerns**: Anything the LLD should address or call out
48
50
 
49
51
  Use the documentation skill's style guide and conventions when structuring your findings — imperative voice, ASCII diagrams for flows, function signatures over implementations, concise.
@@ -1,6 +1,6 @@
1
1
  # dropped Claude tools: Read, Glob, Grep, Edit, Write, Bash, Skill
2
2
  name = "bookkeeper"
3
- description = "Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, marks rows resolved. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on \"drain the bookkeeping queue\", \"dispatch the bookkeeper\", \"/bookkeeper\"."
3
+ description = "Drains docs/operations/bookkeeping.md — performs mechanical fixes (owns: globs, header comments, doc-lint findings, index rows), expands note: rows into doc edits, deletes each row it drains. Dispatched by the main session at turn end or commit time; never self-triggers. Triggers on \"drain the bookkeeping queue\", \"dispatch the bookkeeper\", \"/bookkeeper\"."
4
4
  developer_instructions = '''
5
5
 
6
6
  # Bookkeeper Agent
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.6.6",
4
+ "version": "3.7.1",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -10,5 +10,5 @@
10
10
  "seeds": [
11
11
  ".mcp.json"
12
12
  ],
13
- "digest": "47f4949ced4c"
13
+ "digest": "35d9ca37898e"
14
14
  }
@@ -23,6 +23,7 @@ import {
23
23
  readQueue,
24
24
  countRows,
25
25
  formatAutoRow,
26
+ formatNeedsSessionRow,
26
27
  appendRows,
27
28
  resolveRows,
28
29
  unexpiredWaivers,
@@ -245,6 +246,30 @@ function detectorForFile(projectDir, rel, meta, rows) {
245
246
  if (rel.endsWith(".md")) {
246
247
  // An edited doc resolves any doc-implicated row that pointed at it.
247
248
  resolveRows(projectDir, (row) => row.kind === "auto" && row.body.includes("doc-implicated") && row.body.includes(rel));
249
+
250
+ // A plan's Doc Obligations ledger names docs whose stated behavior the plan falsifies. Whether
251
+ // one is discharged is a judgment, never a mechanical fix — so it becomes a `needs-session:`
252
+ // row, the one kind the bookkeeper leaves untouched, rather than an `auto:` row a sub-agent
253
+ // would tick without reading the code. Raising it here rather than walling it at Stop is what
254
+ // gives it the queue's three existing walls (Stop, commit, CI) instead of a fourth special case.
255
+ // Resolve-then-append keeps exactly one row per plan, so re-editing never stacks duplicates.
256
+ if (rel.startsWith("docs/history/plans/")) {
257
+ const planText = readSafe(path.join(projectDir, rel));
258
+ resolveRows(projectDir, (row) => row.kind === "needs-session" && row.body.includes(rel));
259
+ if (
260
+ /^\*\*Status:\*\*\s*Complete\b/m.test(planText) &&
261
+ /^##\s+Doc Obligations\b/m.test(planText) &&
262
+ /^\s*-\s*\[ \]\s*Every Doc Obligations row discharged/m.test(planText)
263
+ ) {
264
+ rows.push(
265
+ formatNeedsSessionRow({
266
+ date: meta.date,
267
+ description: `${rel} reached Status: Complete with its Doc Obligations rows undischarged`,
268
+ refs: rel,
269
+ })
270
+ );
271
+ }
272
+ }
248
273
  if (rel.startsWith("docs/")) {
249
274
  const findings = docLint(projectDir, rel);
250
275
  if (findings.length) {
@@ -41,13 +41,16 @@ for (const file of plans) {
41
41
  const unchecked = (text.match(/^- \[ \]/gm) || []).length;
42
42
  if (unchecked > 0) fail(`${file} is Complete with ${unchecked} unchecked Done Criteria box(es)`);
43
43
 
44
- const spec = (/^\*\*Spec:\*\*\s*\[[^\]]*\]\(([^)]+)\)/m.exec(text) || [])[1];
45
- if (spec) {
44
+ // EVERY Spec line, not the first. A plan may implement against several feature docs, and a
45
+ // single-match parse silently exempts all but one — the same singular assumption the Doc
46
+ // Obligations ledger exists to break, reintroduced here where nothing would surface it.
47
+ const specs = [...text.matchAll(/^\*\*Spec:\*\*\s*\[[^\]]*\]\(([^)]+)\)/gm)].map((m) => m[1]);
48
+ for (const spec of specs) {
46
49
  const specPath = path.resolve(plansDir, spec);
47
50
  const specText = readSafe(specPath);
48
51
  const specStatus = (field(frontmatter(specText), "status") || "").toString();
49
52
  if (specText && specStatus !== "active") {
50
- fail(`${file} is Complete but its Spec LLD is status: ${specStatus || "unknown"} (expected active)`);
53
+ fail(`${file} is Complete but its Spec LLD ${spec} is status: ${specStatus || "unknown"} (expected active)`);
51
54
  }
52
55
  const feature = path.basename(specPath, ".md");
53
56
  if (!fs.existsSync(path.join(projectDir, "docs", "tests", `${feature}.md`))) {
@@ -75,6 +75,7 @@ function openGatedPlans(projectDir) {
75
75
  status,
76
76
  gates,
77
77
  hasLedger: /^##\s+Test Obligations\b/m.test(text),
78
+ hasDocLedger: /^##\s+Doc Obligations\b/m.test(text),
78
79
  });
79
80
  }
80
81
  return plans;
@@ -110,6 +111,12 @@ function fileGate(projectDir, rel, toolName) {
110
111
  `BLOCKED — plan gate. \`${plan.file}\` has no \`## Test Obligations\` section. Run the \`testing\` skill (plan-gate mode); its ledger is what the verification gate discharges.`
111
112
  );
112
113
  }
114
+ if (!plan.hasDocLedger) {
115
+ return decision(
116
+ "deny",
117
+ `BLOCKED — plan gate. \`${plan.file}\` has no \`## Doc Obligations\` section. Resolve the Proposed Changes paths against the ownership index and fill the table yourself — it is a lookup, not a skill run. "None" is a valid row when the plan touches only unowned paths.`
118
+ );
119
+ }
113
120
  }
114
121
  }
115
122
 
@@ -381,6 +388,10 @@ function stopGate(projectDir, payload) {
381
388
  };
382
389
  }
383
390
 
391
+ // An undischarged Doc Obligations ledger is not walled here — the detector raises it as a
392
+ // `needs-session:` row, which the queue wall above already blocks on, and which outlives this
393
+ // stop through the commit gate and CI. A second wall keyed to one checkbox would add a special
394
+ // case beside a general rule that already covers it.
384
395
  const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
385
396
  (rel) => !planHasChangelogEntry(projectDir, rel)
386
397
  );
@@ -14,7 +14,7 @@ export const QUEUE_PATH = ["docs", "operations", "bookkeeping.md"];
14
14
 
15
15
  export const QUEUE_HEADER = `# Bookkeeping
16
16
 
17
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A \`waived:\` row carries \`expires=\` and is PR-reviewable history.
17
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to \`waived:\`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the \`documentation\` skill's \`templates/bookkeeping.md\`. A \`waived:\` row carries \`expires=\` and is PR-reviewable history.
18
18
  `;
19
19
 
20
20
  export function queueFile(projectDir) {
@@ -63,6 +63,13 @@ export function formatAutoRow({ date, kind, description, action, sessionId }) {
63
63
  return `- auto: ${date} · ${kind} · ${description} → ${action} | sid=${sessionId || "-"}`;
64
64
  }
65
65
 
66
+ // Format one needs-session: row — a finding detected mechanically that only a session can resolve.
67
+ // It carries no `sid=`: unlike an auto: row it is never drained by the bookkeeper, so the session
68
+ // that raised it is not the one that must answer it.
69
+ export function formatNeedsSessionRow({ date, description, refs }) {
70
+ return `- needs-session: ${date} · ${description} → main session resolves | refs=${refs}`;
71
+ }
72
+
66
73
  // Append rows to the queue, creating the file with its genre header when absent.
67
74
  // Dedupes on the row text before the `| sid=` tail, so a re-fired detector never stacks duplicates.
68
75
  export function appendRows(projectDir, rowLines) {
@@ -57,8 +57,12 @@ The packs are a floor, not a fence: report any contract that passes the ships-gr
57
57
 
58
58
  ## Suggested plan amendments
59
59
  ## Missing plan subsections
60
+ ## Docs the plan's ledger missed
61
+ | Feature doc | Owned path in the touch list | In Doc Obligations? |
60
62
  ```
61
63
 
64
+ **The doc cross-check is free and you are the only one who can make it.** You already hold the touch list; resolving those paths against the ownership index and comparing the result to the plan's **Doc Obligations** rows costs one lookup. The plan's author wrote that ledger, so it is the one gate artifact nobody independently checked — a `None` that contradicts your touch list is the failure this section exists to catch. Report the gap; never edit the ledger yourself.
65
+
62
66
  **BLOCK** = any CRITICAL. HIGH findings yield **PASS WITH AMENDMENTS** — fold the amendments into the plan before coding. Order amendments sequencing-first: many violations dissolve by reordering steps (expand-then-contract, migrate-before-code) rather than changing what ships — prefer that fix, drawn from the entry's safe-change sequence.
63
67
 
64
68
  6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps these exact forms — implementation edits stay blocked while the segment reads `pending`. A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
@@ -7,7 +7,7 @@ The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:`
7
7
  ```
8
8
  # Bookkeeping
9
9
 
10
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A `waived:` row carries `expires=` and is PR-reviewable history.
10
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
11
 
12
12
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
13
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -4,14 +4,16 @@ Record. Use for: every implementation plan. Plans are created here directly with
4
4
 
5
5
  The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `testing` skill (plan-gate mode) writes a **Test Obligations** section into the plan at the same gate. Keep that structure stable — section headings and bullet shape are load-bearing for the parsers, and the Test Obligations table is the ledger the verification gate discharges before the plan may reach `Status: Complete`.
6
6
 
7
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. Never hand-edit the line to skip a gate.
7
+ **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
+
9
+ The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
8
10
 
9
11
  ```
10
12
  # {Feature Name}
11
13
 
12
14
  **Status:** Draft | In Progress | Complete | Abandoned
13
- **Gates:** Boundary: pending · Test-plan: pending
14
- **Spec:** [docs/features/{feature}.md](../../features/{feature}.md)
15
+ **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
16
+ **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
15
17
  **Started:** YYYY-MM-DD
16
18
  **Completed:** YYYY-MM-DD <!-- fill when status flips to Complete -->
17
19
 
@@ -44,6 +46,18 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
44
46
  - List which cross-cutting contracts this plan affects (db schema, wire envelope, auth, events, env/config, …)
45
47
  - "None" is a valid answer and means the `boundary` check will pass cleanly
46
48
 
49
+ ## Doc Obligations
50
+
51
+ <!-- One row per feature doc whose `owns:` globs intersect the Proposed Changes above — resolve
52
+ them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
+ Amendable while the plan is open; discharged at Completion. -->
54
+
55
+ | Doc | Claim that changes | `owns:` still accurate? |
56
+ | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
58
+
59
+ **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
+
47
61
  ## Implementation Steps
48
62
 
49
63
  1. Concrete, ordered, testable
@@ -60,7 +74,7 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
60
74
 
61
75
  - [ ] Implementation complete
62
76
  - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
63
- - [ ] Affected docs updated (use the injected ownership index to find them)
77
+ - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
64
78
  - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
65
79
  - [ ] Plan marked `Status: Complete`, **Completed:** date filled
66
80
 
@@ -55,7 +55,7 @@ Three peer modes plus a Completion step — enter wherever the user's thinking a
55
55
  | **LLD** (low-level design) | a new prompt family, task type, or eval suite | `lld-mode` agent + `documentation` skill | Plan |
56
56
  | **Plan** | an LLD is approved | `documentation` skill → `docs/history/plans/YYYY-MM-DD-{slug}.md` | Completion |
57
57
 
58
- **On Plan Completion:** freeze the plan (`Status: Complete`, Completed date); flip the feature LLD to `status: active` and extend it in place; confirm its `owns:` globs against the actual prompt files; write the changelog entry as a draft immediately.
58
+ **On Plan Completion:** freeze the plan (`Status: Complete`, Completed date); discharge every Doc Obligations row into **updated**, **owes nothing** (with the reason), or **blocked** (→ backlog) — for each row's doc, flip `status: active`, extend it in place, and confirm its `owns:` globs against the actual prompt files; write the changelog entry as a draft immediately.
59
59
 
60
60
  **On Brainstorm Close:** the skill's `stop` procedure — converge, a user-approved harvest manifest, apply, uselessness test; the record moves to `docs/history/brainstorms/{topic}/`.
61
61
 
@@ -33,6 +33,7 @@ This tells you what already exists, what's adjacent, how detailed the LLD should
33
33
  4. **Current state** — Understand how the system works today in the area being specified
34
34
  5. **Dependencies** — Map what this feature touches and what depends on it
35
35
  6. **Ownership conflicts** — Compare the new feature's intended code paths against existing feature docs' `owns:` globs. Report overlaps (two docs claiming the same paths at equal specificity).
36
+ 7. **Invalidation** — a different question from conflict, and the one that gets missed. A conflict is two docs claiming one glob; an invalidation is **one existing doc whose stated behavior this feature makes false** — it may own entirely different paths and still be wrong once this ships. For every doc whose territory this feature touches or whose described flow it changes, name the specific claim that stops being true.
36
37
 
37
38
  ## What You Return
38
39
 
@@ -44,6 +45,7 @@ Structure your findings as:
44
45
  - **Current behavior**: How the relevant part of the system works today
45
46
  - **Conventions**: Naming, structure, and patterns to follow based on existing code
46
47
  - **Ownership conflicts**: Existing feature docs whose `owns:` globs overlap with the proposed feature. Include the conflicting paths and the owning doc name.
48
+ - **Invalidated claims**: One row per existing doc this feature makes wrong — doc path, the claim, why it stops being true. This list seeds the plan's **Doc Obligations** ledger, so it is the research finding most likely to survive into an artifact; "none" is a real answer and says the feature is genuinely additive.
47
49
  - **Risks or concerns**: Anything the LLD should address or call out
48
50
 
49
51
  Use the documentation skill's style guide and conventions when structuring your findings — imperative voice, ASCII diagrams for flows, function signatures over implementations, concise.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.1.7",
4
+ "version": "0.2.1",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "49197cb78351"
11
+ "digest": "9200d5f0c074"
12
12
  }
@@ -7,7 +7,7 @@ The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:`
7
7
  ```
8
8
  # Bookkeeping
9
9
 
10
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A `waived:` row carries `expires=` and is PR-reviewable history.
10
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
11
 
12
12
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
13
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -4,14 +4,16 @@ Record. Use for: every implementation plan. Plans are created here directly with
4
4
 
5
5
  The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `testing` skill (plan-gate mode) writes a **Test Obligations** section into the plan at the same gate. Keep that structure stable — section headings and bullet shape are load-bearing for the parsers, and the Test Obligations table is the ledger the verification gate discharges before the plan may reach `Status: Complete`.
6
6
 
7
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. Never hand-edit the line to skip a gate.
7
+ **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
+
9
+ The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
8
10
 
9
11
  ```
10
12
  # {Feature Name}
11
13
 
12
14
  **Status:** Draft | In Progress | Complete | Abandoned
13
- **Gates:** Boundary: pending · Test-plan: pending
14
- **Spec:** [docs/features/{feature}.md](../../features/{feature}.md)
15
+ **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
16
+ **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
15
17
  **Started:** YYYY-MM-DD
16
18
  **Completed:** YYYY-MM-DD <!-- fill when status flips to Complete -->
17
19
 
@@ -44,6 +46,18 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
44
46
  - List which cross-cutting contracts this plan affects (db schema, wire envelope, auth, events, env/config, …)
45
47
  - "None" is a valid answer and means the `boundary` check will pass cleanly
46
48
 
49
+ ## Doc Obligations
50
+
51
+ <!-- One row per feature doc whose `owns:` globs intersect the Proposed Changes above — resolve
52
+ them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
+ Amendable while the plan is open; discharged at Completion. -->
54
+
55
+ | Doc | Claim that changes | `owns:` still accurate? |
56
+ | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
58
+
59
+ **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
+
47
61
  ## Implementation Steps
48
62
 
49
63
  1. Concrete, ordered, testable
@@ -60,7 +74,7 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
60
74
 
61
75
  - [ ] Implementation complete
62
76
  - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
63
- - [ ] Affected docs updated (use the injected ownership index to find them)
77
+ - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
64
78
  - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
65
79
  - [ ] Plan marked `Status: Complete`, **Completed:** date filled
66
80
 
@@ -66,7 +66,7 @@ Three peer modes plus a Completion step — enter wherever the user's thinking a
66
66
  **On Plan Completion:**
67
67
 
68
68
  1. Freeze the plan — `Status: Complete`, fill the Completed date. The file is now frozen.
69
- 2. Flip the feature LLD to `status: active`; extend it in place with the how-it-works sections; confirm its `owns:` globs against the actual components.
69
+ 2. **Discharge the plan's Doc Obligations ledger — every row, into exactly one outcome**: **updated**, **owes nothing** (with the reason), or **blocked** (→ backlog). For each row's doc: flip `status: active` if it was `draft`, extend it in place with the how-it-works sections, confirm its `owns:` globs against the actual components. The ledger, not memory, says how many docs this plan owes.
70
70
  3. The `boundary` skill must already have gated this plan before its first implementation step — see Ownership, Boundaries & Documentation.
71
71
 
72
72
  **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.
@@ -33,6 +33,7 @@ This tells you what already exists, what's adjacent, how detailed the LLD should
33
33
  4. **Current state** — Understand how the system works today in the area being specified
34
34
  5. **Dependencies** — Map what this feature touches and what depends on it
35
35
  6. **Ownership conflicts** — Compare the new feature's intended code paths against existing feature docs' `owns:` globs. Report overlaps (two docs claiming the same paths at equal specificity).
36
+ 7. **Invalidation** — a different question from conflict, and the one that gets missed. A conflict is two docs claiming one glob; an invalidation is **one existing doc whose stated behavior this feature makes false** — it may own entirely different paths and still be wrong once this ships. For every doc whose territory this feature touches or whose described flow it changes, name the specific claim that stops being true.
36
37
 
37
38
  ## What You Return
38
39
 
@@ -44,6 +45,7 @@ Structure your findings as:
44
45
  - **Current behavior**: How the relevant part of the system works today
45
46
  - **Conventions**: Naming, structure, and patterns to follow based on existing code
46
47
  - **Ownership conflicts**: Existing feature docs whose `owns:` globs overlap with the proposed feature. Include the conflicting paths and the owning doc name.
48
+ - **Invalidated claims**: One row per existing doc this feature makes wrong — doc path, the claim, why it stops being true. This list seeds the plan's **Doc Obligations** ledger, so it is the research finding most likely to survive into an artifact; "none" is a real answer and says the feature is genuinely additive.
47
49
  - **Risks or concerns**: Anything the LLD should address or call out
48
50
 
49
51
  Use the documentation skill's style guide and conventions when structuring your findings — imperative voice, ASCII diagrams for flows, function signatures over implementations, concise.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.3.10",
4
+ "version": "1.4.1",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [],
11
- "digest": "e9751a673dc0"
11
+ "digest": "dbb7424aa7c7"
12
12
  }
@@ -57,8 +57,12 @@ The packs are a floor, not a fence: report any contract that passes the ships-gr
57
57
 
58
58
  ## Suggested plan amendments
59
59
  ## Missing plan subsections
60
+ ## Docs the plan's ledger missed
61
+ | Feature doc | Owned path in the touch list | In Doc Obligations? |
60
62
  ```
61
63
 
64
+ **The doc cross-check is free and you are the only one who can make it.** You already hold the touch list; resolving those paths against the ownership index and comparing the result to the plan's **Doc Obligations** rows costs one lookup. The plan's author wrote that ledger, so it is the one gate artifact nobody independently checked — a `None` that contradicts your touch list is the failure this section exists to catch. Report the gap; never edit the ledger yourself.
65
+
62
66
  **BLOCK** = any CRITICAL. HIGH findings yield **PASS WITH AMENDMENTS** — fold the amendments into the plan before coding. Order amendments sequencing-first: many violations dissolve by reordering steps (expand-then-contract, migrate-before-code) rather than changing what ships — prefer that fix, drawn from the entry's safe-change sequence.
63
67
 
64
68
  6. **Record the verdict in the plan's `Gates:` line.** Rewrite only your own segment, to exactly `Boundary: passed YYYY-MM-DD` (PASS, or PASS WITH AMENDMENTS once the amendments are folded) or `Boundary: blocked YYYY-MM-DD` (BLOCK). The compliance hook greps these exact forms — implementation edits stay blocked while the segment reads `pending`. A plan with no `Gates:` line predates the format; add the line rather than skipping the write-back.
@@ -7,7 +7,7 @@ The row grammar is machine-read by the hooks and CI; keep it exact. A `waived:`
7
7
  ```
8
8
  # Bookkeeping
9
9
 
10
- The shared work queue keeping documentation and code in sync — rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is resolved or waived. Row grammar and lifecycle: the behavioral-hooks feature doc. A `waived:` row carries `expires=` and is PR-reviewable history.
10
+ The shared work queue keeping documentation and code in sync — a queue, never an archive. Rows are appended mechanically (auto:) and by working sessions (note:), drained by the bookkeeper agent, and walled at commit: a commit is possible only when every row is either **resolved, which deletes it**, or converted to `waived:`. A drained row leaves no trace here; what it closed is recorded in the doc it corrected, and git history holds the row itself. Row grammar and lifecycle: the `documentation` skill's `templates/bookkeeping.md`. A `waived:` row carries `expires=` and is PR-reviewable history.
11
11
 
12
12
  - auto: YYYY-MM-DD · {finding-kind} · {description} → {action} | sid={session_id} tp={transcript_path} ts={ISO}
13
13
  - note: YYYY-MM-DD · {area} · {one-line context only the session holds} → lands-in: {owning doc | backlog | memory} | sid=… tp=… ts=…
@@ -4,14 +4,16 @@ Record. Use for: every implementation plan. Plans are created here directly with
4
4
 
5
5
  The `boundary` skill parses the **Proposed Changes** sections to check boundary impact before execution, and the `testing` skill (plan-gate mode) writes a **Test Obligations** section into the plan at the same gate. Keep that structure stable — section headings and bullet shape are load-bearing for the parsers, and the Test Obligations table is the ledger the verification gate discharges before the plan may reach `Status: Complete`.
6
6
 
7
- The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. Never hand-edit the line to skip a gate.
7
+ **Doc Obligations is the one ledger the plan's author writes.** Deriving tests from behavior is judgment, which is why `testing` owns that section and it is marked never-hand-author; resolving Proposed Changes' file paths to the feature docs that own them is a **lookup** against the ownership index already injected at session start. Different work, different rule — fill it yourself, while the touch list is fresh.
8
+
9
+ The **`Gates:`** line is machine-read: the compliance hook greps exactly the forms below, and implementation edits are blocked while a segment reads `pending`. Each gate skill rewrites only its own segment when it runs — `boundary` to `Boundary: passed YYYY-MM-DD` or `Boundary: blocked YYYY-MM-DD`; `testing` (plan-gate mode) to `Test-plan: written YYYY-MM-DD (N rows)` — the segment label stays `Test-plan`, greppable. The author writes `Docs: mapped YYYY-MM-DD (N docs)` when the Doc Obligations table is filled. Never hand-edit the line to skip a gate.
8
10
 
9
11
  ```
10
12
  # {Feature Name}
11
13
 
12
14
  **Status:** Draft | In Progress | Complete | Abandoned
13
- **Gates:** Boundary: pending · Test-plan: pending
14
- **Spec:** [docs/features/{feature}.md](../../features/{feature}.md)
15
+ **Gates:** Boundary: pending · Test-plan: pending · Docs: pending
16
+ **Spec:** [docs/features/{feature}.md](../../features/{feature}.md) <!-- one line per feature doc this plan implements against; a plan spanning three features names three -->
15
17
  **Started:** YYYY-MM-DD
16
18
  **Completed:** YYYY-MM-DD <!-- fill when status flips to Complete -->
17
19
 
@@ -44,6 +46,18 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
44
46
  - List which cross-cutting contracts this plan affects (db schema, wire envelope, auth, events, env/config, …)
45
47
  - "None" is a valid answer and means the `boundary` check will pass cleanly
46
48
 
49
+ ## Doc Obligations
50
+
51
+ <!-- One row per feature doc whose `owns:` globs intersect the Proposed Changes above — resolve
52
+ them against the injected ownership index. Hand-authored: this is a lookup, not research.
53
+ Amendable while the plan is open; discharged at Completion. -->
54
+
55
+ | Doc | Claim that changes | `owns:` still accurate? |
56
+ | --- | --- | --- |
57
+ | `docs/features/{feature}.md` | what it says today that this plan makes false | yes / no — new paths to add |
58
+
59
+ **"None" is valid** — a plan touching only unowned paths owes no doc update, and says so explicitly. It is not a free pass: `boundary` builds its own touch list from the same Proposed Changes and reports docs this table missed.
60
+
47
61
  ## Implementation Steps
48
62
 
49
63
  1. Concrete, ordered, testable
@@ -60,7 +74,7 @@ Why this work is happening *now*. Link to the spec for *what* we're building; th
60
74
 
61
75
  - [ ] Implementation complete
62
76
  - [ ] Verification gate passed — every Test Obligations row discharged (satisfied / owes nothing / blocked → backlog), gate run date and results noted here
63
- - [ ] Affected docs updated (use the injected ownership index to find them)
77
+ - [ ] Every Doc Obligations row discharged — **updated** / **owes nothing** (with the reason) / **blocked** (→ backlog item, so the debt outlives the frozen plan)
64
78
  - [ ] Changelog entry created in `docs/history/changelog/`, carrying the gate's results
65
79
  - [ ] Plan marked `Status: Complete`, **Completed:** date filled
66
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.19.57",
3
+ "version": "0.19.59",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,5 +42,5 @@
42
42
  "agent",
43
43
  "plugin"
44
44
  ],
45
- "etnaFingerprint": "73e48dd4eae4"
45
+ "etnaFingerprint": "44a8ddcfae78"
46
46
  }