@nurix/etna 0.19.65 → 0.19.67

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 (250) hide show
  1. package/bin/etna.js +10 -1
  2. package/data/harness-index.json +1 -5
  3. package/data/harnesses/code/CLAUDE.md +3 -1
  4. package/data/harnesses/code/agents/maintenance.md +4 -1
  5. package/data/harnesses/code/harness.json +7 -2
  6. package/data/harnesses/code/hooks/check-completion-integrity.mjs +8 -0
  7. package/data/harnesses/code/hooks/compliance.mjs +139 -27
  8. package/data/harnesses/code/hooks/lib/frozen.mjs +3 -0
  9. package/data/harnesses/code/hooks/lib/ledger.mjs +31 -0
  10. package/data/harnesses/code/hooks/lib/ownership.mjs +3 -1
  11. package/data/harnesses/code/hooks/lib/session-log.mjs +75 -0
  12. package/data/harnesses/code/rules/changelog.md +2 -0
  13. package/data/harnesses/code/skills/documentation/SKILL.md +1 -0
  14. package/data/harnesses/code/skills/documentation/references/frontmatter.md +1 -1
  15. package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +2 -2
  16. package/data/harnesses/code/skills/documentation/templates/Readme.md +1 -0
  17. package/data/harnesses/code/skills/documentation/templates/behavioral-checklist.md +2 -0
  18. package/data/harnesses/code/skills/documentation/templates/changelog-entry.md +2 -2
  19. package/data/harnesses/code/skills/documentation/templates/feature.md +1 -0
  20. package/data/harnesses/code/skills/documentation/templates/implementation-map.md +42 -0
  21. package/data/harnesses/code/skills/testing/SKILL.md +5 -5
  22. package/data/harnesses/delivery/agents/maintenance.md +4 -1
  23. package/data/harnesses/delivery/harness.json +2 -2
  24. package/data/harnesses/delivery/rules/changelog.md +2 -0
  25. package/data/harnesses/delivery/skills/documentation/SKILL.md +1 -0
  26. package/data/harnesses/delivery/skills/documentation/references/frontmatter.md +1 -1
  27. package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +2 -2
  28. package/data/harnesses/delivery/skills/documentation/templates/Readme.md +1 -0
  29. package/data/harnesses/delivery/skills/documentation/templates/behavioral-checklist.md +2 -0
  30. package/data/harnesses/delivery/skills/documentation/templates/changelog-entry.md +2 -2
  31. package/data/harnesses/delivery/skills/documentation/templates/feature.md +1 -0
  32. package/data/harnesses/delivery/skills/documentation/templates/implementation-map.md +42 -0
  33. package/data/harnesses/etna/agents/maintenance.md +4 -1
  34. package/data/harnesses/etna/harness.json +2 -2
  35. package/data/harnesses/etna/rules/changelog.md +2 -0
  36. package/data/harnesses/etna/skills/documentation/SKILL.md +1 -0
  37. package/data/harnesses/etna/skills/documentation/references/frontmatter.md +1 -1
  38. package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +2 -2
  39. package/data/harnesses/etna/skills/documentation/templates/Readme.md +1 -0
  40. package/data/harnesses/etna/skills/documentation/templates/behavioral-checklist.md +2 -0
  41. package/data/harnesses/etna/skills/documentation/templates/changelog-entry.md +2 -2
  42. package/data/harnesses/etna/skills/documentation/templates/feature.md +1 -0
  43. package/data/harnesses/etna/skills/documentation/templates/implementation-map.md +42 -0
  44. package/lib/install/materialize.mjs +51 -57
  45. package/package.json +2 -2
  46. package/data/harnesses/apollo/.mcp.json +0 -8
  47. package/data/harnesses/apollo/.prettierignore +0 -29
  48. package/data/harnesses/apollo/.prettierrc.json +0 -3
  49. package/data/harnesses/apollo/.vscode/settings.json +0 -19
  50. package/data/harnesses/apollo/CLAUDE.md +0 -162
  51. package/data/harnesses/apollo/agents/bookkeeper.md +0 -28
  52. package/data/harnesses/apollo/agents/brainstorm.md +0 -144
  53. package/data/harnesses/apollo/agents/change-review.md +0 -251
  54. package/data/harnesses/apollo/agents/lld-mode.md +0 -58
  55. package/data/harnesses/apollo/agents/maintenance.md +0 -233
  56. package/data/harnesses/apollo/agents/performance-review.md +0 -223
  57. package/data/harnesses/apollo/agents/retro.md +0 -46
  58. package/data/harnesses/apollo/codex/agents/bookkeeper.toml +0 -26
  59. package/data/harnesses/apollo/codex/agents/brainstorm.toml +0 -142
  60. package/data/harnesses/apollo/codex/agents/change-review.toml +0 -249
  61. package/data/harnesses/apollo/codex/agents/lld-mode.toml +0 -52
  62. package/data/harnesses/apollo/codex/agents/maintenance.toml +0 -228
  63. package/data/harnesses/apollo/codex/agents/performance-review.toml +0 -222
  64. package/data/harnesses/apollo/codex/agents/retro.toml +0 -45
  65. package/data/harnesses/apollo/harness.json +0 -19
  66. package/data/harnesses/apollo/hooks/approved-packages.md +0 -44
  67. package/data/harnesses/apollo/hooks/awareness.mjs +0 -405
  68. package/data/harnesses/apollo/hooks/check-bookkeeping-clean.mjs +0 -19
  69. package/data/harnesses/apollo/hooks/check-changelog-presence.mjs +0 -57
  70. package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +0 -71
  71. package/data/harnesses/apollo/hooks/check-ownership.mjs +0 -22
  72. package/data/harnesses/apollo/hooks/compliance.mjs +0 -457
  73. package/data/harnesses/apollo/hooks/hooks.json +0 -23
  74. package/data/harnesses/apollo/hooks/lib/frozen.mjs +0 -58
  75. package/data/harnesses/apollo/hooks/lib/ownership.mjs +0 -242
  76. package/data/harnesses/apollo/hooks/lib/state.mjs +0 -170
  77. package/data/harnesses/apollo/rules/changelog.md +0 -25
  78. package/data/harnesses/apollo/rules/database.md +0 -34
  79. package/data/harnesses/apollo/rules/frontend.md +0 -30
  80. package/data/harnesses/apollo/rules/general.md +0 -40
  81. package/data/harnesses/apollo/rules/nomenclature.md +0 -30
  82. package/data/harnesses/apollo/rules/pnpm.md +0 -57
  83. package/data/harnesses/apollo/rules/testing.md +0 -64
  84. package/data/harnesses/apollo/skills/boundary/SKILL.md +0 -75
  85. package/data/harnesses/apollo/skills/boundary/references/index.md +0 -16
  86. package/data/harnesses/apollo/skills/boundary/references/mechanisms.md +0 -99
  87. package/data/harnesses/apollo/skills/boundary/references/packs/async-messaging.md +0 -81
  88. package/data/harnesses/apollo/skills/boundary/references/packs/auth.md +0 -93
  89. package/data/harnesses/apollo/skills/boundary/references/packs/build-config-runtime.md +0 -93
  90. package/data/harnesses/apollo/skills/boundary/references/packs/cloudflare.md +0 -93
  91. package/data/harnesses/apollo/skills/boundary/references/packs/frontend.md +0 -93
  92. package/data/harnesses/apollo/skills/boundary/references/packs/generic-tail.md +0 -55
  93. package/data/harnesses/apollo/skills/boundary/references/packs/persistence.md +0 -93
  94. package/data/harnesses/apollo/skills/boundary/references/packs/wire-api.md +0 -87
  95. package/data/harnesses/apollo/skills/brainstorm/README.md +0 -56
  96. package/data/harnesses/apollo/skills/brainstorm/SKILL.md +0 -173
  97. package/data/harnesses/apollo/skills/brainstorm/references/doctrine.md +0 -98
  98. package/data/harnesses/apollo/skills/brainstorm/references/harvest.md +0 -86
  99. package/data/harnesses/apollo/skills/brainstorm/references/session-schema.md +0 -186
  100. package/data/harnesses/apollo/skills/brainstorm/scripts/detector.mjs +0 -460
  101. package/data/harnesses/apollo/skills/design/SKILL.md +0 -121
  102. package/data/harnesses/apollo/skills/design/components/charts.md +0 -76
  103. package/data/harnesses/apollo/skills/design/components/collections.md +0 -132
  104. package/data/harnesses/apollo/skills/design/components/feedback.md +0 -47
  105. package/data/harnesses/apollo/skills/design/components/hooks.md +0 -35
  106. package/data/harnesses/apollo/skills/design/components/index.md +0 -143
  107. package/data/harnesses/apollo/skills/design/components/inputs.md +0 -327
  108. package/data/harnesses/apollo/skills/design/components/layout.md +0 -52
  109. package/data/harnesses/apollo/skills/design/components/media.md +0 -66
  110. package/data/harnesses/apollo/skills/design/components/motion.md +0 -82
  111. package/data/harnesses/apollo/skills/design/components/navigation.md +0 -29
  112. package/data/harnesses/apollo/skills/design/components/overlays.md +0 -72
  113. package/data/harnesses/apollo/skills/design/components/setup.md +0 -40
  114. package/data/harnesses/apollo/skills/design/components/typography.md +0 -46
  115. package/data/harnesses/apollo/skills/design/components/utils.md +0 -37
  116. package/data/harnesses/apollo/skills/design/data/blueprints/browse.md +0 -38
  117. package/data/harnesses/apollo/skills/design/data/blueprints/creation-flow.md +0 -38
  118. package/data/harnesses/apollo/skills/design/data/blueprints/dashboard.md +0 -38
  119. package/data/harnesses/apollo/skills/design/data/blueprints/grid.md +0 -38
  120. package/data/harnesses/apollo/skills/design/data/blueprints/queue.md +0 -38
  121. package/data/harnesses/apollo/skills/design/data/blueprints/record-detail.md +0 -38
  122. package/data/harnesses/apollo/skills/design/data/blueprints/settings.md +0 -38
  123. package/data/harnesses/apollo/skills/design/data/charts/census.csv +0 -58
  124. package/data/harnesses/apollo/skills/design/data/checklists/browse.md +0 -21
  125. package/data/harnesses/apollo/skills/design/data/checklists/creation-flow.md +0 -20
  126. package/data/harnesses/apollo/skills/design/data/checklists/dashboard.md +0 -21
  127. package/data/harnesses/apollo/skills/design/data/checklists/grid.md +0 -21
  128. package/data/harnesses/apollo/skills/design/data/checklists/queue.md +0 -21
  129. package/data/harnesses/apollo/skills/design/data/checklists/record-detail.md +0 -21
  130. package/data/harnesses/apollo/skills/design/data/checklists/settings.md +0 -21
  131. package/data/harnesses/apollo/skills/design/data/chrome/board-column.md +0 -68
  132. package/data/harnesses/apollo/skills/design/data/chrome/data-table.md +0 -63
  133. package/data/harnesses/apollo/skills/design/data/chrome/side-panel.md +0 -60
  134. package/data/harnesses/apollo/skills/design/data/chrome/sidebar.md +0 -78
  135. package/data/harnesses/apollo/skills/design/data/chrome/stat-cards.md +0 -62
  136. package/data/harnesses/apollo/skills/design/data/chrome/topbar.md +0 -65
  137. package/data/harnesses/apollo/skills/design/data/elevation/ant-design-directional-shadow.md +0 -19
  138. package/data/harnesses/apollo/skills/design/data/elevation/apple-liquid-glass-material.md +0 -18
  139. package/data/harnesses/apollo/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +0 -20
  140. package/data/harnesses/apollo/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +0 -20
  141. package/data/harnesses/apollo/skills/design/data/elevation/github-primer-sparing-shadow.md +0 -19
  142. package/data/harnesses/apollo/skills/design/data/elevation/ibm-carbon-layer-and-border.md +0 -19
  143. package/data/harnesses/apollo/skills/design/data/elevation/linear-dark-surface-ladder.md +0 -17
  144. package/data/harnesses/apollo/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +0 -19
  145. package/data/harnesses/apollo/skills/design/data/elevation/material-3-tonal-elevation.md +0 -21
  146. package/data/harnesses/apollo/skills/design/data/elevation/neobrutalist-hard-offset.md +0 -19
  147. package/data/harnesses/apollo/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +0 -18
  148. package/data/harnesses/apollo/skills/design/data/elevation/notion-warm-minimal-hairline.md +0 -17
  149. package/data/harnesses/apollo/skills/design/data/elevation/polaris-elevation-inset-bevel.md +0 -21
  150. package/data/harnesses/apollo/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +0 -19
  151. package/data/harnesses/apollo/skills/design/data/elevation/shadcn-neutral-derived-radius.md +0 -19
  152. package/data/harnesses/apollo/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +0 -18
  153. package/data/harnesses/apollo/skills/design/data/elevation/tailwind-default-utility-scale.md +0 -19
  154. package/data/harnesses/apollo/skills/design/data/elevation/vercel-geist-shadow-as-border.md +0 -20
  155. package/data/harnesses/apollo/skills/design/data/iconography.md +0 -199
  156. package/data/harnesses/apollo/skills/design/data/motion/accordion-expand.md +0 -18
  157. package/data/harnesses/apollo/skills/design/data/motion/destructive-hold-confirm.md +0 -18
  158. package/data/harnesses/apollo/skills/design/data/motion/drag-dismiss-momentum.md +0 -18
  159. package/data/harnesses/apollo/skills/design/data/motion/empty-state-arrive.md +0 -18
  160. package/data/harnesses/apollo/skills/design/data/motion/hover-lift.md +0 -18
  161. package/data/harnesses/apollo/skills/design/data/motion/modal-center-arrive.md +0 -18
  162. package/data/harnesses/apollo/skills/design/data/motion/number-ticker-roll.md +0 -18
  163. package/data/harnesses/apollo/skills/design/data/motion/onboarding-bounce.md +0 -18
  164. package/data/harnesses/apollo/skills/design/data/motion/panel-slide.md +0 -18
  165. package/data/harnesses/apollo/skills/design/data/motion/press-feedback.md +0 -18
  166. package/data/harnesses/apollo/skills/design/data/motion/progress-linear-loop.md +0 -18
  167. package/data/harnesses/apollo/skills/design/data/motion/route-crossfade.md +0 -18
  168. package/data/harnesses/apollo/skills/design/data/motion/rubber-band-overscroll.md +0 -18
  169. package/data/harnesses/apollo/skills/design/data/motion/skeleton-shimmer.md +0 -18
  170. package/data/harnesses/apollo/skills/design/data/motion/stagger-arrive.md +0 -18
  171. package/data/harnesses/apollo/skills/design/data/motion/toast-enter-stack.md +0 -18
  172. package/data/harnesses/apollo/skills/design/data/motion/tooltip-chain-instant.md +0 -18
  173. package/data/harnesses/apollo/skills/design/data/motion/trigger-anchored-reveal.md +0 -18
  174. package/data/harnesses/apollo/skills/design/data/tells.csv +0 -34
  175. package/data/harnesses/apollo/skills/design/data/voices/bright-collaborator.md +0 -29
  176. package/data/harnesses/apollo/skills/design/data/voices/direct-operator.md +0 -29
  177. package/data/harnesses/apollo/skills/design/data/voices/earnest-helper.md +0 -29
  178. package/data/harnesses/apollo/skills/design/data/voices/editorial-composed.md +0 -29
  179. package/data/harnesses/apollo/skills/design/data/voices/quiet-concierge.md +0 -29
  180. package/data/harnesses/apollo/skills/design/data/voices/warm-guide.md +0 -29
  181. package/data/harnesses/apollo/skills/design/references/compose.md +0 -23
  182. package/data/harnesses/apollo/skills/design/references/content.md +0 -28
  183. package/data/harnesses/apollo/skills/design/references/emission.md +0 -21
  184. package/data/harnesses/apollo/skills/design/references/existing-apps.md +0 -27
  185. package/data/harnesses/apollo/skills/design/references/foundations.md +0 -57
  186. package/data/harnesses/apollo/skills/design/references/intake.md +0 -40
  187. package/data/harnesses/apollo/skills/design/references/object-pass.md +0 -24
  188. package/data/harnesses/apollo/skills/design/references/ship-gate.md +0 -29
  189. package/data/harnesses/apollo/skills/design/references/ux-patterns/Readme.md +0 -195
  190. package/data/harnesses/apollo/skills/design/references/ux-patterns/ai-layer.md +0 -49
  191. package/data/harnesses/apollo/skills/design/references/ux-patterns/attributes-lifecycle.md +0 -43
  192. package/data/harnesses/apollo/skills/design/references/ux-patterns/container-choice.md +0 -44
  193. package/data/harnesses/apollo/skills/design/references/ux-patterns/creation-paths.md +0 -46
  194. package/data/harnesses/apollo/skills/design/references/ux-patterns/dashboards.md +0 -42
  195. package/data/harnesses/apollo/skills/design/references/ux-patterns/modes.md +0 -42
  196. package/data/harnesses/apollo/skills/design/references/ux-patterns/object-model.md +0 -46
  197. package/data/harnesses/apollo/skills/design/references/ux-patterns/page-archetype.md +0 -48
  198. package/data/harnesses/apollo/skills/design/references/ux-patterns/promotion-demotion.md +0 -37
  199. package/data/harnesses/apollo/skills/design/references/ux-patterns/settings.md +0 -42
  200. package/data/harnesses/apollo/skills/design/references/ux-patterns/standard-pages.md +0 -46
  201. package/data/harnesses/apollo/skills/design/references/ux-patterns/state-matrix.md +0 -41
  202. package/data/harnesses/apollo/skills/design/references/ux-patterns/table-mechanics.md +0 -48
  203. package/data/harnesses/apollo/skills/design/references/ux-patterns/verbs.md +0 -45
  204. package/data/harnesses/apollo/skills/design/references/ux-patterns/viewer-context.md +0 -40
  205. package/data/harnesses/apollo/skills/design/scripts/README-validator.md +0 -27
  206. package/data/harnesses/apollo/skills/design/scripts/select-chart.mjs +0 -622
  207. package/data/harnesses/apollo/skills/design/scripts/validate-design-md.mjs +0 -1198
  208. package/data/harnesses/apollo/skills/documentation/SKILL.md +0 -134
  209. package/data/harnesses/apollo/skills/documentation/references/examples.md +0 -279
  210. package/data/harnesses/apollo/skills/documentation/references/frontmatter.md +0 -44
  211. package/data/harnesses/apollo/skills/documentation/references/instruction-style.md +0 -50
  212. package/data/harnesses/apollo/skills/documentation/references/style-guide.md +0 -384
  213. package/data/harnesses/apollo/skills/documentation/references/untemplated.md +0 -38
  214. package/data/harnesses/apollo/skills/documentation/scripts/doc-lint.mjs +0 -140
  215. package/data/harnesses/apollo/skills/documentation/templates/Readme.md +0 -34
  216. package/data/harnesses/apollo/skills/documentation/templates/api-reference.md +0 -20
  217. package/data/harnesses/apollo/skills/documentation/templates/architecture-decision.md +0 -121
  218. package/data/harnesses/apollo/skills/documentation/templates/backlog.md +0 -33
  219. package/data/harnesses/apollo/skills/documentation/templates/behavioral-checklist.md +0 -79
  220. package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +0 -18
  221. package/data/harnesses/apollo/skills/documentation/templates/changelog-entry.md +0 -57
  222. package/data/harnesses/apollo/skills/documentation/templates/changelog-index.md +0 -41
  223. package/data/harnesses/apollo/skills/documentation/templates/docs-readme.md +0 -72
  224. package/data/harnesses/apollo/skills/documentation/templates/domain-model.md +0 -30
  225. package/data/harnesses/apollo/skills/documentation/templates/environment-registry.md +0 -15
  226. package/data/harnesses/apollo/skills/documentation/templates/feature.md +0 -149
  227. package/data/harnesses/apollo/skills/documentation/templates/glossary.md +0 -11
  228. package/data/harnesses/apollo/skills/documentation/templates/journal.md +0 -13
  229. package/data/harnesses/apollo/skills/documentation/templates/memory-main.md +0 -25
  230. package/data/harnesses/apollo/skills/documentation/templates/migration-runbook.md +0 -34
  231. package/data/harnesses/apollo/skills/documentation/templates/non-goals.md +0 -11
  232. package/data/harnesses/apollo/skills/documentation/templates/operations-procedure.md +0 -36
  233. package/data/harnesses/apollo/skills/documentation/templates/plan.md +0 -88
  234. package/data/harnesses/apollo/skills/documentation/templates/protected-files.md +0 -37
  235. package/data/harnesses/apollo/skills/documentation/templates/root-readme.md +0 -104
  236. package/data/harnesses/apollo/skills/documentation/templates/security-overview.md +0 -27
  237. package/data/harnesses/apollo/skills/documentation/templates/structure.md +0 -65
  238. package/data/harnesses/apollo/skills/documentation/templates/system-overview.md +0 -32
  239. package/data/harnesses/apollo/skills/documentation/templates/testing-strategy.md +0 -29
  240. package/data/harnesses/apollo/skills/schema-design/SKILL.md +0 -141
  241. package/data/harnesses/apollo/skills/test/SKILL.md +0 -104
  242. package/data/harnesses/apollo/skills/testing/SKILL.md +0 -166
  243. package/data/harnesses/apollo/skills/workflow-mapper/SKILL.md +0 -451
  244. package/data/harnesses/apollo/skills/workflow-mapper/references/patterns.md +0 -214
  245. /package/data/harnesses/{apollo → code}/docs/README.md +0 -0
  246. /package/data/harnesses/{apollo → code}/docs/history/CHANGELOG.md +0 -0
  247. /package/data/harnesses/{apollo → code}/docs/history/backlog.md +0 -0
  248. /package/data/harnesses/{apollo → code}/docs/memory/main.md +0 -0
  249. /package/data/harnesses/{apollo → code}/docs/structure.md +0 -0
  250. /package/data/harnesses/{apollo → code}/rules/cloudflare.md +0 -0
package/bin/etna.js CHANGED
@@ -232,7 +232,16 @@ async function main(args) {
232
232
  await sleep(2000);
233
233
  boot.stop('Ready');
234
234
 
235
- const harnessName = await resolveHarnessName(name, harnesses);
235
+ // Transition alias: the `apollo` harness merged into `code` (the code harness is pre-baked —
236
+ // it ships the docs scaffold as seeds and auto-baselines on first contact). Scripts and docs
237
+ // that pin the old name keep working through the alias; it retires after one window.
238
+ let requested = name;
239
+ if (requested === 'apollo' && !harnesses.apollo) {
240
+ p.log.info('`apollo` merged into `code` — installing `code` (the pre-baked harness).');
241
+ requested = 'code';
242
+ }
243
+
244
+ const harnessName = await resolveHarnessName(requested, harnesses);
236
245
  if (!harnessName) return; // user cancelled the interactive pick
237
246
 
238
247
  await installHarness(harnessName, harnesses);
@@ -1,10 +1,6 @@
1
1
  {
2
- "fingerprint": "b0d947332bf0",
2
+ "fingerprint": "546f4bccb873",
3
3
  "harnesses": {
4
- "apollo": {
5
- "plugin": "apollo",
6
- "description": "the pre-baked `code` harness, built for Apollo Studio exports"
7
- },
8
4
  "code": {
9
5
  "plugin": "code",
10
6
  "description": "the foundation every Nurix project starts with"
@@ -6,6 +6,8 @@ How AI coding agents — any vendor, any runtime — work on this project. Rules
6
6
 
7
7
  ## Maintenance
8
8
 
9
+ **Baseline first**: if `docs/structure.md` lacks a `harness-version` frontmatter marker, or it is behind the `version` in `.claude/harness.json`, dispatch the `maintenance` agent **as a sub-agent** before taking up the user's first ask — baseline is bulky mechanical work, and dispatching it keeps this context clean for the ask. Tell the user what it will do; on a repo with existing docs or `.bkp` files, name what the fold will touch. No dispatch mechanism ⇒ run it inline.
10
+
9
11
  Invoke the `maintenance` agent when a mechanical condition holds: `docs/structure.md` is missing (the repo isn't reconciled — baseline first), the `harness-version` marker is behind the installed version, a doc contradicts code being touched, or source paths are unowned — not on prose that merely looks stale. Structural reconciliation is never done by hand — the agent auto-selects its mode (baseline when the marker is missing or behind, coherence audit otherwise) and hands back; if it returns a follow-up (typically the module proposal), re-invoke it to finish. Single-glob or single-line doc corrections are done inline, per § Documentation. Each component's internals live in the component, not here.
10
12
 
11
13
  ---
@@ -50,7 +52,7 @@ These four mechanisms work in tandem to keep the repo self-updating and to preve
50
52
 
51
53
  ### Ownership — route before editing
52
54
 
53
- Which doc is canonical for which code paths is the **feature doc's own frontmatter**. A feature doc (flat in `docs/features/`) carries `owns:` (code-path globs — the only ownership key), `module:` (a bounded-context label, omitted when cross-cutting), `keywords:` (discovery), and `status:`. Code → doc ownership is the union of every feature doc's `owns:` globs, surfaced as the generated ownership index at session start. A module is tied to code by the `owns:` globs of the docs carrying its `module:` label — not a folder, not a separate map. Architecture docs describe cross-cutting patterns and own no files; operations docs describe procedures.
55
+ Which doc is canonical for which code paths is the **feature doc's own frontmatter**. A feature doc (flat in `docs/features/`) carries `owns:` (code-path globs — the only ownership key), `module:` (a bounded-context label, omitted when cross-cutting), `keywords:` (discovery), and `status:`. Code → doc ownership is the union of every feature doc's `owns:` globs, surfaced as the generated ownership index at session start. A module is tied to code by the `owns:` globs of the docs carrying its `module:` label — not a folder, not a separate map. Architecture docs describe cross-cutting patterns and own no files; operations docs describe procedures. Every doc carries `type:` — its genre, stamped by document generators or at authoring, read by tooling (UI grouping, protected-file mapping); the `maintenance` agent stamps it where missing. Like `last-reviewed:`, it is tooling metadata: preserve it on edits, never inject or narrate it.
54
56
 
55
57
  Before modifying code in an unfamiliar area:
56
58
 
@@ -51,7 +51,7 @@ The ownership source of truth is the **union of every feature doc's `owns:` fron
51
51
  ### Step 1: Build the ownership map
52
52
 
53
53
  1. Read `docs/structure.md` for the authority map.
54
- 2. Glob `docs/features/**/*.md`; for each, parse the frontmatter `owns:` globs and `module:` label.
54
+ 2. Glob `docs/features/**/*.md`, ignoring `*.bkp.*` files (installer backups awaiting the fold); for each, parse the frontmatter `owns:` globs and `module:` label.
55
55
  3. Build a map: glob → owning doc + module. Narrower glob wins on overlap. **This IS the source of truth.**
56
56
 
57
57
  **Architecture-first fallback.** If no feature doc carries `owns:` yet (a fresh-baseline repo can carry thousands of LOC and zero feature docs), drive this pass's ownership/drift surface off `docs/architecture/**` file-references instead — they become the **primary** ownership map for Steps 2–5. The `owns:` map becomes primary the moment any feature doc declares ownership — this is a fallback, not a replacement.
@@ -62,6 +62,7 @@ For each `owns:` glob across the feature docs:
62
62
 
63
63
  - Verify the glob matches existing files. Report globs matching nothing, deleted paths, and orphaned docs.
64
64
  - Flag **equal-specificity overlaps** — two feature docs whose `owns:` globs claim the same paths at the same specificity (an ambiguous owner). Surface as a question.
65
+ - **Stamp `type:` where missing.** Every doc carries a `type:` genre key (enum = the `documentation` skill's template names); docs predating the key are stamped mechanically — the genre is readable from placement and shape (a `docs/features/` doc is `type: feature`, an AD is `type: architecture-decision`, and so on). Stamping is remediation, not a question; an ambiguous genre is backlogged instead.
65
66
 
66
67
  For each code symbol or path a doc explicitly **names** (function/export/route/type):
67
68
 
@@ -185,6 +186,8 @@ This is what makes baseline more than a fresh scaffold. Detect old-shape artifac
185
186
  | `docs/architecture/overview.md` | split into `domain-model.md` / `glossary.md` / `non-goals.md` / `references.md` / `open-questions.md` |
186
187
  | `docs/architecture/adr/` (legacy folder name), or `ADR-NNN-*.md` records anywhere under `docs/architecture/` | **rename to the AD genre.** The folder becomes `docs/architecture/decisions/`; each record file `ADR-NNN-{slug}.md` becomes `AD-NNN-{slug}.md`, keeping its number and slug. Inside each record, update the retired vocabulary — "ADR", "Architecture Decision Record", and any supersession framing ("once accepted, an ADR is never edited; a later ADR supersedes it") — to the AD model: one AD per subject, **updated in place**, its journey confined to a `Decision Changelog` tail. A record's **reasoning is never rewritten** by a reconcile; only its genre vocabulary, its own title/id, and paths it cites. The folder's `Readme.md` is an **index, not a record** — reframe its genre description while preserving authored content such as a candidate-decision list verbatim. Repoint live references **inside `docs/`**, including one AD citing another; leave frozen records' links as point-in-time. A live reference **outside** your write scope — most often the root `README.md`, which you never overwrite — cannot be repointed: record it as a backlog item naming the file and line rather than leaving the residue silent |
187
188
  | a legacy `docs/brainstorm/{topic}/`, a singular `docs/history/brainstorm/`, or a root `brainstorm/`/`brainstorming/` folder | move each **active** record to `docs/research/{topic}-brainstorm/` and each **harvested** one to `docs/history/brainstorms/{topic}/` (dropping the suffix); create/refresh both `Readme.md` indexes; delete the emptied folder |
189
+ | `*.bkp.*` files under `docs/` (the installer's first-contact backups of the project's pre-existing files) | **fold the content, then delete the backup.** Classify each backup's content by nature and land it in the current shape — a backed-up changelog's rows into `docs/history/CHANGELOG.md` and per-entry files, a backed-up structure/readme's real claims into the scaffolded truth doc, a backed-up feature doc's content merged into its sibling (or adopted whole if the sibling is a pristine seed). Content is folded, never discarded silently; what cannot be classified goes to the backlog with the backup kept. When the fold lands, delete the `.bkp` file |
190
+ | `*.bkp.*` files at the repo root or in config (`CLAUDE.bkp.md`, `.prettierrc.bkp.json`, …) | **never merge these.** Top-level config is harness-owned by design; list each one to the user as their call, and leave the file in place |
188
191
  | no `harness-version` in `structure.md` | stamp it (Step B4) |
189
192
 
190
193
  ### Step B3 — Present the reconcile plan, then execute
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.7.1",
4
+ "version": "3.8.1",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
8
8
  ".vscode/settings.json"
9
9
  ],
10
10
  "seeds": [
11
+ "docs/README.md",
12
+ "docs/structure.md",
13
+ "docs/history/CHANGELOG.md",
14
+ "docs/history/backlog.md",
15
+ "docs/memory/main.md",
11
16
  ".mcp.json"
12
17
  ],
13
- "digest": "105461e25748"
18
+ "digest": "f120a269e99c"
14
19
  }
@@ -7,6 +7,7 @@
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { frontmatter, field } from "./lib/ownership.mjs";
10
+ import { undischargedRows } from "./lib/ledger.mjs";
10
11
 
11
12
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
12
13
  const plansDir = path.join(projectDir, "docs", "history", "plans");
@@ -41,6 +42,13 @@ for (const file of plans) {
41
42
  const unchecked = (text.match(/^- \[ \]/gm) || []).length;
42
43
  if (unchecked > 0) fail(`${file} is Complete with ${unchecked} unchecked Done Criteria box(es)`);
43
44
 
45
+ // The ledger is discharged per row, in the file — a Done Criteria checkbox asserting "every
46
+ // row discharged" is exactly the checkbox-nothing-reads failure the doc ledger existed to fix.
47
+ const rows = undischargedRows(text);
48
+ if (rows.length > 0) {
49
+ fail(`${file} is Complete with ${rows.length} Test Obligations row(s) missing a discharge outcome (#${rows.join(", #")})`);
50
+ }
51
+
44
52
  // EVERY Spec line, not the first. A plan may implement against several feature docs, and a
45
53
  // single-match parse silently exempts all but one — the same singular assumption the Doc
46
54
  // Obligations ledger exists to break, reintroduced here where nothing would surface it.
@@ -11,6 +11,8 @@ import { spawnSync } from "node:child_process";
11
11
  import { readStdin, resolveProjectDir, frontmatter } from "./lib/ownership.mjs";
12
12
  import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state.mjs";
13
13
  import { frozenStatus } from "./lib/frozen.mjs";
14
+ import { undischargedRows } from "./lib/ledger.mjs";
15
+ import { appendEvent, unresolvedDenials, sessionLogPath } from "./lib/session-log.mjs";
14
16
 
15
17
  function readSafe(file) {
16
18
  try {
@@ -38,6 +40,23 @@ function decision(permissionDecision, reason) {
38
40
  };
39
41
  }
40
42
 
43
+ // A deny is also an event in the session gate log — the record that makes going silent after a
44
+ // block impossible. The log is a dated per-session record (never drained; bookkeeping stays a
45
+ // queue); the Stop and commit walls block on `denied` lines with no later `resolved` pair.
46
+ function deny(projectDir, sessionId, gate, target, reason) {
47
+ appendEvent(projectDir, sessionId, "denied", gate, target);
48
+ return decision("deny", reason);
49
+ }
50
+
51
+ // The mechanical half of resolution: when the once-denied operation later succeeds through the
52
+ // gate, machinery writes `retried-ok` itself — the session hand-writes only the judgment claims
53
+ // (`waived → {ref}`, `disclosed → {entry}`).
54
+ function autoResolve(projectDir, sessionId, gate, target) {
55
+ if (unresolvedDenials(projectDir, sessionId).includes(`${gate} · ${target}`)) {
56
+ appendEvent(projectDir, sessionId, "resolved", gate, target, "retried-ok");
57
+ }
58
+ }
59
+
41
60
  // ---------------------------------------------------------------------------
42
61
  // PreToolUse — file gates
43
62
  // ---------------------------------------------------------------------------
@@ -83,40 +102,54 @@ function openGatedPlans(projectDir) {
83
102
 
84
103
  const SOURCE_LIKE = /\.(m?[jt]sx?|py|rs|go|rb|java|kt|swift|c|h|cpp|hpp|cs|sh|sql|vue|svelte)$/;
85
104
 
86
- function fileGate(projectDir, rel, toolName) {
105
+ function fileGate(projectDir, rel, toolName, sessionId) {
87
106
  const frozen = frozenStatus(projectDir, rel);
88
107
  if (frozen.frozen) {
89
- return decision(
90
- "deny",
108
+ return deny(
109
+ projectDir,
110
+ sessionId,
111
+ "frozen-record",
112
+ rel,
91
113
  `BLOCKED — frozen record. \`${rel}\` is committed history and is never edited (${frozen.reason}). A correction is a new dated entry that links back.`
92
114
  );
93
115
  }
94
116
 
95
117
  if (protectedPaths(projectDir).includes(rel)) {
96
- return decision("deny", `BLOCKED — protected file. \`${rel}\` is on the protected list.`);
118
+ return deny(projectDir, sessionId, "protected-file", rel, `BLOCKED — protected file. \`${rel}\` is on the protected list.`);
97
119
  }
98
120
 
99
121
  const inDocs = rel.startsWith("docs/") || rel.startsWith(".claude/") || rel.endsWith(".md");
100
122
  if (!inDocs && SOURCE_LIKE.test(rel)) {
101
123
  for (const plan of openGatedPlans(projectDir)) {
102
124
  if (/Boundary:\s*pending/i.test(plan.gates)) {
103
- return decision(
104
- "deny",
125
+ return deny(
126
+ projectDir,
127
+ sessionId,
128
+ "plan-gate",
129
+ plan.file,
105
130
  `BLOCKED — plan gate. \`${plan.file}\` is open but its Gates line reads 'Boundary: pending'. Run the \`boundary\` skill and record its verdict in the plan before implementation edits.`
106
131
  );
107
132
  }
108
133
  if (!plan.hasLedger) {
109
- return decision(
110
- "deny",
134
+ return deny(
135
+ projectDir,
136
+ sessionId,
137
+ "plan-gate",
138
+ plan.file,
111
139
  `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.`
112
140
  );
113
141
  }
114
142
  if (!plan.hasDocLedger) {
115
- return decision(
116
- "deny",
143
+ return deny(
144
+ projectDir,
145
+ sessionId,
146
+ "plan-gate",
147
+ plan.file,
117
148
  `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
149
  );
119
150
  }
151
+ // The gates pass now — a plan-gate denial recorded earlier is a successful retry.
152
+ autoResolve(projectDir, sessionId, "plan-gate", plan.file);
120
153
  }
121
154
  }
122
155
 
@@ -158,30 +191,52 @@ function stagedExempt(files) {
158
191
  HARNESS_PREFIXES.some((p) => f.startsWith(p)) ||
159
192
  HARNESS_FILES.includes(f) ||
160
193
  f.startsWith("docs/history/changelog/") ||
194
+ f.startsWith("docs/history/sessions/") ||
195
+ f.includes(".bkp.") ||
161
196
  f === "docs/history/CHANGELOG.md"
162
197
  );
163
198
  }
164
199
 
165
- function commitGate(projectDir) {
200
+ function commitGate(projectDir, sessionId) {
166
201
  const staged = (git(projectDir, ["diff", "--cached", "--name-only"]) || "")
167
202
  .split("\n")
168
203
  .map((s) => s.trim())
169
204
  .filter(Boolean);
170
205
  if (staged.length > 0 && !stagedExempt(staged)) {
171
206
  if (!staged.some((f) => f.startsWith("docs/history/changelog/"))) {
172
- return decision(
173
- "deny",
207
+ return deny(
208
+ projectDir,
209
+ sessionId,
210
+ "changelog",
211
+ "git commit",
174
212
  "BLOCKED — changelog. This commit stages no `docs/history/changelog/` entry. Every non-exempt commit carries its entry inside it (exempt: release/bot, harness installs and bumps, lockfile-only, pure renames, changelog housekeeping — state which if claiming one, then stage accordingly or re-run)."
175
213
  );
176
214
  }
177
215
  }
178
216
  const counts = countRows(readQueue(projectDir));
179
217
  if (counts.unresolved > 0) {
218
+ return deny(
219
+ projectDir,
220
+ sessionId,
221
+ "bookkeeping",
222
+ "git commit",
223
+ `BLOCKED — bookkeeping. ${counts.unresolved} unresolved row(s) in docs/operations/bookkeeping.md. Dispatch the bookkeeper, resolve, or convert to \`waived:\` with a reason and expiry.`
224
+ );
225
+ }
226
+
227
+ // This session's own open denials wall the commit too — a commit is the arc's end as much as a
228
+ // stop is, and an undisclosed failure must not ride out inside one.
229
+ const open = unresolvedDenials(projectDir, sessionId).filter((pair) => !pair.startsWith("changelog ·") && !pair.startsWith("bookkeeping ·"));
230
+ if (open.length > 0) {
180
231
  return decision(
181
232
  "deny",
182
- `BLOCKED — bookkeeping. ${counts.unresolved} unresolved row(s) in docs/operations/bookkeeping.md. Dispatch the bookkeeper, resolve, or convert to \`waived:\` with a reason and expiry.`
233
+ `BLOCKED — session gate log. ${open.length} denial(s) this session have no resolved line (${open.join("; ")}). Append \`resolved\` to the session log — retried-ok happens automatically on a successful retry; otherwise \`waived → {ref}\` or \`disclosed → {changelog entry}\`. Never delete log lines.`
183
234
  );
184
235
  }
236
+
237
+ // All commit gates pass — a changelog/bookkeeping denial recorded earlier is a successful retry.
238
+ autoResolve(projectDir, sessionId, "changelog", "git commit");
239
+ autoResolve(projectDir, sessionId, "bookkeeping", "git commit");
185
240
  return null;
186
241
  }
187
242
 
@@ -200,22 +255,40 @@ function approvedPackages(projectDir) {
200
255
  return names;
201
256
  }
202
257
 
203
- function packageGate(projectDir, command) {
204
- const m = new RegExp(String.raw`(?:^|&&|;|\|)\s*(?:sudo\s+)?(?:pnpm|npm|yarn|bun)\s+(?:add|install|i)\s+(.+)`).exec(command);
258
+ // A token must parse as an npm package spec — name or @scope/name, optional @range tail —
259
+ // before it may be judged against the table. Everything else on an install line (redirections
260
+ // like 2>&1, chained commands, flags, paths, URLs) is shell, not a package. A non-candidate is
261
+ // skipped, never blocked: a miss is caught downstream by the lockfile, while a false deny
262
+ // teaches the session the gate is noise exactly where the gate matters most.
263
+ const PKG_SPEC = /^(@[a-z0-9~-][\w.-]*\/)?[a-z0-9~-][\w.-]*(@[\w.^~*<>=|x-]+)?$/;
264
+
265
+ function packageGate(projectDir, command, sessionId) {
266
+ const m = new RegExp(
267
+ String.raw`(?:^|&&|;|\|)\s*(?:sudo\s+)?(?:pnpm|npm|yarn|bun)\s+(?:add|install|i)(?=\s|$)([^&;|]*)`
268
+ ).exec(command);
205
269
  if (!m) return null;
206
270
  const approved = approvedPackages(projectDir);
207
271
  if (approved.size === 0) return null; // no rules file installed — nothing to enforce
208
- const args = m[1].split(/\s+/).filter((a) => a && !a.startsWith("-"));
272
+ const args = (m[1] || "")
273
+ .split(/\s+/)
274
+ .filter((a) => a && !a.startsWith("-") && PKG_SPEC.test(a));
209
275
  for (const arg of args) {
210
276
  const pkg = arg.startsWith("@") ? arg.split("@").slice(0, 2).join("@") : arg.split("@")[0];
211
- if (!pkg || pkg === "." || pkg.includes("/") && !pkg.startsWith("@")) continue;
212
277
  if (!approved.has(pkg)) {
213
- return decision(
214
- "deny",
278
+ return deny(
279
+ projectDir,
280
+ sessionId,
281
+ "package-table",
282
+ pkg,
215
283
  `BLOCKED — package table. \`${pkg}\` is not in the approved table (.claude/hooks/approved-packages.md; test packages: .claude/rules/testing.md). The table changes only by explicit user decision.`
216
284
  );
217
285
  }
218
286
  }
287
+ // Every named package is approved — a package-table denial for one of them is a successful retry.
288
+ for (const arg of args) {
289
+ const pkg = arg.startsWith("@") ? arg.split("@").slice(0, 2).join("@") : arg.split("@")[0];
290
+ autoResolve(projectDir, sessionId, "package-table", pkg);
291
+ }
219
292
  return null;
220
293
  }
221
294
 
@@ -252,7 +325,7 @@ function bashGate(projectDir, command, sessionId) {
252
325
  if (!command) return null;
253
326
 
254
327
  if (statement(String.raw`git\s+commit\b`).test(command)) {
255
- return commitGate(projectDir);
328
+ return commitGate(projectDir, sessionId);
256
329
  }
257
330
 
258
331
  // `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
@@ -277,7 +350,7 @@ function bashGate(projectDir, command, sessionId) {
277
350
  );
278
351
  }
279
352
 
280
- return packageGate(projectDir, command);
353
+ return packageGate(projectDir, command, sessionId);
281
354
  }
282
355
 
283
356
  // ---------------------------------------------------------------------------
@@ -397,13 +470,44 @@ function stopGate(projectDir, payload) {
397
470
  };
398
471
  }
399
472
 
473
+ // Open gate denials wall the stop: silence after a block is the one ending the log exists to
474
+ // make impossible. Resolution appends to the log — retried-ok is machinery-written on a
475
+ // successful retry; waived/disclosed are the session's hand-written, auditable claims.
476
+ const openDenials = unresolvedDenials(projectDir, payload.session_id);
477
+ if (openDenials.length > 0) {
478
+ const rel = path.relative(projectDir, sessionLogPath(projectDir, payload.session_id));
479
+ return {
480
+ decision: "block",
481
+ reason:
482
+ `${openDenials.length} gate denial(s) this session have no resolved line in ${rel}: ${openDenials.join("; ")}. ` +
483
+ "For each, either retry the operation properly (a success appends `retried-ok` automatically), convert it to a waiver and append " +
484
+ "`resolved · {gate} · {target} · waived → {ref}`, or record the failure honestly in the changelog entry and append " +
485
+ "`resolved · {gate} · {target} · disclosed → {entry file}`. Never delete log lines — the log is a record, not a queue.",
486
+ };
487
+ }
488
+
400
489
  // An undischarged Doc Obligations ledger is not walled here — the detector raises it as a
401
490
  // `needs-session:` row, which the queue wall above already blocks on, and which outlives this
402
491
  // stop through the commit gate and CI. A second wall keyed to one checkbox would add a special
403
492
  // case beside a general rule that already covers it.
404
- const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
405
- (rel) => !planHasChangelogEntry(projectDir, rel)
406
- );
493
+ const completed = plansCompletedThisSession(projectDir, payload.session_id);
494
+
495
+ // The Test Obligations ledger IS walled per row: the Outcome cell lives in the plan file, so the
496
+ // only way past this block is reading the file — a context-held copy of the ledger is exactly
497
+ // how rows get missed. (Tables without an Outcome column are legacy and pass; see lib/ledger.)
498
+ for (const rel of completed) {
499
+ const rows = undischargedRows(readSafe(path.join(projectDir, rel)));
500
+ if (rows.length > 0) {
501
+ return {
502
+ decision: "block",
503
+ reason:
504
+ `Plan \`${rel}\` is Status: Complete with ${rows.length} Test Obligations row(s) missing a discharge outcome (#${rows.join(", #")}). ` +
505
+ "Re-read the ledger from the plan file and discharge every row at the verification gate — satisfied, owes nothing (reason), or blocked → backlog — before completing.",
506
+ };
507
+ }
508
+ }
509
+
510
+ const undocumented = completed.filter((rel) => !planHasChangelogEntry(projectDir, rel));
407
511
  if (undocumented.length > 0) {
408
512
  return {
409
513
  decision: "block",
@@ -434,7 +538,7 @@ function main() {
434
538
  out = bashGate(commandRoot(projectDir, payload, input.command), input.command, payload.session_id);
435
539
  } else {
436
540
  const rel = relIfInside(projectDir, input.file_path || input.notebook_path);
437
- if (rel) out = fileGate(projectDir, rel, payload.tool_name);
541
+ if (rel) out = fileGate(projectDir, rel, payload.tool_name, payload.session_id);
438
542
  }
439
543
  } else if (event === "Stop") {
440
544
  out = stopGate(projectDir, payload);
@@ -451,7 +555,15 @@ function main() {
451
555
  try {
452
556
  main();
453
557
  } catch (e) {
454
- // Fail open: a broken gate must never become an outage.
558
+ // Fail open: a broken gate must never become an outage. But a gate that crashes silently is a
559
+ // gate nobody knows stopped guarding — leave a trace where the next SessionStart can find it.
560
+ try {
561
+ const dir = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), ".claude", "state");
562
+ fs.mkdirSync(dir, { recursive: true });
563
+ fs.appendFileSync(path.join(dir, "hook-failures.log"), `${new Date().toISOString()} compliance.mjs ${e?.message || e}\n`);
564
+ } catch (e2) {
565
+ /* truly nowhere to write — fail open stays absolute */
566
+ }
455
567
  } finally {
456
568
  process.exit(0);
457
569
  }
@@ -29,6 +29,9 @@ function readSafe(projectDir, filePath) {
29
29
 
30
30
  // The predicate. Returns { frozen: boolean, reason?: string }.
31
31
  export function frozenStatus(projectDir, filePath) {
32
+ // *.bkp.* siblings are install-time backups, not records — the maintenance fold edits and
33
+ // deletes them, which a frozen verdict would forbid.
34
+ if (relPosix(projectDir, filePath).includes(".bkp.")) return { frozen: false };
32
35
  const rel = relPosix(projectDir, filePath);
33
36
  if (rel.startsWith("docs/history/changelog/") && rel.endsWith(".md")) {
34
37
  if (isCommitted(projectDir, rel)) {
@@ -0,0 +1,31 @@
1
+ // harness/code/hooks/lib/ledger.mjs — the Test Obligations table, read the way the walls read it.
2
+ // A row is discharged when its Outcome cell states one of the gate's three outcomes. The plan file
3
+ // is the checked truth — a discharge held only in conversation memory does not exist, which is what
4
+ // forces the gate to re-read the ledger from disk. Tables without an Outcome column predate the
5
+ // contract and are out of scope: the same legacy tolerance the Gates: line gets, so an upgrade
6
+ // cannot brick a plan already Complete.
7
+
8
+ const OUTCOME = /^(satisfied|owes nothing|blocked)\b/i;
9
+
10
+ // Row numbers (the `#` column) of every ledger row whose Outcome cell is empty or off-vocabulary.
11
+ export function undischargedRows(planText) {
12
+ const start = (planText || "").search(/^##\s+Test Obligations\b/m);
13
+ if (start === -1) return [];
14
+ const body = [];
15
+ for (const line of planText.slice(start).split("\n").slice(1)) {
16
+ if (/^##\s/.test(line)) break; // next section
17
+ body.push(line);
18
+ }
19
+ const rows = body.filter((line) => /^\s*\|/.test(line));
20
+ if (rows.length < 3) return []; // header + divider + at least one data row
21
+ const header = rows[0].split("|").map((cell) => cell.trim().toLowerCase());
22
+ const col = header.indexOf("outcome");
23
+ if (col === -1) return []; // legacy table — predates the Outcome contract
24
+ const out = [];
25
+ for (const line of rows.slice(2)) {
26
+ const cells = line.split("|").map((cell) => cell.trim());
27
+ if (!/^\d+$/.test(cells[1] || "")) continue;
28
+ if (!OUTCOME.test(cells[col] || "")) out.push(cells[1]);
29
+ }
30
+ return out;
31
+ }
@@ -117,7 +117,9 @@ export function asList(v) {
117
117
  // Shape: { doc (repo-relative, /-separated), owns[], module, keywords[], status }.
118
118
  export function scanFeatureDocs(projectDir) {
119
119
  const featuresDir = path.join(projectDir, "docs", "features");
120
- const files = listMarkdown(featuresDir);
120
+ // *.bkp.* files are install-time backups awaiting the maintenance fold — a backed-up feature
121
+ // doc still carries owns: frontmatter, and scanning it would double-own every path it names.
122
+ const files = listMarkdown(featuresDir).filter((f) => !f.includes(".bkp."));
121
123
  const docs = [];
122
124
  for (const file of files) {
123
125
  let text;
@@ -0,0 +1,75 @@
1
+ // harness/code/hooks/lib/session-log.mjs — the per-session gate log: a dated record, never drained.
2
+ // Bookkeeping is a queue whose success state is empty; this file is the opposite genre — the
3
+ // session's operational story, kept like memory. Machinery appends `denied` and `hook-crash`
4
+ // events; the session appends `resolved` lines. The walls read PAIRS: a `denied` with no later
5
+ // `resolved` for the same gate·target is an open failure, and silence about it is what the Stop
6
+ // and commit walls refuse. Every line survives forever — the row's afterlife is the failure
7
+ // stream later intelligence reads, so resolution appends rather than deletes.
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+
12
+ const DIR = ["docs", "history", "sessions"];
13
+
14
+ function sid8(sessionId) {
15
+ return String(sessionId || "unknown").replace(/[^a-zA-Z0-9-]/g, "").slice(0, 8) || "unknown";
16
+ }
17
+
18
+ export function sessionLogPath(projectDir, sessionId) {
19
+ const dir = path.join(projectDir, ...DIR);
20
+ const suffix = `-${sid8(sessionId)}.md`;
21
+ try {
22
+ const existing = fs.readdirSync(dir).find((f) => f.endsWith(suffix));
23
+ if (existing) return path.join(dir, existing);
24
+ } catch (e) {
25
+ /* dir absent — first event creates it */
26
+ }
27
+ const date = new Date().toISOString().slice(0, 10);
28
+ return path.join(dir, `${date}${suffix}`);
29
+ }
30
+
31
+ // Append one event line, creating the file (with its header) on first write. Best-effort by
32
+ // design: a log append must never become an outage, so failures return false and are the
33
+ // caller's cue to fall back to .claude/state/hook-failures.log.
34
+ export function appendEvent(projectDir, sessionId, kind, gate, target, detail = "") {
35
+ try {
36
+ const file = sessionLogPath(projectDir, sessionId);
37
+ fs.mkdirSync(path.dirname(file), { recursive: true });
38
+ if (!fs.existsSync(file)) {
39
+ fs.writeFileSync(
40
+ file,
41
+ `# Session gate log · sid=${sessionId || "unknown"}\n\n` +
42
+ "Machinery-written record (a dated record, never edited down — resolutions append). " +
43
+ "Every `denied` line needs a later `resolved` line for the same gate · target — " +
44
+ "`retried-ok`, `waived → {ref}`, or `disclosed → {changelog entry}` — before the " +
45
+ "session ends or commits.\n\n"
46
+ );
47
+ }
48
+ const ts = new Date().toISOString().slice(11, 19);
49
+ const tail = detail ? ` · ${detail}` : "";
50
+ fs.appendFileSync(file, `- ${ts} ${kind} · ${gate} · ${target}${tail}\n`);
51
+ return true;
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ // Open failures: gate·target pairs denied with no later `resolved` line. Multiple denials of one
58
+ // pair are one open failure (the retry count stays in the story; the wall asks one question).
59
+ export function unresolvedDenials(projectDir, sessionId) {
60
+ let text = "";
61
+ try {
62
+ text = fs.readFileSync(sessionLogPath(projectDir, sessionId), "utf8");
63
+ } catch (e) {
64
+ return [];
65
+ }
66
+ const open = new Map();
67
+ for (const line of text.split("\n")) {
68
+ const m = /^- \S+ (denied|resolved) · ([^·]+) · ([^·\n]+?)(?: · |$)/.exec(line);
69
+ if (!m) continue;
70
+ const key = `${m[2].trim()} · ${m[3].trim()}`;
71
+ if (m[1] === "denied") open.set(key, true);
72
+ else open.delete(key);
73
+ }
74
+ return [...open.keys()];
75
+ }
@@ -12,6 +12,8 @@ Every unit of work leaves one record — `docs/history/changelog/YYYY-MM-DD-{thr
12
12
 
13
13
  An entry is owed only by work that changes promised behavior or takes a design decision; mechanical and cosmetic commits — a typo, a copy tweak, a config value, a log line — take only their `docs/history/CHANGELOG.md` row, no entry file. The failure fields (`Ask:`, `Skills:`, `Pin:`) are required only when non-none.
14
14
 
15
+ Route the lesson by who pays for losing it: product context — what changed and why — belongs in the entry; anything the **next session would re-pay** — a library's API drifted from what the model expects, a validator's quirk, a tooling workaround — goes to `docs/memory/main.md` as well, because a changelog is read by people tracing history and memory is read by every session before it repeats the mistake.
16
+
15
17
  ## Lifecycle
16
18
 
17
19
  Draft (created before its commit exists) → complete (missing fields filled from the diff and the session) → frozen the moment it rides in a commit. **Immutability begins at commit, not at creation** — a draft is meant to be edited, which is what lets a completed plan record its context before anything is committed; a committed entry is history, and a correction is a new entry linking back, **never** an edit.
@@ -73,6 +73,7 @@ The two guarantees are complementary and both break on mixing: a reader holding
73
73
  | Testing strategy | [`templates/testing-strategy.md`](templates/testing-strategy.md) |
74
74
  | Behavioral checklist for one feature (`docs/tests/{feature}.md`) | [`templates/behavioral-checklist.md`](templates/behavioral-checklist.md) — the verification gate's driving script, and the source its suite tests are drawn from |
75
75
  | Recurring procedure/setup | [`templates/operations-procedure.md`](templates/operations-procedure.md) |
76
+ | Cold-start build order (implementation map) | [`templates/implementation-map.md`](templates/implementation-map.md) — authored by the executing session at build start, updated in place as waves complete |
76
77
  | Capture mid-execution knowledge (`docs/history/journal.md`) | [`templates/journal.md`](templates/journal.md) (record — append-only; a row resolves by landing in its owning doc) |
77
78
  | Bookkeeping work queue (`docs/operations/bookkeeping.md`) | [`templates/bookkeeping.md`](templates/bookkeeping.md) — machine-read row grammar; appended by hooks and sessions, drained by the `bookkeeper` agent, walled at Stop and commit |
78
79
  | One-shot migration/upgrade | [`templates/migration-runbook.md`](templates/migration-runbook.md) (record) |
@@ -12,7 +12,7 @@ When creating or updating a feature doc, derive its **`owns:` frontmatter** —
12
12
 
13
13
  ## How to derive the frontmatter
14
14
 
15
- A feature doc's ownership frontmatter is `owns:` (required), `module:` (optional), `keywords:` (required), and `status:` (required). An optional `last-reviewed:` date (stamped whenever the doc is substantively reviewed or updated) gives the `maintenance` agent a staleness signal to rank its audits by — it is tooling metadata, never narrated in the body.
15
+ A feature doc's ownership frontmatter is `owns:` (required), `module:` (optional), `keywords:` (required), and `status:` (required). An optional `last-reviewed:` date (stamped whenever the doc is substantively reviewed or updated) gives the `maintenance` agent a staleness signal to rank its audits by — it is tooling metadata, never narrated in the body. `type:` (required — the genre, enum = the template names) and `rev:` (generator-written generation stamp) are the same class: external tooling reads them, sessions **preserve them on edits** and never narrate or inject them. The `maintenance` agent stamps `type:` on docs that predate the key.
16
16
 
17
17
  **1. `owns:`** (the path scope)
18
18
 
@@ -23,7 +23,7 @@ const RULES = [
23
23
  // Sections inside a truth doc where dated history is sanctioned (the two bridges).
24
24
  const BRIDGE_HEADINGS = /^#{1,4}\s+(decision changelog|health log)\b/i;
25
25
  // Instruction files and generated trees are a different genre — out of this linter's scope.
26
- const SKIP = /(^|\/)(\.claude|node_modules|skills|rules|agents|harness|packages\/[^/]+\/data)\/|(^|\/)(CLAUDE|AGENTS)\.md$/;
26
+ const SKIP = /(^|\/)(\.claude|node_modules|skills|rules|agents|harness|packages\/[^/]+\/data)\/|(^|\/)(CLAUDE|AGENTS)\.md$|\.bkp\./;
27
27
  const RECORD = /(^|\/)docs\/(history|research|memory)\//;
28
28
 
29
29
  // Classify a path: skipped genre, record, or truth.
@@ -89,7 +89,7 @@ function lintFile(path, findings) {
89
89
  const isFolderIndex = /(^|\/)(readme)\.md$/i.test(path);
90
90
  if (scope === 'truth' && !isFolderIndex && /(^|\/)docs\/features\/[^/]+\.md$/.test(path)) {
91
91
  const fm = text.startsWith('---') ? text.slice(0, text.indexOf('\n---', 3)) : '';
92
- for (const key of ['owns:', 'keywords:', 'status:'])
92
+ for (const key of ['owns:', 'keywords:', 'status:', 'type:'])
93
93
  if (!fm.includes(key)) findings.push({ path, n: 1, severity: 'block', id: 'feature-frontmatter', message: `feature doc missing \`${key}\` frontmatter` });
94
94
  }
95
95
  }
@@ -16,6 +16,7 @@ One file per template. Every template is either a **truth document** (states cur
16
16
  | Testing strategy | [testing-strategy.md](testing-strategy.md) | Truth |
17
17
  | Behavioral checklist | [behavioral-checklist.md](behavioral-checklist.md) | Truth |
18
18
  | Operations procedure | [operations-procedure.md](operations-procedure.md) | Truth |
19
+ | Implementation map | [implementation-map.md](implementation-map.md) | Truth |
19
20
  | Bookkeeping ledger | [bookkeeping.md](bookkeeping.md) | Truth (work queue) |
20
21
  | Protected files | [protected-files.md](protected-files.md) | Truth |
21
22
  | Project root README | [root-readme.md](root-readme.md) | Truth |
@@ -16,6 +16,7 @@ Required: **Conventions**, at least one numbered behavior section, **Failure Rep
16
16
  - **Prefer exact literals** — the real prompt text, the real button label, the real count. A vague assertion passes a broken product.
17
17
  - **Assert persistence separately from display.** "It appeared on screen" and "it was written" are two claims; a surface that streams unsaved content satisfies the first while failing the second.
18
18
  - **State each section's trace.** `Pinned:` names the suite test that now guards it, or says `driven only` **with the reason** — an unmarked section is indistinguishable from an unexamined one.
19
+ - **State each section's `AC:` keys.** Where the repo carries a keyed acceptance-criteria source, each section names the keys it proves, so the chain from criterion to checklist to suite test survives the last hop; the gate reports coverage and backlogs uncovered keys. `none — not AC-mapped` is the honest line where no keyed source exists.
19
20
  - **A known-failing assertion stays written as the contract.** A checklist states what the feature promises; the *run* records the failure and the backlog carries the defect. Never soften an assertion to make a run go green.
20
21
 
21
22
  ```
@@ -44,6 +45,7 @@ Each step is an action; each **Assert** is the observable it must produce.
44
45
  - [ ] **Assert**: {the persistence claim, where the behavior writes something}
45
46
 
46
47
  > Pinned: `{tests/path/to/file.test.ts}` — or `driven only — {why no durable test is bought}`
48
+ > AC: {the acceptance-criteria keys this section proves} — or `none — not AC-mapped`
47
49
 
48
50
  ### 2. {The next behavior}
49
51