@haposoft/cafekit 0.16.0-rc.7 → 0.16.0

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 (299) hide show
  1. package/README.md +116 -27
  2. package/bin/__tests__/codex-hooks-ownership.test.js +239 -0
  3. package/bin/__tests__/codex-hooks.test.js +295 -0
  4. package/bin/__tests__/codex-native.test.js +2289 -34
  5. package/bin/__tests__/develop-contract.test.js +400 -23
  6. package/bin/__tests__/installer-safety.test.js +3 -3
  7. package/bin/__tests__/optional-skill-inventory.test.js +249 -0
  8. package/bin/__tests__/output-styles.test.js +214 -0
  9. package/bin/__tests__/package-inventory.test.js +2131 -16
  10. package/bin/__tests__/runtime-schema.test.js +111 -0
  11. package/bin/__tests__/secret-output-guardrail.test.js +122 -0
  12. package/bin/__tests__/skill-routing-source.test.js +172 -0
  13. package/bin/__tests__/specs-v2-execution-closeout.test.js +1 -1
  14. package/bin/install.js +10 -0
  15. package/bin/lib/codex-install.js +170 -5
  16. package/bin/lib/context.js +14 -0
  17. package/bin/lib/i18n.js +12 -6
  18. package/bin/lib/skill-deps.js +5 -5
  19. package/bin/phases/claude-runtime.js +23 -0
  20. package/bin/phases/claude-settings.js +8 -0
  21. package/bin/phases/codex-hooks.js +156 -0
  22. package/bin/phases/codex-runtime.js +29 -38
  23. package/bin/phases/copy-payload.js +8 -2
  24. package/bin/phases/select-skill-bundles.js +96 -0
  25. package/bin/phases/skill-inventory.js +71 -0
  26. package/bin/phases/skills-setup.js +9 -2
  27. package/bin/phases/write-metadata.js +8 -5
  28. package/package.json +8 -2
  29. package/src/claude/CLAUDE.md +3 -3
  30. package/src/claude/agents/brainstormer.md +52 -69
  31. package/src/claude/agents/code-auditor.md +32 -14
  32. package/src/claude/agents/debugger.md +35 -20
  33. package/src/claude/agents/deployer.md +12 -5
  34. package/src/claude/agents/docs-keeper.md +11 -1
  35. package/src/claude/agents/implementer.md +25 -13
  36. package/src/claude/agents/inspector.md +6 -3
  37. package/src/claude/agents/project-manager.md +1 -1
  38. package/src/claude/agents/researcher.md +26 -55
  39. package/src/claude/agents/spec-maker.md +24 -13
  40. package/src/claude/agents/strategist.md +52 -0
  41. package/src/claude/agents/test-runner.md +117 -132
  42. package/src/claude/agents/ui-ux-designer.md +1 -1
  43. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -0
  44. package/src/claude/hooks/__tests__/precompact.test.js +158 -0
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +87 -4
  46. package/src/claude/hooks/__tests__/state.test.js +254 -3
  47. package/src/claude/hooks/__tests__/statusline.test.js +196 -0
  48. package/src/claude/hooks/agent.cjs +1 -1
  49. package/src/claude/hooks/docs-sync.cjs +12 -10
  50. package/src/claude/hooks/inspect-block.cjs +1 -1
  51. package/src/claude/hooks/lib/config.cjs +3 -0
  52. package/src/claude/hooks/lib/hook-state-dir.cjs +39 -0
  53. package/src/claude/hooks/lib/secret-keywords.cjs +42 -0
  54. package/src/claude/hooks/precompact.cjs +105 -0
  55. package/src/claude/hooks/rules.cjs +1 -1
  56. package/src/claude/hooks/secret-output-guardrail.cjs +43 -0
  57. package/src/claude/hooks/session.cjs +27 -1
  58. package/src/claude/hooks/spec-gate.cjs +3 -3
  59. package/src/claude/hooks/spec-state.cjs +32 -5
  60. package/src/claude/hooks/state.cjs +1 -1
  61. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  62. package/src/claude/hooks/usage.cjs +1 -1
  63. package/src/claude/migration-manifest.json +29 -15
  64. package/src/claude/output-styles/coding-level-0-eli5.md +103 -0
  65. package/src/claude/output-styles/coding-level-1-junior.md +124 -0
  66. package/src/claude/output-styles/coding-level-2-mid.md +146 -0
  67. package/src/claude/output-styles/coding-level-3-senior.md +148 -0
  68. package/src/claude/output-styles/coding-level-4-lead.md +159 -0
  69. package/src/claude/output-styles/coding-level-5-god.md +91 -0
  70. package/src/claude/references/debugger/core-philosophy.md +5 -3
  71. package/src/claude/references/debugger/frontend-verification.md +2 -2
  72. package/src/claude/references/debugger/log-ci-analysis.md +13 -1
  73. package/src/claude/references/debugger/parallel-agent-hydration.md +19 -34
  74. package/src/claude/references/debugger/performance-diagnostics.md +2 -2
  75. package/src/claude/references/debugger/root-cause-tracing.md +37 -6
  76. package/src/claude/references/debugger/side-effect-gate.md +3 -1
  77. package/src/claude/references/debugger/verification-protocol.md +13 -12
  78. package/src/claude/rules/orchestrator.md +7 -1
  79. package/src/claude/rules/process-management.md +39 -0
  80. package/src/claude/rules/review-audit-self-decision.md +48 -0
  81. package/src/claude/rules/skill-domain-routing.md +26 -24
  82. package/src/claude/rules/skill-workflow-routing.md +22 -14
  83. package/src/claude/rules/workflow.md +3 -3
  84. package/src/claude/runtime.json +1 -0
  85. package/src/claude/runtime.schema.json +205 -0
  86. package/src/claude/scripts/generate-skill-catalog.cjs +156 -122
  87. package/src/claude/scripts/provenance.cjs +18 -5
  88. package/src/claude/scripts/spec-receipt.cjs +76 -21
  89. package/src/claude/scripts/spec-resolver.cjs +144 -4
  90. package/src/claude/scripts/workflow-policy.cjs +1 -1
  91. package/src/claude/settings/settings.json +16 -0
  92. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  93. package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
  94. package/src/claude/skills/brainstorm/SKILL.md +193 -172
  95. package/src/claude/skills/brainstorm/references/question-framework.md +105 -167
  96. package/src/claude/skills/chrome-devtools/SKILL.md +1 -1
  97. package/src/claude/skills/code-review/SKILL.md +43 -25
  98. package/src/claude/skills/code-review/references/spec-compliance-review.md +3 -4
  99. package/src/claude/skills/code-review/references/verification-gate.md +1 -1
  100. package/src/claude/skills/debug/SKILL.md +48 -19
  101. package/src/claude/skills/delegate/SKILL.md +1 -1
  102. package/src/claude/skills/develop/SKILL.md +92 -65
  103. package/src/claude/skills/develop/references/parallel-waves.md +36 -63
  104. package/src/claude/skills/develop/references/quality-gate.md +31 -4
  105. package/src/claude/skills/develop/references/subagent-patterns.md +21 -7
  106. package/src/claude/skills/docs/SKILL.md +42 -23
  107. package/src/claude/skills/docs/references/init-workflow.md +4 -4
  108. package/src/claude/skills/docs/references/reconstruct-workflow.md +6 -6
  109. package/src/claude/skills/docs/references/standard-docs-workflow.md +6 -2
  110. package/src/claude/skills/docs/references/summarize-workflow.md +2 -2
  111. package/src/claude/skills/docs/references/update-workflow.md +9 -7
  112. package/src/claude/skills/docs/templates/requirements-as-is.md +1 -1
  113. package/src/claude/skills/docx/SKILL.md +1 -1
  114. package/src/claude/skills/git/SKILL.md +1 -1
  115. package/src/claude/skills/git/references/commit-protocols.md +1 -1
  116. package/src/claude/skills/git/references/finish-branch.md +1 -1
  117. package/src/claude/skills/hotfix/SKILL.md +159 -170
  118. package/src/claude/skills/hotfix/references/diagnosis-protocol.md +14 -9
  119. package/src/claude/skills/hotfix/references/parallel-patterns.md +27 -13
  120. package/src/claude/skills/hotfix/references/prevention-gate.md +5 -0
  121. package/src/claude/skills/hotfix/references/review-cycle.md +21 -15
  122. package/src/claude/skills/hotfix/references/workflow-specialized.md +53 -21
  123. package/src/claude/skills/inspect/SKILL.md +55 -46
  124. package/src/claude/skills/inspect/references/internal-inspection.md +21 -5
  125. package/src/claude/skills/loop/SKILL.md +114 -0
  126. package/src/claude/skills/loop/references/bounded-loop-protocol.md +104 -0
  127. package/src/claude/skills/loop/references/metric-and-guard-contract.md +76 -0
  128. package/src/claude/skills/pdf/SKILL.md +1 -1
  129. package/src/claude/skills/pptx/SKILL.md +1 -1
  130. package/src/claude/skills/question/SKILL.md +16 -13
  131. package/src/claude/skills/research/SKILL.md +70 -43
  132. package/src/claude/skills/route/SKILL.md +94 -0
  133. package/src/claude/skills/route/references/agent-timing.md +46 -0
  134. package/src/claude/skills/route/references/chaining-patterns.md +53 -0
  135. package/src/claude/skills/route/references/task-taxonomy.md +48 -0
  136. package/src/claude/skills/specs/SKILL.md +48 -57
  137. package/src/claude/skills/specs/references/review.md +26 -8
  138. package/src/claude/skills/specs/references/templates.md +76 -31
  139. package/src/claude/skills/specs/templates/requirements-init.md +1 -1
  140. package/src/claude/skills/specs/templates/research.md +2 -2
  141. package/src/claude/skills/sync/SKILL.md +6 -6
  142. package/src/claude/skills/sync/references/sync-protocols.md +1 -1
  143. package/src/claude/skills/test/SKILL.md +120 -120
  144. package/src/claude/skills/test/references/execution-strategy.md +208 -361
  145. package/src/claude/skills/test/references/failure-triage.md +55 -121
  146. package/src/claude/skills/test/references/test-memory.md +20 -34
  147. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  148. package/src/claude/skills/web-testing/SKILL.md +1 -1
  149. package/src/claude/skills/xlsx/SKILL.md +1 -1
  150. package/src/claude/status.cjs +135 -33
  151. package/src/codex/AGENTS.md +14 -4
  152. package/src/codex/hooks/docs-sync.cjs +8 -3
  153. package/src/codex/hooks/lib/hook-context.cjs +18 -4
  154. package/src/codex/hooks/rules.cjs +29 -1
  155. package/src/codex/hooks/secret-output-guardrail.cjs +30 -0
  156. package/src/codex/hooks/spec-gate.cjs +3 -8
  157. package/src/codex/hooks/spec-state.cjs +34 -5
  158. package/src/codex/hooks.json +5 -0
  159. package/src/codex/runtime.json +2 -0
  160. package/src/common/.env.example +1 -1
  161. package/src/claude/skills/backend-development/SKILL.md +0 -101
  162. package/src/claude/skills/backend-development/references/backend-api-design.md +0 -495
  163. package/src/claude/skills/backend-development/references/backend-architecture.md +0 -454
  164. package/src/claude/skills/backend-development/references/backend-authentication.md +0 -338
  165. package/src/claude/skills/backend-development/references/backend-code-quality.md +0 -659
  166. package/src/claude/skills/backend-development/references/backend-debugging.md +0 -904
  167. package/src/claude/skills/backend-development/references/backend-devops.md +0 -491
  168. package/src/claude/skills/backend-development/references/backend-mindset.md +0 -387
  169. package/src/claude/skills/backend-development/references/backend-performance.md +0 -397
  170. package/src/claude/skills/backend-development/references/backend-security.md +0 -342
  171. package/src/claude/skills/backend-development/references/backend-technologies.md +0 -256
  172. package/src/claude/skills/backend-development/references/backend-testing.md +0 -429
  173. package/src/claude/skills/devops/.env.example +0 -76
  174. package/src/claude/skills/devops/SKILL.md +0 -102
  175. package/src/claude/skills/devops/references/browser-rendering.md +0 -305
  176. package/src/claude/skills/devops/references/cloudflare-d1-kv.md +0 -123
  177. package/src/claude/skills/devops/references/cloudflare-platform.md +0 -271
  178. package/src/claude/skills/devops/references/cloudflare-r2-storage.md +0 -280
  179. package/src/claude/skills/devops/references/cloudflare-workers-advanced.md +0 -312
  180. package/src/claude/skills/devops/references/cloudflare-workers-apis.md +0 -309
  181. package/src/claude/skills/devops/references/cloudflare-workers-basics.md +0 -418
  182. package/src/claude/skills/devops/references/docker-basics.md +0 -297
  183. package/src/claude/skills/devops/references/docker-compose.md +0 -292
  184. package/src/claude/skills/devops/references/gcloud-platform.md +0 -297
  185. package/src/claude/skills/devops/references/gcloud-services.md +0 -304
  186. package/src/claude/skills/devops/references/kubernetes-basics.md +0 -95
  187. package/src/claude/skills/devops/references/kubernetes-helm-advanced.md +0 -70
  188. package/src/claude/skills/devops/references/kubernetes-helm.md +0 -81
  189. package/src/claude/skills/devops/references/kubernetes-kubectl.md +0 -74
  190. package/src/claude/skills/devops/references/kubernetes-security-advanced.md +0 -94
  191. package/src/claude/skills/devops/references/kubernetes-security.md +0 -89
  192. package/src/claude/skills/devops/references/kubernetes-troubleshooting-advanced.md +0 -74
  193. package/src/claude/skills/devops/references/kubernetes-troubleshooting.md +0 -49
  194. package/src/claude/skills/devops/references/kubernetes-workflows-advanced.md +0 -75
  195. package/src/claude/skills/devops/references/kubernetes-workflows.md +0 -76
  196. package/src/claude/skills/devops/scripts/cloudflare_deploy.py +0 -269
  197. package/src/claude/skills/devops/scripts/docker_optimize.py +0 -332
  198. package/src/claude/skills/devops/scripts/requirements.txt +0 -20
  199. package/src/claude/skills/devops/scripts/tests/requirements.txt +0 -3
  200. package/src/claude/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
  201. package/src/claude/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
  202. package/src/claude/skills/frontend-design/SKILL.md +0 -135
  203. package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +0 -165
  204. package/src/claude/skills/frontend-design/references/analysis-best-practices.md +0 -80
  205. package/src/claude/skills/frontend-design/references/analysis-prompts.md +0 -141
  206. package/src/claude/skills/frontend-design/references/analysis-techniques.md +0 -118
  207. package/src/claude/skills/frontend-design/references/animejs.md +0 -396
  208. package/src/claude/skills/frontend-design/references/anti-slop-rules.md +0 -103
  209. package/src/claude/skills/frontend-design/references/asset-generation.md +0 -337
  210. package/src/claude/skills/frontend-design/references/bento-motion-engine.md +0 -142
  211. package/src/claude/skills/frontend-design/references/design-extraction-overview.md +0 -71
  212. package/src/claude/skills/frontend-design/references/extraction-best-practices.md +0 -141
  213. package/src/claude/skills/frontend-design/references/extraction-output-templates.md +0 -162
  214. package/src/claude/skills/frontend-design/references/extraction-prompts.md +0 -127
  215. package/src/claude/skills/frontend-design/references/magicui-components.md +0 -129
  216. package/src/claude/skills/frontend-design/references/performance-guardrails.md +0 -169
  217. package/src/claude/skills/frontend-design/references/premium-design-patterns.md +0 -93
  218. package/src/claude/skills/frontend-design/references/redesign-audit-checklist.md +0 -114
  219. package/src/claude/skills/frontend-design/references/technical-accessibility.md +0 -119
  220. package/src/claude/skills/frontend-design/references/technical-best-practices.md +0 -97
  221. package/src/claude/skills/frontend-design/references/technical-optimization.md +0 -44
  222. package/src/claude/skills/frontend-design/references/technical-overview.md +0 -90
  223. package/src/claude/skills/frontend-design/references/technical-workflows.md +0 -150
  224. package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +0 -95
  225. package/src/claude/skills/frontend-design/references/workflow-3d.md +0 -102
  226. package/src/claude/skills/frontend-design/references/workflow-describe.md +0 -87
  227. package/src/claude/skills/frontend-design/references/workflow-immersive.md +0 -87
  228. package/src/claude/skills/frontend-design/references/workflow-quick.md +0 -57
  229. package/src/claude/skills/frontend-design/references/workflow-screenshot.md +0 -63
  230. package/src/claude/skills/frontend-design/references/workflow-video.md +0 -74
  231. package/src/claude/skills/frontend-development/SKILL.md +0 -406
  232. package/src/claude/skills/frontend-development/resources/common-patterns.md +0 -331
  233. package/src/claude/skills/frontend-development/resources/complete-examples.md +0 -872
  234. package/src/claude/skills/frontend-development/resources/component-patterns.md +0 -502
  235. package/src/claude/skills/frontend-development/resources/data-fetching.md +0 -767
  236. package/src/claude/skills/frontend-development/resources/file-organization.md +0 -502
  237. package/src/claude/skills/frontend-development/resources/loading-and-error-states.md +0 -501
  238. package/src/claude/skills/frontend-development/resources/performance.md +0 -406
  239. package/src/claude/skills/frontend-development/resources/routing-guide.md +0 -364
  240. package/src/claude/skills/frontend-development/resources/styling-guide.md +0 -428
  241. package/src/claude/skills/frontend-development/resources/typescript-standards.md +0 -418
  242. package/src/claude/skills/mobile-development/SKILL.md +0 -218
  243. package/src/claude/skills/mobile-development/references/mobile-android.md +0 -604
  244. package/src/claude/skills/mobile-development/references/mobile-best-practices.md +0 -544
  245. package/src/claude/skills/mobile-development/references/mobile-debugging.md +0 -1089
  246. package/src/claude/skills/mobile-development/references/mobile-frameworks.md +0 -465
  247. package/src/claude/skills/mobile-development/references/mobile-ios.md +0 -496
  248. package/src/claude/skills/mobile-development/references/mobile-mindset.md +0 -542
  249. package/src/claude/skills/react-best-practices/AGENTS.md +0 -2249
  250. package/src/claude/skills/react-best-practices/README.md +0 -123
  251. package/src/claude/skills/react-best-practices/SKILL.md +0 -128
  252. package/src/claude/skills/react-best-practices/metadata.json +0 -15
  253. package/src/claude/skills/react-best-practices/rules/_sections.md +0 -46
  254. package/src/claude/skills/react-best-practices/rules/_template.md +0 -28
  255. package/src/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  256. package/src/claude/skills/react-best-practices/rules/advanced-use-latest.md +0 -49
  257. package/src/claude/skills/react-best-practices/rules/async-api-routes.md +0 -38
  258. package/src/claude/skills/react-best-practices/rules/async-defer-await.md +0 -80
  259. package/src/claude/skills/react-best-practices/rules/async-dependencies.md +0 -36
  260. package/src/claude/skills/react-best-practices/rules/async-parallel.md +0 -28
  261. package/src/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -99
  262. package/src/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -59
  263. package/src/claude/skills/react-best-practices/rules/bundle-conditional.md +0 -31
  264. package/src/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +0 -49
  265. package/src/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  266. package/src/claude/skills/react-best-practices/rules/bundle-preload.md +0 -50
  267. package/src/claude/skills/react-best-practices/rules/client-event-listeners.md +0 -74
  268. package/src/claude/skills/react-best-practices/rules/client-swr-dedup.md +0 -56
  269. package/src/claude/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
  270. package/src/claude/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
  271. package/src/claude/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
  272. package/src/claude/skills/react-best-practices/rules/js-cache-storage.md +0 -70
  273. package/src/claude/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
  274. package/src/claude/skills/react-best-practices/rules/js-early-exit.md +0 -50
  275. package/src/claude/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
  276. package/src/claude/skills/react-best-practices/rules/js-index-maps.md +0 -37
  277. package/src/claude/skills/react-best-practices/rules/js-length-check-first.md +0 -49
  278. package/src/claude/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
  279. package/src/claude/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
  280. package/src/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
  281. package/src/claude/skills/react-best-practices/rules/rendering-activity.md +0 -26
  282. package/src/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  283. package/src/claude/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
  284. package/src/claude/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
  285. package/src/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  286. package/src/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  287. package/src/claude/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
  288. package/src/claude/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
  289. package/src/claude/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
  290. package/src/claude/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
  291. package/src/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
  292. package/src/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  293. package/src/claude/skills/react-best-practices/rules/rerender-memo.md +0 -44
  294. package/src/claude/skills/react-best-practices/rules/rerender-transitions.md +0 -40
  295. package/src/claude/skills/react-best-practices/rules/server-after-nonblocking.md +0 -73
  296. package/src/claude/skills/react-best-practices/rules/server-cache-lru.md +0 -41
  297. package/src/claude/skills/react-best-practices/rules/server-cache-react.md +0 -26
  298. package/src/claude/skills/react-best-practices/rules/server-parallel-fetching.md +0 -79
  299. 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
+ });
@@ -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';
@@ -99,6 +101,10 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
99
101
  '',
100
102
  'Status: done',
101
103
  '',
104
+ '## Dependencies',
105
+ '',
106
+ '- none',
107
+ '',
102
108
  '## Verification Plan',
103
109
  '',
104
110
  `- Command: ${plannedCommand}`,
@@ -282,6 +288,11 @@ function installClaudeGate(root, policyMode = 'valid') {
282
288
  fs.mkdirSync(hooks, { recursive: true });
283
289
  fs.mkdirSync(scripts, { recursive: true });
284
290
  fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
291
+ fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
292
+ fs.copyFileSync(
293
+ path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
294
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
295
+ );
285
296
  for (const name of [
286
297
  'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
287
298
  ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
@@ -1318,10 +1329,80 @@ test('32. process-v3 requires Receipt heading and runtime-bound provenance', ()
1318
1329
  }
1319
1330
  });
1320
1331
 
1332
+ test('32b. process-v3 ignores a Receipt heading and proof contained only in a fence', () => {
1333
+ const dir = makeWorkflowFixture([
1334
+ '~~~~markdown', '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1335
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', 'pass: 1', '```', '~~~~',
1336
+ ]);
1337
+ try {
1338
+ clearCache();
1339
+ const body = parseBlock(runHook({}, dir).stdout);
1340
+ assert.ok(body, 'fenced-only process-v3 Receipt must block');
1341
+ assert.strictEqual(body.decision, 'block');
1342
+ assert.match(body.reason, /missing_receipt/);
1343
+ } finally {
1344
+ fs.rmSync(dir, { recursive: true, force: true });
1345
+ }
1346
+ });
1347
+
1348
+ test('32c. process-v3 ignores canonical Receipt fields contained only in its output fence', () => {
1349
+ const dir = makeWorkflowFixture([
1350
+ '## Receipt', '', '```text', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1351
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, 'pass: 1', '```',
1352
+ ]);
1353
+ try {
1354
+ clearCache();
1355
+ const body = parseBlock(runHook({}, dir).stdout);
1356
+ assert.ok(body, 'fenced-only canonical fields must block');
1357
+ assert.strictEqual(body.decision, 'block');
1358
+ assert.match(body.reason, /verification_state/);
1359
+ assert.match(body.reason, /command/);
1360
+ assert.match(body.reason, /provenance/);
1361
+ } finally {
1362
+ fs.rmSync(dir, { recursive: true, force: true });
1363
+ }
1364
+ });
1365
+
1366
+ test('32d. process-v3 preserves explicit failures from the Receipt output fence', () => {
1367
+ const dir = makeWorkflowFixture([
1368
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1369
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
1370
+ 'exit code: 1', 'FAIL', '```',
1371
+ ]);
1372
+ try {
1373
+ clearCache();
1374
+ const body = parseBlock(runHook({}, dir).stdout);
1375
+ assert.ok(body, 'explicit fenced output failure must block');
1376
+ assert.strictEqual(body.decision, 'block');
1377
+ assert.match(body.reason, /verification_state/);
1378
+ assert.match(body.reason, /exit_result/);
1379
+ } finally {
1380
+ fs.rmSync(dir, { recursive: true, force: true });
1381
+ }
1382
+ });
1383
+
1384
+ test('32e. process-v3 rejects output whose closing fence is shorter than its opener', () => {
1385
+ const dir = makeWorkflowFixture([
1386
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1387
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '````text', '$ node --test',
1388
+ 'pass: 1', '```',
1389
+ ]);
1390
+ try {
1391
+ clearCache();
1392
+ const body = parseBlock(runHook({}, dir).stdout);
1393
+ assert.ok(body, 'malformed output fence must block');
1394
+ assert.strictEqual(body.decision, 'block');
1395
+ assert.match(body.reason, /command_output/);
1396
+ } finally {
1397
+ fs.rmSync(dir, { recursive: true, force: true });
1398
+ }
1399
+ });
1400
+
1321
1401
  test('33. process-v3 done task with canonical inline Receipt passes', () => {
1322
1402
  const dir = makeWorkflowFixture([
1323
1403
  '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
1324
- `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test', 'pass: 1', '```',
1404
+ `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test',
1405
+ 'Status: pending', '## Receipt', 'pass: 1', '```',
1325
1406
  ]);
1326
1407
  try {
1327
1408
  clearCache();
@@ -1360,6 +1441,7 @@ test('35. process-v3 Stop ignores a completed packet when one packet remains act
1360
1441
  fs.writeFileSync(path.join(activeDir, 'plan.md'), '# Active plan\n');
1361
1442
  fs.writeFileSync(path.join(activeDir, 'task-01-active.md'), [
1362
1443
  '# Task 01: active', '', 'Status: pending', '',
1444
+ '## Dependencies', '', '- none', '',
1363
1445
  '## Verification Plan', '', '- Command: node --test', '',
1364
1446
  ].join('\n'));
1365
1447
  clearCache();
@@ -1389,6 +1471,7 @@ test('36. process-v3 Stop accepts two completed packets with valid Receipts', ()
1389
1471
  });
1390
1472
  fs.writeFileSync(path.join(otherDir, 'task-01-other.md'), [
1391
1473
  '# Task 01: other', '', 'Status: done', '',
1474
+ '## Dependencies', '', '- none', '',
1392
1475
  '## Verification Plan', '', '- Command: node --test', '',
1393
1476
  '## Receipt', '',
1394
1477
  'Verification: PASS', 'Command: node --test', 'Exit: 0',
@@ -7,7 +7,258 @@ const fs = require('node:fs');
7
7
  const os = require('node:os');
8
8
  const path = require('node:path');
9
9
 
10
- const HOOK = path.join(__dirname, '..', 'state.cjs');
10
+ const CLAUDE_ROOT = path.join(__dirname, '..', '..');
11
+ const STATE_HOOK = path.join(CLAUDE_ROOT, 'hooks', 'state.cjs');
12
+ const POLICY = require(path.join(CLAUDE_ROOT, 'scripts', 'workflow-policy.cjs'));
13
+
14
+ function processTaskContent(title, status, dependency = 'none') {
15
+ return [
16
+ `# ${title}`, '', `Status: ${status}`, '',
17
+ '## Dependencies', '', `- ${dependency}`, '',
18
+ '## Verification Plan', '', '- Command: node --test', '',
19
+ ].join('\n');
20
+ }
21
+
22
+ function canonicalProcessTask(root, title, dependency = 'none') {
23
+ const context = POLICY.deriveRuntimeContext({
24
+ projectRoot: root,
25
+ specsRoot: path.join(root, 'specs'),
26
+ specFile: path.join(root, 'specs', 'auth', 'plan.md'),
27
+ featureName: 'auth',
28
+ runtimeSession: 'session-auth-state',
29
+ });
30
+ return [
31
+ processTaskContent(title, 'done', dependency),
32
+ '## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
33
+ `Base: ${context.base}`, `Head: ${context.head}`,
34
+ '```text', 'node --test', '1 test passed', '```', '',
35
+ ].join('\n');
36
+ }
37
+
38
+ function inInstalledProcessSpecStateFixture(run) {
39
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-state-'));
40
+ try {
41
+ const hooks = path.join(dir, '.claude', 'hooks');
42
+ const scripts = path.join(dir, '.claude', 'scripts');
43
+ fs.mkdirSync(hooks, { recursive: true });
44
+ fs.mkdirSync(scripts, { recursive: true });
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(
48
+ path.join(CLAUDE_ROOT, 'hooks', 'lib', 'hook-state-dir.cjs'),
49
+ path.join(hooks, 'lib', 'hook-state-dir.cjs'),
50
+ );
51
+ for (const file of [
52
+ 'provenance.cjs',
53
+ 'spec-receipt.cjs',
54
+ 'spec-resolver.cjs',
55
+ 'workflow-policy.cjs',
56
+ ]) {
57
+ fs.copyFileSync(path.join(CLAUDE_ROOT, 'scripts', file), path.join(scripts, file));
58
+ }
59
+
60
+ const feature = path.join(dir, 'specs', 'auth');
61
+ fs.mkdirSync(feature, { recursive: true });
62
+ fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
63
+
64
+ for (const args of [
65
+ ['init', '-q'],
66
+ ['config', 'user.email', 'cafekit@example.invalid'],
67
+ ['config', 'user.name', 'CafeKit Test'],
68
+ ['commit', '--allow-empty', '-qm', 'fixture'],
69
+ ]) {
70
+ const git = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
71
+ assert.strictEqual(git.status, 0, git.stderr);
72
+ }
73
+
74
+ const writeTask = (basename, title, status, dependency = 'none') => {
75
+ fs.writeFileSync(
76
+ path.join(feature, basename),
77
+ processTaskContent(title, status, dependency),
78
+ );
79
+ };
80
+ const runState = () => spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
81
+ cwd: dir,
82
+ env: { ...process.env, PROJECT_ROOT: dir },
83
+ input: JSON.stringify({
84
+ cwd: dir,
85
+ featureName: 'auth',
86
+ session_id: 'session-auth-state',
87
+ hook_event_name: 'UserPromptSubmit',
88
+ prompt: 'Continue',
89
+ }),
90
+ encoding: 'utf8',
91
+ });
92
+ return run({
93
+ cacheFile: path.join(hooks, '.logs', 'tollgate-last.txt'),
94
+ dir,
95
+ feature,
96
+ hooks,
97
+ runState,
98
+ writePlan: (content) => fs.writeFileSync(path.join(feature, 'plan.md'), content),
99
+ writeTask,
100
+ });
101
+ } finally {
102
+ fs.rmSync(dir, { recursive: true, force: true });
103
+ }
104
+ }
105
+
106
+ test('Claude installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
107
+ inInstalledProcessSpecStateFixture(({ cacheFile, hooks, runState, writeTask }) => {
108
+ writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
109
+
110
+ const blocked = runState();
111
+ assert.strictEqual(blocked.status, 0, blocked.stderr);
112
+ assert.match(blocked.stdout, /0 pending, 1 blocked/);
113
+ assert.doesNotMatch(blocked.stdout, /Next unblocked/);
114
+ assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
115
+ assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
116
+ const blockedCache = fs.readFileSync(cacheFile, 'utf8');
117
+
118
+ writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
119
+ const pending = runState();
120
+ assert.strictEqual(pending.status, 0, pending.stderr);
121
+ assert.match(pending.stdout, /Spec state changed: `auth`/);
122
+ assert.match(pending.stdout, /1 pending, 0 blocked/);
123
+ assert.match(pending.stdout, /Next unblocked: `task-01-auth\.md`/);
124
+ assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), blockedCache, 'state cache must change');
125
+ });
126
+ });
127
+
128
+ test('Claude installed spec-state requires current canonical dependency proof and keys receipt mutations', () => {
129
+ inInstalledProcessSpecStateFixture(({ cacheFile, dir, feature, hooks, runState, writeTask }) => {
130
+ const predecessor = 'task-01-semantic-review.md';
131
+ const dependent = 'task-02-implementation.md';
132
+ writeTask(predecessor, 'Task 01: semantic review', 'blocked');
133
+ writeTask(dependent, 'Task 02: implementation', 'pending', predecessor);
134
+ const dependentPath = path.join(feature, dependent);
135
+ const unchangedDependent = fs.readFileSync(dependentPath, 'utf8');
136
+
137
+ const blocked = runState();
138
+ assert.strictEqual(blocked.status, 0, blocked.stderr);
139
+ assert.match(blocked.stdout, /0 done \/ 2 total \(0 in_progress, 1 pending, 1 blocked\)/);
140
+ assert.doesNotMatch(blocked.stdout, /Next unblocked/);
141
+ assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
142
+ assert.strictEqual(fs.existsSync(cacheFile), true, 'installed cache must be created');
143
+ const blockedCache = fs.readFileSync(cacheFile, 'utf8');
144
+
145
+ writeTask(predecessor, 'Task 01: semantic review', 'done');
146
+ const missing = runState();
147
+ assert.strictEqual(missing.status, 0, missing.stderr);
148
+ assert.doesNotMatch(missing.stdout, /Next unblocked/);
149
+ const missingCache = fs.readFileSync(cacheFile, 'utf8');
150
+ assert.notStrictEqual(missingCache, blockedCache, 'done without proof must change cached proof state');
151
+
152
+ fs.appendFileSync(path.join(feature, predecessor), '\n## Receipt\n\nVerification: PASS\n');
153
+ const malformed = runState();
154
+ assert.strictEqual(malformed.status, 0, malformed.stderr);
155
+ assert.doesNotMatch(malformed.stdout, /Next unblocked/);
156
+
157
+ fs.writeFileSync(path.join(feature, predecessor), canonicalProcessTask(dir, 'Task 01: semantic review'));
158
+ const ready = runState();
159
+ assert.strictEqual(ready.status, 0, ready.stderr);
160
+ assert.match(ready.stdout, /Next unblocked: `task-02-implementation\.md`/);
161
+ const readyCache = fs.readFileSync(cacheFile, 'utf8');
162
+ assert.notStrictEqual(readyCache, missingCache, 'canonical proof must change cached proof state');
163
+
164
+ fs.writeFileSync(
165
+ path.join(feature, predecessor),
166
+ fs.readFileSync(path.join(feature, predecessor), 'utf8').replace('Verification: PASS', 'Verification: FAIL'),
167
+ );
168
+ const mutated = runState();
169
+ assert.strictEqual(mutated.status, 0, mutated.stderr);
170
+ assert.doesNotMatch(mutated.stdout, /Next unblocked/);
171
+ assert.notStrictEqual(fs.readFileSync(cacheFile, 'utf8'), readyCache, 'receipt-only mutation must change cache');
172
+ assert.strictEqual(fs.readFileSync(dependentPath, 'utf8'), unchangedDependent, 'dependent must remain pending');
173
+ });
174
+ });
175
+
176
+ test('Claude installed spec-state keeps unversioned pending packets out of Next with migration guidance', () => {
177
+ inInstalledProcessSpecStateFixture(({ feature, runState, writePlan, writeTask }) => {
178
+ const fencedMarkerPlan = '# Auth plan\n\n```markdown\nSpecs-Contract: process-first-ready-v1\n```\n';
179
+ writePlan(fencedMarkerPlan);
180
+ writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
181
+ const taskBytes = fs.readFileSync(path.join(feature, 'task-01-auth.md'));
182
+ const result = runState();
183
+ assert.strictEqual(result.status, 0, result.stderr);
184
+ assert.doesNotMatch(result.stdout, /Next unblocked/);
185
+ assert.match(result.stdout, /Migration required: add `Specs-Contract: process-first-ready-v1`/);
186
+ const cached = runState();
187
+ assert.strictEqual(cached.status, 0, cached.stderr);
188
+ assert.doesNotMatch(cached.stdout, /Next unblocked/);
189
+ assert.match(cached.stdout, /Add `Specs-Contract: process-first-ready-v1`/);
190
+
191
+ writePlan(fencedMarkerPlan.replace('# Auth plan\n', '# Auth plan\nSpecs-Contract: process-first-ready-v1\n'));
192
+ const marked = runState();
193
+ assert.strictEqual(marked.status, 0, marked.stderr);
194
+ assert.match(marked.stdout, /Spec state changed: `auth`/);
195
+ assert.match(marked.stdout, /Next unblocked: `task-01-auth\.md`/);
196
+ assert.deepStrictEqual(fs.readFileSync(path.join(feature, 'task-01-auth.md')), taskBytes, 'marker-only transition must not change task bytes');
197
+ });
198
+ });
199
+
200
+ test('Claude resolver ignores fenced workflow annotations and rejects fenced-only Status', () => {
201
+ inInstalledProcessSpecStateFixture(({ dir, feature, runState, writePlan, writeTask }) => {
202
+ const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
203
+ const trailingFence = resolver.annotatedMarkdownLines([
204
+ '```markdown', '``` trailing', 'Specs-Contract: process-first-ready-v1',
205
+ 'Status: done', '## Receipt', '```',
206
+ ].join('\n'));
207
+ assert.deepStrictEqual(trailingFence.slice(2, 5).map(({ outsideFence }) => outsideFence), [false, false, false]);
208
+ const fourSpaceFence = resolver.annotatedMarkdownLines(' ```markdown\nStatus: pending');
209
+ assert.strictEqual(fourSpaceFence[1].outsideFence, true);
210
+ writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
211
+ fs.writeFileSync(path.join(feature, 'task-01-fenced-only.md'), [
212
+ '# Task 01', '', '```markdown', 'Status: done', '```', '',
213
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '',
214
+ ].join('\n'));
215
+ const malformed = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
216
+ assert.strictEqual(malformed.error, 'explicit_malformed');
217
+ assert.match(malformed.reason, /must contain exactly one supported Status field/);
218
+
219
+ fs.rmSync(path.join(feature, 'task-01-fenced-only.md'));
220
+ writeTask('task-01-dependent.md', 'Task 01: dependent', 'pending', 'task-02-predecessor.md');
221
+ fs.writeFileSync(path.join(feature, 'task-02-predecessor.md'), [
222
+ '# Task 02', '', 'Status: pending', '', '## Dependencies', '', '- none', '',
223
+ '## Verification Plan', '', '- Command: node --test', '',
224
+ '~~~~markdown', 'Status: done', '## Dependencies', '- task-01-dependent.md', '## Receipt',
225
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0', '```text', 'fake pass', '```', '~~~~', '',
226
+ ].join('\n'));
227
+ const result = runState();
228
+ assert.strictEqual(result.status, 0, result.stderr);
229
+ assert.match(result.stdout, /Next unblocked: `task-02-predecessor\.md`/);
230
+ assert.doesNotMatch(result.stdout, /Next unblocked: `task-01-dependent\.md`/);
231
+ });
232
+ });
233
+
234
+ test('Claude resolver accepts unique legacy task numbers, rejects ambiguity, and rejects exact cycles', () => {
235
+ inInstalledProcessSpecStateFixture(({ dir, feature, writePlan, writeTask }) => {
236
+ const resolver = require(path.join(dir, '.claude', 'scripts', 'spec-resolver.cjs'));
237
+ writePlan('# Auth plan\n');
238
+ writeTask('task-01-a.md', 'Task 01: a', 'pending');
239
+ writeTask('task-02-b.md', 'Task 02: b', 'pending', 'Task 01');
240
+ fs.writeFileSync(path.join(feature, 'task-03-c.md'), '# Task 03: c\n\nStatus: pending\n');
241
+ const unique = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
242
+ assert.deepStrictEqual(unique.taskRegistry['task-02-b.md'].dependencies, ['task-01-a.md']);
243
+ assert.deepStrictEqual(unique.taskRegistry['task-03-c.md'].dependencies, []);
244
+ assert.strictEqual(unique.workflowContract, null);
245
+ assert.strictEqual(unique.queueReady, false);
246
+
247
+ writeTask('task-01-second.md', 'Task 01: second', 'pending');
248
+ const ambiguous = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
249
+ assert.strictEqual(ambiguous.error, 'explicit_malformed');
250
+ assert.match(ambiguous.reason, /legacy dependency Task 01 maps to 2 task basenames/);
251
+
252
+ fs.rmSync(path.join(feature, 'task-01-second.md'));
253
+ fs.rmSync(path.join(feature, 'task-03-c.md'));
254
+ writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
255
+ writeTask('task-01-a.md', 'Task 01: a', 'pending', 'task-02-b.md');
256
+ writeTask('task-02-b.md', 'Task 02: b', 'pending', 'task-01-a.md');
257
+ const cycle = resolver.resolveWorkflowCandidate({ projectRoot: dir, runtime: {}, explicitFeature: 'auth' });
258
+ assert.strictEqual(cycle.error, 'explicit_malformed');
259
+ assert.match(cycle.reason, /dependency cycle detected: task-01-a\.md -> task-02-b\.md -> task-01-a\.md/);
260
+ });
261
+ });
11
262
 
12
263
  test('Stop snapshot includes tracked modifications and untracked files', () => {
13
264
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-state-'));
@@ -22,7 +273,7 @@ test('Stop snapshot includes tracked modifications and untracked files', () => {
22
273
 
23
274
  fs.writeFileSync(path.join(dir, 'tracked.txt'), 'after\n');
24
275
  fs.writeFileSync(path.join(dir, 'new-untracked.json'), '{}\n');
25
- const result = spawnSync(process.execPath, [HOOK], {
276
+ const result = spawnSync(process.execPath, [STATE_HOOK], {
26
277
  cwd: dir,
27
278
  input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
28
279
  encoding: 'utf8',
@@ -47,7 +298,7 @@ test('Stop snapshot includes untracked files before the first commit', () => {
47
298
  spawnSync('git', ['init', '-q'], { cwd: dir });
48
299
  fs.writeFileSync(path.join(dir, 'first-untracked.txt'), 'new\n');
49
300
 
50
- const result = spawnSync(process.execPath, [HOOK], {
301
+ const result = spawnSync(process.execPath, [STATE_HOOK], {
51
302
  cwd: dir,
52
303
  input: JSON.stringify({ hook_event_name: 'Stop', cwd: dir }),
53
304
  encoding: 'utf8',