@haposoft/cafekit 0.18.3 → 0.18.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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/bin/__tests__/adv-skill.test.js +1 -2
  3. package/bin/__tests__/claude-plugin-build.test.js +2 -2
  4. package/bin/__tests__/codex-hook-runtime-fixture.cjs +26 -0
  5. package/bin/__tests__/codex-hooks-ownership.test.js +8 -11
  6. package/bin/__tests__/codex-hooks.test.js +49 -79
  7. package/bin/__tests__/codex-implicit-policy.test.js +2 -2
  8. package/bin/__tests__/codex-native-hook-boundaries.test.js +2 -4
  9. package/bin/__tests__/codex-native.test.js +125 -2294
  10. package/bin/__tests__/codex-quiet-hooks.test.js +2 -8
  11. package/bin/__tests__/codex-skill-projections.test.js +101 -0
  12. package/bin/__tests__/develop-contract.test.js +100 -874
  13. package/bin/__tests__/orca-session.test.js +2 -16
  14. package/bin/__tests__/package-inventory.test.js +228 -2044
  15. package/bin/__tests__/secret-scanner.test.js +73 -0
  16. package/bin/__tests__/skill-routing-source.test.js +6 -0
  17. package/bin/__tests__/toaa-skill.test.js +195 -0
  18. package/bin/__tests__/validator-grounding.test.js +0 -198
  19. package/bin/__tests__/vibe-skill.test.js +1 -2
  20. package/bin/__tests__/website-docs.test.js +58 -0
  21. package/bin/lib/codex-install.js +5 -3
  22. package/package.json +1 -1
  23. package/src/claude/hooks/__tests__/privacy-block.test.js +0 -26
  24. package/src/claude/hooks/__tests__/spec-gate.test.js +0 -47
  25. package/src/claude/hooks/__tests__/state.test.js +21 -0
  26. package/src/claude/hooks/state.cjs +22 -0
  27. package/src/claude/migration-manifest.json +1 -0
  28. package/src/claude/scripts/scan-staged-secrets.cjs +49 -3
  29. package/src/claude/skills/develop/SKILL.md +6 -0
  30. package/src/claude/skills/sync/SKILL.md +4 -0
  31. package/src/claude/skills/toaa/SKILL.md +82 -0
  32. package/src/claude/skills/toaa/references/autonomous-orchestration.md +54 -0
  33. package/src/claude/skills/toaa/references/prerequisites-checklist.md +37 -0
  34. package/src/codex/hooks/lib/hook-context.cjs +1 -0
  35. package/src/codex/hooks/lib/state-store.cjs +27 -3
  36. package/bin/__tests__/codex-research-projection.test.js +0 -70
  37. package/bin/__tests__/codex-specs-projection.test.js +0 -76
  38. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -58
  39. package/src/claude/skills/chrome-devtools/scripts/__tests__/error-handling.test.js +0 -102
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
4
4
 
5
- [![Version](https://img.shields.io/badge/version-0.18.3-blue.svg)](https://github.com/haposoft/cafekit)
5
+ [![Version](https://img.shields.io/badge/version-0.18.4-blue.svg)](https://github.com/haposoft/cafekit)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
7
  [![Claude%20Code](https://img.shields.io/badge/Claude%20Code-Native-orange.svg)](https://claude.ai/code)
8
8
  [![Codex%20CLI](https://img.shields.io/badge/Codex%20CLI-Native-111111.svg)](https://developers.openai.com/codex)
@@ -75,11 +75,10 @@ function assertLinksResolve(baseDir, file) {
75
75
  return links.length;
76
76
  }
77
77
 
78
- test('manifest requires adv in alphabetical position', () => {
78
+ test('manifest requires adv', () => {
79
79
  // Without the entry the installer never copies the skill and nothing else fails.
80
80
  const required = loadClaudeMigrationManifest().skills.required;
81
81
  assert.equal(required.includes('adv'), true);
82
- assert.deepEqual([...required], [...required].sort());
83
82
  });
84
83
 
85
84
  test('frontmatter carries the catalog identity, manual-only invocation, and a routable description', () => {
@@ -29,10 +29,10 @@ before(async () => {
29
29
 
30
30
  after(() => fs.rmSync(work, { recursive: true, force: true }));
31
31
 
32
- test('inventory: 17 in-scope skills, 14 agents, no excluded component', () => {
32
+ test('inventory: 18 in-scope skills, 14 agents, no excluded component', () => {
33
33
  const skills = fs.readdirSync(path.join(tree, 'skills')).sort();
34
34
  const agents = fs.readdirSync(path.join(tree, 'agents')).map((f) => f.replace(/\.md$/, '')).sort();
35
- assert.equal(skills.length, 17);
35
+ assert.equal(skills.length, 18);
36
36
  assert.equal(agents.length, 14);
37
37
  for (const name of lib.EXCLUDED_SKILLS) assert.ok(!skills.includes(name), `excluded skill shipped: ${name}`);
38
38
  for (const name of lib.EXCLUDED_AGENTS) assert.ok(!agents.includes(name), `excluded agent shipped: ${name}`);
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { codexRuntimeLayout } = require('../phases/codex-runtime');
6
+
7
+ // Groups the installer materializes under .codex that Codex hooks load at run time.
8
+ const HOOK_RUNTIME_GROUPS = new Set(['hooks', 'schema', 'shared-lib']);
9
+
10
+ /**
11
+ * Lay the Codex hook runtime into `<root>/.codex` exactly as the installer does,
12
+ * driven by the canonical layout so a new hook or shared library cannot be missed.
13
+ * Returns the hooks directory.
14
+ */
15
+ function installCodexHookRuntime(root) {
16
+ const runtimeRoot = path.join(root, '.codex');
17
+ for (const entry of codexRuntimeLayout()) {
18
+ if (!HOOK_RUNTIME_GROUPS.has(entry.group)) continue;
19
+ const target = path.join(runtimeRoot, entry.dest);
20
+ fs.mkdirSync(path.dirname(target), { recursive: true });
21
+ fs.copyFileSync(entry.src, target);
22
+ }
23
+ return path.join(runtimeRoot, 'hooks');
24
+ }
25
+
26
+ module.exports = { installCodexHookRuntime };
@@ -79,17 +79,6 @@ function addUserHooks(root) {
79
79
  fs.writeFileSync(hooksPath(root), `${JSON.stringify(config, null, 2)}\n`);
80
80
  }
81
81
 
82
- test('a fresh install registers the precompact capture hook', () => {
83
- inTempProject((root) => {
84
- assert.ok(managedScripts().PreCompact?.includes('precompact.cjs'));
85
- const result = install(root);
86
- assert.equal(result.status, 0, result.stderr);
87
- const scripts = (readHooks(root).hooks.PreCompact || []).flatMap(group => group.hooks || []).map(handler => hookScript(handler.command));
88
- assert.ok(scripts.includes('precompact.cjs'));
89
- assert.ok(fs.existsSync(path.join(root, '.codex/hooks/precompact.cjs')));
90
- });
91
- });
92
-
93
82
  test('a fresh install registers every CafeKit hook under its own event', () => {
94
83
  inTempProject((root) => {
95
84
  const result = install(root);
@@ -103,7 +92,10 @@ test('a fresh install registers every CafeKit hook under its own event', () => {
103
92
  .filter(Boolean);
104
93
  for (const script of scripts) {
105
94
  assert.ok(registered.includes(script), `${script} missing from ${eventName}`);
95
+ assert.ok(fs.existsSync(path.join(root, '.codex/hooks', script)), `${script} not installed`);
106
96
  }
97
+ const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8')).hooks[eventName];
98
+ assert.deepEqual(installed.hooks[eventName].map((group) => group.matcher), template.map((group) => group.matcher));
107
99
  }
108
100
  });
109
101
  });
@@ -119,6 +111,11 @@ test('a reinstall preserves user-added hooks instead of overwriting them', () =>
119
111
  const commands = allCommands(readHooks(root));
120
112
  assert.ok(commands.includes(USER_STOP_COMMAND), 'user Stop hook was erased by the reinstall');
121
113
  assert.ok(commands.includes(USER_PRETOOL_COMMAND), 'user PreToolUse hook was erased by the reinstall');
114
+ const after = readHooks(root).hooks;
115
+ const userGroup = (event, command) => after[event].find((group) => group.hooks.some((handler) => handler.command === command));
116
+ assert.deepEqual(userGroup('Stop', USER_STOP_COMMAND), { matcher: '*', hooks: [{ type: 'command', command: USER_STOP_COMMAND }] });
117
+ assert.deepEqual(userGroup('PreToolUse', USER_PRETOOL_COMMAND), { matcher: 'Bash', hooks: [{ type: 'command', command: USER_PRETOOL_COMMAND }] });
118
+ assert.doesNotMatch(second.stdout + second.stderr, /matcher preserved/);
122
119
  });
123
120
  });
124
121
 
@@ -7,6 +7,7 @@ const os = require('node:os');
7
7
  const path = require('node:path');
8
8
  const { spawnSync, spawn } = require('node:child_process');
9
9
  const { test } = require('node:test');
10
+ const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
10
11
 
11
12
  const OLD_INSPECTION_MATCHER = 'Bash|exec_command|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*|shell';
12
13
  const NEW_INSPECTION_MATCHER = 'Bash|exec_command|shell|apply_patch|mcp__.*|collaborationspawn_agent|collaborationwait_agent';
@@ -44,31 +45,6 @@ test('upgrade preserves a mixed user group and warns', () => matcherFixture(f =>
44
45
  assert.ok(f.read().hooks.PreToolUse[0].hooks.some(h => h.command === 'echo user-owned'));
45
46
  assert.ok(f.warnings.some(line => /matcher.*preserv/i.test(line)));
46
47
  }));
47
- test('reinstall keeps purely user groups unchanged without matcher warnings', () => matcherFixture(f => {
48
- const userPre = { matcher: 'Bash', hooks: [{ command: 'my-linter' }] };
49
- const userPost = { hooks: [{ command: 'my-logger' }] };
50
- f.config.hooks = { PreToolUse: [userPre], PostToolUse: [userPost] }; f.save();
51
- const installs = Array.from({ length: 2 }, () => {
52
- const result = spawnSync(process.execPath, [path.resolve(__dirname, '../install.js'), '--platform', 'codex', '-y'], {
53
- cwd: f.root, encoding: 'utf8',
54
- });
55
- return { result, config: f.read() };
56
- });
57
- for (const { result, config } of installs) {
58
- assert.equal(result.status, 0, result.stderr);
59
- for (const [event, original] of [['PreToolUse', userPre], ['PostToolUse', userPost]]) {
60
- const group = config.hooks[event].find(g => g.hooks.some(h => h.command === original.hooks[0].command));
61
- assert.equal(JSON.stringify(group), JSON.stringify(original));
62
- }
63
- assert.doesNotMatch(result.stdout + result.stderr, /matcher preserved/);
64
- }
65
- }));
66
- test('fresh hook registration uses every template matcher', () => matcherFixture(f => {
67
- f.merge();
68
- for (const [event, groups] of Object.entries(f.config.hooks)) {
69
- assert.deepEqual(f.read().hooks[event].map(g => g.matcher), groups.map(g => g.matcher));
70
- }
71
- }));
72
48
  test('upgrade preserves a user-selected managed matcher and warns', () => matcherFixture(f => {
73
49
  f.config.hooks.PreToolUse[0].matcher = 'Read|Grep'; f.save(); f.merge();
74
50
  assert.equal(f.read().hooks.PreToolUse[0].matcher, 'Read|Grep');
@@ -93,12 +69,18 @@ const { stateDir: codexStateDir } = require(
93
69
  path.join(CODEX_HOOKS, 'lib', 'state-store.cjs')
94
70
  );
95
71
 
72
+ // A hook child that never exits must fail its test, not hang the whole suite.
73
+ const HOOK_TIMEOUT_MS = 30000;
74
+
96
75
  function runHook(file, cwd, payload) {
97
- return spawnSync(process.execPath, [file], {
76
+ const result = spawnSync(process.execPath, [file], {
98
77
  cwd,
99
78
  input: JSON.stringify(payload),
100
- encoding: 'utf8'
79
+ encoding: 'utf8',
80
+ timeout: HOOK_TIMEOUT_MS
101
81
  });
82
+ assert.equal(result.error, undefined, `${path.basename(file)} did not exit: ${result.error?.message}`);
83
+ return result;
102
84
  }
103
85
 
104
86
  function runtimeContext(root, feature = 'auth', session = 'session-a') {
@@ -145,26 +127,7 @@ function inHookFixture(run, options = {}) {
145
127
  const hooks = path.join(root, '.codex', 'hooks');
146
128
  let deferred = false;
147
129
  try {
148
- fs.cpSync(CODEX_HOOKS, hooks, { recursive: true });
149
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
150
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
151
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
152
- fs.copyFileSync(
153
- path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/inspect-paths.cjs'),
154
- path.join(hooks, 'lib/inspect-paths.cjs'),
155
- );
156
- fs.copyFileSync(
157
- path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-command-analysis.cjs'),
158
- path.join(hooks, 'lib/privacy-command-analysis.cjs'),
159
- );
160
- fs.copyFileSync(
161
- path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-paths.cjs'),
162
- path.join(hooks, 'lib/privacy-paths.cjs'),
163
- );
164
- fs.copyFileSync(
165
- path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
166
- path.join(hooks, 'lib/runtime-path-safety.cjs'),
167
- );
130
+ installCodexHookRuntime(root);
168
131
  fs.mkdirSync(path.join(root, '.codex', 'scripts'), { recursive: true });
169
132
  if (options.policy !== 'missing') {
170
133
  if (options.policy === 'malformed') {
@@ -216,15 +179,7 @@ function inNoCommitCodexFixture(run) {
216
179
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-no-commit-'));
217
180
  const hooks = path.join(root, '.codex/hooks');
218
181
  try {
219
- fs.cpSync(CODEX_HOOKS, hooks, { recursive: true });
220
- for (const name of ['privacy-paths.cjs', 'privacy-command-analysis.cjs']) {
221
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib', name), path.join(hooks, 'lib', name));
222
- }
223
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
224
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
225
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
226
- fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
227
- path.join(hooks, 'lib/runtime-path-safety.cjs'));
182
+ installCodexHookRuntime(root);
228
183
  const scripts = path.join(root, '.codex/scripts');
229
184
  fs.mkdirSync(scripts, { recursive: true });
230
185
  for (const name of ['workflow-policy.cjs', 'provenance.cjs', 'spec-receipt.cjs', 'spec-resolver.cjs']) {
@@ -539,7 +494,7 @@ for (const method of ['spawnSync', 'execFileSync']) {
539
494
  }
540
495
  `);
541
496
  const result = spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
542
- cwd: root, encoding: 'utf8',
497
+ cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
543
498
  env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --require ${JSON.stringify(preload)}` },
544
499
  input: JSON.stringify({ cwd: root, featureName: 'auth', hook_event_name: 'UserPromptSubmit',
545
500
  session_id: 'one-snapshot', prompt: 'Continue specs/auth' }),
@@ -754,7 +709,7 @@ test('Codex secret guardrail runs from source with its shared keyword shim', ()
754
709
  delete env.CLAUDE_PROJECT_DIR;
755
710
  delete env.PROJECT_ROOT;
756
711
  const result = spawnSync(process.execPath, [path.join(CODEX_HOOKS, 'secret-output-guardrail.cjs')], {
757
- cwd: PACKAGE_ROOT, env, encoding: 'utf8', input: JSON.stringify({ prompt: 'Explain secret handling.' }),
712
+ cwd: PACKAGE_ROOT, env, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS, input: JSON.stringify({ prompt: 'Explain secret handling.' }),
758
713
  });
759
714
  assert.equal(result.status, 0);
760
715
  assert.equal(result.stderr, '');
@@ -855,7 +810,7 @@ test('Codex compact releases only its own rules reservation', () => {
855
810
  test('Codex precompact ignores empty stdin without recording a crash', () => {
856
811
  inHookFixture((root, hooks) => {
857
812
  const result = spawnSync(process.execPath, [path.join(hooks, 'precompact.cjs')], {
858
- cwd: root, input: '', encoding: 'utf8',
813
+ cwd: root, input: '', encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
859
814
  });
860
815
  assert.equal(result.status, 0);
861
816
  assert.equal(result.stderr, '');
@@ -900,7 +855,8 @@ function sessionWithoutUpdateNetwork(root, hooks, session_id) {
900
855
  throw new Error('network disabled in update test');
901
856
  };\n`);
902
857
  const result = spawnSync(process.execPath, ['--require', preload, path.join(hooks, 'session.cjs')], {
903
- cwd: root, encoding: 'utf8', input: JSON.stringify({ cwd: root, hook_event_name: 'SessionStart', source: 'startup', session_id }),
858
+ cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
859
+ input: JSON.stringify({ cwd: root, hook_event_name: 'SessionStart', source: 'startup', session_id }),
904
860
  });
905
861
  assert.equal(fs.existsSync(marker), false, 'the cached update check must not attempt a network request');
906
862
  return result;
@@ -976,7 +932,7 @@ function shortPrivacyFixture(check) {
976
932
  hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: `cat ${file}` } });
977
933
  const block = (file, id) => runHook(path.join(hooks, 'privacy-block.cjs'), root, payload(file, id));
978
934
  const submit = (prompt, session_id = session, env = process.env) => spawnSync(process.execPath,
979
- [path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env,
935
+ [path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env, timeout: HOOK_TIMEOUT_MS,
980
936
  input: JSON.stringify({ cwd: root, session_id, hook_event_name: 'UserPromptSubmit', prompt }) });
981
937
  const files = prefix => fs.existsSync(dir) ? fs.readdirSync(dir).filter(name => name.startsWith(prefix)) : [];
982
938
  const id = result => {
@@ -1682,6 +1638,7 @@ test('empty or forged Codex hook authority fails closed', () => {
1682
1638
  env: { ...process.env },
1683
1639
  input,
1684
1640
  encoding: 'utf8',
1641
+ timeout: HOOK_TIMEOUT_MS,
1685
1642
  });
1686
1643
  assert.equal(result.status, 0, `${JSON.stringify(input)}: ${result.stderr}`);
1687
1644
  assert.equal(result.stderr, '');
@@ -2390,7 +2347,37 @@ test('Codex hook-context canonicalizes symlink project root (regression)', () =>
2390
2347
  });
2391
2348
  });
2392
2349
 
2393
- test('Codex resolver and state reject symlink escape', () => {
2350
+ test('Codex session state refuses a symlinked state path and writes nothing outside the project', () => {
2351
+ inHookFixture((root, hooks) => {
2352
+ const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
2353
+ const sessionId = 'sess-symlink';
2354
+ const dir = stateStore.stateDir(root, sessionId);
2355
+ const sessionState = path.dirname(dir);
2356
+ // A repository can commit either the shared state folder or one session folder as a link.
2357
+ for (const link of [sessionState, dir]) {
2358
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-state-outside-'));
2359
+ try {
2360
+ fs.rmSync(sessionState, { recursive: true, force: true });
2361
+ fs.mkdirSync(path.dirname(link), { recursive: true });
2362
+ fs.symlinkSync(outside, link);
2363
+ let called = false;
2364
+ const ok = stateStore.withStateLock(root, sessionId, (stateDir) => {
2365
+ called = true;
2366
+ stateStore.writeData(stateDir, { todos: [] });
2367
+ });
2368
+ assert.equal(ok, false, `${path.relative(root, link)} link must be refused`);
2369
+ assert.equal(called, false);
2370
+ assert.deepEqual(fs.readdirSync(outside), [], 'nothing may be written outside the project');
2371
+ } finally {
2372
+ fs.rmSync(sessionState, { recursive: true, force: true });
2373
+ fs.rmSync(outside, { recursive: true, force: true });
2374
+ }
2375
+ }
2376
+ assert.equal(stateStore.withStateLock(root, sessionId, () => {}), true, 'a plain state folder still locks');
2377
+ });
2378
+ });
2379
+
2380
+ test('Codex resolver rejects symlink escape', () => {
2394
2381
  inHookFixture((root, hooks) => {
2395
2382
  const specUtils = require(path.join(hooks, 'lib', 'spec-utils.cjs'));
2396
2383
  const resolverPath = path.join(root, '.codex', 'scripts', 'spec-resolver.cjs');
@@ -2418,23 +2405,6 @@ test('Codex resolver and state reject symlink escape', () => {
2418
2405
  );
2419
2406
  fs.rmSync(outsideScripts, { recursive: true, force: true });
2420
2407
  }
2421
- // State dir symlink escape: create symlink for session-state dir
2422
- const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
2423
- const sessionId = 'sess-symlink';
2424
- const dir = stateStore.stateDir(root, sessionId);
2425
- fs.mkdirSync(path.dirname(dir), { recursive: true });
2426
- // Create a symlink that points outside project
2427
- const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'));
2428
- try {
2429
- if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
2430
- fs.symlinkSync(outside, dir);
2431
- // withStateLock should handle symlink safely (no crash, no write outside)
2432
- const ok = stateStore.withStateLock(root, sessionId, () => {});
2433
- assert.equal(typeof ok, 'boolean');
2434
- } finally {
2435
- try { fs.unlinkSync(dir); } catch {}
2436
- fs.rmSync(outside, { recursive: true, force: true });
2437
- }
2438
2408
  });
2439
2409
  });
2440
2410
 
@@ -55,8 +55,8 @@ function catalog(root, script, skillsRoot) {
55
55
  return JSON.parse(result.stdout);
56
56
  }
57
57
 
58
- test('the explicit-only skills are vibe and adv', () => {
59
- assert.deepEqual(explicitOnly.sort(), ['adv', 'vibe']);
58
+ test('the explicit-only skills are adv, toaa and vibe', () => {
59
+ assert.deepEqual(explicitOnly.sort(), ['adv', 'toaa', 'vibe']);
60
60
  });
61
61
 
62
62
  test('a Codex install writes the policy file for every explicit-only skill, owned, and for no other skill', () => {
@@ -6,16 +6,14 @@ const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const { spawnSync } = require('node:child_process');
8
8
  const { test } = require('node:test');
9
+ const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
9
10
  const SOURCE = path.resolve(__dirname, '../../src');
10
11
 
11
12
  function fixture(run) {
12
13
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-native-boundaries-'));
13
14
  const hooks = path.join(root, '.codex/hooks');
14
15
  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
- }
16
+ installCodexHookRuntime(root);
19
17
  fs.cpSync(path.join(SOURCE, 'claude/scripts'), path.join(root, '.codex/scripts'), { recursive: true });
20
18
  fs.writeFileSync(path.join(root, '.gitignore'), '.codex/\n');
21
19
  for (const args of [['init', '-q'], ['config', 'user.email', 'probe@example.invalid'], ['config', 'user.name', 'Probe'], ['add', '.gitignore'], ['commit', '-qm', 'base']]) {