@haposoft/cafekit 0.18.1 → 0.18.2
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/README.md +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +774 -6
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +192 -59
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +1 -1
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -52
- package/bin/phases/copy-payload.js +1 -3
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +141 -10
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +43 -59
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +8 -1
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const { test } = require('node:test');
|
|
9
|
+
const SOURCE = path.resolve(__dirname, '../../src');
|
|
10
|
+
|
|
11
|
+
function fixture(run) {
|
|
12
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-native-boundaries-'));
|
|
13
|
+
const hooks = path.join(root, '.codex/hooks');
|
|
14
|
+
try {
|
|
15
|
+
fs.cpSync(path.join(SOURCE, 'codex/hooks'), hooks, { recursive: true });
|
|
16
|
+
for (const name of ['runtime-path-safety', 'privacy-command-analysis', 'privacy-paths', 'secret-keywords', 'inspect-paths', 'update-check']) {
|
|
17
|
+
fs.copyFileSync(path.join(SOURCE, `claude/hooks/lib/${name}.cjs`), path.join(hooks, `lib/${name}.cjs`));
|
|
18
|
+
}
|
|
19
|
+
fs.cpSync(path.join(SOURCE, 'claude/scripts'), path.join(root, '.codex/scripts'), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(root, '.gitignore'), '.codex/\n');
|
|
21
|
+
for (const args of [['init', '-q'], ['config', 'user.email', 'probe@example.invalid'], ['config', 'user.name', 'Probe'], ['add', '.gitignore'], ['commit', '-qm', 'base']]) {
|
|
22
|
+
assert.equal(spawnSync('git', ['-C', root, ...args]).status, 0);
|
|
23
|
+
}
|
|
24
|
+
const hook = (name, payload = {}) => spawnSync(process.execPath, [path.join(hooks, `${name}.cjs`)], {
|
|
25
|
+
cwd: root, input: JSON.stringify({ cwd: root, session_id: 'a', ...payload }), encoding: 'utf8',
|
|
26
|
+
});
|
|
27
|
+
run({ root, hooks, hook });
|
|
28
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
29
|
+
}
|
|
30
|
+
function task(root, name, status = 'pending', receipt = '') {
|
|
31
|
+
const dir = path.join(root, 'specs/demo'); fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(dir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
33
|
+
fs.writeFileSync(path.join(dir, name), `# Task\nStatus: ${status}\n## Dependencies\n- none\n## Verification Plan\n- Command: node --test\n${receipt}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('privacy walks nested operation arrays but ignores prose', () => fixture(({ hook }) => {
|
|
37
|
+
for (const tool_input of [{ operations: [{ path: 'credentials.json' }] }, { operations: [[{ file_path: 'credentials.json' }]] }, { paths: ['credentials.json'] }]) {
|
|
38
|
+
const result = hook('privacy-block', { tool_name: 'mcp__files__read', tool_input });
|
|
39
|
+
assert.match(result.stdout, /APPROVE CAFEKIT PRIVACY/);
|
|
40
|
+
assert.match(JSON.parse(result.stdout).hookSpecificOutput.permissionDecisionReason, /y \(or yes \/ có\)/);
|
|
41
|
+
}
|
|
42
|
+
assert.equal(hook('privacy-block', { tool_name: 'mcp__files__read', tool_input: { messages: ['credentials.json', { text: 'credentials.json' }] } }).stdout, '');
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
test('privacy-block allows ls when configured specs escapes the project', () => fixture(({ root, hook }) => {
|
|
46
|
+
fs.writeFileSync(path.join(root, '.codex/runtime.json'), JSON.stringify({ paths: { specs: '../outside' } }));
|
|
47
|
+
const result = hook('privacy-block', { tool_name: 'Bash', tool_input: { command: 'ls' } });
|
|
48
|
+
assert.equal(result.status, 0);
|
|
49
|
+
assert.equal(result.stdout, '');
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
test('privacy-block issues approval for a sensitive read when configured specs escapes the project', () => fixture(({ root, hook }) => {
|
|
53
|
+
fs.writeFileSync(path.join(root, '.env'), '');
|
|
54
|
+
fs.writeFileSync(path.join(root, '.codex/runtime.json'), JSON.stringify({ paths: { specs: '../outside' } }));
|
|
55
|
+
const result = hook('privacy-block', { tool_name: 'Bash', tool_input: { command: 'cat .env' } });
|
|
56
|
+
assert.equal(result.status, 0);
|
|
57
|
+
const output = JSON.parse(result.stdout).hookSpecificOutput;
|
|
58
|
+
assert.equal(output.permissionDecision, 'deny');
|
|
59
|
+
assert.match(output.permissionDecisionReason, /APPROVE CAFEKIT PRIVACY [a-f0-9]{24}/);
|
|
60
|
+
assert.match(output.permissionDecisionReason, /y \(or yes \/ có\)/);
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
test('privacy-approval grants a pending read when configured specs escapes the project', () => fixture(({ root, hook }) => {
|
|
64
|
+
fs.writeFileSync(path.join(root, '.env'), '');
|
|
65
|
+
const payload = { tool_name: 'Bash', tool_input: { command: 'cat .env' } };
|
|
66
|
+
const pending = hook('privacy-block', payload);
|
|
67
|
+
assert.equal(pending.status, 0);
|
|
68
|
+
const approval = pending.stdout.match(/APPROVE CAFEKIT PRIVACY [a-f0-9]{24}/);
|
|
69
|
+
assert.ok(approval, 'the sensitive read must issue a pending approval');
|
|
70
|
+
fs.writeFileSync(path.join(root, '.codex/runtime.json'), JSON.stringify({ paths: { specs: '../outside' } }));
|
|
71
|
+
const granted = hook('privacy-approval', { prompt: approval[0] });
|
|
72
|
+
assert.equal(granted.status, 0);
|
|
73
|
+
assert.match(granted.stdout, /User approved the pending sensitive access/);
|
|
74
|
+
const allowed = hook('privacy-block', payload);
|
|
75
|
+
assert.equal(allowed.status, 0);
|
|
76
|
+
assert.equal(allowed.stdout, '');
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
test('session startup revokes only its own pending requests and grants', () => fixture(({ root, hooks, hook }) => {
|
|
80
|
+
const privacy = require(path.join(hooks, 'lib/privacy-state.cjs'));
|
|
81
|
+
const request = { projectRoot: root, sessionCwd: root, sessionId: 'a', filePath: 'credentials.json' };
|
|
82
|
+
const grant = privacy.createPending(request);
|
|
83
|
+
assert.equal(privacy.approvePending({ ...request, requestId: grant.requestId }).ok, true);
|
|
84
|
+
const pending = privacy.createPending(request);
|
|
85
|
+
for (const session_id of ['b', undefined]) {
|
|
86
|
+
assert.equal(hook('session', { session_id, hook_event_name: 'SessionStart', source: 'compact' }).status, 0);
|
|
87
|
+
assert.equal(privacy.hasGrant(request), true);
|
|
88
|
+
assert.ok(fs.existsSync(path.join(hooks, `.privacy/pending-${pending.requestId}.json`)));
|
|
89
|
+
}
|
|
90
|
+
assert.equal(hook('session', { hook_event_name: 'SessionStart', source: 'resume' }).status, 0);
|
|
91
|
+
assert.equal(privacy.hasGrant(request), false);
|
|
92
|
+
assert.equal(privacy.approvePending({ ...request, requestId: pending.requestId }).ok, false);
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
test('cache persistence failure does not replace receipt authority', () => fixture(({ root, hooks, hook }) => {
|
|
96
|
+
task(root, 'task-01-work.md');
|
|
97
|
+
fs.mkdirSync(path.join(hooks, '.logs/spec-gate-last.json'), { recursive: true });
|
|
98
|
+
assert.equal(hook('spec-gate', { hook_event_name: 'Stop' }).stdout, '');
|
|
99
|
+
task(root, 'task-01-work.md', 'done');
|
|
100
|
+
const result = hook('spec-gate', { hook_event_name: 'Stop', explicitFeature: 'demo' });
|
|
101
|
+
assert.match(JSON.parse(result.stdout).reason, /lack a verification receipt/);
|
|
102
|
+
assert.doesNotMatch(result.stdout, /EISDIR|controlled failure/);
|
|
103
|
+
fs.writeFileSync(path.join(root, '.codex/runtime.json'), '{"spec":{"completion_gate":false}}');
|
|
104
|
+
assert.match(hook('spec-gate', { hook_event_name: 'Stop' }).stdout, /no completion-gate bypass/);
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
test('build output directories are allowed while inspection commands stay blocked', () => fixture(({ hook }) => {
|
|
108
|
+
for (const command of ['vite build --outDir ./dist', 'tsc --outDir ./build', 'npm run build -- --outDir ./dist', 'cargo build --target-dir ./target']) {
|
|
109
|
+
assert.equal(hook('inspect-block', { tool_name: 'exec_command', tool_input: { command } }).status, 0, command);
|
|
110
|
+
}
|
|
111
|
+
for (const command of ['cat dist/index.js', 'find ./dist', 'grep x dist/index.js', 'vite build --outDir ./dist && cat dist/index.js', 'npm exec cat ./dist/index.js', 'vite build --outDir ./dist $(cat dist/index.js)']) {
|
|
112
|
+
assert.equal(hook('inspect-block', { tool_name: 'exec_command', tool_input: { command } }).status, 2, command);
|
|
113
|
+
}
|
|
114
|
+
assert.equal(hook('inspect-block', { tool_input: { path: './dist/index.js' } }).status, 2);
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
test('one gate invocation captures one stable snapshot for multiple receipts and rechecks next run', () => fixture(({ root, hook }) => {
|
|
118
|
+
task(root, 'task-06-pending.md');
|
|
119
|
+
const provenanceFile = path.join(root, '.codex/scripts/provenance.cjs');
|
|
120
|
+
const trace = path.join(root, '.codex/manifest-count');
|
|
121
|
+
fs.writeFileSync(provenanceFile, fs.readFileSync(provenanceFile, 'utf8').replace('function manifest(root, specsRoot) {', `function manifest(root, specsRoot) { fs.appendFileSync(${JSON.stringify(trace)}, 'x');`));
|
|
122
|
+
const provenance = require(provenanceFile);
|
|
123
|
+
const context = provenance.deriveRuntimeContext({ projectRoot: root, specsRoot: path.join(root, 'specs'), specFile: path.join(root, 'specs/demo/plan.md'), featureName: 'demo', runtimeSession: 'a' });
|
|
124
|
+
const receipt = `## Receipt\nVerification: PASS\nCommand: node --test\nExit: 0\nBase: ${context.base}\nHead: ${context.head}\n\`\`\`text\nnode --test\n1 test passed\n\`\`\`\n`;
|
|
125
|
+
for (let n = 1; n <= 5; n += 1) task(root, `task-0${n}-work.md`, 'done', receipt);
|
|
126
|
+
for (const args of [['add', 'specs'], ['commit', '-qm', 'packet']]) assert.equal(spawnSync('git', ['-C', root, ...args]).status, 0);
|
|
127
|
+
fs.writeFileSync(trace, '');
|
|
128
|
+
assert.equal(hook('spec-gate', { hook_event_name: 'Stop' }).stdout, '');
|
|
129
|
+
assert.equal(fs.readFileSync(trace, 'utf8').length, 2);
|
|
130
|
+
fs.writeFileSync(path.join(root, 'changed.js'), 'changed');
|
|
131
|
+
fs.appendFileSync(path.join(root, 'specs/demo/task-01-work.md'), '\n');
|
|
132
|
+
const blocked = hook('spec-gate', { hook_event_name: 'Stop' }).stdout;
|
|
133
|
+
assert.match(blocked, /provenance/);
|
|
134
|
+
assert.match(blocked, /lines are present but are not the pair/);
|
|
135
|
+
assert.equal(fs.readFileSync(trace, 'utf8').length, 4);
|
|
136
|
+
}));
|
|
@@ -260,6 +260,99 @@ function parseGeneratedTomlString(content, key) {
|
|
|
260
260
|
return JSON.parse(match[1]);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
test('installed Codex instructions reference all ten runtime rules', () => {
|
|
264
|
+
inTempProject(root => {
|
|
265
|
+
const result = install(root);
|
|
266
|
+
assert.equal(result.status, 0, result.stderr);
|
|
267
|
+
const instructions = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
268
|
+
const rules = ['workflow', 'ai-dev-rules', 'orchestrator', 'manage-docs', 'skill-workflow-routing', 'skill-domain-routing', 'review-audit-self-decision', 'process-management', 'state-sync', 'hook-protocols'];
|
|
269
|
+
for (const rule of rules) {
|
|
270
|
+
assert.ok(instructions.includes(`.codex/rules/${rule}.md`), rule);
|
|
271
|
+
assert.ok(fs.existsSync(path.join(root, '.codex/rules', `${rule}.md`)), rule);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('installed Codex orchestrator describes high reasoning on the session model', () => {
|
|
277
|
+
inTempProject(root => {
|
|
278
|
+
const source = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/orchestrator.md'));
|
|
279
|
+
const result = install(root);
|
|
280
|
+
assert.equal(result.status, 0, result.stderr);
|
|
281
|
+
const content = fs.readFileSync(path.join(root, '.codex/rules/orchestrator.md'), 'utf8');
|
|
282
|
+
assert.doesNotMatch(content, /strongest|below[^\n]*fable/);
|
|
283
|
+
assert.match(content, /high reasoning effort/);
|
|
284
|
+
assert.match(content, /session model unless[^.]*configur/i);
|
|
285
|
+
assert.deepEqual(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/rules/orchestrator.md')), source);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('installed hook protocols share malformed-output handling and Codex receipt guidance', () => {
|
|
290
|
+
inTempProject(root => {
|
|
291
|
+
const result = installPlatforms(root, ['claude', 'codex']);
|
|
292
|
+
assert.equal(result.status, 0, result.stderr);
|
|
293
|
+
const sentence = 'If hook output is malformed, stop and report the exact failure instead of guessing.';
|
|
294
|
+
for (const runtime of ['.claude', '.codex']) {
|
|
295
|
+
const content = fs.readFileSync(path.join(root, runtime, 'rules/hook-protocols.md'), 'utf8');
|
|
296
|
+
assert.ok(content.includes(sentence), runtime);
|
|
297
|
+
}
|
|
298
|
+
assert.match(fs.readFileSync(path.join(root, '.codex/rules/hook-protocols.md'), 'utf8'), /state-sync\.md/);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('installed Claude ignore rules preserve env samples and ignore temporary files', () => {
|
|
303
|
+
inTempProject(root => {
|
|
304
|
+
const result = installPlatforms(root, ['claude']);
|
|
305
|
+
assert.equal(result.status, 0, result.stderr);
|
|
306
|
+
const lines = fs.readFileSync(path.join(root, '.claude/.gitignore'), 'utf8').split(/\r?\n/);
|
|
307
|
+
for (const line of ['!.env.sample', '!.env.template', '*.tmp']) assert.ok(lines.includes(line), line);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const READ_ONLY_ROLE_SUFFIX = '\n\n## Read-only role\n\nRead-only role: never create, never edit, and never delete files, and never run commands that write to disk; return findings in your reply.';
|
|
312
|
+
|
|
313
|
+
function assertInstalledAgentFields(content, name) {
|
|
314
|
+
const source = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/agents', `${name.replace(/_/g, '-')}.md`), 'utf8');
|
|
315
|
+
const { frontmatter, body } = require('../lib/codex-frontmatter').splitFrontmatter(source);
|
|
316
|
+
for (const key of ['name', 'description', 'developer_instructions']) {
|
|
317
|
+
assert.equal((content.match(new RegExp(`^${key} =`, 'gm')) || []).length, 1);
|
|
318
|
+
}
|
|
319
|
+
assert.equal(parseGeneratedTomlString(content, 'name'), name);
|
|
320
|
+
assert.equal(parseGeneratedTomlString(content, 'description'), normalizeCodexBody(frontmatter.description));
|
|
321
|
+
const suffix = ['brainstormer', 'code_auditor', 'inspector', 'researcher'].includes(name)
|
|
322
|
+
? READ_ONLY_ROLE_SUFFIX : '';
|
|
323
|
+
const expectedBody = name === 'git_ops'
|
|
324
|
+
? body.slice(0, body.indexOf('When instantiated as a team member'))
|
|
325
|
+
+ body.split('\n').find(line => line.includes('NEVER arbitrarily use `force push`')).replace(/^3\. /, '- ') + '\n'
|
|
326
|
+
: body;
|
|
327
|
+
assert.equal(parseGeneratedTomlString(content, 'developer_instructions'), normalizeCodexBody(expectedBody).trim() + suffix);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const name of ['brainstormer', 'code_auditor', 'inspector', 'researcher']) {
|
|
331
|
+
test(`installed ${name} agent has exactly one read-only sandbox setting`, () => {
|
|
332
|
+
inTempProject(root => {
|
|
333
|
+
const result = install(root);
|
|
334
|
+
assert.equal(result.status, 0, result.stderr);
|
|
335
|
+
const content = fs.readFileSync(path.join(root, '.codex/agents', `${name}.toml`), 'utf8');
|
|
336
|
+
assert.deepEqual(content.match(/^sandbox_mode\s*=.*$/gm) || [], ['sandbox_mode = "read-only"']);
|
|
337
|
+
assert.ok(parseGeneratedTomlString(content, 'developer_instructions').endsWith(READ_ONLY_ROLE_SUFFIX));
|
|
338
|
+
assertInstalledAgentFields(content, name);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
test('installed implementation agents retain their sandbox defaults', () => {
|
|
344
|
+
inTempProject(root => {
|
|
345
|
+
const result = install(root);
|
|
346
|
+
assert.equal(result.status, 0, result.stderr);
|
|
347
|
+
for (const name of ['implementer', 'test_runner', 'debugger', 'git_ops']) {
|
|
348
|
+
const content = fs.readFileSync(path.join(root, '.codex/agents', `${name}.toml`), 'utf8');
|
|
349
|
+
assert.doesNotMatch(content, /^sandbox_mode\s*=/m);
|
|
350
|
+
assert.doesNotMatch(parseGeneratedTomlString(content, 'developer_instructions'), /## Read-only role/);
|
|
351
|
+
assertInstalledAgentFields(content, name);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
263
356
|
function replaceGeneratedTomlString(content, key, value) {
|
|
264
357
|
const pattern = new RegExp(`^${key} = .+$`, 'gm');
|
|
265
358
|
const matches = [...content.matchAll(pattern)];
|
|
@@ -816,7 +909,7 @@ function brainstormProjectionIssues(files) {
|
|
|
816
909
|
}
|
|
817
910
|
if (!skill.includes('Persist only approved decisions and semantics, only with user authority')
|
|
818
911
|
|| !skill.includes('redact live secrets, credentials, private keys, access tokens, and unnecessary PII;')
|
|
819
|
-
|| !framework.includes('Do not write "user selected" unless direct user text or the
|
|
912
|
+
|| !framework.includes('Do not write "user selected" unless direct user text or the question in the turn\'s final answer confirms it.')) {
|
|
820
913
|
issues.add('approval-persistence');
|
|
821
914
|
}
|
|
822
915
|
for (const [issue, clause] of Object.entries(adaptiveClauses)) {
|
|
@@ -824,9 +917,9 @@ function brainstormProjectionIssues(files) {
|
|
|
824
917
|
}
|
|
825
918
|
const visible = `${skill}\n${framework}\n${agent}`;
|
|
826
919
|
if (/\bAskUserQuestion\b/.test(visible)
|
|
827
|
-
|| /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a
|
|
828
|
-
|| /\ban
|
|
829
|
-
|| /\ba
|
|
920
|
+
|| /\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a question in the turn\'s final answer\b/i.test(visible)
|
|
921
|
+
|| /\ban question in the turn\'s final answer\b/i.test(visible)
|
|
922
|
+
|| /\ba question in the turn\'s final answer\s+(?:calls|batches)\b/i.test(visible)) {
|
|
830
923
|
issues.add('projection-grammar');
|
|
831
924
|
}
|
|
832
925
|
return [...issues].sort();
|
|
@@ -1379,26 +1472,26 @@ test('Codex payload transform emits native skill and subagent syntax', () => {
|
|
|
1379
1472
|
|
|
1380
1473
|
test('Codex payload transform keeps structured user-input grammar across determiners', () => {
|
|
1381
1474
|
const cases = [
|
|
1382
|
-
['standalone bare token', 'AskUserQuestion', 'a
|
|
1383
|
-
['standalone code token', '`AskUserQuestion`', 'a
|
|
1384
|
-
['standalone multi-backtick token', '``AskUserQuestion``', 'a
|
|
1385
|
-
['standalone padded multi-backtick token', '`` AskUserQuestion ``', 'a
|
|
1386
|
-
['one', 'one AskUserQuestion call', 'one
|
|
1387
|
-
['one multi-backtick', 'one ``AskUserQuestion`` call', 'one
|
|
1388
|
-
['one padded multi-backtick', 'one `` AskUserQuestion `` call', 'one
|
|
1389
|
-
['each', 'each AskUserQuestion call', 'each
|
|
1390
|
-
['every', 'every AskUserQuestion batch', 'every
|
|
1391
|
-
['another', 'another AskUserQuestion call', 'another
|
|
1392
|
-
['this', 'this AskUserQuestion call', 'this
|
|
1393
|
-
['that', 'that AskUserQuestion call', 'that
|
|
1394
|
-
['a', 'a AskUserQuestion call', 'a
|
|
1395
|
-
['an', 'an AskUserQuestion call', 'a
|
|
1396
|
-
['capitalized each', 'Each AskUserQuestion call', 'Each
|
|
1397
|
-
['capitalized an', 'An AskUserQuestion call', 'A
|
|
1398
|
-
['the', 'the AskUserQuestion call', 'the
|
|
1399
|
-
['numeric', '27 AskUserQuestion calls', '27
|
|
1400
|
-
['plural calls', 'AskUserQuestion calls', '
|
|
1401
|
-
['plural batches', 'AskUserQuestion batches', '
|
|
1475
|
+
['standalone bare token', 'AskUserQuestion', 'a question in the turn\'s final answer'],
|
|
1476
|
+
['standalone code token', '`AskUserQuestion`', 'a question in the turn\'s final answer'],
|
|
1477
|
+
['standalone multi-backtick token', '``AskUserQuestion``', 'a question in the turn\'s final answer'],
|
|
1478
|
+
['standalone padded multi-backtick token', '`` AskUserQuestion ``', 'a question in the turn\'s final answer'],
|
|
1479
|
+
['one', 'one AskUserQuestion call', 'one question in the turn\'s final answer call'],
|
|
1480
|
+
['one multi-backtick', 'one ``AskUserQuestion`` call', 'one question in the turn\'s final answer call'],
|
|
1481
|
+
['one padded multi-backtick', 'one `` AskUserQuestion `` call', 'one question in the turn\'s final answer call'],
|
|
1482
|
+
['each', 'each AskUserQuestion call', 'each question in the turn\'s final answer call'],
|
|
1483
|
+
['every', 'every AskUserQuestion batch', 'every question in the turn\'s final answer batch'],
|
|
1484
|
+
['another', 'another AskUserQuestion call', 'another question in the turn\'s final answer call'],
|
|
1485
|
+
['this', 'this AskUserQuestion call', 'this question in the turn\'s final answer call'],
|
|
1486
|
+
['that', 'that AskUserQuestion call', 'that question in the turn\'s final answer call'],
|
|
1487
|
+
['a', 'a AskUserQuestion call', 'a question in the turn\'s final answer call'],
|
|
1488
|
+
['an', 'an AskUserQuestion call', 'a question in the turn\'s final answer call'],
|
|
1489
|
+
['capitalized each', 'Each AskUserQuestion call', 'Each question in the turn\'s final answer call'],
|
|
1490
|
+
['capitalized an', 'An AskUserQuestion call', 'A question in the turn\'s final answer call'],
|
|
1491
|
+
['the', 'the AskUserQuestion call', 'the question in the turn\'s final answer call'],
|
|
1492
|
+
['numeric', '27 AskUserQuestion calls', '27 question in the turn\'s final answer calls'],
|
|
1493
|
+
['plural calls', 'AskUserQuestion calls', 'question in the turn\'s final answer calls'],
|
|
1494
|
+
['plural batches', 'AskUserQuestion batches', 'question in the turn\'s final answer batches']
|
|
1402
1495
|
];
|
|
1403
1496
|
|
|
1404
1497
|
for (const [name, input, expected] of cases) {
|
|
@@ -1406,27 +1499,27 @@ test('Codex payload transform keeps structured user-input grammar across determi
|
|
|
1406
1499
|
assert.equal(actual, expected, name);
|
|
1407
1500
|
assert.equal(normalizeCodexBody(actual, '/fixture/instructions.md'), expected, `${name} idempotence`);
|
|
1408
1501
|
assert.doesNotMatch(actual, /\bAskUserQuestion\b/, `${name} Claude-only token`);
|
|
1409
|
-
assert.doesNotMatch(actual, /\ban
|
|
1502
|
+
assert.doesNotMatch(actual, /\ban question in the turn\'s final answer\b/i, `${name} incompatible article`);
|
|
1410
1503
|
assert.doesNotMatch(
|
|
1411
1504
|
actual,
|
|
1412
|
-
/\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a
|
|
1505
|
+
/\b(?:a|an|one|each|every|another|this|that|the|[0-9]+)\s+a question in the turn\'s final answer\b/i,
|
|
1413
1506
|
`${name} doubled article`
|
|
1414
1507
|
);
|
|
1415
1508
|
}
|
|
1416
1509
|
|
|
1417
1510
|
assert.equal(
|
|
1418
1511
|
normalizeCodexBody('one\nAskUserQuestion call', '/fixture/instructions.md'),
|
|
1419
|
-
'one\
|
|
1512
|
+
'one\nquestion in the turn\'s final answer call',
|
|
1420
1513
|
'a single Markdown soft break must preserve its determiner relationship'
|
|
1421
1514
|
);
|
|
1422
1515
|
assert.equal(
|
|
1423
1516
|
normalizeCodexBody('AskUserQuestion\nCalls', '/fixture/instructions.md'),
|
|
1424
|
-
'
|
|
1517
|
+
'question in the turn\'s final answer\nCalls',
|
|
1425
1518
|
'a single Markdown soft break must preserve a plural relationship'
|
|
1426
1519
|
);
|
|
1427
1520
|
assert.equal(
|
|
1428
1521
|
normalizeCodexBody('one\n\nAskUserQuestion call', '/fixture/instructions.md'),
|
|
1429
|
-
'one\n\na
|
|
1522
|
+
'one\n\na question in the turn\'s final answer call',
|
|
1430
1523
|
'a paragraph break must not bind a prior determiner'
|
|
1431
1524
|
);
|
|
1432
1525
|
assert.equal(
|
|
@@ -1504,9 +1597,9 @@ test('Codex structured-input corpus oracle stays differential and production-awa
|
|
|
1504
1597
|
assert.deepEqual(actualPaths, expectedPaths, 'every source occurrence needs an explicit projection oracle');
|
|
1505
1598
|
|
|
1506
1599
|
const instructionExpectedSnippets = new Map([
|
|
1507
|
-
['src/claude/skills/git/SKILL.md', 'Present options via a
|
|
1508
|
-
['src/claude/skills/scout/SKILL.md', '**Fallback to a
|
|
1509
|
-
['src/claude/skills/sync/references/rebind-and-audit.md', 'End by asking with a
|
|
1600
|
+
['src/claude/skills/git/SKILL.md', 'Present options via a question in the turn\'s final answer — header'],
|
|
1601
|
+
['src/claude/skills/scout/SKILL.md', '**Fallback to a question in the turn\'s final answer:**'],
|
|
1602
|
+
['src/claude/skills/sync/references/rebind-and-audit.md', 'End by asking with a question in the turn\'s final answer when the host has']
|
|
1510
1603
|
]);
|
|
1511
1604
|
for (const [relativePath, expectedSnippet] of instructionExpectedSnippets) {
|
|
1512
1605
|
const sourcePath = path.join(PACKAGE_ROOT, relativePath);
|
|
@@ -1515,7 +1608,7 @@ test('Codex structured-input corpus oracle stays differential and production-awa
|
|
|
1515
1608
|
const withSentinel = source.replace(/`?\bAskUserQuestion\b`?/g, sentinel);
|
|
1516
1609
|
assert.notEqual(withSentinel, source, `${relativePath} differential control must replace a token`);
|
|
1517
1610
|
const expected = normalizeCodexBody(withSentinel, sourcePath)
|
|
1518
|
-
.replaceAll(sentinel, 'a
|
|
1611
|
+
.replaceAll(sentinel, 'a question in the turn\'s final answer');
|
|
1519
1612
|
const actual = normalizeCodexBody(source, sourcePath);
|
|
1520
1613
|
assert.equal(actual, expected, `${relativePath} independent differential oracle`);
|
|
1521
1614
|
assert.match(actual, new RegExp(expectedSnippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
@@ -1524,7 +1617,7 @@ test('Codex structured-input corpus oracle stays differential and production-awa
|
|
|
1524
1617
|
const specMaker = fs.readFileSync(SPEC_MAKER_SOURCE_PATH, 'utf8');
|
|
1525
1618
|
const convertedAgent = convertCodexAgentContent(specMaker, path.basename(SPEC_MAKER_SOURCE_PATH));
|
|
1526
1619
|
assert.doesNotMatch(convertedAgent, /\bAskUserQuestion\b/);
|
|
1527
|
-
assert.doesNotMatch(convertedAgent, /
|
|
1620
|
+
assert.doesNotMatch(convertedAgent, /question in the turn\'s final answer/);
|
|
1528
1621
|
|
|
1529
1622
|
const sessionPath = path.join(PACKAGE_ROOT, 'src/claude/hooks/session.cjs');
|
|
1530
1623
|
const session = normalizeCodexBody(fs.readFileSync(sessionPath, 'utf8'), sessionPath);
|
|
@@ -1996,6 +2089,45 @@ function sessionLauncher(projectRoot) {
|
|
|
1996
2089
|
return { config, command: config.hooks.SessionStart[0].hooks[0].command };
|
|
1997
2090
|
}
|
|
1998
2091
|
|
|
2092
|
+
test('parity regression: moved Codex checkout rebinds both launcher fields and preserves metadata', () => {
|
|
2093
|
+
inTempProject((root) => {
|
|
2094
|
+
const oldRoot = path.join(root, "old ' quoted project"), newRoot = path.join(root, "new ' quoted project");
|
|
2095
|
+
fs.mkdirSync(oldRoot);
|
|
2096
|
+
assert.equal(install(oldRoot).status, 0);
|
|
2097
|
+
const configPath = path.join(oldRoot, '.codex/hooks.json');
|
|
2098
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
2099
|
+
config.hooks.SessionStart[0].matcher = 'custom-startup';
|
|
2100
|
+
config.hooks.SessionStart[0].hooks[0].timeout = 41;
|
|
2101
|
+
fs.writeFileSync(configPath, JSON.stringify(config));
|
|
2102
|
+
fs.renameSync(oldRoot, newRoot);
|
|
2103
|
+
const movedConfig = path.join(newRoot, '.codex/hooks.json');
|
|
2104
|
+
const before = fs.readFileSync(movedConfig, 'utf8');
|
|
2105
|
+
assert.equal(install(newRoot, ['--dry-run']).status, 0);
|
|
2106
|
+
assert.equal(fs.readFileSync(movedConfig, 'utf8'), before, 'dry run modified launchers');
|
|
2107
|
+
const upgraded = install(newRoot);
|
|
2108
|
+
assert.equal(upgraded.status, 0, `${upgraded.stdout}\n${upgraded.stderr}`);
|
|
2109
|
+
const rebound = sessionLauncher(newRoot);
|
|
2110
|
+
assert.equal(rebound.config.hooks.SessionStart[0].matcher, 'custom-startup');
|
|
2111
|
+
assert.equal(rebound.config.hooks.SessionStart[0].hooks[0].timeout, 41);
|
|
2112
|
+
const launched = runPosixLauncher(rebound.command, newRoot);
|
|
2113
|
+
assert.equal(launched.status, 0, launched.stderr);
|
|
2114
|
+
assert.match(launched.stdout, /Session startup\./);
|
|
2115
|
+
const windowsCommand = rebound.config.hooks.SessionStart[0].hooks[0].commandWindows.replace(/^node /, `"${process.execPath}" `);
|
|
2116
|
+
const launchedWindows = spawnSync(windowsCommand, { cwd: newRoot, encoding: 'utf8', input: JSON.stringify({ session_id: 'moved-windows', hook_event_name: 'SessionStart', source: 'startup', cwd: newRoot }), shell: true });
|
|
2117
|
+
assert.equal(launchedWindows.status, 0, launchedWindows.stderr);
|
|
2118
|
+
assert.match(launchedWindows.stdout, /Session startup\./);
|
|
2119
|
+
for (const { handler } of allHookLaunchers(rebound.config)) {
|
|
2120
|
+
const encoded = handler.commandWindows.match(/\s([A-Za-z0-9_-]+)$/)?.[1];
|
|
2121
|
+
assert.ok(encoded);
|
|
2122
|
+
const target = Buffer.from(encoded, 'base64url').toString('utf8');
|
|
2123
|
+
assert.equal(path.dirname(target), fs.realpathSync(path.join(newRoot, '.codex/hooks')));
|
|
2124
|
+
}
|
|
2125
|
+
const stable = fs.readFileSync(movedConfig, 'utf8');
|
|
2126
|
+
assert.equal(install(newRoot).status, 0);
|
|
2127
|
+
assert.equal(fs.readFileSync(movedConfig, 'utf8'), stable, 'reinstall changed stable hooks config');
|
|
2128
|
+
});
|
|
2129
|
+
});
|
|
2130
|
+
|
|
1999
2131
|
test('Codex POSIX hook launchers run without Git and outside the repository root', () => {
|
|
2000
2132
|
inTempProject((root) => {
|
|
2001
2133
|
// A launcher that asks Git for its root gets nothing here and the outer root one
|
|
@@ -2248,7 +2380,7 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
|
|
|
2248
2380
|
const questionSkill = path.join(root, '.agents', 'skills', 'ask', 'SKILL.md');
|
|
2249
2381
|
fs.appendFileSync(questionSkill, '\nUSER-CODEX-SENTINEL\n');
|
|
2250
2382
|
// Fresh installs are exact. Refresh/upgrade intentionally documents the
|
|
2251
|
-
//
|
|
2383
|
+
// Untracked obsolete files and edited current skills both stay on refresh.
|
|
2252
2384
|
const obsoleteSpecsRule = path.join(
|
|
2253
2385
|
root, '.agents', 'skills', 'specs', 'rules', 'design-principles.md'
|
|
2254
2386
|
);
|
|
@@ -2261,8 +2393,8 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
|
|
|
2261
2393
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
2262
2394
|
assert.equal(
|
|
2263
2395
|
fs.existsSync(obsoleteSpecsRule),
|
|
2264
|
-
|
|
2265
|
-
'Codex refresh
|
|
2396
|
+
true,
|
|
2397
|
+
'Codex refresh preserves untracked obsolete skill files'
|
|
2266
2398
|
);
|
|
2267
2399
|
|
|
2268
2400
|
const metadataPath = path.join(root, '.codex', 'cafekit.json');
|
|
@@ -2277,9 +2409,11 @@ test('Codex installed Specs and spec-maker reject adaptive coverage mutations',
|
|
|
2277
2409
|
assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
2278
2410
|
assert.equal(
|
|
2279
2411
|
fs.existsSync(obsoleteSpecsRule),
|
|
2280
|
-
|
|
2281
|
-
'Codex upgrade
|
|
2412
|
+
true,
|
|
2413
|
+
'Codex upgrade preserves untracked obsolete skill files'
|
|
2282
2414
|
);
|
|
2415
|
+
assert.match(sameVersion.stdout + sameVersion.stderr, /Preserved 1 obsolete runtime/);
|
|
2416
|
+
assert.match(upgrade.stdout + upgrade.stderr, /Preserved 1 obsolete runtime/);
|
|
2283
2417
|
assert.ok(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8').startsWith(userInstructions));
|
|
2284
2418
|
|
|
2285
2419
|
const forced = install(root, ['--force-overwrite']);
|
|
@@ -2509,7 +2643,7 @@ test('Codex installed Brainstorm skill reference and agent preserve proportional
|
|
|
2509
2643
|
{
|
|
2510
2644
|
name: 'decision-provenance-removed',
|
|
2511
2645
|
source: 'framework',
|
|
2512
|
-
from: 'Do not write "user selected" unless direct user text or the
|
|
2646
|
+
from: 'Do not write "user selected" unless direct user text or the question in the turn\'s final answer\n confirms it.',
|
|
2513
2647
|
to: 'Write "user selected" for inferred defaults.',
|
|
2514
2648
|
expected: ['approval-persistence']
|
|
2515
2649
|
},
|
|
@@ -2517,7 +2651,7 @@ test('Codex installed Brainstorm skill reference and agent preserve proportional
|
|
|
2517
2651
|
name: 'installed-grammar-corruption',
|
|
2518
2652
|
source: 'skill',
|
|
2519
2653
|
from: '## Completion bar',
|
|
2520
|
-
to: 'one a
|
|
2654
|
+
to: 'one a question in the turn\'s final answer call\n\n## Completion bar',
|
|
2521
2655
|
expected: ['projection-grammar']
|
|
2522
2656
|
}
|
|
2523
2657
|
];
|
|
@@ -3079,28 +3213,28 @@ test('Codex installed code_auditor carries no Strict attestation marker', () =>
|
|
|
3079
3213
|
});
|
|
3080
3214
|
});
|
|
3081
3215
|
|
|
3082
|
-
test('reinstall
|
|
3083
|
-
inTempProject(
|
|
3216
|
+
test('reinstall preserves untracked Codex runtime files while omp keeps its cleanup policy', () => {
|
|
3217
|
+
for (const platform of ['codex', 'omp']) inTempProject(root => {
|
|
3084
3218
|
const first = installPlatforms(root, ['codex', 'omp']);
|
|
3085
3219
|
assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
|
|
3220
|
+
const runtime = `.${platform}`;
|
|
3086
3221
|
const retired = ['change-firewall.cjs', 'spec-final-state.cjs'];
|
|
3087
|
-
for (const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
const retiredSkillFiles = ['code-review/references/verification-gate.md', 'specs/references/archive-workflow.md'];
|
|
3091
|
-
for (const rel of retiredSkillFiles) fs.writeFileSync(path.join(root, '.agents', 'skills', rel), 'legacy\n');
|
|
3092
|
-
const again = installPlatforms(root, ['codex', 'omp'], ['--force-overwrite']);
|
|
3093
|
-
assert.equal(again.status, 0, `${again.stdout}\n${again.stderr}`);
|
|
3222
|
+
for (const name of retired) fs.writeFileSync(path.join(root, runtime, 'scripts', name), 'legacy\n');
|
|
3223
|
+
const retiredSkillFiles = platform === 'codex'
|
|
3224
|
+
? ['code-review/references/verification-gate.md', 'specs/references/archive-workflow.md'] : [];
|
|
3094
3225
|
for (const rel of retiredSkillFiles) {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
for (const runtime of ['.codex', '.omp']) {
|
|
3099
|
-
for (const name of retired) {
|
|
3100
|
-
assert.equal(fs.existsSync(path.join(root, runtime, 'scripts', name)), false, `${runtime}/scripts/${name} must be removed`);
|
|
3101
|
-
}
|
|
3102
|
-
assert.equal(fs.existsSync(path.join(root, runtime, 'scripts', 'spec-resolver.cjs')), true, `${runtime} keeps the resolver`);
|
|
3226
|
+
const target = path.join(root, '.agents/skills', rel);
|
|
3227
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
3228
|
+
fs.writeFileSync(target, 'legacy\n');
|
|
3103
3229
|
}
|
|
3230
|
+
const again = installPlatforms(root, [platform], ['--force-overwrite']);
|
|
3231
|
+
assert.equal(again.status, 0, `${again.stdout}\n${again.stderr}`);
|
|
3232
|
+
const preserved = platform === 'codex';
|
|
3233
|
+
for (const rel of retiredSkillFiles) assert.equal(fs.existsSync(path.join(root, '.agents/skills', rel)), preserved, rel);
|
|
3234
|
+
for (const name of retired) assert.equal(fs.existsSync(path.join(root, runtime, 'scripts', name)), preserved, name);
|
|
3235
|
+
if (preserved) assert.match(again.stdout + again.stderr, /Preserved 4 obsolete runtime/);
|
|
3236
|
+
assert.equal(fs.existsSync(path.join(root, '.agents/skills/code-review/SKILL.md')), true);
|
|
3237
|
+
assert.equal(fs.existsSync(path.join(root, runtime, 'scripts/spec-resolver.cjs')), true);
|
|
3104
3238
|
});
|
|
3105
3239
|
});
|
|
3106
3240
|
|
|
@@ -3222,4 +3356,3 @@ test('Claude and Codex installed rules preserve the ported review and process gu
|
|
|
3222
3356
|
}
|
|
3223
3357
|
});
|
|
3224
3358
|
});
|
|
3225
|
-
|