@codyswann/lisa 2.124.1 → 2.124.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 (78) hide show
  1. package/dist/agy/mcp-installer.d.ts +22 -5
  2. package/dist/agy/mcp-installer.d.ts.map +1 -1
  3. package/dist/agy/mcp-installer.js +52 -12
  4. package/dist/agy/mcp-installer.js.map +1 -1
  5. package/dist/codex/hooks-installer.d.ts.map +1 -1
  6. package/dist/codex/hooks-installer.js +12 -11
  7. package/dist/codex/hooks-installer.js.map +1 -1
  8. package/dist/core/lisa.d.ts +19 -11
  9. package/dist/core/lisa.d.ts.map +1 -1
  10. package/dist/core/lisa.js +44 -12
  11. package/dist/core/lisa.js.map +1 -1
  12. package/package.json +1 -1
  13. package/plugins/lisa/.claude-plugin/plugin.json +1 -10
  14. package/plugins/lisa/{hooks → .codex-plugin}/hooks.json +0 -11
  15. package/plugins/lisa/.codex-plugin/plugin.json +2 -2
  16. package/plugins/lisa/hooks/block-no-verify.agy.sh +45 -0
  17. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +45 -0
  18. package/plugins/lisa-agy/hooks.json +15 -0
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -12
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -12
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +2 -2
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +2 -2
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails/.codex-plugin/plugin.json +2 -2
  49. package/plugins/lisa-rails-agy/plugin.json +1 -1
  50. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript/.codex-plugin/plugin.json +2 -2
  54. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  55. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  60. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/src/base/.claude-plugin/plugin.json +0 -1
  63. package/plugins/src/base/hooks/block-no-verify.agy.sh +45 -0
  64. package/scripts/generate-agy-plugin-artifacts.mjs +158 -19
  65. package/scripts/generate-codex-plugin-artifacts.mjs +45 -25
  66. package/scripts/generate-copilot-plugin-artifacts.mjs +6 -5
  67. package/scripts/generate-cursor-plugin-artifacts.mjs +5 -3
  68. package/scripts/lib/per-agent-hook-filter.mjs +29 -18
  69. package/dist/codex/scripts/notify-ntfy.sh +0 -18
  70. package/plugins/lisa/hooks/notify-ntfy.sh +0 -183
  71. package/plugins/lisa-copilot/hooks/notify-ntfy.sh +0 -183
  72. package/plugins/lisa-cursor/hooks/notify-ntfy.sh +0 -183
  73. package/plugins/lisa-expo-agy/.mcp.json +0 -8
  74. package/plugins/src/base/hooks/notify-ntfy.sh +0 -183
  75. /package/plugins/lisa-harper-fabric/{hooks → .codex-plugin}/hooks.json +0 -0
  76. /package/plugins/lisa-nestjs/{hooks → .codex-plugin}/hooks.json +0 -0
  77. /package/plugins/lisa-rails/{hooks → .codex-plugin}/hooks.json +0 -0
  78. /package/plugins/lisa-typescript/{hooks → .codex-plugin}/hooks.json +0 -0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # Antigravity (agy) PreToolUse hook: blocks shell commands that bypass Lisa's
3
+ # git quality gates via `--no-verify` (exact parity with the Claude
4
+ # block-no-verify.sh — only the long flag, to avoid `-n` false positives).
5
+ #
6
+ # agy protocol (distinct from the Claude block-no-verify.sh exit-code protocol):
7
+ # - stdin = JSON: { "toolCall": { "name": "run_command",
8
+ # "args": { "CommandLine": "<shell command>" } }, ... }
9
+ # - stdout = JSON decision: {"decision":"deny","reason":"..."} | {"decision":"allow"}
10
+ #
11
+ # Shipped as a GLOBAL agy plugin hook (hooks.json at the plugin root, installed
12
+ # to ~/.gemini/config/plugins/<variant>/). Matches agy's shell tool `run_command`
13
+ # on PreToolUse. jq parses the JSON envelope (per project rule: never grep/sed
14
+ # JSON); the command string itself is matched with grep (it is a plain string,
15
+ # not JSON). Malformed/empty stdin → allow (fail open, never crash the tool).
16
+ set -uo pipefail
17
+
18
+ allow() {
19
+ printf '%s\n' '{"decision":"allow"}'
20
+ exit 0
21
+ }
22
+
23
+ deny() {
24
+ printf '%s\n' '{"decision":"deny","reason":"--no-verify bypasses Lisa pre-commit/pre-push quality gates. Fix the underlying issue (lint, tests, formatting) or ask the user before bypassing."}'
25
+ exit 0
26
+ }
27
+
28
+ input="$(cat 2>/dev/null || true)"
29
+ [ -z "$input" ] && allow
30
+
31
+ command_str="$(printf '%s' "$input" | jq -r '.toolCall.args.CommandLine // empty' 2>/dev/null || true)"
32
+ [ -z "$command_str" ] && allow
33
+
34
+ # Bounded --no-verify (catches `git commit`/`git push --no-verify` in any
35
+ # position, incl. subshells) while excluding longer flags like --no-verify-ssl.
36
+ # Exact parity with the Claude block-no-verify.sh: only --no-verify is matched.
37
+ # The short `-n` form is intentionally NOT matched — `-n` appears in commit
38
+ # message prose (`git commit -m "fix -n flag"`) and unrelated piped commands
39
+ # (`sort -n x; git commit`), so guarding it false-positives on valid work.
40
+ if printf '%s' "$command_str" |
41
+ grep -Eq '(^|[^[:alnum:]_-])--no-verify($|[^[:alnum:]_-])'; then
42
+ deny
43
+ fi
44
+
45
+ allow
@@ -4,12 +4,33 @@
4
4
  * Claude artifact.
5
5
  *
6
6
  * agy's plugin manifest is a bare `plugin.json` at the plugin root (NOT
7
- * `.claude-plugin/plugin.json`). The Wave 1 audit also established that agy
8
- * plugin-bundled hooks DO NOT FIRE in `-p` headless mode, so the agy variant
9
- * ships no hooks at all — the manifest's `hooks` field is dropped and the
10
- * `hooks/` directory is omitted. Rules-injection for agy uses the AGENTS.md
11
- * bake-in alternative implemented in `src/agy/rules-bake.ts` (per the parity
12
- * research artifact's Cluster 4-agy / Option α).
7
+ * `.claude-plugin/plugin.json`). This generator copies + reshapes the artifact
8
+ * AND emits a plugin-bundled hooks config.
9
+ *
10
+ * HOOKS (plugin-bundled, ROOT-level): a runtime probe of agy 1.0.3 (ticket-1054)
11
+ * proved agy loads a plugin's hooks ONLY from a `hooks.json` at the plugin ROOT
12
+ * of an installed global plugin (`~/.gemini/config/plugins/<variant>/hooks.json`)
13
+ * — a `hooks/` SUBDIR hooks.json (the earlier attempt) is NOT scanned. Lisa
14
+ * already `agy plugin install`s these variants there, so this generator emits a
15
+ * root `hooks.json` in agy's schema (top-level HOOK NAME → event → handlers),
16
+ * matcher `run_command` (agy's shell tool), and ships the agy-protocol script
17
+ * into the variant's `hooks/` subdir (scripts in a subdir are fine — only
18
+ * hooks.json must be at root; the command points at the absolute installed path
19
+ * via `$HOME`). Only events agy supports map: PreToolUse / PostToolUse /
20
+ * PreInvocation / PostInvocation / Stop. SessionStart is NOT supported, so
21
+ * install-pkgs / setup-jira-cli CANNOT ship as agy hooks — only block-no-verify
22
+ * (PreToolUse) maps. Only the BASE plugin manifest carries the universal hooks,
23
+ * so only `lisa-agy` gets a hooks.json; stack variants emit none.
24
+ *
25
+ * MCP (user-global, NOT plugin-bundled): agy ignores plugin-bundled MCP and only
26
+ * reads the user-global `~/.gemini/config/mcp_config.json`, so MCP is delivered
27
+ * by the runtime installer (`src/agy/mcp-installer.ts`), and this generator
28
+ * drops `.mcp.json`. Rules use the AGENTS.md bake (rules-once invariant).
29
+ *
30
+ * Net: the agy variant ships a root `hooks.json` (base only) + its agy-protocol
31
+ * script under `hooks/`, but NO `mcp_config.json`, NO `.mcp.json`, NO `rules/`,
32
+ * and NO `hooks/hooks.json` subdir. The manifest carries neither `hooks` nor
33
+ * `mcpServers`.
13
34
  *
14
35
  * Usage: node scripts/generate-agy-plugin-artifacts.mjs <source-plugin-dir> <out-dir> <version>
15
36
  *
@@ -74,12 +95,16 @@ function copyDir(src, dst, keep = () => true) {
74
95
  /**
75
96
  * Generate the agy variant.
76
97
  *
77
- * Transformation steps (from Wave 2 pattern-b-fan-out-spec.md):
98
+ * Transformation steps:
78
99
  * 0. Filter skills/ against scripts/internal-agy-skill-policy.json.
79
- * 1. Copy source to outDir minus filtered skills, .codex-plugin/, hooks/, and rules/.
100
+ * 1. Copy source to outDir minus filtered skills, .codex-plugin/, hooks/,
101
+ * rules/, and the untranslated .mcp.json.
80
102
  * 2. Move .claude-plugin/plugin.json to bare plugin.json at root; drop .claude-plugin/.
81
- * 3. Drop the hooks field from the manifest (agy plugin hooks don't fire in -p).
103
+ * 3. Drop the hooks + mcpServers fields from the manifest (delivered by the
104
+ * root hooks.json / runtime MCP installer, not as manifest components).
82
105
  * 4. Inject the version.
106
+ * 5. Emit the plugin-bundled root hooks.json (base variant only) + copy the
107
+ * agy-protocol script(s) into the variant's hooks/ subdir.
83
108
  *
84
109
  * @param {string} srcDir Built Claude plugin directory (input).
85
110
  * @param {string} outDir agy variant output directory.
@@ -101,8 +126,14 @@ export function generateAgyVariant(srcDir, outDir, version) {
101
126
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin") {
102
127
  return false;
103
128
  }
104
- if (relPath.startsWith("hooks/") || relPath === "hooks") return false; // hooks don't fire on agy
105
- if (relPath.startsWith("rules/") || relPath === "rules") return false; // rules not a plugin component on agy
129
+ // Drop the source hooks/ (Claude scripts + stale codex hooks.json). The agy
130
+ // hooks.json (root) + the agy-protocol script are re-emitted by
131
+ // emitAgyPluginHooks below.
132
+ if (relPath.startsWith("hooks/") || relPath === "hooks") return false;
133
+ if (relPath.startsWith("rules/") || relPath === "rules") return false; // rules delivered via AGENTS.md bake
134
+ // Drop the untranslated Claude .mcp.json — agy ignores it (and the agy
135
+ // MCP shape differs); MCP is delivered by the user-global runtime MCP installer.
136
+ if (relPath === ".mcp.json") return false;
106
137
  // Drop Codex-specific per-skill openai.yaml artifacts.
107
138
  if (/^skills\/[^/]+\/agents\/openai\.ya?ml$/.test(relPath)) return false;
108
139
  // Apply skill denylist.
@@ -132,26 +163,134 @@ export function generateAgyVariant(srcDir, outDir, version) {
132
163
  }
133
164
  }
134
165
 
135
- // 2. Read the Claude manifest, drop hooks, rename to bare plugin.json.
166
+ // 2. Read the Claude manifest, drop hooks + mcpServers, write bare plugin.json.
167
+ // agy reads hooks from a root hooks.json (emitted below), not the manifest;
168
+ // MCP is user-global. So the bare manifest carries neither field.
136
169
  const manifest = JSON.parse(fs.readFileSync(claudeManifest, "utf8"));
137
170
  manifest.version = version;
171
+ const sourceHooks = manifest.hooks ?? {};
138
172
  delete manifest.hooks;
139
-
140
- // 3. Drop any pointer fields that agy doesn't understand.
141
- // agy reads bare plugin.json with components: skills, agents, commands,
142
- // mcpServers, hooks. We omit hooks above. MCP is not a plugin component on
143
- // agy, so drop any `mcpServers` if present (Lisa's base today does not
144
- // emit one, but be defensive).
145
173
  delete manifest.mcpServers;
146
174
 
147
175
  const bareManifestPath = path.join(outDir, "plugin.json");
148
176
  fs.writeFileSync(bareManifestPath, JSON.stringify(manifest, null, 2) + "\n");
149
177
 
150
- // 4. Ensure no .claude-plugin/ directory survives.
178
+ // 3. Ensure no .claude-plugin/ directory survives.
151
179
  const ghostDir = path.join(outDir, ".claude-plugin");
152
180
  if (fs.existsSync(ghostDir)) {
153
181
  fs.rmSync(ghostDir, { recursive: true, force: true });
154
182
  }
183
+
184
+ // 4. Emit the plugin-bundled root hooks.json + agy-protocol script (base only).
185
+ // `agy plugin install` names the install dir by the manifest `name`
186
+ // (`~/.gemini/config/plugins/<name>/`, verified-by-run per
187
+ // reference_agy_plugin_capabilities), NOT the source dir basename — so the
188
+ // hook command path must use manifest.name (e.g. "lisa"), falling back to the
189
+ // dir basename only if a manifest somehow omits name.
190
+ const installDirName = manifest.name ?? path.basename(outDir);
191
+ emitAgyPluginHooks(srcDir, outDir, sourceHooks, installDirName);
192
+ }
193
+
194
+ /**
195
+ * agy-portable hook map. Only events agy supports + scripts whose protocol has
196
+ * an agy variant. Each entry emits one top-level hook-name key in the root
197
+ * hooks.json. `sourceScript` is what the BASE Claude manifest references (used
198
+ * to detect whether this variant should carry the hook); `agyScript` is the
199
+ * agy-protocol script copied into the variant's hooks/ and referenced by the
200
+ * command. NOTE: install-pkgs / setup-jira-cli are SessionStart-only, which agy
201
+ * hooks don't support, so they are intentionally absent. inject-rules is absent
202
+ * too (rules-once via the AGENTS.md bake).
203
+ */
204
+ const AGY_PLUGIN_HOOKS = [
205
+ {
206
+ sourceScript: "block-no-verify.sh",
207
+ hookName: "lisa-block-no-verify",
208
+ event: "PreToolUse",
209
+ matcher: "run_command",
210
+ agyScript: "block-no-verify.agy.sh",
211
+ },
212
+ ];
213
+
214
+ /**
215
+ * Whether the source manifest hook block references `scriptName` anywhere. Used
216
+ * to ship a hook only for the variant whose manifest carries it (the base
217
+ * plugin); stack variants have empty manifest hooks and emit no hooks.json.
218
+ * @param {Record<string, Array<{ hooks?: Array<{ command?: string }> }>>} sourceHooks
219
+ * @param {string} scriptName
220
+ * @returns {boolean}
221
+ */
222
+ function sourceReferencesScript(sourceHooks, scriptName) {
223
+ return Object.values(sourceHooks ?? {}).some(
224
+ entries =>
225
+ Array.isArray(entries) &&
226
+ entries.some(
227
+ e =>
228
+ Array.isArray(e?.hooks) &&
229
+ e.hooks.some(
230
+ h =>
231
+ typeof h?.command === "string" && h.command.includes(scriptName)
232
+ )
233
+ )
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Emit the plugin-bundled root `hooks.json` (agy schema) and copy the
239
+ * agy-protocol script(s) into the variant's `hooks/` subdir. No-op for variants
240
+ * whose source manifest carries none of the mapped hooks (e.g. stack variants).
241
+ * @param {string} srcDir Built Claude plugin directory (input).
242
+ * @param {string} outDir agy variant output directory.
243
+ * @param {Record<string, unknown>} sourceHooks Source manifest hook block.
244
+ * @param {string} installDirName Name agy installs the plugin under in
245
+ * `~/.gemini/config/plugins/<installDirName>/` (the manifest `name`); baked
246
+ * into the hook command path so it resolves to the installed script.
247
+ * @returns {void}
248
+ */
249
+ function emitAgyPluginHooks(srcDir, outDir, sourceHooks, installDirName) {
250
+ const applicable = AGY_PLUGIN_HOOKS.filter(h => {
251
+ if (!sourceReferencesScript(sourceHooks, h.sourceScript)) return false;
252
+ const scriptSource = path.join(srcDir, "hooks", h.agyScript);
253
+ if (!fs.existsSync(scriptSource)) {
254
+ throw new Error(
255
+ `Missing agy hook script for ${h.sourceScript}: ${scriptSource}`
256
+ );
257
+ }
258
+ return true;
259
+ });
260
+ if (applicable.length === 0) return;
261
+
262
+ const hooksConfig = Object.fromEntries(
263
+ applicable.map(h => [
264
+ h.hookName,
265
+ {
266
+ [h.event]: [
267
+ {
268
+ matcher: h.matcher,
269
+ hooks: [
270
+ {
271
+ type: "command",
272
+ command: `bash "$HOME/.gemini/config/plugins/${installDirName}/hooks/${h.agyScript}"`,
273
+ },
274
+ ],
275
+ },
276
+ ],
277
+ },
278
+ ])
279
+ );
280
+ fs.writeFileSync(
281
+ path.join(outDir, "hooks.json"),
282
+ JSON.stringify(hooksConfig, null, 2) + "\n"
283
+ );
284
+
285
+ // Copy the agy-protocol scripts into the variant's hooks/ subdir.
286
+ const hooksDir = path.join(outDir, "hooks");
287
+ fs.mkdirSync(hooksDir, { recursive: true });
288
+ for (const h of applicable) {
289
+ const scriptSource = path.join(srcDir, "hooks", h.agyScript);
290
+ const scriptDest = path.join(hooksDir, h.agyScript);
291
+ fs.copyFileSync(scriptSource, scriptDest);
292
+ fs.chmodSync(scriptDest, 0o755);
293
+ }
155
294
  }
156
295
 
157
296
  // CLI entrypoint.
@@ -7,17 +7,31 @@
7
7
  *
8
8
  * HOOKS: as of Codex 0.125.0 (verified via codex features list showing
9
9
  * codex_hooks as `stable`), the plugin manifest accepts a `hooks` field
10
- * pointing at a sibling `hooks.json`. `emitCodexHooks` below derives the
11
- * Codex-shape hooks block from the Claude manifest by applying the Wave 1
12
- * per-agent ship-list audit
10
+ * pointing at a `hooks.json`. Per the official docs + the structural analogy to
11
+ * the working `skills`/`mcpServers` pointers, that pointer resolves RELATIVE TO
12
+ * THE PLUGIN ROOT — but end-to-end firing is trust-gated (interactive `/hooks`,
13
+ * no headless bypass in 0.125.0), so this is verified by docs + structure, not
14
+ * by an automated run; the per-project installer (src/codex/hooks-installer.ts)
15
+ * remains the verified-working Codex delivery path. `emitCodexHooks` below
16
+ * derives the Codex-shape hooks block from the Claude manifest by applying the
17
+ * Wave 1 per-agent ship-list audit
13
18
  * (wiki/architecture/lisa-hook-per-agent-ship-list.md):
14
19
  * - Drop every `entire hooks claude-code *` command (Claude-only analytics).
15
20
  * - Drop every reference to `enforce-team-first.sh` (Claude-team-specific).
16
21
  * - Drop `inject-flow-context.sh` ONLY when targeting an agent without
17
22
  * SubagentStart (Codex 0.125.0 has SubagentStart, so we ship it).
18
- * - Rewrite ${CLAUDE_PLUGIN_ROOT}/hooks/<n>.sh to ./hooks/<n>.sh so the
19
- * hooks.json sibling can resolve the script path relative to itself.
20
- * - Copy the surviving scripts into .codex-plugin/hooks/.
23
+ * - Rewrite ${CLAUDE_PLUGIN_ROOT}/hooks/<n>.sh to ${PLUGIN_ROOT}/hooks/<n>.sh
24
+ * (the env var Codex exposes to hook commands), which still resolves to the
25
+ * shared scripts at <plugin-root>/hooks/*.sh.
26
+ *
27
+ * The derived hooks.json is written to <plugin-root>/.codex-plugin/hooks.json,
28
+ * NOT <plugin-root>/hooks/hooks.json. The latter is where Claude Code (and the
29
+ * cursor/copilot Claude-protocol variants) AUTO-DISCOVER plugin hooks, so a
30
+ * Codex-shaped file there gets run by Claude too — and because it uses
31
+ * ${PLUGIN_ROOT} (undefined in Claude), the path expands to an empty prefix
32
+ * (`/hooks/<n>.sh: No such file`). Keeping the Codex file under .codex-plugin/
33
+ * (which Claude never scans) is what lets Claude, Codex, cursor, copilot, and
34
+ * agy each load exactly their own hooks. See issue #1058.
21
35
  *
22
36
  * SessionEnd is documented as unsupported by Codex; the `entire hooks
23
37
  * claude-code session-end` hook is stripped per the Claude-only rule above
@@ -459,27 +473,33 @@ function main() {
459
473
 
460
474
  /**
461
475
  * Per Wave 1 audit + Codex 0.125.0 supporting plugin-bundled hooks: derive
462
- * a Codex-shaped hooks.json from the Claude manifest's hooks block and copy
463
- * the surviving scripts into .codex-plugin/hooks/.
476
+ * a Codex-shaped hooks.json from the Claude manifest's hooks block and write it
477
+ * to <plugin-root>/.codex-plugin/hooks.json (advertised via the manifest `hooks`
478
+ * pointer in componentPointers).
479
+ *
480
+ * The file deliberately does NOT go to <plugin-root>/hooks/hooks.json: Claude
481
+ * Code and the cursor/copilot Claude-protocol variants auto-discover hooks
482
+ * there, and the Codex file's ${PLUGIN_ROOT} (undefined in Claude) would expand
483
+ * to an empty prefix and fail at startup (issue #1058). The hook SCRIPTS stay
484
+ * at <plugin-root>/hooks/*.sh — shared, copied from plugins/src by the Claude
485
+ * build — and ${PLUGIN_ROOT}/hooks/<n>.sh resolves to them regardless of where
486
+ * hooks.json itself lives.
464
487
  *
465
488
  * No-op when the input has no hooks block or every entry is stripped.
466
489
  *
467
490
  * @param {string} pluginDir Built Claude plugin directory.
468
491
  * @param {object} claudeManifest Parsed contents of .claude-plugin/plugin.json.
469
492
  */
470
- function emitCodexHooks(pluginDir, claudeManifest) {
471
- // Codex auto-discovers a plugin's hooks at <plugin-root>/hooks/hooks.json and
472
- // resolves the manifest hooks pointer + ${PLUGIN_ROOT} relative to the plugin
473
- // root (developers.openai.com/codex/plugins/build). The hook scripts already
474
- // ship at <plugin-root>/hooks/ (copied from plugins/src by the Claude build),
475
- // so the Codex hooks.json lives alongside them and no script copy is needed.
476
- const hooksDir = path.join(pluginDir, "hooks");
477
- const hooksJsonPath = path.join(hooksDir, "hooks.json");
478
- // Clean up the pre-2.121 layout (hooks.json + copied scripts under
479
- // .codex-plugin/) so a rebuilt plugin never ships both.
480
- const legacyCodexPluginDir = path.join(pluginDir, ".codex-plugin");
481
- fs.rmSync(path.join(legacyCodexPluginDir, "hooks.json"), { force: true });
482
- fs.rmSync(path.join(legacyCodexPluginDir, "hooks"), {
493
+ export function emitCodexHooks(pluginDir, claudeManifest) {
494
+ const codexPluginDir = path.join(pluginDir, ".codex-plugin");
495
+ const hooksJsonPath = path.join(codexPluginDir, "hooks.json");
496
+ // Purge the older layouts so a rebuilt plugin never ships a stale/duplicate
497
+ // file:
498
+ // - <plugin-root>/hooks/hooks.json (2.121–2.124; broke Claude startup)
499
+ // - <plugin-root>/.codex-plugin/hooks/ (pre-2.121 copied-scripts dir)
500
+ // The shared .sh scripts at <plugin-root>/hooks/ are left untouched.
501
+ fs.rmSync(path.join(pluginDir, "hooks", "hooks.json"), { force: true });
502
+ fs.rmSync(path.join(codexPluginDir, "hooks"), {
483
503
  force: true,
484
504
  recursive: true,
485
505
  });
@@ -490,7 +510,7 @@ function emitCodexHooks(pluginDir, claudeManifest) {
490
510
  fs.rmSync(hooksJsonPath, { force: true });
491
511
  return;
492
512
  }
493
- fs.mkdirSync(hooksDir, { recursive: true });
513
+ fs.mkdirSync(codexPluginDir, { recursive: true });
494
514
  fs.writeFileSync(
495
515
  hooksJsonPath,
496
516
  `${JSON.stringify(buildCodexHooksDocument(filtered), null, 2)}\n`
@@ -541,7 +561,7 @@ function writeCodexManifest(pluginDir, claudeManifest, pluginName, version) {
541
561
  );
542
562
  }
543
563
 
544
- function componentPointers(pluginDir) {
564
+ export function componentPointers(pluginDir) {
545
565
  return {
546
566
  ...(fs.existsSync(path.join(pluginDir, "skills"))
547
567
  ? { skills: "./skills/" }
@@ -549,8 +569,8 @@ function componentPointers(pluginDir) {
549
569
  ...(fs.existsSync(path.join(pluginDir, ".mcp.json"))
550
570
  ? { mcpServers: "./.mcp.json" }
551
571
  : {}),
552
- ...(fs.existsSync(path.join(pluginDir, "hooks", "hooks.json"))
553
- ? { hooks: "./hooks/hooks.json" }
572
+ ...(fs.existsSync(path.join(pluginDir, ".codex-plugin", "hooks.json"))
573
+ ? { hooks: "./.codex-plugin/hooks.json" }
554
574
  : {}),
555
575
  };
556
576
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Per the Wave 1 audit, Copilot ships:
14
14
  * block-no-verify.sh, inject-rules.sh (conservative default — conditional on
15
- * the rules-auto-load probe), notify-ntfy.sh, install-pkgs.sh, setup-jira-cli.sh.
15
+ * the rules-auto-load probe), install-pkgs.sh, setup-jira-cli.sh.
16
16
  *
17
17
  * Per the Wave 2 pattern-b-fan-out-spec.md, this generator runs four pre-flight
18
18
  * probes when `copilot` is on PATH and caches the results. When `copilot` is
@@ -139,10 +139,11 @@ export function generateCopilotVariant(srcDir, outDir, version) {
139
139
  copyDir(srcDir, outDir, relPath => {
140
140
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin")
141
141
  return false;
142
- // Drop the Codex hooks manifest the base build emits at hooks/hooks.json —
143
- // Copilot reads its (camelCase) hooks from .claude-plugin/plugin.json, not
144
- // this Codex-shaped (PascalCase, ${PLUGIN_ROOT}) file. Keeping it would ship
145
- // a spurious, wrong-shaped hooks manifest in the Copilot variant.
142
+ // Defensively drop any hooks/hooks.json. The base build now emits the Codex
143
+ // hooks manifest under .codex-plugin/ (stripped above), not here (issue
144
+ // #1058), but guard against a regression reintroducing it: Copilot reads its
145
+ // (camelCase) hooks from .claude-plugin/plugin.json, not a Codex-shaped
146
+ // (PascalCase, ${PLUGIN_ROOT}) file.
146
147
  if (relPath === path.join("hooks", "hooks.json")) return false;
147
148
  // Drop Codex-specific per-skill openai.yaml artifacts — Copilot does not use them.
148
149
  if (/^skills\/[^/]+\/agents\/openai\.ya?ml$/.test(relPath)) return false;
@@ -105,9 +105,11 @@ export function generateCursorVariant(srcDir, outDir, version) {
105
105
  // Drop the `.codex-plugin/` directory — Cursor does not consume it.
106
106
  if (relPath.startsWith(".codex-plugin/") || relPath === ".codex-plugin")
107
107
  return false;
108
- // Drop the Codex hooks manifest the base build emits at hooks/hooks.json —
109
- // Cursor reads its (filtered) hooks from .claude-plugin/plugin.json, not this
110
- // Codex-shaped file. The surviving .sh scripts in hooks/ are kept below.
108
+ // Defensively drop any hooks/hooks.json. The base build now emits the Codex
109
+ // hooks manifest under .codex-plugin/ (stripped above), not here (issue
110
+ // #1058), but guard against a regression reintroducing it: Cursor reads its
111
+ // (filtered) hooks from .claude-plugin/plugin.json, not a Codex-shaped file.
112
+ // The surviving .sh scripts in hooks/ are kept below.
111
113
  if (relPath === path.join("hooks", "hooks.json")) return false;
112
114
  // Drop Codex-specific per-skill openai.yaml artifacts — Cursor does not use them.
113
115
  // These live at skills/<n>/agents/openai.yaml and are generated by the Codex
@@ -15,7 +15,15 @@
15
15
  * generate-codex-plugin-artifacts.mjs path)
16
16
  * - Cursor: strip inject-rules.sh (Cursor auto-loads rules/ natively), strip
17
17
  * Claude-team-specific scripts and `entire hooks claude-code *` calls
18
- * - agy: ship NOTHING (agy plugin hooks do not fire in -p mode per verified-by-run)
18
+ * - agy: this filter is NOT consumed for agy. agy hooks ship as a
19
+ * plugin-bundled ROOT hooks.json emitted by
20
+ * generate-agy-plugin-artifacts.mjs (its own AGY_PLUGIN_HOOKS map is the
21
+ * source of truth), and only `block-no-verify` (PreToolUse) is portable —
22
+ * agy doesn't support SessionStart, so install-pkgs / setup-jira-cli can't
23
+ * ship as agy hooks. The agy column below is retained only as conceptual
24
+ * ship-list documentation (block-no-verify.sh, install-pkgs.sh,
25
+ * setup-jira-cli.sh; strips inject-rules.sh — rules-once via AGENTS.md bake
26
+ * — enforce-team-first.sh, inject-flow-context.sh, and `entire ...` calls).
19
27
  * - Copilot: strip SubagentStart hooks (event missing), strip Claude-team-specific
20
28
  * scripts, conditionally strip inject-rules.sh if the rules-auto-load probe is
21
29
  * positive (caller passes copilotRulesAutoLoads via options)
@@ -29,7 +37,7 @@ const SCRIPT_RULES = {
29
37
  claude: true,
30
38
  codex: true,
31
39
  cursor: true,
32
- agy: false,
40
+ agy: true,
33
41
  copilot: true,
34
42
  },
35
43
  "enforce-team-first.sh": {
@@ -43,35 +51,28 @@ const SCRIPT_RULES = {
43
51
  claude: true,
44
52
  codex: true,
45
53
  cursor: false, // collision: Cursor auto-loads rules/ natively
46
- agy: false, // hooks don't fire in -p
54
+ agy: false, // rules delivered via AGENTS.md bake, not a hook (rules-once invariant)
47
55
  copilot: true, // conservative default; conditionally stripped if rules-auto-load probe positive
48
56
  },
49
57
  "inject-flow-context.sh": {
50
58
  claude: true,
51
59
  codex: true,
52
60
  cursor: false, // SubagentStart unverified on Cursor; conservative default
53
- agy: false,
61
+ agy: false, // SubagentStart-only; not in agy's universal ship-list
54
62
  copilot: false, // Copilot lacks SubagentStart event
55
63
  },
56
64
  "install-pkgs.sh": {
57
65
  claude: true,
58
66
  codex: true,
59
67
  cursor: true,
60
- agy: false,
61
- copilot: true,
62
- },
63
- "notify-ntfy.sh": {
64
- claude: true,
65
- codex: true,
66
- cursor: true,
67
- agy: false,
68
+ agy: true,
68
69
  copilot: true,
69
70
  },
70
71
  "setup-jira-cli.sh": {
71
72
  claude: true,
72
73
  codex: true,
73
74
  cursor: true,
74
- agy: false,
75
+ agy: true,
75
76
  copilot: true,
76
77
  },
77
78
  // Unregistered scripts — exclude by default until classified.
@@ -92,7 +93,11 @@ const SCRIPT_RULES = {
92
93
  };
93
94
 
94
95
  /** Universal exclude pattern: development helpers. */
95
- const SCRIPT_EXCLUDE_PATTERNS = [/debug/i];
96
+ // `.agy.sh` scripts are agy-protocol variants emitted into the agy plugin
97
+ // artifact by generate-agy-plugin-artifacts.mjs (not via this filter); exclude
98
+ // them from every other agent's ship-list so they never leak into cursor /
99
+ // copilot / codex variants.
100
+ const SCRIPT_EXCLUDE_PATTERNS = [/debug/i, /\.agy\.sh$/];
96
101
 
97
102
  /** Hook command shape: { type: "command", command: "..." } */
98
103
  const isEntireClaudeCodeCommand = cmd =>
@@ -178,6 +183,10 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
178
183
  // Cursor collision rule for rules + Copilot conditional rules strip
179
184
  if (scriptName === "inject-rules.sh") {
180
185
  if (agent === "cursor") return false;
186
+ // Belt-and-suspenders rules-once guard: agy gets rules via the AGENTS.md
187
+ // bake, not a hook (rules-once invariant). The SCRIPT_RULES table already
188
+ // sets agy:false, but keep this explicit so the invariant survives a
189
+ // future table edit.
181
190
  if (agent === "agy") return false;
182
191
  if (agent === "copilot" && opts.copilotRulesAutoLoads === true)
183
192
  return false;
@@ -195,8 +204,12 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
195
204
  * Returns the new hook block (or undefined when the block ends up empty after
196
205
  * filtering, which means the manifest should omit the hooks field entirely).
197
206
  *
198
- * For agy this function returns undefined regardless of input because agy
199
- * variants ship no hooks.
207
+ * This function is invoked only for cursor/copilot. The "agy" branch still
208
+ * works (3 universal scripts survive, PascalCase events) and is exercised by
209
+ * unit tests as conceptual ship-list documentation, but agy hooks are NOT
210
+ * emitted through this path — they ship as a plugin-bundled root hooks.json
211
+ * built by generate-agy-plugin-artifacts.mjs (only block-no-verify is portable;
212
+ * agy lacks SessionStart).
200
213
  *
201
214
  * @param {Record<string, Array<{ matcher?: string, hooks: Array<object> }>>} hookBlock
202
215
  * The Claude-format hook block from .claude-plugin/plugin.json.
@@ -205,7 +218,6 @@ export function shouldShipHook(hook, _eventName, agent, opts = {}) {
205
218
  * @returns {Record<string, Array<{ matcher?: string, hooks: Array<object> }>> | undefined}
206
219
  */
207
220
  export function filterHooksForAgent(hookBlock, agent, opts = {}) {
208
- if (agent === "agy") return undefined;
209
221
  if (!hookBlock || typeof hookBlock !== "object") return undefined;
210
222
 
211
223
  /** @type {Record<string, Array<{ matcher?: string, hooks: Array<object> }>>} */
@@ -244,6 +256,5 @@ export function filterHooksForAgent(hookBlock, agent, opts = {}) {
244
256
  * @returns {string[]}
245
257
  */
246
258
  export function filterScriptsForAgent(scriptFilenames, agent) {
247
- if (agent === "agy") return [];
248
259
  return scriptFilenames.filter(name => shouldShipScript(name, agent));
249
260
  }
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Lisa-managed Codex hook script (Stop event).
3
- # Sends a desktop/push notification when a Codex session completes.
4
- # No-op if NTFY_TOPIC is not set or curl is unavailable.
5
- set -euo pipefail
6
-
7
- [ -n "${NTFY_TOPIC:-}" ] || exit 0
8
- command -v curl >/dev/null 2>&1 || exit 0
9
-
10
- TITLE="${NTFY_TITLE:-Codex session complete}"
11
- MESSAGE="${NTFY_MESSAGE:-Your Codex session has finished. Check the terminal for results.}"
12
-
13
- curl -s -m 5 \
14
- -H "Title: ${TITLE}" \
15
- -H "Priority: default" \
16
- -d "${MESSAGE}" \
17
- "https://ntfy.sh/${NTFY_TOPIC}" \
18
- >/dev/null 2>&1 || true