@haposoft/cafekit 0.16.7 → 0.18.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 (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -20,7 +20,7 @@ const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
20
20
  const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
21
21
  const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
22
22
  const FEATURE = 'demo';
23
- const TASK_REL = 'tasks/task-R0-01-x.md';
23
+ const TASK_REL = 'task-01-x.md';
24
24
  const VALID_BASE = '0123456789abcdef0123456789abcdef01234567';
25
25
  const VALID_HEAD = '89abcdef0123456789abcdef0123456789abcdef';
26
26
  const FIXTURE_ARTIFACT = Buffer.from('cafekit claude fixture artifact\n');
@@ -29,11 +29,6 @@ const FIXTURE_ARTIFACT_DIGEST = crypto.createHash('sha256').update(FIXTURE_ARTIF
29
29
  function runHook(payload, cwd) {
30
30
  const projectRoot = cwd || payload.cwd;
31
31
  const specsRoot = path.join(projectRoot, 'specs');
32
- if (fs.existsSync(specsRoot)) {
33
- for (const featureName of fs.readdirSync(specsRoot)) {
34
- installFeatureReceipt(projectRoot, featureName, payload.session_id || 'test');
35
- }
36
- }
37
32
  // PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
38
33
  // monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
39
34
  const env = { ...process.env, PROJECT_ROOT: projectRoot };
@@ -69,7 +64,7 @@ function runtimeContext(dir) {
69
64
  return PROVENANCE.deriveRuntimeContext({
70
65
  projectRoot: dir,
71
66
  specsRoot: path.join(dir, 'specs'),
72
- specFile: path.join(dir, 'specs', FEATURE, 'spec.json'),
67
+ specFile: path.join(dir, 'specs', FEATURE, 'plan.md'),
73
68
  featureName: FEATURE,
74
69
  runtimeSession: 'test',
75
70
  });
@@ -118,38 +113,6 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
118
113
  return dir;
119
114
  }
120
115
 
121
- function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
122
- const featureDir = path.join(dir, 'specs', featureName);
123
- const specFile = path.join(featureDir, 'spec.json');
124
- if (!fs.existsSync(specFile)) return;
125
- let spec;
126
- try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { return; }
127
- const lifecyclePhase = spec.current_phase || spec.phase;
128
- const explicitCloseout = ['completed', 'complete'].includes(spec.status)
129
- || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
130
- if (!explicitCloseout) return;
131
- const tasks = Object.values(spec.task_registry || {});
132
- if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) return;
133
- const context = PROVENANCE.deriveRuntimeContext({
134
- projectRoot: dir,
135
- specsRoot: path.join(dir, 'specs'),
136
- specFile,
137
- featureName,
138
- runtimeSession: session,
139
- });
140
- fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
141
- `Feature: ${featureName}`,
142
- 'Expected: final integration verification passes',
143
- 'Observed: final integration verification passed',
144
- 'Verification: PASS',
145
- 'Command: node --test',
146
- 'Exit: 0',
147
- `Base: ${context.base}`,
148
- `Head: ${context.head}`,
149
- '',
150
- ].join('\n'));
151
- }
152
-
153
116
  function clearCache() {
154
117
  try { fs.unlinkSync(CACHE); } catch { /* absent */ }
155
118
  }
@@ -164,52 +127,32 @@ function readCache() {
164
127
  }
165
128
 
166
129
  /**
167
- * Build a minimal active-spec fixture under dir.
130
+ * Build a minimal process-first packet under dir: plan.md plus one flat task
131
+ * whose Verification Plan runs `npm test` and whose inline Receipt varies.
168
132
  * @param {object} opts
169
133
  * @param {string} [opts.taskStatus='done']
170
- * @param {string|null} [opts.completed_at='2026-07-01T00:00:00Z']
171
- * @param {string} [opts.mdStatus='done']
172
- * @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
134
+ * @param {'valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
173
135
  * @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
174
- * @param {object|null} [opts.workflowPolicy] — if set, persist completion obligations
175
- * @param {string} [opts.phase='closeout'] — persisted lifecycle boundary
176
136
  */
177
137
  function makeFixture(opts = {}) {
178
138
  const {
179
139
  taskStatus = 'done',
180
- completed_at = '2026-07-01T00:00:00Z',
181
- mdStatus = 'done',
182
140
  evidence = 'valid',
183
141
  runtime = null,
184
- workflowPolicy = null,
185
- phase = 'closeout',
186
142
  } = opts;
187
143
  const dir = tmpDir();
188
144
  const featureDir = path.join(dir, 'specs', FEATURE);
189
- const tasksDir = path.join(featureDir, 'tasks');
190
- fs.mkdirSync(tasksDir, { recursive: true });
191
-
192
- const entry = { status: taskStatus };
193
- if (completed_at !== null) entry.completed_at = completed_at;
194
-
195
- const spec = {
196
- status: 'in_progress',
197
- feature_name: FEATURE,
198
- current_phase: phase,
199
- task_registry: { [TASK_REL]: entry },
200
- };
201
- if (workflowPolicy) spec.workflow_policy = workflowPolicy;
202
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
145
+ fs.mkdirSync(featureDir, { recursive: true });
146
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
203
147
 
204
- let evidenceBlock = '';
148
+ let receiptBlock = '';
205
149
  if (evidence === 'valid') {
206
- evidenceBlock = [
207
- '## Evidence',
150
+ receiptBlock = [
151
+ '## Receipt',
208
152
  '',
209
153
  'Verification: PASS',
210
154
  'Command: npm test',
211
155
  'Exit: 0',
212
- 'Result: PASS',
213
156
  `Base: ${VALID_BASE}`,
214
157
  `Head: ${VALID_HEAD}`,
215
158
  `Artifact: output/bundle.js (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
@@ -219,34 +162,38 @@ function makeFixture(opts = {}) {
219
162
  '```',
220
163
  '',
221
164
  ].join('\n');
222
- } else if (evidence === 'legacy-valid') {
223
- evidenceBlock = `## Evidence\n\nVerification: PASS\nCommand: npm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\nnpm test — passed, exit code 0\n`;
224
165
  } else if (evidence === 'failed') {
225
- evidenceBlock = '## Evidence\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
166
+ receiptBlock = '## Receipt\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
226
167
  } else if (evidence === 'fence-only') {
227
- evidenceBlock = '## Evidence\n\n```\nnpm test\n```\n';
168
+ receiptBlock = '## Receipt\n\n```\nnpm test\n```\n';
228
169
  } else if (evidence === 'placeholder') {
229
- evidenceBlock = [
230
- '## Evidence',
170
+ receiptBlock = [
171
+ '## Receipt',
231
172
  '',
232
173
  'Command: `{{TYPECHECK / TEST COMMAND}}`',
233
174
  'Expected: {{What proves success}}',
234
175
  '',
235
176
  ].join('\n');
236
177
  } else if (evidence === 'missing-evidence') {
237
- evidenceBlock = '## Risk Assessment\n\nNone.\n';
238
- } else {
239
- evidenceBlock = '';
178
+ receiptBlock = '## Risk Assessment\n\nNone.\n';
240
179
  }
241
180
 
242
181
  fs.writeFileSync(
243
- path.join(dir, 'specs', FEATURE, TASK_REL),
182
+ path.join(featureDir, TASK_REL),
244
183
  [
245
- `# Task R0-01: example`,
184
+ '# Task 01: example',
185
+ '',
186
+ `Status: ${taskStatus}`,
187
+ '',
188
+ '## Dependencies',
189
+ '',
190
+ '- none',
246
191
  '',
247
- `**Status:** ${mdStatus}`,
192
+ '## Verification Plan',
248
193
  '',
249
- evidenceBlock,
194
+ '- Command: npm test',
195
+ '',
196
+ receiptBlock,
250
197
  ].join('\n'),
251
198
  );
252
199
 
@@ -261,9 +208,7 @@ function makeFixture(opts = {}) {
261
208
  if (evidence === 'valid') {
262
209
  fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
263
210
  fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
264
- }
265
- if (evidence === 'valid' || evidence === 'legacy-valid') {
266
- const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
211
+ const taskFile = path.join(featureDir, TASK_REL);
267
212
  fs.writeFileSync(taskFile, bindFixtureReceipt(dir, fs.readFileSync(taskFile, 'utf8')));
268
213
  }
269
214
 
@@ -295,10 +240,7 @@ function installClaudeGate(root, policyMode = 'valid') {
295
240
  path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
296
241
  path.join(hooks, 'lib', 'hook-state-dir.cjs'),
297
242
  );
298
- for (const name of [
299
- 'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
300
- ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
301
- for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs', 'validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
243
+ for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs']) {
302
244
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
303
245
  }
304
246
  if (policyMode === 'valid') {
@@ -404,31 +346,21 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
404
346
  const dir = tmpDir();
405
347
  const gate = installClaudeGate(dir);
406
348
  const featureDir = path.join(dir, 'specs', FEATURE);
349
+ fs.mkdirSync(featureDir, { recursive: true });
350
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
407
351
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
408
352
  fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
409
353
  fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
410
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
411
- status: 'in_progress',
412
- current_phase: 'closeout',
413
- feature_name: FEATURE,
414
- task_registry: {
415
- [TASK_REL]: {
416
- status: 'done',
417
- completed_at: '2026-08-11T00:00:00.000Z',
418
- artifacts: ['output/bundle.js'],
419
- },
420
- },
421
- }));
422
354
  fs.writeFileSync(path.join(featureDir, TASK_REL), [
423
- '# Task', '', '**Status:** done', '', '## Evidence', '',
355
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
424
356
  'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
425
357
  'Artifact: output/bundle.js', suffix,
358
+ '```', 'node --test', 'ok 1', '```',
426
359
  ].join('\n'));
427
360
  fs.writeFileSync(
428
361
  path.join(featureDir, TASK_REL),
429
362
  bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
430
363
  );
431
- installFeatureReceipt(dir);
432
364
  try {
433
365
  const result = spawnSync(process.execPath, [gate], {
434
366
  cwd: dir,
@@ -458,6 +390,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
458
390
  for (const { artifactPath, symlink } of cases) {
459
391
  const dir = tmpDir();
460
392
  const featureDir = path.join(dir, 'specs', FEATURE);
393
+ fs.mkdirSync(featureDir, { recursive: true });
394
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
461
395
  const artifactBytes = Buffer.from(`artifact-${artifactPath}\n`);
462
396
  const digest = crypto.createHash('sha256').update(artifactBytes).digest('hex');
463
397
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
@@ -469,20 +403,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
469
403
  } else {
470
404
  fs.writeFileSync(path.join(dir, 'outside.js'), artifactBytes);
471
405
  }
472
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
473
- status: 'in_progress',
474
- current_phase: 'closeout',
475
- feature_name: FEATURE,
476
- task_registry: {
477
- [TASK_REL]: {
478
- status: 'done',
479
- completed_at: '2026-08-11T00:00:00.000Z',
480
- artifacts: [artifactPath],
481
- },
482
- },
483
- }));
484
406
  fs.writeFileSync(path.join(featureDir, TASK_REL), bindFixtureReceipt(dir, [
485
- '# Task', '', '**Status:** done', '', '## Evidence', '',
407
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
486
408
  'Verification: PASS', 'Command: node --test', 'Exit: 0',
487
409
  `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
488
410
  `Artifact: ${artifactPath} (sha256:${digest})`,
@@ -628,27 +550,6 @@ test('empty or malformed Claude hook payload fails closed', () => {
628
550
  }
629
551
  });
630
552
 
631
- test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
632
- const dir = makeFixture({
633
- workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
634
- });
635
- try {
636
- const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
637
- const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
638
- spec.proofs = {
639
- needsInspection: 'inspection completed',
640
- needsIndependentAudit: 'PASS',
641
- needsResearchGrounding: 'research completed',
642
- };
643
- fs.writeFileSync(specPath, JSON.stringify(spec));
644
- const body = parseBlock(runHook({}, dir).stdout);
645
- assert.equal(body?.decision, 'block');
646
- assert.match(body.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
647
- } finally {
648
- fs.rmSync(dir, { recursive: true, force: true });
649
- }
650
- });
651
-
652
553
  test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
653
554
  const dir = makeFixture({ evidence: 'placeholder' });
654
555
  try {
@@ -689,70 +590,20 @@ test('9. a code fence alone is not verification proof', () => {
689
590
  }
690
591
  });
691
592
 
692
- test('10. completed_at must be a valid ISO timestamp', () => {
693
- const dir = makeFixture({ completed_at: 'yesterday' });
694
- try {
695
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
696
- const body = parseBlock(runHook({}, dir).stdout);
697
- assert.ok(body);
698
- assert.match(body.reason, /\bcompleted_at\b/);
699
- } finally {
700
- fs.rmSync(dir, { recursive: true, force: true });
701
- }
702
- });
703
-
704
- test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
705
- const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
706
- try {
707
- seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
708
- assert.strictEqual(runHook({}, dir).stdout, '');
709
- assert.strictEqual(readCache()[FEATURE][TASK_REL], 'pending');
710
-
711
- const specFile = path.join(dir, 'specs', FEATURE, 'spec.json');
712
- const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
713
- spec.task_registry[TASK_REL] = {
714
- status: 'done',
715
- completed_at: '2026-07-01T00:00:00Z',
716
- };
717
- spec.current_phase = 'closeout';
718
- fs.writeFileSync(specFile, JSON.stringify(spec));
719
- const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
720
- fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
721
-
722
- assert.ok(parseBlock(runHook({}, dir).stdout), 're-completed failing task must be gated');
723
- } finally {
724
- fs.rmSync(dir, { recursive: true, force: true });
725
- }
726
- });
727
-
728
- test('12. legacy successful receipt remains read-compatible', () => {
729
- const dir = makeFixture({ evidence: 'legacy-valid' });
730
- try {
731
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
732
- assert.strictEqual(runHook({}, dir).stdout, '');
733
- } finally {
734
- fs.rmSync(dir, { recursive: true, force: true });
735
- }
736
- });
737
-
738
593
  test('13. provenance requires both Base and Head - only Base fails', () => {
739
594
  const dir = tmpDir();
740
595
  const featureDir = path.join(dir, 'specs', FEATURE);
596
+ fs.mkdirSync(featureDir, { recursive: true });
597
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
741
598
  const tasksDir = path.join(featureDir, 'tasks');
742
599
  fs.mkdirSync(tasksDir, { recursive: true });
743
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
744
- status: 'in_progress',
745
- feature_name: FEATURE,
746
- current_phase: 'closeout',
747
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
748
- }));
749
600
  // Only Base, missing Head
750
601
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
751
602
  '# Task',
752
603
  '',
753
- '**Status:** done',
604
+ 'Status: done',
754
605
  '',
755
- '## Evidence',
606
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
756
607
  '',
757
608
  'Verification: PASS',
758
609
  'Command: npm test',
@@ -776,21 +627,17 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
776
627
  test('14. provenance requires both Base and Head - only Head fails', () => {
777
628
  const dir = tmpDir();
778
629
  const featureDir = path.join(dir, 'specs', FEATURE);
630
+ fs.mkdirSync(featureDir, { recursive: true });
631
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
779
632
  const tasksDir = path.join(featureDir, 'tasks');
780
633
  fs.mkdirSync(tasksDir, { recursive: true });
781
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
782
- status: 'in_progress',
783
- feature_name: FEATURE,
784
- current_phase: 'closeout',
785
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
786
- }));
787
634
  // Only Head, missing Base
788
635
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), [
789
636
  '# Task',
790
637
  '',
791
- '**Status:** done',
638
+ 'Status: done',
792
639
  '',
793
- '## Evidence',
640
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
794
641
  '',
795
642
  'Verification: PASS',
796
643
  'Command: npm test',
@@ -814,20 +661,16 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
814
661
  test('15. provenance with both Base and Head passes', () => {
815
662
  const dir = tmpDir();
816
663
  const featureDir = path.join(dir, 'specs', FEATURE);
664
+ fs.mkdirSync(featureDir, { recursive: true });
665
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
817
666
  const tasksDir = path.join(featureDir, 'tasks');
818
667
  fs.mkdirSync(tasksDir, { recursive: true });
819
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
820
- status: 'in_progress',
821
- feature_name: FEATURE,
822
- current_phase: 'closeout',
823
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
824
- }));
825
668
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
826
669
  '# Task',
827
670
  '',
828
- '**Status:** done',
671
+ 'Status: done',
829
672
  '',
830
- '## Evidence',
673
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
831
674
  '',
832
675
  'Verification: PASS',
833
676
  'Command: npm test',
@@ -847,57 +690,6 @@ test('15. provenance with both Base and Head passes', () => {
847
690
  }
848
691
  });
849
692
 
850
- test('16. registry task path with ../ escape or sibling-prefix is rejected (check a)', () => {
851
- const dir = tmpDir();
852
- const featureDir = path.join(dir, 'specs', FEATURE);
853
- const evilFeature = path.join(dir, 'specs', `${FEATURE}-evil`);
854
- const maliciousRel = `../${FEATURE}-evil/tasks/task.md`;
855
- const maliciousAbs = path.join(evilFeature, 'tasks/task.md');
856
- // Prepare evil file with valid receipt outside feature (should be ignored due to path traversal)
857
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
858
- fs.mkdirSync(path.join(evilFeature, 'tasks'), { recursive: true });
859
- fs.writeFileSync(maliciousAbs, [
860
- '# Task',
861
- '',
862
- '**Status:** done',
863
- '',
864
- '## Evidence',
865
- '',
866
- 'Verification: PASS',
867
- 'Command: npm test',
868
- 'Exit: 0',
869
- `Base: ${VALID_BASE}`,
870
- `Head: ${VALID_HEAD}`,
871
- '```',
872
- 'pass',
873
- '```',
874
- ].join('\n'));
875
- // Also create sibling-prefix style path: tasks/../../demo-evil/tasks/task.md
876
- const siblingPrefixRel = `tasks/../../${FEATURE}-evil/tasks/task.md`;
877
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
878
- status: 'in_progress',
879
- feature_name: FEATURE,
880
- current_phase: 'closeout',
881
- task_registry: {
882
- [maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
883
- [siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
884
- [path.resolve(featureDir, maliciousRel)]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
885
- },
886
- }));
887
- try {
888
- seedCache({ [FEATURE]: { [maliciousRel]: 'pending', [siblingPrefixRel]: 'pending', [path.resolve(featureDir, maliciousRel)]: 'pending' } });
889
- const body = parseBlock(runHook({}, dir).stdout);
890
- assert.ok(body, 'traversal path should block');
891
- assert.strictEqual(body.decision, 'block');
892
- // Must fail check a (file + Status)
893
- assert.match(body.reason, /\ba\b/);
894
- // Ensure malicious path is named in reason
895
- assert.ok(body.reason.includes(maliciousRel) || body.reason.includes(siblingPrefixRel));
896
- } finally {
897
- fs.rmSync(dir, { recursive: true, force: true });
898
- }
899
- });
900
-
901
693
  test('17. empty or bare provenance fails — Base:/Head: require non-empty same-line values', () => {
902
694
  const cases = [
903
695
  { evidence: ['Verification: PASS','Command: npm test','Exit: 0','Base:','Head: def456'].join('\n'), desc: 'empty Base:' },
@@ -911,14 +703,10 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
911
703
  for (const { evidence, desc } of cases) {
912
704
  const dir = tmpDir();
913
705
  const featureDir = path.join(dir, 'specs', FEATURE);
706
+ fs.mkdirSync(featureDir, { recursive: true });
707
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
914
708
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
915
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
916
- status: 'in_progress',
917
- feature_name: FEATURE,
918
- current_phase: 'closeout',
919
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
920
- }));
921
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
709
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
922
710
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
923
711
  const body = parseBlock(runHook({}, dir).stdout);
924
712
  assert.ok(body, `empty/bare provenance should block for ${desc}`);
@@ -937,15 +725,17 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
937
725
  for (const { evidence, shouldPass } of validCases) {
938
726
  const dir = tmpDir();
939
727
  const featureDir = path.join(dir, 'specs', FEATURE);
728
+ fs.mkdirSync(featureDir, { recursive: true });
729
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
940
730
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
941
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
942
- status: 'in_progress',
943
- feature_name: FEATURE,
944
- current_phase: 'closeout',
945
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
946
- }));
947
- const boundEvidence = shouldPass ? bindFixtureReceipt(dir, evidence) : evidence;
948
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + boundEvidence,'```','pass','```'].join('\n'));
731
+ const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
732
+ const taskText = (receipt) => ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + receipt,'```','pass','```'].join('\n');
733
+ // Base and Head bind to the live tree only for a committed task file that has since
734
+ // changed, so every case commits the receipt and then edits it: a pass case rebinds
735
+ // to the runtime-derived pair, a fail case keeps its arbitrary values.
736
+ fs.writeFileSync(taskFile, taskText(evidence));
737
+ commitAll(dir, 'receipt');
738
+ fs.writeFileSync(taskFile, shouldPass ? taskText(bindFixtureReceipt(dir, evidence)) : `${taskText(evidence)}\n`);
949
739
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
950
740
  const result = runHook({}, dir);
951
741
  if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
@@ -982,8 +772,8 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
982
772
  // mutate: strip Verification: PASS and Command — keep file as done
983
773
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
984
774
  fs.writeFileSync(taskFile, [
985
- '# Task', '', '**Status:** done', '',
986
- '## Evidence', '',
775
+ '# Task', '', 'Status: done', '',
776
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '',
987
777
  'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
988
778
  '```', 'pass', '```',
989
779
  ].join('\n'));
@@ -1007,8 +797,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1007
797
  // mutate: remove Head, leave only Base
1008
798
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
1009
799
  fs.writeFileSync(taskFile, [
1010
- '# Task', '', '**Status:** done', '',
1011
- '## Evidence', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
800
+ '# Task', '', 'Status: done', '',
801
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
1012
802
  '```', 'pass', '```',
1013
803
  ].join('\n'));
1014
804
  let body = parseBlock(runHook({}, dir).stdout);
@@ -1016,8 +806,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1016
806
  assert.match(body.reason, /\bprovenance\b/);
1017
807
  // mutate back to include both but with empty Head value (stale)
1018
808
  fs.writeFileSync(taskFile, [
1019
- '# Task', '', '**Status:** done', '',
1020
- '## Evidence', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
809
+ '# Task', '', 'Status: done', '',
810
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
1021
811
  '```', 'pass', '```',
1022
812
  ].join('\n'));
1023
813
  body = parseBlock(runHook({}, dir).stdout);
@@ -1028,22 +818,6 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1028
818
  }
1029
819
  });
1030
820
 
1031
- test('22. cache-hit: deleted task file blocks even though spec still says done', () => {
1032
- const dir = makeFixture({ evidence: 'valid' });
1033
- try {
1034
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1035
- assert.strictEqual(runHook({}, dir).stdout, '');
1036
- assert.strictEqual(readCache()[FEATURE][TASK_REL], 'done');
1037
- fs.unlinkSync(path.join(dir, 'specs', FEATURE, TASK_REL));
1038
- const body = parseBlock(runHook({}, dir).stdout);
1039
- assert.ok(body, 'deleted receipt file should block on cache-hit');
1040
- assert.strictEqual(body.decision, 'block');
1041
- assert.match(body.reason, /\ba\b/);
1042
- } finally {
1043
- fs.rmSync(dir, { recursive: true, force: true });
1044
- }
1045
- });
1046
-
1047
821
  test('23. malformed cache JSON does not bypass validation', () => {
1048
822
  // valid receipt with malformed cache → must still PASS (fail-open on cache parse, not on receipt)
1049
823
  const dirValid = makeFixture({ evidence: 'valid' });
@@ -1096,9 +870,10 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1096
870
  for (const { evidence, check, desc } of cases) {
1097
871
  const dir = tmpDir();
1098
872
  const featureDir = path.join(dir, 'specs', FEATURE);
873
+ fs.mkdirSync(featureDir, { recursive: true });
874
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1099
875
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1100
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1101
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
876
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1102
877
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1103
878
  const body = parseBlock(runHook({}, dir).stdout);
1104
879
  assert.ok(body, `placeholder case ${desc} should block`);
@@ -1110,8 +885,8 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1110
885
  const dir2 = tmpDir();
1111
886
  const featureDir2 = path.join(dir2, 'specs', FEATURE);
1112
887
  fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
1113
- fs.writeFileSync(path.join(featureDir2, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1114
- fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`,'```','pass','```'].join('\n')));
888
+ fs.writeFileSync(path.join(featureDir2, 'plan.md'), '# Demo plan\n');
889
+ fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm run todo:test', '', '## Receipt','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`,'```','pass','```'].join('\n')));
1115
890
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1116
891
  assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
1117
892
  fs.rmSync(dir2, { recursive: true, force: true });
@@ -1126,9 +901,10 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1126
901
  for (const { evidence, desc } of cases) {
1127
902
  const dir = tmpDir();
1128
903
  const featureDir = path.join(dir, 'specs', FEATURE);
904
+ fs.mkdirSync(featureDir, { recursive: true });
905
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1129
906
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1130
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1131
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
907
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: pnpm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1132
908
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1133
909
  const body = parseBlock(runHook({}, dir).stdout);
1134
910
  assert.ok(body, `explicit failure ${desc} should block`);
@@ -1140,8 +916,9 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1140
916
  test('27. task markdown symlink outside feature must be rejected (check a)', () => {
1141
917
  const dir = tmpDir();
1142
918
  const featureDir = path.join(dir, 'specs', FEATURE);
919
+ fs.mkdirSync(featureDir, { recursive: true });
920
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1143
921
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1144
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1145
922
  const outside = path.join(dir, 'outside.md');
1146
923
  fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1147
924
  const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
@@ -1151,23 +928,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
1151
928
  const body = parseBlock(runHook({}, dir).stdout);
1152
929
  assert.ok(body, 'symlink task outside should block');
1153
930
  assert.strictEqual(body.decision, 'block');
1154
- assert.match(body.reason, /\ba\b/);
1155
- fs.rmSync(dir, { recursive: true, force: true });
1156
- });
1157
-
1158
- test('28. malformed spec.json must block with invalid_specs', () => {
1159
- const dir = tmpDir();
1160
- fs.mkdirSync(path.join(dir, 'specs', 'good'), { recursive: true });
1161
- fs.mkdirSync(path.join(dir, 'specs', 'bad'), { recursive: true });
1162
- fs.writeFileSync(path.join(dir, 'specs', 'good', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'good', task_registry: {} }));
1163
- fs.writeFileSync(path.join(dir, 'specs', 'bad', 'spec.json'), '{ malformed');
1164
- seedCache({});
1165
- const { stdout } = runHook({}, dir);
1166
- const body = parseBlock(stdout);
1167
- assert.ok(body, 'malformed spec should block');
1168
- assert.strictEqual(body.decision, 'block');
1169
- assert.match(body.reason, /invalid spec/i);
1170
- assert.match(body.reason, /bad/);
931
+ assert.match(body.reason, /invalid workflow packet/);
1171
932
  fs.rmSync(dir, { recursive: true, force: true });
1172
933
  });
1173
934
 
@@ -1177,7 +938,6 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1177
938
  fs.mkdirSync(specsDir, { recursive: true });
1178
939
  const outside = path.join(dir, 'outside-feat');
1179
940
  fs.mkdirSync(outside, { recursive: true });
1180
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1181
941
  const link = path.join(specsDir, 'linked');
1182
942
  fs.symlinkSync(outside, link);
1183
943
  // non-explicit should block with invalid_specs (fail-closed, not phantom no-active)
@@ -1185,7 +945,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1185
945
  const nonExplicit = parseBlock(runHook({}, dir).stdout);
1186
946
  assert.ok(nonExplicit, 'non-explicit symlink outside should block with invalid_specs');
1187
947
  assert.strictEqual(nonExplicit.decision, 'block');
1188
- assert.match(nonExplicit.reason, /invalid spec/i);
948
+ assert.match(nonExplicit.reason, /invalid workflow packet/i);
1189
949
  assert.match(nonExplicit.reason, /linked/);
1190
950
  // explicit should block explicit_malformed
1191
951
  const body = parseBlock(runHook({ featureName: 'linked' }, dir).stdout);
@@ -1227,15 +987,11 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1227
987
  for (const v of vectors) {
1228
988
  const dir = tmpDir();
1229
989
  const featureDir = path.join(dir, 'specs', FEATURE);
990
+ fs.mkdirSync(featureDir, { recursive: true });
991
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1230
992
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1231
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1232
- status: 'in_progress',
1233
- feature_name: FEATURE,
1234
- current_phase: 'closeout',
1235
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1236
- }));
1237
993
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
1238
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
994
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1239
995
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1240
996
  const body = parseBlock(runHook({}, dir).stdout);
1241
997
  assert.ok(body, `vector should block: ${v}`);
@@ -1274,14 +1030,9 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1274
1030
  const dir = tmpDir();
1275
1031
  const featureDir = path.join(dir, 'specs', FEATURE);
1276
1032
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1277
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1278
- status: 'in_progress',
1279
- feature_name: FEATURE,
1280
- current_phase: 'closeout',
1281
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1282
- }));
1033
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1283
1034
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
1284
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
1035
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1285
1036
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1286
1037
  const { stdout } = runHook({}, dir);
1287
1038
  assert.strictEqual(stdout, '', `positive control should not block: ${v}`);
@@ -1880,6 +1631,26 @@ test('52. the block names what to change, not just which check failed', () => {
1880
1631
  }
1881
1632
  });
1882
1633
 
1634
+ test('53. a hand-typed provenance pair is told to re-derive, not to add lines', () => {
1635
+ // Reported from a project whose receipt carried git short SHAs. Telling someone to
1636
+ // add `Base:` and `Head:` lines they had already written sends them looking in the
1637
+ // wrong place; the pair is there, it just is not the one the runtime derives.
1638
+ const dir = makeWorkflowFixture([
1639
+ '## Receipt', '', '- Verification: PASS', '- Command: node --test', '- Exit: 0',
1640
+ '- Base: d72bb57', '- Head: 5da1eca', '```text', '$ node --test', 'pass: 1', '```',
1641
+ ]);
1642
+ try {
1643
+ clearCache();
1644
+ const body = parseBlock(runHook({}, dir).stdout);
1645
+ assert.ok(body && body.decision === 'block');
1646
+ assert.match(body.reason, /present but are not the pair this runtime derives/);
1647
+ assert.match(body.reason, /short SHAs/);
1648
+ assert.doesNotMatch(body.reason, /add runtime-derived/, 'the lines exist; do not ask for them again');
1649
+ } finally {
1650
+ fs.rmSync(dir, { recursive: true, force: true });
1651
+ }
1652
+ });
1653
+
1883
1654
  test('49. an unversioned specs root does not keep every receipt bound', () => {
1884
1655
  // Committing specs/ is the user's choice, not the tool's requirement. Without a
1885
1656
  // committed copy of the task file there is no baseline, so the selector used to keep
@@ -1955,3 +1726,143 @@ test('48. the gate\'s own runtime state does not defeat structure mode', () => {
1955
1726
  fs.rmSync(dir, { recursive: true, force: true });
1956
1727
  }
1957
1728
  });
1729
+
1730
+ test('52. a plan with several commands binds the Receipt to the last one', () => {
1731
+ // A proof that takes several steps declares several commands. The Receipt must name the
1732
+ // last: naming an earlier, cheaper step would let the task close without its real proof.
1733
+ const planned = 'node -v\n- Command: node --test';
1734
+ const earlier = boundReceipt().map((line) => line
1735
+ .replace('Command: node --test', 'Command: node -v').replace('$ node --test', '$ node -v'));
1736
+ const absent = boundReceipt().map((line) => line
1737
+ .replace('Command: node --test', 'Command: npm test').replace('$ node --test', '$ npm test'));
1738
+ const cases = [
1739
+ [boundReceipt(), null, 'the last declared command closes the task'],
1740
+ [earlier, /command_identity/, 'an earlier declared command must not close the task'],
1741
+ [absent, /command_identity/, 'a command the plan never declared must not close the task'],
1742
+ ];
1743
+ for (const [receipt, expected, message] of cases) {
1744
+ const dir = makeWorkflowFixture(receipt, planned);
1745
+ try {
1746
+ clearCache();
1747
+ const out = runHook({}, dir).stdout;
1748
+ if (expected === null) {
1749
+ assert.strictEqual(out, '', message);
1750
+ } else {
1751
+ const body = parseBlock(out);
1752
+ assert.ok(body && body.decision === 'block', message);
1753
+ assert.match(body.reason, expected, message);
1754
+ }
1755
+ } finally {
1756
+ fs.rmSync(dir, { recursive: true, force: true });
1757
+ }
1758
+ }
1759
+ });
1760
+
1761
+ test('53. an indented or sub-heading Command never becomes the canonical one', () => {
1762
+ // A cheap command nested under a Counterexample, or placed under a ### step after the main
1763
+ // block, must not become the command a Receipt can close on.
1764
+ const cheap = boundReceipt().map((line) => line
1765
+ .replace('Command: node --test', 'Command: true').replace('$ node --test', '$ true'));
1766
+ for (const planned of [
1767
+ 'node --test\n- Counterexample: none\n - Command: true',
1768
+ 'node --test\n\n### Cleanup\n\n- Command: true',
1769
+ ]) {
1770
+ for (const [receipt, expected] of [[cheap, /command_identity/], [boundReceipt(), null]]) {
1771
+ const dir = makeWorkflowFixture(receipt, planned);
1772
+ try {
1773
+ clearCache();
1774
+ const out = runHook({}, dir).stdout;
1775
+ if (expected === null) assert.strictEqual(out, '', `the main command must still close: ${planned}`);
1776
+ else assert.match(parseBlock(out).reason, expected, `a nested command must not close: ${planned}`);
1777
+ } finally {
1778
+ fs.rmSync(dir, { recursive: true, force: true });
1779
+ }
1780
+ }
1781
+ }
1782
+ });
1783
+
1784
+ function plantLegacyPacket(dir, name = 'old') {
1785
+ const legacyDir = path.join(dir, 'specs', name);
1786
+ fs.mkdirSync(path.join(legacyDir, 'tasks'), { recursive: true });
1787
+ fs.writeFileSync(path.join(legacyDir, 'spec.json'), JSON.stringify({
1788
+ feature_name: name,
1789
+ status: 'in_progress',
1790
+ task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
1791
+ }));
1792
+ fs.writeFileSync(path.join(legacyDir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
1793
+ }
1794
+
1795
+ function writeActiveFeature(dir, featureName) {
1796
+ fs.mkdirSync(path.join(dir, 'specs', '_shared'), { recursive: true });
1797
+ fs.writeFileSync(path.join(dir, 'specs', '_shared', 'active-feature.json'), JSON.stringify({ featureName }));
1798
+ }
1799
+
1800
+ test('a leftover legacy packet alone never blocks Stop', () => {
1801
+ const dir = tmpDir();
1802
+ try {
1803
+ plantLegacyPacket(dir);
1804
+ clearCache();
1805
+ assert.strictEqual(runHook({}, dir).stdout, '');
1806
+ } finally {
1807
+ fs.rmSync(dir, { recursive: true, force: true });
1808
+ }
1809
+ });
1810
+
1811
+ test('a recorded active feature naming a legacy packet does not block Stop', () => {
1812
+ const dir = makeFixture({ evidence: 'valid' });
1813
+ try {
1814
+ plantLegacyPacket(dir);
1815
+ writeActiveFeature(dir, 'old');
1816
+ clearCache();
1817
+ assert.strictEqual(runHook({}, dir).stdout, '');
1818
+ } finally {
1819
+ fs.rmSync(dir, { recursive: true, force: true });
1820
+ }
1821
+ });
1822
+
1823
+ test('a recorded active feature naming a missing packet still blocks as explicit_not_found', () => {
1824
+ const dir = makeFixture({ evidence: 'valid' });
1825
+ try {
1826
+ writeActiveFeature(dir, 'nope');
1827
+ clearCache();
1828
+ const body = parseBlock(runHook({}, dir).stdout);
1829
+ assert.strictEqual(body?.decision, 'block');
1830
+ assert.match(body.reason, /explicit_not_found/);
1831
+ } finally {
1832
+ fs.rmSync(dir, { recursive: true, force: true });
1833
+ }
1834
+ });
1835
+
1836
+ test('a process-first packet with a dependency cycle and a done task blocks Stop', () => {
1837
+ const dir = tmpDir();
1838
+ try {
1839
+ const featureDir = path.join(dir, 'specs', FEATURE);
1840
+ fs.mkdirSync(featureDir, { recursive: true });
1841
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
1842
+ const task = (status, dependency) => [
1843
+ '# Task', '', `Status: ${status}`, '', '## Dependencies', '', `- ${dependency}`, '',
1844
+ '## Verification Plan', '', '- Command: npm test', '',
1845
+ ].join('\n');
1846
+ fs.writeFileSync(path.join(featureDir, 'task-01-a.md'), task('done', 'task-02-b.md'));
1847
+ fs.writeFileSync(path.join(featureDir, 'task-02-b.md'), task('pending', 'task-01-a.md'));
1848
+ clearCache();
1849
+ const body = parseBlock(runHook({}, dir).stdout);
1850
+ assert.strictEqual(body?.decision, 'block');
1851
+ assert.match(body.reason, /invalid workflow packet/);
1852
+ } finally {
1853
+ fs.rmSync(dir, { recursive: true, force: true });
1854
+ }
1855
+ });
1856
+
1857
+ test('a packet holding both plan.md and spec.json is read as process-first and its unreceipted done task blocks', () => {
1858
+ const dir = makeFixture({ evidence: 'missing-evidence' });
1859
+ try {
1860
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'spec.json'), JSON.stringify({ feature_name: FEATURE }));
1861
+ clearCache();
1862
+ const body = parseBlock(runHook({}, dir).stdout);
1863
+ assert.strictEqual(body?.decision, 'block');
1864
+ assert.match(body.reason, new RegExp(TASK_REL));
1865
+ } finally {
1866
+ fs.rmSync(dir, { recursive: true, force: true });
1867
+ }
1868
+ });