@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
|
@@ -17,21 +17,81 @@ const { PLATFORMS } = require('../lib/context');
|
|
|
17
17
|
|
|
18
18
|
const CODEX_SRC = path.join(__dirname, '../../src/codex');
|
|
19
19
|
const HOOK_COMMAND_TEMPLATE = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
const LEGACY_GIT_COMMAND = /^node "\$\(git rev-parse --show-toplevel\)\/\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
21
|
+
const SHELL_QUOTED = "'(?:[^']|'\\\\'')*'";
|
|
22
|
+
const POSIX_COMMAND = new RegExp(`^(?:PATH="\\$PATH"(?::${SHELL_QUOTED})+ )?node (${SHELL_QUOTED})$`);
|
|
23
|
+
const WINDOWS_LAUNCHER = 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ';
|
|
24
|
+
|
|
25
|
+
// Former template values from the hooks.json history. Custom expressions never
|
|
26
|
+
// establish ownership, even when every handler belongs to CafeKit.
|
|
27
|
+
const FORMER_MATCHERS = {
|
|
28
|
+
PreToolUse: new Set([
|
|
29
|
+
'Bash|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*',
|
|
30
|
+
'Bash|exec_command|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*|shell',
|
|
31
|
+
]),
|
|
32
|
+
PostToolUse: new Set([
|
|
33
|
+
'Agent|spawn_agent|update_plan|task|Task|TodoWrite',
|
|
34
|
+
'apply_patch|Edit|Write|MultiEdit',
|
|
35
|
+
]),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function refreshManagedMatchers(config, template, ctx) {
|
|
39
|
+
let updated = 0;
|
|
40
|
+
const hooks = { ...(config.hooks || {}) };
|
|
41
|
+
for (const [event, former] of Object.entries(FORMER_MATCHERS)) {
|
|
42
|
+
if (!Array.isArray(hooks[event])) continue;
|
|
43
|
+
hooks[event] = hooks[event].map(group => {
|
|
44
|
+
const handlers = Array.isArray(group?.hooks) ? group.hooks : [];
|
|
45
|
+
const scripts = handlers.map(managedHandlerScript);
|
|
46
|
+
if (!scripts.some(Boolean)) return group;
|
|
47
|
+
const candidates = handlers.length && scripts.every(Boolean)
|
|
48
|
+
? (template.hooks[event] || []).filter(target => {
|
|
49
|
+
const wanted = target.hooks.map(managedHandlerScript);
|
|
50
|
+
return scripts.every(script => wanted.includes(script));
|
|
51
|
+
}) : [];
|
|
52
|
+
const target = candidates.length === 1 ? candidates[0] : null;
|
|
53
|
+
if (target && (group.matcher || '') === (target.matcher || '')) return group;
|
|
54
|
+
if (target && former.has(group.matcher)) {
|
|
55
|
+
updated += 1;
|
|
56
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: ${event} matcher updated`);
|
|
57
|
+
return { ...group, matcher: target.matcher };
|
|
58
|
+
}
|
|
59
|
+
ctx.ui.warn(`Codex hooks: ${event} matcher preserved; custom, mixed, or ambiguous group. Review it manually.`);
|
|
60
|
+
return group;
|
|
61
|
+
});
|
|
33
62
|
}
|
|
34
|
-
return
|
|
63
|
+
return { config: { ...config, hooks }, updated };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A basename alone cannot prove ownership: users may also run tools/hooks/session.cjs.
|
|
67
|
+
// Only known source scripts at the native runtime path and an exact emitted launcher
|
|
68
|
+
// shape may be rebound or count as already registered.
|
|
69
|
+
const MANAGED_SCRIPTS = new Set(fs.readdirSync(path.join(CODEX_SRC, 'hooks')).filter((name) => /^[a-z0-9-]+\.cjs$/.test(name)));
|
|
70
|
+
|
|
71
|
+
function nativeScript(target) {
|
|
72
|
+
if (!path.posix.isAbsolute(target) && !path.win32.isAbsolute(target)) return null;
|
|
73
|
+
const match = target.match(/[/\\]\.codex[/\\]hooks[/\\]([a-z0-9-]+\.cjs)$/);
|
|
74
|
+
return match && MANAGED_SCRIPTS.has(match[1]) ? match[1] : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function managedCommandScript(command) {
|
|
78
|
+
if (typeof command !== 'string') return null;
|
|
79
|
+
const template = command.match(HOOK_COMMAND_TEMPLATE) || command.match(LEGACY_GIT_COMMAND);
|
|
80
|
+
if (template) return MANAGED_SCRIPTS.has(template[1]) ? template[1] : null;
|
|
81
|
+
const posix = command.match(POSIX_COMMAND);
|
|
82
|
+
if (posix) return nativeScript(posix[1].slice(1, -1).replace(/'\\''/g, "'"));
|
|
83
|
+
if (!command.startsWith(WINDOWS_LAUNCHER)) return null;
|
|
84
|
+
const encoded = command.slice(WINDOWS_LAUNCHER.length);
|
|
85
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) return null;
|
|
86
|
+
const target = Buffer.from(encoded, 'base64url').toString('utf8');
|
|
87
|
+
return Buffer.from(target, 'utf8').toString('base64url') === encoded ? nativeScript(target) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function managedHandlerScript(handler) {
|
|
91
|
+
if (handler?.type !== 'command') return null;
|
|
92
|
+
const fields = ['command', 'commandWindows'].filter((field) => handler[field] !== undefined);
|
|
93
|
+
const scripts = fields.map((field) => managedCommandScript(handler[field]));
|
|
94
|
+
return scripts.length > 0 && scripts.every((script) => script && script === scripts[0]) ? scripts[0] : null;
|
|
35
95
|
}
|
|
36
96
|
|
|
37
97
|
/** The hook script a command runs, which is stable across installs and platforms. */
|
|
@@ -57,8 +117,8 @@ function hookPathFor(script, projectRoot) {
|
|
|
57
117
|
* nothing outside a repository and the *outer* root for a project nested inside one. Both
|
|
58
118
|
* cases produced a path that does not exist, so Codex started every hook, every hook died,
|
|
59
119
|
* and the gates were silently absent. The path is known at install time, so it is written
|
|
60
|
-
* directly. The literal
|
|
61
|
-
*
|
|
120
|
+
* directly. The literal `.codex/hooks/<script>.cjs` path and emitted launcher shape
|
|
121
|
+
* let managedHandlerScript() recognize CafeKit's own entries on a reinstall.
|
|
62
122
|
*/
|
|
63
123
|
/**
|
|
64
124
|
* Directories to fall back on when PATH carries no `node`.
|
|
@@ -88,7 +148,42 @@ function posixHookCommand(script, projectRoot) {
|
|
|
88
148
|
*/
|
|
89
149
|
function windowsHookCommand(script, projectRoot) {
|
|
90
150
|
const encoded = Buffer.from(hookPathFor(script, projectRoot), 'utf8').toString('base64url');
|
|
91
|
-
return
|
|
151
|
+
return WINDOWS_LAUNCHER + encoded;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The Codex plugin cannot carry absolute paths: its root moves on every update and
|
|
155
|
+
// differs per machine. Codex exports PLUGIN_ROOT to plugin hooks, so both launchers
|
|
156
|
+
// resolve it at run time, and the PATH floor lists only machine-independent aliases.
|
|
157
|
+
const PLUGIN_NODE_FALLBACKS = ['/usr/local/bin', '/opt/homebrew/bin'];
|
|
158
|
+
|
|
159
|
+
/** Launchers for one hook script inside the Codex plugin tree. */
|
|
160
|
+
function pluginHookCommands(script) {
|
|
161
|
+
if (!/^[a-z0-9-]+\.cjs$/.test(script)) throw new Error(`Unsupported Codex plugin hook script: ${script}`);
|
|
162
|
+
const fallbacks = PLUGIN_NODE_FALLBACKS.map((dir) => `:${shellQuote(dir)}`).join('');
|
|
163
|
+
return {
|
|
164
|
+
command: `PATH="$PATH"${fallbacks} node "\${PLUGIN_ROOT}/hooks/${script}"`,
|
|
165
|
+
commandWindows: `node -e "process.argv[1]=require('path').join(process.env.PLUGIN_ROOT,'hooks','${script}');require('module').runMain()"`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The plugin's hooks.json: the managed template with every launcher rebound to PLUGIN_ROOT. */
|
|
170
|
+
function pluginHooksConfig(template) {
|
|
171
|
+
const hooks = {};
|
|
172
|
+
for (const [eventName, groups] of Object.entries(template.hooks || {})) {
|
|
173
|
+
hooks[eventName] = groups.map((group) => ({
|
|
174
|
+
...group,
|
|
175
|
+
hooks: group.hooks.map((handler) => {
|
|
176
|
+
const match = String(handler.command).match(HOOK_COMMAND_TEMPLATE);
|
|
177
|
+
if (!match) throw new Error(`Unsupported Codex hook command: ${handler.command}`);
|
|
178
|
+
return { ...handler, ...pluginHookCommands(match[1]) };
|
|
179
|
+
}),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...template,
|
|
184
|
+
description: 'CafeKit lifecycle hooks for the Codex plugin. Review and trust them with /hooks before they run.',
|
|
185
|
+
hooks,
|
|
186
|
+
};
|
|
92
187
|
}
|
|
93
188
|
|
|
94
189
|
/** Rewrite a managed template handler into absolute launchers for both platforms. */
|
|
@@ -104,10 +199,9 @@ function materializeHookCommands(handler, projectRoot) {
|
|
|
104
199
|
}
|
|
105
200
|
|
|
106
201
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* CafeKit itself emitted is rewritten; anything else stays untouched.
|
|
202
|
+
* Rebind recognized CafeKit launchers after an upgrade or a project move. Registration
|
|
203
|
+
* preserves the user's event/matcher placement; only emitted command shapes change,
|
|
204
|
+
* and a custom field for another platform remains untouched.
|
|
111
205
|
*/
|
|
112
206
|
function repairLegacyCodexLaunchers(config, projectRoot) {
|
|
113
207
|
if (!config.hooks) return { config, repaired: 0 };
|
|
@@ -120,13 +214,13 @@ function repairLegacyCodexLaunchers(config, projectRoot) {
|
|
|
120
214
|
return {
|
|
121
215
|
...group,
|
|
122
216
|
hooks: group.hooks.map((handler) => {
|
|
123
|
-
|
|
124
|
-
const template = typeof handler?.commandWindows === 'string'
|
|
125
|
-
&& handler.commandWindows.match(HOOK_COMMAND_TEMPLATE);
|
|
126
|
-
if (!legacy && !template) return handler;
|
|
217
|
+
if (handler?.type !== 'command') return handler;
|
|
127
218
|
const next = { ...handler };
|
|
128
|
-
|
|
129
|
-
|
|
219
|
+
for (const [field, build] of [['command', posixHookCommand], ['commandWindows', windowsHookCommand]]) {
|
|
220
|
+
const script = managedCommandScript(handler[field]);
|
|
221
|
+
if (script) next[field] = build(script, projectRoot);
|
|
222
|
+
}
|
|
223
|
+
if (next.command === handler.command && next.commandWindows === handler.commandWindows) return handler;
|
|
130
224
|
repaired += 1;
|
|
131
225
|
return next;
|
|
132
226
|
}),
|
|
@@ -139,7 +233,7 @@ function repairLegacyCodexLaunchers(config, projectRoot) {
|
|
|
139
233
|
/** Drop CafeKit hooks the manifest has retired, leaving foreign entries untouched. */
|
|
140
234
|
function pruneObsoleteCodexHooks(config, ctx) {
|
|
141
235
|
const obsolete = ctx.manifest?.obsolete?.settingsHookCommandSubstrings || [];
|
|
142
|
-
if (!config.hooks
|
|
236
|
+
if (!config.hooks) return { config, removed: 0 };
|
|
143
237
|
|
|
144
238
|
let removed = 0;
|
|
145
239
|
const pruned = {};
|
|
@@ -150,7 +244,8 @@ function pruneObsoleteCodexHooks(config, ctx) {
|
|
|
150
244
|
if (!Array.isArray(group?.hooks)) { keptGroups.push(group); continue; }
|
|
151
245
|
const keptHooks = group.hooks.filter((handler) => {
|
|
152
246
|
const command = handler?.command || '';
|
|
153
|
-
const isObsolete = obsolete.some((substring) => command.includes(substring))
|
|
247
|
+
const isObsolete = obsolete.some((substring) => command.includes(substring))
|
|
248
|
+
|| (eventName === 'SessionStart' && managedHandlerScript(handler) === 'docs-sync.cjs');
|
|
154
249
|
if (isObsolete) removed += 1;
|
|
155
250
|
return !isObsolete;
|
|
156
251
|
});
|
|
@@ -196,12 +291,15 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
196
291
|
ctx.results.updated++;
|
|
197
292
|
}
|
|
198
293
|
|
|
199
|
-
const { config:
|
|
294
|
+
const { config: rebound, repaired } = repairLegacyCodexLaunchers(pruned, projectRoot);
|
|
200
295
|
if (repaired > 0) {
|
|
201
|
-
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: rebound ${repaired} launcher(s)
|
|
296
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: rebound ${repaired} launcher(s) to the current project`);
|
|
202
297
|
ctx.results.updated++;
|
|
203
298
|
}
|
|
204
299
|
|
|
300
|
+
const { config: base, updated: matcherUpdates } = refreshManagedMatchers(rebound, managed, ctx);
|
|
301
|
+
ctx.results.updated += matcherUpdates;
|
|
302
|
+
|
|
205
303
|
const merged = { ...base };
|
|
206
304
|
if (typeof managed.description === 'string' && typeof merged.description !== 'string') {
|
|
207
305
|
merged.description = managed.description;
|
|
@@ -215,7 +313,7 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
215
313
|
// it. Matching on the script rather than the whole command survives the path
|
|
216
314
|
// differences between installs and platforms.
|
|
217
315
|
const registered = new Set(
|
|
218
|
-
groups.flatMap((group) => (group?.hooks
|
|
316
|
+
groups.flatMap((group) => (Array.isArray(group?.hooks) ? group.hooks : []).map(managedHandlerScript)).filter(Boolean),
|
|
219
317
|
);
|
|
220
318
|
|
|
221
319
|
for (const managedGroup of managedGroups) {
|
|
@@ -242,7 +340,7 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
242
340
|
merged.hooks[eventName] = groups;
|
|
243
341
|
}
|
|
244
342
|
|
|
245
|
-
if (added === 0 && removed === 0) {
|
|
343
|
+
if (added === 0 && removed === 0 && matcherUpdates === 0) {
|
|
246
344
|
ctx.ui.detail(` → ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: already registered`);
|
|
247
345
|
ctx.results.unchanged++;
|
|
248
346
|
}
|
|
@@ -254,8 +352,11 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
254
352
|
}
|
|
255
353
|
|
|
256
354
|
module.exports = {
|
|
355
|
+
managedHandlerScript,
|
|
257
356
|
mergeCodexHooks,
|
|
258
357
|
pruneObsoleteCodexHooks,
|
|
259
358
|
repairLegacyCodexLaunchers,
|
|
260
359
|
hookScript,
|
|
360
|
+
pluginHookCommands,
|
|
361
|
+
pluginHooksConfig,
|
|
261
362
|
};
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { PLATFORMS } = require('../lib/context');
|
|
6
|
-
const { writeManagedFile
|
|
5
|
+
const { PLATFORMS, getCopyOptions } = require('../lib/context');
|
|
6
|
+
const { writeManagedFile } = require('../lib/managed-writer');
|
|
7
7
|
const { report, treeAction } = require('./report');
|
|
8
|
-
const {
|
|
8
|
+
const { isGeneratedArtifact } = require('../lib/copy-utils');
|
|
9
|
+
const { removeObsoleteRuntimeFiles } = require('./claude-runtime');
|
|
9
10
|
const {
|
|
10
11
|
managedRange,
|
|
11
12
|
upsertManagedCodexBlock
|
|
@@ -23,30 +24,9 @@ const CODEX_OWN_RUNTIME = [
|
|
|
23
24
|
['runtime.json', 'runtime.json']
|
|
24
25
|
];
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Delete hooks, scripts and skill files the manifest marks obsolete. Codex tracker keys are project-relative
|
|
28
|
-
* (`.codex/hooks/…`), so the record is pruned through keyFor, and always: a record whose
|
|
29
|
-
* file is already gone must still leave the manifest.
|
|
30
|
-
*/
|
|
27
|
+
/** Use the shared ownership and symlink checks for Codex runtime cleanup. */
|
|
31
28
|
function removeObsoleteCodexHooks(ctx, platformKey) {
|
|
32
|
-
|
|
33
|
-
const tracker = ctx.trackers && ctx.trackers[platformKey];
|
|
34
|
-
const obsoleteHooks = (ctx.manifest?.obsolete?.runtimeFiles || []).filter((rel) => rel.startsWith('hooks/') || rel.startsWith('scripts/') || rel.startsWith('skills/'));
|
|
35
|
-
for (const rel of obsoleteHooks) {
|
|
36
|
-
// Skill files live in the shared skills directory, not under the runtime folder.
|
|
37
|
-
const target = rel.startsWith('skills/')
|
|
38
|
-
? path.join(platform.skillsDir, rel.slice('skills/'.length))
|
|
39
|
-
: path.join(platform.folder, rel);
|
|
40
|
-
const exists = fs.existsSync(target);
|
|
41
|
-
if (!ctx.dryRun) {
|
|
42
|
-
if (exists) fs.rmSync(target, { force: true, recursive: fs.statSync(target).isDirectory() });
|
|
43
|
-
if (tracker) tracker.prune(tracker.keyFor(target));
|
|
44
|
-
}
|
|
45
|
-
if (exists) {
|
|
46
|
-
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete Codex runtime: ${rel}`);
|
|
47
|
-
ctx.results.updated++;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
29
|
+
removeObsoleteRuntimeFiles(ctx, platformKey);
|
|
50
30
|
}
|
|
51
31
|
|
|
52
32
|
function writeSourceFile(ctx, platformKey, src, dest, label) {
|
|
@@ -61,8 +41,105 @@ function writeSourceFile(ctx, platformKey, src, dest, label) {
|
|
|
61
41
|
report(ctx, action, label);
|
|
62
42
|
}
|
|
63
43
|
|
|
44
|
+
// A shim in src/codex/hooks/lib re-exports one Claude library so the source tree
|
|
45
|
+
// keeps a single copy; installs ship the real Claude file in its place.
|
|
46
|
+
const SHIM_PATTERN = /^(?:['"]use strict['"];?\s*)?module\.exports\s*=\s*require\(['"]\.\.\/\.\.\/\.\.\/claude\/hooks\/lib\/([^'"/]+)['"]\);?\s*$/;
|
|
47
|
+
|
|
48
|
+
function isCodexShim(file, name) {
|
|
49
|
+
const match = fs.readFileSync(file, 'utf8').match(SHIM_PATTERN);
|
|
50
|
+
return Boolean(match && match[1] === name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The Codex runtime files and where they land under the runtime root, shared by
|
|
55
|
+
* this installer and the Codex plugin build so the two cannot drift apart.
|
|
56
|
+
* Each entry: { src, dest (relative), group, transform: 'codex-body' | null }.
|
|
57
|
+
*/
|
|
58
|
+
function codexRuntimeLayout() {
|
|
59
|
+
const entries = [];
|
|
60
|
+
const add = (src, dest, group, transform = null) => entries.push({ src, dest, group, transform });
|
|
61
|
+
const hookDir = path.join(CODEX_SRC, 'hooks');
|
|
62
|
+
const libDir = path.join(hookDir, 'lib');
|
|
63
|
+
// Hooks ship file by file; a directory or symlink here would be dropped silently.
|
|
64
|
+
const requireFile = (file) => {
|
|
65
|
+
if (!fs.lstatSync(file).isFile()) throw new Error(`Codex runtime layout expects a regular file: ${file}`);
|
|
66
|
+
return file;
|
|
67
|
+
};
|
|
68
|
+
for (const name of fs.readdirSync(hookDir)) {
|
|
69
|
+
if (name !== 'lib' && !isGeneratedArtifact(name)) add(requireFile(path.join(hookDir, name)), `hooks/${name}`, 'hooks');
|
|
70
|
+
}
|
|
71
|
+
const sharedLibraries = [];
|
|
72
|
+
for (const name of fs.readdirSync(libDir)) {
|
|
73
|
+
if (isGeneratedArtifact(name)) continue;
|
|
74
|
+
const file = requireFile(path.join(libDir, name));
|
|
75
|
+
if (isCodexShim(file, name)) sharedLibraries.push(name);
|
|
76
|
+
else add(file, `hooks/lib/${name}`, 'hooks');
|
|
77
|
+
}
|
|
78
|
+
add(path.join(SRC, 'claude', 'runtime.schema.json'), 'runtime.schema.json', 'schema');
|
|
79
|
+
for (const name of sharedLibraries) add(path.join(SRC, 'claude', 'hooks', 'lib', name), `hooks/lib/${name}`, 'shared-lib');
|
|
80
|
+
const overrides = fs.readdirSync(path.join(CODEX_SRC, 'rules')).filter((name) => !isGeneratedArtifact(name));
|
|
81
|
+
for (const name of fs.readdirSync(path.join(SRC, 'claude', 'rules'))) {
|
|
82
|
+
if (!overrides.includes(name) && !isGeneratedArtifact(name)) add(path.join(SRC, 'claude', 'rules', name), `rules/${name}`, 'rules', 'codex-body');
|
|
83
|
+
}
|
|
84
|
+
for (const name of fs.readdirSync(path.join(SRC, 'claude', 'output-styles'))) {
|
|
85
|
+
if (!isGeneratedArtifact(name)) add(path.join(SRC, 'claude', 'output-styles', name), `output-styles/${name}`, 'output-styles', 'codex-body');
|
|
86
|
+
}
|
|
87
|
+
for (const name of overrides) add(path.join(CODEX_SRC, 'rules', name), `rules/${name}`, 'rule-overrides');
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every layout group in install order with its report label; null reports each file. */
|
|
92
|
+
const LAYOUT_GROUPS = [
|
|
93
|
+
['hooks', 'Codex native hooks'],
|
|
94
|
+
['schema', null],
|
|
95
|
+
['shared-lib', null],
|
|
96
|
+
['rules', 'rules/ directory'],
|
|
97
|
+
['output-styles', 'output-styles/ directory'],
|
|
98
|
+
['rule-overrides', 'Codex native rule overrides']
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const SHARED_LIB_LABELS = {
|
|
102
|
+
'runtime.schema.json': 'Codex runtime schema',
|
|
103
|
+
'privacy-command-analysis.cjs': 'Codex privacy command analyzer',
|
|
104
|
+
'privacy-paths.cjs': 'Codex privacy path analyzer',
|
|
105
|
+
'secret-keywords.cjs': 'Codex secret keyword matcher',
|
|
106
|
+
'runtime-path-safety.cjs': 'Codex runtime path safety',
|
|
107
|
+
'inspect-paths.cjs': 'Codex inspection path analyzer',
|
|
108
|
+
'spec-touch.cjs': 'Codex spec touch helpers',
|
|
109
|
+
'update-check.cjs': 'Codex update check'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Write one layout group; `label` reports the group, or each file when omitted. */
|
|
113
|
+
function writeLayoutGroup(ctx, platformKey, entries, label) {
|
|
114
|
+
const platform = PLATFORMS[platformKey];
|
|
115
|
+
const bodyTransform = getCopyOptions(platformKey, {}).transform;
|
|
116
|
+
const agg = { created: 0, updated: 0, unchanged: 0, preserved: 0, missing: 0 };
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const { action } = writeManagedFile({
|
|
119
|
+
src: entry.src,
|
|
120
|
+
dest: path.join(platform.folder, entry.dest),
|
|
121
|
+
sourceRoot: SRC,
|
|
122
|
+
platformFolder: platform.folder,
|
|
123
|
+
ctx,
|
|
124
|
+
tracker: ctx.trackers[platformKey],
|
|
125
|
+
transform: entry.transform === 'codex-body' ? bodyTransform : undefined
|
|
126
|
+
});
|
|
127
|
+
if (label) {
|
|
128
|
+
if (agg[action] !== undefined) agg[action]++;
|
|
129
|
+
} else {
|
|
130
|
+
const name = path.basename(entry.dest);
|
|
131
|
+
report(ctx, action, SHARED_LIB_LABELS[name] || `Codex hook helper: ${name}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (label) report(ctx, treeAction(agg), label);
|
|
135
|
+
}
|
|
136
|
+
|
|
64
137
|
function installRuntimeFiles(ctx, platformKey) {
|
|
65
138
|
const platform = PLATFORMS[platformKey];
|
|
139
|
+
// Validate the whole layout before the first write, so a bad source cannot leave a partial install.
|
|
140
|
+
const layout = codexRuntimeLayout();
|
|
141
|
+
const unknown = layout.filter((entry) => !LAYOUT_GROUPS.some(([name]) => name === entry.group));
|
|
142
|
+
if (unknown.length) throw new Error(`Codex runtime layout group not installed: ${unknown[0].group}`);
|
|
66
143
|
for (const [sourceRel, targetRel] of CODEX_OWN_RUNTIME) {
|
|
67
144
|
writeSourceFile(
|
|
68
145
|
ctx,
|
|
@@ -82,61 +159,12 @@ function installRuntimeFiles(ctx, platformKey) {
|
|
|
82
159
|
|
|
83
160
|
mergeCodexHooks(ctx, platformKey);
|
|
84
161
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
ctx,
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
report(ctx, treeAction(agg), 'Codex native hooks');
|
|
93
|
-
removeObsoleteCodexHooks(ctx, platformKey);
|
|
94
|
-
// Keep one canonical command analyzer in the Claude source tree, then
|
|
95
|
-
// materialize the same bytes into the installed Codex runtime.
|
|
96
|
-
// One canonical runtime schema lives in the Claude source tree; both installed
|
|
97
|
-
// runtimes reference it as ./runtime.schema.json beside their runtime.json.
|
|
98
|
-
writeSourceFile(
|
|
99
|
-
ctx,
|
|
100
|
-
platformKey,
|
|
101
|
-
path.join(SRC, 'claude', 'runtime.schema.json'),
|
|
102
|
-
path.join(platform.folder, 'runtime.schema.json'),
|
|
103
|
-
'Codex runtime schema'
|
|
104
|
-
);
|
|
105
|
-
writeSourceFile(
|
|
106
|
-
ctx,
|
|
107
|
-
platformKey,
|
|
108
|
-
path.join(SRC, 'claude', 'hooks', 'lib', 'privacy-command-analysis.cjs'),
|
|
109
|
-
path.join(platform.folder, 'hooks', 'lib', 'privacy-command-analysis.cjs'),
|
|
110
|
-
'Codex privacy command analyzer'
|
|
111
|
-
);
|
|
112
|
-
writeSourceFile(
|
|
113
|
-
ctx,
|
|
114
|
-
platformKey,
|
|
115
|
-
path.join(SRC, 'claude', 'hooks', 'lib', 'secret-keywords.cjs'),
|
|
116
|
-
path.join(platform.folder, 'hooks', 'lib', 'secret-keywords.cjs'),
|
|
117
|
-
'Codex secret keyword matcher'
|
|
118
|
-
);
|
|
119
|
-
writeSourceFile(
|
|
120
|
-
ctx,
|
|
121
|
-
platformKey,
|
|
122
|
-
path.join(SRC, 'claude', 'hooks', 'lib', 'runtime-path-safety.cjs'),
|
|
123
|
-
path.join(platform.folder, 'hooks', 'lib', 'runtime-path-safety.cjs'),
|
|
124
|
-
'Codex runtime path safety'
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function installNativeRuleOverrides(ctx, platformKey) {
|
|
129
|
-
const platform = PLATFORMS[platformKey];
|
|
130
|
-
const source = path.join(CODEX_SRC, 'rules');
|
|
131
|
-
if (!fs.existsSync(source)) return;
|
|
132
|
-
const agg = copyManagedTree({
|
|
133
|
-
src: source,
|
|
134
|
-
dest: path.join(platform.folder, 'rules'),
|
|
135
|
-
platformFolder: platform.folder,
|
|
136
|
-
ctx,
|
|
137
|
-
tracker: ctx.trackers[platformKey]
|
|
138
|
-
});
|
|
139
|
-
report(ctx, treeAction(agg), 'Codex native rule overrides');
|
|
162
|
+
for (const [name, label] of LAYOUT_GROUPS) {
|
|
163
|
+
writeLayoutGroup(ctx, platformKey, layout.filter((entry) => entry.group === name), label);
|
|
164
|
+
// Obsolete hook files go before the canonical schema and shared Claude libraries
|
|
165
|
+
// are materialized, matching the order earlier installs used.
|
|
166
|
+
if (name === 'hooks') removeObsoleteCodexHooks(ctx, platformKey);
|
|
167
|
+
}
|
|
140
168
|
}
|
|
141
169
|
|
|
142
170
|
function installManagedAgentsMd(ctx) {
|
|
@@ -170,11 +198,8 @@ function installManagedAgentsMd(ctx) {
|
|
|
170
198
|
function installCodexRuntime(ctx, platformKey) {
|
|
171
199
|
if (platformKey !== 'codex') return ctx;
|
|
172
200
|
installRuntimeFiles(ctx, platformKey);
|
|
173
|
-
copyRulesDirectory(ctx, platformKey);
|
|
174
|
-
copyOutputStylesDirectory(ctx, platformKey);
|
|
175
|
-
installNativeRuleOverrides(ctx, platformKey);
|
|
176
201
|
installManagedAgentsMd(ctx);
|
|
177
202
|
return ctx;
|
|
178
203
|
}
|
|
179
204
|
|
|
180
|
-
module.exports = { installCodexRuntime };
|
|
205
|
+
module.exports = { installCodexRuntime, codexRuntimeLayout, LAYOUT_GROUPS };
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
getCopyOptions
|
|
17
17
|
} = require('../lib/context');
|
|
18
18
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
19
|
+
const { hashFile } = require('../lib/manifest');
|
|
19
20
|
const { report, treeAction } = require('./report');
|
|
20
21
|
const {
|
|
21
22
|
codexAgentName,
|
|
@@ -24,6 +25,43 @@ const {
|
|
|
24
25
|
|
|
25
26
|
const SRC = path.join(__dirname, '../../src');
|
|
26
27
|
|
|
28
|
+
// Codex ignores `disable-model-invocation`; its explicit-only switch is this file beside SKILL.md.
|
|
29
|
+
const CODEX_POLICY_FILE = path.join('agents', 'openai.yaml');
|
|
30
|
+
const CODEX_EXPLICIT_ONLY_POLICY = 'policy:\n allow_implicit_invocation: false\n';
|
|
31
|
+
|
|
32
|
+
function isExplicitOnlySkill(skillSource) {
|
|
33
|
+
const skillFile = path.join(skillSource, 'SKILL.md');
|
|
34
|
+
if (!fs.existsSync(skillFile)) return false;
|
|
35
|
+
const frontmatter = fs.readFileSync(skillFile, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
36
|
+
// Quoted or bare, as the catalog parser reads it: a skill the catalog hides must also get the policy.
|
|
37
|
+
return Boolean(frontmatter) && /^disable-model-invocation:\s*(["']?)true\1\s*$/m.test(frontmatter[1]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Keep a Codex skill's policy file in step with its source: write it (owned) for an explicit-only
|
|
42
|
+
* skill, and remove an owned, unmodified copy once the skill is no longer explicit-only. A copy the
|
|
43
|
+
* user wrote or edited is left alone.
|
|
44
|
+
*/
|
|
45
|
+
function syncCodexInvocationPolicy({ skillSource, skillDest, platformFolder, ctx, tracker }) {
|
|
46
|
+
const dest = path.join(skillDest, CODEX_POLICY_FILE);
|
|
47
|
+
if (isExplicitOnlySkill(skillSource)) {
|
|
48
|
+
const { action } = writeManagedFile({ dest, platformFolder, ctx, tracker, content: CODEX_EXPLICIT_ONLY_POLICY });
|
|
49
|
+
report(ctx, action, `skill policy: ${path.basename(skillDest)}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isFile()) return;
|
|
53
|
+
const key = tracker.keyFor(dest);
|
|
54
|
+
const owned = ctx.ownership?.[platformFolder]?.files?.[key]?.sha256;
|
|
55
|
+
if (!owned || owned !== hashFile(dest)) return;
|
|
56
|
+
if (!ctx.dryRun) {
|
|
57
|
+
fs.rmSync(dest, { force: true });
|
|
58
|
+
tracker.prune(key);
|
|
59
|
+
const agentsDir = path.dirname(dest);
|
|
60
|
+
if (fs.readdirSync(agentsDir).length === 0) fs.rmdirSync(agentsDir);
|
|
61
|
+
}
|
|
62
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed skill policy: ${dest}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
27
65
|
/**
|
|
28
66
|
* Spec command files installed for a platform. Claude ships spec-* commands.
|
|
29
67
|
*/
|
|
@@ -97,9 +135,7 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
97
135
|
// The specs skill tree above carries its templates; remove the ones retired
|
|
98
136
|
// with the spec.json planning flow from earlier installs.
|
|
99
137
|
if (platformKey === 'claude' || platformKey === 'codex') {
|
|
100
|
-
const retiredTemplates =
|
|
101
|
-
? ['init.json', 'spec-state.json', 'requirements-init.md', 'requirements.md', 'design.md', 'task.md']
|
|
102
|
-
: ['spec-state.json', 'requirements-init.md', 'requirements.md', 'design.md', 'task.md'];
|
|
138
|
+
const retiredTemplates = ['init.json', 'spec-state.json', 'requirements-init.md', 'requirements.md', 'design.md', 'task.md'];
|
|
103
139
|
for (const fileName of retiredTemplates) {
|
|
104
140
|
const retired = path.join(platform.skillsDir, 'specs', 'templates', fileName);
|
|
105
141
|
if (!fs.existsSync(retired)) continue;
|
|
@@ -134,6 +170,9 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
134
170
|
});
|
|
135
171
|
ctx.results.installedSkills++;
|
|
136
172
|
report(ctx, treeAction(agg), `Skill: ${skillName}`);
|
|
173
|
+
if (platformKey === 'codex') {
|
|
174
|
+
syncCodexInvocationPolicy({ skillSource, skillDest, platformFolder, ctx, tracker });
|
|
175
|
+
}
|
|
137
176
|
} else {
|
|
138
177
|
report(ctx, 'missing', `skill: ${skillName}`);
|
|
139
178
|
}
|
|
@@ -223,4 +262,11 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
223
262
|
return ctx;
|
|
224
263
|
}
|
|
225
264
|
|
|
226
|
-
module.exports = {
|
|
265
|
+
module.exports = {
|
|
266
|
+
copyPlatformFiles,
|
|
267
|
+
getPlatformSpecFiles,
|
|
268
|
+
// Shared with the Codex plugin build so plugin and npx skills carry the same policy.
|
|
269
|
+
isExplicitOnlySkill,
|
|
270
|
+
CODEX_POLICY_FILE,
|
|
271
|
+
CODEX_EXPLICIT_ONLY_POLICY
|
|
272
|
+
};
|
|
@@ -23,7 +23,7 @@ const path = require('path');
|
|
|
23
23
|
const { PLATFORMS } = require('../lib/context');
|
|
24
24
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
25
25
|
const { report, treeAction } = require('./report');
|
|
26
|
-
const { copyRulesDirectory } = require('./claude-runtime');
|
|
26
|
+
const { copyRulesDirectory, removeObsoleteRuntimeFiles } = require('./claude-runtime');
|
|
27
27
|
|
|
28
28
|
const SRC = path.join(__dirname, '../../src');
|
|
29
29
|
const CLAUDE_SRC = path.join(SRC, 'claude');
|
|
@@ -128,34 +128,12 @@ function installBridge(ctx, platformKey) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
132
|
-
* Delete hooks, scripts and skill files the manifest marks obsolete. Mirrors removeObsoleteClaudeRuntimeFiles, but the
|
|
133
|
-
* omp tracker records keys relative to the project root (`.omp/hooks/…`), so the record is
|
|
134
|
-
* pruned through keyFor rather than by the bare `hooks/…` path.
|
|
135
|
-
*/
|
|
136
|
-
function removeObsoleteOmpHooks(ctx, platformKey) {
|
|
137
|
-
const platform = PLATFORMS[platformKey];
|
|
138
|
-
const tracker = ctx.trackers && ctx.trackers[platformKey];
|
|
139
|
-
const obsoleteHooks = (ctx.manifest?.obsolete?.runtimeFiles || []).filter((rel) => rel.startsWith('hooks/') || rel.startsWith('scripts/') || rel.startsWith('skills/'));
|
|
140
|
-
for (const rel of obsoleteHooks) {
|
|
141
|
-
// Skill files live in the shared skills directory, not under the runtime folder.
|
|
142
|
-
const target = rel.startsWith('skills/')
|
|
143
|
-
? path.join(platform.skillsDir, rel.slice('skills/'.length))
|
|
144
|
-
: path.join(platform.folder, rel);
|
|
145
|
-
if (!fs.existsSync(target)) continue;
|
|
146
|
-
if (!ctx.dryRun) {
|
|
147
|
-
fs.rmSync(target, { force: true, recursive: fs.statSync(target).isDirectory() });
|
|
148
|
-
if (tracker) tracker.prune(tracker.keyFor(target));
|
|
149
|
-
}
|
|
150
|
-
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete omp runtime: ${rel}`);
|
|
151
|
-
ctx.results.updated++;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
131
|
function installOmpRuntime(ctx, platformKey) {
|
|
156
132
|
if (platformKey !== 'omp') return;
|
|
157
133
|
installHooks(ctx, platformKey);
|
|
158
|
-
|
|
134
|
+
// Obsolete hooks, scripts and the old `.mjs` bridge go only when their bytes still match the
|
|
135
|
+
// ownership record; an edited, untracked or symlinked copy is kept and reported.
|
|
136
|
+
removeObsoleteRuntimeFiles(ctx, platformKey);
|
|
159
137
|
installRuntimeFiles(ctx, platformKey);
|
|
160
138
|
installBridge(ctx, platformKey);
|
|
161
139
|
copyRulesDirectory(ctx, platformKey);
|
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* True if `lines` already carries `pattern` with or without a trailing slash, or as
|
|
22
|
+
* `dir/*`, which ignores the same files while letting a later `!dir/sub/` re-include one.
|
|
23
|
+
*/
|
|
21
24
|
function hasPattern(lines, pattern) {
|
|
22
25
|
const bare = pattern.replace(/\/$/, '');
|
|
23
26
|
const withSlash = bare + '/';
|
|
24
|
-
return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
|
|
27
|
+
return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash) || lines.includes(bare + '/*');
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const PLAN_PATTERNS = new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
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",
|
package/src/claude/CLAUDE.md
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
## Claude Code runtime
|
|
9
9
|
|
|
10
|
+
Project notes (commands, files to leave unchanged, slow or expensive operations)
|
|
11
|
+
belong in the root AGENTS.md after the CAFEKIT CORE END marker; Claude Code reads
|
|
12
|
+
them through `@AGENTS.md`.
|
|
13
|
+
|
|
10
14
|
- Edit project skills under `.claude/skills/`, not `~/.claude/skills`.
|
|
11
15
|
- Run Python skill scripts with the skill venv:
|
|
12
16
|
- macOS/Linux: `.claude/skills/.venv/bin/python3 scripts/<script>.py`
|
|
@@ -17,7 +21,7 @@
|
|
|
17
21
|
GATE-REVIEW after adversarial review. It never starts implementation.
|
|
18
22
|
- Start implementation only through a new explicit `/cf:develop` invocation.
|
|
19
23
|
Execute one unblocked task at a time; each task has exactly one `Status:`
|
|
20
|
-
field and the controller is its sole state-and-proof writer.
|
|
24
|
+
field and the controller is its sole state-and-proof writer. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
21
25
|
- Use `/cf:sync` for surgical updates to observed file state. A done task
|
|
22
26
|
requires a canonical final inline `## Receipt` with the exact command,
|
|
23
27
|
`Exit: 0`, `Verification: PASS`, runtime-derived Base and Head values, and
|