@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
|
@@ -14,6 +14,14 @@ const REQUIRED_PAYLOAD = [
|
|
|
14
14
|
'src/claude/scripts/scan-staged-secrets.cjs',
|
|
15
15
|
'src/claude/scripts/workflow-policy.cjs',
|
|
16
16
|
'src/claude/scripts/provenance.cjs',
|
|
17
|
+
'src/claude/scripts/spec-receipt.cjs',
|
|
18
|
+
'src/claude/scripts/spec-ground.cjs',
|
|
19
|
+
'src/claude/scripts/spec-final-state.cjs',
|
|
20
|
+
'src/claude/scripts/spec-readiness.cjs',
|
|
21
|
+
'src/claude/scripts/spec-semantic-model.cjs',
|
|
22
|
+
'src/claude/scripts/validate-spec-output.cjs',
|
|
23
|
+
'src/claude/scripts/spec-authoring-validation.cjs',
|
|
24
|
+
'src/claude/scripts/spec-authoring-digest.cjs',
|
|
17
25
|
];
|
|
18
26
|
const FORBIDDEN_PAYLOAD = [
|
|
19
27
|
/(^|\/)\.logs(\/|$)/,
|
|
@@ -31,12 +39,18 @@ const RUNTIMES = {
|
|
|
31
39
|
};
|
|
32
40
|
|
|
33
41
|
function npmPack(args, cwd) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const cache = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-npm-pack-cache-'));
|
|
43
|
+
try {
|
|
44
|
+
const result = spawnSync('npm', ['pack', '--ignore-scripts', ...args], {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
env: { ...process.env, npm_config_cache: cache },
|
|
48
|
+
});
|
|
49
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
50
|
+
return JSON.parse(result.stdout)[0];
|
|
51
|
+
} finally {
|
|
52
|
+
fs.rmSync(cache, { recursive: true, force: true });
|
|
53
|
+
}
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
function packedInventory(tarball) {
|
|
@@ -62,12 +76,47 @@ function assertCleanInventory(inventory) {
|
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
function
|
|
79
|
+
function packageDirectory(name, from) {
|
|
80
|
+
let directory = path.dirname(require.resolve(name, { paths: [from] }));
|
|
81
|
+
while (directory !== path.dirname(directory)) {
|
|
82
|
+
const manifest = path.join(directory, 'package.json');
|
|
83
|
+
if (fs.existsSync(manifest)) {
|
|
84
|
+
const metadata = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
85
|
+
if (metadata.name === name) return { directory, metadata };
|
|
86
|
+
}
|
|
87
|
+
directory = path.dirname(directory);
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`cannot resolve local runtime dependency ${name}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function packedRuntimeClosure(destination) {
|
|
93
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
94
|
+
const rootManifest = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
95
|
+
const queue = Object.keys(rootManifest.dependencies || {});
|
|
96
|
+
const visited = new Set();
|
|
97
|
+
const tarballs = [];
|
|
98
|
+
while (queue.length > 0) {
|
|
99
|
+
const name = queue.shift();
|
|
100
|
+
if (visited.has(name)) continue;
|
|
101
|
+
visited.add(name);
|
|
102
|
+
const resolved = packageDirectory(name, PACKAGE_ROOT);
|
|
103
|
+
const packed = npmPack(['--ignore-scripts', '--pack-destination', destination, '--json'], resolved.directory);
|
|
104
|
+
tarballs.push(path.join(destination, packed.filename));
|
|
105
|
+
queue.push(...Object.keys(resolved.metadata.dependencies || {}));
|
|
106
|
+
}
|
|
107
|
+
return tarballs.sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installPacked(tarball, root, runtimeClosure) {
|
|
66
111
|
fs.mkdirSync(root, { recursive: true });
|
|
67
112
|
const result = spawnSync('npm', [
|
|
68
113
|
'install', '--offline', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
69
|
-
'--package-lock=false', '--prefix', root, tarball,
|
|
70
|
-
], {
|
|
114
|
+
'--package-lock=false', '--prefix', root, tarball, ...runtimeClosure,
|
|
115
|
+
], {
|
|
116
|
+
cwd: root,
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
env: { ...process.env, npm_config_cache: path.join(root, '.cafekit-npm-cache') },
|
|
119
|
+
});
|
|
71
120
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
72
121
|
const installer = path.join(root, 'node_modules', '@haposoft', 'cafekit', 'bin', 'install.js');
|
|
73
122
|
assert.ok(fs.existsSync(installer), 'npm install must resolve packed package bin');
|
|
@@ -129,6 +178,9 @@ function assertInstalledProvenance(root, platform, fixture) {
|
|
|
129
178
|
const helper = path.join(scripts, 'provenance.cjs');
|
|
130
179
|
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
131
180
|
const resolver = path.join(scripts, 'spec-resolver.cjs');
|
|
181
|
+
const receipt = path.join(scripts, 'spec-receipt.cjs');
|
|
182
|
+
const finalState = path.join(scripts, 'spec-final-state.cjs');
|
|
183
|
+
const readiness = path.join(scripts, 'spec-readiness.cjs');
|
|
132
184
|
const gate = path.join(
|
|
133
185
|
root,
|
|
134
186
|
platform === 'claude' ? '.claude/hooks/spec-gate.cjs' : '.codex/hooks/spec-gate.cjs'
|
|
@@ -137,14 +189,37 @@ function assertInstalledProvenance(root, platform, fixture) {
|
|
|
137
189
|
const authority = path.join(authorityDir, 'completion-authority.cjs');
|
|
138
190
|
const authorityCheck = path.join(authorityDir, 'completion-authority-check.cjs');
|
|
139
191
|
const authorityState = path.join(authorityDir, 'completion-authority-state.cjs');
|
|
140
|
-
|
|
192
|
+
const semanticAuthority = path.join(authorityDir, 'semantic-review-authority.cjs');
|
|
193
|
+
for (const file of [helper, policy, resolver, receipt, finalState, readiness]) {
|
|
141
194
|
assert.equal(fs.existsSync(file), true, `${platform} installed file missing: ${file}`);
|
|
142
195
|
}
|
|
143
196
|
assert.equal(fs.existsSync(gate), true, `${platform} installed gate missing: ${gate}`);
|
|
144
197
|
|
|
145
|
-
for (const file of [authority, authorityCheck, authorityState]) {
|
|
198
|
+
for (const file of [authority, authorityCheck, authorityState, semanticAuthority]) {
|
|
146
199
|
assert.equal(fs.existsSync(file), true, `${platform} installed completion authority missing: ${file}`);
|
|
147
200
|
}
|
|
201
|
+
const finalStateBytes = fs.readFileSync(finalState);
|
|
202
|
+
fs.rmSync(finalState);
|
|
203
|
+
const missingFinalState = spawnSync(process.execPath, [authority, '--stop'], {
|
|
204
|
+
cwd: root,
|
|
205
|
+
env: { ...process.env, PROJECT_ROOT: root },
|
|
206
|
+
input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
});
|
|
209
|
+
assert.equal(JSON.parse(missingFinalState.stdout).decision, 'block', `${platform} missing final-state closure must fail closed`);
|
|
210
|
+
const escapedFinalState = path.join(root, 'escaped-spec-final-state.cjs');
|
|
211
|
+
fs.writeFileSync(escapedFinalState, finalStateBytes);
|
|
212
|
+
fs.symlinkSync(escapedFinalState, finalState);
|
|
213
|
+
const symlinkFinalState = spawnSync(process.execPath, [authority, '--stop'], {
|
|
214
|
+
cwd: root,
|
|
215
|
+
env: { ...process.env, PROJECT_ROOT: root },
|
|
216
|
+
input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
|
|
217
|
+
encoding: 'utf8',
|
|
218
|
+
});
|
|
219
|
+
assert.equal(JSON.parse(symlinkFinalState.stdout).decision, 'block', `${platform} symlinked final-state closure must fail closed`);
|
|
220
|
+
fs.rmSync(finalState);
|
|
221
|
+
fs.writeFileSync(finalState, finalStateBytes);
|
|
222
|
+
fs.rmSync(escapedFinalState);
|
|
148
223
|
const authorityCheckBytes = fs.readFileSync(authorityCheck);
|
|
149
224
|
fs.writeFileSync(authorityCheck, 'module.exports = {};\n');
|
|
150
225
|
const malformedAuthority = spawnSync(process.execPath, [authority, '--stop'], {
|
|
@@ -304,13 +379,26 @@ function assertInstalledScripts(root, platform) {
|
|
|
304
379
|
const scripts = path.join(root, RUNTIMES[platform].root, 'scripts');
|
|
305
380
|
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
306
381
|
const scanner = path.join(scripts, 'scan-staged-secrets.cjs');
|
|
382
|
+
const grounder = path.join(scripts, 'spec-ground.cjs');
|
|
383
|
+
const finalState = path.join(scripts, 'spec-final-state.cjs');
|
|
384
|
+
const readiness = path.join(scripts, 'spec-readiness.cjs');
|
|
385
|
+
const validator = path.join(scripts, 'validate-spec-output.cjs');
|
|
307
386
|
assert.ok(fs.existsSync(policy), `installed policy missing: ${policy}`);
|
|
308
387
|
assert.ok(fs.existsSync(scanner), `installed scanner missing: ${scanner}`);
|
|
388
|
+
assert.ok(fs.existsSync(grounder), `installed validator dependency missing: ${grounder}`);
|
|
389
|
+
assert.ok(fs.existsSync(finalState), `installed final-state dependency missing: ${finalState}`);
|
|
390
|
+
assert.ok(fs.existsSync(readiness), `installed readiness finalizer missing: ${readiness}`);
|
|
391
|
+
assert.ok(fs.existsSync(validator), `installed validator missing: ${validator}`);
|
|
309
392
|
|
|
310
393
|
const policyRun = spawnSync(process.execPath, [policy, '--json'], { cwd: root, encoding: 'utf8' });
|
|
311
394
|
assert.equal(policyRun.status, 0, `${policyRun.stdout}\n${policyRun.stderr}`);
|
|
312
395
|
assert.equal(JSON.parse(policyRun.stdout).contract, 'execution-policy');
|
|
313
396
|
|
|
397
|
+
const validatorRun = spawnSync(process.execPath, [validator], { cwd: root, encoding: 'utf8' });
|
|
398
|
+
assert.equal(validatorRun.status, 2, `${validatorRun.stdout}\n${validatorRun.stderr}`);
|
|
399
|
+
assert.match(validatorRun.stderr, /Usage: .*validate-spec-output\.cjs/);
|
|
400
|
+
assert.doesNotMatch(validatorRun.stderr, /MODULE_NOT_FOUND|Cannot find module/);
|
|
401
|
+
|
|
314
402
|
const safe = path.join(root, 'safe.txt');
|
|
315
403
|
fs.writeFileSync(safe, 'mode=safe\n');
|
|
316
404
|
spawnSync('git', ['init', '-q'], { cwd: root });
|
|
@@ -320,6 +408,421 @@ function assertInstalledScripts(root, platform) {
|
|
|
320
408
|
assert.match(scannerRun.stdout, /No staged secrets found/);
|
|
321
409
|
}
|
|
322
410
|
|
|
411
|
+
function installedSemanticPaths(root, platform) {
|
|
412
|
+
const runtimeRoot = path.join(root, RUNTIMES[platform].root);
|
|
413
|
+
const skillRoot = platform === 'codex'
|
|
414
|
+
? path.join(root, '.agents', 'skills')
|
|
415
|
+
: path.join(runtimeRoot, 'skills');
|
|
416
|
+
const paths = {
|
|
417
|
+
policy: path.join(runtimeRoot, 'scripts', 'workflow-policy.cjs'),
|
|
418
|
+
scaffold: path.join(runtimeRoot, 'scripts', 'spec-scaffold.cjs'),
|
|
419
|
+
validator: path.join(runtimeRoot, 'scripts', 'validate-spec-output.cjs'),
|
|
420
|
+
authoringValidator: path.join(runtimeRoot, 'scripts', 'spec-authoring-validation.cjs'),
|
|
421
|
+
authoringDigest: path.join(runtimeRoot, 'scripts', 'spec-authoring-digest.cjs'),
|
|
422
|
+
grounder: path.join(runtimeRoot, 'scripts', 'spec-ground.cjs'),
|
|
423
|
+
finalState: path.join(runtimeRoot, 'scripts', 'spec-final-state.cjs'),
|
|
424
|
+
readiness: path.join(runtimeRoot, 'scripts', 'spec-readiness.cjs'),
|
|
425
|
+
semanticModel: path.join(runtimeRoot, 'scripts', 'spec-semantic-model.cjs'),
|
|
426
|
+
resolver: path.join(runtimeRoot, 'scripts', 'spec-resolver.cjs'),
|
|
427
|
+
provenance: path.join(runtimeRoot, 'scripts', 'provenance.cjs'),
|
|
428
|
+
completion: path.join(runtimeRoot, 'hooks', 'completion-authority-check.cjs'),
|
|
429
|
+
semanticAuthority: path.join(runtimeRoot, 'hooks', 'semantic-review-authority.cjs'),
|
|
430
|
+
stateTemplate: path.join(skillRoot, 'specs', 'templates', 'spec-state.json'),
|
|
431
|
+
};
|
|
432
|
+
for (const [name, target] of Object.entries(paths)) {
|
|
433
|
+
assert.equal(fs.existsSync(target), true, `${platform} installed ${name} missing: ${target}`);
|
|
434
|
+
assert.equal(path.relative(root, target).startsWith('..'), false, `${platform} ${name} escaped install root`);
|
|
435
|
+
}
|
|
436
|
+
return paths;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function runInstalled(script, args, root, expected = 0) {
|
|
440
|
+
const result = spawnSync(process.execPath, [script, ...args], { cwd: root, encoding: 'utf8' });
|
|
441
|
+
assert.equal(result.status, expected, `${script}\n${result.stdout}\n${result.stderr}`);
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function writeJson(target, value) {
|
|
446
|
+
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function materializeSpecState(paths, root, feature, { strict = false } = {}) {
|
|
450
|
+
const featureDir = path.join(root, 'specs', feature);
|
|
451
|
+
runInstalled(paths.scaffold, [feature], root);
|
|
452
|
+
const state = JSON.parse(fs.readFileSync(path.join(featureDir, 'spec.json'), 'utf8'));
|
|
453
|
+
state.scope_lock.in_scope = [`${feature} behavior`];
|
|
454
|
+
state.scope_lock.out_of_scope = ['unrelated behavior'];
|
|
455
|
+
if (strict) {
|
|
456
|
+
state.workflow_policy.assurance_level = 'Strict';
|
|
457
|
+
state.workflow_policy.classified_minimum.assurance_level = 'Strict';
|
|
458
|
+
state.workflow_policy.risks = ['auth'];
|
|
459
|
+
}
|
|
460
|
+
writeJson(path.join(featureDir, 'spec.json'), state);
|
|
461
|
+
return { featureDir, specFile: path.join(featureDir, 'spec.json'), state };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function requirementsProjection({ proof = false } = {}) {
|
|
465
|
+
return `# Requirements
|
|
466
|
+
|
|
467
|
+
## Requirements
|
|
468
|
+
|
|
469
|
+
### Requirement 1: Installed behavior
|
|
470
|
+
|
|
471
|
+
- **R1.1**: The CommonJS entrypoint \`src/entry.js\` shall return an object containing only status \`enabled\` when its sole \`valid\` argument is the boolean \`true\`; it shall return an object containing only status \`rejected\` for \`false\`, a missing argument, or any non-boolean value.${proof ? '\n- **R1.2**: The installed verifier shall prove the observed entry behavior through its grounded evidence boundary.' : ''}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const TYPED_ANCHOR_COLUMNS = ['ID', 'Type', 'Target', 'Role', 'Access', 'Action'];
|
|
476
|
+
|
|
477
|
+
function typedAnchorTable(rows) {
|
|
478
|
+
assert.ok(Array.isArray(rows) && rows.length > 0, 'typed anchor projection requires rows');
|
|
479
|
+
const header = `| ${TYPED_ANCHOR_COLUMNS.join(' | ')} |`;
|
|
480
|
+
const separator = `|${TYPED_ANCHOR_COLUMNS.map(() => '---').join('|')}|`;
|
|
481
|
+
const body = rows.map((row) => {
|
|
482
|
+
assert.deepEqual(Object.keys(row), ['id', 'type', 'target', 'role', 'access', 'action']);
|
|
483
|
+
assert.ok(Object.values(row).every((value) => typeof value === 'string' && value.trim() !== ''));
|
|
484
|
+
return `| ${row.id} | ${row.type} | \`${row.target}\` | ${row.role} | ${row.access} | ${row.action} |`;
|
|
485
|
+
});
|
|
486
|
+
return [header, separator, ...body].join('\n');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function designProjection({
|
|
490
|
+
expected = 'exit 0; boolean true returns the exact enabled object; false, undefined, null, `"true"`, 1, and an object return the exact rejected object',
|
|
491
|
+
taskful = false,
|
|
492
|
+
} = {}) {
|
|
493
|
+
const anchors = typedAnchorTable([{
|
|
494
|
+
id: 'A-D-01', type: 'file', target: taskful ? 'src/design-boundary.js' : 'src/entry.js',
|
|
495
|
+
role: taskful ? 'existing design boundary for the entrypoint' : 'existing runtime entrypoint and contract owner',
|
|
496
|
+
access: 'read', action: 'read',
|
|
497
|
+
}]);
|
|
498
|
+
const reachabilityAnchors = taskful ? 'A-R1-01-01, A-R1-02-02' : 'A-D-01';
|
|
499
|
+
return `# Design
|
|
500
|
+
|
|
501
|
+
## Boundary
|
|
502
|
+
|
|
503
|
+
- **Owns:** The exact return contract of the CommonJS function exported by \`src/entry.js\`.
|
|
504
|
+
- **Reads:** One \`valid\` argument; only the boolean \`true\` is valid.
|
|
505
|
+
- **Writes/exposes:** Exactly one status field whose value is \`enabled\` or \`rejected\`.
|
|
506
|
+
- **Outside boundary:** Unrelated runtime behavior.
|
|
507
|
+
|
|
508
|
+
## Typed Anchors
|
|
509
|
+
|
|
510
|
+
${anchors}
|
|
511
|
+
|
|
512
|
+
## Decisions and Invariants
|
|
513
|
+
|
|
514
|
+
### D1 — Installed result decision
|
|
515
|
+
|
|
516
|
+
- **Decision:** \`valid === true\` returns an object containing only status \`enabled\`; every other value returns an object containing only status \`rejected\`.
|
|
517
|
+
- **Rejects ambiguity:** Truthy non-booleans such as \`"true"\` never count as valid.
|
|
518
|
+
- **Negative path:** \`false\`, \`undefined\`, \`null\`, strings, numbers, and objects return rejected.
|
|
519
|
+
- **Anchors:** A-D-01
|
|
520
|
+
|
|
521
|
+
### I1 — Rejection invariant
|
|
522
|
+
|
|
523
|
+
Any input other than the boolean \`true\` never returns enabled.
|
|
524
|
+
|
|
525
|
+
### C1 — Result contract
|
|
526
|
+
|
|
527
|
+
- **Owner:** A-D-01
|
|
528
|
+
- **Consumers:** \`test/entry.test.js\` and callers of the CommonJS export.
|
|
529
|
+
- **Shape/behavior:** The result is an object with exactly one \`status\` field set to \`enabled\` or \`rejected\`.
|
|
530
|
+
- **Compatibility:** The strict boolean discriminator, exact object shape, and two status values remain stable.
|
|
531
|
+
|
|
532
|
+
## Verification Definitions
|
|
533
|
+
|
|
534
|
+
- **V1**: Criteria R1.1; Owner ${taskful ? 'R1-01' : 'A-D-01'}; ${taskful ? 'Proof criteria R1.2; Proof owner R1-02; Evidence anchor A-R1-02-02; ' : ''}Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected ${expected}; Negative/failure \`false\`, missing input, \`null\`, and truthy non-booleans all return the exact rejected object; Reachability/grounding entrypoint \`src/entry.js\` via ${reachabilityAnchors}.
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function completeSemanticReview(paths, root, fixture, counterexamples) {
|
|
539
|
+
// C16/D13: only the installed spec-authoring-validation.cjs coordinator may
|
|
540
|
+
// flip authoring.* to validated and write the matching receipt (I21/R3.9) —
|
|
541
|
+
// run it over the fixture's current bytes instead of hand-writing the enum.
|
|
542
|
+
// Lifecycle order (I15/R3.7): authoring -> coordinator -> semantic review ->
|
|
543
|
+
// readiness, never the reverse.
|
|
544
|
+
runInstalled(paths.scaffold, [fixture.state.feature_name, '--sync-semantic-model'], root);
|
|
545
|
+
runInstalled(paths.authoringValidator, [fixture.featureDir], root);
|
|
546
|
+
const state = JSON.parse(fs.readFileSync(fixture.specFile, 'utf8'));
|
|
547
|
+
fixture.reviewResult = {
|
|
548
|
+
verdict: 'PASS',
|
|
549
|
+
findings: [],
|
|
550
|
+
unresolved_decisions: [],
|
|
551
|
+
graph_coverage: [
|
|
552
|
+
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
553
|
+
'assumption_provenance', 'compatibility_migration',
|
|
554
|
+
].map((surface) => ({
|
|
555
|
+
surface, covered: true,
|
|
556
|
+
notes: 'The review covers this semantic surface against the canonical model.',
|
|
557
|
+
})),
|
|
558
|
+
reviewed_criteria: counterexamples.map(({ criterion }) => criterion),
|
|
559
|
+
counterexamples,
|
|
560
|
+
reviewer_evidence: null,
|
|
561
|
+
};
|
|
562
|
+
if (state.workflow_policy.assurance_level === 'Strict') {
|
|
563
|
+
const digest = runInstalled(paths.validator, [fixture.featureDir, '--semantic-digest'], root).stdout.trim();
|
|
564
|
+
assert.match(digest, /^sha256:[a-f0-9]{64}$/);
|
|
565
|
+
return digest;
|
|
566
|
+
}
|
|
567
|
+
const reviewFile = path.join(fixture.featureDir, '.review-result.json');
|
|
568
|
+
writeJson(reviewFile, fixture.reviewResult);
|
|
569
|
+
runInstalled(paths.readiness, [fixture.featureDir, '--review-result', reviewFile], root);
|
|
570
|
+
fs.unlinkSync(reviewFile);
|
|
571
|
+
runInstalled(paths.validator, [fixture.featureDir], root);
|
|
572
|
+
runInstalled(paths.grounder, [fixture.featureDir, '--root', root], root);
|
|
573
|
+
return JSON.parse(fs.readFileSync(fixture.specFile, 'utf8')).validation.semantic_review.semantic_digest;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function createTasklessFixture(paths, root, feature, options = {}) {
|
|
577
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
578
|
+
fs.mkdirSync(path.join(root, 'test'), { recursive: true });
|
|
579
|
+
fs.writeFileSync(path.join(root, 'src', 'entry.js'), 'module.exports = (valid) => ({ status: valid === true ? "enabled" : "rejected" });\n');
|
|
580
|
+
fs.writeFileSync(path.join(root, 'src', 'design-boundary.js'), 'module.exports = { entry: "src/entry.js" };\n');
|
|
581
|
+
fs.writeFileSync(path.join(root, 'test', 'entry.test.js'), `const test = require('node:test');
|
|
582
|
+
const assert = require('node:assert/strict');
|
|
583
|
+
const entry = require('../src/entry.js');
|
|
584
|
+
|
|
585
|
+
test('entry accepts only the boolean true discriminator', () => {
|
|
586
|
+
assert.deepEqual(entry(true), { status: 'enabled' });
|
|
587
|
+
for (const invalid of [false, undefined, null, 'true', 1, {}]) {
|
|
588
|
+
assert.deepEqual(entry(invalid), { status: 'rejected' });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
`);
|
|
592
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ scripts: { test: 'node --test' } }));
|
|
593
|
+
const fixture = materializeSpecState(paths, root, feature, options);
|
|
594
|
+
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection());
|
|
595
|
+
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection());
|
|
596
|
+
fixture.digest = completeSemanticReview(paths, root, fixture, [{
|
|
597
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
598
|
+
scenario: 'The installed entry receives input without the required discriminator.',
|
|
599
|
+
expected: 'The installed entry returns rejected and never returns enabled.',
|
|
600
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
601
|
+
}]);
|
|
602
|
+
return fixture;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function taskProjection({ id, title, ownedPath, criterion, verificationRef, role, artifact = false }) {
|
|
606
|
+
const anchors = typedAnchorTable([
|
|
607
|
+
{ id: `A-${id}-01`, type: 'file', target: ownedPath, role: 'owner', access: 'write', action: 'modify' },
|
|
608
|
+
...(artifact ? [{
|
|
609
|
+
id: `A-${id}-02`, type: 'artifact', target: `artifacts/${id}.json`,
|
|
610
|
+
role: 'verifier', access: 'write', action: 'create',
|
|
611
|
+
}] : []),
|
|
612
|
+
{
|
|
613
|
+
id: `A-${id}-${artifact ? '03' : '02'}`, type: 'command',
|
|
614
|
+
target: 'node --test test/entry.test.js', role: 'verifier', access: 'read', action: 'read',
|
|
615
|
+
},
|
|
616
|
+
]);
|
|
617
|
+
return `# Task ${id}: ${title}
|
|
618
|
+
**Status:** pending
|
|
619
|
+
|
|
620
|
+
## Outcome
|
|
621
|
+
|
|
622
|
+
Deliver observable ${title.toLowerCase()} behavior through the installed entrypoint.
|
|
623
|
+
|
|
624
|
+
## Scope
|
|
625
|
+
|
|
626
|
+
- **In scope:** Exact behavior owned at ${ownedPath}.
|
|
627
|
+
- **Out of scope:** Unrelated runtime behavior.
|
|
628
|
+
|
|
629
|
+
## Anchors and Ownership
|
|
630
|
+
|
|
631
|
+
${anchors}
|
|
632
|
+
|
|
633
|
+
## Changes
|
|
634
|
+
|
|
635
|
+
- [ ] Implement the exact owned behavior. _Requirements: ${criterion.slice(1)}_
|
|
636
|
+
|
|
637
|
+
## Acceptance
|
|
638
|
+
|
|
639
|
+
- **${criterion}:** The installed command returns the criterion-specific observable state.
|
|
640
|
+
|
|
641
|
+
## Dependencies
|
|
642
|
+
|
|
643
|
+
- none
|
|
644
|
+
|
|
645
|
+
## Verification Plan
|
|
646
|
+
|
|
647
|
+
- **Verification ref:** ${verificationRef}
|
|
648
|
+
- **Task role:** ${role}
|
|
649
|
+
- **Command:** \`node --test test/entry.test.js\`
|
|
650
|
+
- **Expected:** Exit code 0 and the criterion-specific state is observed.
|
|
651
|
+
- **Negative path:** Invalid input returns the named rejected state.
|
|
652
|
+
- **Reachability:** \`src/entry.js\` is reached by the installed test command.
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function createTaskFixture(paths, root, feature) {
|
|
657
|
+
fs.mkdirSync(path.join(root, 'artifacts'), { recursive: true });
|
|
658
|
+
const fixture = materializeSpecState(paths, root, feature);
|
|
659
|
+
fs.writeFileSync(path.join(fixture.featureDir, 'requirements.md'), requirementsProjection({ proof: true }));
|
|
660
|
+
fs.writeFileSync(path.join(fixture.featureDir, 'design.md'), designProjection({
|
|
661
|
+
expected: 'exit 0, status enabled, and verifier artifact `artifacts/R1-02.json`',
|
|
662
|
+
taskful: true,
|
|
663
|
+
}));
|
|
664
|
+
const taskOne = 'tasks/task-R1-01-implement.md';
|
|
665
|
+
const taskTwo = 'tasks/task-R1-02-verify.md';
|
|
666
|
+
fs.mkdirSync(path.join(fixture.featureDir, 'tasks'), { recursive: true });
|
|
667
|
+
fs.writeFileSync(path.join(fixture.featureDir, taskOne), taskProjection({
|
|
668
|
+
id: 'R1-01', title: 'Implement behavior', ownedPath: 'src/entry.js', criterion: 'R1.1',
|
|
669
|
+
verificationRef: 'V1', role: 'subject implements R1.1',
|
|
670
|
+
}));
|
|
671
|
+
fs.writeFileSync(path.join(fixture.featureDir, taskTwo), taskProjection({
|
|
672
|
+
id: 'R1-02', title: 'Verify behavior', ownedPath: 'test/entry.test.js', criterion: 'R1.2',
|
|
673
|
+
verificationRef: 'V1', role: 'verifier verifies V1 through separately owned R1.2 proof', artifact: true,
|
|
674
|
+
}));
|
|
675
|
+
const state = fixture.state;
|
|
676
|
+
state.authoring.tasks = 'draft';
|
|
677
|
+
state.task_files = [taskOne, taskTwo];
|
|
678
|
+
state.task_registry = Object.fromEntries([
|
|
679
|
+
[taskOne, { id: 'R1-01', title: 'Implement behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
|
|
680
|
+
[taskTwo, { id: 'R1-02', title: 'Verify behavior', status: 'pending', dependencies: [], blocker: null, started_at: null, completed_at: null, last_updated_at: null }],
|
|
681
|
+
]);
|
|
682
|
+
state.coordination.boundaries = [{
|
|
683
|
+
id: 'B-OWN', type: 'ownership', tasks: ['R1-01', 'R1-02'],
|
|
684
|
+
write_sets: { 'R1-01': ['src/entry.js'], 'R1-02': ['test/entry.test.js', 'artifacts/R1-02.json'] },
|
|
685
|
+
}, {
|
|
686
|
+
id: 'B-PROOF', type: 'proof', subject: 'R1-01', verifier: 'R1-02',
|
|
687
|
+
verification_ref: 'V1', artifact_anchor: 'A-R1-02-02',
|
|
688
|
+
}];
|
|
689
|
+
writeJson(fixture.specFile, state);
|
|
690
|
+
fixture.digest = completeSemanticReview(paths, root, fixture, [{
|
|
691
|
+
criterion: 'R1.1', case_kind: 'failure',
|
|
692
|
+
scenario: 'Invalid input reaches the installed implementation boundary.',
|
|
693
|
+
expected: 'The implementation returns rejected and never reports enabled.',
|
|
694
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
695
|
+
}, {
|
|
696
|
+
criterion: 'R1.2', case_kind: 'failure',
|
|
697
|
+
scenario: 'The verification command completes without producing its proof artifact.',
|
|
698
|
+
expected: 'The verification criterion remains unsatisfied until the artifact exists.',
|
|
699
|
+
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
700
|
+
}]);
|
|
701
|
+
return fixture;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function initializeGit(root) {
|
|
705
|
+
fs.writeFileSync(path.join(root, '.gitignore'), `${'node_' + 'modules'}/\n`);
|
|
706
|
+
for (const args of [
|
|
707
|
+
['init', '-q'], ['config', 'user.name', 'CafeKit Packed E2E'],
|
|
708
|
+
['config', 'user.email', 'packed-e2e@example.invalid'],
|
|
709
|
+
['add', '--', '.gitignore', 'src/entry.js', 'src/design-boundary.js', 'test/entry.test.js', 'package.json'],
|
|
710
|
+
['commit', '--no-gpg-sign', '-qm', 'packed semantic fixture'],
|
|
711
|
+
]) {
|
|
712
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
713
|
+
assert.equal(result.status, 0, `${args.join(' ')}\n${result.stdout}\n${result.stderr}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function assertInstalledCompletion(paths, root, platform, fixture) {
|
|
718
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-completion-home-'));
|
|
719
|
+
const code = `
|
|
720
|
+
const fs = require('fs');
|
|
721
|
+
const path = require('path');
|
|
722
|
+
const policy = require(process.argv[1]);
|
|
723
|
+
const provenance = require(process.argv[2]);
|
|
724
|
+
const checker = require(process.argv[3]);
|
|
725
|
+
const resolver = require(process.argv[4]);
|
|
726
|
+
const root = process.argv[5];
|
|
727
|
+
const feature = process.argv[6];
|
|
728
|
+
const specFile = path.join(root, 'specs', feature, 'spec.json');
|
|
729
|
+
const state = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
730
|
+
state.status = 'done';
|
|
731
|
+
fs.writeFileSync(specFile, JSON.stringify(state, null, 2) + '\\n');
|
|
732
|
+
const context = provenance.deriveRuntimeContext({ projectRoot: root, specsRoot: path.join(root, 'specs'), specFile, featureName: feature, runtimeSession: 'packed-session' });
|
|
733
|
+
fs.writeFileSync(path.join(root, 'specs', feature, 'feature-receipt.md'), ['Verification: PASS','Command: node --test','Exit: 0','Result: PASS','Expected: installed behavior passes','Observed: installed behavior passed','Base: ' + context.base,'Head: ' + context.head,'Feature: ' + feature].join('\\n') + '\\n');
|
|
734
|
+
const result = checker.evaluateCloseout({ resolver, policy, projectRoot: root, runtime: {}, payload: { session_id: 'packed-session', featureName: feature } });
|
|
735
|
+
process.stdout.write(JSON.stringify({ ok: result.ok, active: result.active, reason: result.reason || null, feature: result.candidate && result.candidate.featureName }));`;
|
|
736
|
+
const result = spawnSync(process.execPath, ['-e', code, paths.policy, paths.provenance, paths.completion, paths.resolver, root, fixture.state.feature_name], {
|
|
737
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, HOME: home },
|
|
738
|
+
});
|
|
739
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
740
|
+
assert.equal(result.status, 0, `${platform}\n${result.stdout}\n${result.stderr}`);
|
|
741
|
+
assert.deepEqual(JSON.parse(result.stdout), {
|
|
742
|
+
ok: true, active: true, reason: null, feature: fixture.state.feature_name,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function assertInstalledResolution(paths, root, platform, explicitFeature) {
|
|
747
|
+
const code = `
|
|
748
|
+
const checker = require(process.argv[1]); const resolver = require(process.argv[2]);
|
|
749
|
+
const root = process.argv[3]; const platform = process.argv[4]; const feature = process.argv[5];
|
|
750
|
+
const base = { projectRoot: root, runtime: {}, payload: {} };
|
|
751
|
+
if (platform === 'claude') base.resolver = resolver;
|
|
752
|
+
const explicit = checker.resolveCandidate({ ...base, payload: { featureName: feature } });
|
|
753
|
+
const ambiguous = checker.resolveCandidate(base);
|
|
754
|
+
process.stdout.write(JSON.stringify({ explicit: explicit && explicit.featureName, error: ambiguous && ambiguous.error, candidates: ambiguous && ambiguous.candidates }));`;
|
|
755
|
+
const result = spawnSync(process.execPath, ['-e', code, paths.completion, paths.resolver, root, platform, explicitFeature], { cwd: root, encoding: 'utf8' });
|
|
756
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
757
|
+
const resolution = JSON.parse(result.stdout);
|
|
758
|
+
assert.equal(resolution.explicit, explicitFeature);
|
|
759
|
+
assert.equal(resolution.error, 'multiple_persisted');
|
|
760
|
+
assert.ok(resolution.candidates.length >= 2);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function assertStrictSimulatedHandlerGuardrail(paths, root, fixture) {
|
|
764
|
+
// Simulated SubagentStop handler chain — NOT a live Codex host E2E.
|
|
765
|
+
// This exercises the installed hook's handler logic via direct spawn,
|
|
766
|
+
// not a real Codex binary dispatching SubagentStop. For live host,
|
|
767
|
+
// see the opt-in CAFEKIT_CODEX_HOST_E2E test below.
|
|
768
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-strict-home-'));
|
|
769
|
+
const code = `
|
|
770
|
+
const authority = require(process.argv[1]); const readiness = require(process.argv[2]); const root = process.argv[3]; const feature = process.argv[4]; const digest = process.argv[5]; const review = JSON.parse(process.argv[6]);
|
|
771
|
+
const spec_file = 'specs/' + feature + '/spec.json';
|
|
772
|
+
const marker = authority.MARKER + JSON.stringify({ feature_name: feature, spec_file, semantic_digest: digest, verdict: 'PASS' });
|
|
773
|
+
const before = authority.verifyAttestation(root, spec_file, feature, digest);
|
|
774
|
+
const {spawnSync} = require('child_process');
|
|
775
|
+
const authorityPath = process.argv[1];
|
|
776
|
+
function handlerInvoke(payload) {
|
|
777
|
+
return spawnSync(process.execPath, [authorityPath], {cwd: root, input: JSON.stringify(payload), encoding:'utf8', env: {...process.env, HOME: process.env.HOME, USERPROFILE: process.env.HOME, PROJECT_ROOT: root}});
|
|
778
|
+
}
|
|
779
|
+
const forgedPayload = {cwd: root, hook_event_name: 'SubagentStop', session_id: 'self-session', agent_id: 'self', agent_type: 'spec-maker', last_assistant_message: marker};
|
|
780
|
+
const forgedRes = handlerInvoke(forgedPayload);
|
|
781
|
+
const afterForged = authority.verifyAttestation(root, spec_file, feature, digest);
|
|
782
|
+
const observedPayload = {cwd: root, hook_event_name: 'SubagentStop', session_id: 'host-session', agent_id: 'reviewer-1', agent_type: 'code-auditor', last_assistant_message: marker};
|
|
783
|
+
const observedRes = handlerInvoke(observedPayload);
|
|
784
|
+
const afterObserved = authority.verifyAttestation(root, spec_file, feature, digest);
|
|
785
|
+
const finalized = readiness.finalizeReadiness({ specDir: require('path').join(root, 'specs', feature), projectRoot: root, reviewResult: review });
|
|
786
|
+
process.stdout.write(JSON.stringify({ before: before.ok, forged: afterForged.ok, afterForged: afterForged.ok, observed: afterObserved.ok, afterObserved: afterObserved.ok, ready: finalized.spec.ready_for_implementation, forgedStderr: forgedRes.stderr.trim(), observedStderr: observedRes.stderr.trim() }));`;
|
|
787
|
+
const result = spawnSync(process.execPath, ['-e', code, paths.semanticAuthority, paths.readiness, root, fixture.state.feature_name, fixture.digest, JSON.stringify(fixture.reviewResult)], {
|
|
788
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, HOME: home, PROJECT_ROOT: root },
|
|
789
|
+
});
|
|
790
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
791
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
792
|
+
const out = JSON.parse(result.stdout);
|
|
793
|
+
// Simulated handler: forged self-attestation via spec-maker must NOT create observation, code-auditor must.
|
|
794
|
+
assert.equal(out.before, false, 'strict attestation must be missing before simulated handler observation');
|
|
795
|
+
assert.equal(out.afterForged, false, 'forged self-attestation must NOT create simulated handler observation');
|
|
796
|
+
assert.equal(out.afterObserved, true, 'code-auditor simulated handler must create observation');
|
|
797
|
+
assert.equal(out.ready, true, 'SPEC_READY after simulated handler observation');
|
|
798
|
+
assert.ok(out.forgedStderr.includes('rejected') || out.forgedStderr === '', 'forged handler invoke should be rejected or silent but not create observation');
|
|
799
|
+
assert.equal(out.observedStderr, '', 'code-auditor simulated handler observation should be silent');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function assertStaleDigestMutations(paths, root, taskless, taskBearing) {
|
|
803
|
+
const requirementFile = path.join(taskless.featureDir, 'requirements.md');
|
|
804
|
+
const requirementBytes = fs.readFileSync(requirementFile);
|
|
805
|
+
fs.appendFileSync(requirementFile, '\nThe installed Markdown meaning changed.\n');
|
|
806
|
+
assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
807
|
+
fs.writeFileSync(requirementFile, requirementBytes);
|
|
808
|
+
|
|
809
|
+
const tasklessState = JSON.parse(fs.readFileSync(taskless.specFile, 'utf8'));
|
|
810
|
+
const originalPolicy = JSON.parse(JSON.stringify(tasklessState.workflow_policy));
|
|
811
|
+
tasklessState.workflow_policy.planning_depth = 'Full';
|
|
812
|
+
writeJson(taskless.specFile, tasklessState);
|
|
813
|
+
assert.match(runInstalled(paths.validator, [taskless.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
814
|
+
tasklessState.workflow_policy = originalPolicy;
|
|
815
|
+
writeJson(taskless.specFile, tasklessState);
|
|
816
|
+
|
|
817
|
+
const taskState = JSON.parse(fs.readFileSync(taskBearing.specFile, 'utf8'));
|
|
818
|
+
const originalId = taskState.coordination.boundaries[0].id;
|
|
819
|
+
taskState.coordination.boundaries[0].id = 'B-OWN-MUTATED';
|
|
820
|
+
writeJson(taskBearing.specFile, taskState);
|
|
821
|
+
assert.match(runInstalled(paths.validator, [taskBearing.featureDir], root, 1).stderr, /semantic_digest: stale/);
|
|
822
|
+
taskState.coordination.boundaries[0].id = originalId;
|
|
823
|
+
writeJson(taskBearing.specFile, taskState);
|
|
824
|
+
}
|
|
825
|
+
|
|
323
826
|
function assertTransforms(root, platform) {
|
|
324
827
|
const skill = path.join(root, RUNTIMES[platform].root === '.codex' ? '.agents/skills' : `${RUNTIMES[platform].root}/skills`, 'develop', 'SKILL.md');
|
|
325
828
|
const content = fs.readFileSync(skill, 'utf8');
|
|
@@ -348,6 +851,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
348
851
|
try {
|
|
349
852
|
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
350
853
|
const tarball = path.join(destination, packed.filename);
|
|
854
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
351
855
|
assertCleanInventory(packedInventory(tarball));
|
|
352
856
|
|
|
353
857
|
const cases = [
|
|
@@ -360,7 +864,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
360
864
|
for (const matrixCase of cases) {
|
|
361
865
|
for (const lang of [null, 'vi']) {
|
|
362
866
|
const project = path.join(root, `${matrixCase.name}-${lang ? 'lang-vi' : 'no-lang'}`);
|
|
363
|
-
const installer = installPacked(tarball, project);
|
|
867
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
364
868
|
runInstaller(installer, project, matrixCase.platforms, lang);
|
|
365
869
|
for (const platform of matrixCase.platforms) {
|
|
366
870
|
assertHookLanguage(project, platform, lang, `${project}-${matrixCase.name}-${lang || 'none'}`);
|
|
@@ -388,6 +892,43 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
388
892
|
}
|
|
389
893
|
});
|
|
390
894
|
|
|
895
|
+
test('packed Claude and Codex installs execute semantic kernel behavior without package source', () => {
|
|
896
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-packed-semantic-'));
|
|
897
|
+
const destination = path.join(root, 'pack');
|
|
898
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
899
|
+
try {
|
|
900
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
901
|
+
const tarball = path.join(destination, packed.filename);
|
|
902
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
903
|
+
assertCleanInventory(packedInventory(tarball));
|
|
904
|
+
|
|
905
|
+
for (const platform of ['claude', 'codex']) {
|
|
906
|
+
const project = path.join(root, platform);
|
|
907
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
908
|
+
runInstaller(installer, project, [platform], null);
|
|
909
|
+
const packedSource = path.resolve(path.dirname(installer), '..', 'src');
|
|
910
|
+
fs.rmSync(packedSource, { recursive: true, force: true });
|
|
911
|
+
assert.equal(fs.existsSync(packedSource), false, `${platform} package source must be unavailable`);
|
|
912
|
+
|
|
913
|
+
const paths = installedSemanticPaths(project, platform);
|
|
914
|
+
const taskless = createTasklessFixture(paths, project, 'compact-installed');
|
|
915
|
+
initializeGit(project);
|
|
916
|
+
assertInstalledCompletion(paths, project, platform, taskless);
|
|
917
|
+
|
|
918
|
+
const strict = createTasklessFixture(paths, project, 'strict-installed', { strict: true });
|
|
919
|
+
const taskBearing = createTaskFixture(paths, project, 'tasks-installed');
|
|
920
|
+
assertInstalledResolution(paths, project, platform, 'strict-installed');
|
|
921
|
+
assertStrictSimulatedHandlerGuardrail(paths, project, strict);
|
|
922
|
+
assertStaleDigestMutations(paths, project, taskless, taskBearing);
|
|
923
|
+
runInstalled(paths.validator, [taskless.featureDir], project);
|
|
924
|
+
runInstalled(paths.validator, [taskBearing.featureDir], project);
|
|
925
|
+
runInstalled(paths.grounder, [taskBearing.featureDir, '--root', project], project);
|
|
926
|
+
}
|
|
927
|
+
} finally {
|
|
928
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
391
932
|
test('packed Claude and Codex installs self-contain runtime provenance and fail closed without it', () => {
|
|
392
933
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-provenance-package-'));
|
|
393
934
|
const destination = path.join(root, 'pack');
|
|
@@ -395,11 +936,12 @@ test('packed Claude and Codex installs self-contain runtime provenance and fail
|
|
|
395
936
|
try {
|
|
396
937
|
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
397
938
|
const tarball = path.join(destination, packed.filename);
|
|
939
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
398
940
|
assertCleanInventory(packedInventory(tarball));
|
|
399
941
|
|
|
400
942
|
for (const platform of ['claude', 'codex']) {
|
|
401
943
|
const project = path.join(root, platform);
|
|
402
|
-
const installer = installPacked(tarball, project);
|
|
944
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
403
945
|
runInstaller(installer, project, [platform], null);
|
|
404
946
|
|
|
405
947
|
const packedSource = path.resolve(path.dirname(installer), '..', 'src');
|
|
@@ -413,3 +955,153 @@ test('packed Claude and Codex installs self-contain runtime provenance and fail
|
|
|
413
955
|
fs.rmSync(root, { recursive: true, force: true });
|
|
414
956
|
}
|
|
415
957
|
});
|
|
958
|
+
|
|
959
|
+
test('packed Codex live host E2E via codex binary (opt-in)', async (t) => {
|
|
960
|
+
if (process.env.CAFEKIT_CODEX_HOST_E2E !== '1') {
|
|
961
|
+
t.skip('opt-in only: set CAFEKIT_CODEX_HOST_E2E=1 to run live Codex host');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-live-codex-'));
|
|
965
|
+
const destination = path.join(root, 'pack');
|
|
966
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
967
|
+
const cafekitHome = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-live-home-'));
|
|
968
|
+
const originalHome = process.env.HOME;
|
|
969
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
970
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
971
|
+
// Preserve original home for Codex auth (without inspecting contents)
|
|
972
|
+
const authHome = originalCodexHome || (originalHome ? path.join(originalHome, '.codex') : null);
|
|
973
|
+
let project;
|
|
974
|
+
try {
|
|
975
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
976
|
+
const tarball = path.join(destination, packed.filename);
|
|
977
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
978
|
+
project = path.join(root, 'codex-live');
|
|
979
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
980
|
+
runInstaller(installer, project, ['codex'], null);
|
|
981
|
+
const paths = installedSemanticPaths(project, 'codex');
|
|
982
|
+
const fixture = createTasklessFixture(paths, project, 'live-strict', { strict: true });
|
|
983
|
+
initializeGit(project);
|
|
984
|
+
// Verify via child process to keep CafeKit state isolated without mutating global env
|
|
985
|
+
const verifyViaChild = (digest) => {
|
|
986
|
+
const script = 'const a=require(process.argv[1]); console.log(JSON.stringify(a.verifyAttestation(process.argv[2],process.argv[3],process.argv[4],process.argv[5])))';
|
|
987
|
+
const res = spawnSync(process.execPath, ['-e', script, paths.semanticAuthority, project, path.join(project, 'specs', 'live-strict', 'spec.json'), 'live-strict', digest], {
|
|
988
|
+
encoding: 'utf8',
|
|
989
|
+
env: { ...process.env, HOME: cafekitHome, USERPROFILE: cafekitHome },
|
|
990
|
+
});
|
|
991
|
+
assert.equal(res.status, 0, `verify child failed: ${res.stderr}`);
|
|
992
|
+
return JSON.parse(res.stdout);
|
|
993
|
+
};
|
|
994
|
+
const before = verifyViaChild(fixture.digest);
|
|
995
|
+
assert.equal(before.ok, false, 'before host observation, Strict attestation must be missing (fail-closed)');
|
|
996
|
+
|
|
997
|
+
const model = process.env.CAFEKIT_CODEX_MODEL || 'gpt-5.6-luna';
|
|
998
|
+
const reasoning = process.env.CAFEKIT_CODEX_REASONING || 'max';
|
|
999
|
+
const codexBin = process.env.CAFEKIT_CODEX_BIN || 'codex';
|
|
1000
|
+
const canonicalProject = fs.realpathSync(project);
|
|
1001
|
+
const codeAuditorConfig = path.join(canonicalProject, '.codex', 'agents', 'code_auditor.toml');
|
|
1002
|
+
const semanticAuthorityHook = fs.realpathSync(paths.semanticAuthority);
|
|
1003
|
+
const shellQuote = (value) => `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
1004
|
+
const posixHookCommand = `${shellQuote(process.execPath)} ${shellQuote(semanticAuthorityHook)}`;
|
|
1005
|
+
const encodedHookPath = Buffer.from(semanticAuthorityHook, 'utf8').toString('base64url');
|
|
1006
|
+
const windowsHookCommand = 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encodedHookPath;
|
|
1007
|
+
const subagentStopHooks = `[{ matcher = "*", hooks = [{ type = "command", command = ${JSON.stringify(posixHookCommand)}, commandWindows = ${JSON.stringify(windowsHookCommand)} }] }]`;
|
|
1008
|
+
assert.equal(fs.existsSync(codeAuditorConfig), true, 'packed install must contain code_auditor config');
|
|
1009
|
+
const versionCheck = spawnSync(codexBin, ['--version'], { encoding: 'utf8' });
|
|
1010
|
+
if (versionCheck.status !== 0) {
|
|
1011
|
+
assert.fail(`Codex binary not available at ${codexBin}: ${versionCheck.stderr.trim()} (exit ${versionCheck.status})`);
|
|
1012
|
+
}
|
|
1013
|
+
const controllerPrompt = [
|
|
1014
|
+
'You are a CafeKit controller. Do NOT emit CAFEKIT_SEMANTIC_REVIEW_ATTESTATION yourself.',
|
|
1015
|
+
`Spawn exactly one subagent with agent_type="code_auditor" to perform Strict semantic review for feature live-strict.`,
|
|
1016
|
+
'Wait for that exact subagent to finish by calling wait_agent before you answer.',
|
|
1017
|
+
'This is a specs-only authoring review: assess requirements, design, semantic model, and counterexample coverage only. Do not require implementation code, execution receipts, or a Develop run.',
|
|
1018
|
+
'The request has assurance_level: Strict. Keep the review bounded to this fixture and do not invoke a skill.',
|
|
1019
|
+
'This is intentionally a pre-finalization candidate: semantic_model is null and semantic_review is not-run until a PASS marker is observed and spec-readiness atomically promotes them. Do not run or require the full validator before that host event because it must fail closed at this stage.',
|
|
1020
|
+
`The subagent must recompute the digest via: node .codex/scripts/validate-spec-output.cjs specs/live-strict --semantic-digest`,
|
|
1021
|
+
`The exact review result that will be passed to the finalizer is ${JSON.stringify(fixture.reviewResult)}. Verify every criterion, counterexample, decision ref, verification ref, and artifact statement before PASS.`,
|
|
1022
|
+
`If the subagent verifies PASS and the digest matches ${fixture.digest}, it must emit exactly:`,
|
|
1023
|
+
`CAFEKIT_SEMANTIC_REVIEW_ATTESTATION {"feature_name":"live-strict","spec_file":"specs/live-strict/spec.json","semantic_digest":"${fixture.digest}","verdict":"PASS"}`,
|
|
1024
|
+
'No other marker. After the subagent, do not add any marker.',
|
|
1025
|
+
].join(' ');
|
|
1026
|
+
|
|
1027
|
+
const execArgs = [
|
|
1028
|
+
'exec',
|
|
1029
|
+
'-m', model,
|
|
1030
|
+
'-c', `model_reasoning_effort=${reasoning}`,
|
|
1031
|
+
// Project agents are intentionally skipped until a user trusts the repo.
|
|
1032
|
+
// This disposable E2E must not mutate global trust, so register the
|
|
1033
|
+
// packed profile explicitly for this process. Static packed tests cover
|
|
1034
|
+
// the normal trusted-project auto-discovery path and file schema.
|
|
1035
|
+
'-c', 'agents.code_auditor.description="CafeKit Strict semantic reviewer"',
|
|
1036
|
+
'-c', `agents.code_auditor.config_file=${JSON.stringify(codeAuditorConfig)}`,
|
|
1037
|
+
// Isolate this E2E from the user's config/hooks while retaining auth.
|
|
1038
|
+
// The process-scoped hook executes the packed entrypoint directly.
|
|
1039
|
+
'--ignore-user-config',
|
|
1040
|
+
// Only thread-spawned Codex subagents emit the supported SubagentStop
|
|
1041
|
+
// event. The legacy internal path emits spawn PostToolUse but no
|
|
1042
|
+
// hook-observable completion payload and must remain fail-closed.
|
|
1043
|
+
'--enable', 'multi_agent_v2',
|
|
1044
|
+
'-c', 'features.hooks=true',
|
|
1045
|
+
'-c', `hooks.SubagentStop=${subagentStopHooks}`,
|
|
1046
|
+
'-s', 'read-only',
|
|
1047
|
+
'--dangerously-bypass-hook-trust',
|
|
1048
|
+
'--ephemeral',
|
|
1049
|
+
'-C', canonicalProject,
|
|
1050
|
+
controllerPrompt,
|
|
1051
|
+
];
|
|
1052
|
+
const codexResult = spawnSync(codexBin, execArgs, {
|
|
1053
|
+
cwd: project,
|
|
1054
|
+
encoding: 'utf8',
|
|
1055
|
+
timeout: 600000,
|
|
1056
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
1057
|
+
env: { ...process.env, HOME: cafekitHome, USERPROFILE: cafekitHome, ...(authHome ? { CODEX_HOME: authHome } : {}) },
|
|
1058
|
+
});
|
|
1059
|
+
if (codexResult.status !== 0) {
|
|
1060
|
+
const cliOutput = `${codexResult.stderr.trim()}\n${codexResult.stdout.trim()}`.trim();
|
|
1061
|
+
const processState = [
|
|
1062
|
+
`status=${codexResult.status}`,
|
|
1063
|
+
`signal=${codexResult.signal || 'none'}`,
|
|
1064
|
+
codexResult.error ? `spawn_error=${codexResult.error.code || codexResult.error.message}` : null,
|
|
1065
|
+
].filter(Boolean).join(', ');
|
|
1066
|
+
const isEnvAuth = /authentication|unauthorized|credential|not logged in|login required|CODEX_HOME|not.*found|ENOENT/i.test(cliOutput);
|
|
1067
|
+
if (isEnvAuth) {
|
|
1068
|
+
assert.fail(`Live Codex host failed due to CLI/environment (${processState}): ${cliOutput.slice(0, 1600)}`);
|
|
1069
|
+
}
|
|
1070
|
+
assert.fail(`Live Codex host exec failed (${processState}): ${cliOutput.slice(0, 1600)}`);
|
|
1071
|
+
}
|
|
1072
|
+
// Verify via public authority API only (no HOME inspection) via child process
|
|
1073
|
+
const after = verifyViaChild(fixture.digest);
|
|
1074
|
+
const liveTrace = `${codexResult.stderr || ''}\n${codexResult.stdout || ''}`.trim();
|
|
1075
|
+
assert.equal(
|
|
1076
|
+
after.ok,
|
|
1077
|
+
true,
|
|
1078
|
+
`host observation must exist after Codex reviewer completion event (got ${after.reason})\n` +
|
|
1079
|
+
`Codex trace tail:\n${liveTrace.slice(-6000)}`,
|
|
1080
|
+
);
|
|
1081
|
+
assert.equal(after.record.reviewer_agent_type, 'code_auditor', 'observation must be from code_auditor');
|
|
1082
|
+
const reviewFile = path.join(project, 'specs', 'live-strict', '.live-review.json');
|
|
1083
|
+
fs.writeFileSync(reviewFile, JSON.stringify(fixture.reviewResult));
|
|
1084
|
+
const readinessResult = spawnSync(process.execPath, [paths.readiness, path.join(project, 'specs', 'live-strict'), '--review-result', reviewFile], {
|
|
1085
|
+
cwd: project,
|
|
1086
|
+
encoding: 'utf8',
|
|
1087
|
+
env: { ...process.env, HOME: cafekitHome },
|
|
1088
|
+
});
|
|
1089
|
+
fs.unlinkSync(reviewFile);
|
|
1090
|
+
assert.equal(readinessResult.status, 0, `readiness should succeed after host observation: ${readinessResult.stderr.trim()}`);
|
|
1091
|
+
assert.match(readinessResult.stdout, /SPEC_READY/);
|
|
1092
|
+
const specAfter = JSON.parse(fs.readFileSync(path.join(project, 'specs', 'live-strict', 'spec.json'), 'utf8'));
|
|
1093
|
+
assert.equal(specAfter.ready_for_implementation, true, 'SPEC_READY after live host observation');
|
|
1094
|
+
} finally {
|
|
1095
|
+
// Restore exact environment on all paths
|
|
1096
|
+
if (originalHome !== undefined) process.env.HOME = originalHome; else delete process.env.HOME;
|
|
1097
|
+
if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile; else delete process.env.USERPROFILE;
|
|
1098
|
+
if (originalCodexHome !== undefined) process.env.CODEX_HOME = originalCodexHome; else delete process.env.CODEX_HOME;
|
|
1099
|
+
// Clear require cache for authority to avoid polluting other tests
|
|
1100
|
+
try {
|
|
1101
|
+
const tmpPaths = path.join(root || os.tmpdir(), 'codex-live', '.codex', 'hooks', 'semantic-review-authority.cjs');
|
|
1102
|
+
delete require.cache[require.resolve(tmpPaths)];
|
|
1103
|
+
} catch {}
|
|
1104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1105
|
+
fs.rmSync(cafekitHome, { recursive: true, force: true });
|
|
1106
|
+
}
|
|
1107
|
+
});
|