@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.
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- 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 +705 -13
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- 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 +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -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 +99 -28
- 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/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -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 +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- 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 +214 -262
- 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 +35 -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 +61 -38
- 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 +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- 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
|
@@ -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.
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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,
|
|
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, /
|
|
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.
|
|
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
|
|
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: '
|
|
476
|
-
const
|
|
477
|
-
assert.match(
|
|
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
|
|
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.
|
|
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
|
-
};
|
|
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
|
|
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
|
-
|
|
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}:
|
|
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.
|
|
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}:
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
});
|