@haposoft/cafekit 0.18.1 → 0.18.3
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__/adv-skill.test.js +371 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- 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 +812 -12
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +198 -58
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- 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-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -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 +81 -21
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +147 -17
- package/bin/__tests__/omp-hooks.test.js +35 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +501 -0
- package/bin/__tests__/usage-hook-retired.test.js +29 -9
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +124 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +141 -12
- package/bin/lib/context.js +6 -1
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +93 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +134 -33
- package/bin/phases/codex-runtime.js +110 -85
- package/bin/phases/copy-payload.js +50 -4
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +5 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +213 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
- 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 +11 -7
- package/src/claude/hooks/inspect-block.cjs +20 -66
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
- package/src/claude/hooks/lib/privacy-paths.cjs +673 -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 +80 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +25 -206
- package/src/claude/hooks/rules.cjs +10 -29
- package/src/claude/hooks/session.cjs +37 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +44 -59
- package/src/claude/hooks/state.cjs +101 -52
- package/src/claude/migration-manifest.json +14 -2
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
- package/src/claude/scripts/provenance.cjs +54 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- 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/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +5 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +5 -3
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +9 -1
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +15 -8
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +116 -17
- 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/state-store.cjs +5 -4
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +40 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +11 -216
- package/src/codex/hooks/rules.cjs +14 -34
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +59 -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/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
- package/src/omp/hooks/privacy-block.cjs +26 -207
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex loads no agents from a plugin, so the plugin ships them as TOML with an
|
|
4
|
+
// ownership marker and a script copies them into the user's Codex agents directory.
|
|
5
|
+
// The script replaces only files whose marker hash still matches their body.
|
|
6
|
+
|
|
7
|
+
const { test, before, after } = require('node:test');
|
|
8
|
+
const assert = require('node:assert/strict');
|
|
9
|
+
const crypto = require('node:crypto');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { spawnSync } = require('node:child_process');
|
|
14
|
+
|
|
15
|
+
const SCRIPTS = path.resolve(__dirname, '../../scripts');
|
|
16
|
+
const SRC = path.resolve(__dirname, '../../src');
|
|
17
|
+
const { convertCodexAgentContent, codexAgentName } = require('../lib/codex-install.js');
|
|
18
|
+
const { CODEX_POLICY_FILE, CODEX_EXPLICIT_ONLY_POLICY } = require('../phases/copy-payload.js');
|
|
19
|
+
const MARKER = /^# CafeKit agent: plugin=(\S+) version=(\S+) sha256=([0-9a-f]{64})\n/;
|
|
20
|
+
const REAL_AGENTS = path.join(os.homedir(), '.codex', 'agents');
|
|
21
|
+
let work;
|
|
22
|
+
let tree;
|
|
23
|
+
let agentNames;
|
|
24
|
+
let realBefore;
|
|
25
|
+
|
|
26
|
+
const sha256 = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
27
|
+
|
|
28
|
+
function realState() {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(REAL_AGENTS);
|
|
31
|
+
return `${stat.mtimeMs}:${fs.readdirSync(REAL_AGENTS).sort().join(',')}`;
|
|
32
|
+
} catch {
|
|
33
|
+
return 'absent';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
before(async () => {
|
|
38
|
+
realBefore = realState();
|
|
39
|
+
work = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ck-codex-agents-')));
|
|
40
|
+
const lib = await import(path.join(SCRIPTS, 'build-codex-plugin.mjs'));
|
|
41
|
+
const claudeLib = await import(path.join(SCRIPTS, 'build-claude-plugin.mjs'));
|
|
42
|
+
agentNames = claudeLib.inScopeAgents();
|
|
43
|
+
tree = lib.build(path.join(work, 'plugin', 'cf-codex'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
after(() => {
|
|
47
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
48
|
+
assert.equal(realState(), realBefore, 'the real ~/.codex/agents changed');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let caseCount = 0;
|
|
52
|
+
function setup() {
|
|
53
|
+
const dir = path.join(work, `case-${++caseCount}`);
|
|
54
|
+
const home = path.join(dir, 'home');
|
|
55
|
+
const repo = path.join(dir, 'repo');
|
|
56
|
+
fs.mkdirSync(home, { recursive: true });
|
|
57
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
58
|
+
spawnSync('git', ['init', '-q'], { cwd: repo });
|
|
59
|
+
return { home, repo, agents: path.join(home, '.codex', 'agents') };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setupAgents({ home, repo }, args = [], codexHome) {
|
|
63
|
+
const env = { ...process.env, HOME: home };
|
|
64
|
+
delete env.CODEX_HOME;
|
|
65
|
+
if (codexHome !== undefined) env.CODEX_HOME = codexHome;
|
|
66
|
+
return spawnSync('node', [path.join(tree, 'scripts/setup-agents.cjs'), ...args], { cwd: repo, encoding: 'utf8', env });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shipped(name) {
|
|
70
|
+
return fs.readFileSync(path.join(tree, 'agents', name), 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// An older CafeKit release of the same agent: a valid marker over a different body.
|
|
74
|
+
function olderOwn(name) {
|
|
75
|
+
const body = `name = "${name.replace(/\.toml$/, '')}"\ndescription = "an older release"\ndeveloper_instructions = "old"\n`;
|
|
76
|
+
return `# CafeKit agent: plugin=cf version=0.0.1 sha256=${sha256(body)}\n${body}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test('agents-built-with-hash-marker: every in-scope agent ships as TOML whose marker hashes its body', () => {
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf8'));
|
|
81
|
+
const files = fs.readdirSync(path.join(tree, 'agents')).sort();
|
|
82
|
+
assert.deepEqual(files, agentNames.map((agent) => `${codexAgentName(agent)}.toml`).sort());
|
|
83
|
+
for (const agent of agentNames) {
|
|
84
|
+
const text = shipped(`${codexAgentName(agent)}.toml`);
|
|
85
|
+
const [marker, plugin, version, digest] = text.match(MARKER) || [];
|
|
86
|
+
assert.ok(marker, `${agent} has no marker`);
|
|
87
|
+
assert.equal(plugin, 'cf');
|
|
88
|
+
assert.equal(version, pkg.version);
|
|
89
|
+
const body = text.slice(marker.length);
|
|
90
|
+
assert.equal(sha256(body), digest, agent);
|
|
91
|
+
assert.equal(body, convertCodexAgentContent(fs.readFileSync(path.join(SRC, 'claude/agents', `${agent}.md`), 'utf8'), `${agent}.md`), `${agent} differs from the npx conversion`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('write-when-absent: an empty agents directory receives every shipped agent', () => {
|
|
96
|
+
const ctx = setup();
|
|
97
|
+
const result = setupAgents(ctx);
|
|
98
|
+
assert.equal(result.status, 0, result.stderr);
|
|
99
|
+
const files = fs.readdirSync(path.join(tree, 'agents')).sort();
|
|
100
|
+
assert.deepEqual(fs.readdirSync(ctx.agents).sort(), files);
|
|
101
|
+
for (const file of files) assert.equal(fs.readFileSync(path.join(ctx.agents, file), 'utf8'), shipped(file), file);
|
|
102
|
+
assert.equal(result.stdout.match(/^written: /gm).length, files.length);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('replace-own-unmodified: an unedited agent from an older release is replaced', () => {
|
|
106
|
+
const ctx = setup();
|
|
107
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
108
|
+
fs.writeFileSync(path.join(ctx.agents, 'test_runner.toml'), olderOwn('test_runner.toml'));
|
|
109
|
+
const result = setupAgents(ctx);
|
|
110
|
+
assert.equal(result.status, 0, result.stderr);
|
|
111
|
+
assert.equal(fs.readFileSync(path.join(ctx.agents, 'test_runner.toml'), 'utf8'), shipped('test_runner.toml'));
|
|
112
|
+
assert.match(result.stdout, /^replaced: .*test_runner\.toml$/m);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('keep-modified-own: an agent whose body was edited under the marker keeps its bytes', () => {
|
|
116
|
+
const ctx = setup();
|
|
117
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
118
|
+
const edited = `${shipped('code_auditor.toml')}# my own note\n`;
|
|
119
|
+
fs.writeFileSync(path.join(ctx.agents, 'code_auditor.toml'), edited);
|
|
120
|
+
const result = setupAgents(ctx);
|
|
121
|
+
assert.equal(result.status, 0, result.stderr);
|
|
122
|
+
assert.equal(sha256(fs.readFileSync(path.join(ctx.agents, 'code_auditor.toml'), 'utf8')), sha256(edited));
|
|
123
|
+
assert.match(result.stdout, /^kept \(modified\): .*code_auditor\.toml$/m);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('keep-foreign: a file CafeKit did not install keeps its bytes', () => {
|
|
127
|
+
const ctx = setup();
|
|
128
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
129
|
+
const foreign = 'name = "debugger"\ndescription = "mine"\ndeveloper_instructions = "mine"\n';
|
|
130
|
+
fs.writeFileSync(path.join(ctx.agents, 'debugger.toml'), foreign);
|
|
131
|
+
const result = setupAgents(ctx);
|
|
132
|
+
assert.equal(result.status, 0, result.stderr);
|
|
133
|
+
assert.equal(fs.readFileSync(path.join(ctx.agents, 'debugger.toml'), 'utf8'), foreign);
|
|
134
|
+
assert.match(result.stdout, /^kept \(not installed by CafeKit\): .*debugger\.toml$/m);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('mode-preserved: replacing an owned agent keeps its file mode', () => {
|
|
138
|
+
const ctx = setup();
|
|
139
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
140
|
+
const file = path.join(ctx.agents, 'implementer.toml');
|
|
141
|
+
fs.writeFileSync(file, olderOwn('implementer.toml'), { mode: 0o600 });
|
|
142
|
+
fs.chmodSync(file, 0o600);
|
|
143
|
+
const result = setupAgents(ctx);
|
|
144
|
+
assert.equal(result.status, 0, result.stderr);
|
|
145
|
+
assert.equal(fs.readFileSync(file, 'utf8'), shipped('implementer.toml'));
|
|
146
|
+
assert.equal(fs.statSync(file).mode & 0o777, 0o600);
|
|
147
|
+
assert.deepEqual(fs.readdirSync(ctx.agents).filter((f) => f.endsWith('.tmp')), [], 'a temp file was left behind');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('codex-home-honored: an absolute CODEX_HOME receives the agents and a relative one is refused', () => {
|
|
151
|
+
const ctx = setup();
|
|
152
|
+
const codexHome = path.join(ctx.home, 'custom-codex-home');
|
|
153
|
+
const result = setupAgents(ctx, [], codexHome);
|
|
154
|
+
assert.equal(result.status, 0, result.stderr);
|
|
155
|
+
assert.deepEqual(fs.readdirSync(path.join(codexHome, 'agents')).sort(), fs.readdirSync(path.join(tree, 'agents')).sort());
|
|
156
|
+
assert.ok(!fs.existsSync(path.join(ctx.home, '.codex')), 'agents also went to ~/.codex');
|
|
157
|
+
const refused = setup();
|
|
158
|
+
const relative = setupAgents(refused, [], 'relative-home');
|
|
159
|
+
assert.equal(relative.status, 1);
|
|
160
|
+
assert.match(relative.stderr, /CODEX_HOME must be an absolute path/);
|
|
161
|
+
assert.ok(!fs.existsSync(path.join(refused.repo, 'relative-home')) && !fs.existsSync(path.join(refused.home, '.codex')), 'a refused CODEX_HOME still wrote');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('unreadable-kept: a file it cannot read is kept and the other agents still install', { skip: process.getuid?.() === 0 }, () => {
|
|
165
|
+
const ctx = setup();
|
|
166
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
167
|
+
const locked = path.join(ctx.agents, 'debugger.toml');
|
|
168
|
+
fs.writeFileSync(locked, 'mine\n');
|
|
169
|
+
fs.chmodSync(locked, 0o000);
|
|
170
|
+
try {
|
|
171
|
+
const result = setupAgents(ctx);
|
|
172
|
+
assert.equal(result.status, 0, result.stderr);
|
|
173
|
+
assert.match(result.stdout, /^kept \(unreadable\): .*debugger\.toml$/m);
|
|
174
|
+
assert.equal(fs.readdirSync(ctx.agents).length, fs.readdirSync(path.join(tree, 'agents')).length);
|
|
175
|
+
} finally {
|
|
176
|
+
fs.chmodSync(locked, 0o600);
|
|
177
|
+
}
|
|
178
|
+
assert.equal(fs.readFileSync(locked, 'utf8'), 'mine\n');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('symlink-kept: a symlinked agent stays a symlink and its target is untouched', () => {
|
|
182
|
+
const ctx = setup();
|
|
183
|
+
fs.mkdirSync(ctx.agents, { recursive: true });
|
|
184
|
+
const elsewhere = path.join(ctx.home, 'dotfiles-researcher.toml');
|
|
185
|
+
fs.writeFileSync(elsewhere, olderOwn('researcher.toml'));
|
|
186
|
+
const before = fs.readFileSync(elsewhere, 'utf8');
|
|
187
|
+
fs.symlinkSync(elsewhere, path.join(ctx.agents, 'researcher.toml'));
|
|
188
|
+
const result = setupAgents(ctx);
|
|
189
|
+
assert.equal(result.status, 0, result.stderr);
|
|
190
|
+
assert.ok(fs.lstatSync(path.join(ctx.agents, 'researcher.toml')).isSymbolicLink());
|
|
191
|
+
assert.equal(fs.readlinkSync(path.join(ctx.agents, 'researcher.toml')), elsewhere);
|
|
192
|
+
assert.equal(fs.readFileSync(elsewhere, 'utf8'), before);
|
|
193
|
+
assert.match(result.stdout, /^kept \(symlink\): .*researcher\.toml$/m);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('repo-collision-reported: an agent of the same name in the repository is reported', () => {
|
|
197
|
+
const ctx = setup();
|
|
198
|
+
fs.mkdirSync(path.join(ctx.repo, '.codex/agents'), { recursive: true });
|
|
199
|
+
fs.writeFileSync(path.join(ctx.repo, '.codex/agents/test_runner.toml'), 'name = "test_runner"\n');
|
|
200
|
+
const result = setupAgents(ctx);
|
|
201
|
+
assert.equal(result.status, 0, result.stderr);
|
|
202
|
+
assert.match(result.stdout, /^collision: .*\.codex\/agents\/test_runner\.toml/m);
|
|
203
|
+
assert.equal((result.stdout.match(/^collision: /gm) || []).length, 1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('skill-explicit-no-target: the skill is explicit-only and the script refuses any argument', () => {
|
|
207
|
+
const skill = fs.readFileSync(path.join(tree, 'skills/setup-agents/SKILL.md'), 'utf8');
|
|
208
|
+
assert.match(skill, /^---\nname: cf-setup-agents\n/);
|
|
209
|
+
assert.match(skill, /\{cafekit:setup-agents\}/);
|
|
210
|
+
assert.equal(fs.readFileSync(path.join(tree, 'skills/setup-agents', CODEX_POLICY_FILE), 'utf8'), CODEX_EXPLICIT_ONLY_POLICY);
|
|
211
|
+
const ctx = setup();
|
|
212
|
+
const target = path.join(ctx.home, 'elsewhere');
|
|
213
|
+
const result = setupAgents(ctx, [target]);
|
|
214
|
+
assert.equal(result.status, 2);
|
|
215
|
+
assert.ok(!fs.existsSync(target) && !fs.existsSync(ctx.agents), 'the script wrote with an argument');
|
|
216
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { test } = require('node:test');
|
|
7
|
+
const { normalizeCodexBody, CODEX_SPECS_SCOPE_MARKER, CODEX_SPECS_SCOPE_NOTE,
|
|
8
|
+
CODEX_RESEARCH_DEPTH_MARKER } = require('../lib/codex-install');
|
|
9
|
+
const { splitFrontmatter } = require('../lib/codex-frontmatter');
|
|
10
|
+
|
|
11
|
+
const SKILLS = path.resolve(__dirname, '../../src/claude/skills');
|
|
12
|
+
const specsPath = path.join(SKILLS, 'specs/SKILL.md');
|
|
13
|
+
const specs = fs.readFileSync(specsPath, 'utf8');
|
|
14
|
+
|
|
15
|
+
function skillFiles(directory) {
|
|
16
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
|
|
17
|
+
const file = path.join(directory, entry.name);
|
|
18
|
+
return entry.isDirectory() ? skillFiles(file) : entry.name === 'SKILL.md' ? [file] : [];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertMarker() {
|
|
23
|
+
assert.equal(typeof CODEX_SPECS_SCOPE_MARKER, 'string');
|
|
24
|
+
assert.ok(CODEX_SPECS_SCOPE_MARKER.length > 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('codex specs projection carries the scope note immediately after unchanged frontmatter', () => {
|
|
28
|
+
assertMarker();
|
|
29
|
+
assert.equal(typeof CODEX_SPECS_SCOPE_NOTE, 'string');
|
|
30
|
+
const plain = normalizeCodexBody(specs);
|
|
31
|
+
const projected = normalizeCodexBody(specs, specsPath);
|
|
32
|
+
assert.match(projected, /^---\r?\n/);
|
|
33
|
+
const before = splitFrontmatter(plain), after = splitFrontmatter(projected);
|
|
34
|
+
assert.equal(after.frontmatter.name, 'cf-specs');
|
|
35
|
+
assert.deepEqual(after.frontmatter, before.frontmatter);
|
|
36
|
+
assert.equal(projected.slice(0, projected.length - after.body.length), plain.slice(0, plain.length - before.body.length));
|
|
37
|
+
assert.equal(after.body, `\n${CODEX_SPECS_SCOPE_NOTE}\n\n${before.body}`);
|
|
38
|
+
assert.equal(projected.split(CODEX_SPECS_SCOPE_MARKER).length - 1, 1);
|
|
39
|
+
assert.equal(normalizeCodexBody(projected, specsPath), projected);
|
|
40
|
+
assert.ok(CODEX_SPECS_SCOPE_NOTE.includes(CODEX_SPECS_SCOPE_MARKER));
|
|
41
|
+
assert.match(CODEX_SPECS_SCOPE_MARKER, /GATE-SCOPE.*final answer/);
|
|
42
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /not in commentary or through `request_user_input`/);
|
|
43
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /Do not send a summary after/);
|
|
44
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /\[NEEDS CLARIFICATION: <question>\]/);
|
|
45
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /real question.*not a placeholder/);
|
|
46
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /EXPAND, KEEP, and CUT together/);
|
|
47
|
+
assert.match(CODEX_SPECS_SCOPE_NOTE, /Preserve.*NEEDS CLARIFICATION.*EXPAND.*KEEP.*CUT.*do not translate/);
|
|
48
|
+
assert.doesNotMatch(CODEX_SPECS_SCOPE_NOTE, /\b(?:Write|Edit|Read|Bash|WebSearch|WebFetch|AskUserQuestion)\b/);
|
|
49
|
+
assert.doesNotMatch(CODEX_SPECS_SCOPE_NOTE, /customer|sales|notif|khách|bán hàng|thông báo/i);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('specs marker is absent from every other skill and research retains exactly one depth marker', () => {
|
|
53
|
+
assertMarker();
|
|
54
|
+
const others = skillFiles(SKILLS).filter(file => file !== specsPath);
|
|
55
|
+
for (const name of ['ask', 'research']) assert.ok(others.includes(path.join(SKILLS, name, 'SKILL.md')));
|
|
56
|
+
for (const file of others) {
|
|
57
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
58
|
+
const projected = normalizeCodexBody(source, file);
|
|
59
|
+
assert.ok(!projected.includes(CODEX_SPECS_SCOPE_MARKER), file);
|
|
60
|
+
if (file === path.join(SKILLS, 'research/SKILL.md')) {
|
|
61
|
+
assert.equal(projected.split(CODEX_RESEARCH_DEPTH_MARKER).length - 1, 1);
|
|
62
|
+
} else if (file !== path.join(SKILLS, 'ask/SKILL.md')) {
|
|
63
|
+
assert.equal(projected, normalizeCodexBody(source), file);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('specs note is scoped to the exact skill source path and supports Windows separators', () => {
|
|
69
|
+
assertMarker();
|
|
70
|
+
for (const sourcePath of ['', 'skills/specs/references/templates.md', 'other-skills/specs/SKILL.md']) {
|
|
71
|
+
const projected = normalizeCodexBody(specs, sourcePath);
|
|
72
|
+
assert.ok(!projected.includes(CODEX_SPECS_SCOPE_MARKER), sourcePath);
|
|
73
|
+
assert.equal(projected, normalizeCodexBody(specs), sourcePath);
|
|
74
|
+
}
|
|
75
|
+
assert.ok(normalizeCodexBody(specs, 'C:\\project\\skills\\specs\\SKILL.md').includes(CODEX_SPECS_SCOPE_MARKER));
|
|
76
|
+
});
|
|
@@ -78,7 +78,7 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
|
|
|
78
78
|
fs.copyFileSync(path.join(hooksRoot, hook), target);
|
|
79
79
|
}
|
|
80
80
|
// Hook helpers the raw-copied hooks require at runtime.
|
|
81
|
-
for (const helper of ['hook-state-dir.cjs', 'runtime-dir.cjs', 'hook-payload.cjs']) {
|
|
81
|
+
for (const helper of ['hook-state-dir.cjs', 'runtime-dir.cjs', 'hook-payload.cjs', 'project-root.cjs']) {
|
|
82
82
|
const target = path.join(destinationRoot, 'hooks', 'lib', helper);
|
|
83
83
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
84
84
|
fs.copyFileSync(path.join(hooksRoot, 'lib', helper), target);
|
|
@@ -47,29 +47,34 @@ function makeStale(root) {
|
|
|
47
47
|
execSync('git -c user.email=t@t -c user.name=t add -A && git -c user.email=t@t -c user.name=t commit -qm fixture', { cwd: root });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
for (const [runtime, folder, config] of [['claude', '.claude', 'settings.json'], ['codex', '.codex', 'hooks.json']]) {
|
|
51
|
+
test(`fresh ${runtime} install runs no docs-sync at SessionStart`, () => {
|
|
52
|
+
inTempProject((root) => {
|
|
53
|
+
install(root, [runtime]);
|
|
54
|
+
const settings = JSON.parse(fs.readFileSync(path.join(root, folder, config), 'utf8'));
|
|
55
|
+
assert.equal(commands(settings, 'SessionStart').some((c) => c.includes('docs-sync.cjs')), false);
|
|
56
|
+
assert.equal(fs.existsSync(path.join(root, folder, 'hooks', 'docs-sync.cjs')), true, 'the hook stays installed for on-demand use');
|
|
57
|
+
});
|
|
56
58
|
});
|
|
57
|
-
});
|
|
58
59
|
|
|
59
|
-
test(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
test(`${runtime} upgrade drops the managed SessionStart docs-sync entry`, () => {
|
|
61
|
+
inTempProject((root) => {
|
|
62
|
+
install(root, [runtime]);
|
|
63
|
+
const settingsPath = path.join(root, folder, config);
|
|
64
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
65
|
+
const kept = commands(settings, 'SessionStart');
|
|
66
|
+
const command = runtime === 'claude'
|
|
67
|
+
? 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/docs-sync.cjs"'
|
|
68
|
+
: 'node ".codex/hooks/docs-sync.cjs"';
|
|
69
|
+
settings.hooks.SessionStart[0].hooks.push({ type: 'command', command });
|
|
70
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
71
|
+
install(root, [runtime]);
|
|
72
|
+
const after = commands(JSON.parse(fs.readFileSync(settingsPath, 'utf8')), 'SessionStart');
|
|
73
|
+
assert.equal(after.some((c) => c.includes('docs-sync.cjs')), false);
|
|
74
|
+
for (const command of kept) assert.ok(after.includes(command), `kept: ${command}`);
|
|
75
|
+
});
|
|
71
76
|
});
|
|
72
|
-
}
|
|
77
|
+
}
|
|
73
78
|
|
|
74
79
|
test('the docs skill runs docs-sync on demand', () => {
|
|
75
80
|
for (const rel of ['SKILL.md', 'references/update-workflow.md']) {
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
// Invoke installed-layout hooks with stdin JSON: denial must be real, not a crash.
|
|
12
|
+
function fixture(host, run, variant = '') {
|
|
13
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-inspect-'));
|
|
14
|
+
const root = path.join(temp, 'build/app');
|
|
15
|
+
const link = path.join(temp, 'project-link');
|
|
16
|
+
const hooks = path.join(root, `.${host}/hooks`);
|
|
17
|
+
try {
|
|
18
|
+
fs.mkdirSync(root, { recursive: true });
|
|
19
|
+
fs.symlinkSync(root, link, 'dir');
|
|
20
|
+
fs.cpSync(path.join(SOURCE, `${host}/hooks`), hooks, { recursive: true });
|
|
21
|
+
if (host === 'codex') {
|
|
22
|
+
for (const name of ['runtime-path-safety', 'privacy-command-analysis', 'privacy-paths', 'secret-keywords', 'inspect-paths']) {
|
|
23
|
+
const source = path.join(SOURCE, `claude/hooks/lib/${name}.cjs`);
|
|
24
|
+
// Before the shared lib exists, exercise the original native implementation.
|
|
25
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(hooks, `lib/${name}.cjs`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const cwd = variant === 'nested' ? path.join(root, 'packages/x') : root;
|
|
29
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
30
|
+
const projectRoot = variant === 'symlink' ? link : root;
|
|
31
|
+
run({ root, temp, cwd, hook(tool_name, tool_input, payload = {}) {
|
|
32
|
+
const log = path.join(hooks, '.logs/hook-log.jsonl');
|
|
33
|
+
const before = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
|
|
34
|
+
const result = spawnSync(process.execPath, [path.join(hooks, 'inspect-block.cjs')], {
|
|
35
|
+
cwd, encoding: 'utf8', input: JSON.stringify({ cwd, tool_name, tool_input, ...payload }),
|
|
36
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectRoot },
|
|
37
|
+
});
|
|
38
|
+
const after = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
|
|
39
|
+
assert.doesNotMatch(after.slice(before.length), /"status":"crash"/, result.stderr);
|
|
40
|
+
return result;
|
|
41
|
+
} });
|
|
42
|
+
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cases = [];
|
|
46
|
+
function add(name, tool, input, deny = false, variant = '', payload = {}) {
|
|
47
|
+
cases.push({ name, tool, input, deny, variant, payload });
|
|
48
|
+
}
|
|
49
|
+
function shell(command, deny = false) { add(command, 'Bash', { command }, deny); }
|
|
50
|
+
add('Read node_modules', 'Read', { file_path: 'node_modules/x/index.js' }, true);
|
|
51
|
+
add('Read absolute dist', 'Read', ({ root }) => ({ file_path: `${root}/dist/a.js` }), true);
|
|
52
|
+
add('Glob broad', 'Glob', { pattern: '**/*' }, true);
|
|
53
|
+
add('Glob dist', 'Glob', { pattern: '*.js', path: 'dist' }, true);
|
|
54
|
+
add('Grep path', 'Grep', { pattern: 'foo', path: 'node_modules' }, true);
|
|
55
|
+
add('Grep glob', 'Grep', { pattern: 'foo', glob: 'dist/**' }, true);
|
|
56
|
+
for (const command of [
|
|
57
|
+
'cat dist/index.js', 'nl -ba dist/a.js', 'sed -n 1,5p build/x',
|
|
58
|
+
'grep -rn foo node_modules', 'rg foo dist', 'find ./dist', 'ls node_modules',
|
|
59
|
+
'npm exec cat ./dist/index.js', 'npx cat dist/a.js', 'npm exec -- cat dist/a',
|
|
60
|
+
'FOO=1 cat dist/a', 'env FOO=1 cat dist/a', 'sudo -u root cat dist/a',
|
|
61
|
+
'vite build --outDir ./dist $(cat dist/index.js)', 'echo `cat dist/a`', 'echo "$(cat dist/a)"',
|
|
62
|
+
]) shell(command, true);
|
|
63
|
+
add('Bash tilde path', 'Bash', ({ root }) => ({ command: `cat ~/${path.relative(os.homedir(), root)}/dist/a` }), true);
|
|
64
|
+
add('missing tool name', undefined, { path: './dist/index.js' }, true);
|
|
65
|
+
add('exec_command reader', 'exec_command', { cmd: 'cat dist/index.js' }, true);
|
|
66
|
+
for (const tool of ['Edit', 'Write', 'MultiEdit']) {
|
|
67
|
+
add(`${tool} write`, tool, ({ root }) => ({ file_path: `${root}/scripts/build/release.js` }));
|
|
68
|
+
}
|
|
69
|
+
add('NotebookEdit write', 'NotebookEdit', { notebook_path: 'dist/a.ipynb' });
|
|
70
|
+
add('patch package main', 'apply_patch', { command: '*** Begin Patch\n*** Update File: package.json\n+"main": "./dist/index.js"\n*** End Patch' });
|
|
71
|
+
add('patch Add File', 'apply_patch', { command: '*** Begin Patch\n*** Add File: src/build/config.ts\n+x\n*** End Patch' });
|
|
72
|
+
for (const pattern of ['target', 'node_modules']) add(`Grep regex ${pattern}`, 'Grep', { pattern });
|
|
73
|
+
for (const command of [
|
|
74
|
+
'.agents/skills/.venv/bin/python3 scripts/search.py q', '.claude/skills/.venv/bin/python3 scripts/x.py',
|
|
75
|
+
'python3 -m venv .venv', 'source .venv/bin/activate && pytest',
|
|
76
|
+
'node_modules/.bin/eslint src', 'npx eslint dist/', 'python3 scripts/x.py build/out',
|
|
77
|
+
'tsc --outDir ./build', 'vite build --outDir ./dist', 'npm run build -- --outDir ./dist',
|
|
78
|
+
'cargo build --target-dir ./target', 'cd build && cmake ..', 'tree -I node_modules',
|
|
79
|
+
'grep -rn foo . --exclude-dir node_modules', "rg --glob '!**/node_modules/**' foo",
|
|
80
|
+
'find . -path ./node_modules -prune -o -print', 'echo concat vendor', "echo '$(cat dist/a)'",
|
|
81
|
+
"git commit -F - <<'EOF'\nls node_modules cleanup\nEOF",
|
|
82
|
+
]) shell(command);
|
|
83
|
+
add('Grep excluded glob', 'Grep', { pattern: 'foo', glob: '!dist/**' });
|
|
84
|
+
for (const variant of ['', 'nested', 'symlink']) {
|
|
85
|
+
add(`project above blocked segment ${variant || 'root'}`, 'Read', ({ root }) => ({ file_path: `${fs.realpathSync(root)}/src/a.js` }), false, variant);
|
|
86
|
+
}
|
|
87
|
+
add('Read ordinary', 'Read', { file_path: 'src/a.js' });
|
|
88
|
+
shell('cat src/a.js');
|
|
89
|
+
|
|
90
|
+
// Wrapper options, nested substitutions, and structured paths.
|
|
91
|
+
for (const command of [
|
|
92
|
+
'time -p env -u FOO BAR=1 command -- cat dist/a', 'nice -n 5 nohup cat dist/a',
|
|
93
|
+
'xargs -I {} cat dist/a', 'pnpm exec -- cat dist/a', 'pnpx --yes cat dist/a',
|
|
94
|
+
'yarn exec -- cat dist/a', 'bunx --bun cat dist/a', 'npx -p cat -- cat dist/a',
|
|
95
|
+
'echo "$(echo $(cat dist/a))"', 'echo ok; cat dist/a', 'echo ok\ncat dist/a',
|
|
96
|
+
'echo ok | cat dist/a', 'rg -e foo dist/a', 'awk \'{print $1}\' dist/a',
|
|
97
|
+
'less dist/a', 'more dist/a', 'bat dist/a', 'wc dist/a', 'du dist', 'file dist/a', 'stat dist/a',
|
|
98
|
+
'cat -n dist/a', 'wc -c dist/a', 'du -s dist', 'ls -L dist',
|
|
99
|
+
'echo $( (cat dist/a) )', 'env -S "cat dist/a"', 'env --split-string="cat" dist/a',
|
|
100
|
+
'npm exec -c "cat dist/a"', 'npx --call="cat dist/a"',
|
|
101
|
+
'cat src/a <<\\EOF\nprose\nEOF\ncat dist/a', "cat src/a <<E'OF'\nprose\nEOF\ncat dist/a",
|
|
102
|
+
]) shell(command, true);
|
|
103
|
+
for (const command of [
|
|
104
|
+
'grep --include node_modules foo src', 'rg -e node_modules src',
|
|
105
|
+
'rg --exclude-dir=node_modules foo src', 'tree -P dist',
|
|
106
|
+
'echo "cat dist/a"', "echo '`cat dist/a`'", 'cat src/a.js <<EOF\ncat dist/a\nEOF',
|
|
107
|
+
]) shell(command);
|
|
108
|
+
add('nested paths', 'mcp__files__read', { operations: [{ paths: ['dist/a'] }] }, true);
|
|
109
|
+
add('notebook path reader', 'Read', { notebook_path: 'dist/a.ipynb' }, true);
|
|
110
|
+
add('unknown command reader', 'mcp__shell', { command: 'cat dist/a' }, true);
|
|
111
|
+
add('missing name command reader', undefined, { command: 'cat dist/a' }, true);
|
|
112
|
+
add('write skips command too', 'Write', { file_path: 'dist/a', command: 'cat dist/a' });
|
|
113
|
+
|
|
114
|
+
// Output redirections are writes; search option values are not patterns.
|
|
115
|
+
for (const command of [
|
|
116
|
+
"cat > dist/index.html <<'EOF'\n<html></html>\nEOF",
|
|
117
|
+
'ls src > build/files.txt', 'wc -l src/a.ts > coverage/summary.txt',
|
|
118
|
+
'grep x src 2> build/err', 'ls src >> build/x', 'ls src 2>&1', 'ls src>build/x',
|
|
119
|
+
'rg --max-depth 2 target src', 'rg -j 4 build src', 'rg -M 200 dist src',
|
|
120
|
+
'rg --sort path target src', 'grep -rn -d skip build src',
|
|
121
|
+
]) shell(command);
|
|
122
|
+
for (const command of ['cat < dist/a', 'cat <dist/a']) shell(command, true);
|
|
123
|
+
|
|
124
|
+
add('Grep glob:"**/*.ts", path:"src"', 'Grep', { pattern: 'x', glob: '**/*.ts', path: 'src' });
|
|
125
|
+
add('Glob pattern:"**/*.ts", path:"src"', 'Glob', { pattern: '**/*.ts', path: 'src' });
|
|
126
|
+
shell("rg -g '**/*.ts' x src");
|
|
127
|
+
add('Glob broad without path', 'Glob', { pattern: '**/*' }, true);
|
|
128
|
+
add('Grep glob:"**/*.ts", path:"."', 'Grep', { pattern: 'x', glob: '**/*.ts', path: '.' }, true);
|
|
129
|
+
add('Glob broad absolute project root', 'Glob', ({ root }) => ({ pattern: '**/*.ts', path: root }), true);
|
|
130
|
+
add('Glob broad path:".."', 'Glob', { pattern: '**/*', path: '..' }, true);
|
|
131
|
+
add('Glob broad filesystem root', 'Glob', { pattern: '**/*', path: '/' }, true);
|
|
132
|
+
add('Glob broad home', 'Glob', { pattern: '**/*', path: '~' }, true);
|
|
133
|
+
add('Glob broad normalized project root', 'Glob', { pattern: '**/*', path: 'src/..' }, true);
|
|
134
|
+
add('Glob broad blocked directory', 'Glob', { pattern: '**/*.ts', path: 'node_modules' }, true);
|
|
135
|
+
shell("rg --glob '**/*' symbol .", true);
|
|
136
|
+
shell("rg --glob '**/*' symbol; cat src/file.ts", true);
|
|
137
|
+
shell("cd ..; rg --glob '**/*' symbol src", true);
|
|
138
|
+
shell("env -C .. rg --glob '**/*' symbol src", true);
|
|
139
|
+
for (const command of [
|
|
140
|
+
"rg -E utf8 -g '**/*' x", "rg --ignore-file .gitignore -g '**/*' x",
|
|
141
|
+
"rg --engine pcre2 -g '**/*' x", "rg --type-add 'web:*.html' -g '**/*' x",
|
|
142
|
+
"echo . | xargs rg -g '**/*' x src", "env -C.. rg -g '**/*' x src",
|
|
143
|
+
"builtin cd .. && rg -g '**/*' x src",
|
|
144
|
+
]) shell(command, true);
|
|
145
|
+
add('Bash broad glob with non-string session cwd', 'Bash', { command: "rg -g '**/*' x src" }, true, '', { cwd: 1 });
|
|
146
|
+
add('Bash broad glob with non-string input cwd', 'Bash', { command: "rg -g '**/*' x src", cwd: 1 }, true);
|
|
147
|
+
for (const key of ['cwd', 'workdir']) {
|
|
148
|
+
add(`Bash broad glob with ${key}:".."`, 'Bash', { command: "rg -g '**/*' x src", [key]: '..' }, true);
|
|
149
|
+
}
|
|
150
|
+
for (const flag of [
|
|
151
|
+
'--json', '-S', '--smart-case', '-N', '--no-heading', '--heading', '--column', '--vimgrep',
|
|
152
|
+
'--follow', '--no-ignore', '--no-ignore-dot', '--no-ignore-exclude', '--no-ignore-files',
|
|
153
|
+
'--no-ignore-global', '--no-ignore-parent', '--no-ignore-vcs', '--no-ignore-messages',
|
|
154
|
+
'--no-ignore-file-case-insensitive', '--count-matches', '-b', '--trim',
|
|
155
|
+
'-C3', '-A2', '-m5', '-tts',
|
|
156
|
+
]) shell(`rg -g '**/*.ts' ${flag} x src`);
|
|
157
|
+
for (const command of [
|
|
158
|
+
"eval 'cd ..'; rg -g '**/*' x src", "source setup.sh; rg -g '**/*' x src",
|
|
159
|
+
". setup.sh; rg -g '**/*' x src", "env -iC.. rg -g '**/*' x src",
|
|
160
|
+
]) shell(command, true);
|
|
161
|
+
|
|
162
|
+
for (const host of ['claude', 'codex']) {
|
|
163
|
+
for (const entry of cases) {
|
|
164
|
+
if (host === 'codex' && entry.payload.cwd === 1) continue;
|
|
165
|
+
test(`inspect-paths ${host}: ${entry.name}`, () => fixture(host, (context) => {
|
|
166
|
+
const input = typeof entry.input === 'function' ? entry.input(context) : entry.input;
|
|
167
|
+
const result = context.hook(entry.tool, input, entry.payload);
|
|
168
|
+
assert.equal(result.status, entry.deny ? 2 : 0, result.stderr || result.stdout);
|
|
169
|
+
if (entry.deny) {
|
|
170
|
+
if (host === 'claude') assert.equal(JSON.parse(result.stdout).hookSpecificOutput.permissionDecision, 'deny');
|
|
171
|
+
assert.match(result.stderr, /SCOPE LIMIT EXCEEDED/);
|
|
172
|
+
} else assert.equal(result.stdout, '');
|
|
173
|
+
}, entry.variant));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
test('inspect-paths lib blocks broad glob with non-string context.cwd', () => {
|
|
178
|
+
const { inspectInput } = require('../../src/claude/hooks/lib/inspect-paths.cjs');
|
|
179
|
+
const result = inspectInput('Bash', { command: "rg -g '**/*' x src" }, {
|
|
180
|
+
cwd: 1, projectRoot: process.cwd(),
|
|
181
|
+
});
|
|
182
|
+
assert.equal(result.broadGlob, '**/*');
|
|
183
|
+
});
|