@haposoft/cafekit 0.16.0-rc.2 → 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 +794 -21
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +1838 -112
- package/bin/__tests__/installer-safety.test.js +82 -29
- package/bin/__tests__/opencode-spec-gate.test.js +799 -0
- package/bin/__tests__/package-inventory.test.js +841 -10
- 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 +2469 -71
- 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 +153 -202
- package/src/claude/hooks/__tests__/completion-authority.test.js +953 -0
- 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 +604 -56
- package/src/claude/hooks/completion-authority-check.cjs +56 -0
- package/src/claude/hooks/completion-authority-state.cjs +527 -0
- package/src/claude/hooks/completion-authority.cjs +154 -0
- 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/session.cjs +33 -19
- package/src/claude/hooks/spec-gate.cjs +181 -139
- package/src/claude/hooks/spec-state.cjs +112 -47
- package/src/claude/migration-manifest.json +15 -2
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/provenance.cjs +315 -0
- 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 +501 -0
- package/src/claude/scripts/spec-scaffold.cjs +1945 -55
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3204 -262
- package/src/claude/scripts/workflow-policy.cjs +1888 -203
- package/src/claude/settings/settings.json +12 -0
- package/src/claude/skills/code-review/SKILL.md +94 -66
- 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 +138 -283
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +123 -148
- package/src/claude/skills/develop/references/subagent-patterns.md +42 -37
- package/src/claude/skills/specs/SKILL.md +192 -397
- 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 +46 -46
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -271
- 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 +26 -67
- package/src/claude/skills/specs/templates/task.md +30 -82
- package/src/claude/skills/sync/SKILL.md +45 -15
- package/src/claude/skills/sync/references/sync-protocols.md +34 -16
- package/src/claude/skills/test/SKILL.md +151 -167
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +63 -0
- package/src/codex/hooks/completion-authority-state.cjs +527 -0
- package/src/codex/hooks/completion-authority.cjs +106 -0
- package/src/codex/hooks/lib/hook-context.cjs +58 -4
- 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 +86 -115
- package/src/codex/hooks/lib/spec-utils.cjs +104 -27
- 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/session.cjs +2 -0
- package/src/codex/hooks/spec-gate.cjs +151 -24
- package/src/codex/hooks/spec-state.cjs +65 -12
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +16 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
- package/src/opencode/AGENTS.md +4 -2
- package/src/opencode/plugins/spec-gate.ts +296 -122
- package/src/opencode/plugins/spec-state.ts +183 -116
|
@@ -13,6 +13,13 @@ const REQUIRED_PAYLOAD = [
|
|
|
13
13
|
'src/claude/migration-manifest.json',
|
|
14
14
|
'src/claude/scripts/scan-staged-secrets.cjs',
|
|
15
15
|
'src/claude/scripts/workflow-policy.cjs',
|
|
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',
|
|
16
23
|
];
|
|
17
24
|
const FORBIDDEN_PAYLOAD = [
|
|
18
25
|
/(^|\/)\.logs(\/|$)/,
|
|
@@ -30,12 +37,18 @@ const RUNTIMES = {
|
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
function npmPack(args, cwd) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
function packedInventory(tarball) {
|
|
@@ -61,12 +74,47 @@ function assertCleanInventory(inventory) {
|
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
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) {
|
|
65
109
|
fs.mkdirSync(root, { recursive: true });
|
|
66
110
|
const result = spawnSync('npm', [
|
|
67
111
|
'install', '--offline', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
68
|
-
'--package-lock=false', '--prefix', root, tarball,
|
|
69
|
-
], {
|
|
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
|
+
});
|
|
70
118
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
71
119
|
const installer = path.join(root, 'node_modules', '@haposoft', 'cafekit', 'bin', 'install.js');
|
|
72
120
|
assert.ok(fs.existsSync(installer), 'npm install must resolve packed package bin');
|
|
@@ -84,6 +132,162 @@ function runInstaller(installer, root, platforms, lang) {
|
|
|
84
132
|
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
85
133
|
}
|
|
86
134
|
|
|
135
|
+
function createProvenanceFixture(root) {
|
|
136
|
+
const feature = 'installer-provenance';
|
|
137
|
+
const specFile = path.join('specs', feature, 'spec.json');
|
|
138
|
+
fs.mkdirSync(path.join(root, path.dirname(specFile)), { recursive: true });
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
path.join(root, specFile),
|
|
141
|
+
JSON.stringify({ feature_name: feature, status: 'in_progress' }) + '\n'
|
|
142
|
+
);
|
|
143
|
+
fs.writeFileSync(path.join(root, 'tracked-fixture.txt'), 'tracked fixture\n');
|
|
144
|
+
|
|
145
|
+
for (const args of [
|
|
146
|
+
['init', '-q'],
|
|
147
|
+
['add', '--', 'tracked-fixture.txt', specFile],
|
|
148
|
+
['-c', 'user.name=CafeKit Fixture', '-c', 'user.email=cafekit-fixture@example.invalid', 'commit', '--no-gpg-sign', '-qm', 'provenance fixture'],
|
|
149
|
+
]) {
|
|
150
|
+
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
151
|
+
assert.equal(result.status, 0, `${args.join(' ')}\n${result.stdout}\n${result.stderr}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
specsRoot: 'specs',
|
|
156
|
+
specFile,
|
|
157
|
+
feature,
|
|
158
|
+
session: `package-${path.basename(root)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function provenanceCliArgs(helper, root, fixture) {
|
|
163
|
+
return [
|
|
164
|
+
helper,
|
|
165
|
+
'--json',
|
|
166
|
+
'--project-root', root,
|
|
167
|
+
'--specs-root', fixture.specsRoot,
|
|
168
|
+
'--spec-file', fixture.specFile,
|
|
169
|
+
'--feature-name', fixture.feature,
|
|
170
|
+
'--session', fixture.session,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function assertInstalledProvenance(root, platform, fixture) {
|
|
175
|
+
const scripts = path.join(root, RUNTIMES[platform].root, 'scripts');
|
|
176
|
+
const helper = path.join(scripts, 'provenance.cjs');
|
|
177
|
+
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
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');
|
|
182
|
+
const gate = path.join(
|
|
183
|
+
root,
|
|
184
|
+
platform === 'claude' ? '.claude/hooks/spec-gate.cjs' : '.codex/hooks/spec-gate.cjs'
|
|
185
|
+
);
|
|
186
|
+
const authorityDir = path.join(root, platform === 'claude' ? '.claude/hooks' : '.codex/hooks');
|
|
187
|
+
const authority = path.join(authorityDir, 'completion-authority.cjs');
|
|
188
|
+
const authorityCheck = path.join(authorityDir, 'completion-authority-check.cjs');
|
|
189
|
+
const authorityState = path.join(authorityDir, 'completion-authority-state.cjs');
|
|
190
|
+
const semanticAuthority = path.join(authorityDir, 'semantic-review-authority.cjs');
|
|
191
|
+
for (const file of [helper, policy, resolver, receipt, finalState, readiness]) {
|
|
192
|
+
assert.equal(fs.existsSync(file), true, `${platform} installed file missing: ${file}`);
|
|
193
|
+
}
|
|
194
|
+
assert.equal(fs.existsSync(gate), true, `${platform} installed gate missing: ${gate}`);
|
|
195
|
+
|
|
196
|
+
for (const file of [authority, authorityCheck, authorityState, semanticAuthority]) {
|
|
197
|
+
assert.equal(fs.existsSync(file), true, `${platform} installed completion authority missing: ${file}`);
|
|
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);
|
|
221
|
+
const authorityCheckBytes = fs.readFileSync(authorityCheck);
|
|
222
|
+
fs.writeFileSync(authorityCheck, 'module.exports = {};\n');
|
|
223
|
+
const malformedAuthority = spawnSync(process.execPath, [authority, '--stop'], {
|
|
224
|
+
cwd: root,
|
|
225
|
+
env: { ...process.env, PROJECT_ROOT: root },
|
|
226
|
+
input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
|
|
227
|
+
encoding: 'utf8',
|
|
228
|
+
});
|
|
229
|
+
assert.equal(malformedAuthority.status, 0, `${platform} malformed authority must fail closed`);
|
|
230
|
+
assert.equal(JSON.parse(malformedAuthority.stdout).decision, 'block');
|
|
231
|
+
fs.writeFileSync(authorityCheck, authorityCheckBytes);
|
|
232
|
+
fs.rmSync(authorityCheck);
|
|
233
|
+
const missingAuthority = spawnSync(process.execPath, [authority, '--stop'], {
|
|
234
|
+
cwd: root,
|
|
235
|
+
env: { ...process.env, PROJECT_ROOT: root },
|
|
236
|
+
input: JSON.stringify({ cwd: root, session_id: fixture.session, hook_event_name: 'Stop' }),
|
|
237
|
+
encoding: 'utf8',
|
|
238
|
+
});
|
|
239
|
+
assert.equal(missingAuthority.status, 0, `${platform} missing authority dependency must fail closed`);
|
|
240
|
+
assert.equal(JSON.parse(missingAuthority.stdout).decision, 'block');
|
|
241
|
+
fs.writeFileSync(authorityCheck, authorityCheckBytes);
|
|
242
|
+
|
|
243
|
+
const helperRun = spawnSync(process.execPath, provenanceCliArgs(helper, root, fixture), {
|
|
244
|
+
cwd: root,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
});
|
|
247
|
+
assert.equal(helperRun.status, 0, `${helperRun.stdout}\n${helperRun.stderr}`);
|
|
248
|
+
const output = JSON.parse(helperRun.stdout);
|
|
249
|
+
assert.equal(output.ok, true);
|
|
250
|
+
assert.match(output.Base, /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
|
|
251
|
+
assert.match(output.Head, /^[a-f0-9]{64}$/);
|
|
252
|
+
assert.match(output.context_id, /^[a-f0-9]{64}$/);
|
|
253
|
+
assert.equal(output.context.runtime_session, fixture.session);
|
|
254
|
+
|
|
255
|
+
const policyLoad = spawnSync(process.execPath, [policy, '--json'], {
|
|
256
|
+
cwd: root,
|
|
257
|
+
encoding: 'utf8',
|
|
258
|
+
});
|
|
259
|
+
assert.equal(policyLoad.status, 0, `${policyLoad.stdout}\n${policyLoad.stderr}`);
|
|
260
|
+
assert.equal(JSON.parse(policyLoad.stdout).ok, true);
|
|
261
|
+
|
|
262
|
+
const gateInput = JSON.stringify({
|
|
263
|
+
cwd: root,
|
|
264
|
+
session_id: fixture.session,
|
|
265
|
+
featureName: fixture.feature,
|
|
266
|
+
});
|
|
267
|
+
const assertControlledBlock = (label) => {
|
|
268
|
+
const gateRun = spawnSync(process.execPath, [gate], {
|
|
269
|
+
cwd: root,
|
|
270
|
+
input: `${gateInput}\n`,
|
|
271
|
+
encoding: 'utf8',
|
|
272
|
+
});
|
|
273
|
+
assert.equal(gateRun.status, 0, `${label}: ${gateRun.stdout}\n${gateRun.stderr}`);
|
|
274
|
+
const decision = JSON.parse(gateRun.stdout);
|
|
275
|
+
assert.equal(decision.decision, 'block', `${label}: ${gateRun.stdout}`);
|
|
276
|
+
assert.equal(decision.ok, undefined, `${label}: ${gateRun.stdout}`);
|
|
277
|
+
assert.match(decision.reason, /unavailable|shared workflow policy|provenance|helper/i);
|
|
278
|
+
assert.doesNotMatch(gateRun.stdout, /"decision"\s*:\s*"allow"|"ok"\s*:\s*true/);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const helperBytes = fs.readFileSync(helper);
|
|
282
|
+
fs.rmSync(helper);
|
|
283
|
+
assertControlledBlock('missing provenance helper');
|
|
284
|
+
|
|
285
|
+
fs.writeFileSync(helper, 'module.exports = {;\n');
|
|
286
|
+
assertControlledBlock('malformed provenance helper');
|
|
287
|
+
|
|
288
|
+
fs.writeFileSync(helper, helperBytes);
|
|
289
|
+
}
|
|
290
|
+
|
|
87
291
|
function managedBlock(content, start, end) {
|
|
88
292
|
const startIndex = content.indexOf(start);
|
|
89
293
|
const endIndex = content.indexOf(end);
|
|
@@ -173,13 +377,26 @@ function assertInstalledScripts(root, platform) {
|
|
|
173
377
|
const scripts = path.join(root, RUNTIMES[platform].root, 'scripts');
|
|
174
378
|
const policy = path.join(scripts, 'workflow-policy.cjs');
|
|
175
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');
|
|
176
384
|
assert.ok(fs.existsSync(policy), `installed policy missing: ${policy}`);
|
|
177
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}`);
|
|
178
390
|
|
|
179
391
|
const policyRun = spawnSync(process.execPath, [policy, '--json'], { cwd: root, encoding: 'utf8' });
|
|
180
392
|
assert.equal(policyRun.status, 0, `${policyRun.stdout}\n${policyRun.stderr}`);
|
|
181
393
|
assert.equal(JSON.parse(policyRun.stdout).contract, 'execution-policy');
|
|
182
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
|
+
|
|
183
400
|
const safe = path.join(root, 'safe.txt');
|
|
184
401
|
fs.writeFileSync(safe, 'mode=safe\n');
|
|
185
402
|
spawnSync('git', ['init', '-q'], { cwd: root });
|
|
@@ -189,6 +406,405 @@ function assertInstalledScripts(root, platform) {
|
|
|
189
406
|
assert.match(scannerRun.stdout, /No staged secrets found/);
|
|
190
407
|
}
|
|
191
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
|
+
|
|
192
808
|
function assertTransforms(root, platform) {
|
|
193
809
|
const skill = path.join(root, RUNTIMES[platform].root === '.codex' ? '.agents/skills' : `${RUNTIMES[platform].root}/skills`, 'develop', 'SKILL.md');
|
|
194
810
|
const content = fs.readFileSync(skill, 'utf8');
|
|
@@ -217,6 +833,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
217
833
|
try {
|
|
218
834
|
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
219
835
|
const tarball = path.join(destination, packed.filename);
|
|
836
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
220
837
|
assertCleanInventory(packedInventory(tarball));
|
|
221
838
|
|
|
222
839
|
const cases = [
|
|
@@ -229,7 +846,7 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
229
846
|
for (const matrixCase of cases) {
|
|
230
847
|
for (const lang of [null, 'vi']) {
|
|
231
848
|
const project = path.join(root, `${matrixCase.name}-${lang ? 'lang-vi' : 'no-lang'}`);
|
|
232
|
-
const installer = installPacked(tarball, project);
|
|
849
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
233
850
|
runInstaller(installer, project, matrixCase.platforms, lang);
|
|
234
851
|
for (const platform of matrixCase.platforms) {
|
|
235
852
|
assertHookLanguage(project, platform, lang, `${project}-${matrixCase.name}-${lang || 'none'}`);
|
|
@@ -256,3 +873,217 @@ test('packed tarball installer matrix proves locale, transforms, paths, and reru
|
|
|
256
873
|
fs.rmSync(root, { recursive: true, force: true });
|
|
257
874
|
}
|
|
258
875
|
});
|
|
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
|
+
|
|
914
|
+
test('packed Claude and Codex installs self-contain runtime provenance and fail closed without it', () => {
|
|
915
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-provenance-package-'));
|
|
916
|
+
const destination = path.join(root, 'pack');
|
|
917
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
918
|
+
try {
|
|
919
|
+
const packed = npmPack(['--pack-destination', destination, '--json'], PACKAGE_ROOT);
|
|
920
|
+
const tarball = path.join(destination, packed.filename);
|
|
921
|
+
const runtimeClosure = packedRuntimeClosure(path.join(root, 'runtime-closure'));
|
|
922
|
+
assertCleanInventory(packedInventory(tarball));
|
|
923
|
+
|
|
924
|
+
for (const platform of ['claude', 'codex']) {
|
|
925
|
+
const project = path.join(root, platform);
|
|
926
|
+
const installer = installPacked(tarball, project, runtimeClosure);
|
|
927
|
+
runInstaller(installer, project, [platform], null);
|
|
928
|
+
|
|
929
|
+
const packedSource = path.resolve(path.dirname(installer), '..', 'src');
|
|
930
|
+
fs.rmSync(packedSource, { recursive: true, force: true });
|
|
931
|
+
assert.equal(fs.existsSync(packedSource), false, 'installed gate must not need package source fallback');
|
|
932
|
+
|
|
933
|
+
const fixture = createProvenanceFixture(project);
|
|
934
|
+
assertInstalledProvenance(project, platform, fixture);
|
|
935
|
+
}
|
|
936
|
+
} finally {
|
|
937
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
938
|
+
}
|
|
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
|
+
});
|