@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5

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 (98) hide show
  1. package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
  2. package/bin/__tests__/codex-hooks.test.js +531 -29
  3. package/bin/__tests__/codex-native.test.js +228 -7
  4. package/bin/__tests__/develop-contract.test.js +653 -185
  5. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
  6. package/bin/__tests__/installer-safety.test.js +39 -0
  7. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  8. package/bin/__tests__/package-inventory.test.js +705 -13
  9. package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
  10. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
  11. package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
  12. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
  13. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
  14. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  15. package/bin/__tests__/validator-grounding.test.js +2013 -221
  16. package/bin/lib/managed-writer.js +12 -3
  17. package/bin/phases/codex-runtime.js +17 -1
  18. package/package.json +5 -2
  19. package/src/claude/CLAUDE.md +6 -0
  20. package/src/claude/agents/code-auditor.md +20 -0
  21. package/src/claude/agents/spec-maker.md +143 -99
  22. package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
  23. package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
  24. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  25. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  26. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  27. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  28. package/src/claude/hooks/completion-authority.cjs +13 -7
  29. package/src/claude/hooks/docs-sync.cjs +5 -2
  30. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  31. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  32. package/src/claude/hooks/privacy-block.cjs +99 -28
  33. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  34. package/src/claude/hooks/spec-gate.cjs +47 -117
  35. package/src/claude/hooks/spec-state.cjs +8 -2
  36. package/src/claude/migration-manifest.json +9 -1
  37. package/src/claude/rules/state-sync.md +52 -21
  38. package/src/claude/rules/workflow.md +17 -9
  39. package/src/claude/scripts/change-firewall.cjs +1748 -0
  40. package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
  41. package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
  42. package/src/claude/scripts/spec-final-state.cjs +290 -0
  43. package/src/claude/scripts/spec-ground.cjs +571 -173
  44. package/src/claude/scripts/spec-readiness.cjs +425 -0
  45. package/src/claude/scripts/spec-receipt.cjs +162 -0
  46. package/src/claude/scripts/spec-resolver.cjs +67 -16
  47. package/src/claude/scripts/spec-scaffold.cjs +1594 -139
  48. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  49. package/src/claude/scripts/validate-spec-output.cjs +3110 -218
  50. package/src/claude/scripts/workflow-policy.cjs +501 -183
  51. package/src/claude/settings/settings.json +4 -0
  52. package/src/claude/skills/code-review/SKILL.md +12 -3
  53. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  54. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  55. package/src/claude/skills/develop/SKILL.md +47 -60
  56. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  57. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  58. package/src/claude/skills/specs/SKILL.md +183 -177
  59. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  60. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  61. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  62. package/src/claude/skills/specs/references/review.md +214 -262
  63. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  64. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  65. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  66. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  67. package/src/claude/skills/specs/rules/design-review.md +42 -97
  68. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  69. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  70. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  71. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  72. package/src/claude/skills/specs/templates/design.md +49 -263
  73. package/src/claude/skills/specs/templates/requirements.md +26 -36
  74. package/src/claude/skills/specs/templates/research.md +9 -98
  75. package/src/claude/skills/specs/templates/spec-state.json +35 -85
  76. package/src/claude/skills/specs/templates/task.md +30 -102
  77. package/src/claude/skills/sync/SKILL.md +33 -10
  78. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  79. package/src/claude/skills/test/SKILL.md +17 -7
  80. package/src/codex/AGENTS.md +6 -0
  81. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  82. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  83. package/src/codex/hooks/completion-authority.cjs +13 -7
  84. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  85. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  86. package/src/codex/hooks/lib/privacy-state.cjs +61 -38
  87. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  88. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  89. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  90. package/src/codex/hooks/privacy-approval.cjs +6 -2
  91. package/src/codex/hooks/privacy-block.cjs +94 -15
  92. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  93. package/src/codex/hooks/spec-gate.cjs +49 -34
  94. package/src/codex/hooks/spec-state.cjs +10 -3
  95. package/src/codex/hooks/state.cjs +2 -0
  96. package/src/codex/hooks.json +6 -1
  97. package/src/codex/rules/state-sync.md +47 -27
  98. package/src/common/AGENTS.md +1 -1
@@ -26,6 +26,12 @@ const FIXTURE_ARTIFACT_DIGEST = crypto.createHash('sha256').update(FIXTURE_ARTIF
26
26
 
27
27
  function runHook(payload, cwd) {
28
28
  const projectRoot = cwd || payload.cwd;
29
+ const specsRoot = path.join(projectRoot, 'specs');
30
+ if (fs.existsSync(specsRoot)) {
31
+ for (const featureName of fs.readdirSync(specsRoot)) {
32
+ installFeatureReceipt(projectRoot, featureName, payload.session_id || 'test');
33
+ }
34
+ }
29
35
  // PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
30
36
  // monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
31
37
  const env = { ...process.env, PROJECT_ROOT: projectRoot };
@@ -72,6 +78,38 @@ function bindFixtureReceipt(dir, value) {
72
78
  return value.replaceAll(VALID_BASE, context.base).replaceAll(VALID_HEAD, context.head);
73
79
  }
74
80
 
81
+ function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
82
+ const featureDir = path.join(dir, 'specs', featureName);
83
+ const specFile = path.join(featureDir, 'spec.json');
84
+ if (!fs.existsSync(specFile)) return;
85
+ let spec;
86
+ try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { return; }
87
+ const lifecyclePhase = spec.current_phase || spec.phase;
88
+ const explicitCloseout = ['completed', 'complete'].includes(spec.status)
89
+ || ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
90
+ if (!explicitCloseout) return;
91
+ const tasks = Object.values(spec.task_registry || {});
92
+ if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) return;
93
+ const context = PROVENANCE.deriveRuntimeContext({
94
+ projectRoot: dir,
95
+ specsRoot: path.join(dir, 'specs'),
96
+ specFile,
97
+ featureName,
98
+ runtimeSession: session,
99
+ });
100
+ fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
101
+ `Feature: ${featureName}`,
102
+ 'Expected: final integration verification passes',
103
+ 'Observed: final integration verification passed',
104
+ 'Verification: PASS',
105
+ 'Command: node --test',
106
+ 'Exit: 0',
107
+ `Base: ${context.base}`,
108
+ `Head: ${context.head}`,
109
+ '',
110
+ ].join('\n'));
111
+ }
112
+
75
113
  function clearCache() {
76
114
  try { fs.unlinkSync(CACHE); } catch { /* absent */ }
77
115
  }
@@ -94,6 +132,7 @@ function readCache() {
94
132
  * @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
95
133
  * @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
96
134
  * @param {object|null} [opts.workflowPolicy] — if set, persist completion obligations
135
+ * @param {string} [opts.phase='closeout'] — persisted lifecycle boundary
97
136
  */
98
137
  function makeFixture(opts = {}) {
99
138
  const {
@@ -103,6 +142,7 @@ function makeFixture(opts = {}) {
103
142
  evidence = 'valid',
104
143
  runtime = null,
105
144
  workflowPolicy = null,
145
+ phase = 'closeout',
106
146
  } = opts;
107
147
  const dir = tmpDir();
108
148
  const featureDir = path.join(dir, 'specs', FEATURE);
@@ -115,7 +155,7 @@ function makeFixture(opts = {}) {
115
155
  const spec = {
116
156
  status: 'in_progress',
117
157
  feature_name: FEATURE,
118
- current_phase: 'implementation',
158
+ current_phase: phase,
119
159
  task_registry: { [TASK_REL]: entry },
120
160
  };
121
161
  if (workflowPolicy) spec.workflow_policy = workflowPolicy;
@@ -208,7 +248,12 @@ function installClaudeGate(root, policyMode = 'valid') {
208
248
  fs.mkdirSync(hooks, { recursive: true });
209
249
  fs.mkdirSync(scripts, { recursive: true });
210
250
  fs.copyFileSync(HOOK, path.join(hooks, 'spec-gate.cjs'));
211
- fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', 'spec-resolver.cjs'), path.join(scripts, 'spec-resolver.cjs'));
251
+ for (const name of [
252
+ 'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
253
+ ]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
254
+ 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']) {
255
+ fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
256
+ }
212
257
  if (policyMode === 'valid') {
213
258
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'), path.join(scripts, 'workflow-policy.cjs'));
214
259
  fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'), path.join(scripts, 'provenance.cjs'));
@@ -317,6 +362,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
317
362
  fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
318
363
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
319
364
  status: 'in_progress',
365
+ current_phase: 'closeout',
320
366
  feature_name: FEATURE,
321
367
  task_registry: {
322
368
  [TASK_REL]: {
@@ -335,6 +381,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
335
381
  path.join(featureDir, TASK_REL),
336
382
  bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
337
383
  );
384
+ installFeatureReceipt(dir);
338
385
  try {
339
386
  const result = spawnSync(process.execPath, [gate], {
340
387
  cwd: dir,
@@ -346,7 +393,7 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
346
393
  if (expected) {
347
394
  const block = parseBlock(result.stdout);
348
395
  assert.equal(block?.decision, 'block', `${label} must block`);
349
- assert.match(block.reason, /\bh\b/);
396
+ assert.match(block.reason, /\bartifact_hash\b/);
350
397
  } else {
351
398
  assert.equal(result.stdout, '', `${label} must pass`);
352
399
  }
@@ -377,6 +424,7 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
377
424
  }
378
425
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
379
426
  status: 'in_progress',
427
+ current_phase: 'closeout',
380
428
  feature_name: FEATURE,
381
429
  task_registry: {
382
430
  [TASK_REL]: {
@@ -395,7 +443,7 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
395
443
  try {
396
444
  const body = parseBlock(runHook({}, dir).stdout);
397
445
  assert.equal(body?.decision, 'block', `${artifactPath} must block`);
398
- assert.match(body.reason, /\bh\b/);
446
+ assert.match(body.reason, /\bartifact_hash\b/);
399
447
  } finally {
400
448
  fs.rmSync(dir, { recursive: true, force: true });
401
449
  }
@@ -442,6 +490,32 @@ test('4. stop_hook_active: true → exit 0 silent even with violating task', ()
442
490
  }
443
491
  });
444
492
 
493
+ test('4b. done task before explicit closeout without receipt blocks', () => {
494
+ const dir = makeFixture({ evidence: 'missing-evidence', phase: 'implementation' });
495
+ try {
496
+ const { code, stdout } = runHook({}, dir);
497
+ assert.strictEqual(code, 0);
498
+ const body = parseBlock(stdout);
499
+ assert.strictEqual(body?.decision, 'block');
500
+ assert.match(body.reason, new RegExp(TASK_REL));
501
+ assert.strictEqual(fs.existsSync(path.join(dir, 'specs', FEATURE, 'feature-receipt.md')), false);
502
+ } finally {
503
+ fs.rmSync(dir, { recursive: true, force: true });
504
+ }
505
+ });
506
+
507
+ test('4c. done task before explicit closeout with valid task receipt is silent', () => {
508
+ const dir = makeFixture({ evidence: 'valid', phase: 'implementation' });
509
+ try {
510
+ const { code, stdout } = runHook({}, dir);
511
+ assert.strictEqual(code, 0);
512
+ assert.strictEqual(stdout, '');
513
+ assert.strictEqual(fs.existsSync(path.join(dir, 'specs', FEATURE, 'feature-receipt.md')), false);
514
+ } finally {
515
+ fs.rmSync(dir, { recursive: true, force: true });
516
+ }
517
+ });
518
+
445
519
  test('5. first run (no cache) with done-without-receipt → block (cache is not truth)', () => {
446
520
  const dir = makeFixture({ evidence: 'missing-evidence' });
447
521
  try {
@@ -507,9 +581,9 @@ test('empty or malformed Claude hook payload fails closed', () => {
507
581
  }
508
582
  });
509
583
 
510
- test('Critical completion ignores worker-writable proof strings and remains blocked', () => {
584
+ test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
511
585
  const dir = makeFixture({
512
- workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true } }),
586
+ workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
513
587
  });
514
588
  try {
515
589
  const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
@@ -528,7 +602,7 @@ test('Critical completion ignores worker-writable proof strings and remains bloc
528
602
  }
529
603
  });
530
604
 
531
- test('7. Evidence with {{...}} placeholder → blocked (check c)', () => {
605
+ test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
532
606
  const dir = makeFixture({ evidence: 'placeholder' });
533
607
  try {
534
608
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
@@ -538,8 +612,8 @@ test('7. Evidence with {{...}} placeholder → blocked (check c)', () => {
538
612
  assert.ok(body, 'expected block JSON on stdout');
539
613
  assert.strictEqual(body.decision, 'block');
540
614
  assert.ok(
541
- body.reason.includes(TASK_REL) && /\bc\b/.test(body.reason),
542
- `reason must cite task path and check c; got: ${body.reason}`,
615
+ body.reason.includes(TASK_REL) && /\bplaceholder\b/.test(body.reason),
616
+ `reason must cite task path and placeholder; got: ${body.reason}`,
543
617
  );
544
618
  } finally {
545
619
  fs.rmSync(dir, { recursive: true, force: true });
@@ -552,7 +626,7 @@ test('8. explicit failure cannot pass even when PASS text is present', () => {
552
626
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
553
627
  const body = parseBlock(runHook({}, dir).stdout);
554
628
  assert.ok(body);
555
- assert.match(body.reason, /\bc\b/);
629
+ assert.match(body.reason, /\bverification_state\b/);
556
630
  } finally {
557
631
  fs.rmSync(dir, { recursive: true, force: true });
558
632
  }
@@ -574,14 +648,14 @@ test('10. completed_at must be a valid ISO timestamp', () => {
574
648
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
575
649
  const body = parseBlock(runHook({}, dir).stdout);
576
650
  assert.ok(body);
577
- assert.match(body.reason, /\bd\b/);
651
+ assert.match(body.reason, /\bcompleted_at\b/);
578
652
  } finally {
579
653
  fs.rmSync(dir, { recursive: true, force: true });
580
654
  }
581
655
  });
582
656
 
583
- test('11. done → pending cache transition is persisted and can be gated again', () => {
584
- const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending' });
657
+ test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
658
+ const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
585
659
  try {
586
660
  seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
587
661
  assert.strictEqual(runHook({}, dir).stdout, '');
@@ -593,6 +667,7 @@ test('11. done → pending cache transition is persisted and can be gated again'
593
667
  status: 'done',
594
668
  completed_at: '2026-07-01T00:00:00Z',
595
669
  };
670
+ spec.current_phase = 'closeout';
596
671
  fs.writeFileSync(specFile, JSON.stringify(spec));
597
672
  const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
598
673
  fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
@@ -621,7 +696,7 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
621
696
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
622
697
  status: 'in_progress',
623
698
  feature_name: FEATURE,
624
- current_phase: 'implementation',
699
+ current_phase: 'closeout',
625
700
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
626
701
  }));
627
702
  // Only Base, missing Head
@@ -645,7 +720,7 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
645
720
  const body = parseBlock(runHook({}, dir).stdout);
646
721
  assert.ok(body, 'only Base should block');
647
722
  assert.strictEqual(body.decision, 'block');
648
- assert.match(body.reason, /\bg\b/);
723
+ assert.match(body.reason, /\bprovenance\b/);
649
724
  } finally {
650
725
  fs.rmSync(dir, { recursive: true, force: true });
651
726
  }
@@ -659,7 +734,7 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
659
734
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
660
735
  status: 'in_progress',
661
736
  feature_name: FEATURE,
662
- current_phase: 'implementation',
737
+ current_phase: 'closeout',
663
738
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
664
739
  }));
665
740
  // Only Head, missing Base
@@ -683,7 +758,7 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
683
758
  const body = parseBlock(runHook({}, dir).stdout);
684
759
  assert.ok(body, 'only Head should block');
685
760
  assert.strictEqual(body.decision, 'block');
686
- assert.match(body.reason, /\bg\b/);
761
+ assert.match(body.reason, /\bprovenance\b/);
687
762
  } finally {
688
763
  fs.rmSync(dir, { recursive: true, force: true });
689
764
  }
@@ -697,7 +772,7 @@ test('15. provenance with both Base and Head passes', () => {
697
772
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
698
773
  status: 'in_progress',
699
774
  feature_name: FEATURE,
700
- current_phase: 'implementation',
775
+ current_phase: 'closeout',
701
776
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
702
777
  }));
703
778
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
@@ -755,7 +830,7 @@ test('16. registry task path with ../ escape or sibling-prefix is rejected (chec
755
830
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
756
831
  status: 'in_progress',
757
832
  feature_name: FEATURE,
758
- current_phase: 'implementation',
833
+ current_phase: 'closeout',
759
834
  task_registry: {
760
835
  [maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
761
836
  [siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
@@ -793,7 +868,7 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
793
868
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
794
869
  status: 'in_progress',
795
870
  feature_name: FEATURE,
796
- current_phase: 'implementation',
871
+ current_phase: 'closeout',
797
872
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
798
873
  }));
799
874
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
@@ -801,7 +876,7 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
801
876
  const body = parseBlock(runHook({}, dir).stdout);
802
877
  assert.ok(body, `empty/bare provenance should block for ${desc}`);
803
878
  assert.strictEqual(body.decision, 'block', `empty/bare should block: ${desc}`);
804
- assert.match(body.reason, /\bg\b/, `should fail provenance g for ${desc}`);
879
+ assert.match(body.reason, /\bprovenance\b/, `should fail provenance for ${desc}`);
805
880
  fs.rmSync(dir, { recursive: true, force: true });
806
881
  }
807
882
  });
@@ -819,7 +894,7 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
819
894
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
820
895
  status: 'in_progress',
821
896
  feature_name: FEATURE,
822
- current_phase: 'implementation',
897
+ current_phase: 'closeout',
823
898
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
824
899
  }));
825
900
  const boundEvidence = shouldPass ? bindFixtureReceipt(dir, evidence) : evidence;
@@ -827,7 +902,7 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
827
902
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
828
903
  const result = runHook({}, dir);
829
904
  if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
830
- else assert.match(parseBlock(result.stdout).reason, /\bg\b/, 'arbitrary valid-length SHA values must block');
905
+ else assert.match(parseBlock(result.stdout).reason, /\bprovenance\b/, 'arbitrary valid-length SHA values must block');
831
906
  fs.rmSync(dir, { recursive: true, force: true });
832
907
  }
833
908
  });
@@ -868,7 +943,7 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
868
943
  const blocked = parseBlock(runHook({}, dir).stdout);
869
944
  assert.ok(blocked, 'mutated receipt missing Verification/Command should block on cache-hit');
870
945
  assert.strictEqual(blocked.decision, 'block');
871
- assert.match(blocked.reason, /\bc\b|\be\b/);
946
+ assert.match(blocked.reason, /\bverification_state\b|\bcommand\b/);
872
947
  // cache must not have been promoted to still-valid done; next run must still block
873
948
  const secondBlocked = parseBlock(runHook({}, dir).stdout);
874
949
  assert.ok(secondBlocked, 'second hit after mutation must still block');
@@ -891,7 +966,7 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
891
966
  ].join('\n'));
892
967
  let body = parseBlock(runHook({}, dir).stdout);
893
968
  assert.ok(body, 'missing Head provenance should block on cache-hit');
894
- assert.match(body.reason, /\bg\b/);
969
+ assert.match(body.reason, /\bprovenance\b/);
895
970
  // mutate back to include both but with empty Head value (stale)
896
971
  fs.writeFileSync(taskFile, [
897
972
  '# Task', '', '**Status:** done', '',
@@ -900,7 +975,7 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
900
975
  ].join('\n'));
901
976
  body = parseBlock(runHook({}, dir).stdout);
902
977
  assert.ok(body, 'empty Head should block as stale provenance');
903
- assert.match(body.reason, /\bg\b/);
978
+ assert.match(body.reason, /\bprovenance\b/);
904
979
  } finally {
905
980
  fs.rmSync(dir, { recursive: true, force: true });
906
981
  }
@@ -964,18 +1039,18 @@ test('24. first-run (no cache file) with valid receipt passes and seeds cache',
964
1039
 
965
1040
  test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty must block', () => {
966
1041
  const cases = [
967
- { evidence: ['Verification: PASS','Command: TODO','Exit: 0','Base: a','Head: b'].join('\n'), check: /e\b/, desc: 'Command TODO' },
968
- { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: TBD','Head: b'].join('\n'), check: /g\b/, desc: 'Base TBD' },
969
- { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: N/A'].join('\n'), check: /g\b/, desc: 'Head N/A' },
970
- { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','base_sha: pending','head_sha: unknown'].join('\n'), check: /g\b/, desc: 'base_sha pending' },
971
- { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: '].join('\n'), check: /h\b/, desc: 'artifact empty sha' },
972
- { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: TBD'].join('\n'), check: /h\b/, desc: 'artifact TBD sha' },
1042
+ { evidence: ['Verification: PASS','Command: TODO','Exit: 0','Base: a','Head: b'].join('\n'), check: /\bcommand\b/, desc: 'Command TODO' },
1043
+ { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: TBD','Head: b'].join('\n'), check: /\bprovenance\b/, desc: 'Base TBD' },
1044
+ { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: N/A'].join('\n'), check: /\bprovenance\b/, desc: 'Head N/A' },
1045
+ { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','base_sha: pending','head_sha: unknown'].join('\n'), check: /\bprovenance\b/, desc: 'base_sha pending' },
1046
+ { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: '].join('\n'), check: /\bartifact_hash\b/, desc: 'artifact empty sha' },
1047
+ { evidence: ['Verification: PASS','Command: pnpm test','Exit: 0','Base: a','Head: b','Artifact: bundle','sha256: TBD'].join('\n'), check: /\bartifact_hash\b/, desc: 'artifact TBD sha' },
973
1048
  ];
974
1049
  for (const { evidence, check, desc } of cases) {
975
1050
  const dir = tmpDir();
976
1051
  const featureDir = path.join(dir, 'specs', FEATURE);
977
1052
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
978
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'implementation', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1053
+ 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' } } }));
979
1054
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
980
1055
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
981
1056
  const body = parseBlock(runHook({}, dir).stdout);
@@ -988,7 +1063,7 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
988
1063
  const dir2 = tmpDir();
989
1064
  const featureDir2 = path.join(dir2, 'specs', FEATURE);
990
1065
  fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
991
- fs.writeFileSync(path.join(featureDir2, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'implementation', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1066
+ 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' } } }));
992
1067
  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')));
993
1068
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
994
1069
  assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
@@ -1005,7 +1080,7 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
1005
1080
  const dir = tmpDir();
1006
1081
  const featureDir = path.join(dir, 'specs', FEATURE);
1007
1082
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1008
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'implementation', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
1083
+ 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' } } }));
1009
1084
  fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
1010
1085
  seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
1011
1086
  const body = parseBlock(runHook({}, dir).stdout);
@@ -1019,7 +1094,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
1019
1094
  const dir = tmpDir();
1020
1095
  const featureDir = path.join(dir, 'specs', FEATURE);
1021
1096
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
1022
- fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'implementation', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
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' } } }));
1023
1098
  const outside = path.join(dir, 'outside.md');
1024
1099
  fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
1025
1100
  const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
@@ -1073,7 +1148,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
1073
1148
  fs.rmSync(dir, { recursive: true, force: true });
1074
1149
  });
1075
1150
 
1076
- test('30. phantom vectors via gate evidence must block with check c (r3)', () => {
1151
+ test('30. phantom vectors via gate evidence must block with verification_state (r3)', () => {
1077
1152
  const vectors = [
1078
1153
  'ℹ tests 0',
1079
1154
  'Tests: 0 total',
@@ -1109,7 +1184,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
1109
1184
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1110
1185
  status: 'in_progress',
1111
1186
  feature_name: FEATURE,
1112
- current_phase: 'implementation',
1187
+ current_phase: 'closeout',
1113
1188
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1114
1189
  }));
1115
1190
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
@@ -1118,7 +1193,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
1118
1193
  const body = parseBlock(runHook({}, dir).stdout);
1119
1194
  assert.ok(body, `vector should block: ${v}`);
1120
1195
  assert.strictEqual(body.decision, 'block', `vector ${v} should block`);
1121
- assert.match(body.reason, /\bc\b/, `vector ${v} should fail check c`);
1196
+ assert.match(body.reason, /\bverification_state\b/, `vector ${v} should fail verification_state`);
1122
1197
  fs.rmSync(dir, { recursive: true, force: true });
1123
1198
  }
1124
1199
  // positive controls must not block
@@ -1155,7 +1230,7 @@ test('30. phantom vectors via gate evidence must block with check c (r3)', () =>
1155
1230
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
1156
1231
  status: 'in_progress',
1157
1232
  feature_name: FEATURE,
1158
- current_phase: 'implementation',
1233
+ current_phase: 'closeout',
1159
1234
  task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
1160
1235
  }));
1161
1236
  const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));