@haposoft/cafekit 0.16.0-rc.8 → 0.16.1

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 (323) hide show
  1. package/README.md +117 -28
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +175 -0
  4. package/bin/__tests__/codex-native.test.js +1882 -70
  5. package/bin/__tests__/develop-contract.test.js +442 -23
  6. package/bin/__tests__/grok-envelope.test.js +271 -0
  7. package/bin/__tests__/grok-platform.test.js +133 -0
  8. package/bin/__tests__/hook-payload.test.js +194 -0
  9. package/bin/__tests__/installer-safety.test.js +3 -3
  10. package/bin/__tests__/omp-bridge.test.js +168 -0
  11. package/bin/__tests__/omp-hooks.test.js +178 -0
  12. package/bin/__tests__/omp-runtime.test.js +150 -0
  13. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  14. package/bin/__tests__/output-styles.test.js +214 -0
  15. package/bin/__tests__/package-inventory.test.js +1735 -98
  16. package/bin/__tests__/runtime-dir.test.js +158 -0
  17. package/bin/__tests__/runtime-schema.test.js +113 -0
  18. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  19. package/bin/__tests__/skill-routing-source.test.js +172 -0
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
  21. package/bin/install.js +15 -5
  22. package/bin/lib/codex-install.js +170 -5
  23. package/bin/lib/context.js +64 -3
  24. package/bin/lib/i18n.js +15 -36
  25. package/bin/lib/skill-deps.js +5 -5
  26. package/bin/phases/claude-runtime.js +23 -0
  27. package/bin/phases/claude-settings.js +8 -0
  28. package/bin/phases/codex-hooks.js +156 -0
  29. package/bin/phases/codex-runtime.js +29 -38
  30. package/bin/phases/copy-payload.js +15 -2
  31. package/bin/phases/omp-runtime.js +140 -0
  32. package/bin/phases/root-config.js +3 -0
  33. package/bin/phases/select-platform.js +22 -1
  34. package/bin/phases/select-skill-bundles.js +96 -0
  35. package/bin/phases/skill-inventory.js +71 -0
  36. package/bin/phases/skills-setup.js +9 -2
  37. package/bin/phases/summary.js +0 -4
  38. package/bin/phases/write-metadata.js +8 -5
  39. package/package.json +8 -2
  40. package/src/claude/CLAUDE.md +3 -3
  41. package/src/claude/agents/brainstormer.md +52 -69
  42. package/src/claude/agents/code-auditor.md +32 -14
  43. package/src/claude/agents/debugger.md +35 -20
  44. package/src/claude/agents/deployer.md +12 -5
  45. package/src/claude/agents/docs-keeper.md +11 -1
  46. package/src/claude/agents/implementer.md +25 -13
  47. package/src/claude/agents/inspector.md +6 -3
  48. package/src/claude/agents/project-manager.md +1 -1
  49. package/src/claude/agents/researcher.md +26 -55
  50. package/src/claude/agents/spec-maker.md +12 -7
  51. package/src/claude/agents/strategist.md +52 -0
  52. package/src/claude/agents/test-runner.md +117 -132
  53. package/src/claude/agents/ui-ux-designer.md +1 -1
  54. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  55. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  56. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
  57. package/src/claude/hooks/__tests__/spec-gate.test.js +348 -3
  58. package/src/claude/hooks/__tests__/state.test.js +7 -0
  59. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  60. package/src/claude/hooks/agent.cjs +13 -8
  61. package/src/claude/hooks/completion-authority.cjs +10 -4
  62. package/src/claude/hooks/docs-sync.cjs +14 -11
  63. package/src/claude/hooks/inspect-block.cjs +28 -8
  64. package/src/claude/hooks/lib/config.cjs +8 -2
  65. package/src/claude/hooks/lib/counter.cjs +7 -5
  66. package/src/claude/hooks/lib/hook-payload.cjs +160 -0
  67. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  68. package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
  69. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  70. package/src/claude/hooks/precompact.cjs +106 -0
  71. package/src/claude/hooks/privacy-block.cjs +4 -2
  72. package/src/claude/hooks/rules.cjs +5 -3
  73. package/src/claude/hooks/secret-output-guardrail.cjs +44 -0
  74. package/src/claude/hooks/semantic-review-authority.cjs +6 -4
  75. package/src/claude/hooks/session.cjs +37 -9
  76. package/src/claude/hooks/spec-gate.cjs +21 -11
  77. package/src/claude/hooks/spec-state.cjs +11 -7
  78. package/src/claude/hooks/state.cjs +7 -5
  79. package/src/claude/hooks/task-scaffold-guard.cjs +30 -9
  80. package/src/claude/hooks/usage.cjs +6 -5
  81. package/src/claude/migration-manifest.json +31 -15
  82. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  83. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  84. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  85. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  86. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  87. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  88. package/src/claude/references/debugger/core-philosophy.md +5 -3
  89. package/src/claude/references/debugger/frontend-verification.md +2 -2
  90. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  91. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  92. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  93. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  94. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  95. package/src/claude/references/debugger/verification-protocol.md +13 -12
  96. package/src/claude/rules/orchestrator.md +7 -1
  97. package/src/claude/rules/process-management.md +39 -0
  98. package/src/claude/rules/review-audit-self-decision.md +48 -0
  99. package/src/claude/rules/skill-domain-routing.md +26 -24
  100. package/src/claude/rules/skill-workflow-routing.md +22 -14
  101. package/src/claude/rules/state-sync.md +7 -1
  102. package/src/claude/rules/workflow.md +5 -3
  103. package/src/claude/runtime.json +1 -0
  104. package/src/claude/runtime.schema.json +205 -0
  105. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  106. package/src/claude/scripts/provenance.cjs +29 -9
  107. package/src/claude/scripts/spec-receipt.cjs +65 -4
  108. package/src/claude/scripts/workflow-policy.cjs +1 -1
  109. package/src/claude/settings/settings.json +16 -0
  110. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  111. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  112. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  113. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  114. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  115. package/src/claude/skills/code-review/SKILL.md +43 -25
  116. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  117. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  118. package/src/claude/skills/debug/SKILL.md +48 -19
  119. package/src/claude/skills/delegate/SKILL.md +1 -1
  120. package/src/claude/skills/develop/SKILL.md +92 -65
  121. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  122. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  123. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  124. package/src/claude/skills/docs/SKILL.md +42 -23
  125. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  126. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  127. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  128. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  129. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  130. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  131. package/src/claude/skills/docx/SKILL.md +1 -1
  132. package/src/claude/skills/git/SKILL.md +1 -1
  133. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  134. package/src/claude/skills/git/references/finish-branch.md +1 -1
  135. package/src/claude/skills/hotfix/SKILL.md +159 -170
  136. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  137. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  138. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  139. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  140. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  141. package/src/claude/skills/inspect/SKILL.md +55 -46
  142. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  143. package/src/claude/skills/loop/SKILL.md +114 -0
  144. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  145. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  146. package/src/claude/skills/pdf/SKILL.md +1 -1
  147. package/src/claude/skills/pptx/SKILL.md +1 -1
  148. package/src/claude/skills/question/SKILL.md +16 -13
  149. package/src/claude/skills/research/SKILL.md +70 -43
  150. package/src/claude/skills/route/SKILL.md +94 -0
  151. package/src/claude/skills/route/references/agent-timing.md +46 -0
  152. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  153. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  154. package/src/claude/skills/specs/SKILL.md +51 -71
  155. package/src/claude/skills/specs/references/review.md +11 -8
  156. package/src/claude/skills/specs/references/templates.md +34 -18
  157. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  158. package/src/claude/skills/specs/templates/research.md +2 -2
  159. package/src/claude/skills/sync/SKILL.md +6 -6
  160. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  161. package/src/claude/skills/test/SKILL.md +120 -120
  162. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  163. package/src/claude/skills/test/references/failure-triage.md +55 -121
  164. package/src/claude/skills/test/references/test-memory.md +20 -34
  165. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  166. package/src/claude/skills/web-testing/SKILL.md +1 -1
  167. package/src/claude/skills/xlsx/SKILL.md +1 -1
  168. package/src/claude/status.cjs +135 -33
  169. package/src/codex/AGENTS.md +14 -4
  170. package/src/codex/hooks/docs-sync.cjs +8 -3
  171. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  172. package/src/codex/hooks/rules.cjs +29 -1
  173. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  174. package/src/codex/hooks/spec-gate.cjs +3 -8
  175. package/src/codex/hooks/spec-state.cjs +3 -2
  176. package/src/codex/hooks.json +5 -0
  177. package/src/codex/rules/state-sync.md +7 -1
  178. package/src/codex/runtime.json +2 -0
  179. package/src/common/.env.example +1 -1
  180. package/src/omp/extensions/cafekit-bridge.mjs +233 -0
  181. package/src/omp/hooks/privacy-block.cjs +250 -0
  182. package/src/omp/runtime.json +33 -0
  183. package/bin/lib/rtk-run.js +0 -28
  184. package/bin/phases/setup-rtk.js +0 -159
  185. package/src/claude/skills/backend-development/SKILL.md +0 -101
  186. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  187. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  188. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  189. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  190. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  191. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  192. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  193. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  194. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  195. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  196. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  197. package/src/claude/skills/devops/.env.example +0 -76
  198. package/src/claude/skills/devops/SKILL.md +0 -102
  199. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  200. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  201. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  202. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  203. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  204. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  205. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  206. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  207. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  208. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  209. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  210. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  211. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  212. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  213. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  214. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  215. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  216. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  217. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  218. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  219. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  220. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  221. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  222. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  223. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  224. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  225. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  226. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  227. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  228. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  229. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  230. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  231. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  232. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  233. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  234. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  235. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  236. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  237. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  238. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  239. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  240. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  241. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  242. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  243. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  244. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  245. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  246. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  247. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  248. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  249. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  250. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  251. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  252. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  253. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  254. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  255. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  256. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  257. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  258. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  259. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  260. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  261. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  262. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  263. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  264. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  265. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  266. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  267. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  268. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  269. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  270. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  271. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  272. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  273. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  274. package/src/claude/skills/react-best-practices/README.md +0 -123
  275. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  276. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  277. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  278. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  279. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  280. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  281. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  282. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  283. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  284. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  285. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  286. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  287. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  288. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  289. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  290. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  291. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  293. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  294. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  295. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  296. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  297. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  298. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  299. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  300. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  301. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  302. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  303. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  304. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  305. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  306. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  307. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  308. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  309. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  310. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  311. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  312. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  313. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  314. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  315. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  316. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  317. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  318. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  319. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  320. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  321. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  322. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  323. package/src/claude/skills/react-best-practices/rules/server-serialization.md +0 -38
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ // Behavioral tests for precompact.cjs and the compact-restart readback in
4
+ // session.cjs. Both hooks run as real subprocesses against a temporary git
5
+ // fixture; the capture is asserted from the file it writes and from what the
6
+ // next session actually prints.
7
+
8
+ const { test } = require('node:test');
9
+ const assert = require('node:assert');
10
+ const { spawnSync } = require('node:child_process');
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+
15
+ const PRECOMPACT = path.join(__dirname, '..', 'precompact.cjs');
16
+ const SESSION = path.join(__dirname, '..', 'session.cjs');
17
+ const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
18
+ const RECOVERY = path.join(hookStateDir(), 'compact-recovery.json');
19
+
20
+ function fixture() {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-'));
22
+ for (const args of [
23
+ ['init', '-q', '-b', 'work'],
24
+ ['config', 'user.email', 'cafekit@example.invalid'],
25
+ ['config', 'user.name', 'CafeKit Test'],
26
+ ]) {
27
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
28
+ assert.strictEqual(result.status, 0, result.stderr);
29
+ }
30
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'one\n');
31
+ for (const args of [['add', '-A'], ['commit', '-qm', 'fixture']]) {
32
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
33
+ assert.strictEqual(result.status, 0, result.stderr);
34
+ }
35
+ return dir;
36
+ }
37
+
38
+ function run(hook, dir, payload) {
39
+ const result = spawnSync(process.execPath, [hook], {
40
+ cwd: dir,
41
+ input: JSON.stringify(payload),
42
+ encoding: 'utf8',
43
+ env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
44
+ });
45
+ return { code: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
46
+ }
47
+
48
+ function clearCapture() {
49
+ try { fs.unlinkSync(RECOVERY); } catch { /* absent */ }
50
+ }
51
+
52
+ function readCapture() {
53
+ return JSON.parse(fs.readFileSync(RECOVERY, 'utf8'));
54
+ }
55
+
56
+ test('PreCompact records branch, HEAD, dirty count and the in-flight task', () => {
57
+ const dir = fixture();
58
+ try {
59
+ fs.writeFileSync(path.join(dir, 'tracked.txt'), 'two\n');
60
+ fs.writeFileSync(path.join(dir, 'untracked.txt'), 'scratch\n');
61
+ const featureDir = path.join(dir, 'specs', 'demo');
62
+ fs.mkdirSync(featureDir, { recursive: true });
63
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
64
+ for (const [name, status] of [['task-01-one.md', 'in_progress'], ['task-02-two.md', 'pending']]) {
65
+ fs.writeFileSync(path.join(featureDir, name), [
66
+ `# ${name}`, '', `Status: ${status}`, '',
67
+ '## Dependencies', '', '- none', '',
68
+ '## Verification Plan', '', '- Command: node --test', '',
69
+ ].join('\n'));
70
+ }
71
+
72
+ clearCapture();
73
+ const result = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'auto', session_id: 'compact-session', cwd: dir });
74
+ assert.strictEqual(result.code, 0, result.stderr);
75
+
76
+ const record = readCapture();
77
+ assert.strictEqual(record.branch, 'work');
78
+ assert.match(record.head, /^[0-9a-f]{7,}$/);
79
+ assert.strictEqual(record.dirty_count, 3, 'one modified file, one untracked file, one untracked specs directory');
80
+ assert.strictEqual(record.trigger, 'auto');
81
+ assert.strictEqual(record.session_id, 'compact-session');
82
+ assert.strictEqual(record.workflow.feature, 'demo');
83
+ assert.deepStrictEqual(record.workflow.in_progress, ['task-01-one.md']);
84
+ assert.strictEqual(record.workflow.counts.in_progress, 1);
85
+ assert.strictEqual(record.workflow.counts.pending, 1);
86
+ } finally {
87
+ fs.rmSync(dir, { recursive: true, force: true });
88
+ clearCapture();
89
+ }
90
+ });
91
+
92
+ test('a compact restart prints the captured anchors alongside the authorization warning', () => {
93
+ const dir = fixture();
94
+ try {
95
+ clearCapture();
96
+ assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', trigger: 'manual', cwd: dir }).code, 0);
97
+ const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
98
+ assert.strictEqual(restart.code, 0, restart.stderr);
99
+ assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/, 'the existing warning must survive');
100
+ assert.match(restart.stdout, /State before compaction/);
101
+ assert.match(restart.stdout, /branch work/);
102
+ assert.match(restart.stdout, /Re-read these from disk before acting/);
103
+ } finally {
104
+ fs.rmSync(dir, { recursive: true, force: true });
105
+ clearCapture();
106
+ }
107
+ });
108
+
109
+ test('a compact restart without a capture still prints the warning and nothing invented', () => {
110
+ const dir = fixture();
111
+ try {
112
+ clearCapture();
113
+ const restart = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'compact', cwd: dir });
114
+ assert.strictEqual(restart.code, 0, restart.stderr);
115
+ assert.match(restart.stdout, /VERIFY PENDING AUTHORIZATIONS/);
116
+ assert.doesNotMatch(restart.stdout, /State before compaction/, 'no capture means no claim about prior state');
117
+ } finally {
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test('a normal session start prints no compaction anchors', () => {
123
+ const dir = fixture();
124
+ try {
125
+ clearCapture();
126
+ assert.strictEqual(run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir }).code, 0);
127
+ const startup = run(SESSION, dir, { hook_event_name: 'SessionStart', source: 'startup', cwd: dir });
128
+ assert.strictEqual(startup.code, 0, startup.stderr);
129
+ assert.doesNotMatch(startup.stdout, /State before compaction/);
130
+ assert.doesNotMatch(startup.stdout, /VERIFY PENDING AUTHORIZATIONS/);
131
+ } finally {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ clearCapture();
134
+ }
135
+ });
136
+
137
+ test('PreCompact fails open outside a repository and on malformed input', () => {
138
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-precompact-bare-'));
139
+ try {
140
+ clearCapture();
141
+ const noGit = run(PRECOMPACT, dir, { hook_event_name: 'PreCompact', cwd: dir });
142
+ assert.strictEqual(noGit.code, 0, 'a directory outside git must not fail the hook');
143
+ const record = readCapture();
144
+ assert.strictEqual(record.branch, null, 'unavailable git facts are recorded as null, never guessed');
145
+ assert.strictEqual(record.head, null);
146
+ assert.strictEqual(record.workflow, null);
147
+
148
+ clearCapture();
149
+ const malformed = spawnSync(process.execPath, [PRECOMPACT], {
150
+ cwd: dir, input: 'not json', encoding: 'utf8', env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
151
+ });
152
+ assert.strictEqual(malformed.status, 0, 'malformed payload must fail open');
153
+ assert.strictEqual(fs.existsSync(RECOVERY), false, 'a malformed payload writes no capture');
154
+ } finally {
155
+ fs.rmSync(dir, { recursive: true, force: true });
156
+ clearCapture();
157
+ }
158
+ });
@@ -30,6 +30,8 @@ function installSemanticRuntime(target, runtime) {
30
30
  for (const name of ['semantic-review-authority.cjs', 'completion-authority-state.cjs']) {
31
31
  fs.copyFileSync(path.join(PACKAGE_ROOT, `src/${runtime}/hooks`, name), path.join(hooks, name));
32
32
  }
33
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-dir.cjs'), path.join(hooks, 'lib/runtime-dir.cjs'));
34
+ fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/hook-payload.cjs'), path.join(hooks, 'lib/hook-payload.cjs'));
33
35
  fs.copyFileSync(
34
36
  path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
35
37
  path.join(hooks, 'lib/runtime-path-safety.cjs'),
@@ -2,8 +2,9 @@
2
2
 
3
3
  // Behavioral tests for spec-gate.cjs (Stop completion gate). The hook is run as
4
4
  // a real subprocess: a Stop payload is piped to stdin; we assert exit code and
5
- // whether stdout carries {"decision":"block",...}. Cache lives under the hook's
6
- // own .logs/ dir (shared), so each test seeds/clears it deliberately.
5
+ // whether stdout carries {"decision":"block",...}. The hook resolves its own
6
+ // state directory; running from source that is a temp directory rather than the
7
+ // source tree, and it is shared, so each test seeds/clears it deliberately.
7
8
 
8
9
  const { test, beforeEach, after } = require('node:test');
9
10
  const assert = require('node:assert');
@@ -14,7 +15,8 @@ const os = require('node:os');
14
15
  const path = require('node:path');
15
16
 
16
17
  const HOOK = path.join(__dirname, '..', 'spec-gate.cjs');
17
- const CACHE = path.join(__dirname, '..', '.logs', 'spec-gate-last.json');
18
+ const { hookStateDir } = require(path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'));
19
+ const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
18
20
  const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
19
21
  const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
20
22
  const FEATURE = 'demo';
@@ -286,6 +288,13 @@ function installClaudeGate(root, policyMode = 'valid') {
286
288
  fs.mkdirSync(hooks, { recursive: true });
287
289
  fs.mkdirSync(scripts, { recursive: true });
288
290
  fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
291
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
292
+ fs.copyFileSync(path.join(__dirname, '..', 'lib', 'runtime-dir.cjs'), path.join(hooks, 'lib', 'runtime-dir.cjs'));
293
+ fs.copyFileSync(path.join(__dirname, '..', 'lib', 'hook-payload.cjs'), path.join(hooks, 'lib', 'hook-payload.cjs'));
294
+ fs.copyFileSync(
295
+ path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
296
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
297
+ );
289
298
  for (const name of [
290
299
  'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
291
300
  ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
@@ -1479,3 +1488,339 @@ test('36. process-v3 Stop accepts two completed packets with valid Receipts', ()
1479
1488
  fs.rmSync(dir, { recursive: true, force: true });
1480
1489
  }
1481
1490
  });
1491
+
1492
+ // ── Receipt freshness: bound while written or altered, structure-only once committed on a clean tree ──
1493
+
1494
+ const STALE_BASE = 'f'.repeat(40);
1495
+ const STALE_HEAD = 'e'.repeat(40);
1496
+
1497
+ function commitAll(dir, message) {
1498
+ for (const args of [['add', '-A'], ['commit', '-qm', message, '--allow-empty']]) {
1499
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
1500
+ assert.strictEqual(result.status, 0, result.stderr);
1501
+ }
1502
+ }
1503
+
1504
+ function boundReceipt(base = VALID_BASE, head = VALID_HEAD) {
1505
+ return [
1506
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1507
+ `Base: ${base}`, `Head: ${head}`, '```text', '$ node --test', 'pass: 1', '```',
1508
+ ];
1509
+ }
1510
+
1511
+ function runHookFrom(dir, cwd) {
1512
+ const res = spawnSync(process.execPath, [HOOK], {
1513
+ cwd,
1514
+ input: JSON.stringify({ hook_event_name: 'Stop', session_id: 'test', transcript_path: '/tmp/t', stop_hook_active: false, cwd: dir }),
1515
+ encoding: 'utf8',
1516
+ env: { ...process.env, PROJECT_ROOT: dir },
1517
+ });
1518
+ return { code: res.status, stdout: (res.stdout || '').trim(), stderr: res.stderr || '' };
1519
+ }
1520
+
1521
+ test('37. committed receipt on a clean tree survives later commits', () => {
1522
+ const dir = makeWorkflowFixture(boundReceipt());
1523
+ try {
1524
+ commitAll(dir, 'receipt');
1525
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
1526
+ fs.writeFileSync(path.join(dir, 'src', 'unrelated.txt'), 'later change\n');
1527
+ commitAll(dir, 'unrelated source change');
1528
+ clearCache();
1529
+ const result = runHook({}, dir);
1530
+ assert.strictEqual(result.code, 0, result.stderr);
1531
+ assert.strictEqual(result.stdout, '', 'a committed, unchanged receipt on a clean tree must stay accepted after unrelated commits');
1532
+ } finally {
1533
+ fs.rmSync(dir, { recursive: true, force: true });
1534
+ }
1535
+ });
1536
+
1537
+ test('38. untracked task file requires binding', () => {
1538
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1539
+ try {
1540
+ clearCache();
1541
+ const body = parseBlock(runHook({}, dir).stdout);
1542
+ assert.ok(body && body.decision === 'block', 'an untracked task file must keep full binding');
1543
+ assert.match(body.reason, /\bprovenance\b/);
1544
+ } finally {
1545
+ fs.rmSync(dir, { recursive: true, force: true });
1546
+ }
1547
+ });
1548
+
1549
+ test('38b. the same stale receipt is accepted once committed on a clean tree', () => {
1550
+ // Documents the accepted trade-off: once committed and clean, Base and Head
1551
+ // must be concrete but are no longer compared against the runtime.
1552
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1553
+ try {
1554
+ commitAll(dir, 'receipt');
1555
+ clearCache();
1556
+ const result = runHook({}, dir);
1557
+ assert.strictEqual(result.stdout, '', result.stderr);
1558
+ } finally {
1559
+ fs.rmSync(dir, { recursive: true, force: true });
1560
+ }
1561
+ });
1562
+
1563
+ test('39. staged but uncommitted task file requires binding', () => {
1564
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1565
+ try {
1566
+ const add = spawnSync('git', ['-C', dir, 'add', '-A'], { encoding: 'utf8' });
1567
+ assert.strictEqual(add.status, 0, add.stderr);
1568
+ clearCache();
1569
+ const body = parseBlock(runHook({}, dir).stdout);
1570
+ assert.ok(body && body.decision === 'block', 'git add alone must not grant structure-only');
1571
+ assert.match(body.reason, /\bprovenance\b/);
1572
+ } finally {
1573
+ fs.rmSync(dir, { recursive: true, force: true });
1574
+ }
1575
+ });
1576
+
1577
+ test('40. modified receipt requires binding', () => {
1578
+ const dir = makeWorkflowFixture(boundReceipt());
1579
+ try {
1580
+ commitAll(dir, 'receipt');
1581
+ const taskFile = path.join(dir, 'specs', FEATURE, 'task-01-demo.md');
1582
+ const context = workflowRuntimeContext(dir);
1583
+ const before = fs.readFileSync(taskFile, 'utf8');
1584
+ const after = before.replace(`Head: ${context.head}`, `Head: ${STALE_HEAD}`);
1585
+ assert.notStrictEqual(after, before, 'fixture must contain the bound Head');
1586
+ fs.writeFileSync(taskFile, after);
1587
+ clearCache();
1588
+ const body = parseBlock(runHook({}, dir).stdout);
1589
+ assert.ok(body && body.decision === 'block', 'a receipt edited after commit must be rebound');
1590
+ assert.match(body.reason, /\bprovenance\b/);
1591
+ } finally {
1592
+ fs.rmSync(dir, { recursive: true, force: true });
1593
+ }
1594
+ });
1595
+
1596
+ test('41. unborn HEAD fails closed before any receipt check', () => {
1597
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-gate-'));
1598
+ try {
1599
+ for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test']]) {
1600
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
1601
+ assert.strictEqual(result.status, 0, result.stderr);
1602
+ }
1603
+ const featureDir = path.join(dir, 'specs', FEATURE);
1604
+ fs.mkdirSync(featureDir, { recursive: true });
1605
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1606
+ fs.writeFileSync(path.join(featureDir, 'task-01-demo.md'), [
1607
+ '# Task 01: demo', '', 'Status: done', '', '## Dependencies', '', '- none', '',
1608
+ '## Verification Plan', '', '- Command: node --test', '', ...boundReceipt(STALE_BASE, STALE_HEAD), '',
1609
+ ].join('\n'));
1610
+ clearCache();
1611
+ const body = parseBlock(runHook({}, dir).stdout);
1612
+ assert.ok(body && body.decision === 'block', 'an unborn HEAD must block');
1613
+ // The block arrives from provenance derivation, which cannot resolve a base
1614
+ // commit here, so the receipt check is never reached. Asserting the reason
1615
+ // keeps this case honest: it guards fail-closed behavior on an unborn HEAD,
1616
+ // not the binding-mode selector. The selector's own HEAD-unreadable branch
1617
+ // is guarded by cases 38 and 39.
1618
+ assert.match(body.reason, /controlled failure/);
1619
+ } finally {
1620
+ fs.rmSync(dir, { recursive: true, force: true });
1621
+ }
1622
+ });
1623
+
1624
+ test('42. dirty tree outside specs requires binding, regardless of working directory', () => {
1625
+ const dir = makeWorkflowFixture(boundReceipt());
1626
+ try {
1627
+ commitAll(dir, 'receipt');
1628
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
1629
+ fs.writeFileSync(path.join(dir, 'src', 'dirty.txt'), 'uncommitted\n');
1630
+ clearCache();
1631
+ const fromRoot = parseBlock(runHook({}, dir).stdout);
1632
+ assert.ok(fromRoot && fromRoot.decision === 'block', 'an uncommitted change outside specs must rebind');
1633
+ assert.match(fromRoot.reason, /\bprovenance\b/);
1634
+ clearCache();
1635
+ const fromSpecs = parseBlock(runHookFrom(dir, path.join(dir, 'specs', FEATURE)).stdout);
1636
+ assert.ok(fromSpecs && fromSpecs.decision === 'block', 'the status check must pin the project root, not the working directory');
1637
+ assert.match(fromSpecs.reason, /\bprovenance\b/);
1638
+ } finally {
1639
+ fs.rmSync(dir, { recursive: true, force: true });
1640
+ }
1641
+ });
1642
+
1643
+ test('42b. a change hidden by skip-worktree or assume-unchanged requires binding', () => {
1644
+ // `git status` honors both index flags, so a tracked file outside the specs
1645
+ // root can differ from HEAD while the tree reports clean. Structure mode must
1646
+ // not be granted from a report that git itself is suppressing.
1647
+ for (const flag of ['--skip-worktree', '--assume-unchanged']) {
1648
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1649
+ try {
1650
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
1651
+ fs.writeFileSync(path.join(dir, 'src', 'hidden.txt'), 'committed\n');
1652
+ commitAll(dir, 'receipt and source');
1653
+ const marked = spawnSync('git', ['-C', dir, 'update-index', flag, 'src/hidden.txt'], { encoding: 'utf8' });
1654
+ assert.strictEqual(marked.status, 0, marked.stderr);
1655
+ fs.writeFileSync(path.join(dir, 'src', 'hidden.txt'), 'tampered\n');
1656
+ const status = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' });
1657
+ assert.strictEqual(status.stdout.trim(), '', `${flag} must make the tree look clean for this case to mean anything`);
1658
+ clearCache();
1659
+ const body = parseBlock(runHook({}, dir).stdout);
1660
+ assert.ok(body && body.decision === 'block', `${flag} must not grant structure-only`);
1661
+ assert.match(body.reason, /\bprovenance\b/);
1662
+ } finally {
1663
+ fs.rmSync(dir, { recursive: true, force: true });
1664
+ }
1665
+ }
1666
+ });
1667
+
1668
+ test('43. structure checks still block in committed mode', () => {
1669
+ const dir = makeWorkflowFixture([
1670
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 1',
1671
+ `Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`, '```text', '$ node --test', 'fail: 1', '```',
1672
+ ]);
1673
+ try {
1674
+ commitAll(dir, 'receipt');
1675
+ clearCache();
1676
+ const body = parseBlock(runHook({}, dir).stdout);
1677
+ assert.ok(body && body.decision === 'block');
1678
+ assert.match(body.reason, /exit_result/);
1679
+ assert.doesNotMatch(body.reason, /\bprovenance\b/, 'committed mode must not report a provenance failure for a stale but concrete pair');
1680
+ } finally {
1681
+ fs.rmSync(dir, { recursive: true, force: true });
1682
+ }
1683
+ });
1684
+
1685
+ test('43b. committed mode still requires concrete Base and Head fields', () => {
1686
+ const dir = makeWorkflowFixture([
1687
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1688
+ `Base: ${STALE_BASE}`, '```text', '$ node --test', 'pass: 1', '```',
1689
+ ]);
1690
+ try {
1691
+ commitAll(dir, 'receipt without Head');
1692
+ clearCache();
1693
+ const body = parseBlock(runHook({}, dir).stdout);
1694
+ assert.ok(body && body.decision === 'block', 'a missing Head field must still block');
1695
+ assert.match(body.reason, /\bprovenance\b/);
1696
+ } finally {
1697
+ fs.rmSync(dir, { recursive: true, force: true });
1698
+ }
1699
+ });
1700
+
1701
+ test('44. edited artifact bytes still block in committed mode', () => {
1702
+ const artifactPath = 'output/bundle.js';
1703
+ const dir = makeWorkflowFixture([
1704
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1705
+ `Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`,
1706
+ `Artifact: ${artifactPath} (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
1707
+ '```text', '$ node --test', 'pass: 1', '```',
1708
+ ]);
1709
+ try {
1710
+ fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
1711
+ fs.writeFileSync(path.join(dir, artifactPath), FIXTURE_ARTIFACT);
1712
+ commitAll(dir, 'receipt and artifact');
1713
+ clearCache();
1714
+ const accepted = runHook({}, dir);
1715
+ assert.strictEqual(accepted.stdout, '', accepted.stderr);
1716
+ fs.writeFileSync(path.join(dir, artifactPath), 'tampered\n');
1717
+ commitAll(dir, 'tamper artifact');
1718
+ clearCache();
1719
+ const body = parseBlock(runHook({}, dir).stdout);
1720
+ assert.ok(body && body.decision === 'block', 'artifact bytes must still be verified in committed mode');
1721
+ assert.match(body.reason, /artifact_hash/);
1722
+ } finally {
1723
+ fs.rmSync(dir, { recursive: true, force: true });
1724
+ }
1725
+ });
1726
+
1727
+ test('45. the cache grants no authority across a state change', () => {
1728
+ // A cached PASS must not survive a later mutation. The first run accepts and
1729
+ // writes the cache; the second run sees tampered artifact bytes committed on a
1730
+ // still-clean tree and must block without the cache being cleared first.
1731
+ const artifactPath = 'output/cached.js';
1732
+ const dir = makeWorkflowFixture([
1733
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1734
+ `Base: ${STALE_BASE}`, `Head: ${STALE_HEAD}`,
1735
+ `Artifact: ${artifactPath} (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
1736
+ '```text', '$ node --test', 'pass: 1', '```',
1737
+ ]);
1738
+ try {
1739
+ fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
1740
+ fs.writeFileSync(path.join(dir, artifactPath), FIXTURE_ARTIFACT);
1741
+ commitAll(dir, 'receipt and artifact');
1742
+ clearCache();
1743
+ const accepted = runHook({}, dir);
1744
+ assert.strictEqual(accepted.stdout, '', 'first run must accept and populate the cache');
1745
+ fs.writeFileSync(path.join(dir, artifactPath), 'tampered after the cached pass\n');
1746
+ commitAll(dir, 'tamper artifact, tree stays clean');
1747
+ const body = parseBlock(runHook({}, dir).stdout);
1748
+ assert.ok(body && body.decision === 'block', 'a cached pass must not hide a later mutation');
1749
+ assert.match(body.reason, /artifact_hash/);
1750
+ } finally {
1751
+ fs.rmSync(dir, { recursive: true, force: true });
1752
+ }
1753
+ });
1754
+
1755
+ test('46. a dirty file inside the specs root does not force binding', () => {
1756
+ // The status and index queries exclude the specs root on purpose: a receipt is
1757
+ // written into that root, so counting it would make every receipt invalidate
1758
+ // itself. Dropping the exclusion from either query must fail this case.
1759
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1760
+ try {
1761
+ commitAll(dir, 'receipt');
1762
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'plan.md'), '# Demo plan\n\nEdited after the receipt was committed.\n');
1763
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'notes-untracked.md'), 'scratch\n');
1764
+ const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8' });
1765
+ assert.match(dirty.stdout, /specs\//, 'the specs root must actually be dirty for this case to mean anything');
1766
+ clearCache();
1767
+ const result = runHook({}, dir);
1768
+ assert.strictEqual(result.stdout, '', 'work inside the specs root must not reopen a committed receipt');
1769
+ } finally {
1770
+ fs.rmSync(dir, { recursive: true, force: true });
1771
+ }
1772
+ });
1773
+
1774
+ test('47. completed-set branch accepts committed receipts and blocks a tampered one', () => {
1775
+ // Two finished packets resolve to the completed-set branch, which is the path
1776
+ // that revalidated every done receipt on every Stop. Prove the new mode works
1777
+ // there, then prove it still blocks.
1778
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1779
+ try {
1780
+ const secondDir = path.join(dir, 'specs', 'second');
1781
+ fs.mkdirSync(secondDir, { recursive: true });
1782
+ fs.writeFileSync(path.join(secondDir, 'plan.md'), '# Second plan\n');
1783
+ fs.writeFileSync(path.join(secondDir, 'task-01-second.md'), [
1784
+ '# Task 01: second', '', 'Status: done', '', '## Dependencies', '', '- none', '',
1785
+ '## Verification Plan', '', '- Command: node --test', '', ...boundReceipt(STALE_BASE, STALE_HEAD), '',
1786
+ ].join('\n'));
1787
+ commitAll(dir, 'two completed packets');
1788
+ clearCache();
1789
+ const accepted = runHook({}, dir);
1790
+ assert.strictEqual(accepted.stdout, '', 'the completed-set branch must accept committed receipts on a clean tree');
1791
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
1792
+ fs.writeFileSync(path.join(dir, 'src', 'dirty.txt'), 'uncommitted\n');
1793
+ clearCache();
1794
+ const body = parseBlock(runHook({}, dir).stdout);
1795
+ assert.ok(body && body.decision === 'block', 'the completed-set branch must rebind on a dirty tree');
1796
+ assert.match(body.reason, /\bprovenance\b/);
1797
+ assert.match(body.reason, /second|demo/);
1798
+ } finally {
1799
+ fs.rmSync(dir, { recursive: true, force: true });
1800
+ }
1801
+ });
1802
+
1803
+ test('48. the gate\'s own runtime state does not defeat structure mode', () => {
1804
+ // The gate writes its cache under .claude/hooks/.logs on every Stop. If that
1805
+ // write counted as a dirty tree, a project tracking its runtime directory could
1806
+ // never reach structure mode and the gate would defeat itself. The status query
1807
+ // excludes the same runtime-state roots the provenance manifest already ignores.
1808
+ const dir = makeWorkflowFixture(boundReceipt(STALE_BASE, STALE_HEAD));
1809
+ try {
1810
+ const stateDir = path.join(dir, '.claude', 'hooks', '.logs');
1811
+ fs.mkdirSync(stateDir, { recursive: true });
1812
+ fs.writeFileSync(path.join(stateDir, 'spec-gate-last.json'), '{}\n');
1813
+ fs.writeFileSync(path.join(dir, '.claude', 'runtime.json'), '{}\n');
1814
+ commitAll(dir, 'receipt plus tracked runtime state');
1815
+ // Mutate the tracked runtime state exactly as a Stop would.
1816
+ fs.writeFileSync(path.join(stateDir, 'spec-gate-last.json'), '{"demo":{"task-01-demo.md":"done"}}\n');
1817
+ fs.writeFileSync(path.join(dir, '.claude', 'runtime.json'), '{"spec":{}}\n');
1818
+ const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' });
1819
+ assert.match(dirty.stdout, /\.claude\//, 'the runtime state must actually be dirty for this case to mean anything');
1820
+ clearCache();
1821
+ const result = runHook({}, dir);
1822
+ assert.strictEqual(result.stdout, '', 'generated runtime state must not be read as a dirty worktree');
1823
+ } finally {
1824
+ fs.rmSync(dir, { recursive: true, force: true });
1825
+ }
1826
+ });
@@ -43,6 +43,13 @@ function inInstalledProcessSpecStateFixture(run) {
43
43
  fs.mkdirSync(hooks, { recursive: true });
44
44
  fs.mkdirSync(scripts, { recursive: true });
45
45
  fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'spec-state.cjs'), path.join(hooks, 'spec-state.cjs'));
46
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
47
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'lib', 'runtime-dir.cjs'), path.join(hooks, 'lib', 'runtime-dir.cjs'));
48
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-payload.cjs'), path.join(hooks, 'lib', 'hook-payload.cjs'));
49
+ fs.copyFileSync(
50
+ path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-state-dir.cjs'),
51
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
52
+ );
46
53
  for (const file of [
47
54
  'provenance.cjs',
48
55
  'spec-receipt.cjs',