@nurix/etna 0.19.56 → 0.19.58

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 (43) 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/change-review.md +1 -1
  5. package/data/harnesses/apollo/agents/lld-mode.md +3 -1
  6. package/data/harnesses/apollo/agents/maintenance.md +1 -1
  7. package/data/harnesses/apollo/agents/performance-review.md +1 -1
  8. package/data/harnesses/apollo/agents/retro.md +1 -1
  9. package/data/harnesses/apollo/harness.json +2 -2
  10. package/data/harnesses/apollo/hooks/compliance.mjs +70 -2
  11. package/data/harnesses/apollo/rules/database.md +2 -2
  12. package/data/harnesses/apollo/skills/boundary/SKILL.md +4 -0
  13. package/data/harnesses/apollo/skills/documentation/templates/plan.md +18 -4
  14. package/data/harnesses/apollo/skills/schema-design/SKILL.md +0 -30
  15. package/data/harnesses/code/CLAUDE.md +1 -1
  16. package/data/harnesses/code/agents/bookkeeper.md +1 -1
  17. package/data/harnesses/code/agents/change-review.md +1 -1
  18. package/data/harnesses/code/agents/lld-mode.md +3 -1
  19. package/data/harnesses/code/agents/maintenance.md +1 -1
  20. package/data/harnesses/code/agents/performance-review.md +1 -1
  21. package/data/harnesses/code/agents/retro.md +1 -1
  22. package/data/harnesses/code/harness.json +2 -2
  23. package/data/harnesses/code/hooks/compliance.mjs +70 -2
  24. package/data/harnesses/code/rules/database.md +2 -2
  25. package/data/harnesses/code/skills/boundary/SKILL.md +4 -0
  26. package/data/harnesses/code/skills/documentation/templates/plan.md +18 -4
  27. package/data/harnesses/code/skills/schema-design/SKILL.md +0 -30
  28. package/data/harnesses/delivery/CLAUDE.md +1 -1
  29. package/data/harnesses/delivery/agents/lld-mode.md +3 -1
  30. package/data/harnesses/delivery/agents/maintenance.md +1 -1
  31. package/data/harnesses/delivery/agents/retro.md +1 -1
  32. package/data/harnesses/delivery/harness.json +2 -2
  33. package/data/harnesses/delivery/skills/documentation/templates/plan.md +18 -4
  34. package/data/harnesses/etna/CLAUDE.md +1 -1
  35. package/data/harnesses/etna/agents/change-review.md +1 -1
  36. package/data/harnesses/etna/agents/lld-mode.md +3 -1
  37. package/data/harnesses/etna/agents/maintenance.md +1 -1
  38. package/data/harnesses/etna/agents/performance-review.md +1 -1
  39. package/data/harnesses/etna/agents/retro.md +1 -1
  40. package/data/harnesses/etna/harness.json +2 -2
  41. package/data/harnesses/etna/skills/boundary/SKILL.md +4 -0
  42. package/data/harnesses/etna/skills/documentation/templates/plan.md +18 -4
  43. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "e7493c40d358",
2
+ "fingerprint": "411f905588e3",
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.
@@ -2,7 +2,7 @@
2
2
  name: bookkeeper
3
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".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash, Skill
5
- model: inherit
5
+ model: sonnet
6
6
  ---
7
7
 
8
8
  # Bookkeeper Agent
@@ -2,7 +2,7 @@
2
2
  name: change-review
3
3
  description: Velocity + change-risk analysis. First classifies recent commits into a pipe-delimited velocity/severity CSV by dispatching parallel low-cost subagents (module, feature, impact, severity, churn), then derives a commit heatmap, risk zones (high churn in contract-heavy code), stabilization signals, and team/bus-factor distribution. Self-contained — produces its own CSV, no separate git-history step. Use when planning where to invest in review focus or rules.
4
4
  tools: Bash, Read, Write, Glob, Grep, Task
5
- model: inherit
5
+ model: sonnet
6
6
  ---
7
7
 
8
8
  # Change-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: lld-mode
3
3
  description: LLD researcher for Feature and Product workflows. Use to explore the codebase and gather the context needed for writing a feature LLD (low-level design — the feature doc in docs/features/). Returns research findings — does not write files.
4
4
  tools: Read, Glob, Grep, WebSearch, WebFetch
5
- model: inherit
5
+ model: opus
6
6
  skills:
7
7
  - documentation
8
8
  ---
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: maintenance
3
3
  description: The single docs steward. Default behavior is the docs-vs-code coherence audit — detects and fixes docs drifting from code (stale docs, broken ownership, unowned source paths, architecture/backlog drift). An additive **baseline mode** layers structural reconcile on top: when the repo's `harness-version` marker is missing or behind, it converges `docs/` to the installed harness shape (scaffold the standard tree, fold dissolved-shape artifacts forward, stamp the version, propose modules) before running the audit. Mode is auto-selected from the marker. Run weekly, after major changes, or to baseline/upgrade a repo. Cross-cutting contract analysis is the `boundary` skill (`check` for plans, `scan` for diffs) — it derives contracts dynamically from source; there are no persisted boundary docs to audit. Triggers on "run maintenance", "docs health", "baseline this repo", "reconcile the harness", "upgrade the harness", "is this repo on the current harness", "/maintenance".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Maintenance Agent
@@ -2,7 +2,7 @@
2
2
  name: performance-review
3
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.
4
4
  tools: Read, Glob, Grep, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Performance-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: retro
3
3
  description: Session-distillation agent for the changelog. Reads the per-unit-of-work entries under `docs/history/changelog/` accumulated since its last run and distills the refinement delta — what user-driven iteration added that the one-shot build lacked — into a typed punch list of doc-content gaps, doc-structure moves (missing doc / mothership split / merge), skill-evolution findings backed by survived-vs-overridden counts, unpinned discoveries needing an enforced artifact (test, token, guardrail, schema), and cross-project harness rules. Proposes only — actionable items land in the backlog, never applied directly. Run on demand: before a brainstorm or LLD session, before starting a new project, or after a refinement arc. Triggers on "run retro", "distill the changelog", "refinement retro", "/retro".
4
4
  tools: Read, Glob, Grep, Bash, Write, Edit
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Retro — changelog distillation
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "apollo",
3
3
  "name": "apollo",
4
- "version": "1.1.5",
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": "47955a71244a"
18
+ "digest": "6c0123cfa9ee"
19
19
  }
@@ -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
 
@@ -301,6 +308,15 @@ function plansCompletedThisSession(projectDir, sessionId) {
301
308
  return out;
302
309
  }
303
310
 
311
+ // A completed plan whose Doc Obligations ledger was never discharged. Discharge is a judgment the
312
+ // hook cannot make — whether a doc was actually updated well is a human read — so what it checks is
313
+ // the one mechanical trace the act leaves: the Done Criteria checkbox that says every row was
314
+ // examined. Presence of the section is gated at the implementation-edit wall; this is the other end.
315
+ function docLedgerUndischarged(text) {
316
+ if (!/^##\s+Doc Obligations\b/m.test(text)) return false;
317
+ return /^\s*-\s*\[ \]\s*Every Doc Obligations row discharged/m.test(text);
318
+ }
319
+
304
320
  // Whether some changelog entry already accounts for this plan.
305
321
  //
306
322
  // COMMITTED COUNTS. The test used to be `git status --porcelain docs/history/changelog/` — i.e. "is
@@ -325,6 +341,43 @@ function planHasChangelogEntry(projectDir, planRel) {
325
341
  return names.some((name) => name.endsWith(".md") && readSafe(path.join(dir, name)).includes(slug));
326
342
  }
327
343
 
344
+ // Approximate the context the next turn will re-read, from the transcript's last recorded usage.
345
+ // Best-effort: an absent or unparseable transcript reports nothing.
346
+ function sessionContextTokens(transcriptPath) {
347
+ const text = readSafe(transcriptPath || "");
348
+ if (!text) return 0;
349
+ const lines = text.slice(-400000).split("\n");
350
+ for (let i = lines.length - 1; i >= 0; i--) {
351
+ if (!lines[i].includes('"usage"')) continue;
352
+ try {
353
+ const u = JSON.parse(lines[i]).message.usage;
354
+ return (
355
+ (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0)
356
+ );
357
+ } catch (e) {
358
+ // the slice cut this line in half — keep scanning upward
359
+ }
360
+ }
361
+ return 0;
362
+ }
363
+
364
+ // Context re-reads in full on every turn, so its size IS the standing per-turn floor — the one
365
+ // number that argues for ending a session rather than carrying it into unrelated work. The agent
366
+ // cannot act on this (it can neither end a session nor start one), so it is addressed to the user
367
+ // as a systemMessage and never blocks. Kept out of the model's context deliberately: a running
368
+ // token count in-context produces context anxiety, not shorter sessions.
369
+ const CONTEXT_ADVISORY_TOKENS = 250000;
370
+
371
+ function sessionSizeNote(payload) {
372
+ const tokens = sessionContextTokens(payload.transcript_path);
373
+ if (tokens < CONTEXT_ADVISORY_TOKENS) return null;
374
+ return (
375
+ `Session context is roughly ${Math.round(tokens / 1000)}K tokens, and every turn re-reads all of it — ` +
376
+ "that is the per-turn floor before anything new is produced. A fresh session starts near zero; " +
377
+ "worth ending this one if the next task is unrelated."
378
+ );
379
+ }
380
+
328
381
  function stopGate(projectDir, payload) {
329
382
  if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
330
383
 
@@ -344,9 +397,19 @@ function stopGate(projectDir, payload) {
344
397
  };
345
398
  }
346
399
 
347
- const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
348
- (rel) => !planHasChangelogEntry(projectDir, rel)
400
+ const completed = plansCompletedThisSession(projectDir, payload.session_id);
401
+
402
+ const docsOpen = completed.filter((rel) =>
403
+ docLedgerUndischarged(readSafe(path.join(projectDir, rel)))
349
404
  );
405
+ if (docsOpen.length > 0) {
406
+ return {
407
+ decision: "block",
408
+ reason: `Plan \`${docsOpen[0]}\` reached Status: Complete with its Doc Obligations rows undischarged. Walk each row — update the doc, or mark it owes nothing with the reason, or send it to the backlog as blocked — then tick the Done Criteria line. The feature docs are the only artifact the next session inherits.`,
409
+ };
410
+ }
411
+
412
+ const undocumented = completed.filter((rel) => !planHasChangelogEntry(projectDir, rel));
350
413
  if (undocumented.length > 0) {
351
414
  return {
352
415
  decision: "block",
@@ -381,6 +444,11 @@ function main() {
381
444
  }
382
445
  } else if (event === "Stop") {
383
446
  out = stopGate(projectDir, payload);
447
+ // Advisory rides alongside a block rather than replacing it — both are true at once.
448
+ if (!payload.stop_hook_active) {
449
+ const note = sessionSizeNote(payload);
450
+ if (note) out = { ...(out || {}), systemMessage: note };
451
+ }
384
452
  }
385
453
 
386
454
  if (out) process.stdout.write(JSON.stringify(out));
@@ -7,7 +7,7 @@ Naming and the fixed column vocabulary for relational schemas in **every** Nurix
7
7
  Three column roles are fixed across every table and every project:
8
8
 
9
9
  - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres), never a synthetic or natural key (❌ `type_page_button`, `human:default`). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. **Never** mix synthetic and UUID ids in one table. The single exemption is a log whose **composite key is its ordering** (`PRIMARY KEY (<entity>_id, sequence)`), declared at the table that uses it — everything else, logs included, takes a UUIDv7.
10
- - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping (§ Copying rows between scopes). ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
10
+ - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping — copying between scopes is the `schema-design` skill. ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
11
11
  - **`label`** — the **human display name** — ❌ never `name`, `title`, or `display_name`.
12
12
 
13
13
  Reference columns name what they point at: `<entity>_id` for a UUID-PK reference (`user_id`, `app_id`), `<entity>_slug` for a slug reference (`source_slug`).
@@ -21,7 +21,7 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
21
21
  ## Naming
22
22
 
23
23
  - **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
24
- - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N (§ A junction spells many-to-many).
24
+ - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N — the cardinality test that decides junction vs. column is the `schema-design` skill.
25
25
  - **Columns** — `snake_case`; booleans `is_*` / `has_*`; timestamps `created_at` / `updated_at`; foreign keys `<entity>_id`.
26
26
  - **Indexes** — `<table>_<col(s)>_idx`; unique `<table>_<col(s)>_unique`; a junction's composite unique reads `<table>_<colA>_<colB>_unique` (`app_user_map_app_user_unique`).
27
27
  - **Drizzle mirror** — the exported object is the camelCase table name (`app_user_map` → `appUserMap`); column props are the camelCase of the column (`created_at` → `createdAt`). The `sqliteTable("…")` string and every index name must match the DDL (`init.sql`) exactly — the schema-parity boundary.
@@ -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.
@@ -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
 
@@ -85,22 +85,6 @@ One entity that exists at several scopes is **one table with a nullable scope co
85
85
  - **Store the source's version at copy time** — `version` + `source_version` make "is an upgrade available" a comparison of two stored values, not a content diff.
86
86
  - **A copy is a row, never a live read.** Nothing at the child scope reads the parent's row at runtime; propagation happens only through an explicit re-copy, so a local edit is never silently overwritten.
87
87
 
88
- ## Keys & the shared vocabulary
89
-
90
- Three column roles are fixed across every table and every project:
91
-
92
- - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres), never a synthetic or natural key (❌ `type_page_button`, `human:default`). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. **Never** mix synthetic and UUID ids in one table. The single exemption is a log whose **composite key is its ordering** (`PRIMARY KEY (<entity>_id, sequence)`), declared at the table that uses it — everything else, logs included, takes a UUIDv7.
93
- - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping (§ Copying rows between scopes). ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
94
- - **`label`** — the **human display name** — ❌ never `name`, `title`, or `display_name`.
95
-
96
- Reference columns name what they point at: `<entity>_id` for a UUID-PK reference (`user_id`, `app_id`), `<entity>_slug` for a slug reference (`source_slug`).
97
-
98
- - **Enforced, same scope** (the referenced row must exist) → a real `FOREIGN KEY (<x>_id) REFERENCES <table>(id)`.
99
- - **Cross-scope** (the reference must survive being copied) → `<x>_slug` as a plain column, **no** FK — an FK would bind the row to one scope's copy of the target, which is the remapping the slug exists to avoid.
100
- - **Drift-tolerant** (a controlled-vocabulary value that may fall off-list) → a plain value column, **no** FK, paired with an `is_*` boolean recording whether the value is in the canonical set.
101
-
102
- **Every inbound FK to a hub table declares a deliberate `ON DELETE`.** A hub (`users`, `devices`, anything several modules point at) collects edges from teams that never meet, and a single `NO ACTION` among them turns deleting the parent into a constraint error naming neither table. Decide per edge: the child dies with the row (`CASCADE`) or survives it (`SET NULL`, which needs a nullable column) — `NO ACTION` on a hub is a deferred outage, not a default.
103
-
104
88
  ## A junction spells many-to-many — nothing else
105
89
 
106
90
  **A `_map` table is the schema's notation for M:N. Using it for 1:M is a category error** — the junction's own uniqueness admits many parents per child, so the "one parent" rule stops being enforced the moment it is written this way. Cardinality decides between the only two shapes:
@@ -131,14 +115,6 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
131
115
  - **`CASCADE` from the append-only side's own parent is still fine** — a cascade is a DELETE, so a `BEFORE UPDATE` trigger never sees it: an App's log dying with the App is legitimate, distinct from erasing a log to delete a person.
132
116
  - **State the exemption where it exists.** A catalog whose rows must not disappear under a referencing row (a service definition, a vocabulary) keeps `NO ACTION` deliberately — that guarantee is worth one line saying so.
133
117
 
134
- ## Naming
135
-
136
- - **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
137
- - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N (§ A junction spells many-to-many).
138
- - **Columns** — `snake_case`; booleans `is_*` / `has_*`; timestamps `created_at` / `updated_at`; foreign keys `<entity>_id`.
139
- - **Indexes** — `<table>_<col(s)>_idx`; unique `<table>_<col(s)>_unique`; a junction's composite unique reads `<table>_<colA>_<colB>_unique` (`app_user_map_app_user_unique`).
140
- - **Drizzle mirror** — the exported object is the camelCase table name (`app_user_map` → `appUserMap`); column props are the camelCase of the column (`created_at` → `createdAt`). The `sqliteTable("…")` string and every index name must match the DDL (`init.sql`) exactly — the schema-parity boundary.
141
-
142
118
  ## Uniqueness with a nullable column
143
119
 
144
120
  **`UNIQUE (a, b)` does not constrain rows where `b` is `NULL`** — SQL treats every `NULL` as distinct, so a composite unique over a nullable discriminator silently permits exactly the duplicates it was introduced to keep apart. Write one partial index per scope instead:
@@ -150,12 +126,6 @@ CREATE UNIQUE INDEX <t>_child_slug_unique ON <t> (child_id, slug) WHERE child_
150
126
 
151
127
  **Prove a uniqueness claim by inserting the duplicate it forbids** — a schema review cannot tell a firing index from a silent one by reading.
152
128
 
153
- ## Column types (SQLite / D1)
154
-
155
- - **Timestamps** — `created_at` + `updated_at`, both `TEXT NOT NULL DEFAULT (datetime('now'))`, on **every** table — except append-only logs, which carry only `created_at`.
156
- - **Booleans** — `INTEGER` (Drizzle `integer(…, { mode: "boolean" })`); the `is_*` / `has_*` names are § Naming.
157
- - **JSON** — `TEXT` (Drizzle `text(…, { mode: "json" })`).
158
-
159
129
  ## Vocabulary / lookup tables
160
130
 
161
131
  A controlled vocabulary earns a **table** only when a user row FKs into it and reads its `label` / `description` at runtime — a service catalog rendered in a picker; static dev-authored config nobody references by FK stays **config** (§ What earns a table) — a `doc_type` value list. Enumerable is not tabular, and a `type`-style column validated against the constants file beats an FK to a seeded mirror. An earned table has a fixed shape: `id` (UUID) · `slug` (UNIQUE) · `label` · optional `description` · `status` · timestamps.
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: bookkeeper
3
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".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash, Skill
5
- model: inherit
5
+ model: sonnet
6
6
  ---
7
7
 
8
8
  # Bookkeeper Agent
@@ -2,7 +2,7 @@
2
2
  name: change-review
3
3
  description: Velocity + change-risk analysis. First classifies recent commits into a pipe-delimited velocity/severity CSV by dispatching parallel low-cost subagents (module, feature, impact, severity, churn), then derives a commit heatmap, risk zones (high churn in contract-heavy code), stabilization signals, and team/bus-factor distribution. Self-contained — produces its own CSV, no separate git-history step. Use when planning where to invest in review focus or rules.
4
4
  tools: Bash, Read, Write, Glob, Grep, Task
5
- model: inherit
5
+ model: sonnet
6
6
  ---
7
7
 
8
8
  # Change-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: lld-mode
3
3
  description: LLD researcher for Feature and Product workflows. Use to explore the codebase and gather the context needed for writing a feature LLD (low-level design — the feature doc in docs/features/). Returns research findings — does not write files.
4
4
  tools: Read, Glob, Grep, WebSearch, WebFetch
5
- model: inherit
5
+ model: opus
6
6
  skills:
7
7
  - documentation
8
8
  ---
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: maintenance
3
3
  description: The single docs steward. Default behavior is the docs-vs-code coherence audit — detects and fixes docs drifting from code (stale docs, broken ownership, unowned source paths, architecture/backlog drift). An additive **baseline mode** layers structural reconcile on top: when the repo's `harness-version` marker is missing or behind, it converges `docs/` to the installed harness shape (scaffold the standard tree, fold dissolved-shape artifacts forward, stamp the version, propose modules) before running the audit. Mode is auto-selected from the marker. Run weekly, after major changes, or to baseline/upgrade a repo. Cross-cutting contract analysis is the `boundary` skill (`check` for plans, `scan` for diffs) — it derives contracts dynamically from source; there are no persisted boundary docs to audit. Triggers on "run maintenance", "docs health", "baseline this repo", "reconcile the harness", "upgrade the harness", "is this repo on the current harness", "/maintenance".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Maintenance Agent
@@ -2,7 +2,7 @@
2
2
  name: performance-review
3
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.
4
4
  tools: Read, Glob, Grep, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Performance-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: retro
3
3
  description: Session-distillation agent for the changelog. Reads the per-unit-of-work entries under `docs/history/changelog/` accumulated since its last run and distills the refinement delta — what user-driven iteration added that the one-shot build lacked — into a typed punch list of doc-content gaps, doc-structure moves (missing doc / mothership split / merge), skill-evolution findings backed by survived-vs-overridden counts, unpinned discoveries needing an enforced artifact (test, token, guardrail, schema), and cross-project harness rules. Proposes only — actionable items land in the backlog, never applied directly. Run on demand: before a brainstorm or LLD session, before starting a new project, or after a refinement arc. Triggers on "run retro", "distill the changelog", "refinement retro", "/retro".
4
4
  tools: Read, Glob, Grep, Bash, Write, Edit
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Retro — changelog distillation
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.6.5",
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": "a3c50783b9c0"
13
+ "digest": "ecbd84f05b8e"
14
14
  }
@@ -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
 
@@ -301,6 +308,15 @@ function plansCompletedThisSession(projectDir, sessionId) {
301
308
  return out;
302
309
  }
303
310
 
311
+ // A completed plan whose Doc Obligations ledger was never discharged. Discharge is a judgment the
312
+ // hook cannot make — whether a doc was actually updated well is a human read — so what it checks is
313
+ // the one mechanical trace the act leaves: the Done Criteria checkbox that says every row was
314
+ // examined. Presence of the section is gated at the implementation-edit wall; this is the other end.
315
+ function docLedgerUndischarged(text) {
316
+ if (!/^##\s+Doc Obligations\b/m.test(text)) return false;
317
+ return /^\s*-\s*\[ \]\s*Every Doc Obligations row discharged/m.test(text);
318
+ }
319
+
304
320
  // Whether some changelog entry already accounts for this plan.
305
321
  //
306
322
  // COMMITTED COUNTS. The test used to be `git status --porcelain docs/history/changelog/` — i.e. "is
@@ -325,6 +341,43 @@ function planHasChangelogEntry(projectDir, planRel) {
325
341
  return names.some((name) => name.endsWith(".md") && readSafe(path.join(dir, name)).includes(slug));
326
342
  }
327
343
 
344
+ // Approximate the context the next turn will re-read, from the transcript's last recorded usage.
345
+ // Best-effort: an absent or unparseable transcript reports nothing.
346
+ function sessionContextTokens(transcriptPath) {
347
+ const text = readSafe(transcriptPath || "");
348
+ if (!text) return 0;
349
+ const lines = text.slice(-400000).split("\n");
350
+ for (let i = lines.length - 1; i >= 0; i--) {
351
+ if (!lines[i].includes('"usage"')) continue;
352
+ try {
353
+ const u = JSON.parse(lines[i]).message.usage;
354
+ return (
355
+ (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0)
356
+ );
357
+ } catch (e) {
358
+ // the slice cut this line in half — keep scanning upward
359
+ }
360
+ }
361
+ return 0;
362
+ }
363
+
364
+ // Context re-reads in full on every turn, so its size IS the standing per-turn floor — the one
365
+ // number that argues for ending a session rather than carrying it into unrelated work. The agent
366
+ // cannot act on this (it can neither end a session nor start one), so it is addressed to the user
367
+ // as a systemMessage and never blocks. Kept out of the model's context deliberately: a running
368
+ // token count in-context produces context anxiety, not shorter sessions.
369
+ const CONTEXT_ADVISORY_TOKENS = 250000;
370
+
371
+ function sessionSizeNote(payload) {
372
+ const tokens = sessionContextTokens(payload.transcript_path);
373
+ if (tokens < CONTEXT_ADVISORY_TOKENS) return null;
374
+ return (
375
+ `Session context is roughly ${Math.round(tokens / 1000)}K tokens, and every turn re-reads all of it — ` +
376
+ "that is the per-turn floor before anything new is produced. A fresh session starts near zero; " +
377
+ "worth ending this one if the next task is unrelated."
378
+ );
379
+ }
380
+
328
381
  function stopGate(projectDir, payload) {
329
382
  if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
330
383
 
@@ -344,9 +397,19 @@ function stopGate(projectDir, payload) {
344
397
  };
345
398
  }
346
399
 
347
- const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
348
- (rel) => !planHasChangelogEntry(projectDir, rel)
400
+ const completed = plansCompletedThisSession(projectDir, payload.session_id);
401
+
402
+ const docsOpen = completed.filter((rel) =>
403
+ docLedgerUndischarged(readSafe(path.join(projectDir, rel)))
349
404
  );
405
+ if (docsOpen.length > 0) {
406
+ return {
407
+ decision: "block",
408
+ reason: `Plan \`${docsOpen[0]}\` reached Status: Complete with its Doc Obligations rows undischarged. Walk each row — update the doc, or mark it owes nothing with the reason, or send it to the backlog as blocked — then tick the Done Criteria line. The feature docs are the only artifact the next session inherits.`,
409
+ };
410
+ }
411
+
412
+ const undocumented = completed.filter((rel) => !planHasChangelogEntry(projectDir, rel));
350
413
  if (undocumented.length > 0) {
351
414
  return {
352
415
  decision: "block",
@@ -381,6 +444,11 @@ function main() {
381
444
  }
382
445
  } else if (event === "Stop") {
383
446
  out = stopGate(projectDir, payload);
447
+ // Advisory rides alongside a block rather than replacing it — both are true at once.
448
+ if (!payload.stop_hook_active) {
449
+ const note = sessionSizeNote(payload);
450
+ if (note) out = { ...(out || {}), systemMessage: note };
451
+ }
384
452
  }
385
453
 
386
454
  if (out) process.stdout.write(JSON.stringify(out));
@@ -7,7 +7,7 @@ Naming and the fixed column vocabulary for relational schemas in **every** Nurix
7
7
  Three column roles are fixed across every table and every project:
8
8
 
9
9
  - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres), never a synthetic or natural key (❌ `type_page_button`, `human:default`). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. **Never** mix synthetic and UUID ids in one table. The single exemption is a log whose **composite key is its ordering** (`PRIMARY KEY (<entity>_id, sequence)`), declared at the table that uses it — everything else, logs included, takes a UUIDv7.
10
- - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping (§ Copying rows between scopes). ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
10
+ - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping — copying between scopes is the `schema-design` skill. ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
11
11
  - **`label`** — the **human display name** — ❌ never `name`, `title`, or `display_name`.
12
12
 
13
13
  Reference columns name what they point at: `<entity>_id` for a UUID-PK reference (`user_id`, `app_id`), `<entity>_slug` for a slug reference (`source_slug`).
@@ -21,7 +21,7 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
21
21
  ## Naming
22
22
 
23
23
  - **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
24
- - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N (§ A junction spells many-to-many).
24
+ - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N — the cardinality test that decides junction vs. column is the `schema-design` skill.
25
25
  - **Columns** — `snake_case`; booleans `is_*` / `has_*`; timestamps `created_at` / `updated_at`; foreign keys `<entity>_id`.
26
26
  - **Indexes** — `<table>_<col(s)>_idx`; unique `<table>_<col(s)>_unique`; a junction's composite unique reads `<table>_<colA>_<colB>_unique` (`app_user_map_app_user_unique`).
27
27
  - **Drizzle mirror** — the exported object is the camelCase table name (`app_user_map` → `appUserMap`); column props are the camelCase of the column (`created_at` → `createdAt`). The `sqliteTable("…")` string and every index name must match the DDL (`init.sql`) exactly — the schema-parity boundary.
@@ -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.
@@ -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
 
@@ -85,22 +85,6 @@ One entity that exists at several scopes is **one table with a nullable scope co
85
85
  - **Store the source's version at copy time** — `version` + `source_version` make "is an upgrade available" a comparison of two stored values, not a content diff.
86
86
  - **A copy is a row, never a live read.** Nothing at the child scope reads the parent's row at runtime; propagation happens only through an explicit re-copy, so a local edit is never silently overwritten.
87
87
 
88
- ## Keys & the shared vocabulary
89
-
90
- Three column roles are fixed across every table and every project:
91
-
92
- - **`id`** — the primary key, **always a UUID** (`TEXT PRIMARY KEY` on D1, `uuid` on Postgres), never a synthetic or natural key (❌ `type_page_button`, `human:default`). Runtime rows mint a UUIDv7; seed rows derive a deterministic UUIDv5 of the natural key so re-seeds stay stable. **Never** mix synthetic and UUID ids in one table. The single exemption is a log whose **composite key is its ordering** (`PRIMARY KEY (<entity>_id, sequence)`), declared at the table that uses it — everything else, logs included, takes a UUIDv7.
93
- - **`slug`** — the **stable key rows in another scope reference**: `UNIQUE`, lowercase `snake_case`. A slug means the same thing in every scope, so a slug reference survives a copy with no remapping (§ Copying rows between scopes). ❌ Never rename this role `handle`, `key`, `code`, or `name` in another project.
94
- - **`label`** — the **human display name** — ❌ never `name`, `title`, or `display_name`.
95
-
96
- Reference columns name what they point at: `<entity>_id` for a UUID-PK reference (`user_id`, `app_id`), `<entity>_slug` for a slug reference (`source_slug`).
97
-
98
- - **Enforced, same scope** (the referenced row must exist) → a real `FOREIGN KEY (<x>_id) REFERENCES <table>(id)`.
99
- - **Cross-scope** (the reference must survive being copied) → `<x>_slug` as a plain column, **no** FK — an FK would bind the row to one scope's copy of the target, which is the remapping the slug exists to avoid.
100
- - **Drift-tolerant** (a controlled-vocabulary value that may fall off-list) → a plain value column, **no** FK, paired with an `is_*` boolean recording whether the value is in the canonical set.
101
-
102
- **Every inbound FK to a hub table declares a deliberate `ON DELETE`.** A hub (`users`, `devices`, anything several modules point at) collects edges from teams that never meet, and a single `NO ACTION` among them turns deleting the parent into a constraint error naming neither table. Decide per edge: the child dies with the row (`CASCADE`) or survives it (`SET NULL`, which needs a nullable column) — `NO ACTION` on a hub is a deferred outage, not a default.
103
-
104
88
  ## A junction spells many-to-many — nothing else
105
89
 
106
90
  **A `_map` table is the schema's notation for M:N. Using it for 1:M is a category error** — the junction's own uniqueness admits many parents per child, so the "one parent" rule stops being enforced the moment it is written this way. Cardinality decides between the only two shapes:
@@ -131,14 +115,6 @@ Reference columns name what they point at: `<entity>_id` for a UUID-PK reference
131
115
  - **`CASCADE` from the append-only side's own parent is still fine** — a cascade is a DELETE, so a `BEFORE UPDATE` trigger never sees it: an App's log dying with the App is legitimate, distinct from erasing a log to delete a person.
132
116
  - **State the exemption where it exists.** A catalog whose rows must not disappear under a referencing row (a service definition, a vocabulary) keeps `NO ACTION` deliberately — that guarantee is worth one line saying so.
133
117
 
134
- ## Naming
135
-
136
- - **Tables** — `snake_case`, plural noun for what they hold (`users`, `apps`, `devices`).
137
- - **Junction (M:N) tables** — `<entityA>_<entityB>_map`, **always**, even when the join carries its own columns; singular stems, owning/anchor entity first (`app_user_map`, `app_device_map`). ❌ Never a "nice" noun (`members`, `installs`, `memberships`, `enrolments`) — the name must read as a join, and a reviewer should know both sides from it. The `_map` suffix is reserved for genuine M:N (§ A junction spells many-to-many).
138
- - **Columns** — `snake_case`; booleans `is_*` / `has_*`; timestamps `created_at` / `updated_at`; foreign keys `<entity>_id`.
139
- - **Indexes** — `<table>_<col(s)>_idx`; unique `<table>_<col(s)>_unique`; a junction's composite unique reads `<table>_<colA>_<colB>_unique` (`app_user_map_app_user_unique`).
140
- - **Drizzle mirror** — the exported object is the camelCase table name (`app_user_map` → `appUserMap`); column props are the camelCase of the column (`created_at` → `createdAt`). The `sqliteTable("…")` string and every index name must match the DDL (`init.sql`) exactly — the schema-parity boundary.
141
-
142
118
  ## Uniqueness with a nullable column
143
119
 
144
120
  **`UNIQUE (a, b)` does not constrain rows where `b` is `NULL`** — SQL treats every `NULL` as distinct, so a composite unique over a nullable discriminator silently permits exactly the duplicates it was introduced to keep apart. Write one partial index per scope instead:
@@ -150,12 +126,6 @@ CREATE UNIQUE INDEX <t>_child_slug_unique ON <t> (child_id, slug) WHERE child_
150
126
 
151
127
  **Prove a uniqueness claim by inserting the duplicate it forbids** — a schema review cannot tell a firing index from a silent one by reading.
152
128
 
153
- ## Column types (SQLite / D1)
154
-
155
- - **Timestamps** — `created_at` + `updated_at`, both `TEXT NOT NULL DEFAULT (datetime('now'))`, on **every** table — except append-only logs, which carry only `created_at`.
156
- - **Booleans** — `INTEGER` (Drizzle `integer(…, { mode: "boolean" })`); the `is_*` / `has_*` names are § Naming.
157
- - **JSON** — `TEXT` (Drizzle `text(…, { mode: "json" })`).
158
-
159
129
  ## Vocabulary / lookup tables
160
130
 
161
131
  A controlled vocabulary earns a **table** only when a user row FKs into it and reads its `label` / `description` at runtime — a service catalog rendered in a picker; static dev-authored config nobody references by FK stays **config** (§ What earns a table) — a `doc_type` value list. Enumerable is not tabular, and a `type`-style column validated against the constants file beats an FK to a seeded mirror. An earned table has a fixed shape: `id` (UUID) · `slug` (UNIQUE) · `label` · optional `description` · `status` · timestamps.
@@ -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
 
@@ -2,7 +2,7 @@
2
2
  name: lld-mode
3
3
  description: LLD researcher for Feature and Product workflows. Use to explore the codebase and gather the context needed for writing a feature LLD (low-level design — the feature doc in docs/features/). Returns research findings — does not write files.
4
4
  tools: Read, Glob, Grep, WebSearch, WebFetch
5
- model: inherit
5
+ model: opus
6
6
  skills:
7
7
  - documentation
8
8
  ---
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: maintenance
3
3
  description: The single docs steward. Default behavior is the docs-vs-code coherence audit — detects and fixes docs drifting from code (stale docs, broken ownership, unowned source paths, architecture/backlog drift). An additive **baseline mode** layers structural reconcile on top: when the repo's `harness-version` marker is missing or behind, it converges `docs/` to the installed harness shape (scaffold the standard tree, fold dissolved-shape artifacts forward, stamp the version, propose modules) before running the audit. Mode is auto-selected from the marker. Run weekly, after major changes, or to baseline/upgrade a repo. Cross-cutting contract analysis is the `boundary` skill (`check` for plans, `scan` for diffs) — it derives contracts dynamically from source; there are no persisted boundary docs to audit. Triggers on "run maintenance", "docs health", "baseline this repo", "reconcile the harness", "upgrade the harness", "is this repo on the current harness", "/maintenance".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Maintenance Agent
@@ -2,7 +2,7 @@
2
2
  name: retro
3
3
  description: Session-distillation agent for the changelog. Reads the per-unit-of-work entries under `docs/history/changelog/` accumulated since its last run and distills the refinement delta — what user-driven iteration added that the one-shot build lacked — into a typed punch list of doc-content gaps, doc-structure moves (missing doc / mothership split / merge), skill-evolution findings backed by survived-vs-overridden counts, unpinned discoveries needing an enforced artifact (test, token, guardrail, schema), and cross-project harness rules. Proposes only — actionable items land in the backlog, never applied directly. Run on demand: before a brainstorm or LLD session, before starting a new project, or after a refinement arc. Triggers on "run retro", "distill the changelog", "refinement retro", "/retro".
4
4
  tools: Read, Glob, Grep, Bash, Write, Edit
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Retro — changelog distillation
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "delivery",
3
3
  "name": "delivery",
4
- "version": "0.1.6",
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": "0480314707cd"
11
+ "digest": "a2c8df3022bd"
12
12
  }
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: change-review
3
3
  description: Velocity + change-risk analysis. First classifies recent commits into a pipe-delimited velocity/severity CSV by dispatching parallel low-cost subagents (module, feature, impact, severity, churn), then derives a commit heatmap, risk zones (high churn in contract-heavy code), stabilization signals, and team/bus-factor distribution. Self-contained — produces its own CSV, no separate git-history step. Use when planning where to invest in review focus or rules.
4
4
  tools: Bash, Read, Write, Glob, Grep, Task
5
- model: inherit
5
+ model: sonnet
6
6
  ---
7
7
 
8
8
  # Change-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: lld-mode
3
3
  description: LLD researcher for Feature and Product workflows. Use to explore the codebase and gather the context needed for writing a feature LLD (low-level design — the feature doc in docs/features/). Returns research findings — does not write files.
4
4
  tools: Read, Glob, Grep, WebSearch, WebFetch
5
- model: inherit
5
+ model: opus
6
6
  skills:
7
7
  - documentation
8
8
  ---
@@ -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.
@@ -2,7 +2,7 @@
2
2
  name: maintenance
3
3
  description: The single docs steward. Default behavior is the docs-vs-code coherence audit — detects and fixes docs drifting from code (stale docs, broken ownership, unowned source paths, architecture/backlog drift). An additive **baseline mode** layers structural reconcile on top: when the repo's `harness-version` marker is missing or behind, it converges `docs/` to the installed harness shape (scaffold the standard tree, fold dissolved-shape artifacts forward, stamp the version, propose modules) before running the audit. Mode is auto-selected from the marker. Run weekly, after major changes, or to baseline/upgrade a repo. Cross-cutting contract analysis is the `boundary` skill (`check` for plans, `scan` for diffs) — it derives contracts dynamically from source; there are no persisted boundary docs to audit. Triggers on "run maintenance", "docs health", "baseline this repo", "reconcile the harness", "upgrade the harness", "is this repo on the current harness", "/maintenance".
4
4
  tools: Read, Glob, Grep, Edit, Write, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Maintenance Agent
@@ -2,7 +2,7 @@
2
2
  name: performance-review
3
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.
4
4
  tools: Read, Glob, Grep, Bash
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Performance-Review Agent
@@ -2,7 +2,7 @@
2
2
  name: retro
3
3
  description: Session-distillation agent for the changelog. Reads the per-unit-of-work entries under `docs/history/changelog/` accumulated since its last run and distills the refinement delta — what user-driven iteration added that the one-shot build lacked — into a typed punch list of doc-content gaps, doc-structure moves (missing doc / mothership split / merge), skill-evolution findings backed by survived-vs-overridden counts, unpinned discoveries needing an enforced artifact (test, token, guardrail, schema), and cross-project harness rules. Proposes only — actionable items land in the backlog, never applied directly. Run on demand: before a brainstorm or LLD session, before starting a new project, or after a refinement arc. Triggers on "run retro", "distill the changelog", "refinement retro", "/retro".
4
4
  tools: Read, Glob, Grep, Bash, Write, Edit
5
- model: inherit
5
+ model: opus
6
6
  ---
7
7
 
8
8
  # Retro — changelog distillation
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "harness": "etna",
3
3
  "name": "etna",
4
- "version": "1.3.9",
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": "96095ea9c98e"
11
+ "digest": "171db7dbad6a"
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.
@@ -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.56",
3
+ "version": "0.19.58",
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": "e7493c40d358"
45
+ "etnaFingerprint": "411f905588e3"
46
46
  }