@goodfoot/agent-hooks 1.0.7 → 1.0.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @goodfoot/agent-hooks npm package changelog
2
2
 
3
+ ## 1.0.9
4
+ - Added support for Antigravity hooks, including event handling, output formatting, and transport
5
+ - Added scaffolding for Antigravity hook projects via the CLI
6
+ - Documented the Antigravity hook contract in the README
7
+
8
+ ## 1.0.9
9
+
10
+ Adds full Antigravity support to the public package: typed hook factories and outputs, transport behavior, the `@goodfoot/agent-hooks/antigravity` export, CLI compilation, and a scaffold that emits an installable Antigravity plugin manifest. The implementation is covered by unit, conformance, export-surface, scaffold, and live host smoke tests.
11
+
12
+ ## 1.0.8
13
+
14
+ Maintenance release accompanying a repository version-bump hook fix: a version surface is now exempt only for its version, not for its whole contents, so an edit to a manifest or a version-bearing source file can no longer ship unversioned. No changes to the hook API, the compiler, or the CLI.
15
+
3
16
  ## 1.0.7
4
17
 
5
18
  Corrects the README's OpenCode install rationale. The plugin packages now declare a `main` entrypoint, so `opencode plugin` no longer rejects them with `No plugin targets found`; the reason a checkout is still required is that `opencode plugin` never writes `skills.paths`, so installing the plugin does not deliver the skill. Also moves a sentence about compiling hooks for OpenCode out of the version-checking paragraph it had attached itself to. No changes to the hook API, the compiler, or the CLI.
package/README.md CHANGED
@@ -6,17 +6,15 @@
6
6
  `@goodfoot/codex-hooks`: a shared agent-neutral core (hook factory primitive, transport
7
7
  driver, `Logger`, env/stdin utilities) with a per-agent entry point on top. Write
8
8
  TypeScript hooks, compile them with the bundled CLI, and run the resulting
9
- self-contained executables under Claude Code or Codex.
9
+ self-contained executables under Claude Code, Codex, OpenCode, or Antigravity.
10
10
 
11
- **Status: `1.0.0`.** Three entry points ship and are published: `@goodfoot/agent-hooks/claude-code`
11
+ **Status: `1.0.8`.** Four host entry points ship: `@goodfoot/agent-hooks/claude-code`
12
12
  (full parity with `@goodfoot/claude-code-hooks`), `@goodfoot/agent-hooks/codex` (full
13
- parity with `@goodfoot/codex-hooks`), and `@goodfoot/agent-hooks/opencode` (native support
14
- for OpenCode's in-process plugin model — see [OpenCode](#opencode) below). Antigravity CLI
15
- support exists internally as typed factories, output builders, and a conformance matrix,
16
- but is **not** part of this release's public surface — there is no
17
- `@goodfoot/agent-hooks/antigravity` export yet, and `--agent antigravity` is rejected by
18
- the CLI. It ships once a non-interactive, authenticated Antigravity CLI exists to
19
- validate against.
13
+ parity with `@goodfoot/codex-hooks`), `@goodfoot/agent-hooks/opencode` (native support
14
+ for OpenCode's in-process plugin model — see [OpenCode](#opencode) below), and
15
+ `@goodfoot/agent-hooks/antigravity` (five typed events, host-native output builders,
16
+ manifest generation, scaffolding, and an exit-0 payload transport). The Antigravity
17
+ surface is pinned to and behaviorally verified against `agy` 1.1.22.
20
18
 
21
19
  ## Install
22
20
 
@@ -35,9 +33,10 @@ yarn add @goodfoot/agent-hooks
35
33
  ### The plugin, for development
36
34
 
37
35
  All four hosts install from
38
- [`goodfoot-io/marketplace`](https://github.com/goodfoot-io/marketplace). Claude Code,
39
- Codex, and Antigravity resolve the repository directly. Only OpenCode needs a checkout,
40
- because it loads skills from a config path rather than from a plugin.
36
+ [`goodfoot-io/marketplace`](https://github.com/goodfoot-io/marketplace). Claude Code and
37
+ Codex add the repository as a marketplace and install by name. Antigravity installs one
38
+ plugin root at a time. OpenCode needs a checkout, because these plugin packages are not
39
+ published to npm.
41
40
 
42
41
  Note that the plugin and the npm package version independently — the plugin version you
43
42
  install will not match the package version above.
@@ -50,6 +49,8 @@ claude plugin install agent-hooks@goodfoot
50
49
  ```
51
50
 
52
51
  Verify with `claude plugin list` — `agent-hooks@goodfoot` should be listed as enabled.
52
+ `claude plugin details agent-hooks` reports the component inventory, confirming the skill
53
+ itself arrived rather than just the plugin.
53
54
 
54
55
  #### Codex
55
56
 
@@ -59,48 +60,71 @@ codex plugin add agent-hooks@goodfoot
59
60
  ```
60
61
 
61
62
  The marketplace source must be a repository, git URL, or directory; passing the manifest
62
- path itself is rejected. `codex plugin add` requires the `<plugin>@<marketplace>` form.
63
- Verify with `codex plugin list --json`; the skill tree lands under
63
+ path itself is rejected. A bare plugin name is rejected even when exactly one marketplace
64
+ is configured — qualify it as `agent-hooks@goodfoot` or pass `--marketplace goodfoot`. If
65
+ you set `CODEX_HOME`, create that directory first; Codex will not create it and fails with
66
+ `failed to resolve CODEX_HOME`.
67
+
68
+ Verify with `codex plugin list`, which lists every marketplace plugin with a `STATUS`
69
+ column. Do not use `codex plugin list --json` for this: on codex-cli 0.150.1 its
70
+ `available` array is empty even when the plain listing shows all seven plugins, so a
71
+ correctly installed plugin can look missing. The skill tree lands under
64
72
  `$CODEX_HOME/plugins/cache/goodfoot/agent-hooks/<version>/skills`.
65
73
 
66
74
  #### Antigravity
67
75
 
68
- `agy` has no marketplace command. It installs a plugin root directly, from a git URL
69
- with the in-repo path appended:
76
+ `agy` installs one plugin root at a time, from a git URL with the in-repo path appended:
70
77
 
71
78
  ```bash
72
79
  agy plugin install https://github.com/goodfoot-io/marketplace.git/plugins-antigravity/agent-hooks
73
80
  ```
74
81
 
75
- The `#subdir` fragment form other tools accept fails here with `could not detect plugin
76
- structure`. A local path works the same way: `agy plugin install
77
- ./plugins-antigravity/agent-hooks`.
82
+ There is no marketplace form that installs several at once: pointing `agy plugin install`
83
+ at the repository root is rejected with `could not detect plugin structure`, as is the
84
+ `#subdir` fragment form other tools accept. A local path works the same way: `agy plugin
85
+ install ./plugins-antigravity/agent-hooks`.
86
+
87
+ To add the sibling plugins, repeat the command for each root you want —
88
+ `plugins-antigravity/` carries `agent-hooks`, `agent-skills`, `claude-code-skill-reader`,
89
+ `gmail`, `goodfoot`, `jsdoczoom`, and `linear`.
78
90
 
79
91
  A successful install reports `skills : 1 processed`; `agents`, `commands`, `mcpServers`,
80
92
  and `hooks` report `skipped (not found)`, expected for a skills-only plugin. Files land
81
- in `~/.gemini/config/plugins/agent-hooks/`. Verify with `agy plugin list`.
93
+ in `~/.gemini/config/plugins/agent-hooks/`. Verify with `agy plugin list`, which reports
94
+ `"components": ["skills"]` for the installed plugin.
82
95
 
83
- This installs the authoring skill only. It is unrelated to `--agent antigravity`, which
84
- the compiler CLI still rejects (see the status note above).
96
+ This marketplace plugin installs the authoring skill only. Hook projects use the npm
97
+ package separately: import `@goodfoot/agent-hooks/antigravity` and compile with
98
+ `agent-hooks --agent antigravity`. The compiler writes the host-native `hooks.json` at
99
+ the plugin root plus its executable bundles under `bin/`; install that resulting plugin
100
+ root with `agy plugin install <dir>`. Scaffolding an Antigravity hook project also emits
101
+ the root `plugin.json` required by that install command.
85
102
 
86
103
  #### OpenCode
87
104
 
88
- OpenCode plugins are hook-transport modules and cannot contribute skills — skills load
89
- only from `skills.paths`, and `opencode plugin` never writes that key. Installing the
90
- plugin is not a way to get the skill. Clone the repository and register the skill
91
- directory in `opencode.json`:
105
+ These plugin packages are not published to npm, so `opencode plugin` takes a path into a
106
+ checkout rather than a module name:
92
107
 
93
108
  ```bash
94
109
  git clone https://github.com/goodfoot-io/marketplace.git
110
+ opencode plugin "$PWD/marketplace/plugins-opencode/agent-hooks"
95
111
  ```
96
112
 
97
- ```json
98
- {
99
- "skills": {
100
- "paths": ["./marketplace/plugins-opencode/agent-hooks/skills"]
101
- }
102
- }
103
- ```
113
+ Pass a path, not a bare name. `opencode plugin` resolves a specifier containing a
114
+ separator as a path — absolute, `./`-prefixed, and plain relative forms all load on
115
+ OpenCode 1.18.23. A single segment with no separator (`agent-hooks`) is instead resolved as an
116
+ npm package name and silently fails to load, with nothing logged. The absolute form
117
+ above avoids the ambiguity.
118
+
119
+ That is the whole install — no `opencode.json` editing. `opencode plugin` writes only the
120
+ top-level `plugin` key, and OpenCode keeps `plugin` and `skills` as disjoint config keys,
121
+ so registering the module would not by itself surface its skills. The plugin closes that
122
+ gap from the inside: its `config` hook runs against the resolved config before use and
123
+ appends its own bundled `skills/` directory to `skills.paths`, resolved relative to
124
+ wherever the package was installed. Installing is enough.
125
+
126
+ Without flags this installs into the current project's `.opencode/opencode.json`. Add
127
+ `-g` to install into the global config instead.
104
128
 
105
129
  Verify with `opencode debug skill`. Its output is large and embeds whole skill bodies:
106
130
  redirect it to a file and search there. Piping it to `head` truncates the JSON mid-string
@@ -119,7 +143,8 @@ Re-running the install command does not move the version on every host.
119
143
  - **Codex** — `codex plugin marketplace upgrade goodfoot` moves the installed version;
120
144
  there is no separate plugin-update command.
121
145
  - **Antigravity** — re-run `agy plugin install`.
122
- - **OpenCode** — pull the checkout.
146
+ - **OpenCode** — pull the checkout; the plugin resolves its skills from that directory, so
147
+ no reinstall is needed. Re-run with `-f` to replace a pinned plugin version.
123
148
 
124
149
  Check the installed version rather than the exit code. Several hosts report success while
125
150
  leaving a stale version in place, and a newer directory in a host's cache proves only that
@@ -251,6 +276,12 @@ export default defineOpenCodePlugin({
251
276
  `observe`/`observeResumed` record first-seen-event distinctly and `isRoot`/`isResumed`
252
277
  answer from that record.
253
278
 
279
+ `Hooks` also includes `config?: (input: Config) => Promise<void>`, which runs against the
280
+ resolved config before use and can mutate it. That is how a plugin contributes
281
+ configuration it would otherwise need the user to hand-write — the plugins in this
282
+ repository use it to append their own `skills/` directory to `skills.paths`, so installing
283
+ one delivers its skills without any `opencode.json` edit.
284
+
254
285
  Compile with the same unified CLI, but `--agent opencode` treats `-o`/`--output` as a
255
286
  plugin-artifact **directory**, not a manifest path — OpenCode config references built
256
287
  files directly, so there is no `hooks.json`-equivalent manifest or shebang wrapper:
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Antigravity-specific CLI internals: source analysis, bundle compilation
3
+ * linking the Antigravity runtime module, and `hooks.json` generation in the
4
+ * host's own named-hook shape.
5
+ *
6
+ * The manifest shape here has no counterpart in the other agents. Claude Code
7
+ * and Codex both emit `{ "hooks": { "<Event>": [...] } }`; Antigravity's
8
+ * top-level keys are **hook names**, each mapping to its own event table, and
9
+ * only two of the five events wrap their handlers in a `{ matcher, hooks }`
10
+ * group. The host also sets each handler's working directory to the directory
11
+ * containing `hooks.json`, so commands are emitted relative to it and need no
12
+ * `${PLUGIN_ROOT}` substitution or git-toplevel escape. See `CONTRACT.md` in
13
+ * this directory for the pinned reference all of that is transcribed from.
14
+ *
15
+ * Driver-level primitives (loader parsing, glob discovery, content hashing,
16
+ * symlink re-rooting, stale-bundle pruning) are imported from the shared CLI
17
+ * module — never re-derived here.
18
+ * @module
19
+ */
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import * as esbuild from "esbuild";
24
+ import ts from "typescript";
25
+ import { buildEsbuildLoaderMap, discoverHookSourceFiles, parseEsbuildLoaderFlag, pruneStaleHashedBundles, sha256Prefix8, symlinkVisiblePath, } from "../../cli.js";
26
+ import { DEFAULT_TIMEOUT_SECONDS, GROUPED_EVENTS, HOOK_FACTORY_TO_EVENT } from "./constants.js";
27
+ import { HOOK_EVENT_NAMES } from "./events.js";
28
+ /** The matcher emitted when a grouped event's config names none. `"*"` matches every tool. */
29
+ const MATCH_ALL = "*";
30
+ /**
31
+ * Recovers the bound event, matcher, and timeout from a hook module's default
32
+ * export. Returns `undefined` when the default export is not a call to a
33
+ * known Antigravity factory, which is how non-hook files in the input glob
34
+ * are skipped.
35
+ */
36
+ export function analyzeAntigravityHookFile(sourcePath) {
37
+ const sourceFile = ts.createSourceFile(sourcePath, fs.readFileSync(sourcePath, "utf-8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
38
+ const unwrap = (node) => {
39
+ if (ts.isParenthesizedExpression(node) || ts.isAwaitExpression(node)) {
40
+ return unwrap(node.expression);
41
+ }
42
+ return ts.isCallExpression(node) ? node : undefined;
43
+ };
44
+ const factoryOf = (call) => {
45
+ if (ts.isIdentifier(call.expression)) {
46
+ return call.expression.text;
47
+ }
48
+ return ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : undefined;
49
+ };
50
+ const literalString = (node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
51
+ let found;
52
+ const inspect = (node) => {
53
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
54
+ const call = unwrap(node.expression);
55
+ const factory = call === undefined ? undefined : factoryOf(call);
56
+ const hookEventName = factory === undefined ? undefined : HOOK_FACTORY_TO_EVENT[factory];
57
+ if (call !== undefined && hookEventName !== undefined) {
58
+ const result = { hookEventName };
59
+ const [config] = call.arguments;
60
+ if (config !== undefined && ts.isObjectLiteralExpression(config)) {
61
+ for (const property of config.properties) {
62
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
63
+ continue;
64
+ }
65
+ if (property.name.text === "matcher") {
66
+ result.matcher = literalString(property.initializer);
67
+ }
68
+ if (property.name.text === "timeout" && ts.isNumericLiteral(property.initializer)) {
69
+ result.timeout = Number(property.initializer.text);
70
+ }
71
+ }
72
+ }
73
+ found = result;
74
+ }
75
+ }
76
+ ts.forEachChild(node, inspect);
77
+ };
78
+ inspect(sourceFile);
79
+ return found;
80
+ }
81
+ /**
82
+ * Bundles one hook source into a self-contained ESM executable whose entry
83
+ * wrapper calls this agent's `execute`.
84
+ */
85
+ export async function compileAntigravityHook(sourcePath, loaders, sourcemap = true) {
86
+ const resolveDir = path.dirname(sourcePath);
87
+ const runtimeAbsolute = symlinkVisiblePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "./transport.js"), resolveDir);
88
+ const runtimeRelative = path.relative(resolveDir, runtimeAbsolute).replace(/\\/g, "/");
89
+ const runtimeSpecifier = runtimeRelative.startsWith(".") ? runtimeRelative : `./${runtimeRelative}`;
90
+ const entry = [
91
+ `import hook from ${JSON.stringify(`./${path.basename(sourcePath)}`)};`,
92
+ `import { execute } from ${JSON.stringify(runtimeSpecifier)};`,
93
+ `execute(hook);`,
94
+ ``,
95
+ ].join("\n");
96
+ const built = await esbuild.build({
97
+ stdin: {
98
+ contents: entry,
99
+ resolveDir,
100
+ sourcefile: `${path.basename(sourcePath, path.extname(sourcePath))}-entry.ts`,
101
+ loader: "ts",
102
+ },
103
+ loader: loaders,
104
+ format: "esm",
105
+ platform: "node",
106
+ target: "node20",
107
+ bundle: true,
108
+ sourcemap: sourcemap ? "inline" : false,
109
+ write: false,
110
+ external: ["node:*", "fs", "path", "os", "crypto", "module", "url"],
111
+ mainFields: ["module", "main"],
112
+ conditions: ["import", "node"],
113
+ preserveSymlinks: true,
114
+ });
115
+ const content = built.outputFiles?.[0]?.text;
116
+ if (content === undefined) {
117
+ throw new Error(`esbuild produced no output for ${sourcePath}`);
118
+ }
119
+ return { content, contentHash: sha256Prefix8(content) };
120
+ }
121
+ /**
122
+ * Converts a source file's basename to the manifest's top-level hook name.
123
+ * Names must be unique across the whole manifest, since the host merges
124
+ * same-named entries; a collision is suffixed rather than silently dropped.
125
+ */
126
+ function assignHookName(sourcePath, taken) {
127
+ const base = path.basename(sourcePath, path.extname(sourcePath));
128
+ if (!taken.has(base)) {
129
+ taken.add(base);
130
+ return base;
131
+ }
132
+ let suffix = 2;
133
+ while (taken.has(`${base}-${suffix}`)) {
134
+ suffix++;
135
+ }
136
+ const unique = `${base}-${suffix}`;
137
+ taken.add(unique);
138
+ return unique;
139
+ }
140
+ async function compileAll(hookFiles, binDir, loaders, options) {
141
+ fs.mkdirSync(binDir, { recursive: true });
142
+ const compiled = [];
143
+ const taken = new Set();
144
+ const written = new Set();
145
+ for (const sourcePath of hookFiles) {
146
+ const metadata = analyzeAntigravityHookFile(sourcePath);
147
+ if (metadata === undefined) {
148
+ continue;
149
+ }
150
+ const { content, contentHash } = await compileAntigravityHook(sourcePath, loaders, options.sourcemap);
151
+ const hookName = assignHookName(sourcePath, taken);
152
+ const filename = options.stableNames ? `${hookName}.mjs` : `${hookName}.${contentHash}.mjs`;
153
+ const outputPath = path.join(binDir, filename);
154
+ fs.writeFileSync(outputPath, `#!/usr/bin/env -S node --enable-source-maps\n${content}`, {
155
+ encoding: "utf-8",
156
+ mode: 0o755,
157
+ });
158
+ written.add(filename);
159
+ compiled.push({ sourcePath, outputPath, hookName, metadata });
160
+ }
161
+ pruneStaleHashedBundles(binDir, written);
162
+ return compiled;
163
+ }
164
+ /**
165
+ * Renders the command string for one bundle. The host runs each handler
166
+ * through `sh -c` with the working directory set to the directory holding
167
+ * `hooks.json`, so a manifest-relative path is portable across every install
168
+ * location without substitution.
169
+ */
170
+ function commandFor(manifestDir, outputPath, executable) {
171
+ const relative = path.relative(manifestDir, outputPath).replace(/\\/g, "/");
172
+ const specifier = relative.startsWith(".") ? relative : `./${relative}`;
173
+ return `${executable} ${JSON.stringify(specifier)}`;
174
+ }
175
+ function timeoutSeconds(timeoutMs) {
176
+ if (timeoutMs === undefined) {
177
+ return DEFAULT_TIMEOUT_SECONDS;
178
+ }
179
+ return Math.max(1, Math.ceil(timeoutMs / 1000));
180
+ }
181
+ /**
182
+ * Builds the manifest. Each compiled hook becomes one top-level named entry
183
+ * holding exactly one event, which is what the host merges across plugins.
184
+ */
185
+ export function generateAntigravityHooksJson(compiled, manifestPath, executable = "node") {
186
+ const manifestDir = path.dirname(manifestPath);
187
+ const manifest = {};
188
+ for (const entry of compiled) {
189
+ const handler = {
190
+ type: "command",
191
+ command: commandFor(manifestDir, entry.outputPath, executable),
192
+ timeout: timeoutSeconds(entry.metadata.timeout),
193
+ };
194
+ const event = entry.metadata.hookEventName;
195
+ const named = {};
196
+ if (GROUPED_EVENTS.has(event)) {
197
+ const group = { matcher: entry.metadata.matcher ?? MATCH_ALL, hooks: [handler] };
198
+ if (event === "PreToolUse") {
199
+ named.PreToolUse = [group];
200
+ }
201
+ else {
202
+ named.PostToolUse = [group];
203
+ }
204
+ }
205
+ else if (event === "PreInvocation") {
206
+ named.PreInvocation = [handler];
207
+ }
208
+ else if (event === "PostInvocation") {
209
+ named.PostInvocation = [handler];
210
+ }
211
+ else {
212
+ named.Stop = [handler];
213
+ }
214
+ manifest[entry.hookName] = named;
215
+ }
216
+ return manifest;
217
+ }
218
+ /** Validates the Antigravity build arguments beyond the shared `--agent` gate. */
219
+ export function validateAntigravityArgs(args) {
220
+ if (args.scaffold !== undefined && args.scaffold !== "") {
221
+ if (args.output === "") {
222
+ return "Scaffold mode requires -o/--output";
223
+ }
224
+ if (args.hooks === undefined || args.hooks === "") {
225
+ return "Scaffold mode requires --hooks";
226
+ }
227
+ const unknown = args.hooks
228
+ .split(",")
229
+ .map((value) => value.trim())
230
+ .filter((value) => value.length > 0)
231
+ .filter((value) => !HOOK_EVENT_NAMES.some((name) => name.toLowerCase() === value.toLowerCase()));
232
+ if (unknown.length > 0) {
233
+ return `Unknown Antigravity hook name(s): ${unknown.join(", ")}. Valid names: ${HOOK_EVENT_NAMES.join(", ")}`;
234
+ }
235
+ return undefined;
236
+ }
237
+ if (args.input === "") {
238
+ return "Missing required argument: -i/--input";
239
+ }
240
+ if (args.output === "") {
241
+ return "Missing required argument: -o/--output";
242
+ }
243
+ for (const flag of args.loaderFlags) {
244
+ if (parseEsbuildLoaderFlag(flag) === undefined) {
245
+ return `Invalid --loader value: ${flag}. Expected .ext=type`;
246
+ }
247
+ }
248
+ return undefined;
249
+ }
250
+ /**
251
+ * Runs the Antigravity build or scaffold branch. Bundle filenames are stable
252
+ * by default: a manifest committed into a plugin tree must not churn on every
253
+ * rebuild, and the host reads the command string rather than globbing the
254
+ * directory.
255
+ */
256
+ export async function runAntigravityCli(args) {
257
+ if (args.scaffold !== undefined && args.scaffold !== "") {
258
+ const { scaffoldProject } = await import("./scaffold.js");
259
+ scaffoldProject({
260
+ directory: path.resolve(process.cwd(), args.scaffold),
261
+ hooks: (args.hooks ?? "")
262
+ .split(",")
263
+ .map((value) => value.trim())
264
+ .filter((value) => value.length > 0),
265
+ outputPath: args.output,
266
+ });
267
+ return 0;
268
+ }
269
+ const cwd = process.cwd();
270
+ const manifestPath = path.resolve(cwd, args.output);
271
+ const binDir = path.join(path.dirname(manifestPath), "bin");
272
+ const hookFiles = await discoverHookSourceFiles(args.input, cwd);
273
+ const loaders = buildEsbuildLoaderMap(args.loaderFlags);
274
+ const compiled = await compileAll(hookFiles, binDir, loaders, {
275
+ stableNames: args.stableNames ?? true,
276
+ sourcemap: args.sourcemap ?? true,
277
+ });
278
+ const manifest = generateAntigravityHooksJson(compiled, manifestPath, args.executable ?? "node");
279
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
280
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
281
+ return compiled.length;
282
+ }
@@ -0,0 +1,20 @@
1
+ export const PACKAGE_NAME = "@goodfoot/agent-hooks";
2
+ /** The host's own default handler timeout, in seconds (`CONTRACT.md`, Hook Handler Fields). */
3
+ export const DEFAULT_TIMEOUT_SECONDS = 30;
4
+ export const DEFAULT_ESBUILD_LOADERS = {
5
+ ".md": "text",
6
+ };
7
+ /** Maps each exported factory name to the event it binds. */
8
+ export const HOOK_FACTORY_TO_EVENT = {
9
+ preToolUseHook: "PreToolUse",
10
+ postToolUseHook: "PostToolUse",
11
+ preInvocationHook: "PreInvocation",
12
+ postInvocationHook: "PostInvocation",
13
+ stopHook: "Stop",
14
+ };
15
+ /**
16
+ * Events whose `hooks.json` entry is a grouped `{ matcher, hooks }` wrapper.
17
+ * The remaining three are flat lists of handler objects; the host ignores a
18
+ * matcher on them.
19
+ */
20
+ export const GROUPED_EVENTS = new Set(["PreToolUse", "PostToolUse"]);
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Antigravity hook event names and the (currently empty) advisory allow-list.
2
+ * Antigravity hook event names, the grouped/flat structural split, and the
3
+ * (empty) advisory allow-list.
3
4
  *
4
- * Unlike Claude Code's and Codex's allow-lists, which transcribe a named set
5
- * of enrichment hooks out of a shipped README, Antigravity has no in-repo
6
- * doc to transcribe from this release (Step 0 descope — see the plan's Step
7
- * 5 blockquote). The allow-list therefore defaults to **every event
8
- * excluded**, not a curated subset, until a future card's `CONTRACT.md`
9
- * names a specific enrichment hook safe to opt into `unexpectedError:
10
- * "continue"`.
5
+ * The host's hook reference — pinned in this directory's `CONTRACT.md` —
6
+ * names five events and splits them structurally: `PreToolUse` and
7
+ * `PostToolUse` wrap their handlers in a `{ matcher, hooks }` group, while
8
+ * `PreInvocation`, `PostInvocation`, and `Stop` list handler objects
9
+ * directly and ignore any matcher. {@link EVENTS_WITH_MATCHER} is that split,
10
+ * and `cli-support.ts` emits `hooks.json` from it.
11
11
  *
12
- * That default is itself gated on a second precondition: because Antigravity
13
- * has no exit-code channel at all (every event is payload-only — see
14
- * `transport.ts` and `outputs.ts`'s {@link AntigravityBlockError}), a
15
- * fail-open swallow on this agent has no narrower fallback than Claude
16
- * Code's or Codex's — there is no stderr+exit-2 loud path to fall back to.
17
- * `AntigravityBlockError` landing first (this same step) is what makes it
18
- * safe to ever widen this list at all: an intentional block computed
19
- * mid-throw still survives `unexpectedError: "continue"` via `drive()`'s
20
- * pre-policy classification, exactly as it does for the other two agents.
12
+ * The advisory allow-list is empty, and stays empty: the reference names no
13
+ * event as safe to fail open. That default is load-bearing here in a way it
14
+ * is not on the other two agents, because Antigravity has no exit-code
15
+ * channel at all (every event is payload-only — see `transport.ts` and
16
+ * `outputs.ts`'s {@link AntigravityBlockError}), so a fail-open swallow has
17
+ * no loud path to degrade to. `AntigravityBlockError` is what would make
18
+ * widening the list safe at all: an intentional block computed mid-throw
19
+ * still survives `unexpectedError: "continue"` via `drive()`'s pre-policy
20
+ * classification, exactly as it does for the other two agents.
21
21
  * @module
22
22
  */
23
23
  /** All Antigravity hook event names as a readonly array. */
@@ -28,15 +28,17 @@ export const HOOK_EVENT_NAMES = [
28
28
  "PostInvocation",
29
29
  "Stop",
30
30
  ];
31
- /** The two events whose config accepts a `matcher` (plan step 5.3: "matcher on the two ToolUse events"). */
31
+ /**
32
+ * The two events whose `hooks.json` entry is grouped under a `matcher`. The
33
+ * other three are flat: the host ignores a matcher on them entirely.
34
+ */
32
35
  export const EVENTS_WITH_MATCHER = ["PreToolUse", "PostToolUse"];
33
36
  /**
34
37
  * Events never allowed the fail-open `unexpectedError: "continue"` policy.
35
38
  *
36
- * Every shipped event, by default — see module docs. This is a stated
37
- * default (plan Decisions: "advisory list default empty until CONTRACT.md
38
- * justifies an entry"), not a silent gap.
39
+ * Every shipped event. The host's hook reference names no event as safe to
40
+ * fail open, so this is a transcribed conclusion, not a placeholder.
39
41
  */
40
42
  export const EXCLUDED_FROM_ADVISORY = [...HOOK_EVENT_NAMES];
41
- /** The advisory allow-list. Empty until a future `CONTRACT.md` names an entry. */
43
+ /** The advisory allow-list. Empty: `CONTRACT.md` names no event safe to fail open. */
42
44
  export const ADVISORY_EVENTS = HOOK_EVENT_NAMES.filter((eventName) => !EXCLUDED_FROM_ADVISORY.includes(eventName));
@@ -2,20 +2,19 @@
2
2
  * Typed factory functions for Antigravity hooks, built on the shared core
3
3
  * {@link ../../core/define-hook.ts|defineHook}.
4
4
  *
5
- * This release ships the typed factory surface and output builders only
6
- * (plan Step 5, item-6 descope) — no `CONTRACT.md`-derived e2e file, no
7
- * `./antigravity` export. Every factory injects the advisory allow-list
8
- * policy gate, so `unexpectedError: "continue"` fails closed at
9
- * factory-call time for every event ({@link ADVISORY_EVENTS} is empty this
10
- * release); {@link HookConfigFor} additionally narrows the policy field at
11
- * compile time.
5
+ * Every factory injects the advisory allow-list policy gate, so
6
+ * `unexpectedError: "continue"` fails closed at factory-call time for every
7
+ * event ({@link ADVISORY_EVENTS} is empty, and `CONTRACT.md` names no event
8
+ * safe to fail open); {@link HookConfigFor} additionally narrows the policy
9
+ * field at compile time.
12
10
  *
13
11
  * Antigravity's config surface is a single conditional type rather than the
14
12
  * separate matcher/no-matcher interface pair Claude Code and Codex use: only
15
13
  * two of the five events (`PreToolUse`, `PostToolUse`) carry a `matcher`
16
14
  * ({@link EVENTS_WITH_MATCHER} in `events.ts`), and a conditional type on the
17
15
  * bound event expresses that distinction without duplicating the base field
18
- * list twice.
16
+ * list twice. The host ignores a matcher on the other three events entirely,
17
+ * so accepting one there would be a silent no-op.
19
18
  * @module
20
19
  */
21
20
  import { defineHook } from "../../core/define-hook.js";
@@ -1,28 +1,17 @@
1
1
  /**
2
2
  * `@goodfoot/agent-hooks/antigravity` — the Antigravity agent surface.
3
3
  *
4
- * **Not yet published as a package subpath.** Step 5 ships under its item-6
5
- * descope (Step 0 concluded "not runnable / unknown" for the real
6
- * `antigravity` CLI in this release's environment): typed factories, output
7
- * builders, and a conformance matrix only. `packages/agent-hooks/package.json`
8
- * deliberately does not add a `./antigravity` `exports` entry this release —
9
- * see `tests/export-surface.test.ts`, which asserts the exports map stays
10
- * closed at `.`/`./claude-code`/`./codex`. This module exists for in-repo
11
- * testing (the conformance matrix imports it directly by relative path) and
12
- * for the follow-up card that resolves the CLI-availability question to
13
- * build on without re-deriving the surface from scratch.
4
+ * Re-exports the five hook factories, their per-event output builders, the
5
+ * `AntigravityBlockError` block signal, the wire input/output types, and the
6
+ * runtime `execute` entry point. No default export.
14
7
  *
15
- * Re-exports the 5 hook factories, their output builders (reserved fields
16
- * emitted only when passed), the `AntigravityBlockError` block signal,
17
- * input/output types, and the runtime `execute` entry point. No default
18
- * export.
8
+ * Every shape here is transcribed from `CONTRACT.md` in this directory, which
9
+ * pins the host's own hook reference. Two consequences shape the whole
10
+ * surface: keys are camelCase, and each event has a **different** stdout
11
+ * shape rather than one shared envelope.
19
12
  * @module
20
13
  */
21
- // Advisory allow-list events
22
14
  export { ADVISORY_EVENTS, EVENTS_WITH_MATCHER, EXCLUDED_FROM_ADVISORY, HOOK_EVENT_NAMES } from "./events.js";
23
- // Hook factory functions - all 5 event types
24
15
  export { postInvocationHook, postToolUseHook, preInvocationHook, preToolUseHook, stopHook } from "./hooks.js";
25
- // Output types and builders (AntigravityBlockError subclasses core HookBlockError)
26
16
  export { AntigravityBlockError, EXIT_CODES, postInvocationOutput, postToolUseOutput, preInvocationOutput, preToolUseOutput, stopOutput, } from "./outputs.js";
27
- // Runtime exports - execute function and transport factory
28
17
  export { createAntigravityTransport, execute } from "./transport.js";