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

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 (91) hide show
  1. package/bin/__tests__/codex-hooks.test.js +444 -14
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +615 -185
  4. package/bin/__tests__/installer-safety.test.js +39 -0
  5. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  6. package/bin/__tests__/package-inventory.test.js +687 -13
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +1914 -221
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +143 -99
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  22. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  23. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  24. package/src/claude/hooks/completion-authority.cjs +13 -7
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/spec-gate.cjs +47 -117
  31. package/src/claude/hooks/spec-state.cjs +8 -2
  32. package/src/claude/migration-manifest.json +9 -1
  33. package/src/claude/rules/state-sync.md +52 -21
  34. package/src/claude/rules/workflow.md +17 -9
  35. package/src/claude/scripts/spec-final-state.cjs +244 -0
  36. package/src/claude/scripts/spec-ground.cjs +535 -173
  37. package/src/claude/scripts/spec-readiness.cjs +118 -0
  38. package/src/claude/scripts/spec-receipt.cjs +162 -0
  39. package/src/claude/scripts/spec-resolver.cjs +67 -16
  40. package/src/claude/scripts/spec-scaffold.cjs +1354 -139
  41. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  42. package/src/claude/scripts/validate-spec-output.cjs +2782 -142
  43. package/src/claude/scripts/workflow-policy.cjs +501 -183
  44. package/src/claude/settings/settings.json +4 -0
  45. package/src/claude/skills/code-review/SKILL.md +12 -3
  46. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  47. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  48. package/src/claude/skills/develop/SKILL.md +47 -60
  49. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  50. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  51. package/src/claude/skills/specs/SKILL.md +183 -177
  52. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  53. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  54. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  55. package/src/claude/skills/specs/references/review.md +200 -260
  56. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  57. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  58. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  59. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  60. package/src/claude/skills/specs/rules/design-review.md +42 -97
  61. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  62. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  63. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  64. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  65. package/src/claude/skills/specs/templates/design.md +49 -263
  66. package/src/claude/skills/specs/templates/requirements.md +26 -36
  67. package/src/claude/skills/specs/templates/research.md +9 -98
  68. package/src/claude/skills/specs/templates/spec-state.json +23 -85
  69. package/src/claude/skills/specs/templates/task.md +30 -102
  70. package/src/claude/skills/sync/SKILL.md +33 -10
  71. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  72. package/src/claude/skills/test/SKILL.md +17 -7
  73. package/src/codex/AGENTS.md +6 -0
  74. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  75. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  76. package/src/codex/hooks/completion-authority.cjs +13 -7
  77. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  78. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  79. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  80. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  81. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  82. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  83. package/src/codex/hooks/privacy-approval.cjs +1 -1
  84. package/src/codex/hooks/privacy-block.cjs +67 -11
  85. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  86. package/src/codex/hooks/spec-gate.cjs +49 -34
  87. package/src/codex/hooks/spec-state.cjs +10 -3
  88. package/src/codex/hooks/state.cjs +2 -0
  89. package/src/codex/hooks.json +6 -1
  90. package/src/codex/rules/state-sync.md +47 -27
  91. package/src/common/AGENTS.md +1 -1
@@ -12,6 +12,9 @@ const CLAUDE_HOOK = path.join(__dirname, '..', 'completion-authority.cjs');
12
12
  const CODEX_HOOKS = path.join(ROOT, 'codex/hooks');
13
13
  const POLICY = require(path.join(ROOT, 'claude/scripts/workflow-policy.cjs'));
14
14
  const PROVENANCE = require(path.join(ROOT, 'claude/scripts/provenance.cjs'));
15
+ const AUTHORITY_CHECK = require(path.join(ROOT, 'claude/hooks/completion-authority-check.cjs'));
16
+ const RESOLVER = require(path.join(ROOT, 'claude/scripts/spec-resolver.cjs'));
17
+ const { copyClaudeTestRuntime } = require(path.join(ROOT, '../bin/__tests__/test-runtime-dependency-closure.cjs'));
15
18
  const FEATURE = 'authority-fixture';
16
19
  const TASK = 'tasks/closeout.md';
17
20
  const TEST_HOMES = new Map();
@@ -56,18 +59,19 @@ function context(root, session = 'session-a', featureName = FEATURE) {
56
59
  });
57
60
  }
58
61
 
59
- function prepare(root, { lane = 'Standard', status = 'in_progress', proofs = true, feature = FEATURE } = {}) {
62
+ function prepare(root, { lane = 'Standard', status = 'in_progress', phase = 'closeout', proofs = true, feature = FEATURE } = {}) {
60
63
  const featureDir = path.join(root, 'specs', feature);
61
64
  fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
62
65
  const policyInput = lane === 'Critical'
63
- ? { riskSignals: { auth: true } }
66
+ ? { riskSignals: { auth: true }, assurance_level: 'Strict' }
64
67
  : lane === 'Direct'
65
68
  ? { reversible: true, lowRisk: true, isolated: true }
66
69
  : { riskSignals: {} };
67
70
  const spec = {
68
71
  status,
72
+ current_phase: phase,
69
73
  feature_name: feature,
70
- workflow_policy: POLICY.workflowPolicySnapshot(policyInput),
74
+ workflow_policy: POLICY.canonicalWorkflowPolicySnapshot(policyInput),
71
75
  task_registry: { [TASK]: { status: 'done', completed_at: '2026-08-12T00:00:00.000Z' } },
72
76
  };
73
77
  fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
@@ -80,6 +84,13 @@ function prepare(root, { lane = 'Standard', status = 'in_progress', proofs = tru
80
84
  `Head: ${runtime.head}`,
81
85
  ].join('\n');
82
86
  fs.writeFileSync(path.join(featureDir, TASK), `# Closeout\n\nStatus: done\n\n## Evidence\n\n${body}\n`);
87
+ fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
88
+ `Feature: ${feature}`,
89
+ 'Expected: final integration verification passes',
90
+ 'Observed: final integration verification passed',
91
+ body,
92
+ '',
93
+ ].join('\n'));
83
94
  if (proofs) {
84
95
  spec.proofs = {
85
96
  needsInspection: { receipt: body },
@@ -121,11 +132,14 @@ function adapter(kind, root) {
121
132
  },
122
133
  };
123
134
  }
135
+ copyClaudeTestRuntime(path.join(ROOT, '..'), path.join(root, '.codex'));
124
136
  const hooks = path.join(root, '.codex/hooks');
125
137
  fs.cpSync(CODEX_HOOKS, hooks, { recursive: true });
126
- fs.mkdirSync(path.join(root, '.codex/scripts'), { recursive: true });
127
- for (const file of ['workflow-policy.cjs', 'provenance.cjs', 'spec-resolver.cjs']) {
128
- fs.copyFileSync(path.join(ROOT, 'claude/scripts', file), path.join(root, '.codex/scripts', file));
138
+ for (const helper of ['privacy-command-analysis.cjs', 'runtime-path-safety.cjs']) {
139
+ fs.copyFileSync(
140
+ path.join(ROOT, 'claude/hooks/lib', helper),
141
+ path.join(hooks, 'lib', helper),
142
+ );
129
143
  }
130
144
  return {
131
145
  root,
@@ -159,6 +173,13 @@ function stateRecord(runtime, prefix) {
159
173
  return path.join(runtime.state, projects.name, files[0]);
160
174
  }
161
175
 
176
+ function writeRuntime(runtime, value) {
177
+ const platformDir = runtime.hook.includes(`${path.sep}.codex${path.sep}`) ? '.codex' : '.claude';
178
+ const dir = path.join(runtime.root, platformDir);
179
+ fs.mkdirSync(dir, { recursive: true });
180
+ fs.writeFileSync(path.join(dir, 'runtime.json'), `${JSON.stringify(value)}\n`);
181
+ }
182
+
162
183
  function sessionStart(runtime, source = 'startup', sessionId = 'session-a') {
163
184
  return spawnSync(process.execPath, [runtime.sessionHook], {
164
185
  cwd: runtime.root,
@@ -196,6 +217,39 @@ function nonceFrom(result) {
196
217
  }
197
218
 
198
219
  for (const kind of ['claude', 'codex']) {
220
+ test(`${kind}: authoring, partial execution, done-before-closeout, and stop loops are silent`, () => {
221
+ const root = gitFixture();
222
+ try {
223
+ const runtime = adapter(kind, root);
224
+ const fixture = prepare(root, { phase: 'authoring' });
225
+ let spec = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
226
+ spec.task_registry = {};
227
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
228
+ fs.rmSync(path.join(fixture.featureDir, 'feature-receipt.md'), { force: true });
229
+ assert.equal(stop(runtime).stdout.trim(), '', 'Compact taskless authoring pause must be silent');
230
+
231
+ spec.task_registry = { [TASK]: { status: 'in_progress', completed_at: null } };
232
+ spec.current_phase = 'implementation';
233
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
234
+ assert.equal(stop(runtime).stdout.trim(), '', 'partial task execution must be silent');
235
+
236
+ spec.task_registry[TASK] = { status: 'done', completed_at: '2026-08-12T00:00:00.000Z' };
237
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
238
+ assert.equal(stop(runtime).stdout.trim(), '', 'done tasks without explicit closeout must be silent');
239
+ assert.equal(stateFiles(runtime).length, 0, 'inactive stops must not mutate authority state');
240
+
241
+ const loop = stop(runtime, { stop_hook_active: true });
242
+ assert.equal(loop.status, 0, loop.stderr);
243
+ assert.equal(loop.stdout.trim(), '', 'recursive Stop invocation must be silent');
244
+
245
+ spec.current_phase = 'closeout';
246
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
247
+ assert.match(block(stop(runtime)).reason, /technical completion proof|feature-receipt/i);
248
+ } finally {
249
+ cleanupFixture(root);
250
+ }
251
+ });
252
+
199
253
  test(`${kind}: exact user approval is one-time and runtime-bound`, () => {
200
254
  const root = gitFixture();
201
255
  try {
@@ -300,7 +354,7 @@ for (const kind of ['claude', 'codex']) {
300
354
  }
301
355
  });
302
356
 
303
- test(`${kind}: completed, empty/malformed candidates, Critical fake proofs, and Direct no-spec are fail-closed`, () => {
357
+ test(`${kind}: completed, invalid/malformed candidates, Critical fake proofs, and Direct no-spec are fail-closed`, () => {
304
358
  const root = gitFixture();
305
359
  try {
306
360
  const runtime = adapter(kind, root);
@@ -319,9 +373,9 @@ for (const kind of ['claude', 'codex']) {
319
373
 
320
374
  const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
321
375
  const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
322
- spec.task_registry = {};
376
+ spec.task_registry[TASK].completed_at = null;
323
377
  fs.writeFileSync(specFile, JSON.stringify(spec));
324
- assert.match(block(stop(runtime)).reason, /task_registry is missing or empty/i);
378
+ assert.match(block(stop(runtime)).reason, /completed_at/i);
325
379
 
326
380
  fs.writeFileSync(specFile, '{ malformed');
327
381
  assert.equal(block(stop(runtime)).decision, 'block');
@@ -435,12 +489,12 @@ for (const kind of ['claude', 'codex']) {
435
489
  const root = gitFixture();
436
490
  try {
437
491
  const runtime = adapter(kind, root);
438
- prepare(root, { proofs: false });
492
+ prepare(root, { lane: 'Critical', proofs: false });
439
493
  const incomplete = block(stop(runtime));
440
494
  assert.match(incomplete.reason, /technical|proof|missing/i);
441
495
  assert.equal(stateFiles(runtime).filter((name) => name.startsWith('baseline-')).length, 1);
442
496
 
443
- prepare(root);
497
+ prepare(root, { lane: 'Critical' });
444
498
  const pending = nonceFrom(stop(runtime));
445
499
  assert.equal(sessionStart(runtime).status, 0);
446
500
  assert.equal(stateFiles(runtime).filter((name) => name.startsWith('baseline-')).length, 1);
@@ -454,7 +508,7 @@ for (const kind of ['claude', 'codex']) {
454
508
  approve(runtime, retry.nonce);
455
509
  const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
456
510
  const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
457
- spec.workflow_policy = POLICY.workflowPolicySnapshot({ reversible: true, lowRisk: true, isolated: true });
511
+ spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' });
458
512
  fs.writeFileSync(specFile, JSON.stringify(spec));
459
513
  const downgrade = block(stop(runtime));
460
514
  assert.match(downgrade.reason, /monotonic|downgraded/i);
@@ -463,7 +517,7 @@ for (const kind of ['claude', 'codex']) {
463
517
  }
464
518
  });
465
519
 
466
- test(`${kind}: Critical feature A survives deletion and blocks Direct feature B`, () => {
520
+ test(`${kind}: Critical feature A cannot raise ceremony for Compact Routine feature B`, () => {
467
521
  const root = gitFixture();
468
522
  try {
469
523
  const runtime = adapter(kind, root);
@@ -472,9 +526,12 @@ for (const kind of ['claude', 'codex']) {
472
526
  assert.equal(stateFiles(runtime).filter((name) => name === 'policy-floor.json').length, 1);
473
527
 
474
528
  fs.rmSync(path.join(root, 'specs', 'critical-feature-a'), { recursive: true, force: true });
475
- prepare(root, { feature: 'direct-feature-b', lane: 'Direct' });
476
- const blocked = block(stop(runtime));
477
- assert.match(blocked.reason, /project policy floor|monotonic|downgraded/i);
529
+ prepare(root, { feature: 'compact-feature-b', lane: 'Standard' });
530
+ const light = nonceFrom(stop(runtime));
531
+ assert.match(light.body.reason, /approval/i);
532
+ const floor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
533
+ assert.deepEqual(floor.policy, POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' }));
534
+ assert.equal(stateFiles(runtime).filter((name) => name.startsWith('baseline-')).length, 2);
478
535
  } finally {
479
536
  cleanupFixture(root);
480
537
  }
@@ -511,40 +568,39 @@ for (const kind of ['claude', 'codex']) {
511
568
  }
512
569
  });
513
570
 
514
- test(`${kind}: project policy floor prevents risk and proof-obligation removal`, () => {
571
+ test(`${kind}: project policy floor prevents risk removal without coupling assurance to durable state`, () => {
515
572
  const root = gitFixture();
516
573
  try {
517
574
  const runtime = adapter(kind, root);
518
575
  prepare(root, { lane: 'Critical' });
519
- nonceFrom(stop(runtime));
520
576
  const specFile = path.join(root, 'specs', FEATURE, 'spec.json');
577
+ const initialSpec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
578
+ initialSpec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ riskSignals: { auth: true, privacy: true } });
579
+ assert.equal(POLICY.readWorkflowPolicySnapshot(initialSpec).proof_obligations.includes('needsDurableTaskState'), false);
580
+ fs.writeFileSync(specFile, JSON.stringify(initialSpec));
581
+ nonceFrom(stop(runtime));
521
582
  const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
522
583
  assert.ok(spec.workflow_policy.risks.length > 0);
523
- assert.ok(spec.workflow_policy.proof_obligations.length > 0);
524
- const removedObligationIndex = spec.workflow_policy.proof_obligations.length - 1;
525
- spec.workflow_policy = {
526
- ...spec.workflow_policy,
527
- risks: spec.workflow_policy.risks.slice(0, -1),
528
- proof_obligations: spec.workflow_policy.proof_obligations.slice(0, -1),
529
- actor_needs: spec.workflow_policy.actor_needs.filter((_, index) => index !== removedObligationIndex),
530
- };
584
+ assert.ok(POLICY.readWorkflowPolicySnapshot(spec).proof_obligations.length > 0);
585
+ spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ riskSignals: { auth: true } });
531
586
  fs.writeFileSync(specFile, JSON.stringify(spec));
532
587
  const blocked = block(stop(runtime));
533
588
  assert.match(blocked.reason, /risk .*removed/i);
534
- assert.match(blocked.reason, /obligation .*removed/i);
535
589
  } finally {
536
590
  cleanupFixture(root);
537
591
  }
538
592
  });
539
593
 
540
- test(`${kind}: missing or corrupt project policy floor fails closed`, () => {
594
+ test(`${kind}: missing legacy floor migrates from config while corrupt floor fails closed`, () => {
541
595
  const missingRoot = gitFixture();
542
596
  try {
543
597
  const runtime = adapter(kind, missingRoot);
544
598
  prepare(missingRoot);
545
599
  nonceFrom(stop(runtime));
546
600
  fs.rmSync(stateRecord(runtime, 'policy-floor'), { force: true });
547
- assert.match(block(stop(runtime)).reason, /floor|authority/i);
601
+ const migrated = nonceFrom(stop(runtime));
602
+ assert.match(migrated.body.reason, /approval/i);
603
+ assert.equal(JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8')).schema_version, 3);
548
604
  } finally {
549
605
  cleanupFixture(missingRoot);
550
606
  }
@@ -561,19 +617,19 @@ for (const kind of ['claude', 'codex']) {
561
617
  }
562
618
  });
563
619
 
564
- test(`${kind}: valid policy escalation is allowed, while missing policy is blocked`, () => {
620
+ test(`${kind}: configured minimum is stable across feature escalation, while missing policy is blocked`, () => {
565
621
  const root = gitFixture();
566
622
  try {
567
623
  const runtime = adapter(kind, root);
568
624
  prepare(root, { lane: 'Standard' });
569
625
  nonceFrom(stop(runtime));
570
626
  const standardFloor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
571
- assert.equal(standardFloor.policy.lane, 'Standard');
627
+ assert.equal(POLICY.readWorkflowPolicySnapshot({ workflow_policy: standardFloor.policy }).lane, 'Standard');
572
628
  prepare(root, { lane: 'Critical' });
573
629
  const escalated = nonceFrom(stop(runtime));
574
630
  assert.match(escalated.body.reason, /approval/i);
575
631
  const criticalFloor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
576
- assert.equal(criticalFloor.policy.lane, 'Critical');
632
+ assert.deepEqual(criticalFloor.policy, standardFloor.policy);
577
633
 
578
634
  const missingRoot = gitFixture();
579
635
  try {
@@ -591,21 +647,63 @@ for (const kind of ['claude', 'codex']) {
591
647
  }
592
648
  });
593
649
 
594
- test(`${kind}: explicit targets cannot hide global persisted specs or mismatch the sole candidate`, () => {
650
+ test(`${kind}: canonical config minimum applies without inheriting feature policy`, () => {
651
+ const root = gitFixture();
652
+ try {
653
+ const runtime = adapter(kind, root);
654
+ writeRuntime(runtime, { spec: { policy_minimum: { planning_depth: 'Full', assurance_level: 'Elevated' } } });
655
+ prepare(root, { lane: 'Standard' });
656
+ assert.match(block(stop(runtime)).reason, /configured project minimum/i);
657
+ const strict = prepare(root, { lane: 'Critical' });
658
+ const spec = JSON.parse(fs.readFileSync(strict.specFile, 'utf8'));
659
+ spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Strict', risks: ['auth'] });
660
+ fs.writeFileSync(strict.specFile, JSON.stringify(spec));
661
+ nonceFrom(stop(runtime));
662
+ const floor = JSON.parse(fs.readFileSync(stateRecord(runtime, 'policy-floor'), 'utf8'));
663
+ assert.deepEqual(floor.policy, POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Elevated' }));
664
+ } finally {
665
+ cleanupFixture(root);
666
+ }
667
+ });
668
+
669
+ test(`${kind}: legacy v2 baseline migrates to canonical 2.1 without losing feature history`, () => {
670
+ const root = gitFixture();
671
+ try {
672
+ const runtime = adapter(kind, root);
673
+ const fixture = prepare(root);
674
+ const spec = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
675
+ spec.workflow_policy = POLICY.workflowPolicySnapshot({ riskSignals: {} });
676
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
677
+ nonceFrom(stop(runtime));
678
+ const first = JSON.parse(fs.readFileSync(stateRecord(runtime, 'baseline-'), 'utf8'));
679
+ assert.equal(first.policy.version, '2.1');
680
+ assert.equal(first.schema_version, 3);
681
+
682
+ spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' });
683
+ fs.writeFileSync(fixture.specFile, JSON.stringify(spec));
684
+ nonceFrom(stop(runtime));
685
+ const migrated = JSON.parse(fs.readFileSync(stateRecord(runtime, 'baseline-'), 'utf8'));
686
+ assert.equal(migrated.policy.version, '2.1');
687
+ assert.equal(migrated.issued_at, first.issued_at);
688
+ } finally {
689
+ cleanupFixture(root);
690
+ }
691
+ });
692
+
693
+ test(`${kind}: explicit target resolves before sibling ambiguity while no target remains ambiguous`, () => {
595
694
  const root = gitFixture();
596
695
  try {
597
696
  const runtime = adapter(kind, root);
598
697
  prepare(root);
599
698
  assert.match(block(stop(runtime, { featureName: 'other-feature' })).reason, /explicit/i);
600
- const otherDir = path.join(root, 'specs', 'other-feature');
601
- fs.mkdirSync(otherDir, { recursive: true });
602
- fs.writeFileSync(path.join(otherDir, 'spec.json'), JSON.stringify({
603
- feature_name: 'other-feature',
604
- status: 'in_progress',
605
- workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: {} }),
606
- task_registry: { [TASK]: { status: 'in_progress' } },
607
- }));
608
- assert.match(block(stop(runtime, { featureName: FEATURE })).reason, /multiple_persisted|multiple/i);
699
+ prepare(root, { feature: 'other-feature', phase: 'implementation' });
700
+ assert.ok(nonceFrom(stop(runtime, { featureName: FEATURE })).nonce);
701
+ assert.match(block(stop(runtime)).reason, /multiple_persisted|multiple/i);
702
+ for (const malformed of [
703
+ { featureName: '' },
704
+ { featureName: 42 },
705
+ { specPath: '../escape/spec.json' },
706
+ ]) assert.match(block(stop(runtime, malformed)).reason, /explicit|malformed|escape/i);
609
707
  } finally {
610
708
  cleanupFixture(root);
611
709
  }
@@ -673,14 +771,7 @@ test('installed scaffold observes Critical policy before Stop and source scaffol
673
771
  });
674
772
  },
675
773
  };
676
- const scaffolded = spawnSync(process.execPath, [
677
- installedScaffold,
678
- 'installed-critical',
679
- '--tasks',
680
- 'R0-01-authority-baseline',
681
- '--lane',
682
- 'Critical',
683
- ], {
774
+ const scaffolded = spawnSync(process.execPath, [installedScaffold, 'installed-critical', '--lane', 'Critical'], {
684
775
  cwd: root,
685
776
  env: { ...process.env, HOME: installedHome, USERPROFILE: installedHome },
686
777
  encoding: 'utf8',
@@ -698,14 +789,7 @@ test('installed scaffold observes Critical policy before Stop and source scaffol
698
789
  const disappeared = block(stop(installedRuntime));
699
790
  assert.match(disappeared.reason, /disappeared/i);
700
791
 
701
- const sourceScaffolded = spawnSync(process.execPath, [
702
- path.join(ROOT, 'claude/scripts/spec-scaffold.cjs'),
703
- 'source-critical',
704
- '--tasks',
705
- 'R0-01-source-baseline',
706
- '--lane',
707
- 'Critical',
708
- ], {
792
+ const sourceScaffolded = spawnSync(process.execPath, [path.join(ROOT, 'claude/scripts/spec-scaffold.cjs'), 'source-critical', '--lane', 'Critical'], {
709
793
  cwd: sourceRoot,
710
794
  env: { ...process.env, HOME: sourceHome, USERPROFILE: sourceHome },
711
795
  encoding: 'utf8',
@@ -718,3 +802,152 @@ test('installed scaffold observes Critical policy before Stop and source scaffol
718
802
  fs.rmSync(sourceHome, { recursive: true, force: true });
719
803
  }
720
804
  });
805
+
806
+ test('canonical final-state decision rejects every stale mutation class before execution proof', () => {
807
+ const root = gitFixture();
808
+ try {
809
+ const featureDir = path.join(root, 'specs', 'final-state-matrix');
810
+ fs.mkdirSync(featureDir, { recursive: true });
811
+ const specFile = path.join(featureDir, 'spec.json');
812
+ const digest = `sha256:${'a'.repeat(64)}`;
813
+ const baseSpec = {
814
+ schema_version: '2.1', feature_name: 'final-state-matrix', ready_for_implementation: true,
815
+ validation: { status: 'completed', semantic_review: { status: 'completed', semantic_digest: digest } },
816
+ workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' }),
817
+ };
818
+ fs.writeFileSync(specFile, JSON.stringify(baseSpec));
819
+ const candidate = { featureName: 'final-state-matrix', featureDir, specFile, spec: baseSpec };
820
+ const state = { validationErrors: [], groundingErrors: [], digest, observed: false };
821
+ const dependencies = {
822
+ validator: {
823
+ validateSpec: () => ({ errors: state.validationErrors, warnings: [] }),
824
+ computeSemanticDigest21: () => ({ errors: [], digest: state.digest }),
825
+ },
826
+ grounder: { groundSpec: () => ({ errors: state.groundingErrors, warnings: [], checked: 0 }) },
827
+ semanticAuthority: { verifyAttestation: () => ({ ok: state.observed, reason: 'missing observation' }) },
828
+ };
829
+ const cases = [
830
+ ['readiness', (spec) => { spec.ready_for_implementation = false; }, /readiness/],
831
+ ['canonical validation', () => { state.validationErrors = ['invalid current state']; }, /validation failed/],
832
+ ['grounding', () => { state.groundingErrors = ['ungrounded target']; }, /grounding failed/],
833
+ ['semantic digest', () => { state.digest = `sha256:${'b'.repeat(64)}`; }, /digest is stale/],
834
+ ];
835
+ for (const [name, mutate, expected] of cases) {
836
+ candidate.spec = structuredClone(baseSpec);
837
+ state.validationErrors = [];
838
+ state.groundingErrors = [];
839
+ state.digest = digest;
840
+ mutate(candidate.spec);
841
+ const result = AUTHORITY_CHECK.validateCanonicalFinalState({ policy: POLICY, projectRoot: root, candidate, dependencies });
842
+ assert.equal(result.ok, false, name);
843
+ assert.match(result.reason, expected, name);
844
+ }
845
+
846
+ candidate.spec = structuredClone(baseSpec);
847
+ state.digest = digest;
848
+ assert.equal(AUTHORITY_CHECK.validateCanonicalFinalState({ policy: POLICY, projectRoot: root, candidate, dependencies }).ok, true);
849
+ candidate.spec.workflow_policy = POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Full', assurance_level: 'Strict' });
850
+ assert.match(
851
+ AUTHORITY_CHECK.validateCanonicalFinalState({ policy: POLICY, projectRoot: root, candidate, dependencies }).reason,
852
+ /host-hook-observed reviewer PASS/,
853
+ );
854
+ state.observed = true;
855
+ assert.equal(AUTHORITY_CHECK.validateCanonicalFinalState({ policy: POLICY, projectRoot: root, candidate, dependencies }).ok, true);
856
+ } finally { cleanupFixture(root); }
857
+ });
858
+
859
+ test('schema 2.1 lifecycle vocabulary is exact and legacy aliases stay in the legacy adapter', () => {
860
+ const canonical = [
861
+ ['in_progress', false],
862
+ ['paused', false],
863
+ ['blocked', false],
864
+ ['done', true],
865
+ ];
866
+ for (const checker of [AUTHORITY_CHECK, require(path.join(ROOT, 'codex/hooks/completion-authority-check.cjs'))]) {
867
+ for (const [status, terminal] of canonical) {
868
+ const spec = { schema_version: '2.1', status };
869
+ assert.equal(checker.isValidLifecycleStatus(spec), true, status);
870
+ assert.equal(checker.isDurableCloseout(spec), terminal, status);
871
+ }
872
+ for (const alias of ['completed', 'complete', 'in-progress']) {
873
+ const spec = { schema_version: '2.1', status: alias, current_phase: 'closeout' };
874
+ assert.equal(checker.isValidLifecycleStatus(spec), false, alias);
875
+ assert.equal(checker.isDurableCloseout(spec), false, alias);
876
+ }
877
+ assert.equal(checker.isValidLifecycleStatus({ schema_version: '2.0', status: 'completed' }), true);
878
+ assert.equal(checker.isDurableCloseout({ schema_version: '2.0', status: 'in_progress', current_phase: 'closeout' }), true);
879
+ }
880
+ });
881
+
882
+ test('final-state authority loads the validator CommonJS API directly and propagates canonical failures', () => {
883
+ const root = gitFixture();
884
+ try {
885
+ const validatorPath = path.join(ROOT, 'claude/scripts/validate-spec-output.cjs');
886
+ const canonicalValidator = require(validatorPath);
887
+ const featureDir = path.join(root, 'specs', 'direct-validator-api');
888
+ fs.mkdirSync(featureDir, { recursive: true });
889
+ const specFile = path.join(featureDir, 'spec.json');
890
+ const digest = `sha256:${'a'.repeat(64)}`;
891
+ const candidate = {
892
+ featureName: 'direct-validator-api', featureDir, specFile,
893
+ spec: {
894
+ schema_version: '2.1', status: 'done', ready_for_implementation: true,
895
+ validation: { status: 'completed', semantic_review: { status: 'completed', semantic_digest: digest } },
896
+ workflow_policy: POLICY.canonicalWorkflowPolicySnapshot({ planning_depth: 'Compact', assurance_level: 'Routine' }),
897
+ },
898
+ };
899
+ fs.writeFileSync(specFile, `${JSON.stringify(candidate.spec, null, 2)}\n`);
900
+ const validatorFixture = path.join(root, 'direct-validator.cjs');
901
+ fs.writeFileSync(validatorFixture, [
902
+ "'use strict';",
903
+ "module.exports = {",
904
+ " validateSpec() { return { errors: ['direct-api-canonical-failure'], warnings: [] }; },",
905
+ ` computeSemanticDigest21() { return { errors: [], digest: '${digest}' }; },`,
906
+ "};",
907
+ '',
908
+ ].join('\n'));
909
+ const invalidFixture = path.join(root, 'invalid-validator.cjs');
910
+ fs.writeFileSync(invalidFixture, 'module.exports = { validateSpec() {} };\n');
911
+
912
+ for (const [name, checkerPath] of [
913
+ ['claude', path.join(ROOT, 'claude/hooks/completion-authority-check.cjs')],
914
+ ['codex', path.join(ROOT, 'codex/hooks/completion-authority-check.cjs')],
915
+ ]) {
916
+ const checker = require(checkerPath);
917
+ assert.doesNotMatch(fs.readFileSync(checkerPath, 'utf8'), /spawnSync|node:child_process|require\(['"]child_process['"]\)/, `${name} has no validator CLI path`);
918
+ const dependencies = checker.finalStateDependencies();
919
+ assert.equal(dependencies.validator.validateSpec, canonicalValidator.validateSpec, `${name} resolves the source CommonJS API directly`);
920
+ assert.equal(dependencies.validator.computeSemanticDigest21, canonicalValidator.computeSemanticDigest21, `${name} resolves the digest API directly`);
921
+ const directValidator = checker.validatorApi(validatorFixture);
922
+ const result = checker.validateCanonicalFinalState({
923
+ policy: POLICY, projectRoot: root, candidate,
924
+ dependencies: {
925
+ validator: directValidator,
926
+ grounder: { groundSpec: () => ({ errors: [], warnings: [] }) },
927
+ semanticAuthority: { verifyAttestation: () => ({ ok: true }) },
928
+ },
929
+ });
930
+ assert.equal(result.ok, false, name);
931
+ assert.match(result.reason, /direct-api-canonical-failure/, name);
932
+ assert.throws(() => checker.validatorApi(invalidFixture), /must export validateSpec and computeSemanticDigest21/, name);
933
+ }
934
+ } finally { cleanupFixture(root); }
935
+ });
936
+
937
+ test('persisted resolver isolates exact explicit target from malformed siblings and scans globally only without target', () => {
938
+ const root = gitFixture();
939
+ try {
940
+ prepare(root, { feature: 'exact-target', phase: 'implementation' });
941
+ const malformedDir = path.join(root, 'specs', 'malformed-sibling');
942
+ fs.mkdirSync(malformedDir, { recursive: true });
943
+ fs.writeFileSync(path.join(malformedDir, 'spec.json'), '{ malformed');
944
+ const explicit = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {}, target: { featureName: 'exact-target' } });
945
+ assert.equal(explicit.error, undefined);
946
+ assert.equal(explicit.featureName, 'exact-target');
947
+ const global = RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {} });
948
+ assert.equal(global.error, 'invalid_specs');
949
+ for (const target of [{ featureName: '' }, { featureName: 42 }, { specPath: '../escape/spec.json' }]) {
950
+ assert.match(RESOLVER.resolvePersistedSpec({ projectRoot: root, runtime: {}, target }).error, /^explicit_/);
951
+ }
952
+ } finally { cleanupFixture(root); }
953
+ });