@haposoft/cafekit 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +938 -237
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +222 -149
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +102 -1362
- 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-authority-decoupled.test.js +14 -19
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -49
- package/bin/phases/copy-payload.js +11 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +17 -89
- package/src/claude/hooks/spec-state.cjs +89 -96
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +20 -8
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- 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/spec-utils.cjs +6 -45
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +32 -74
- package/src/codex/hooks/spec-state.cjs +68 -46
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/common/AGENTS.md +4 -10
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/rules/state-sync.md +0 -60
package/bin/install.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const { spawnSync } = require('child_process');
|
|
19
|
+
const fs = require('fs');
|
|
19
20
|
const { buildContext, PLATFORMS, packageJson } = require('./lib/context');
|
|
20
21
|
const lock = require('./lib/lock');
|
|
21
22
|
const backup = require('./lib/backup');
|
|
@@ -45,6 +46,28 @@ const { runPostInstall } = require('./phases/post-install');
|
|
|
45
46
|
const { setupSkillDeps } = require('./phases/skills-setup');
|
|
46
47
|
const { printSummary } = require('./phases/summary');
|
|
47
48
|
|
|
49
|
+
function preflightTrackedRootFiles(ctx) {
|
|
50
|
+
const files = ['AGENTS.md'];
|
|
51
|
+
if (ctx.platforms.includes('claude')) files.push('CLAUDE.md');
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
try {
|
|
54
|
+
fs.lstatSync(file);
|
|
55
|
+
continue;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT') throw error;
|
|
58
|
+
}
|
|
59
|
+
const result = spawnSync('git', ['ls-files', '-v', '--error-unmatch', '--', file], {
|
|
60
|
+
cwd: process.cwd(), encoding: 'utf8', timeout: 3000, shell: false,
|
|
61
|
+
});
|
|
62
|
+
if (!result.error && result.status === 0) {
|
|
63
|
+
if (result.stdout[0]?.toUpperCase() === 'S') continue;
|
|
64
|
+
throw new Error(`${file} is tracked by git but missing from the working tree; restore it with \`git checkout -- ${file}\`, or \`git rm ${file}\` if the deletion is intended, then rerun the installer.`);
|
|
65
|
+
}
|
|
66
|
+
if (!result.error && (result.status === 1 || /not a git repository/i.test(result.stderr || ''))) continue;
|
|
67
|
+
ctx.ui.warn(`Could not confirm git state for ${file}; installing a fresh copy.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
/** Install a single platform: payload + runtime + metadata, under one spinner. */
|
|
49
72
|
function installPlatform(ctx, platformKey) {
|
|
50
73
|
const platform = PLATFORMS[platformKey];
|
|
@@ -175,6 +198,8 @@ async function main() {
|
|
|
175
198
|
await selectDocumentSkills(ctx);
|
|
176
199
|
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
177
200
|
|
|
201
|
+
preflightTrackedRootFiles(ctx);
|
|
202
|
+
|
|
178
203
|
// ── Pre-run snapshot for rollback ─────────────────────
|
|
179
204
|
// Capture platform folders AND the root files the pipeline mutates
|
|
180
205
|
// (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -8,6 +8,22 @@ const { preserveAddressingSection } = require('./instruction-blocks');
|
|
|
8
8
|
const CODEX_BLOCK_START = '<!-- CAFEKIT CODEX START -->';
|
|
9
9
|
const CODEX_BLOCK_END = '<!-- CAFEKIT CODEX END -->';
|
|
10
10
|
const INSTRUCTION_EXTENSIONS = new Set(['.md', '.mdx', '.txt']);
|
|
11
|
+
const CODEX_ASK_DESCRIPTION_BOUNDARY = 'A request to fix or change something is still answered without editing any file; point the user to $cf-fix.';
|
|
12
|
+
const CODEX_ASK_NOTE = 'Rule 1 (Codex): never edit, create, or delete files in this skill, '
|
|
13
|
+
+ 'even when the user says fix it or sửa giúp luôn. Do not run commands that write files. '
|
|
14
|
+
+ 'Answer the cause with evidence and tell the user to run $cf-fix.';
|
|
15
|
+
const CODEX_RESEARCH_DEPTH_MARKER = 'Codex research depth: treat a settled repository decision that remains in force as a known option.';
|
|
16
|
+
const CODEX_RESEARCH_DEPTH_NOTE = CODEX_RESEARCH_DEPTH_MARKER
|
|
17
|
+
+ ' Use Quick for that decision. An outdated supporting rationale alone does not raise depth '
|
|
18
|
+
+ 'unless correcting it changes the recommendation. Conflicting evidence that changes the '
|
|
19
|
+
+ 'recommendation still requires escalation under the depth table.';
|
|
20
|
+
const CODEX_SPECS_SCOPE_MARKER = 'Codex scope gate: put the GATE-SCOPE question in the final answer of the turn.';
|
|
21
|
+
const CODEX_SPECS_SCOPE_NOTE = CODEX_SPECS_SCOPE_MARKER
|
|
22
|
+
+ ' Ask it there, not in commentary or through `request_user_input`. Do not send a summary after the question. '
|
|
23
|
+
+ 'For each item needing a decision, use `[NEEDS CLARIFICATION: <question>]`, replacing <question> '
|
|
24
|
+
+ 'with the real question, not a placeholder or a quotation of this note. Offer EXPAND, KEEP, and CUT together. '
|
|
25
|
+
+ 'Preserve the exact words NEEDS CLARIFICATION, EXPAND, KEEP, and CUT; do not translate them. '
|
|
26
|
+
+ 'Wait for the user to decide; do not choose scope or implement code on their behalf.';
|
|
11
27
|
|
|
12
28
|
const AGENT_NAMES = [
|
|
13
29
|
'brainstormer',
|
|
@@ -200,11 +216,15 @@ function normalizeAskUserQuestion(content) {
|
|
|
200
216
|
const oneSeparator = '(?:[^\\S\\r\\n]+|[^\\S\\r\\n]*\\r?\\n[^\\S\\r\\n]*)';
|
|
201
217
|
const pattern = new RegExp(
|
|
202
218
|
`(?:(\\b(?:${determinerPattern}|[0-9]+)\\b)(${oneSeparator}))?`
|
|
203
|
-
+ `(\\bAskUserQuestion\\b|${masked.askMarkerPattern})`,
|
|
219
|
+
+ `(\\bAskUserQuestion\\b|${masked.askMarkerPattern}|\\bnative(?:\\s+structured user-input|\\s+input)\\s+tool\\b)`,
|
|
204
220
|
'g'
|
|
205
221
|
);
|
|
206
|
-
const
|
|
207
|
-
|
|
222
|
+
const withoutRuntimePossessive = masked.content.replace(
|
|
223
|
+
/\bthe runtime['’]s\s+native(?:\s+structured user-input|\s+input)\s+tool\b/g,
|
|
224
|
+
"a question in the turn's final answer"
|
|
225
|
+
);
|
|
226
|
+
const normalized = withoutRuntimePossessive.replace(pattern, (match, determiner, spacing, _token, offset, source) => {
|
|
227
|
+
const literalIndex = match.search(/AskUserQuestion|\bnative\b/);
|
|
208
228
|
const tokenOffset = offset + (literalIndex >= 0
|
|
209
229
|
? literalIndex
|
|
210
230
|
: match.indexOf(masked.askMarkerPrefix));
|
|
@@ -219,18 +239,33 @@ function normalizeAskUserQuestion(content) {
|
|
|
219
239
|
const normalizedDeterminer = /^an$/i.test(determiner)
|
|
220
240
|
? (determiner[0] === 'A' ? 'A' : 'a')
|
|
221
241
|
: determiner;
|
|
222
|
-
return `${normalizedDeterminer}${spacing}
|
|
242
|
+
return `${normalizedDeterminer}${spacing}question in the turn's final answer`;
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
const trailing = source.slice(offset + match.length);
|
|
226
246
|
const pluralContext = new RegExp(`^${oneSeparator}(?:calls|batches)\\b`, 'i').test(trailing);
|
|
227
|
-
return `${pluralContext ? '' : 'a '}
|
|
247
|
+
return `${pluralContext ? '' : 'a '}question in the turn's final answer`;
|
|
228
248
|
});
|
|
229
249
|
return masked.restore(normalized);
|
|
230
250
|
}
|
|
231
251
|
|
|
232
|
-
function normalizeRuntimePaths(content) {
|
|
233
|
-
|
|
252
|
+
function normalizeRuntimePaths(content, sourcePath = '') {
|
|
253
|
+
const stateRootsPattern = /const RUNTIME_STATE_ROOTS = Object\.freeze\(\[[\s\S]*?\]\);/;
|
|
254
|
+
const sharedStateRoots = path.basename(sourcePath) === 'provenance.cjs'
|
|
255
|
+
? content.match(stateRootsPattern)?.[0] : null;
|
|
256
|
+
const preservedRoots = [];
|
|
257
|
+
let markerPrefix = '\uE000CAFEKIT_HOST_';
|
|
258
|
+
while (content.includes(markerPrefix)) markerPrefix += '_';
|
|
259
|
+
const rootToken = /(?<![\w/\\])\.claude(?:[/\\])?(?![\w/\\.-])/g;
|
|
260
|
+
const instruction = isInstructionAsset(sourcePath) ? content.replace(/^.*$/gm, line => {
|
|
261
|
+
if (!/(?<![\w/\\])\.codex(?:[/\\])?(?![\w/\\.-])/.test(line)) return line;
|
|
262
|
+
return line.replace(rootToken, original => {
|
|
263
|
+
const marker = `${markerPrefix}${preservedRoots.length}\uE001`;
|
|
264
|
+
preservedRoots.push([marker, original]);
|
|
265
|
+
return marker;
|
|
266
|
+
});
|
|
267
|
+
}) : content;
|
|
268
|
+
let next = normalizeSourcePaths(instruction, {
|
|
234
269
|
runtimeRoot: '.codex',
|
|
235
270
|
skillsRoot: '.agents/skills'
|
|
236
271
|
})
|
|
@@ -239,6 +274,15 @@ function normalizeRuntimePaths(content) {
|
|
|
239
274
|
.replace(/(?<!~\/)\.claude(?=[/\\])/g, '.codex')
|
|
240
275
|
.replace(/(['"])\.claude\1/g, '$1.codex$1')
|
|
241
276
|
.replace(/`\.claude`/g, '`.codex`');
|
|
277
|
+
// This table describes every runtime's generated state, not the install root.
|
|
278
|
+
if (sharedStateRoots) next = next.replace(stateRootsPattern, () => sharedStateRoots);
|
|
279
|
+
if (/\/ai-multimodal\/scripts\/(?:media_optimizer|document_converter)\.py$/.test(sourcePath.replace(/\\/g, '/'))) {
|
|
280
|
+
// Shared skills live under .agents; native support/config still lives in .codex.
|
|
281
|
+
next = next.replace(/\bclaude_dir = skills_dir\.parent\b/g,
|
|
282
|
+
"claude_dir = skills_dir.parent.parent / '.codex'");
|
|
283
|
+
}
|
|
284
|
+
for (const [marker, original] of preservedRoots) next = next.split(marker).join(original);
|
|
285
|
+
return next;
|
|
242
286
|
}
|
|
243
287
|
|
|
244
288
|
function normalizeSkillNames(content) {
|
|
@@ -262,6 +306,7 @@ function normalizeAgentNames(content) {
|
|
|
262
306
|
|
|
263
307
|
function normalizeAgentInvocations(content) {
|
|
264
308
|
return content
|
|
309
|
+
.replace(/\bTask\s*\(\s*Explore\s*\)/g, 'spawn_agent(agent_type="explorer", fork_turns="none", task_name="explore", message="Describe the scan")')
|
|
265
310
|
.replace(/\bAgent\s*\(/g, 'spawn_agent(')
|
|
266
311
|
.replace(/\bsubagent_type\s*=\s*"([^"]+)"/g, (_match, name) => (
|
|
267
312
|
`agent_type="${normalizeAgentRole(name)}", fork_turns="none"`
|
|
@@ -286,15 +331,60 @@ function isInstructionAsset(sourcePath) {
|
|
|
286
331
|
}
|
|
287
332
|
|
|
288
333
|
function normalizeCodexBody(content, sourcePath = '') {
|
|
289
|
-
let next = normalizeRuntimePaths(String(content));
|
|
334
|
+
let next = normalizeRuntimePaths(String(content), sourcePath);
|
|
290
335
|
next = next
|
|
291
|
-
.replace(/\bCLAUDE\.md\b/g,
|
|
336
|
+
.replace(/^.*$/gm, line => line.replace(/\bCLAUDE\.md\b/g, (name, offset) => (
|
|
337
|
+
isInstructionAsset(sourcePath) && /(?<![\w/\\])AGENTS\.md\b/.test(line)
|
|
338
|
+
&& !/[\w/\\]/.test(line[offset - 1] || '') ? name : 'AGENTS.md'
|
|
339
|
+
)))
|
|
292
340
|
.replace(/\bClaude Code\b/g, 'Codex CLI');
|
|
293
341
|
|
|
294
342
|
if (!isInstructionAsset(sourcePath)) return next;
|
|
295
343
|
|
|
344
|
+
if (/(?:^|\/)rules\/orchestrator\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
345
|
+
next = next
|
|
346
|
+
.replace('When the current session or a subagent runs on a model below `fable` (e.g. `opus`, `sonnet`, `haiku`) and hits a hard problem',
|
|
347
|
+
'When the current session or a subagent hits a hard problem')
|
|
348
|
+
.replace('It runs autonomously on the strongest available model',
|
|
349
|
+
'It runs autonomously with high reasoning effort on the session model unless configuration selects another');
|
|
350
|
+
}
|
|
351
|
+
|
|
296
352
|
next = normalizeAgentInvocations(normalizeAgentNames(normalizeSkillNames(next)));
|
|
297
|
-
|
|
353
|
+
next = next.replace(/`?CLAUDE_CODE_SUBAGENT_MODEL`?/g,
|
|
354
|
+
'the configured agent model (`model` in `.codex/agents/<agent>.toml`, or `[agents].default_subagent_model` in native Codex configuration)');
|
|
355
|
+
next = applyReplacements(normalizeAskUserQuestion(next), INSTRUCTION_REPLACEMENTS);
|
|
356
|
+
const { frontmatter, body } = splitFrontmatter(next);
|
|
357
|
+
if (frontmatter.when_to_use && frontmatter.description) {
|
|
358
|
+
const description = `${frontmatter.description} Use when: ${frontmatter.when_to_use}`;
|
|
359
|
+
if (description.length <= 1024) {
|
|
360
|
+
const header = next.slice(0, next.length - body.length)
|
|
361
|
+
.replace(/^description:[^\r\n]*(?:\r?\n[ \t]+[^\r\n]*)*/m,
|
|
362
|
+
() => `description: ${JSON.stringify(description)}`);
|
|
363
|
+
next = header + body;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Only the installed ask skill receives this Codex-specific boundary, never eval prompts.
|
|
367
|
+
if (/(?:^|\/)skills\/ask\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
368
|
+
const { frontmatter, body } = splitFrontmatter(next);
|
|
369
|
+
const description = `${frontmatter.description} ${CODEX_ASK_DESCRIPTION_BOUNDARY}`;
|
|
370
|
+
if (description.length > 1024) throw new Error('Codex ask description exceeds 1024 characters');
|
|
371
|
+
const header = next.slice(0, next.length - body.length)
|
|
372
|
+
.replace(/^description:[^\r\n]*$/m, () => `description: ${JSON.stringify(description)}`);
|
|
373
|
+
next = `${header}\n${CODEX_ASK_NOTE}\n\n${body}`;
|
|
374
|
+
}
|
|
375
|
+
// Only the installed research skill receives this note, never prompts or templates.
|
|
376
|
+
if (/(?:^|\/)skills\/research\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
377
|
+
const { body } = splitFrontmatter(next);
|
|
378
|
+
const header = next.slice(0, next.length - body.length);
|
|
379
|
+
next = `${header}\n${CODEX_RESEARCH_DEPTH_NOTE}\n\n${body}`;
|
|
380
|
+
}
|
|
381
|
+
// Only the installed specs skill receives this note, never prompts or templates.
|
|
382
|
+
if (/(?:^|\/)skills\/specs\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/')) && !next.includes(CODEX_SPECS_SCOPE_NOTE)) {
|
|
383
|
+
const { body } = splitFrontmatter(next);
|
|
384
|
+
const header = next.slice(0, next.length - body.length);
|
|
385
|
+
next = `${header}\n${CODEX_SPECS_SCOPE_NOTE}\n\n${body}`;
|
|
386
|
+
}
|
|
387
|
+
return next;
|
|
298
388
|
}
|
|
299
389
|
|
|
300
390
|
function getCodexCopyOptions(baseOptions = {}) {
|
|
@@ -307,14 +397,42 @@ function getCodexCopyOptions(baseOptions = {}) {
|
|
|
307
397
|
function convertCodexAgentContent(content, fileName = '') {
|
|
308
398
|
const { frontmatter, body } = splitFrontmatter(content);
|
|
309
399
|
const name = codexAgentName(frontmatter.name || fileName);
|
|
310
|
-
|
|
400
|
+
let description = normalizeCodexBody(
|
|
311
401
|
frontmatter.description || `CafeKit Codex agent: ${name}`
|
|
312
402
|
);
|
|
313
|
-
|
|
403
|
+
let agentBody = body;
|
|
404
|
+
if (['git_ops', 'project_manager'].includes(name)) {
|
|
405
|
+
let inTeamSection = false;
|
|
406
|
+
agentBody = body.split('\n').flatMap(line => {
|
|
407
|
+
if (/^## /.test(line)) {
|
|
408
|
+
inTeamSection = /^## (?:Team Mode|Collaborative Interlocking \(Swarm Protocol\))\s*$/.test(line);
|
|
409
|
+
}
|
|
410
|
+
if (!inTeamSection) return [line];
|
|
411
|
+
if (/\b(?:TaskList|TaskGet|TaskUpdate|SendMessage|shutdown_request)\b|When instantiated as a team member|When triggered as an active teammate/.test(line)) return [];
|
|
412
|
+
return [line.replace(/^(\s*)\d+\. /, '$1- ')];
|
|
413
|
+
}).join('\n');
|
|
414
|
+
}
|
|
415
|
+
let instructions = normalizeCodexBody(agentBody).trim();
|
|
416
|
+
const readOnly = ['brainstormer', 'code_auditor', 'inspector', 'researcher'].includes(name);
|
|
417
|
+
if (readOnly) {
|
|
418
|
+
instructions += '\n\n## Read-only role\n\nRead-only role: never create, never edit, and never delete files, and never run commands that write to disk; return findings in your reply.';
|
|
419
|
+
}
|
|
420
|
+
const reasoning = [];
|
|
421
|
+
if (name === 'strategist') {
|
|
422
|
+
description = 'Autonomous advisory counsel with high reasoning effort; inherits the session model unless native spawn or agent defaults select another. No session model switch or user interview. Mention @strategist or spawn it for hard design, debugging, or trade-off calls. Advisory-only; returns advice, not code.';
|
|
423
|
+
instructions = instructions
|
|
424
|
+
.replace('consulted for counsel and running on the strongest available model',
|
|
425
|
+
'consulted for counsel with high reasoning effort on the configured native model')
|
|
426
|
+
.replace(/## Runtime note\s+[\s\S]*$/,
|
|
427
|
+
'## Runtime note\n\nThis agent sets `model_reasoning_effort = "high"` and omits `model`. It inherits the session model unless an explicit native spawn or `[agents].default_subagent_model` selects another. No strongest-model selection is guaranteed. For deployment control, set `model` in `.codex/agents/strategist.toml` or configure native agent defaults in native Codex configuration; choose a model that supports high reasoning effort. Advice remains advisory-only and never counts as execution proof.');
|
|
428
|
+
reasoning.push('model_reasoning_effort = "high"');
|
|
429
|
+
}
|
|
314
430
|
|
|
315
431
|
return [
|
|
316
432
|
`name = ${JSON.stringify(name)}`,
|
|
317
433
|
`description = ${JSON.stringify(description)}`,
|
|
434
|
+
...(readOnly ? ['sandbox_mode = "read-only"'] : []),
|
|
435
|
+
...reasoning,
|
|
318
436
|
`developer_instructions = ${JSON.stringify(instructions)}`,
|
|
319
437
|
''
|
|
320
438
|
].join('\n');
|
|
@@ -365,6 +483,10 @@ function transformManagedCodexContent(content, transform) {
|
|
|
365
483
|
}
|
|
366
484
|
|
|
367
485
|
module.exports = {
|
|
486
|
+
CODEX_RESEARCH_DEPTH_MARKER,
|
|
487
|
+
CODEX_RESEARCH_DEPTH_NOTE,
|
|
488
|
+
CODEX_SPECS_SCOPE_MARKER,
|
|
489
|
+
CODEX_SPECS_SCOPE_NOTE,
|
|
368
490
|
normalizeCodexBody,
|
|
369
491
|
getCodexCopyOptions,
|
|
370
492
|
codexAgentName,
|
package/bin/lib/context.js
CHANGED
|
@@ -128,7 +128,7 @@ const PLATFORMS = {
|
|
|
128
128
|
detectFiles: ['.omp'],
|
|
129
129
|
commandsDir: null,
|
|
130
130
|
// omp discovers .claude/skills and .agents/skills itself, so CafeKit copies
|
|
131
|
-
// no skill payload for it. See specs/omp-runtime-support/plan.md.
|
|
131
|
+
// no skill payload for it. See specs/archive/omp-runtime-support/plan.md.
|
|
132
132
|
skillsDir: '.agents/skills',
|
|
133
133
|
agentsDir: null,
|
|
134
134
|
skillsRef: '.agents/skills',
|
|
@@ -60,9 +60,8 @@ function writeManagedFile(opts) {
|
|
|
60
60
|
// Ownership manifest for this platform (read at run start), NOT the migration manifest.
|
|
61
61
|
const ownership = (ctx.ownership && ctx.ownership[platformFolder]) || { files: {} };
|
|
62
62
|
|
|
63
|
-
// A file already written earlier THIS run
|
|
64
|
-
//
|
|
65
|
-
// it as pristine against what we just wrote, not as a user-created file.
|
|
63
|
+
// A file already written earlier THIS run is ours — treat it as pristine
|
|
64
|
+
// against what we just wrote, not as a user-created file.
|
|
66
65
|
const relForRun = tracker?.keyFor
|
|
67
66
|
? tracker.keyFor(dest)
|
|
68
67
|
: path.relative(platformFolder, dest).replace(/\\/g, '/');
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
getCopyOptions
|
|
17
17
|
} = require('../lib/context');
|
|
18
18
|
const { sha256 } = require('../lib/manifest');
|
|
19
|
+
const { assertNoSymlinkPath } = require('../lib/path-safety');
|
|
19
20
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
20
21
|
const { report, treeAction } = require('./report');
|
|
21
22
|
const {
|
|
@@ -164,30 +165,84 @@ function copyClaudeRuntimeFiles(ctx, platformKey) {
|
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
/**
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
168
|
+
/** Remove only obsolete runtime bytes proven unchanged by the ownership manifest. */
|
|
169
|
+
function removeObsoleteRuntimeFiles(ctx, platformKey) {
|
|
170
|
+
const completed = (ctx.obsoleteRuntimeCleanedPlatforms ||= new Set());
|
|
171
|
+
if (completed.has(platformKey)) return;
|
|
172
|
+
completed.add(platformKey);
|
|
173
|
+
const preserved = new Set();
|
|
174
|
+
for (const key of [platformKey]) {
|
|
175
|
+
const platform = PLATFORMS[key];
|
|
176
|
+
const tracker = ctx.trackers?.[key];
|
|
177
|
+
const ownership = ctx.ownership?.[platform.folder] || { files: {} };
|
|
178
|
+
const obsoleteFiles = (ctx.manifest?.obsolete?.runtimeFiles || [])
|
|
179
|
+
.filter(relative => key === 'claude' || /^(?:hooks|scripts|skills)\//.test(relative));
|
|
180
|
+
for (const relative of obsoleteFiles) {
|
|
181
|
+
const targetPath = key === 'codex' && relative.startsWith('skills/')
|
|
182
|
+
? path.join(platform.skillsDir, relative.slice('skills/'.length))
|
|
183
|
+
: path.join(platform.folder, relative);
|
|
184
|
+
let stat;
|
|
185
|
+
try {
|
|
186
|
+
assertNoSymlinkPath(targetPath);
|
|
187
|
+
stat = fs.statSync(targetPath);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error.code === 'ENOENT') {
|
|
190
|
+
if (key === 'codex' && tracker && !ctx.dryRun) tracker.prune(tracker.keyFor(targetPath));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
preserved.add(targetPath);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
if (!tracker) throw new Error('Missing ownership tracker');
|
|
198
|
+
const trackedKeys = [];
|
|
199
|
+
const inspect = file => {
|
|
200
|
+
assertNoSymlinkPath(file);
|
|
201
|
+
const current = fs.statSync(file);
|
|
202
|
+
if (current.isDirectory()) {
|
|
203
|
+
for (const name of fs.readdirSync(file)) inspect(path.join(file, name));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!current.isFile()) throw new Error('Unsupported obsolete file type');
|
|
207
|
+
const trackedKey = tracker.keyFor(file);
|
|
208
|
+
const recordedHash = ownership.files?.[trackedKey]?.sha256;
|
|
209
|
+
if (!recordedHash || recordedHash !== sha256(fs.readFileSync(file))) {
|
|
210
|
+
throw new Error('Obsolete file is not proven unchanged');
|
|
211
|
+
}
|
|
212
|
+
trackedKeys.push(trackedKey);
|
|
213
|
+
};
|
|
214
|
+
inspect(targetPath);
|
|
215
|
+
assertNoSymlinkPath(targetPath);
|
|
216
|
+
const targetKey = tracker.keyFor(targetPath);
|
|
217
|
+
if (!ctx.dryRun) {
|
|
218
|
+
fs.rmSync(targetPath, { force: true, recursive: stat.isDirectory() });
|
|
219
|
+
for (const trackedKey of trackedKeys) tracker.prune(trackedKey);
|
|
220
|
+
if (stat.isDirectory()) {
|
|
221
|
+
tracker.prune(targetKey);
|
|
222
|
+
tracker.prunePrefix(`${targetKey}/`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete ${key} runtime: ${relative}`);
|
|
226
|
+
ctx.results.updated++;
|
|
227
|
+
} catch {
|
|
228
|
+
preserved.add(targetPath);
|
|
186
229
|
}
|
|
187
230
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
231
|
+
}
|
|
232
|
+
if (preserved.size) {
|
|
233
|
+
ctx.results.preserved = (ctx.results.preserved || 0) + preserved.size;
|
|
234
|
+
(ctx.results.preservedFiles ||= []).push(...preserved);
|
|
235
|
+
}
|
|
236
|
+
const allPreserved = (ctx.obsoleteRuntimePreserved ||= new Set());
|
|
237
|
+
for (const target of preserved) allPreserved.add(target);
|
|
238
|
+
const selected = (ctx.platforms || [platformKey]).filter(key => key === 'claude' || key === 'codex');
|
|
239
|
+
if (selected.every(key => completed.has(key)) && allPreserved.size) {
|
|
240
|
+
ctx.ui.warn(`Preserved ${allPreserved.size} obsolete runtime item(s); review and remove manually if no longer needed: ${[...allPreserved].join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
|
|
245
|
+
if (platformKey === 'claude') removeObsoleteRuntimeFiles(ctx, platformKey);
|
|
191
246
|
}
|
|
192
247
|
|
|
193
248
|
/** Remove renamed agents only when ownership manifest proves pristine bytes. */
|
|
@@ -258,7 +313,7 @@ function copyClaudeMdFile(ctx, platformKey) {
|
|
|
258
313
|
}
|
|
259
314
|
|
|
260
315
|
/** Copy the rules/ tree (ownership-aware; no longer force-overwrites). */
|
|
261
|
-
function copyRulesDirectory(ctx, platformKey) {
|
|
316
|
+
function copyRulesDirectory(ctx, platformKey, excludedNames = new Set()) {
|
|
262
317
|
if (!hasPlatformCapability(platformKey, 'rules')) return;
|
|
263
318
|
|
|
264
319
|
const src = path.join(SRC, 'claude/rules');
|
|
@@ -270,9 +325,19 @@ function copyRulesDirectory(ctx, platformKey) {
|
|
|
270
325
|
}
|
|
271
326
|
|
|
272
327
|
const transform = getCopyOptions(platformKey, {}).transform;
|
|
273
|
-
const
|
|
328
|
+
const options = {
|
|
274
329
|
src, dest, platformFolder: PLATFORMS[platformKey].folder, ctx, tracker: ctx.trackers[platformKey], transform
|
|
275
|
-
}
|
|
330
|
+
};
|
|
331
|
+
let agg;
|
|
332
|
+
if (excludedNames.size === 0) agg = copyManagedTree(options);
|
|
333
|
+
else {
|
|
334
|
+
agg = { created: 0, updated: 0, unchanged: 0, preserved: 0, missing: 0 };
|
|
335
|
+
for (const name of fs.readdirSync(src)) {
|
|
336
|
+
if (excludedNames.has(name)) continue;
|
|
337
|
+
const child = copyManagedTree({ ...options, sourceRoot: src, src: path.join(src, name), dest: path.join(dest, name) });
|
|
338
|
+
for (const key of Object.keys(agg)) agg[key] += child[key];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
276
341
|
report(ctx, treeAction(agg), 'rules/ directory');
|
|
277
342
|
}
|
|
278
343
|
|
|
@@ -303,6 +368,7 @@ module.exports = {
|
|
|
303
368
|
copyOutputStylesDirectory,
|
|
304
369
|
copyClaudeRuntimeFiles,
|
|
305
370
|
removeObsoleteClaudeRuntimeFiles,
|
|
371
|
+
removeObsoleteRuntimeFiles,
|
|
306
372
|
copyClaudeMdFile,
|
|
307
373
|
copyClaudeAgentsMdFile,
|
|
308
374
|
ensureSharedAgentsMdCore,
|
|
@@ -12,6 +12,26 @@ const { PLATFORMS } = require('../lib/context');
|
|
|
12
12
|
|
|
13
13
|
const SRC_CLAUDE = path.join(__dirname, '../../src/claude');
|
|
14
14
|
|
|
15
|
+
const FORMER_MATCHERS = new Set([
|
|
16
|
+
'Read|Write|Edit|MultiEdit|Bash|Glob',
|
|
17
|
+
'Read|Write|Edit|MultiEdit|Bash|Glob|Grep',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function refreshManagedMatchers(settings, template, ctx) {
|
|
21
|
+
if (!Array.isArray(settings.hooks?.PreToolUse)) return settings;
|
|
22
|
+
const targets = template.hooks?.PreToolUse || [];
|
|
23
|
+
const entries = settings.hooks.PreToolUse.map(entry => {
|
|
24
|
+
if (!FORMER_MATCHERS.has(entry?.matcher) || !Array.isArray(entry.hooks) || !entry.hooks.length) return entry;
|
|
25
|
+
const candidates = targets.filter(target =>
|
|
26
|
+
entry.hooks.every(hook => target.hooks.some(managed => managed.command === hook?.command)));
|
|
27
|
+
if (candidates.length !== 1) return entry;
|
|
28
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Settings: hook PreToolUse matcher updated`);
|
|
29
|
+
ctx.results.updated++;
|
|
30
|
+
return { ...entry, matcher: candidates[0].matcher };
|
|
31
|
+
});
|
|
32
|
+
return { ...settings, hooks: { ...settings.hooks, PreToolUse: entries } };
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/**
|
|
16
36
|
* Remove obsolete hook entries from a settings object whose command matches any
|
|
17
37
|
* manifest-listed substring (e.g. a retired router hook). Returns a possibly
|
|
@@ -97,7 +117,8 @@ function mergeClaudeSettings(ctx, platformKey) {
|
|
|
97
117
|
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
|
-
const mergedSettings =
|
|
120
|
+
const mergedSettings = refreshManagedMatchers(
|
|
121
|
+
pruneObsoleteSettingsHooks({ ...existingSettings }, ctx), managedSettings, ctx);
|
|
101
122
|
|
|
102
123
|
// statusLine: adopt managed value if upgrading, missing, or CafeKit-owned.
|
|
103
124
|
if (managedSettings.statusLine) {
|