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