agent-roast 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ npx agent-roast # review what you have not committed yet (on a clean tr
37
37
  ## Stop your coding agent from doing it
38
38
 
39
39
  ```bash
40
- npx agent-roast hook install # Claude Code, Codex and Cursor
40
+ npx agent-roast hook install # Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and Factory Droid
41
41
  ```
42
42
 
43
43
  For Claude Code there is also a plugin: `/plugin install agent-roast --marketplace Nari-Ab/agent-roast`. Any agent that speaks
@@ -122,8 +122,8 @@ two thirds of its blocks were questions with a harmless answer, depending on the
122
122
  (Claude, Copilot, Cursor, Codex, Devin, aider) and on 0.3% of the others. Inside the same repositories the odds for an
123
123
  AI-signed commit were 0.81 times those of another commit (95% CI 0.40–1.67). There is no sign that AI-signed commits weaken
124
124
  checks more often.
125
- - **On repositories it was not written from, between a quarter and two thirds of the blocked commits were false alarms.** Nine samples of 39
126
- or 40 public repositories (356 in all, 102,210 commits) were replayed and every blocked commit was read. The share was 69% for 0.17.0 on the
125
+ - **On repositories it was not written from, between a quarter and two thirds of the blocked commits were false alarms.** Ten samples of 39
126
+ or 40 public repositories (396 in all, 113,401 commits) were replayed and every blocked commit was read. The share was 69% for 0.17.0 on the
127
127
  first sample and on the fourth, 51% for 0.18.0 on the fourth and 27% on the fifth, which it had not seen either. This version, frozen before the
128
128
  sixth sample was drawn, blocked 100 of its 11,335 commits: 34 changed a check a reviewer should see (an assertion that got weaker, a gate
129
129
  that was relaxed, a test switched off or left focused) and 66 were false alarms, but 51 of the 66 were one defect in one repository, an array of
@@ -131,8 +131,8 @@ two thirds of its blocks were questions with a harmless answer, depending on the
131
131
  test that asserts nothing counts as a block for a reviewer; counted as false alarms, 13 of 33 on the fifth sample and 72 of 100 on the sixth. Most of the rest, in the
132
132
  earlier measurements too, were deliberate and explained. One reader (an AI assistant) sorted them, and the fixes were made from the same
133
133
  sorting, so treat the figures as a first estimate. The published 0.19.0 blocked 31 of the 12,000 commits of the seventh sample: 16 for a reviewer and 15 false alarms (48%, 6 of them one
134
- repository's lint configuration); the build with the fixes from reading it, tried on an eighth sample that it had not seen, blocked 26 of 11,081 commits: 16 for a reviewer and 10 false alarms (38%), and the fixes from reading that one are in this release too; this release as it was frozen blocked 46 of 11,286 commits of a ninth sample that it had not seen: 28 for a reviewer and 18 false alarms (39%), and five of those are fixed in it ([section 9](docs/benchmarks.md#9-repositories-nobody-had-tuned-on),
135
- [section 10](docs/benchmarks.md#10-two-more-samples-and-what-the-fixes-from-sample-4-did), [section 11](docs/benchmarks.md#11-a-seventh-sample-and-two-things-that-were-missing), [section 12](docs/benchmarks.md#12-an-eighth-sample-the-first-measurement-of-the-fixes-from-sample-7), [section 14](docs/benchmarks.md#14-a-ninth-sample-the-shipped-build-on-repositories-it-had-not-seen)).
134
+ repository's lint configuration); the build with the fixes from reading it, tried on an eighth sample that it had not seen, blocked 26 of 11,081 commits: 16 for a reviewer and 10 false alarms (38%), and the fixes from reading that one are in this release too; this release as it was frozen blocked 46 of 11,286 commits of a ninth sample that it had not seen: 28 for a reviewer and 18 false alarms (39%), and five of those are fixed in it; this release as it ships blocked 40 of the 11,191 commits of a tenth sample that it had not seen either: 27 for a reviewer and 13 false alarms (33%) ([section 9](docs/benchmarks.md#9-repositories-nobody-had-tuned-on),
135
+ [section 10](docs/benchmarks.md#10-two-more-samples-and-what-the-fixes-from-sample-4-did), [section 11](docs/benchmarks.md#11-a-seventh-sample-and-two-things-that-were-missing), [section 12](docs/benchmarks.md#12-an-eighth-sample-the-first-measurement-of-the-fixes-from-sample-7), [section 14](docs/benchmarks.md#14-a-ninth-sample-the-shipped-build-on-repositories-it-had-not-seen), [section 15](docs/benchmarks.md#15-a-tenth-sample-the-build-that-ships-on-repositories-it-had-not-seen)).
136
136
  - **9,732 commits compared in full** from 164 public repositories that use coding agents (5,454 of them AI-signed) were reviewed again for the
137
137
  last two releases, and every new finding was read. 0.15.0's rules reported 30 new findings there and 7 were false alarms (fixed in 0.16.0);
138
138
  0.17.0's rule for an exact check turned into a bound reported 18 in 14 commits, all of them loosened checks, after two false alarms were
@@ -5041,13 +5041,14 @@ function formatReplayTerminal(result, allRules = false) {
5041
5041
  // src/hook.ts
5042
5042
  import path8 from "path";
5043
5043
  import * as fs7 from "fs/promises";
5044
- var HOOK_AGENTS = ["claude", "codex", "cursor"];
5044
+ var HOOK_AGENTS = ["claude", "codex", "cursor", "gemini", "copilot", "droid"];
5045
5045
  var DEFAULT_TIMEOUT_MS = 25e3;
5046
5046
  var PAUSE_FILE = "agent-roast-hook-paused";
5047
5047
  var LISTED = 6;
5048
5048
  var LISTED_WARNINGS = 3;
5049
5049
  function detectAgent(input) {
5050
5050
  if ("workspace_roots" in input || "cursor_version" in input || "conversation_id" in input) return "cursor";
5051
+ if (input.hook_event_name === "AfterAgent" || "prompt_response" in input) return "gemini";
5051
5052
  if ("turn_id" in input) return "codex";
5052
5053
  return "claude";
5053
5054
  }
@@ -5055,7 +5056,7 @@ function inputCwd(agent, input, env) {
5055
5056
  const roots = input.workspace_roots;
5056
5057
  if (agent === "cursor" && Array.isArray(roots) && typeof roots[0] === "string") return roots[0];
5057
5058
  if (typeof input.cwd === "string" && input.cwd) return input.cwd;
5058
- return env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || process.cwd();
5059
+ return env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || env.GEMINI_PROJECT_DIR || env.FACTORY_PROJECT_DIR || process.cwd();
5059
5060
  }
5060
5061
  function clip(text, limit) {
5061
5062
  const flat = terminalText(text).replace(/\s+/g, " ").trim();
@@ -5133,8 +5134,8 @@ async function runHook(rawInput, options = {}) {
5133
5134
  if (["off", "0", "false"].includes(mode)) return outcome("skip");
5134
5135
  const advisory = options.advisory === true || mode === "advisory";
5135
5136
  const agent = options.agent ?? detectAgent(input);
5136
- const event = String(input.hook_event_name ?? "Stop");
5137
- if (event.toLowerCase() !== "stop") return outcome("skip");
5137
+ const finishing = agent === "gemini" ? "afteragent" : "stop";
5138
+ if (String(input.hook_event_name ?? finishing).toLowerCase() !== finishing) return outcome("skip");
5138
5139
  if (agent === "cursor" && typeof input.status === "string" && input.status !== "completed") return outcome("skip");
5139
5140
  const cwd = options.cwd ?? inputCwd(agent, input, env);
5140
5141
  if (await hookPausedUntil(cwd, options.now)) return outcome("skip");
@@ -5150,15 +5151,17 @@ async function runHook(rawInput, options = {}) {
5150
5151
  } finally {
5151
5152
  clearTimeout(timer);
5152
5153
  }
5153
- if (!result.failedRules.length) return outcome("pass", agent === "cursor" ? "{}\n" : "", "", result);
5154
+ if (!result.failedRules.length) return outcome("pass", agent === "cursor" || agent === "gemini" ? "{}\n" : "", "", result);
5154
5155
  const alreadyAsked = input.stop_hook_active === true || typeof input.loop_count === "number" && input.loop_count > 0;
5155
5156
  if (alreadyAsked || advisory) {
5156
5157
  const warning = formatUserWarning(result);
5157
- return outcome("warn", agent === "cursor" ? "{}\n" : JSON.stringify({ systemMessage: warning }) + "\n", agent === "cursor" && advisory ? `${warning}
5158
+ const notice = agent === "copilot" ? { type: "progress", message: warning } : { systemMessage: warning };
5159
+ return outcome("warn", agent === "cursor" ? "{}\n" : JSON.stringify(notice) + "\n", agent === "cursor" && advisory ? `${warning}
5158
5160
  ` : "", result);
5159
5161
  }
5160
5162
  const message = formatAgentFeedback(result);
5161
- return outcome("block", JSON.stringify(agent === "cursor" ? { followup_message: message } : { decision: "block", reason: message }) + "\n", "", result);
5163
+ const answer = agent === "cursor" ? { followup_message: message } : { decision: agent === "gemini" ? "deny" : "block", reason: message };
5164
+ return outcome("block", JSON.stringify(answer) + "\n", "", result);
5162
5165
  }
5163
5166
 
5164
5167
  // src/hook-install.ts
@@ -5168,8 +5171,9 @@ import path9 from "path";
5168
5171
  import { randomUUID as randomUUID4 } from "crypto";
5169
5172
  import { createRequire as createRequire2 } from "module";
5170
5173
  import { spawnSync as spawnSync3 } from "child_process";
5171
- var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor" };
5174
+ var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor", gemini: "Gemini CLI", copilot: "GitHub Copilot", droid: "Factory Droid" };
5172
5175
  var TIMEOUT_SECONDS = 60;
5176
+ var FINISH_EVENT = { claude: "Stop", codex: "Stop", cursor: "stop", gemini: "AfterAgent", copilot: "Stop", droid: "Stop" };
5173
5177
  function packageVersion() {
5174
5178
  const require3 = createRequire2(import.meta.url);
5175
5179
  return String(require3("../package.json").version);
@@ -5196,6 +5200,9 @@ function hookConfigPath(agent, scope, root, home, env = process.env) {
5196
5200
  const claude = path9.join(scope === "global" ? home : root, ".claude");
5197
5201
  if (agent === "claude") return path9.join(claude, scope === "local" ? "settings.local.json" : "settings.json");
5198
5202
  if (agent === "cursor") return path9.join(scope === "global" ? home : root, ".cursor", "hooks.json");
5203
+ if (agent === "gemini") return path9.join(scope === "global" ? home : root, ".gemini", "settings.json");
5204
+ if (agent === "copilot") return path9.join(scope === "global" ? env.COPILOT_HOME || path9.join(home, ".copilot") : path9.join(root, ".github"), "hooks", "agent-roast.json");
5205
+ if (agent === "droid") return path9.join(scope === "global" ? home : root, ".factory", "hooks.json");
5199
5206
  return path9.join(scope === "global" ? env.CODEX_HOME || path9.join(home, ".codex") : path9.join(root, ".codex"), "hooks.json");
5200
5207
  }
5201
5208
  function readJsonFile(file) {
@@ -5231,40 +5238,47 @@ function writeJsonFile(link, json) {
5231
5238
  }
5232
5239
  }
5233
5240
  var asArray = (value) => Array.isArray(value) ? value : [];
5241
+ var isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
5242
+ function eventMap(agent, data, create) {
5243
+ if (agent === "droid") return data;
5244
+ if (create && !isObject(data.hooks)) data.hooks = {};
5245
+ return isObject(data.hooks) ? data.hooks : void 0;
5246
+ }
5247
+ var isFlat = (agent) => agent === "cursor" || agent === "copilot";
5234
5248
  function addStopHook(agent, data, command) {
5235
5249
  const before = JSON.stringify(data);
5236
- data.hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
5237
- if (agent === "cursor") {
5250
+ const events = eventMap(agent, data, true), event = FINISH_EVENT[agent];
5251
+ if (isFlat(agent)) {
5238
5252
  data.version ??= 1;
5239
- const handlers = asArray(data.hooks.stop);
5253
+ const handlers = asArray(events[event]);
5240
5254
  const ours = handlers.find((handler) => isOurs(handler?.command));
5241
5255
  if (ours) ours.command = command;
5242
- else handlers.push({ command, timeout: TIMEOUT_SECONDS, loop_limit: 2 });
5243
- data.hooks.stop = handlers;
5256
+ else handlers.push(agent === "cursor" ? { command, timeout: TIMEOUT_SECONDS, loop_limit: 2 } : { type: "command", command, timeoutSec: TIMEOUT_SECONDS });
5257
+ events[event] = handlers;
5244
5258
  } else {
5245
- const groups = asArray(data.hooks.Stop);
5259
+ const groups = asArray(events[event]);
5246
5260
  const ours = groups.flatMap((group) => asArray(group?.hooks)).find((handler) => isOurs(handler?.command));
5247
5261
  if (ours) ours.command = command;
5248
- else groups.push({ hooks: [{ type: "command", command, timeout: TIMEOUT_SECONDS }] });
5249
- data.hooks.Stop = groups;
5262
+ else groups.push(agent === "gemini" ? { matcher: "*", hooks: [{ name: "agent-roast", type: "command", command, timeout: TIMEOUT_SECONDS * 1e3 }] } : { hooks: [{ type: "command", command, timeout: TIMEOUT_SECONDS }] });
5263
+ events[event] = groups;
5250
5264
  }
5251
5265
  return JSON.stringify(data) !== before;
5252
5266
  }
5253
5267
  function removeStopHook(agent, data) {
5254
5268
  const before = JSON.stringify(data);
5255
- const hooks = data.hooks;
5256
- if (!hooks || typeof hooks !== "object") return false;
5257
- if (agent === "cursor") {
5258
- const kept = asArray(hooks.stop).filter((handler) => !isOurs(handler?.command));
5259
- if (kept.length) hooks.stop = kept;
5260
- else delete hooks.stop;
5269
+ const events = eventMap(agent, data, false), event = FINISH_EVENT[agent];
5270
+ if (!events) return false;
5271
+ if (isFlat(agent)) {
5272
+ const kept = asArray(events[event]).filter((handler) => !isOurs(handler?.command));
5273
+ if (kept.length) events[event] = kept;
5274
+ else delete events[event];
5261
5275
  } else {
5262
- const groups = asArray(hooks.Stop).map((group) => ({ ...group, hooks: asArray(group?.hooks).filter((handler) => !isOurs(handler?.command)) }));
5276
+ const groups = asArray(events[event]).map((group) => ({ ...group, hooks: asArray(group?.hooks).filter((handler) => !isOurs(handler?.command)) }));
5263
5277
  const kept = groups.filter((group) => group.hooks.length);
5264
- if (kept.length) hooks.Stop = kept;
5265
- else delete hooks.Stop;
5278
+ if (kept.length) events[event] = kept;
5279
+ else delete events[event];
5266
5280
  }
5267
- if (!Object.keys(hooks).length) delete data.hooks;
5281
+ if (events !== data && !Object.keys(events).length) delete data.hooks;
5268
5282
  return JSON.stringify(data) !== before;
5269
5283
  }
5270
5284
  function context(options) {
@@ -5294,14 +5308,15 @@ function uninstallHooks(options) {
5294
5308
  function hookStatus(options) {
5295
5309
  const { root, home, env } = context({ ...options, scope: "project" });
5296
5310
  const rows = [];
5297
- for (const agent of ["claude", "codex", "cursor"]) {
5311
+ for (const agent of ["claude", "codex", "cursor", "gemini", "copilot", "droid"]) {
5298
5312
  for (const scope of ["project", "local", "global"]) {
5299
5313
  if (scope === "local" && agent !== "claude") continue;
5300
5314
  const file = hookConfigPath(agent, scope, root, home, env);
5301
5315
  let command;
5302
5316
  try {
5303
5317
  const data = readJsonFile(file).data;
5304
- const handlers = agent === "cursor" ? asArray(data.hooks?.stop) : asArray(data.hooks?.Stop).flatMap((group) => asArray(group?.hooks));
5318
+ const listed = asArray(eventMap(agent, data, false)?.[FINISH_EVENT[agent]]);
5319
+ const handlers = isFlat(agent) ? listed : listed.flatMap((group) => asArray(group?.hooks));
5305
5320
  command = handlers.map((handler) => handler?.command).find(isOurs);
5306
5321
  } catch {
5307
5322
  }
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  terminalText,
39
39
  uninstallHooks,
40
40
  writeBaseline
41
- } from "./chunk-XVV5FNDV.js";
41
+ } from "./chunk-HDC5C2D3.js";
42
42
 
43
43
  // src/cli.ts
44
44
  import { Command } from "commander";
@@ -100,7 +100,7 @@ function isDefaultInvocation(options) {
100
100
  var NEXT_STEPS = [
101
101
  "",
102
102
  pc.bold("Next steps"),
103
- ` ${pc.cyan("npx agent-roast hook install")} stop your coding agent from weakening checks (Claude Code, Codex, Cursor)`,
103
+ ` ${pc.cyan("npx agent-roast hook install")} stop your coding agent from weakening checks (Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid)`,
104
104
  ` ${pc.cyan("npx agent-roast init --ci")} add the pull-request check (GitHub Actions)`,
105
105
  ` ${pc.cyan("npx agent-roast demo")} see a scripted example`,
106
106
  ""
@@ -505,7 +505,7 @@ program.command("demo").description('See what agent-roast reports for a scripted
505
505
  }
506
506
  });
507
507
  program.command("audit").description("Score the Git history by the shortcuts it adds (the original agent-roast report)").argument("[path]", "Path to git repository (default: current directory)", ".").action((repoPath) => rootAction(repoPath, { ...program.opts(), forceLegacy: true }));
508
- var hook = program.command("hook").description("Guard a coding agent: send it back to fix the code when it weakens a check (Claude Code, Codex, Cursor)");
508
+ var hook = program.command("hook").description("Guard a coding agent: send it back to fix the code when it weakens a check (Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid)");
509
509
  hook.command("run").description("Run by the agent when it is about to finish; reads the agent's JSON on stdin and answers in its dialect").option("--agent <agent>", `${HOOK_AGENTS.join(", ")} (detected from the input when omitted)`).option("--advisory", "only warn the person; never send the agent back", false).action(async (opts) => {
510
510
  let agent;
511
511
  try {
@@ -525,13 +525,13 @@ hook.command("run").description("Run by the agent when it is about to finish; re
525
525
  process.exitCode = 0;
526
526
  }
527
527
  });
528
- hook.command("install").description("Add the hook to Claude Code, Codex or Cursor in this repository").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: the agents that already have a folder in this repository)`).option("--global", "install for every repository of this user instead", false).option("--local", "Claude Code only: use .claude/settings.local.json, which stays out of Git", false).option("--command <program>", "program to run instead of 'npx --yes agent-roast@<version>', for example 'agent-roast' when installed globally").option("--advisory", "try it without risk: the hook only warns you and never sends the agent back", false).option("--instructions", "also write a short 'keep the checks honest' paragraph into AGENTS.md / CLAUDE.md", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
528
+ hook.command("install").description("Add the hook to Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot or Factory Droid in this repository").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: the agents that already have a folder in this repository)`).option("--global", "install for every repository of this user instead", false).option("--local", "Claude Code only: use .claude/settings.local.json, which stays out of Git", false).option("--command <program>", "program to run instead of 'npx --yes agent-roast@<version>', for example 'agent-roast' when installed globally").option("--advisory", "try it without risk: the hook only warns you and never sends the agent back", false).option("--instructions", "also write a short 'keep the checks honest' paragraph into AGENTS.md / CLAUDE.md", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
529
529
  try {
530
530
  if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
531
531
  let agents = parseAgents(names);
532
532
  const root = repositoryRoot(process.cwd());
533
- if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(root, { claude: ".claude", codex: ".codex", cursor: ".cursor" }[agent])));
534
- if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor).");
533
+ if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(root, { claude: ".claude", codex: ".codex", cursor: ".cursor", gemini: ".gemini", copilot: ".github/hooks", droid: ".factory" }[agent])));
534
+ if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor, gemini, copilot, droid).");
535
535
  const scope = opts.global ? "global" : opts.local ? "local" : "project";
536
536
  const advisory = opts.advisory || program.opts().advisory === true;
537
537
  const changes = installHooks({ cwd: process.cwd(), agents, scope, command: opts.command, advisory, dryRun: opts.dryRun });
@@ -542,6 +542,9 @@ hook.command("install").description("Add the hook to Claude Code, Codex or Curso
542
542
  for (const change of installInstructions({ cwd: process.cwd(), dryRun: opts.dryRun })) console.log(` + ${displayPath(change.file)} (${change.action}): keep-the-checks-honest paragraph`);
543
543
  }
544
544
  if (agents.includes("codex")) console.log("\nCodex runs a new hook only after you trust it: open Codex and review it with /hooks.");
545
+ if (agents.includes("gemini")) console.log("\nGemini CLI warns the first time it sees a project hook, and again when its command changes; /hooks panel shows it.");
546
+ if (agents.includes("copilot")) console.log("\nGitHub Copilot reads hook files when it starts: restart it. The cloud agent loads them from the default branch only.");
547
+ if (agents.includes("droid")) console.log("\nFactory Droid takes a snapshot of its hooks at startup: restart it, and review the hook with /hooks.");
545
548
  if (!opts.dryRun && advisory) console.log("\nAdvisory mode: when the agent finishes with weakened checks you get a one-line warning and the agent is never sent back.\nStart sending it back: agent-roast hook install (without --advisory) \xB7 remove it: agent-roast hook uninstall");
546
549
  else if (!opts.dryRun) console.log("\nWhen the agent is about to finish with weakened checks it is sent back once to fix the code.\nTurn it off for a while: agent-roast hook pause \xB7 only warn instead: agent-roast hook install --advisory \xB7 remove it: agent-roast hook uninstall");
547
550
  } catch (error) {
@@ -549,7 +552,7 @@ hook.command("install").description("Add the hook to Claude Code, Codex or Curso
549
552
  process.exitCode = 2;
550
553
  }
551
554
  });
552
- hook.command("uninstall").description("Remove the hook from Claude Code, Codex or Cursor").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: all)`).option("--global", "remove it from the user-level files", false).option("--local", "Claude Code only: remove it from .claude/settings.local.json", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
555
+ hook.command("uninstall").description("Remove the hook from Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot or Factory Droid").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: all)`).option("--global", "remove it from the user-level files", false).option("--local", "Claude Code only: remove it from .claude/settings.local.json", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
553
556
  try {
554
557
  if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
555
558
  const agents = names.length ? parseAgents(names) : [...HOOK_AGENTS].filter((agent) => !(opts.local && agent !== "claude"));
@@ -565,11 +568,11 @@ hook.command("uninstall").description("Remove the hook from Claude Code, Codex o
565
568
  hook.command("status").description("Show where the hook is installed").action(async () => {
566
569
  try {
567
570
  const rows = hookStatus({ cwd: process.cwd() });
568
- for (const row of rows) console.log(` ${row.installed ? pc.green("installed") : pc.dim("not set ")} ${AGENT_LABELS[row.agent].padEnd(11)} ${row.scope.padEnd(8)} ${displayPath(row.file)}${row.advisory ? pc.yellow(" (advisory: warns, never sends the agent back)") : ""}`);
571
+ for (const row of rows) console.log(` ${row.installed ? pc.green("installed") : pc.dim("not set ")} ${AGENT_LABELS[row.agent].padEnd(16)} ${row.scope.padEnd(8)} ${displayPath(row.file)}${row.advisory ? pc.yellow(" (advisory: warns, never sends the agent back)") : ""}`);
569
572
  const paused = await hookPausedUntil(process.cwd());
570
573
  if (paused) console.log(`
571
574
  Paused in this repository until ${paused.toLocaleString()} (agent-roast hook resume).`);
572
- if (!rows.some((row) => row.installed)) console.log("\n Install it with: agent-roast hook install claude (or codex, cursor)");
575
+ if (!rows.some((row) => row.installed)) console.log("\n Install it with: agent-roast hook install claude (or codex, cursor, gemini, copilot, droid)");
573
576
  } catch (error) {
574
577
  console.error(`agent-roast hook status error: ${errorMessage(error)}`);
575
578
  process.exitCode = 2;
package/dist/index.d.ts CHANGED
@@ -475,7 +475,7 @@ declare function replayHistory(options?: ReplayOptions): Promise<ReplayResult>;
475
475
  declare function formatReplayTerminal(result: ReplayResult, allRules?: boolean): string;
476
476
 
477
477
  /** Coding agents that can run a command when they are about to finish and feed its answer back. */
478
- type HookAgent = "claude" | "codex" | "cursor";
478
+ type HookAgent = "claude" | "codex" | "cursor" | "gemini" | "copilot" | "droid";
479
479
  declare const HOOK_AGENTS: readonly HookAgent[];
480
480
  interface HookOptions {
481
481
  /** Which agent is calling. Detected from its input when omitted. */
package/dist/index.js CHANGED
@@ -92,7 +92,7 @@ import {
92
92
  unquoteGitPath,
93
93
  validateExceptions,
94
94
  writeBaseline
95
- } from "./chunk-XVV5FNDV.js";
95
+ } from "./chunk-HDC5C2D3.js";
96
96
  export {
97
97
  AGENT_LABELS,
98
98
  CONFIG_FILENAMES,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.21.0",
4
- "description": "Stops AI coding agents (and rushed humans) from turning a build green by weakening tests, coverage gates or CI. Hook for Claude Code, Codex and Cursor, pre-commit, GitHub Action. Offline.",
3
+ "version": "0.22.0",
4
+ "description": "Stops AI agents (and rushed humans) weakening tests, coverage gates or CI to get a green build. Hook for Claude Code, Codex, Cursor, Gemini CLI, Copilot, Droid; pre-commit; GitHub Action. Offline.",
5
5
  "mcpName": "io.github.Nari-Ab/agent-roast",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -34,6 +34,8 @@
34
34
  "codex",
35
35
  "cursor",
36
36
  "copilot",
37
+ "gemini-cli",
38
+ "factory-droid",
37
39
  "hooks",
38
40
  "guardrails",
39
41
  "code-review",