@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.
- package/dist/agy/mcp-installer.d.ts +22 -5
- package/dist/agy/mcp-installer.d.ts.map +1 -1
- package/dist/agy/mcp-installer.js +52 -12
- package/dist/agy/mcp-installer.js.map +1 -1
- package/dist/codex/hooks-installer.d.ts.map +1 -1
- package/dist/codex/hooks-installer.js +12 -11
- package/dist/codex/hooks-installer.js.map +1 -1
- package/dist/core/lisa.d.ts +19 -11
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +44 -12
- package/dist/core/lisa.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -10
- package/plugins/lisa/{hooks → .codex-plugin}/hooks.json +0 -11
- package/plugins/lisa/.codex-plugin/plugin.json +2 -2
- package/plugins/lisa/hooks/block-no-verify.agy.sh +45 -0
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +45 -0
- package/plugins/lisa-agy/hooks.json +15 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -12
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -12
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +2 -2
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +2 -2
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +2 -2
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +2 -2
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/.claude-plugin/plugin.json +0 -1
- package/plugins/src/base/hooks/block-no-verify.agy.sh +45 -0
- package/scripts/generate-agy-plugin-artifacts.mjs +158 -19
- package/scripts/generate-codex-plugin-artifacts.mjs +45 -25
- package/scripts/generate-copilot-plugin-artifacts.mjs +6 -5
- package/scripts/generate-cursor-plugin-artifacts.mjs +5 -3
- package/scripts/lib/per-agent-hook-filter.mjs +29 -18
- package/dist/codex/scripts/notify-ntfy.sh +0 -18
- package/plugins/lisa/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-copilot/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-cursor/hooks/notify-ntfy.sh +0 -183
- package/plugins/lisa-expo-agy/.mcp.json +0 -8
- package/plugins/src/base/hooks/notify-ntfy.sh +0 -183
- /package/plugins/lisa-harper-fabric/{hooks → .codex-plugin}/hooks.json +0 -0
- /package/plugins/lisa-nestjs/{hooks → .codex-plugin}/hooks.json +0 -0
- /package/plugins/lisa-rails/{hooks → .codex-plugin}/hooks.json +0 -0
- /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`).
|
|
8
|
-
* plugin-bundled hooks
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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/,
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
463
|
-
*
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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(
|
|
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, "
|
|
553
|
-
? { hooks: "
|
|
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),
|
|
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
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
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
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
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:
|
|
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:
|
|
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, //
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
199
|
-
*
|
|
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
|