@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.
Files changed (167) hide show
  1. package/README.md +12 -1
  2. package/bin/__tests__/adv-skill.test.js +371 -0
  3. package/bin/__tests__/agent-authority.test.js +38 -0
  4. package/bin/__tests__/agent-instructions.test.js +54 -0
  5. package/bin/__tests__/claude-plugin-build.test.js +151 -0
  6. package/bin/__tests__/claude-settings.test.js +123 -0
  7. package/bin/__tests__/codex-ask-guard.test.js +123 -0
  8. package/bin/__tests__/codex-ask-projection.test.js +51 -0
  9. package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
  10. package/bin/__tests__/codex-hooks.test.js +812 -12
  11. package/bin/__tests__/codex-implicit-policy.test.js +160 -0
  12. package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
  13. package/bin/__tests__/codex-native.test.js +198 -58
  14. package/bin/__tests__/codex-plugin-build.test.js +230 -0
  15. package/bin/__tests__/codex-plugin-context.test.js +135 -0
  16. package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
  17. package/bin/__tests__/codex-projection-parity.test.js +314 -0
  18. package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
  19. package/bin/__tests__/codex-research-projection.test.js +70 -0
  20. package/bin/__tests__/codex-runtime-layout.test.js +81 -0
  21. package/bin/__tests__/codex-setup-agents.test.js +216 -0
  22. package/bin/__tests__/codex-specs-projection.test.js +76 -0
  23. package/bin/__tests__/develop-contract.test.js +1 -1
  24. package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
  25. package/bin/__tests__/inspect-paths.test.js +183 -0
  26. package/bin/__tests__/installer-safety.test.js +335 -0
  27. package/bin/__tests__/legacy-hooks-retired.test.js +81 -21
  28. package/bin/__tests__/migrate-apply.test.js +373 -0
  29. package/bin/__tests__/migrate-cli.test.js +152 -0
  30. package/bin/__tests__/migrate-merged.test.js +269 -0
  31. package/bin/__tests__/migrate-plan.test.js +271 -0
  32. package/bin/__tests__/omp-bridge.test.js +147 -17
  33. package/bin/__tests__/omp-hooks.test.js +35 -5
  34. package/bin/__tests__/orca-session.test.js +7 -0
  35. package/bin/__tests__/plugin-safety-keys.test.js +121 -0
  36. package/bin/__tests__/privacy-paths.test.js +501 -0
  37. package/bin/__tests__/usage-hook-retired.test.js +29 -9
  38. package/bin/__tests__/vibe-run.test.js +215 -0
  39. package/bin/__tests__/vibe-skill.test.js +549 -0
  40. package/bin/install.js +124 -1
  41. package/bin/lib/backup.js +43 -24
  42. package/bin/lib/codex-install.js +141 -12
  43. package/bin/lib/context.js +6 -1
  44. package/bin/lib/lock.js +98 -18
  45. package/bin/lib/managed-writer.js +16 -10
  46. package/bin/lib/migrate-apply.js +202 -0
  47. package/bin/lib/migrate-merged.js +342 -0
  48. package/bin/lib/migrate-plan.js +419 -0
  49. package/bin/lib/plugin-inventory.js +33 -0
  50. package/bin/phases/claude-runtime.js +93 -25
  51. package/bin/phases/claude-settings.js +22 -1
  52. package/bin/phases/codex-hooks.js +134 -33
  53. package/bin/phases/codex-runtime.js +110 -85
  54. package/bin/phases/copy-payload.js +50 -4
  55. package/bin/phases/omp-runtime.js +4 -26
  56. package/bin/phases/root-config.js +5 -2
  57. package/package.json +1 -1
  58. package/src/claude/CLAUDE.md +5 -1
  59. package/src/claude/agents/advisor.md +55 -0
  60. package/src/claude/agents/debugger.md +2 -3
  61. package/src/claude/agents/deployer.md +2 -1
  62. package/src/claude/agents/docs-keeper.md +12 -0
  63. package/src/claude/agents/inspector.md +1 -1
  64. package/src/claude/agents/project-manager.md +2 -2
  65. package/src/claude/agents/researcher.md +0 -1
  66. package/src/claude/agents/spec-maker.md +4 -1
  67. package/src/claude/gitignore +3 -0
  68. package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
  69. package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
  70. package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
  71. package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
  72. package/src/claude/hooks/__tests__/precompact.test.js +213 -11
  73. package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
  74. package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
  75. package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
  76. package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
  77. package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -8
  78. package/src/claude/hooks/__tests__/state.test.js +207 -7
  79. package/src/claude/hooks/__tests__/update-check.test.js +88 -0
  80. package/src/claude/hooks/agent.cjs +11 -7
  81. package/src/claude/hooks/inspect-block.cjs +20 -66
  82. package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
  83. package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
  84. package/src/claude/hooks/lib/parser.cjs +1 -1
  85. package/src/claude/hooks/lib/plugin-options.cjs +47 -0
  86. package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
  87. package/src/claude/hooks/lib/privacy-paths.cjs +673 -0
  88. package/src/claude/hooks/lib/project-root.cjs +15 -0
  89. package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
  90. package/src/claude/hooks/lib/spec-touch.cjs +80 -0
  91. package/src/claude/hooks/lib/update-check.cjs +77 -0
  92. package/src/claude/hooks/plugin-context.cjs +80 -0
  93. package/src/claude/hooks/precompact.cjs +8 -15
  94. package/src/claude/hooks/privacy-block.cjs +25 -206
  95. package/src/claude/hooks/rules.cjs +10 -29
  96. package/src/claude/hooks/session.cjs +37 -117
  97. package/src/claude/hooks/spec-gate.cjs +9 -26
  98. package/src/claude/hooks/spec-state.cjs +44 -59
  99. package/src/claude/hooks/state.cjs +101 -52
  100. package/src/claude/migration-manifest.json +14 -2
  101. package/src/claude/plugin/commands/setup-statusline.md +12 -0
  102. package/src/claude/plugin/core.md +71 -0
  103. package/src/claude/rules/hook-protocols.md +1 -1
  104. package/src/claude/rules/manage-docs.md +1 -2
  105. package/src/claude/rules/review-audit-self-decision.md +1 -1
  106. package/src/claude/rules/skill-workflow-routing.md +1 -1
  107. package/src/claude/rules/state-sync.md +14 -0
  108. package/src/claude/rules/workflow.md +1 -1
  109. package/src/claude/runtime.json +0 -3
  110. package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
  111. package/src/claude/scripts/provenance.cjs +54 -16
  112. package/src/claude/scripts/setup-statusline.cjs +88 -0
  113. package/src/claude/settings/settings.json +1 -1
  114. package/src/claude/skills/adv/SKILL.md +139 -0
  115. package/src/claude/skills/adv/references/optional-modes.md +41 -0
  116. package/src/claude/skills/adv/references/ultra-mode.md +29 -0
  117. package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
  118. package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
  119. package/src/claude/skills/brainstorm/SKILL.md +1 -1
  120. package/src/claude/skills/code-review/SKILL.md +4 -1
  121. package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
  122. package/src/claude/skills/develop/SKILL.md +5 -3
  123. package/src/claude/skills/develop/references/quality-gate.md +1 -1
  124. package/src/claude/skills/fix/SKILL.md +2 -2
  125. package/src/claude/skills/git/SKILL.md +5 -3
  126. package/src/claude/skills/git/references/merge-pr.md +125 -0
  127. package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
  128. package/src/claude/skills/specs/SKILL.md +2 -2
  129. package/src/claude/skills/specs/references/review.md +1 -1
  130. package/src/claude/skills/specs/references/templates.md +2 -0
  131. package/src/claude/skills/vibe/SKILL.md +69 -0
  132. package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
  133. package/src/claude/skills/vibe/references/report-templates.md +84 -0
  134. package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
  135. package/src/codex/AGENTS.md +9 -1
  136. package/src/codex/hooks/ask-guard.cjs +54 -0
  137. package/src/codex/hooks/inspect-block.cjs +15 -8
  138. package/src/codex/hooks/lib/hook-context.cjs +118 -4
  139. package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
  140. package/src/codex/hooks/lib/plugin-options.cjs +2 -0
  141. package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
  142. package/src/codex/hooks/lib/privacy-state.cjs +116 -17
  143. package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
  144. package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
  145. package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
  146. package/src/codex/hooks/lib/spec-touch.cjs +3 -0
  147. package/src/codex/hooks/lib/state-store.cjs +5 -4
  148. package/src/codex/hooks/lib/update-check.cjs +1 -0
  149. package/src/codex/hooks/precompact.cjs +40 -0
  150. package/src/codex/hooks/privacy-approval.cjs +28 -17
  151. package/src/codex/hooks/privacy-block.cjs +11 -216
  152. package/src/codex/hooks/rules.cjs +14 -34
  153. package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
  154. package/src/codex/hooks/session.cjs +59 -11
  155. package/src/codex/hooks/spec-gate.cjs +21 -6
  156. package/src/codex/hooks/spec-state.cjs +32 -6
  157. package/src/codex/hooks/state.cjs +25 -4
  158. package/src/codex/hooks.json +39 -7
  159. package/src/codex/plugin/core.md +76 -0
  160. package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
  161. package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
  162. package/src/codex/rules/hook-protocols.md +13 -6
  163. package/src/codex/scripts/setup-agents.cjs +124 -0
  164. package/src/common/AGENTS.md +1 -1
  165. package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
  166. package/src/omp/hooks/privacy-block.cjs +26 -207
  167. 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
- // Exact shapes earlier installers wrote, kept only to recognize and upgrade them. Before
21
- // 0.16.2 the path came from Git; 0.16.2 fixed the path but still resolved `node` through
22
- // PATH. Matching the whole command keeps a hook the user wrote from ever being rewritten.
23
- const LEGACY_POSIX_COMMANDS = [
24
- /^node "\$\(git rev-parse --show-toplevel\)\/\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/,
25
- /^node '(?:[^']|'\\'')*[/\\]hooks[/\\]([a-z0-9-]+\.cjs)'$/,
26
- ];
27
-
28
- function legacyPosixScript(command) {
29
- if (typeof command !== 'string') return null;
30
- for (const pattern of LEGACY_POSIX_COMMANDS) {
31
- const match = command.match(pattern);
32
- if (match) return match[1];
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 null;
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 `/hooks/<script>.cjs` segment must survive, because hookScript()
61
- * reads it as the identity that lets a reinstall recognize CafeKit's own entries.
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 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encoded;
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
- * Repair launchers an older installer wrote. A reinstall treats an already-registered
108
- * script as the user's placement and leaves it alone, so without this pass every existing
109
- * project would keep its broken git-derived command forever. Only the exact byte pattern
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
- const legacy = legacyPosixScript(handler?.command);
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
- if (legacy) next.command = posixHookCommand(legacy, projectRoot);
129
- if (template) next.commandWindows = windowsHookCommand(template[1], projectRoot);
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 || obsolete.length === 0) return { config, removed: 0 };
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: base, repaired } = repairLegacyCodexLaunchers(pruned, projectRoot);
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) written by an older installer`);
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 || []).map((handler) => hookScript(handler?.command))).filter(Boolean),
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, copyManagedTree } = require('../lib/managed-writer');
5
+ const { PLATFORMS, getCopyOptions } = require('../lib/context');
6
+ const { writeManagedFile } = require('../lib/managed-writer');
7
7
  const { report, treeAction } = require('./report');
8
- const { copyRulesDirectory, copyOutputStylesDirectory } = require('./claude-runtime');
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
- const platform = PLATFORMS[platformKey];
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 agg = copyManagedTree({
86
- src: path.join(CODEX_SRC, 'hooks'),
87
- dest: path.join(platform.folder, 'hooks'),
88
- platformFolder: platform.folder,
89
- ctx,
90
- tracker: ctx.trackers[platformKey]
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 = platformKey === 'claude'
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 = { copyPlatformFiles, getPlatformSpecFiles };
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
- removeObsoleteOmpHooks(ctx, platformKey);
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
- /** True if `lines` already carries `pattern` with or without a trailing slash. */
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.1",
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",
@@ -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