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