@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
@@ -1,457 +0,0 @@
1
- // harness/code/hooks/compliance.mjs — the gate-always-waivable hook module.
2
- // PreToolUse (Edit|Write|MultiEdit|Bash): deny what is never legitimate (frozen records, protected
3
- // files, plan-gate skips, changelog-less commits, off-catalog packages), ask where intent matters
4
- // (branches, live commands). Stop: block an arc that tries
5
- // to end with unresolved bookkeeping or an unrecorded plan completion. Every gate fails open — a
6
- // crash or unreadable repo allows the action; enforcement is never allowed to become an outage.
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { spawnSync } from "node:child_process";
11
- import { readStdin, resolveProjectDir, frontmatter } from "./lib/ownership.mjs";
12
- import { readQueue, countRows, askedThisSession, recordAsked } from "./lib/state.mjs";
13
- import { frozenStatus } from "./lib/frozen.mjs";
14
-
15
- function readSafe(file) {
16
- try {
17
- return fs.readFileSync(file, "utf8");
18
- } catch (e) {
19
- return "";
20
- }
21
- }
22
-
23
- function relIfInside(projectDir, filePath) {
24
- if (!filePath) return null;
25
- const rel = path.relative(projectDir, path.resolve(projectDir, filePath));
26
- if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
27
- return rel.split(path.sep).join("/");
28
- }
29
-
30
- function git(projectDir, args) {
31
- const res = spawnSync("git", args, { cwd: projectDir, encoding: "utf8", timeout: 5000 });
32
- return res.status === 0 ? (res.stdout || "").trim() : null;
33
- }
34
-
35
- function decision(permissionDecision, reason) {
36
- return {
37
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision, permissionDecisionReason: reason },
38
- };
39
- }
40
-
41
- // ---------------------------------------------------------------------------
42
- // PreToolUse — file gates
43
- // ---------------------------------------------------------------------------
44
-
45
- function protectedPaths(projectDir) {
46
- const listFile = path.join(projectDir, "docs", "operations", "protected-files.md");
47
- const paths = ["AGENTS.md", "docs/operations/protected-files.md"];
48
- const text = readSafe(listFile);
49
- for (const m of text.matchAll(/`([^`\n]+)`/g)) {
50
- const candidate = m[1].trim();
51
- if (candidate && !candidate.includes(" ")) paths.push(candidate.replace(/^\.\//, ""));
52
- }
53
- return paths;
54
- }
55
-
56
- // Open plans in the new format only (a `Gates:` line present). Plans without one are
57
- // legacy and pass every plan gate — otherwise every open plan bricks on harness upgrade.
58
- function openGatedPlans(projectDir) {
59
- const dir = path.join(projectDir, "docs", "history", "plans");
60
- let entries;
61
- try {
62
- entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
63
- } catch (e) {
64
- return [];
65
- }
66
- const plans = [];
67
- for (const file of entries) {
68
- const text = readSafe(path.join(dir, file));
69
- const status = (/^\*\*Status:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
70
- if (!status || /Complete|Abandoned/.test(status)) continue;
71
- const gates = (/^\*\*Gates:\*\*\s*(.+)$/m.exec(text) || [])[1]?.trim();
72
- if (!gates) continue; // legacy tolerance
73
- plans.push({
74
- file,
75
- status,
76
- gates,
77
- hasLedger: /^##\s+Test Obligations\b/m.test(text),
78
- hasDocLedger: /^##\s+Doc Obligations\b/m.test(text),
79
- });
80
- }
81
- return plans;
82
- }
83
-
84
- const SOURCE_LIKE = /\.(m?[jt]sx?|py|rs|go|rb|java|kt|swift|c|h|cpp|hpp|cs|sh|sql|vue|svelte)$/;
85
-
86
- function fileGate(projectDir, rel, toolName) {
87
- const frozen = frozenStatus(projectDir, rel);
88
- if (frozen.frozen) {
89
- return decision(
90
- "deny",
91
- `BLOCKED — frozen record. \`${rel}\` is committed history and is never edited (${frozen.reason}). A correction is a new dated entry that links back.`
92
- );
93
- }
94
-
95
- if (protectedPaths(projectDir).includes(rel)) {
96
- return decision("deny", `BLOCKED — protected file. \`${rel}\` is on the protected list.`);
97
- }
98
-
99
- const inDocs = rel.startsWith("docs/") || rel.startsWith(".claude/") || rel.endsWith(".md");
100
- if (!inDocs && SOURCE_LIKE.test(rel)) {
101
- for (const plan of openGatedPlans(projectDir)) {
102
- if (/Boundary:\s*pending/i.test(plan.gates)) {
103
- return decision(
104
- "deny",
105
- `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
- );
107
- }
108
- if (!plan.hasLedger) {
109
- return decision(
110
- "deny",
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.`
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
- }
120
- }
121
- }
122
-
123
- // Writing a test file is never an interruption. Deferring tests to the verification gate stays
124
- // doctrine (`testing.md` § During the loop), but it needs no gate here: what the ledger
125
- // actually guarantees is enforced at both ends already — a plan may not take implementation
126
- // edits until its `## Test Obligations` section exists (above), and it may not reach
127
- // `Status: Complete` until the gate discharges every row (the Stop gate). Policing the *moment*
128
- // a file appears adds no guarantee those two do not, and it fires hardest at the gate itself,
129
- // where the ledger's tests are precisely what should be getting written.
130
-
131
- // Doc placement is never an interruption: an off-map docs/ folder is detected silently by
132
- // the awareness module (a `doc-placement` auto: row) and resolved by the bookkeeper.
133
- return null;
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // PreToolUse — Bash gates
138
- // ---------------------------------------------------------------------------
139
-
140
- const LOCKFILES = /^(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|Cargo\.lock)$/;
141
-
142
- // Exempt per the changelog rule's closed list, judged mechanically: lockfile-only,
143
- // harness-file-only (.claude/, AGENTS.md, CLAUDE.md), or changelog housekeeping.
144
- const HARNESS_PREFIXES = [".claude/", ".agents/", ".codex/"];
145
- const HARNESS_FILES = [
146
- "AGENTS.md",
147
- "CLAUDE.md",
148
- ".mcp.json",
149
- ".vscode/settings.json",
150
- ".prettierignore",
151
- ".prettierrc.json",
152
- ];
153
-
154
- function stagedExempt(files) {
155
- return files.every(
156
- (f) =>
157
- LOCKFILES.test(path.basename(f)) ||
158
- HARNESS_PREFIXES.some((p) => f.startsWith(p)) ||
159
- HARNESS_FILES.includes(f) ||
160
- f.startsWith("docs/history/changelog/") ||
161
- f === "docs/history/CHANGELOG.md"
162
- );
163
- }
164
-
165
- function commitGate(projectDir) {
166
- const staged = (git(projectDir, ["diff", "--cached", "--name-only"]) || "")
167
- .split("\n")
168
- .map((s) => s.trim())
169
- .filter(Boolean);
170
- if (staged.length > 0 && !stagedExempt(staged)) {
171
- if (!staged.some((f) => f.startsWith("docs/history/changelog/"))) {
172
- return decision(
173
- "deny",
174
- "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
- );
176
- }
177
- }
178
- const counts = countRows(readQueue(projectDir));
179
- if (counts.unresolved > 0) {
180
- return decision(
181
- "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.`
183
- );
184
- }
185
- return null;
186
- }
187
-
188
- // Package names approved in .claude/hooks/approved-packages.md (plus testing.md frameworks and general.md).
189
- function approvedPackages(projectDir) {
190
- const names = new Set();
191
- const sources = [
192
- path.join(projectDir, ".claude", "hooks", "approved-packages.md"),
193
- path.join(projectDir, ".claude", "rules", "general.md"),
194
- path.join(projectDir, ".claude", "rules", "testing.md"),
195
- ];
196
- for (const src of sources) {
197
- const text = readSafe(src);
198
- for (const m of text.matchAll(/`(@?[a-z0-9][\w./-]*)`/g)) names.add(m[1]);
199
- }
200
- return names;
201
- }
202
-
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);
205
- if (!m) return null;
206
- const approved = approvedPackages(projectDir);
207
- 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("-"));
209
- for (const arg of args) {
210
- const pkg = arg.startsWith("@") ? arg.split("@").slice(0, 2).join("@") : arg.split("@")[0];
211
- if (!pkg || pkg === "." || pkg.includes("/") && !pkg.startsWith("@")) continue;
212
- if (!approved.has(pkg)) {
213
- return decision(
214
- "deny",
215
- `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
- );
217
- }
218
- }
219
- return null;
220
- }
221
-
222
- // The repo a Bash command acts on: an explicit `cd <dir>` in the command wins, then the
223
- // payload's cwd, then the session's home project — a session may operate across repos
224
- // (forge + consumer), and gating a commit against the wrong repo blocks or passes falsely.
225
- function commandRoot(projectDir, payload, command) {
226
- const cdMatch = /(?:^|&&|;)\s*cd\s+"?([^"\s;&]+)"?/.exec(command || "");
227
- for (const candidate of [cdMatch && cdMatch[1], payload.cwd]) {
228
- if (!candidate || !path.isAbsolute(candidate)) continue;
229
- try {
230
- if (fs.statSync(candidate).isDirectory()) return candidate;
231
- } catch (e) {
232
- /* fall through */
233
- }
234
- }
235
- return projectDir;
236
- }
237
-
238
- // An ask class fires once per session: the first occurrence asks, later ones pass silently —
239
- // the user has already exercised judgment on this class this session.
240
- function askOnce(projectDir, sessionId, key, message) {
241
- if (askedThisSession(projectDir, sessionId, key)) return null;
242
- recordAsked(projectDir, sessionId, key);
243
- return decision("ask", message);
244
- }
245
-
246
- // Command matchers anchor at statement boundaries (start, &&, ;, |) so quoted text — an
247
- // echoed payload, a commit message naming a command — can never trigger a gate.
248
- const AT_STATEMENT = String.raw`(?:^|&&|;|\|)\s*(?:sudo\s+)?(?:npx\s+(?:--yes\s+)?)?`;
249
- const statement = (body) => new RegExp(AT_STATEMENT + body);
250
-
251
- function bashGate(projectDir, command, sessionId) {
252
- if (!command) return null;
253
-
254
- if (statement(String.raw`git\s+commit\b`).test(command)) {
255
- return commitGate(projectDir);
256
- }
257
-
258
- // `git branch <name>` creates; flag-led forms (--show-current, --list, -d, -vv) query or manage
259
- if (statement(String.raw`git\s+(checkout\s+-b|switch\s+-c|branch\s+(?!-)\S+|worktree\s+add)`).test(command)) {
260
- return askOnce(
261
- projectDir,
262
- sessionId,
263
- "branch-worktree",
264
- "Create a branch/worktree? Policy allows this only when you asked for a PR or branch."
265
- );
266
- }
267
-
268
- if (
269
- statement(String.raw`wrangler(?:@\S+)?\b.*(\bd1\s+execute\b.*--remote|\bsecret\s+(put|bulk)\b|\bdeploy\b)`).test(command) &&
270
- !/--dry-run\b|--local\b/.test(command)
271
- ) {
272
- return askOnce(
273
- projectDir,
274
- sessionId,
275
- "live-command",
276
- "Run this live-system command (deploy / remote DB / secrets)? If it's a one-shot migration, its runbook should exist in docs/history/migrations/ first. Asked once per session."
277
- );
278
- }
279
-
280
- return packageGate(projectDir, command);
281
- }
282
-
283
- // ---------------------------------------------------------------------------
284
- // Stop
285
- // ---------------------------------------------------------------------------
286
-
287
- // True when the transcript's recent tail shows a bookkeeper dispatch this turn — best-effort:
288
- // absent or unreadable transcript reads as "not dispatched".
289
- function bookkeeperDispatched(transcriptPath) {
290
- const text = readSafe(transcriptPath || "");
291
- if (!text) return false;
292
- const tail = text.slice(-200000);
293
- return /bookkeeper/i.test(tail) && /"(Task|Agent)"/.test(tail);
294
- }
295
-
296
- // Plans this session flipped to Complete: Status is Complete AND the file appears in this
297
- // machine's touched.tsv (written by the awareness detector as edits happen).
298
- function plansCompletedThisSession(projectDir, sessionId) {
299
- const touched = readSafe(path.join(projectDir, ".claude", "state", "touched.tsv"));
300
- const touchedPlans = new Set(
301
- touched
302
- .split("\n")
303
- .map((line) => line.split("\t"))
304
- // THIS session's rows only. touched.tsv is shared by every session in the checkout, so an
305
- // unfiltered read blocks one session's Stop for a plan another session completed — whose Ask
306
- // quotes and iterations it does not have and cannot invent. Rows written before the id column
307
- // existed carry no session and belong to none, so they are skipped rather than assumed.
308
- .filter((cols) => sessionId && cols[3] === sessionId)
309
- .map((cols) => cols[1])
310
- .filter((p) => p && p.startsWith("docs/history/plans/"))
311
- );
312
- const out = [];
313
- for (const rel of touchedPlans) {
314
- const text = readSafe(path.join(projectDir, rel));
315
- if (/^\*\*Status:\*\*\s*Complete\b/m.test(text)) out.push(rel);
316
- }
317
- return out;
318
- }
319
-
320
- // Whether some changelog entry already accounts for this plan.
321
- //
322
- // COMMITTED COUNTS. The test used to be `git status --porcelain docs/history/changelog/` — i.e. "is
323
- // there a DIRTY file there" — which a properly committed entry can never satisfy: committing cleans
324
- // the tree, so the gate re-fired immediately after the one action that discharged it, and the only
325
- // way to silence it was to leave an uncommitted file lying in the changelog directory.
326
- //
327
- // An entry names its plan by slug (the template links `../plans/<slug>.md`), so the slug is what is
328
- // searched for. A dirty file still counts too: a draft being written this moment has not necessarily
329
- // named its plan yet, and that was the original check's one real job.
330
- function planHasChangelogEntry(projectDir, planRel) {
331
- if ((git(projectDir, ["status", "--porcelain", "--", "docs/history/changelog/"]) || "").trim()) return true;
332
-
333
- const slug = path.basename(planRel, ".md");
334
- const dir = path.join(projectDir, "docs", "history", "changelog");
335
- let names = [];
336
- try {
337
- names = fs.readdirSync(dir);
338
- } catch {
339
- return false;
340
- }
341
- return names.some((name) => name.endsWith(".md") && readSafe(path.join(dir, name)).includes(slug));
342
- }
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
-
381
- function stopGate(projectDir, payload) {
382
- if (payload.stop_hook_active) return null; // the runtime's infinite-loop guard — never re-block
383
-
384
- // The Stop wall is scoped to this session's own rows (plus sid-less ones) — a concurrent
385
- // session's open work must not block this one's stop. The commit wall stays global.
386
- const mine = readQueue(projectDir).filter(
387
- (row) => !/\bsid=/.test(row.body) || row.body.includes(`sid=${payload.session_id}`)
388
- );
389
- const counts = countRows(mine);
390
- if (counts.unresolved > 0 && !bookkeeperDispatched(payload.transcript_path)) {
391
- return {
392
- decision: "block",
393
- reason:
394
- `${counts.unresolved} unresolved bookkeeping rows from this session (${counts.auto} auto, ${counts.note} note, ${counts.needsSession} needs-session) in docs/operations/bookkeeping.md. ` +
395
- "Dispatch the `bookkeeper` agent to drain them, resolve them now, or convert to `waived:` rows with reason + expiry. " +
396
- "If implementation of the open plan is complete, also report it and offer the verification gate — never run it unbidden.",
397
- };
398
- }
399
-
400
- // An undischarged Doc Obligations ledger is not walled here — the detector raises it as a
401
- // `needs-session:` row, which the queue wall above already blocks on, and which outlives this
402
- // stop through the commit gate and CI. A second wall keyed to one checkbox would add a special
403
- // case beside a general rule that already covers it.
404
- const undocumented = plansCompletedThisSession(projectDir, payload.session_id).filter(
405
- (rel) => !planHasChangelogEntry(projectDir, rel)
406
- );
407
- if (undocumented.length > 0) {
408
- return {
409
- decision: "block",
410
- reason: `Plan \`${undocumented[0]}\` reached Status: Complete this session with no changelog entry drafted. Write the draft now (documentation skill → templates/changelog-entry.md) — the Ask quotes, rejected iterations, and skill verdicts that fill it exist only in this session and die with it.`,
411
- };
412
- }
413
-
414
- return null;
415
- }
416
-
417
- // ---------------------------------------------------------------------------
418
-
419
- function main() {
420
- const raw = readStdin();
421
- let payload = {};
422
- try {
423
- payload = raw ? JSON.parse(raw) : {};
424
- } catch (e) {
425
- payload = {};
426
- }
427
- const projectDir = resolveProjectDir(payload);
428
- const event = payload.hook_event_name;
429
-
430
- let out = null;
431
- if (event === "PreToolUse") {
432
- const input = payload.tool_input || {};
433
- if (payload.tool_name === "Bash") {
434
- out = bashGate(commandRoot(projectDir, payload, input.command), input.command, payload.session_id);
435
- } else {
436
- const rel = relIfInside(projectDir, input.file_path || input.notebook_path);
437
- if (rel) out = fileGate(projectDir, rel, payload.tool_name);
438
- }
439
- } else if (event === "Stop") {
440
- out = stopGate(projectDir, payload);
441
- // Advisory rides alongside a block rather than replacing it — both are true at once.
442
- if (!payload.stop_hook_active) {
443
- const note = sessionSizeNote(payload);
444
- if (note) out = { ...(out || {}), systemMessage: note };
445
- }
446
- }
447
-
448
- if (out) process.stdout.write(JSON.stringify(out));
449
- }
450
-
451
- try {
452
- main();
453
- } catch (e) {
454
- // Fail open: a broken gate must never become an outage.
455
- } finally {
456
- process.exit(0);
457
- }
@@ -1,23 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Edit(docs/**)",
5
- "Edit(**/tests/**)",
6
- "Edit(**/e2e/**)"
7
- ]
8
- },
9
- "hooks": {
10
- "SessionStart": [
11
- { "matcher": "", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"", "timeout": 10 } ] }
12
- ],
13
- "PostToolUse": [
14
- { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/awareness.mjs\"", "timeout": 10 } ] }
15
- ],
16
- "PreToolUse": [
17
- { "matcher": "Edit|Write|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"", "timeout": 10 } ] }
18
- ],
19
- "Stop": [
20
- { "matcher": "", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/compliance.mjs\"", "timeout": 15 } ] }
21
- ]
22
- }
23
- }
@@ -1,58 +0,0 @@
1
- // harness/code/hooks/lib/frozen.mjs — the frozen-record predicate, shared by the compliance gate
2
- // and CI. Frozen: a committed docs/history/changelog/ entry, a plan whose Status is Complete or
3
- // Abandoned, an outcome-stamped migration runbook. Immutability begins at commit for changelog
4
- // entries (a draft in the working tree is meant to be edited), at the status flip for plans, and
5
- // at the Executed stamp for runbooks. Every check fails open: no git, unreadable file ⇒ not frozen.
6
-
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { spawnSync } from "node:child_process";
10
-
11
- function relPosix(projectDir, filePath) {
12
- return path.relative(projectDir, path.resolve(projectDir, filePath)).split(path.sep).join("/");
13
- }
14
-
15
- // True when git tracks the file at HEAD (i.e. it has been committed at least once).
16
- export function isCommitted(projectDir, filePath) {
17
- const rel = relPosix(projectDir, filePath);
18
- const res = spawnSync("git", ["cat-file", "-e", `HEAD:${rel}`], { cwd: projectDir, stdio: "ignore" });
19
- return res.status === 0;
20
- }
21
-
22
- function readSafe(projectDir, filePath) {
23
- try {
24
- return fs.readFileSync(path.resolve(projectDir, filePath), "utf8");
25
- } catch (e) {
26
- return "";
27
- }
28
- }
29
-
30
- // The predicate. Returns { frozen: boolean, reason?: string }.
31
- export function frozenStatus(projectDir, filePath) {
32
- const rel = relPosix(projectDir, filePath);
33
- if (rel.startsWith("docs/history/changelog/") && rel.endsWith(".md")) {
34
- if (isCommitted(projectDir, rel)) {
35
- return { frozen: true, reason: "committed changelog entry — immutability begins at commit" };
36
- }
37
- return { frozen: false };
38
- }
39
- if (rel.startsWith("docs/history/plans/") && rel.endsWith(".md")) {
40
- const text = readSafe(projectDir, rel);
41
- if (/^\*\*Status:\*\*\s*(Complete|Abandoned)\b/m.test(text)) {
42
- return { frozen: true, reason: "plan with Status: Complete or Abandoned" };
43
- }
44
- return { frozen: false };
45
- }
46
- if (rel.startsWith("docs/history/migrations/") && rel.endsWith(".md")) {
47
- const text = readSafe(projectDir, rel);
48
- if (/^\*\*Executed:\*\*\s*\d{4}-\d{2}-\d{2}/m.test(text)) {
49
- return { frozen: true, reason: "outcome-stamped migration runbook" };
50
- }
51
- return { frozen: false };
52
- }
53
- return { frozen: false };
54
- }
55
-
56
- export function isFrozen(projectDir, filePath) {
57
- return frozenStatus(projectDir, filePath).frozen;
58
- }