@haposoft/cafekit 0.18.0 → 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 (83) hide show
  1. package/README.md +1 -1
  2. package/bin/__tests__/codex-hooks.test.js +169 -236
  3. package/bin/__tests__/codex-native.test.js +54 -114
  4. package/bin/__tests__/develop-contract.test.js +101 -1361
  5. package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
  6. package/bin/__tests__/package-inventory.test.js +1 -395
  7. package/bin/__tests__/spec-narrowing.test.js +30 -134
  8. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
  9. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  10. package/bin/__tests__/validator-grounding.test.js +17 -2929
  11. package/bin/lib/managed-writer.js +2 -3
  12. package/bin/phases/codex-runtime.js +6 -3
  13. package/bin/phases/copy-payload.js +13 -28
  14. package/bin/phases/omp-runtime.js +6 -3
  15. package/package.json +2 -3
  16. package/src/claude/agents/code-auditor.md +9 -6
  17. package/src/claude/agents/debugger.md +3 -9
  18. package/src/claude/agents/docs-keeper.md +4 -7
  19. package/src/claude/agents/git-ops.md +2 -4
  20. package/src/claude/agents/implementer.md +6 -7
  21. package/src/claude/agents/inspector.md +2 -0
  22. package/src/claude/agents/project-manager.md +2 -2
  23. package/src/claude/agents/spec-maker.md +4 -4
  24. package/src/claude/agents/test-runner.md +8 -11
  25. package/src/claude/agents/ui-ux-designer.md +0 -1
  26. package/src/claude/hooks/__tests__/spec-gate.test.js +170 -330
  27. package/src/claude/hooks/__tests__/spec-state-touched.test.js +26 -0
  28. package/src/claude/hooks/spec-gate.cjs +8 -63
  29. package/src/claude/hooks/spec-state.cjs +47 -38
  30. package/src/claude/migration-manifest.json +12 -7
  31. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  32. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  33. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  34. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  35. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  36. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  37. package/src/claude/rules/ai-dev-rules.md +1 -2
  38. package/src/claude/rules/workflow.md +1 -1
  39. package/src/claude/scripts/spec-receipt.cjs +0 -114
  40. package/src/claude/scripts/spec-resolver.cjs +41 -457
  41. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  42. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  43. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  44. package/src/claude/skills/code-review/SKILL.md +25 -108
  45. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
  46. package/src/claude/skills/develop/SKILL.md +22 -70
  47. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  48. package/src/claude/skills/research/SKILL.md +24 -55
  49. package/src/claude/skills/sync/SKILL.md +12 -34
  50. package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
  51. package/src/claude/skills/test/SKILL.md +39 -123
  52. package/src/claude/skills/test/references/execution-strategy.md +11 -22
  53. package/src/claude/skills/ui-ux/SKILL.md +1 -1
  54. package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
  55. package/src/claude/skills/web-testing/SKILL.md +2 -20
  56. package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
  57. package/src/codex/hooks/lib/spec-utils.cjs +6 -45
  58. package/src/codex/hooks/spec-gate.cjs +11 -68
  59. package/src/codex/hooks/spec-state.cjs +37 -41
  60. package/src/common/AGENTS.md +4 -10
  61. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  62. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  63. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  64. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  65. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  66. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  67. package/src/claude/hooks/lib/context.cjs +0 -527
  68. package/src/claude/hooks/lib/detect.cjs +0 -474
  69. package/src/claude/scripts/change-firewall.cjs +0 -1748
  70. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  71. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  72. package/src/claude/scripts/spec-final-state.cjs +0 -290
  73. package/src/claude/scripts/spec-ground.cjs +0 -650
  74. package/src/claude/scripts/spec-readiness.cjs +0 -425
  75. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  76. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  77. package/src/claude/scripts/validate-spec-output.cjs +0 -3914
  78. package/src/claude/skills/code-review/references/verification-gate.md +0 -41
  79. package/src/claude/skills/specs/templates/design.md +0 -82
  80. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  81. package/src/claude/skills/specs/templates/requirements.md +0 -42
  82. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  83. package/src/claude/skills/specs/templates/task.md +0 -47
@@ -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',
246
185
  '',
247
- `**Status:** ${mdStatus}`,
186
+ `Status: ${taskStatus}`,
248
187
  '',
249
- evidenceBlock,
188
+ '## Dependencies',
189
+ '',
190
+ '- none',
191
+ '',
192
+ '## Verification Plan',
193
+ '',
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,7 +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 ['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']) {
299
244
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
300
245
  }
301
246
  if (policyMode === 'valid') {
@@ -401,31 +346,21 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
401
346
  const dir = tmpDir();
402
347
  const gate = installClaudeGate(dir);
403
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');
404
351
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
405
352
  fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
406
353
  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
354
  fs.writeFileSync(path.join(featureDir, TASK_REL), [
420
- '# Task', '', '**Status:** done', '', '## Evidence', '',
355
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
421
356
  'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
422
357
  'Artifact: output/bundle.js', suffix,
358
+ '```', 'node --test', 'ok 1', '```',
423
359
  ].join('\n'));
424
360
  fs.writeFileSync(
425
361
  path.join(featureDir, TASK_REL),
426
362
  bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
427
363
  );
428
- installFeatureReceipt(dir);
429
364
  try {
430
365
  const result = spawnSync(process.execPath, [gate], {
431
366
  cwd: dir,
@@ -455,6 +390,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
455
390
  for (const { artifactPath, symlink } of cases) {
456
391
  const dir = tmpDir();
457
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');
458
395
  const artifactBytes = Buffer.from(`artifact-${artifactPath}\n`);
459
396
  const digest = crypto.createHash('sha256').update(artifactBytes).digest('hex');
460
397
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
@@ -466,20 +403,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
466
403
  } else {
467
404
  fs.writeFileSync(path.join(dir, 'outside.js'), artifactBytes);
468
405
  }
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
406
  fs.writeFileSync(path.join(featureDir, TASK_REL), bindFixtureReceipt(dir, [
482
- '# Task', '', '**Status:** done', '', '## Evidence', '',
407
+ '# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
483
408
  'Verification: PASS', 'Command: node --test', 'Exit: 0',
484
409
  `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
485
410
  `Artifact: ${artifactPath} (sha256:${digest})`,
@@ -625,27 +550,6 @@ test('empty or malformed Claude hook payload fails closed', () => {
625
550
  }
626
551
  });
627
552
 
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
553
  test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
650
554
  const dir = makeFixture({ evidence: 'placeholder' });
651
555
  try {
@@ -686,70 +590,20 @@ test('9. a code fence alone is not verification proof', () => {
686
590
  }
687
591
  });
688
592
 
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
593
  test('13. provenance requires both Base and Head - only Base fails', () => {
736
594
  const dir = tmpDir();
737
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');
738
598
  const tasksDir = path.join(featureDir, 'tasks');
739
599
  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
600
  // Only Base, missing Head
747
601
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
748
602
  '# Task',
749
603
  '',
750
- '**Status:** done',
604
+ 'Status: done',
751
605
  '',
752
- '## Evidence',
606
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
753
607
  '',
754
608
  'Verification: PASS',
755
609
  'Command: npm test',
@@ -773,21 +627,17 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
773
627
  test('14. provenance requires both Base and Head - only Head fails', () => {
774
628
  const dir = tmpDir();
775
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');
776
632
  const tasksDir = path.join(featureDir, 'tasks');
777
633
  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
634
  // Only Head, missing Base
785
635
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), [
786
636
  '# Task',
787
637
  '',
788
- '**Status:** done',
638
+ 'Status: done',
789
639
  '',
790
- '## Evidence',
640
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
791
641
  '',
792
642
  'Verification: PASS',
793
643
  'Command: npm test',
@@ -811,20 +661,16 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
811
661
  test('15. provenance with both Base and Head passes', () => {
812
662
  const dir = tmpDir();
813
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');
814
666
  const tasksDir = path.join(featureDir, 'tasks');
815
667
  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
668
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
823
669
  '# Task',
824
670
  '',
825
- '**Status:** done',
671
+ 'Status: done',
826
672
  '',
827
- '## Evidence',
673
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
828
674
  '',
829
675
  'Verification: PASS',
830
676
  'Command: npm test',
@@ -844,57 +690,6 @@ test('15. provenance with both Base and Head passes', () => {
844
690
  }
845
691
  });
846
692
 
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
693
  test('17. empty or bare provenance fails — Base:/Head: require non-empty same-line values', () => {
899
694
  const cases = [
900
695
  { evidence: ['Verification: PASS','Command: npm test','Exit: 0','Base:','Head: def456'].join('\n'), desc: 'empty Base:' },
@@ -908,14 +703,10 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
908
703
  for (const { evidence, desc } of cases) {
909
704
  const dir = tmpDir();
910
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');
911
708
  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'));
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'));
919
710
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
920
711
  const body = parseBlock(runHook({}, dir).stdout);
921
712
  assert.ok(body, `empty/bare provenance should block for ${desc}`);
@@ -934,15 +725,17 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
934
725
  for (const { evidence, shouldPass } of validCases) {
935
726
  const dir = tmpDir();
936
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');
937
730
  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'));
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`);
946
739
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
947
740
  const result = runHook({}, dir);
948
741
  if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
@@ -979,8 +772,8 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
979
772
  // mutate: strip Verification: PASS and Command — keep file as done
980
773
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
981
774
  fs.writeFileSync(taskFile, [
982
- '# Task', '', '**Status:** done', '',
983
- '## Evidence', '',
775
+ '# Task', '', 'Status: done', '',
776
+ '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '',
984
777
  'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
985
778
  '```', 'pass', '```',
986
779
  ].join('\n'));
@@ -1004,8 +797,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1004
797
  // mutate: remove Head, leave only Base
1005
798
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
1006
799
  fs.writeFileSync(taskFile, [
1007
- '# Task', '', '**Status:** done', '',
1008
- '## 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}`,
1009
802
  '```', 'pass', '```',
1010
803
  ].join('\n'));
1011
804
  let body = parseBlock(runHook({}, dir).stdout);
@@ -1013,8 +806,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1013
806
  assert.match(body.reason, /\bprovenance\b/);
1014
807
  // mutate back to include both but with empty Head value (stale)
1015
808
  fs.writeFileSync(taskFile, [
1016
- '# Task', '', '**Status:** done', '',
1017
- '## 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:',
1018
811
  '```', 'pass', '```',
1019
812
  ].join('\n'));
1020
813
  body = parseBlock(runHook({}, dir).stdout);
@@ -1025,22 +818,6 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
1025
818
  }
1026
819
  });
1027
820
 
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
821
  test('23. malformed cache JSON does not bypass validation', () => {
1045
822
  // valid receipt with malformed cache → must still PASS (fail-open on cache parse, not on receipt)
1046
823
  const dirValid = makeFixture({ evidence: 'valid' });
@@ -1093,9 +870,10 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1093
870
  for (const { evidence, check, desc } of cases) {
1094
871
  const dir = tmpDir();
1095
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');
1096
875
  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'));
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'));
1099
877
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1100
878
  const body = parseBlock(runHook({}, dir).stdout);
1101
879
  assert.ok(body, `placeholder case ${desc} should block`);
@@ -1107,8 +885,8 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
1107
885
  const dir2 = tmpDir();
1108
886
  const featureDir2 = path.join(dir2, 'specs', FEATURE);
1109
887
  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')));
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')));
1112
890
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1113
891
  assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
1114
892
  fs.rmSync(dir2, { recursive: true, force: true });
@@ -1123,9 +901,10 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1123
901
  for (const { evidence, desc } of cases) {
1124
902
  const dir = tmpDir();
1125
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');
1126
906
  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'));
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'));
1129
908
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1130
909
  const body = parseBlock(runHook({}, dir).stdout);
1131
910
  assert.ok(body, `explicit failure ${desc} should block`);
@@ -1137,8 +916,9 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1137
916
  test('27. task markdown symlink outside feature must be rejected (check a)', () => {
1138
917
  const dir = tmpDir();
1139
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');
1140
921
  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
922
  const outside = path.join(dir, 'outside.md');
1143
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');
1144
924
  const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
@@ -1148,23 +928,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
1148
928
  const body = parseBlock(runHook({}, dir).stdout);
1149
929
  assert.ok(body, 'symlink task outside should block');
1150
930
  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/);
931
+ assert.match(body.reason, /invalid workflow packet/);
1168
932
  fs.rmSync(dir, { recursive: true, force: true });
1169
933
  });
1170
934
 
@@ -1174,7 +938,6 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1174
938
  fs.mkdirSync(specsDir, { recursive: true });
1175
939
  const outside = path.join(dir, 'outside-feat');
1176
940
  fs.mkdirSync(outside, { recursive: true });
1177
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
1178
941
  const link = path.join(specsDir, 'linked');
1179
942
  fs.symlinkSync(outside, link);
1180
943
  // non-explicit should block with invalid_specs (fail-closed, not phantom no-active)
@@ -1182,7 +945,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1182
945
  const nonExplicit = parseBlock(runHook({}, dir).stdout);
1183
946
  assert.ok(nonExplicit, 'non-explicit symlink outside should block with invalid_specs');
1184
947
  assert.strictEqual(nonExplicit.decision, 'block');
1185
- assert.match(nonExplicit.reason, /invalid spec/i);
948
+ assert.match(nonExplicit.reason, /invalid workflow packet/i);
1186
949
  assert.match(nonExplicit.reason, /linked/);
1187
950
  // explicit should block explicit_malformed
1188
951
  const body = parseBlock(runHook({ featureName: 'linked' }, dir).stdout);
@@ -1224,15 +987,11 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1224
987
  for (const v of vectors) {
1225
988
  const dir = tmpDir();
1226
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');
1227
992
  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
993
  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'));
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'));
1236
995
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1237
996
  const body = parseBlock(runHook({}, dir).stdout);
1238
997
  assert.ok(body, `vector should block: ${v}`);
@@ -1271,14 +1030,9 @@ test('30. phantom vectors via gate evidence must block with verification_state (
1271
1030
  const dir = tmpDir();
1272
1031
  const featureDir = path.join(dir, 'specs', FEATURE);
1273
1032
  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
- }));
1033
+ fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
1280
1034
  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'));
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'));
1282
1036
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1283
1037
  const { stdout } = runHook({}, dir);
1284
1038
  assert.strictEqual(stdout, '', `positive control should not block: ${v}`);
@@ -2026,3 +1780,89 @@ test('53. an indented or sub-heading Command never becomes the canonical one', (
2026
1780
  }
2027
1781
  }
2028
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
+ });