@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.
- package/bin/__tests__/codex-hooks.test.js +444 -14
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +615 -185
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +687 -13
- package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +1914 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +77 -30
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/spec-final-state.cjs +244 -0
- package/src/claude/scripts/spec-ground.cjs +535 -173
- package/src/claude/scripts/spec-readiness.cjs +118 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1354 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +2782 -142
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +200 -260
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +23 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +38 -16
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +1 -1
- package/src/codex/hooks/privacy-block.cjs +67 -11
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- 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.
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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,
|
|
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, /
|
|
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.
|
|
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
|
|
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: '
|
|
476
|
-
const
|
|
477
|
-
assert.match(
|
|
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
|
|
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.
|
|
524
|
-
|
|
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
|
|
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
|
-
|
|
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}:
|
|
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.
|
|
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}:
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
});
|