@haposoft/cafekit 0.18.0 → 0.18.2

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 (162) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/claude-settings.test.js +123 -0
  3. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  4. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  6. package/bin/__tests__/codex-hooks.test.js +938 -237
  7. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  8. package/bin/__tests__/codex-native.test.js +222 -149
  9. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  10. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  11. package/bin/__tests__/codex-research-projection.test.js +70 -0
  12. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  13. package/bin/__tests__/develop-contract.test.js +102 -1362
  14. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  15. package/bin/__tests__/inspect-paths.test.js +183 -0
  16. package/bin/__tests__/installer-safety.test.js +335 -0
  17. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  18. package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
  19. package/bin/__tests__/omp-bridge.test.js +66 -12
  20. package/bin/__tests__/omp-hooks.test.js +11 -5
  21. package/bin/__tests__/orca-session.test.js +7 -0
  22. package/bin/__tests__/package-inventory.test.js +1 -395
  23. package/bin/__tests__/privacy-paths.test.js +317 -0
  24. package/bin/__tests__/spec-narrowing.test.js +30 -134
  25. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  26. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  27. package/bin/__tests__/usage-hook-retired.test.js +5 -3
  28. package/bin/__tests__/validator-grounding.test.js +17 -2929
  29. package/bin/install.js +25 -0
  30. package/bin/lib/codex-install.js +134 -12
  31. package/bin/lib/context.js +1 -1
  32. package/bin/lib/managed-writer.js +2 -3
  33. package/bin/phases/claude-runtime.js +91 -25
  34. package/bin/phases/claude-settings.js +22 -1
  35. package/bin/phases/codex-hooks.js +96 -33
  36. package/bin/phases/codex-runtime.js +45 -49
  37. package/bin/phases/copy-payload.js +11 -28
  38. package/bin/phases/omp-runtime.js +6 -3
  39. package/package.json +2 -3
  40. package/src/claude/CLAUDE.md +4 -0
  41. package/src/claude/agents/code-auditor.md +9 -6
  42. package/src/claude/agents/debugger.md +3 -9
  43. package/src/claude/agents/docs-keeper.md +4 -7
  44. package/src/claude/agents/git-ops.md +2 -4
  45. package/src/claude/agents/implementer.md +6 -7
  46. package/src/claude/agents/inspector.md +2 -0
  47. package/src/claude/agents/project-manager.md +2 -2
  48. package/src/claude/agents/spec-maker.md +4 -4
  49. package/src/claude/agents/test-runner.md +8 -11
  50. package/src/claude/agents/ui-ux-designer.md +0 -1
  51. package/src/claude/gitignore +3 -0
  52. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  53. package/src/claude/hooks/__tests__/precompact.test.js +206 -10
  54. package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
  55. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  56. package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
  57. package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
  58. package/src/claude/hooks/__tests__/state.test.js +207 -7
  59. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  60. package/src/claude/hooks/agent.cjs +7 -6
  61. package/src/claude/hooks/inspect-block.cjs +15 -63
  62. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  63. package/src/claude/hooks/lib/parser.cjs +1 -1
  64. package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
  65. package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
  66. package/src/claude/hooks/lib/project-root.cjs +15 -0
  67. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  68. package/src/claude/hooks/lib/spec-touch.cjs +67 -0
  69. package/src/claude/hooks/lib/update-check.cjs +77 -0
  70. package/src/claude/hooks/precompact.cjs +8 -15
  71. package/src/claude/hooks/privacy-block.cjs +23 -205
  72. package/src/claude/hooks/rules.cjs +6 -28
  73. package/src/claude/hooks/session.cjs +34 -117
  74. package/src/claude/hooks/spec-gate.cjs +17 -89
  75. package/src/claude/hooks/spec-state.cjs +89 -96
  76. package/src/claude/hooks/state.cjs +96 -51
  77. package/src/claude/migration-manifest.json +20 -8
  78. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  79. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  80. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  81. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  82. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  83. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  84. package/src/claude/rules/ai-dev-rules.md +1 -2
  85. package/src/claude/rules/hook-protocols.md +1 -1
  86. package/src/claude/rules/workflow.md +1 -1
  87. package/src/claude/runtime.json +0 -3
  88. package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
  89. package/src/claude/scripts/provenance.cjs +14 -0
  90. package/src/claude/scripts/spec-receipt.cjs +0 -114
  91. package/src/claude/scripts/spec-resolver.cjs +41 -457
  92. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  93. package/src/claude/settings/settings.json +1 -1
  94. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  95. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  96. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  97. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  98. package/src/claude/skills/code-review/SKILL.md +25 -108
  99. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  100. package/src/claude/skills/develop/SKILL.md +22 -70
  101. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  102. package/src/claude/skills/git/SKILL.md +1 -1
  103. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  104. package/src/claude/skills/research/SKILL.md +24 -55
  105. package/src/claude/skills/sync/SKILL.md +12 -34
  106. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  107. package/src/claude/skills/test/SKILL.md +39 -123
  108. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  109. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  110. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  111. package/src/claude/skills/web-testing/SKILL.md +2 -20
  112. package/src/codex/AGENTS.md +8 -0
  113. package/src/codex/hooks/ask-guard.cjs +54 -0
  114. package/src/codex/hooks/inspect-block.cjs +11 -7
  115. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  116. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  117. package/src/codex/hooks/lib/privacy-state.cjs +98 -16
  118. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  119. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  120. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  121. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  122. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  123. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  124. package/src/codex/hooks/lib/update-check.cjs +1 -0
  125. package/src/codex/hooks/precompact.cjs +39 -0
  126. package/src/codex/hooks/privacy-approval.cjs +28 -17
  127. package/src/codex/hooks/privacy-block.cjs +9 -215
  128. package/src/codex/hooks/rules.cjs +4 -30
  129. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  130. package/src/codex/hooks/session.cjs +47 -11
  131. package/src/codex/hooks/spec-gate.cjs +32 -74
  132. package/src/codex/hooks/spec-state.cjs +68 -46
  133. package/src/codex/hooks/state.cjs +25 -4
  134. package/src/codex/hooks.json +39 -7
  135. package/src/codex/rules/hook-protocols.md +13 -6
  136. package/src/common/AGENTS.md +4 -10
  137. package/src/omp/extensions/cafekit-bridge.mjs +12 -4
  138. package/src/omp/hooks/privacy-block.cjs +24 -206
  139. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  140. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  141. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  142. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  143. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  144. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  145. package/src/claude/hooks/lib/context.cjs +0 -527
  146. package/src/claude/hooks/lib/detect.cjs +0 -474
  147. package/src/claude/scripts/change-firewall.cjs +0 -1748
  148. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  149. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  150. package/src/claude/scripts/spec-final-state.cjs +0 -290
  151. package/src/claude/scripts/spec-ground.cjs +0 -650
  152. package/src/claude/scripts/spec-readiness.cjs +0 -425
  153. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  154. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  155. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  156. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  157. package/src/claude/skills/specs/templates/design.md +0 -82
  158. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  159. package/src/claude/skills/specs/templates/requirements.md +0 -42
  160. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  161. package/src/claude/skills/specs/templates/task.md +0 -47
  162. package/src/codex/rules/state-sync.md +0 -60
@@ -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,14 +29,10 @@ 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
- // PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
38
- // monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
39
- const env = { ...process.env, PROJECT_ROOT: projectRoot };
32
+ // Source hooks use payload.cwd without inherited root overrides.
33
+ const env = { ...process.env };
34
+ delete env.CLAUDE_PROJECT_DIR;
35
+ delete env.PROJECT_ROOT;
40
36
  const res = spawnSync(process.execPath, [HOOK], {
41
37
  input: JSON.stringify({
42
38
  hook_event_name: 'Stop',
@@ -69,7 +65,7 @@ function runtimeContext(dir) {
69
65
  return PROVENANCE.deriveRuntimeContext({
70
66
  projectRoot: dir,
71
67
  specsRoot: path.join(dir, 'specs'),
72
- specFile: path.join(dir, 'specs', FEATURE, 'spec.json'),
68
+ specFile: path.join(dir, 'specs', FEATURE, 'plan.md'),
73
69
  featureName: FEATURE,
74
70
  runtimeSession: 'test',
75
71
  });
@@ -118,38 +114,6 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
118
114
  return dir;
119
115
  }
120
116
 
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
117
  function clearCache() {
154
118
  try { fs.unlinkSync(CACHE); } catch { /* absent */ }
155
119
  }
@@ -164,52 +128,32 @@ function readCache() {
164
128
  }
165
129
 
166
130
  /**
167
- * Build a minimal active-spec fixture under dir.
131
+ * Build a minimal process-first packet under dir: plan.md plus one flat task
132
+ * whose Verification Plan runs `npm test` and whose inline Receipt varies.
168
133
  * @param {object} opts
169
134
  * @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']
135
+ * @param {'valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
173
136
  * @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
137
  */
177
138
  function makeFixture(opts = {}) {
178
139
  const {
179
140
  taskStatus = 'done',
180
- completed_at = '2026-07-01T00:00:00Z',
181
- mdStatus = 'done',
182
141
  evidence = 'valid',
183
142
  runtime = null,
184
- workflowPolicy = null,
185
- phase = 'closeout',
186
143
  } = opts;
187
144
  const dir = tmpDir();
188
145
  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));
146
+ fs.mkdirSync(featureDir, { recursive: true });
147
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
203
148
 
204
- let evidenceBlock = '';
149
+ let receiptBlock = '';
205
150
  if (evidence === 'valid') {
206
- evidenceBlock = [
207
- '## Evidence',
151
+ receiptBlock = [
152
+ '## Receipt',
208
153
  '',
209
154
  'Verification: PASS',
210
155
  'Command: npm test',
211
156
  'Exit: 0',
212
- 'Result: PASS',
213
157
  `Base: ${VALID_BASE}`,
214
158
  `Head: ${VALID_HEAD}`,
215
159
  `Artifact: output/bundle.js (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
@@ -219,34 +163,38 @@ function makeFixture(opts = {}) {
219
163
  '```',
220
164
  '',
221
165
  ].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
166
  } else if (evidence === 'failed') {
225
- evidenceBlock = '## Evidence\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
167
+ receiptBlock = '## Receipt\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
226
168
  } else if (evidence === 'fence-only') {
227
- evidenceBlock = '## Evidence\n\n```\nnpm test\n```\n';
169
+ receiptBlock = '## Receipt\n\n```\nnpm test\n```\n';
228
170
  } else if (evidence === 'placeholder') {
229
- evidenceBlock = [
230
- '## Evidence',
171
+ receiptBlock = [
172
+ '## Receipt',
231
173
  '',
232
174
  'Command: `{{TYPECHECK / TEST COMMAND}}`',
233
175
  'Expected: {{What proves success}}',
234
176
  '',
235
177
  ].join('\n');
236
178
  } else if (evidence === 'missing-evidence') {
237
- evidenceBlock = '## Risk Assessment\n\nNone.\n';
238
- } else {
239
- evidenceBlock = '';
179
+ receiptBlock = '## Risk Assessment\n\nNone.\n';
240
180
  }
241
181
 
242
182
  fs.writeFileSync(
243
- path.join(dir, 'specs', FEATURE, TASK_REL),
183
+ path.join(featureDir, TASK_REL),
244
184
  [
245
- `# Task R0-01: example`,
185
+ '# Task 01: example',
186
+ '',
187
+ `Status: ${taskStatus}`,
246
188
  '',
247
- `**Status:** ${mdStatus}`,
189
+ '## Dependencies',
248
190
  '',
249
- evidenceBlock,
191
+ '- none',
192
+ '',
193
+ '## Verification Plan',
194
+ '',
195
+ '- Command: npm test',
196
+ '',
197
+ receiptBlock,
250
198
  ].join('\n'),
251
199
  );
252
200
 
@@ -261,9 +209,7 @@ function makeFixture(opts = {}) {
261
209
  if (evidence === 'valid') {
262
210
  fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
263
211
  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);
212
+ const taskFile = path.join(featureDir, TASK_REL);
267
213
  fs.writeFileSync(taskFile, bindFixtureReceipt(dir, fs.readFileSync(taskFile, 'utf8')));
268
214
  }
269
215
 
@@ -290,12 +236,14 @@ function installClaudeGate(root, policyMode = 'valid') {
290
236
  fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
291
237
  fs.mkdirSync(path.join(hooks, 'lib'), { recursive: true });
292
238
  fs.copyFileSync(path.join(__dirname, '..', 'lib', 'runtime-dir.cjs'), path.join(hooks, 'lib', 'runtime-dir.cjs'));
239
+ fs.copyFileSync(path.join(__dirname, '..', 'lib', 'project-root.cjs'), path.join(hooks, 'lib', 'project-root.cjs'));
240
+ fs.copyFileSync(path.join(__dirname, '..', 'lib', 'spec-touch.cjs'), path.join(hooks, 'lib', 'spec-touch.cjs'));
293
241
  fs.copyFileSync(path.join(__dirname, '..', 'lib', 'hook-payload.cjs'), path.join(hooks, 'lib', 'hook-payload.cjs'));
294
242
  fs.copyFileSync(
295
243
  path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
296
244
  path.join(hooks, 'lib', 'hook-state-dir.cjs'),
297
245
  );
298
- 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']) {
246
+ for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs']) {
299
247
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
300
248
  }
301
249
  if (policyMode === 'valid') {
@@ -401,31 +349,21 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
401
349
  const dir = tmpDir();
402
350
  const gate = installClaudeGate(dir);
403
351
  const featureDir = path.join(dir, 'specs', FEATURE);
352
+ fs.mkdirSync(featureDir, { recursive: true });
353
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
404
354
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
405
355
  fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
406
356
  fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
407
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
408
- status: 'in_progress',
409
- current_phase: 'closeout',
410
- feature_name: FEATURE,
411
- task_registry: {
412
- [TASK_REL]: {
413
- status: 'done',
414
- completed_at: '2026-08-11T00:00:00.000Z',
415
- artifacts: ['output/bundle.js'],
416
- },
417
- },
418
- }));
419
357
  fs.writeFileSync(path.join(featureDir, TASK_REL), [
420
- '# Task', '', '**Status:** done', '', '## Evidence', '',
358
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
421
359
  'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
422
360
  'Artifact: output/bundle.js', suffix,
361
+ '```', 'node --test', 'ok 1', '```',
423
362
  ].join('\n'));
424
363
  fs.writeFileSync(
425
364
  path.join(featureDir, TASK_REL),
426
365
  bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
427
366
  );
428
- installFeatureReceipt(dir);
429
367
  try {
430
368
  const result = spawnSync(process.execPath, [gate], {
431
369
  cwd: dir,
@@ -455,6 +393,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
455
393
  for (const { artifactPath, symlink } of cases) {
456
394
  const dir = tmpDir();
457
395
  const featureDir = path.join(dir, 'specs', FEATURE);
396
+ fs.mkdirSync(featureDir, { recursive: true });
397
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
458
398
  const artifactBytes = Buffer.from(`artifact-${artifactPath}\n`);
459
399
  const digest = crypto.createHash('sha256').update(artifactBytes).digest('hex');
460
400
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
@@ -466,20 +406,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
466
406
  } else {
467
407
  fs.writeFileSync(path.join(dir, 'outside.js'), artifactBytes);
468
408
  }
469
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
470
- status: 'in_progress',
471
- current_phase: 'closeout',
472
- feature_name: FEATURE,
473
- task_registry: {
474
- [TASK_REL]: {
475
- status: 'done',
476
- completed_at: '2026-08-11T00:00:00.000Z',
477
- artifacts: [artifactPath],
478
- },
479
- },
480
- }));
481
409
  fs.writeFileSync(path.join(featureDir, TASK_REL), bindFixtureReceipt(dir, [
482
- '# Task', '', '**Status:** done', '', '## Evidence', '',
410
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
483
411
  'Verification: PASS', 'Command: node --test', 'Exit: 0',
484
412
  `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
485
413
  `Artifact: ${artifactPath} (sha256:${digest})`,
@@ -625,27 +553,6 @@ test('empty or malformed Claude hook payload fails closed', () => {
625
553
  }
626
554
  });
627
555
 
628
- test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
629
- const dir = makeFixture({
630
- workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
631
- });
632
- try {
633
- const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
634
- const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
635
- spec.proofs = {
636
- needsInspection: 'inspection completed',
637
- needsIndependentAudit: 'PASS',
638
- needsResearchGrounding: 'research completed',
639
- };
640
- fs.writeFileSync(specPath, JSON.stringify(spec));
641
- const body = parseBlock(runHook({}, dir).stdout);
642
- assert.equal(body?.decision, 'block');
643
- assert.match(body.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
644
- } finally {
645
- fs.rmSync(dir, { recursive: true, force: true });
646
- }
647
- });
648
-
649
556
  test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
650
557
  const dir = makeFixture({ evidence: 'placeholder' });
651
558
  try {
@@ -686,70 +593,20 @@ test('9. a code fence alone is not verification proof', () => {
686
593
  }
687
594
  });
688
595
 
689
- test('10. completed_at must be a valid ISO timestamp', () => {
690
- const dir = makeFixture({ completed_at: 'yesterday' });
691
- try {
692
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
693
- const body = parseBlock(runHook({}, dir).stdout);
694
- assert.ok(body);
695
- assert.match(body.reason, /\bcompleted_at\b/);
696
- } finally {
697
- fs.rmSync(dir, { recursive: true, force: true });
698
- }
699
- });
700
-
701
- test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
702
- const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
703
- try {
704
- seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
705
- assert.strictEqual(runHook({}, dir).stdout, '');
706
- assert.strictEqual(readCache()[FEATURE][TASK_REL], 'pending');
707
-
708
- const specFile = path.join(dir, 'specs', FEATURE, 'spec.json');
709
- const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
710
- spec.task_registry[TASK_REL] = {
711
- status: 'done',
712
- completed_at: '2026-07-01T00:00:00Z',
713
- };
714
- spec.current_phase = 'closeout';
715
- fs.writeFileSync(specFile, JSON.stringify(spec));
716
- const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
717
- fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
718
-
719
- assert.ok(parseBlock(runHook({}, dir).stdout), 're-completed failing task must be gated');
720
- } finally {
721
- fs.rmSync(dir, { recursive: true, force: true });
722
- }
723
- });
724
-
725
- test('12. legacy successful receipt remains read-compatible', () => {
726
- const dir = makeFixture({ evidence: 'legacy-valid' });
727
- try {
728
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
729
- assert.strictEqual(runHook({}, dir).stdout, '');
730
- } finally {
731
- fs.rmSync(dir, { recursive: true, force: true });
732
- }
733
- });
734
-
735
596
  test('13. provenance requires both Base and Head - only Base fails', () => {
736
597
  const dir = tmpDir();
737
598
  const featureDir = path.join(dir, 'specs', FEATURE);
599
+ fs.mkdirSync(featureDir, { recursive: true });
600
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
738
601
  const tasksDir = path.join(featureDir, 'tasks');
739
602
  fs.mkdirSync(tasksDir, { recursive: true });
740
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
741
- status: 'in_progress',
742
- feature_name: FEATURE,
743
- current_phase: 'closeout',
744
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
745
- }));
746
603
  // Only Base, missing Head
747
604
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
748
605
  '# Task',
749
606
  '',
750
- '**Status:** done',
607
+ 'Status: done',
751
608
  '',
752
- '## Evidence',
609
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
753
610
  '',
754
611
  'Verification: PASS',
755
612
  'Command: npm test',
@@ -773,21 +630,17 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
773
630
  test('14. provenance requires both Base and Head - only Head fails', () => {
774
631
  const dir = tmpDir();
775
632
  const featureDir = path.join(dir, 'specs', FEATURE);
633
+ fs.mkdirSync(featureDir, { recursive: true });
634
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
776
635
  const tasksDir = path.join(featureDir, 'tasks');
777
636
  fs.mkdirSync(tasksDir, { recursive: true });
778
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
779
- status: 'in_progress',
780
- feature_name: FEATURE,
781
- current_phase: 'closeout',
782
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
783
- }));
784
637
  // Only Head, missing Base
785
638
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), [
786
639
  '# Task',
787
640
  '',
788
- '**Status:** done',
641
+ 'Status: done',
789
642
  '',
790
- '## Evidence',
643
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
791
644
  '',
792
645
  'Verification: PASS',
793
646
  'Command: npm test',
@@ -811,20 +664,16 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
811
664
  test('15. provenance with both Base and Head passes', () => {
812
665
  const dir = tmpDir();
813
666
  const featureDir = path.join(dir, 'specs', FEATURE);
667
+ fs.mkdirSync(featureDir, { recursive: true });
668
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
814
669
  const tasksDir = path.join(featureDir, 'tasks');
815
670
  fs.mkdirSync(tasksDir, { recursive: true });
816
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
817
- status: 'in_progress',
818
- feature_name: FEATURE,
819
- current_phase: 'closeout',
820
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
821
- }));
822
671
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
823
672
  '# Task',
824
673
  '',
825
- '**Status:** done',
674
+ 'Status: done',
826
675
  '',
827
- '## Evidence',
676
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
828
677
  '',
829
678
  'Verification: PASS',
830
679
  'Command: npm test',
@@ -844,57 +693,6 @@ test('15. provenance with both Base and Head passes', () => {
844
693
  }
845
694
  });
846
695
 
847
- test('16. registry task path with ../ escape or sibling-prefix is rejected (check a)', () => {
848
- const dir = tmpDir();
849
- const featureDir = path.join(dir, 'specs', FEATURE);
850
- const evilFeature = path.join(dir, 'specs', `${FEATURE}-evil`);
851
- const maliciousRel = `../${FEATURE}-evil/tasks/task.md`;
852
- const maliciousAbs = path.join(evilFeature, 'tasks/task.md');
853
- // Prepare evil file with valid receipt outside feature (should be ignored due to path traversal)
854
- fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
855
- fs.mkdirSync(path.join(evilFeature, 'tasks'), { recursive: true });
856
- fs.writeFileSync(maliciousAbs, [
857
- '# Task',
858
- '',
859
- '**Status:** done',
860
- '',
861
- '## Evidence',
862
- '',
863
- 'Verification: PASS',
864
- 'Command: npm test',
865
- 'Exit: 0',
866
- `Base: ${VALID_BASE}`,
867
- `Head: ${VALID_HEAD}`,
868
- '```',
869
- 'pass',
870
- '```',
871
- ].join('\n'));
872
- // Also create sibling-prefix style path: tasks/../../demo-evil/tasks/task.md
873
- const siblingPrefixRel = `tasks/../../${FEATURE}-evil/tasks/task.md`;
874
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
875
- status: 'in_progress',
876
- feature_name: FEATURE,
877
- current_phase: 'closeout',
878
- task_registry: {
879
- [maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
880
- [siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
881
- [path.resolve(featureDir, maliciousRel)]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
882
- },
883
- }));
884
- try {
885
- seedCache({ [FEATURE]: { [maliciousRel]: 'pending', [siblingPrefixRel]: 'pending', [path.resolve(featureDir, maliciousRel)]: 'pending' } });
886
- const body = parseBlock(runHook({}, dir).stdout);
887
- assert.ok(body, 'traversal path should block');
888
- assert.strictEqual(body.decision, 'block');
889
- // Must fail check a (file + Status)
890
- assert.match(body.reason, /\ba\b/);
891
- // Ensure malicious path is named in reason
892
- assert.ok(body.reason.includes(maliciousRel) || body.reason.includes(siblingPrefixRel));
893
- } finally {
894
- fs.rmSync(dir, { recursive: true, force: true });
895
- }
896
- });
897
-
898
696
  test('17. empty or bare provenance fails — Base:/Head: require non-empty same-line values', () => {
899
697
  const cases = [
900
698
  { evidence: ['Verification: PASS','Command: npm test','Exit: 0','Base:','Head: def456'].join('\n'), desc: 'empty Base:' },
@@ -908,14 +706,10 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
908
706
  for (const { evidence, desc } of cases) {
909
707
  const dir = tmpDir();
910
708
  const featureDir = path.join(dir, 'specs', FEATURE);
709
+ fs.mkdirSync(featureDir, { recursive: true });
710
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
911
711
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
912
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
913
- status: 'in_progress',
914
- feature_name: FEATURE,
915
- current_phase: 'closeout',
916
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
917
- }));
918
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
712
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
919
713
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
920
714
  const body = parseBlock(runHook({}, dir).stdout);
921
715
  assert.ok(body, `empty/bare provenance should block for ${desc}`);
@@ -934,15 +728,17 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
934
728
  for (const { evidence, shouldPass } of validCases) {
935
729
  const dir = tmpDir();
936
730
  const featureDir = path.join(dir, 'specs', FEATURE);
731
+ fs.mkdirSync(featureDir, { recursive: true });
732
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
937
733
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
938
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
939
- status: 'in_progress',
940
- feature_name: FEATURE,
941
- current_phase: 'closeout',
942
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
943
- }));
944
- const boundEvidence = shouldPass ? bindFixtureReceipt(dir, evidence) : evidence;
945
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + boundEvidence,'```','pass','```'].join('\n'));
734
+ const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
735
+ const taskText = (receipt) => ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + receipt,'```','pass','```'].join('\n');
736
+ // Base and Head bind to the live tree only for a committed task file that has since
737
+ // changed, so every case commits the receipt and then edits it: a pass case rebinds
738
+ // to the runtime-derived pair, a fail case keeps its arbitrary values.
739
+ fs.writeFileSync(taskFile, taskText(evidence));
740
+ commitAll(dir, 'receipt');
741
+ fs.writeFileSync(taskFile, shouldPass ? taskText(bindFixtureReceipt(dir, evidence)) : `${taskText(evidence)}\n`);
946
742
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
947
743
  const result = runHook({}, dir);
948
744
  if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
@@ -979,8 +775,8 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
979
775
  // mutate: strip Verification: PASS and Command — keep file as done
980
776
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
981
777
  fs.writeFileSync(taskFile, [
982
- '# Task', '', '**Status:** done', '',
983
- '## Evidence', '',
778
+ '# Task', '', 'Status: done', '',
779
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '',
984
780
  'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
985
781
  '```', 'pass', '```',
986
782
  ].join('\n'));
@@ -1004,8 +800,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1004
800
  // mutate: remove Head, leave only Base
1005
801
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
1006
802
  fs.writeFileSync(taskFile, [
1007
- '# Task', '', '**Status:** done', '',
1008
- '## Evidence', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
803
+ '# Task', '', 'Status: done', '',
804
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
1009
805
  '```', 'pass', '```',
1010
806
  ].join('\n'));
1011
807
  let body = parseBlock(runHook({}, dir).stdout);
@@ -1013,8 +809,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1013
809
  assert.match(body.reason, /\bprovenance\b/);
1014
810
  // mutate back to include both but with empty Head value (stale)
1015
811
  fs.writeFileSync(taskFile, [
1016
- '# Task', '', '**Status:** done', '',
1017
- '## Evidence', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
812
+ '# Task', '', 'Status: done', '',
813
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
1018
814
  '```', 'pass', '```',
1019
815
  ].join('\n'));
1020
816
  body = parseBlock(runHook({}, dir).stdout);
@@ -1025,22 +821,6 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1025
821
  }
1026
822
  });
1027
823
 
1028
- test('22. cache-hit: deleted task file blocks even though spec still says done', () => {
1029
- const dir = makeFixture({ evidence: 'valid' });
1030
- try {
1031
- seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1032
- assert.strictEqual(runHook({}, dir).stdout, '');
1033
- assert.strictEqual(readCache()[FEATURE][TASK_REL], 'done');
1034
- fs.unlinkSync(path.join(dir, 'specs', FEATURE, TASK_REL));
1035
- const body = parseBlock(runHook({}, dir).stdout);
1036
- assert.ok(body, 'deleted receipt file should block on cache-hit');
1037
- assert.strictEqual(body.decision, 'block');
1038
- assert.match(body.reason, /\ba\b/);
1039
- } finally {
1040
- fs.rmSync(dir, { recursive: true, force: true });
1041
- }
1042
- });
1043
-
1044
824
  test('23. malformed cache JSON does not bypass validation', () => {
1045
825
  // valid receipt with malformed cache → must still PASS (fail-open on cache parse, not on receipt)
1046
826
  const dirValid = makeFixture({ evidence: 'valid' });
@@ -1093,9 +873,10 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1093
873
  for (const { evidence, check, desc } of cases) {
1094
874
  const dir = tmpDir();
1095
875
  const featureDir = path.join(dir, 'specs', FEATURE);
876
+ fs.mkdirSync(featureDir, { recursive: true });
877
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1096
878
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1097
- 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' } } }));
1098
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
879
+ 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'));
1099
880
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1100
881
  const body = parseBlock(runHook({}, dir).stdout);
1101
882
  assert.ok(body, `placeholder case ${desc} should block`);
@@ -1107,8 +888,8 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1107
888
  const dir2 = tmpDir();
1108
889
  const featureDir2 = path.join(dir2, 'specs', FEATURE);
1109
890
  fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
1110
- 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' } } }));
1111
- 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')));
891
+ fs.writeFileSync(path.join(featureDir2, 'plan.md'), '# Demo plan\n');
892
+ 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')));
1112
893
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1113
894
  assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
1114
895
  fs.rmSync(dir2, { recursive: true, force: true });
@@ -1123,9 +904,10 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1123
904
  for (const { evidence, desc } of cases) {
1124
905
  const dir = tmpDir();
1125
906
  const featureDir = path.join(dir, 'specs', FEATURE);
907
+ fs.mkdirSync(featureDir, { recursive: true });
908
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1126
909
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1127
- 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' } } }));
1128
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
910
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: pnpm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1129
911
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1130
912
  const body = parseBlock(runHook({}, dir).stdout);
1131
913
  assert.ok(body, `explicit failure ${desc} should block`);
@@ -1137,8 +919,9 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1137
919
  test('27. task markdown symlink outside feature must be rejected (check a)', () => {
1138
920
  const dir = tmpDir();
1139
921
  const featureDir = path.join(dir, 'specs', FEATURE);
922
+ fs.mkdirSync(featureDir, { recursive: true });
923
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1140
924
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1141
- 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' } } }));
1142
925
  const outside = path.join(dir, 'outside.md');
1143
926
  fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1144
927
  const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
@@ -1148,23 +931,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
1148
931
  const body = parseBlock(runHook({}, dir).stdout);
1149
932
  assert.ok(body, 'symlink task outside should block');
1150
933
  assert.strictEqual(body.decision, 'block');
1151
- assert.match(body.reason, /\ba\b/);
1152
- fs.rmSync(dir, { recursive: true, force: true });
1153
- });
1154
-
1155
- test('28. malformed spec.json must block with invalid_specs', () => {
1156
- const dir = tmpDir();
1157
- fs.mkdirSync(path.join(dir, 'specs', 'good'), { recursive: true });
1158
- fs.mkdirSync(path.join(dir, 'specs', 'bad'), { recursive: true });
1159
- fs.writeFileSync(path.join(dir, 'specs', 'good', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'good', task_registry: {} }));
1160
- fs.writeFileSync(path.join(dir, 'specs', 'bad', 'spec.json'), '{ malformed');
1161
- seedCache({});
1162
- const { stdout } = runHook({}, dir);
1163
- const body = parseBlock(stdout);
1164
- assert.ok(body, 'malformed spec should block');
1165
- assert.strictEqual(body.decision, 'block');
1166
- assert.match(body.reason, /invalid spec/i);
1167
- assert.match(body.reason, /bad/);
934
+ assert.match(body.reason, /invalid workflow packet/);
1168
935
  fs.rmSync(dir, { recursive: true, force: true });
1169
936
  });
1170
937
 
@@ -1174,7 +941,6 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1174
941
  fs.mkdirSync(specsDir, { recursive: true });
1175
942
  const outside = path.join(dir, 'outside-feat');
1176
943
  fs.mkdirSync(outside, { recursive: true });
1177
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1178
944
  const link = path.join(specsDir, 'linked');
1179
945
  fs.symlinkSync(outside, link);
1180
946
  // non-explicit should block with invalid_specs (fail-closed, not phantom no-active)
@@ -1182,7 +948,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1182
948
  const nonExplicit = parseBlock(runHook({}, dir).stdout);
1183
949
  assert.ok(nonExplicit, 'non-explicit symlink outside should block with invalid_specs');
1184
950
  assert.strictEqual(nonExplicit.decision, 'block');
1185
- assert.match(nonExplicit.reason, /invalid spec/i);
951
+ assert.match(nonExplicit.reason, /invalid workflow packet/i);
1186
952
  assert.match(nonExplicit.reason, /linked/);
1187
953
  // explicit should block explicit_malformed
1188
954
  const body = parseBlock(runHook({ featureName: 'linked' }, dir).stdout);
@@ -1224,15 +990,11 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1224
990
  for (const v of vectors) {
1225
991
  const dir = tmpDir();
1226
992
  const featureDir = path.join(dir, 'specs', FEATURE);
993
+ fs.mkdirSync(featureDir, { recursive: true });
994
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1227
995
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1228
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1229
- status: 'in_progress',
1230
- feature_name: FEATURE,
1231
- current_phase: 'closeout',
1232
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1233
- }));
1234
996
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
1235
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
997
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1236
998
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1237
999
  const body = parseBlock(runHook({}, dir).stdout);
1238
1000
  assert.ok(body, `vector should block: ${v}`);
@@ -1271,14 +1033,9 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1271
1033
  const dir = tmpDir();
1272
1034
  const featureDir = path.join(dir, 'specs', FEATURE);
1273
1035
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1274
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1275
- status: 'in_progress',
1276
- feature_name: FEATURE,
1277
- current_phase: 'closeout',
1278
- task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1279
- }));
1036
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1280
1037
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
1281
- fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
1038
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
1282
1039
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1283
1040
  const { stdout } = runHook({}, dir);
1284
1041
  assert.strictEqual(stdout, '', `positive control should not block: ${v}`);
@@ -1510,11 +1267,14 @@ function boundReceipt(base = VALID_BASE, head = VALID_HEAD) {
1510
1267
  }
1511
1268
 
1512
1269
  function runHookFrom(dir, cwd) {
1270
+ const env = { ...process.env };
1271
+ delete env.CLAUDE_PROJECT_DIR;
1272
+ delete env.PROJECT_ROOT;
1513
1273
  const res = spawnSync(process.execPath, [HOOK], {
1514
1274
  cwd,
1515
1275
  input: JSON.stringify({ hook_event_name: 'Stop', session_id: 'test', transcript_path: '/tmp/t', stop_hook_active: false, cwd: dir }),
1516
1276
  encoding: 'utf8',
1517
- env: { ...process.env, PROJECT_ROOT: dir },
1277
+ env,
1518
1278
  });
1519
1279
  return { code: res.status, stdout: (res.stdout || '').trim(), stderr: res.stderr || '' };
1520
1280
  }
@@ -1617,17 +1377,142 @@ test('41. unborn HEAD fails closed before any receipt check', () => {
1617
1377
  clearCache();
1618
1378
  const body = parseBlock(runHook({}, dir).stdout);
1619
1379
  assert.ok(body && body.decision === 'block', 'an unborn HEAD must block');
1620
- // The block arrives from provenance derivation, which cannot resolve a base
1621
- // commit here, so the receipt check is never reached. Asserting the reason
1622
- // keeps this case honest: it guards fail-closed behavior on an unborn HEAD,
1623
- // not the binding-mode selector. The selector's own HEAD-unreadable branch
1624
- // is guarded by cases 38 and 39.
1625
- assert.match(body.reason, /controlled failure/);
1380
+ // A done task still blocks before receipt validation, with guidance to
1381
+ // create the first commit before recording its Base/Head.
1382
+ assert.match(body.reason, /first commit/);
1626
1383
  } finally {
1627
1384
  fs.rmSync(dir, { recursive: true, force: true });
1628
1385
  }
1629
1386
  });
1630
1387
 
1388
+ function noHistoryFixture() {
1389
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-spec-no-history-'));
1390
+ for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test']]) {
1391
+ const result = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' });
1392
+ assert.strictEqual(result.status, 0, result.stderr);
1393
+ }
1394
+ installClaudeGate(dir);
1395
+ fs.copyFileSync(path.join(__dirname, '..', 'spec-state.cjs'), path.join(dir, '.claude/hooks/spec-state.cjs'));
1396
+ return dir;
1397
+ }
1398
+
1399
+ test('Stop still blocks multiple active specs for an untouched session', () => {
1400
+ const dir = tmpDir();
1401
+ try {
1402
+ const gate = installClaudeGate(dir);
1403
+ for (const feature of ['demo', 'other']) writeNoHistoryPacket(dir, feature, 'in_progress');
1404
+ const result = spawnSync(process.execPath, [gate], {
1405
+ cwd: dir, encoding: 'utf8', env: { ...process.env, PROJECT_ROOT: dir, CLAUDE_PROJECT_DIR: dir },
1406
+ input: JSON.stringify({ cwd: dir, session_id: 'untouched-session', hook_event_name: 'Stop', prompt: 'Continue' }),
1407
+ });
1408
+ assert.strictEqual(result.status, 0, result.stderr);
1409
+ const body = parseBlock(result.stdout);
1410
+ assert.strictEqual(body.decision, 'block');
1411
+ assert.match(body.reason, /multiple active specs/);
1412
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1413
+ });
1414
+
1415
+ function writeNoHistoryPacket(dir, feature, status) {
1416
+ const packet = path.join(dir, 'specs', feature);
1417
+ fs.mkdirSync(packet, { recursive: true });
1418
+ fs.writeFileSync(path.join(packet, 'plan.md'), `# ${feature}\nSpecs-Contract: process-first-ready-v1\n`);
1419
+ fs.writeFileSync(path.join(packet, 'task-01-work.md'), [
1420
+ '# Task 01: work', '', `Status: ${status}`, '', '## Dependencies', '', '- none', '',
1421
+ '## Verification Plan', '', '- Command: node --test', '',
1422
+ ...(status === 'done' ? ['## Receipt', '', 'Verification: PASS', ''] : []),
1423
+ ].join('\n'));
1424
+ }
1425
+
1426
+ function runInstalledHistoryHook(dir, hook = 'spec-gate', extraEnv = {}) {
1427
+ return spawnSync(process.execPath, [path.join(dir, '.claude/hooks', `${hook}.cjs`)], {
1428
+ cwd: dir, encoding: 'utf8',
1429
+ env: { ...process.env, PROJECT_ROOT: dir, CLAUDE_PROJECT_DIR: dir, ...extraEnv },
1430
+ input: JSON.stringify({ cwd: dir, session_id: 'no-history',
1431
+ hook_event_name: hook === 'spec-state' ? 'UserPromptSubmit' : 'Stop', prompt: 'Continue specs/demo' }),
1432
+ });
1433
+ }
1434
+
1435
+ test('no commits allows Stop without done tasks and prints one reminder', () => {
1436
+ const dir = noHistoryFixture();
1437
+ try {
1438
+ writeNoHistoryPacket(dir, 'demo', 'in_progress');
1439
+ const gate = runInstalledHistoryHook(dir);
1440
+ assert.strictEqual(gate.status, 0, gate.stderr);
1441
+ assert.strictEqual(gate.stdout.trim(), '');
1442
+ const state = runInstalledHistoryHook(dir, 'spec-state');
1443
+ assert.strictEqual(state.status, 0, state.stderr);
1444
+ assert.match(state.stdout, /no commits/);
1445
+ assert.strictEqual(state.stdout.trim().split('\n').length, 1);
1446
+ assert.doesNotMatch(state.stdout, /Spec tollgate unavailable/);
1447
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1448
+ });
1449
+
1450
+ test('no commits blocks a done task with first commit guidance', () => {
1451
+ const dir = noHistoryFixture();
1452
+ try {
1453
+ writeNoHistoryPacket(dir, 'demo', 'done');
1454
+ const result = runInstalledHistoryHook(dir);
1455
+ assert.strictEqual(result.status, 0, result.stderr);
1456
+ const body = parseBlock(result.stdout);
1457
+ assert.strictEqual(body.decision, 'block');
1458
+ assert.match(body.reason, /first commit/);
1459
+ assert.doesNotMatch(body.reason, /controlled failure/);
1460
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1461
+ });
1462
+
1463
+ test('no commits blocks two completed packets with first commit guidance', () => {
1464
+ const dir = noHistoryFixture();
1465
+ try {
1466
+ for (const feature of ['demo', 'other']) writeNoHistoryPacket(dir, feature, 'done');
1467
+ const result = runInstalledHistoryHook(dir);
1468
+ assert.strictEqual(result.status, 0, result.stderr);
1469
+ const body = parseBlock(result.stdout);
1470
+ assert.strictEqual(body.decision, 'block');
1471
+ assert.match(body.reason, /first commit/);
1472
+ assert.doesNotMatch(body.reason, /controlled failure/);
1473
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1474
+ });
1475
+
1476
+ test('a corrupt current branch ref remains a controlled failure', () => {
1477
+ const dir = tmpDir();
1478
+ try {
1479
+ installClaudeGate(dir);
1480
+ writeNoHistoryPacket(dir, 'demo', 'in_progress');
1481
+ const ref = spawnSync('git', ['-C', dir, 'symbolic-ref', '-q', 'HEAD'], { encoding: 'utf8' });
1482
+ assert.strictEqual(ref.status, 0, ref.stderr);
1483
+ fs.writeFileSync(path.join(dir, '.git', ref.stdout.trim()), 'corrupt-ref\n');
1484
+ const result = runInstalledHistoryHook(dir);
1485
+ assert.strictEqual(result.status, 0, result.stderr);
1486
+ const body = parseBlock(result.stdout);
1487
+ assert.strictEqual(body.decision, 'block');
1488
+ assert.match(body.reason, /controlled failure/);
1489
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1490
+ });
1491
+
1492
+ test('unavailable git remains a controlled failure in a repo without commits', () => {
1493
+ const dir = noHistoryFixture();
1494
+ try {
1495
+ writeNoHistoryPacket(dir, 'demo', 'in_progress');
1496
+ const emptyPath = path.join(dir, 'empty-path');
1497
+ fs.mkdirSync(emptyPath);
1498
+ const result = runInstalledHistoryHook(dir, 'spec-gate', { PATH: emptyPath });
1499
+ assert.strictEqual(result.status, 0, result.stderr);
1500
+ const body = parseBlock(result.stdout);
1501
+ assert.strictEqual(body.decision, 'block');
1502
+ assert.match(body.reason, /controlled failure/);
1503
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1504
+ });
1505
+
1506
+ test('a committed repo still accepts a valid done task receipt', () => {
1507
+ const dir = makeFixture({ evidence: 'valid' });
1508
+ try {
1509
+ installClaudeGate(dir);
1510
+ const result = runInstalledHistoryHook(dir);
1511
+ assert.strictEqual(result.status, 0, result.stderr);
1512
+ assert.strictEqual(result.stdout.trim(), '');
1513
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
1514
+ });
1515
+
1631
1516
  test('42. a dirty tree outside specs does not reopen a committed receipt', () => {
1632
1517
  // Contract change. This used to require a block. `Base` is the last commit
1633
1518
  // touching anything outside the specs root, so a receipt written before any
@@ -2026,3 +1911,89 @@ test('53. an indented or sub-heading Command never becomes the canonical one', (
2026
1911
  }
2027
1912
  }
2028
1913
  });
1914
+
1915
+ function plantLegacyPacket(dir, name = 'old') {
1916
+ const legacyDir = path.join(dir, 'specs', name);
1917
+ fs.mkdirSync(path.join(legacyDir, 'tasks'), { recursive: true });
1918
+ fs.writeFileSync(path.join(legacyDir, 'spec.json'), JSON.stringify({
1919
+ feature_name: name,
1920
+ status: 'in_progress',
1921
+ task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
1922
+ }));
1923
+ fs.writeFileSync(path.join(legacyDir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
1924
+ }
1925
+
1926
+ function writeActiveFeature(dir, featureName) {
1927
+ fs.mkdirSync(path.join(dir, 'specs', '_shared'), { recursive: true });
1928
+ fs.writeFileSync(path.join(dir, 'specs', '_shared', 'active-feature.json'), JSON.stringify({ featureName }));
1929
+ }
1930
+
1931
+ test('a leftover legacy packet alone never blocks Stop', () => {
1932
+ const dir = tmpDir();
1933
+ try {
1934
+ plantLegacyPacket(dir);
1935
+ clearCache();
1936
+ assert.strictEqual(runHook({}, dir).stdout, '');
1937
+ } finally {
1938
+ fs.rmSync(dir, { recursive: true, force: true });
1939
+ }
1940
+ });
1941
+
1942
+ test('a recorded active feature naming a legacy packet does not block Stop', () => {
1943
+ const dir = makeFixture({ evidence: 'valid' });
1944
+ try {
1945
+ plantLegacyPacket(dir);
1946
+ writeActiveFeature(dir, 'old');
1947
+ clearCache();
1948
+ assert.strictEqual(runHook({}, dir).stdout, '');
1949
+ } finally {
1950
+ fs.rmSync(dir, { recursive: true, force: true });
1951
+ }
1952
+ });
1953
+
1954
+ test('a recorded active feature naming a missing packet still blocks as explicit_not_found', () => {
1955
+ const dir = makeFixture({ evidence: 'valid' });
1956
+ try {
1957
+ writeActiveFeature(dir, 'nope');
1958
+ clearCache();
1959
+ const body = parseBlock(runHook({}, dir).stdout);
1960
+ assert.strictEqual(body?.decision, 'block');
1961
+ assert.match(body.reason, /explicit_not_found/);
1962
+ } finally {
1963
+ fs.rmSync(dir, { recursive: true, force: true });
1964
+ }
1965
+ });
1966
+
1967
+ test('a process-first packet with a dependency cycle and a done task blocks Stop', () => {
1968
+ const dir = tmpDir();
1969
+ try {
1970
+ const featureDir = path.join(dir, 'specs', FEATURE);
1971
+ fs.mkdirSync(featureDir, { recursive: true });
1972
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
1973
+ const task = (status, dependency) => [
1974
+ '# Task', '', `Status: ${status}`, '', '## Dependencies', '', `- ${dependency}`, '',
1975
+ '## Verification Plan', '', '- Command: npm test', '',
1976
+ ].join('\n');
1977
+ fs.writeFileSync(path.join(featureDir, 'task-01-a.md'), task('done', 'task-02-b.md'));
1978
+ fs.writeFileSync(path.join(featureDir, 'task-02-b.md'), task('pending', 'task-01-a.md'));
1979
+ clearCache();
1980
+ const body = parseBlock(runHook({}, dir).stdout);
1981
+ assert.strictEqual(body?.decision, 'block');
1982
+ assert.match(body.reason, /invalid workflow packet/);
1983
+ } finally {
1984
+ fs.rmSync(dir, { recursive: true, force: true });
1985
+ }
1986
+ });
1987
+
1988
+ test('a packet holding both plan.md and spec.json is read as process-first and its unreceipted done task blocks', () => {
1989
+ const dir = makeFixture({ evidence: 'missing-evidence' });
1990
+ try {
1991
+ fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'spec.json'), JSON.stringify({ feature_name: FEATURE }));
1992
+ clearCache();
1993
+ const body = parseBlock(runHook({}, dir).stdout);
1994
+ assert.strictEqual(body?.decision, 'block');
1995
+ assert.match(body.reason, new RegExp(TASK_REL));
1996
+ } finally {
1997
+ fs.rmSync(dir, { recursive: true, force: true });
1998
+ }
1999
+ });