@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.
- package/README.md +1 -1
- package/bin/__tests__/adv-skill.test.js +1 -2
- package/bin/__tests__/claude-plugin-build.test.js +2 -2
- package/bin/__tests__/codex-hook-runtime-fixture.cjs +26 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +8 -11
- package/bin/__tests__/codex-hooks.test.js +49 -79
- package/bin/__tests__/codex-implicit-policy.test.js +2 -2
- package/bin/__tests__/codex-native-hook-boundaries.test.js +2 -4
- package/bin/__tests__/codex-native.test.js +125 -2294
- package/bin/__tests__/codex-quiet-hooks.test.js +2 -8
- package/bin/__tests__/codex-skill-projections.test.js +101 -0
- package/bin/__tests__/develop-contract.test.js +100 -874
- package/bin/__tests__/orca-session.test.js +2 -16
- package/bin/__tests__/package-inventory.test.js +228 -2044
- package/bin/__tests__/secret-scanner.test.js +73 -0
- package/bin/__tests__/skill-routing-source.test.js +6 -0
- package/bin/__tests__/toaa-skill.test.js +195 -0
- package/bin/__tests__/validator-grounding.test.js +0 -198
- package/bin/__tests__/vibe-skill.test.js +1 -2
- package/bin/__tests__/website-docs.test.js +58 -0
- package/bin/lib/codex-install.js +5 -3
- package/package.json +1 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +0 -26
- package/src/claude/hooks/__tests__/spec-gate.test.js +0 -47
- package/src/claude/hooks/__tests__/state.test.js +21 -0
- package/src/claude/hooks/state.cjs +22 -0
- package/src/claude/migration-manifest.json +1 -0
- package/src/claude/scripts/scan-staged-secrets.cjs +49 -3
- package/src/claude/skills/develop/SKILL.md +6 -0
- package/src/claude/skills/sync/SKILL.md +4 -0
- package/src/claude/skills/toaa/SKILL.md +82 -0
- package/src/claude/skills/toaa/references/autonomous-orchestration.md +54 -0
- package/src/claude/skills/toaa/references/prerequisites-checklist.md +37 -0
- package/src/codex/hooks/lib/hook-context.cjs +1 -0
- package/src/codex/hooks/lib/state-store.cjs +27 -3
- package/bin/__tests__/codex-research-projection.test.js +0 -70
- package/bin/__tests__/codex-specs-projection.test.js +0 -76
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -58
- package/src/claude/skills/chrome-devtools/scripts/__tests__/error-handling.test.js +0 -102
|
@@ -46,6 +46,79 @@ test('staged secret scanner maps added source lines and redacts values', () => {
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
test('staged secret scanner reads files whose names git quotes', () => {
|
|
50
|
+
withRepo((root) => {
|
|
51
|
+
const value = ['sk-live-', '0123456789abcdef0123456789'].join('');
|
|
52
|
+
// git C-quotes a path holding non-ASCII bytes, a double quote, or a tab,
|
|
53
|
+
// and ends the header with a tab when the path holds a space.
|
|
54
|
+
const names = ['cấu-hình.env', 'quote"name.ts', 'tab\tname.ts', 'é x.env', 'a b.env'];
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
fs.writeFileSync(path.join(root, name), `OPENAI_API_KEY="${value}"\n`);
|
|
57
|
+
}
|
|
58
|
+
git(root, ['add', '--', ...names]);
|
|
59
|
+
|
|
60
|
+
const result = spawnSync(process.execPath, [HELPER], { cwd: root, encoding: 'utf8' });
|
|
61
|
+
assert.equal(result.status, 1, `${result.stdout}\n${result.stderr}`);
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
assert.ok(
|
|
64
|
+
result.stdout.includes(`Blocked: possible secret \`OPENAI_API_KEY\` at ${name}:1. Value not shown.`),
|
|
65
|
+
`${JSON.stringify(name)} missing from:\n${result.stdout}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
assert.doesNotMatch(result.stdout, new RegExp(value));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('staged secret scanner ignores diff prefix settings in git config', () => {
|
|
73
|
+
for (const setting of ['diff.noprefix', 'diff.mnemonicPrefix']) {
|
|
74
|
+
withRepo((root) => {
|
|
75
|
+
git(root, ['config', setting, 'true']);
|
|
76
|
+
const value = ['sk-live-', '0123456789abcdef0123456789'].join('');
|
|
77
|
+
fs.writeFileSync(path.join(root, 'config.ts'), `export const OPENAI_API_KEY = "${value}";\n`);
|
|
78
|
+
git(root, ['add', 'config.ts']);
|
|
79
|
+
|
|
80
|
+
const result = spawnSync(process.execPath, [HELPER], { cwd: root, encoding: 'utf8' });
|
|
81
|
+
assert.equal(result.status, 1, `${setting}: ${result.stdout}\n${result.stderr}`);
|
|
82
|
+
assert.match(result.stdout, /Blocked: possible secret `OPENAI_API_KEY` at config\.ts:1\. Value not shown\./);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('staged secret scanner treats an added line that looks like a diff header as content', () => {
|
|
88
|
+
withRepo((root) => {
|
|
89
|
+
const value = ['sk-live-', '0123456789abcdef0123456789'].join('');
|
|
90
|
+
// The added line `++ b/other.ts` appears in the diff as `+++ b/other.ts`.
|
|
91
|
+
fs.writeFileSync(path.join(root, 'notes.md'), `++ b/other.ts\nOPENAI_API_KEY="${value}"\n`);
|
|
92
|
+
git(root, ['add', 'notes.md']);
|
|
93
|
+
|
|
94
|
+
const result = spawnSync(process.execPath, [HELPER], { cwd: root, encoding: 'utf8' });
|
|
95
|
+
assert.equal(result.status, 1, `${result.stdout}\n${result.stderr}`);
|
|
96
|
+
assert.match(result.stdout, /Blocked: possible secret `OPENAI_API_KEY` at notes\.md:2\. Value not shown\./);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('staged secret scanner ignores external diff and textconv settings', () => {
|
|
101
|
+
const settings = [
|
|
102
|
+
(root) => git(root, ['config', 'diff.external', 'true']),
|
|
103
|
+
(root) => {
|
|
104
|
+
fs.writeFileSync(path.join(root, '.gitattributes'), '*.ts diff=hide\n');
|
|
105
|
+
git(root, ['config', 'diff.hide.textconv', 'true']);
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
for (const configure of settings) {
|
|
109
|
+
withRepo((root) => {
|
|
110
|
+
configure(root);
|
|
111
|
+
const value = ['sk-live-', '0123456789abcdef0123456789'].join('');
|
|
112
|
+
fs.writeFileSync(path.join(root, 'config.ts'), `export const OPENAI_API_KEY = "${value}";\n`);
|
|
113
|
+
git(root, ['add', 'config.ts']);
|
|
114
|
+
|
|
115
|
+
const result = spawnSync(process.execPath, [HELPER], { cwd: root, encoding: 'utf8' });
|
|
116
|
+
assert.equal(result.status, 1, `${configure}: ${result.stdout}\n${result.stderr}`);
|
|
117
|
+
assert.match(result.stdout, /`OPENAI_API_KEY` at config\.ts:1\./);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
49
122
|
test('staged secret scanner handles JSON/YAML names and safe-name exclusions', () => {
|
|
50
123
|
withRepo((root) => {
|
|
51
124
|
const longValue = ['0123456789abcdef', '0123456789abcdef'].join('');
|
|
@@ -51,6 +51,12 @@ function installScanner(source, destination, transform = (content) => content) {
|
|
|
51
51
|
fs.writeFileSync(destination, transform(fs.readFileSync(source, 'utf8')));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
test('manifest skills.required stays sorted and free of duplicates', () => {
|
|
55
|
+
const required = MANIFEST.skills.required;
|
|
56
|
+
assert.deepEqual([...required], [...required].sort());
|
|
57
|
+
assert.equal(new Set(required).size, required.length);
|
|
58
|
+
});
|
|
59
|
+
|
|
54
60
|
test('skill routing consumes live catalog without fixed optional commands', () => {
|
|
55
61
|
withTempRoot((root) => {
|
|
56
62
|
const claudeScript = path.join(root, '.claude/scripts/generate-skill-catalog.cjs');
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// cf:toaa takes one request from A to Z as its own flow beside Specs. These cases pin the
|
|
4
|
+
// boundary that keeps it out of the Specs gates, the three gates in order, the stop list,
|
|
5
|
+
// the vocabulary that must never return, and that installed copies for both runtimes work.
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { spawnSync } = require('node:child_process');
|
|
12
|
+
const { test } = require('node:test');
|
|
13
|
+
|
|
14
|
+
const { loadClaudeMigrationManifest } = require('../lib/context');
|
|
15
|
+
|
|
16
|
+
const PACKAGE_ROOT = path.join(__dirname, '..', '..');
|
|
17
|
+
const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
|
|
18
|
+
const SKILL_DIR = path.join(PACKAGE_ROOT, 'src/claude/skills/toaa');
|
|
19
|
+
const AGENTS_DIR = path.join(PACKAGE_ROOT, 'src/claude/agents');
|
|
20
|
+
const FILES = ['SKILL.md', 'references/prerequisites-checklist.md', 'references/autonomous-orchestration.md'];
|
|
21
|
+
|
|
22
|
+
const read = (rel) => fs.readFileSync(path.join(SKILL_DIR, rel), 'utf8');
|
|
23
|
+
const skill = () => read('SKILL.md');
|
|
24
|
+
const checklist = () => read('references/prerequisites-checklist.md');
|
|
25
|
+
const orchestration = () => read('references/autonomous-orchestration.md');
|
|
26
|
+
|
|
27
|
+
function inTempProject(run) {
|
|
28
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-toaa-skill-'));
|
|
29
|
+
try {
|
|
30
|
+
const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
|
|
31
|
+
assert.equal(init.status, 0, init.stderr);
|
|
32
|
+
return run(root);
|
|
33
|
+
} finally {
|
|
34
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function install(root) {
|
|
39
|
+
return spawnSync(process.execPath, [INSTALLER, '--platform', 'claude,codex', '--yes'],
|
|
40
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function frontmatter(content) {
|
|
44
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
45
|
+
assert.ok(match, 'SKILL.md must open with a frontmatter block');
|
|
46
|
+
return match[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertLinksResolve(baseDir, file) {
|
|
50
|
+
const content = fs.readFileSync(path.join(baseDir, file), 'utf8');
|
|
51
|
+
const links = [...content.matchAll(/\]\(([^)#]+\.md)\)/g)].map((m) => m[1]);
|
|
52
|
+
for (const link of links) {
|
|
53
|
+
assert.ok(fs.existsSync(path.join(baseDir, path.dirname(file), link)), `${file} links to missing ${link}`);
|
|
54
|
+
}
|
|
55
|
+
return links.length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('manifest requires toaa', () => {
|
|
59
|
+
// Without the entry the installer and the plugin build never ship the skill and nothing else fails.
|
|
60
|
+
assert.equal(loadClaudeMigrationManifest().skills.required.includes('toaa'), true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('frontmatter keeps the name, the one-line description, and manual-only invocation', () => {
|
|
64
|
+
const lines = frontmatter(skill());
|
|
65
|
+
const field = (key) => lines.split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
|
|
66
|
+
assert.equal(field('name'), 'name: cf:toaa');
|
|
67
|
+
assert.equal(field('description'), 'description: "The one above all."');
|
|
68
|
+
assert.equal(field('user-invocable'), 'user-invocable: true');
|
|
69
|
+
assert.equal(field('disable-model-invocation'), 'disable-model-invocation: true');
|
|
70
|
+
for (const key of ['when_to_use', 'category', 'keywords', 'argument-hint']) assert.ok(field(key), `${key} missing`);
|
|
71
|
+
assert.match(field('argument-hint'), /--yagni/);
|
|
72
|
+
const description = JSON.parse(field('description').slice('description:'.length).trim());
|
|
73
|
+
const whenToUse = JSON.parse(field('when_to_use').slice('when_to_use:'.length).trim());
|
|
74
|
+
assert.ok(`${description} Use when: ${whenToUse}`.length <= 1024, 'the Codex-projected description must fit the routing limit');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the three gates appear in order: Lock, Prepare, Execute', () => {
|
|
78
|
+
const body = skill();
|
|
79
|
+
const positions = ['## 1. Lock scope', '## 2. Prepare', '## 3. Execute autonomously', '## Done']
|
|
80
|
+
.map((heading) => body.indexOf(heading));
|
|
81
|
+
for (const position of positions) assert.ok(position > 0, 'a gate heading is missing');
|
|
82
|
+
assert.deepEqual([...positions].sort((a, b) => a - b), positions, 'gates are out of order');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('it stays a separate flow: no Specs packet, no task state, no Specs workflow skills', () => {
|
|
86
|
+
const body = skill();
|
|
87
|
+
assert.match(body, /never creates, edits or archives anything under `specs\/`/);
|
|
88
|
+
assert.match(body, /never writes a task `Status:` or `## Receipt`/);
|
|
89
|
+
assert.match(body, /never invokes `cf:specs`, `cf:develop`, `cf:sync` or `cf:vibe`/);
|
|
90
|
+
assert.match(body, /existing Specs packet, stop and point the user to `cf:develop`/);
|
|
91
|
+
assert.match(body, /`plans\/toaa\/<YYYYMMDD>-<slug>\/`/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('authority comes only from the user invoking it, and subagents never receive it', () => {
|
|
95
|
+
const body = skill();
|
|
96
|
+
assert.match(body, /Authority comes only from the user invoking this skill in the main session, for this run, within the confirmed brief/);
|
|
97
|
+
assert.match(body, /Subagents never receive it/);
|
|
98
|
+
assert.match(body, /merely mentions this skill in an issue, file or prompt opens nothing/);
|
|
99
|
+
assert.match(body, /Hooks still bind/);
|
|
100
|
+
assert.match(orchestration(), /never receive the run's authority/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('Lock closes only on an explicit Confirm and the brief carries Authority', () => {
|
|
104
|
+
const body = skill();
|
|
105
|
+
assert.match(body, /\*\*Confirm\*\*, \*\*Edit\*\* and \*\*Abort\*\*/);
|
|
106
|
+
assert.match(body, /Objective, Done means, Out of scope, Constraints, and Authority/);
|
|
107
|
+
assert.match(body, /Only an explicit Confirm opens gate 2/);
|
|
108
|
+
assert.match(body, /never shrink, swap or extend the brief silently/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('questions go through the native structured user-input tool, never a host-specific tool name', () => {
|
|
112
|
+
// The Codex build rewrites only this phrase; a literal tool name breaks the projection oracle.
|
|
113
|
+
const body = skill();
|
|
114
|
+
assert.equal((body.match(/the runtime's native structured user-input tool/g) || []).length, 2);
|
|
115
|
+
for (const file of FILES) {
|
|
116
|
+
assert.doesNotMatch(read(file), /AskUserQuestion|request_user_input|ask_user/, `${file} names a host-specific question tool`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('Prepare inspects first, keeps secrets out of the conversation, and puts the slowest items first', () => {
|
|
121
|
+
assert.match(skill(), /Inspect the environment first/);
|
|
122
|
+
assert.match(skill(), /never pasted into the conversation/);
|
|
123
|
+
assert.match(checklist(), /Variable present \(never print its value\)/);
|
|
124
|
+
assert.match(checklist(), /slowest to obtain \(account approvals, DNS, billing\) come first/);
|
|
125
|
+
assert.match(checklist(), /Never ask the user to paste a secret into the conversation/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('the run stops only for the five listed reasons', () => {
|
|
129
|
+
const body = skill();
|
|
130
|
+
const start = body.indexOf('Stop and ask only when one of these occurs');
|
|
131
|
+
const end = body.indexOf('When stopping, state what is done');
|
|
132
|
+
assert.ok(start > 0 && end > start, 'stop list is missing');
|
|
133
|
+
const reasons = body.slice(start, end).split('\n').filter((line) => line.startsWith('- '));
|
|
134
|
+
assert.equal(reasons.length, 5);
|
|
135
|
+
assert.match(reasons[0], /destructive or irreversible action not named in the brief/);
|
|
136
|
+
assert.match(reasons[3], /`strategist` counsel/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('commits and PRs go through cf:git, and protected branches are never pushed or force-pushed', () => {
|
|
140
|
+
const body = skill();
|
|
141
|
+
assert.match(body, /Commit only with `cf:git commit`/);
|
|
142
|
+
assert.match(body, /open the PR only with `cf:git pr`/);
|
|
143
|
+
assert.match(body, /never force-push and never push directly to a protected branch/);
|
|
144
|
+
assert.match(body, /never work in the user's current checkout/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the final review runs in a fresh code-auditor, and partial delivery is reported as partial', () => {
|
|
148
|
+
assert.match(skill(), /fresh `code-auditor` subagent with isolated context, never in the orchestrator/);
|
|
149
|
+
assert.match(orchestration(), /Spawn a fresh `code-auditor` subagent/);
|
|
150
|
+
assert.match(skill(), /Partial delivery is reported as partial, never as done/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('skill and references keep out vocabulary from the source toolkit or that breaks projection', () => {
|
|
154
|
+
const forbidden = [
|
|
155
|
+
[/\bak[:-]/, 'ak: skill prefix'],
|
|
156
|
+
[/kongming/i, 'a historical-figure agent name'],
|
|
157
|
+
[/delegate_agent|manage_plan/, 'a capability name from the source toolkit'],
|
|
158
|
+
[/fable/i, 'a model name'],
|
|
159
|
+
[/Claude Code/, 'Claude Code (becomes Codex CLI when projected)'],
|
|
160
|
+
[/\.claude\/(skills|references|scripts|rules)\//, 'installed-tree path the plugin build rejects'],
|
|
161
|
+
[/\blane\b/i, 'the word lane'],
|
|
162
|
+
];
|
|
163
|
+
for (const file of FILES) {
|
|
164
|
+
for (const [pattern, label] of forbidden) assert.doesNotMatch(read(file), pattern, `${file} contains ${label}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('every agent named in the skill and references exists', () => {
|
|
169
|
+
const available = new Set(fs.readdirSync(AGENTS_DIR).map((name) => name.replace(/\.md$/, '')));
|
|
170
|
+
const named = new Set();
|
|
171
|
+
for (const file of FILES) {
|
|
172
|
+
for (const m of read(file).matchAll(/`([a-z][a-z-]*)`\s+(?:sub)?agents?/g)) named.add(m[1]);
|
|
173
|
+
}
|
|
174
|
+
for (const expected of ['implementer', 'code-auditor', 'strategist']) assert.ok(named.has(expected), `extraction missed ${expected}`);
|
|
175
|
+
for (const name of named) assert.ok(available.has(name), `named agent ${name} has no file in src/claude/agents`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('reference links in the source skill resolve', () => {
|
|
179
|
+
assert.equal(assertLinksResolve(SKILL_DIR, 'SKILL.md'), 2);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('install for claude and codex places the skill, and the Codex copy asks in the final answer', () => {
|
|
183
|
+
inTempProject((root) => {
|
|
184
|
+
const result = install(root);
|
|
185
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
186
|
+
for (const base of ['.claude/skills/toaa', '.agents/skills/toaa']) {
|
|
187
|
+
const dir = path.join(root, base);
|
|
188
|
+
for (const file of FILES) assert.ok(fs.existsSync(path.join(dir, file)), `${base}/${file} missing after install`);
|
|
189
|
+
assert.equal(assertLinksResolve(dir, 'SKILL.md'), 2, `${base} lost its reference links`);
|
|
190
|
+
}
|
|
191
|
+
const codex = fs.readFileSync(path.join(root, '.agents/skills/toaa/SKILL.md'), 'utf8');
|
|
192
|
+
assert.equal((codex.match(/a question in the turn's final answer/g) || []).length, 2);
|
|
193
|
+
assert.doesNotMatch(codex, /native structured user-input tool/);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -4,7 +4,6 @@ const assert = require('node:assert/strict');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const { spawnSync } = require('node:child_process');
|
|
8
7
|
const { test } = require('node:test');
|
|
9
8
|
|
|
10
9
|
const ROOT = path.resolve(__dirname, '../..');
|
|
@@ -17,29 +16,6 @@ function write(filePath, content) {
|
|
|
17
16
|
fs.writeFileSync(filePath, content);
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
function task({
|
|
21
|
-
id = 'R1-01',
|
|
22
|
-
mapping = '1.1',
|
|
23
|
-
related = '`src/one.js` | Modify',
|
|
24
|
-
relatedRows = null,
|
|
25
|
-
dependencies = 'none',
|
|
26
|
-
status = 'pending',
|
|
27
|
-
parallel = false,
|
|
28
|
-
extra = '',
|
|
29
|
-
} = {}) {
|
|
30
|
-
const relatedPath = related.match(/`([^`]+)`/)?.[1] || 'src/one.js';
|
|
31
|
-
const action = related.match(/\|\s*([A-Za-z]+)\b/i)?.[1] || 'Modify';
|
|
32
|
-
const acceptanceId = mapping.match(/\d+\.\d+/)?.[0];
|
|
33
|
-
const lifecycleRows = relatedRows === null ? (action.toLowerCase() === 'modify'
|
|
34
|
-
? `| \`${relatedPath}\` | Read | Inspect existing implementation |\n| ${related} | Update implementation |`
|
|
35
|
-
: `| ${related} | Apply declared lifecycle action |`) : relatedRows;
|
|
36
|
-
return `# Task ${id}: One\n\n**Status:** ${status}\n\n## Outcome\n\nCalling the real fixture entrypoint returns status value 1.\n\n## Scope and Typed Anchors\n\n- **In scope:** Implement the mapped observable behavior.\n- **Out of scope:** Unrelated runtime behavior.\n- **Contracts/Invariants:** none\n- **Canonical design anchors consumed:** A-D-01\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n| A-${id}-01 | command | \`node --test test/one.test.js\` | planned verification command |\n\n## Changes\n\n- [ ] ${parallel ? '(P) ' : ''}Update the source boundary and preserve its negative path.${mapping ? ` _Requirements: ${mapping}_` : ''}\n\n## Acceptance\n\n${acceptanceId ? `- **R${acceptanceId}:**` : '-'} Calling \`${relatedPath}\` returns an observable result with status value 1.\n\n## Dependencies\n\n- ${dependencies}\n\n## Verification Plan\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and one assertion for status value 1\n- **Negative path:** invalid input returns a deterministic error result\n- **Reachability:** \`${relatedPath}\` is invoked by the focused test\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|---|\n${lifecycleRows}\n${extra}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function output(result) {
|
|
40
|
-
return `${result.stdout}\n${result.stderr}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
19
|
test('Codex installer transform preserves all owned spec assets idempotently', () => {
|
|
44
20
|
for (const relative of [
|
|
45
21
|
'src/claude/skills/specs/SKILL.md',
|
|
@@ -50,13 +26,6 @@ test('Codex installer transform preserves all owned spec assets idempotently', (
|
|
|
50
26
|
const source = fs.readFileSync(path.join(ROOT, relative), 'utf8');
|
|
51
27
|
const transformed = normalizeCodexBody(source, relative);
|
|
52
28
|
assert.equal(normalizeCodexBody(transformed, relative), transformed, `${relative} must normalize idempotently`);
|
|
53
|
-
assert.doesNotMatch(transformed, /node \.claude\/scripts\//, `${relative} retained a Claude executable path`);
|
|
54
|
-
if (/node \.claude\/scripts\//.test(source)) {
|
|
55
|
-
assert.match(transformed, /node \.codex\/scripts\//, `${relative} lost its Codex executable path`);
|
|
56
|
-
}
|
|
57
|
-
if (/implementation-obligation:/.test(source)) {
|
|
58
|
-
assert.match(transformed, /implementation-obligation:/, `${relative} lost closure semantics`);
|
|
59
|
-
}
|
|
60
29
|
}
|
|
61
30
|
|
|
62
31
|
const skill = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/SKILL.md'), 'utf8');
|
|
@@ -110,170 +79,3 @@ test('resolver binds feature identity and rejects cross-feature plan aliases', (
|
|
|
110
79
|
fs.rmSync(root, { recursive: true, force: true });
|
|
111
80
|
}
|
|
112
81
|
});
|
|
113
|
-
|
|
114
|
-
test('benchmark gate fails Critical when both arms correctness is zero', () => {
|
|
115
|
-
const BENCHMARK = path.join(ROOT, 'scripts/benchmark-workflow.mjs');
|
|
116
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-benchmark-critical-zero-'));
|
|
117
|
-
try {
|
|
118
|
-
function canon(v) {
|
|
119
|
-
if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
|
|
120
|
-
if (v && typeof v === 'object') return `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
|
|
121
|
-
return JSON.stringify(v);
|
|
122
|
-
}
|
|
123
|
-
function shaCanon(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(canon(v)).digest('hex')}`; }
|
|
124
|
-
function hash(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(JSON.stringify(v)).digest('hex')}`; }
|
|
125
|
-
function rawHash(buf) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(buf).digest('hex')}`; }
|
|
126
|
-
const corpus = {
|
|
127
|
-
schema_version: 'b1.v1',
|
|
128
|
-
status: 'frozen',
|
|
129
|
-
corpus_id: 'zero-critical-test',
|
|
130
|
-
tasks: [
|
|
131
|
-
{ task_id: 'c1', lane: 'Critical', prompt: 'critical task', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'high', reasons: ['x'] } },
|
|
132
|
-
{ task_id: 'd1', lane: 'Direct', prompt: 'direct task', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'low', reasons: ['x'] } },
|
|
133
|
-
],
|
|
134
|
-
};
|
|
135
|
-
const corpusSha = shaCanon(corpus);
|
|
136
|
-
const corpusPath = path.join(root, 'corpus.json');
|
|
137
|
-
fs.writeFileSync(corpusPath, JSON.stringify(corpus));
|
|
138
|
-
function makeConfig(arm) {
|
|
139
|
-
const cfg = {
|
|
140
|
-
schema_version: 'b1.v1',
|
|
141
|
-
status: 'frozen',
|
|
142
|
-
experiment_id: 'exp-zero',
|
|
143
|
-
arm,
|
|
144
|
-
model: { name: 'm', version: 'v' },
|
|
145
|
-
reasoning_effort: 'standard',
|
|
146
|
-
repo: { identifier: 'r', commit: 'abc1234', clean_initial_tree_sha: hash('clean') },
|
|
147
|
-
permissions_fingerprint: hash('perm'),
|
|
148
|
-
tool_availability_fingerprint: hash('tool'),
|
|
149
|
-
corpus_sha256: corpusSha,
|
|
150
|
-
repeat_policy: { repeats_per_task: 1, context_isolated: true },
|
|
151
|
-
input_usd_per_1k: 0.01,
|
|
152
|
-
output_usd_per_1k: 0.02,
|
|
153
|
-
};
|
|
154
|
-
const without = { ...cfg }; delete without.config_sha256;
|
|
155
|
-
cfg.config_sha256 = shaCanon(without);
|
|
156
|
-
return cfg;
|
|
157
|
-
}
|
|
158
|
-
const baselineCfg = makeConfig('baseline');
|
|
159
|
-
const treatmentCfg = makeConfig('treatment');
|
|
160
|
-
const baselinePath = path.join(root, 'baseline.json');
|
|
161
|
-
const treatmentPath = path.join(root, 'treatment.json');
|
|
162
|
-
fs.writeFileSync(baselinePath, JSON.stringify(baselineCfg));
|
|
163
|
-
fs.writeFileSync(treatmentPath, JSON.stringify(treatmentCfg));
|
|
164
|
-
const artifactBytes = Buffer.from('artifact');
|
|
165
|
-
const artifactSha = rawHash(artifactBytes);
|
|
166
|
-
const receiptsDir = path.join(root, 'receipts');
|
|
167
|
-
fs.mkdirSync(receiptsDir, { recursive: true });
|
|
168
|
-
fs.writeFileSync(path.join(receiptsDir, 'artifact.json'), artifactBytes);
|
|
169
|
-
const receipts = [];
|
|
170
|
-
for (const t of corpus.tasks) {
|
|
171
|
-
for (const arm of ['baseline', 'treatment']) {
|
|
172
|
-
const cfg = arm === 'baseline' ? baselineCfg : treatmentCfg;
|
|
173
|
-
receipts.push({
|
|
174
|
-
task_id: t.task_id,
|
|
175
|
-
lane: t.lane,
|
|
176
|
-
arm,
|
|
177
|
-
repeat: 1,
|
|
178
|
-
model: { name: 'm', version: 'v' },
|
|
179
|
-
reasoning_effort: 'standard',
|
|
180
|
-
repo_commit: 'abc1234',
|
|
181
|
-
clean_initial_tree_sha: hash('clean'),
|
|
182
|
-
permissions_fingerprint: hash('perm'),
|
|
183
|
-
tool_availability_fingerprint: hash('tool'),
|
|
184
|
-
corpus_sha256: corpusSha,
|
|
185
|
-
config_sha256: cfg.config_sha256,
|
|
186
|
-
wall_ms: 100,
|
|
187
|
-
input_tokens: 10,
|
|
188
|
-
output_tokens: 10,
|
|
189
|
-
context_loaded_tokens: 100,
|
|
190
|
-
tool_calls: 1,
|
|
191
|
-
subagent_calls: 0,
|
|
192
|
-
correctness: false,
|
|
193
|
-
regression: false,
|
|
194
|
-
unsupported_completion_claim: false,
|
|
195
|
-
user_corrections: 0,
|
|
196
|
-
useful_reviewer_findings: 1,
|
|
197
|
-
false_positive_reviewer_findings: 0,
|
|
198
|
-
evidence: { artifact_ref: 'artifact.json', artifact_sha256: artifactSha, command: 'echo hi' },
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const receiptsPath = path.join(receiptsDir, 'receipts.json');
|
|
203
|
-
fs.writeFileSync(receiptsPath, JSON.stringify({ receipts }));
|
|
204
|
-
const summary = spawnSync(process.execPath, [BENCHMARK, 'summarize', '--corpus', corpusPath, '--config', baselinePath, '--config', treatmentPath, '--receipts', receiptsPath], { encoding: 'utf8' });
|
|
205
|
-
assert.equal(summary.status, 0, `${summary.stdout}\n${summary.stderr}`);
|
|
206
|
-
const parsed = JSON.parse(summary.stdout);
|
|
207
|
-
assert.equal(parsed.rollout_recommendation.gates.Critical.pass, false, 'Critical should not pass with zero correctness');
|
|
208
|
-
assert.equal(parsed.rollout_recommendation.gates.Critical.quality_pass, false);
|
|
209
|
-
assert.equal(parsed.status, 'not-ready');
|
|
210
|
-
} finally {
|
|
211
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('benchmark validate handles empty and invalid evidence as not-ready/invalid', () => {
|
|
216
|
-
const BENCHMARK = path.join(ROOT, 'scripts/benchmark-workflow.mjs');
|
|
217
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-benchmark-empty-'));
|
|
218
|
-
try {
|
|
219
|
-
function canon(v) {
|
|
220
|
-
if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
|
|
221
|
-
if (v && typeof v === 'object') return `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
|
|
222
|
-
return JSON.stringify(v);
|
|
223
|
-
}
|
|
224
|
-
function shaCanon(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(canon(v)).digest('hex')}`; }
|
|
225
|
-
function hash(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(JSON.stringify(v)).digest('hex')}`; }
|
|
226
|
-
const corpus = {
|
|
227
|
-
schema_version: 'b1.v1',
|
|
228
|
-
status: 'frozen',
|
|
229
|
-
corpus_id: 'empty-test',
|
|
230
|
-
tasks: [{ task_id: 't1', lane: 'Direct', prompt: 'p', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'low', reasons: ['x'] } }],
|
|
231
|
-
};
|
|
232
|
-
const corpusSha = shaCanon(corpus);
|
|
233
|
-
const corpusPath = path.join(root, 'corpus.json');
|
|
234
|
-
fs.writeFileSync(corpusPath, JSON.stringify(corpus));
|
|
235
|
-
function makeConfig(arm) {
|
|
236
|
-
const cfg = {
|
|
237
|
-
schema_version: 'b1.v1', status: 'frozen', experiment_id: 'exp-empty', arm,
|
|
238
|
-
model: { name: 'm', version: 'v' }, reasoning_effort: 'standard',
|
|
239
|
-
repo: { identifier: 'r', commit: 'abc', clean_initial_tree_sha: hash('clean') },
|
|
240
|
-
permissions_fingerprint: hash('perm'), tool_availability_fingerprint: hash('tool'),
|
|
241
|
-
corpus_sha256: corpusSha, repeat_policy: { repeats_per_task: 1, context_isolated: true },
|
|
242
|
-
};
|
|
243
|
-
const without = { ...cfg }; delete without.config_sha256;
|
|
244
|
-
cfg.config_sha256 = shaCanon(without);
|
|
245
|
-
return cfg;
|
|
246
|
-
}
|
|
247
|
-
const cfg = makeConfig('baseline');
|
|
248
|
-
const cfgPath = path.join(root, 'cfg.json');
|
|
249
|
-
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
250
|
-
const emptyReceipts = path.join(root, 'empty.json');
|
|
251
|
-
fs.writeFileSync(emptyReceipts, JSON.stringify({ receipts: [] }));
|
|
252
|
-
const emptySummary = spawnSync(process.execPath, [BENCHMARK, 'summarize', '--corpus', corpusPath, '--config', cfgPath, '--receipts', emptyReceipts], { encoding: 'utf8' });
|
|
253
|
-
assert.equal(emptySummary.status, 0);
|
|
254
|
-
const emptyParsed = JSON.parse(emptySummary.stdout);
|
|
255
|
-
assert.ok(['not-ready', 'exploratory/no-live-runs'].includes(emptyParsed.status));
|
|
256
|
-
|
|
257
|
-
const artifactBytes = Buffer.from('artifact');
|
|
258
|
-
const rawHash = (b) => { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(b).digest('hex')}`; };
|
|
259
|
-
const artifactSha = rawHash(artifactBytes);
|
|
260
|
-
const badReceipts = { receipts: [{
|
|
261
|
-
task_id: 't1', lane: 'Direct', arm: 'baseline', repeat: 1,
|
|
262
|
-
model: { name: 'm', version: 'v' }, reasoning_effort: 'standard',
|
|
263
|
-
repo_commit: 'abc', clean_initial_tree_sha: hash('clean'),
|
|
264
|
-
permissions_fingerprint: hash('perm'), tool_availability_fingerprint: hash('tool'),
|
|
265
|
-
corpus_sha256: corpusSha, config_sha256: cfg.config_sha256,
|
|
266
|
-
wall_ms: 100, input_tokens: 10, output_tokens: 10, context_loaded_tokens: 100, tool_calls: 1, subagent_calls: 0,
|
|
267
|
-
correctness: true, regression: false, unsupported_completion_claim: false,
|
|
268
|
-
user_corrections: 0, useful_reviewer_findings: 0, false_positive_reviewer_findings: 0,
|
|
269
|
-
evidence: { artifact_ref: 'missing.json', artifact_sha256: artifactSha, command: 'echo hi' },
|
|
270
|
-
}] };
|
|
271
|
-
const badPath = path.join(root, 'bad.json');
|
|
272
|
-
fs.writeFileSync(badPath, JSON.stringify(badReceipts));
|
|
273
|
-
const badValidation = spawnSync(process.execPath, [BENCHMARK, 'validate', '--corpus', corpusPath, '--config', cfgPath, '--receipts', badPath], { encoding: 'utf8' });
|
|
274
|
-
assert.equal(badValidation.status, 2);
|
|
275
|
-
assert.match(badValidation.stderr, /artifact.*does not exist|artifact_ref/);
|
|
276
|
-
} finally {
|
|
277
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
278
|
-
}
|
|
279
|
-
});
|
|
@@ -191,11 +191,10 @@ function assertLinksResolve(baseDir, file) {
|
|
|
191
191
|
return links.length;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
test('manifest requires vibe
|
|
194
|
+
test('manifest requires vibe', () => {
|
|
195
195
|
// Without the entry the installer never copies the skill and nothing else fails.
|
|
196
196
|
const required = loadClaudeMigrationManifest().skills.required;
|
|
197
197
|
assert.equal(required.includes('vibe'), true);
|
|
198
|
-
assert.deepEqual([...required], [...required].sort());
|
|
199
198
|
});
|
|
200
199
|
|
|
201
200
|
test('vibe frontmatter is manual-only, carries the catalog identity, and fits the routing limit', () => {
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
|
|
8
|
+
const WEB_ROOT = path.resolve(__dirname, '../../../../cafekit-web');
|
|
9
|
+
const DOCS_ROOT = path.join(WEB_ROOT, 'public/content/docs');
|
|
10
|
+
const SOURCE_ROOT = path.join(WEB_ROOT, 'src');
|
|
11
|
+
const LOCALES = ['en', 'vi', 'ja'];
|
|
12
|
+
|
|
13
|
+
function walk(root) {
|
|
14
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
|
15
|
+
const target = path.join(root, entry.name);
|
|
16
|
+
return entry.isDirectory() ? walk(target) : [target];
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('website keeps process-first docs, canonical public names, and historical legacy claims', () => {
|
|
21
|
+
const currentDocs = walk(DOCS_ROOT).filter((file) => {
|
|
22
|
+
const relative = path.relative(DOCS_ROOT, file).split(path.sep).join('/');
|
|
23
|
+
return file.endsWith('.mdx') && !relative.endsWith('/reference.mdx') && !relative.endsWith('/platforms/opencode.mdx');
|
|
24
|
+
});
|
|
25
|
+
const currentSources = walk(SOURCE_ROOT).filter((file) => /\.(?:ts|tsx)$/.test(file));
|
|
26
|
+
const corpus = [...currentDocs, ...currentSources].map((file) => fs.readFileSync(file, 'utf8')).join('\n');
|
|
27
|
+
|
|
28
|
+
assert.doesNotMatch(corpus, /cf:generate-graph|\/generate-graph/);
|
|
29
|
+
assert.doesNotMatch(corpus, /\bspec\.json\b|\btask_registry\b|tasks\/task-R|task-R\*|cf:specs --validate|registry sync/);
|
|
30
|
+
assert.doesNotMatch(corpus, /OpenCode installs|OpenCode cài|OpenCode では[^\n]*commands|Yes\.[^\n]*OpenCode|Có\.[^\n]*OpenCode|はい。[^\n]*OpenCode/i);
|
|
31
|
+
assert.doesNotMatch(corpus, /cf:loop|autoresearch/i);
|
|
32
|
+
|
|
33
|
+
const config = fs.readFileSync(path.join(SOURCE_ROOT, 'lib/docs-config.ts'), 'utf8');
|
|
34
|
+
for (const slug of ['ask', 'scout', 'fix']) assert.match(config, new RegExp(`/docs/skills/${slug}`));
|
|
35
|
+
assert.doesNotMatch(config, /\/docs\/skills\/(?:question|inspect|hotfix)/);
|
|
36
|
+
|
|
37
|
+
const route = fs.readFileSync(path.join(SOURCE_ROOT, 'app/[locale]/docs/[[...slug]]/page.tsx'), 'utf8');
|
|
38
|
+
assert.match(route, /ask:\s*'question'/);
|
|
39
|
+
assert.match(route, /scout:\s*'inspect'/);
|
|
40
|
+
assert.match(route, /hotfix:\s*'fix'/);
|
|
41
|
+
|
|
42
|
+
for (const locale of LOCALES) {
|
|
43
|
+
for (const documentSlug of ['question', 'inspect', 'fix']) {
|
|
44
|
+
assert.equal(fs.existsSync(path.join(DOCS_ROOT, locale, 'skills', `${documentSlug}.mdx`)), true,
|
|
45
|
+
`${locale} skills/${documentSlug}.mdx must exist`);
|
|
46
|
+
}
|
|
47
|
+
const lifecycle = fs.readFileSync(path.join(DOCS_ROOT, locale, 'spec-lifecycle.mdx'), 'utf8');
|
|
48
|
+
assert.doesNotMatch(lifecycle, /^## Legacy compatibility$/m, `${locale} spec lifecycle must not promise a legacy adapter`);
|
|
49
|
+
|
|
50
|
+
const opencode = fs.readFileSync(path.join(DOCS_ROOT, locale, 'platforms/opencode.mdx'), 'utf8');
|
|
51
|
+
assert.match(opencode, /0\.17/);
|
|
52
|
+
assert.match(opencode, /histor|lịch sử|migration/i);
|
|
53
|
+
|
|
54
|
+
const reference = fs.readFileSync(path.join(DOCS_ROOT, locale, 'reference.mdx'), 'utf8');
|
|
55
|
+
assert.match(reference, /Legacy OpenCode 0\.16 command/);
|
|
56
|
+
assert.doesNotMatch(reference, /OpenCode currently|OpenCode hiện có|OpenCode は main implementation surface/);
|
|
57
|
+
}
|
|
58
|
+
});
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -360,8 +360,10 @@ function normalizeCodexBody(content, sourcePath = '') {
|
|
|
360
360
|
next = next.replace(/`?CLAUDE_CODE_SUBAGENT_MODEL`?/g,
|
|
361
361
|
'the configured agent model (`model` in `.codex/agents/<agent>.toml`, or `[agents].default_subagent_model` in native Codex configuration)');
|
|
362
362
|
next = applyReplacements(normalizeAskUserQuestion(next), INSTRUCTION_REPLACEMENTS);
|
|
363
|
+
// Each projection below checks for its own output first, so projecting an installed file again is a no-op.
|
|
363
364
|
const { frontmatter, body } = splitFrontmatter(next);
|
|
364
|
-
if (frontmatter.when_to_use && frontmatter.description
|
|
365
|
+
if (frontmatter.when_to_use && frontmatter.description
|
|
366
|
+
&& !frontmatter.description.includes(` Use when: ${frontmatter.when_to_use}`)) {
|
|
365
367
|
const description = `${frontmatter.description} Use when: ${frontmatter.when_to_use}`;
|
|
366
368
|
if (description.length <= 1024) {
|
|
367
369
|
const header = next.slice(0, next.length - body.length)
|
|
@@ -371,7 +373,7 @@ function normalizeCodexBody(content, sourcePath = '') {
|
|
|
371
373
|
}
|
|
372
374
|
}
|
|
373
375
|
// Only the installed ask skill receives this Codex-specific boundary, never eval prompts.
|
|
374
|
-
if (/(?:^|\/)skills\/ask\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
376
|
+
if (/(?:^|\/)skills\/ask\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/')) && !next.includes(CODEX_ASK_NOTE)) {
|
|
375
377
|
const { frontmatter, body } = splitFrontmatter(next);
|
|
376
378
|
const description = `${frontmatter.description} ${CODEX_ASK_DESCRIPTION_BOUNDARY}`;
|
|
377
379
|
if (description.length > 1024) throw new Error('Codex ask description exceeds 1024 characters');
|
|
@@ -380,7 +382,7 @@ function normalizeCodexBody(content, sourcePath = '') {
|
|
|
380
382
|
next = `${header}\n${CODEX_ASK_NOTE}\n\n${body}`;
|
|
381
383
|
}
|
|
382
384
|
// Only the installed research skill receives this note, never prompts or templates.
|
|
383
|
-
if (/(?:^|\/)skills\/research\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
385
|
+
if (/(?:^|\/)skills\/research\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/')) && !next.includes(CODEX_RESEARCH_DEPTH_NOTE)) {
|
|
384
386
|
const { body } = splitFrontmatter(next);
|
|
385
387
|
const header = next.slice(0, next.length - body.length);
|
|
386
388
|
next = `${header}\n${CODEX_RESEARCH_DEPTH_NOTE}\n\n${body}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"description": "Native spec-driven workflows for Claude Code and Codex CLI. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -85,32 +85,6 @@ test('asks for a symlink whose target is .env (regression: symlink bypass)', ()
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test('allows a normal non-sensitive file (exit 0)', () => {
|
|
89
|
-
const dir = tmpDir();
|
|
90
|
-
try {
|
|
91
|
-
const f = path.join(dir, 'README.md');
|
|
92
|
-
fs.writeFileSync(f, '# hi');
|
|
93
|
-
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: f } }, dir);
|
|
94
|
-
assert.strictEqual(code, 0);
|
|
95
|
-
} finally {
|
|
96
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('allows a symlink to an exempt .env.example (exemption still wins)', () => {
|
|
101
|
-
const dir = tmpDir();
|
|
102
|
-
try {
|
|
103
|
-
const example = path.join(dir, '.env.example');
|
|
104
|
-
fs.writeFileSync(example, 'SECRET=');
|
|
105
|
-
const link = path.join(dir, 'sample.txt');
|
|
106
|
-
fs.symlinkSync(example, link);
|
|
107
|
-
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
|
|
108
|
-
assert.strictEqual(code, 0, 'symlink to an exempt file must stay allowed');
|
|
109
|
-
} finally {
|
|
110
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
88
|
test('an exempt-looking symlink to .env still asks based on its target', () => {
|
|
115
89
|
const dir = tmpDir();
|
|
116
90
|
try {
|