agent-roast 0.23.0 → 0.25.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, Cursor, Gemini CLI, GitHub Copilot and Factory Droid
40
+ npx agent-roast hook install # Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid, Kiro and Antigravity CLI
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
@@ -112,7 +112,7 @@ Measured, with the denominators and the caveats in [docs/benchmarks.md](docs/ben
112
112
  deliberate, and not specific to AI. agent-roast is a tripwire that asks about them. On repositories it had not seen, between a quarter and
113
113
  two thirds of its blocks were questions with a harmless answer, depending on the sample.**
114
114
 
115
- - **164 of 164** hand-labeled cheating edits are caught and **153 of 153** harmless look-alikes (reformats, moves, raised
115
+ - **165 of 165** hand-labeled cheating edits are caught and **154 of 154** harmless look-alikes (reformats, moves, raised
116
116
  thresholds, environment-gated skips) stay silent. The cases were written with the detector, so they guard against regressions;
117
117
  they do not prove accuracy. One gap is documented and still open.
118
118
  - **A catalog of cheats that someone else wrote.** Of the nine hacks in [reward-hack-zoo](https://github.com/MatthewWaller/reward-hack-zoo) (hand-written specimens; none from a model) four are reported (a skip, a patched target, a `conftest.py` that rewrites reports,
@@ -167,6 +167,8 @@ It is a review aid. It cannot prove who wrote a change or that code is correct,
167
167
  configuration, exceptions, baselines, limits) · [Comparison](docs/comparison.md) · [Measurements](docs/benchmarks.md) ·
168
168
  [Real blocks](docs/examples.md) · [Try it on your repository](docs/pilot.md) · [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md)
169
169
 
170
+ [MCP server setup](docs/integrations.md#mcp-server) · [![MCPRush scan grade for the Agent Roast MCP server](https://mcprush.com/badge/agent-roast-mcp/score.svg)](https://mcprush.com/nari-ab/agent-roast-mcp)
171
+
170
172
  Found a finding that was wrong, or a shortcut it missed? That is the most useful report there is:
171
173
  [open an issue](https://github.com/Nari-Ab/agent-roast/issues/new/choose).
172
174
 
@@ -4538,6 +4538,32 @@ function gatingCalls(file, text) {
4538
4538
  if (!(/\.gitlab-ci/.test(file) || CI_GATE.test(gatingTriggers(text) ?? text))) return [];
4539
4539
  return [...text.matchAll(/^\s*-?\s*uses:\s*["']?\.\/(\.github\/workflows\/[^\s"'#@]+\.ya?ml)/gm)].map((match) => match[1]);
4540
4540
  }
4541
+ function lostCalls(file, oldText, newText) {
4542
+ if (!CI_FILE.test(file)) return [];
4543
+ const before = gatingCalls(file, oldText ?? "");
4544
+ if (!before.length) return [];
4545
+ const after = new Set(gatingCalls(file, newText ?? ""));
4546
+ return before.filter((workflow) => !after.has(workflow));
4547
+ }
4548
+ function calledTestCommands(text) {
4549
+ const directories = [...new Set([...text.matchAll(/^\s*(?:-\s+)?working-directory\s*:\s*["']?([^\s"'#]+)/gm)].map((match) => match[1]))].sort().join(",");
4550
+ return ciTestEntries(text).map((entry) => `${entry.command.replace(/\s#.*$/, "").replace(/^\s*-?\s*(?:run\s*:\s*)?/, "").replace(/\s+/g, " ").trim()} @${directories}`);
4551
+ }
4552
+ function resolveLostCallers(lost, commit) {
4553
+ const out = [];
4554
+ for (const [file, { content, stillCalled }] of lost) {
4555
+ if (stillCalled || gatingTestCommands(file, content).length > 0 || calledTestCommands(content).length === 0) continue;
4556
+ out.push(finding(
4557
+ "guard-weakened",
4558
+ file,
4559
+ 1,
4560
+ "nothing calls this workflow on a push or pull request any more",
4561
+ "Workflow keeps its tests, but the workflow that called it is gone",
4562
+ commit
4563
+ ));
4564
+ }
4565
+ return out;
4566
+ }
4541
4567
  function resolveCiTestSteps(counts, commit, elsewhere = /* @__PURE__ */ new Set(), called = /* @__PURE__ */ new Set()) {
4542
4568
  const before = counts.reduce((sum, item) => sum + item.before, 0);
4543
4569
  const after = counts.reduce((sum, item) => sum + item.after + item.reusableAfter, 0);
@@ -4800,6 +4826,8 @@ async function compareSnapshot(options, cwd, snapshot) {
4800
4826
  const guardFindings = [];
4801
4827
  const ciCounts = [];
4802
4828
  const calledInChange = /* @__PURE__ */ new Set();
4829
+ const lostCalledWorkflows = /* @__PURE__ */ new Set();
4830
+ const touchedCiContent = /* @__PURE__ */ new Map();
4803
4831
  let productionChanged = false;
4804
4832
  const decode = (content) => {
4805
4833
  totalBytes += content.length;
@@ -4837,8 +4865,12 @@ async function compareSnapshot(options, cwd, snapshot) {
4837
4865
  const newText = await readNew(change);
4838
4866
  guardFindings.push(...analyzeGuardFile(change.newPath, oldText, newText, refs.headSha));
4839
4867
  const ci = ciTestSteps(change.newPath, oldText, newText);
4840
- if (ci) ciCounts.push(ci);
4841
- if (ci && newText !== null) for (const workflow of gatingCalls(change.newPath, newText)) calledInChange.add(workflow);
4868
+ if (ci) {
4869
+ ciCounts.push(ci);
4870
+ touchedCiContent.set(change.newPath, newText);
4871
+ for (const workflow of lostCalls(change.newPath, oldText, newText)) lostCalledWorkflows.add(workflow);
4872
+ if (newText !== null) for (const workflow of gatingCalls(change.newPath, newText)) calledInChange.add(workflow);
4873
+ }
4842
4874
  result.filesInspected++;
4843
4875
  } catch (error) {
4844
4876
  assertNotAborted(options.signal);
@@ -4897,7 +4929,8 @@ async function compareSnapshot(options, cwd, snapshot) {
4897
4929
  result.complete = result.skippedFiles.length === 0;
4898
4930
  const rules = policy.config.rules ?? {};
4899
4931
  const elsewhere = /* @__PURE__ */ new Set(), called = new Set(calledInChange);
4900
- if (ciCounts.some((item) => item.before > 0 && item.after === 0 && item.reusableAfter === 0)) {
4932
+ const untouchedLostCallerContent = /* @__PURE__ */ new Map();
4933
+ if (ciCounts.some((item) => item.before > 0 && item.after === 0 && item.reusableAfter === 0) || lostCalledWorkflows.size > 0) {
4901
4934
  const touched = new Set(changes.flatMap((change) => [change.oldPath, change.newPath]));
4902
4935
  const tree = /^[a-f0-9]{40,64}$/.test(refs.headSha) ? refs.headSha : refs.baseSha;
4903
4936
  try {
@@ -4906,6 +4939,7 @@ async function compareSnapshot(options, cwd, snapshot) {
4906
4939
  const text = new TextDecoder().decode(await readBlob(cwd, file.oid, options.signal, 512 * 1024));
4907
4940
  for (const command of enforcedTestCommands(file.path, text)) elsewhere.add(command);
4908
4941
  for (const workflow of gatingCalls(file.path, text)) called.add(workflow);
4942
+ if (lostCalledWorkflows.has(file.path)) untouchedLostCallerContent.set(file.path, text);
4909
4943
  } catch (error) {
4910
4944
  assertNotAborted(options.signal);
4911
4945
  }
@@ -4914,7 +4948,16 @@ async function compareSnapshot(options, cwd, snapshot) {
4914
4948
  assertNotAborted(options.signal);
4915
4949
  }
4916
4950
  }
4917
- result.infractions.push(...[...resolveTampering(tamperingFiles, productionChanged), ...guardFindings, ...resolveCiTestSteps(ciCounts, refs.headSha, elsewhere, called)].filter((finding2) => rules[finding2.type] !== "off"));
4951
+ const ciFindings = resolveCiTestSteps(ciCounts, refs.headSha, elsewhere, called);
4952
+ const alreadyFlagged = new Set(ciFindings.map((found) => found.file));
4953
+ const lostCallers = /* @__PURE__ */ new Map();
4954
+ for (const workflow of lostCalledWorkflows) {
4955
+ if (alreadyFlagged.has(workflow)) continue;
4956
+ const content = touchedCiContent.has(workflow) ? touchedCiContent.get(workflow) ?? null : untouchedLostCallerContent.get(workflow) ?? null;
4957
+ if (content === null) continue;
4958
+ lostCallers.set(workflow, { content, stillCalled: called.has(workflow) });
4959
+ }
4960
+ result.infractions.push(...[...resolveTampering(tamperingFiles, productionChanged), ...guardFindings, ...ciFindings, ...resolveLostCallers(lostCallers, refs.headSha)].filter((finding2) => rules[finding2.type] !== "off"));
4918
4961
  const exceptions = applyExceptions(result.infractions, options.ignoreExceptions ? [] : policy.config.exceptions, /* @__PURE__ */ new Date(), existingCounts);
4919
4962
  result.infractions = exceptions.findings;
4920
4963
  result.suppressedFindings = exceptions.suppressedFindings;
@@ -5052,7 +5095,7 @@ function formatReplayTerminal(result, allRules = false) {
5052
5095
  // src/hook.ts
5053
5096
  import path8 from "path";
5054
5097
  import * as fs7 from "fs/promises";
5055
- var HOOK_AGENTS = ["claude", "codex", "cursor", "gemini", "copilot", "droid"];
5098
+ var HOOK_AGENTS = ["claude", "codex", "cursor", "gemini", "copilot", "droid", "kiro", "antigravity"];
5056
5099
  var DEFAULT_TIMEOUT_MS = 25e3;
5057
5100
  var PAUSE_FILE = "agent-roast-hook-paused";
5058
5101
  var LISTED = 6;
@@ -5060,14 +5103,16 @@ var LISTED_WARNINGS = 3;
5060
5103
  function detectAgent(input) {
5061
5104
  if ("workspace_roots" in input || "cursor_version" in input || "conversation_id" in input) return "cursor";
5062
5105
  if (input.hook_event_name === "AfterAgent" || "prompt_response" in input) return "gemini";
5106
+ if ("conversationId" in input || "fullyIdle" in input) return "antigravity";
5063
5107
  if ("turn_id" in input) return "codex";
5064
5108
  return "claude";
5065
5109
  }
5066
5110
  function inputCwd(agent, input, env) {
5067
5111
  const roots = input.workspace_roots;
5068
5112
  if (agent === "cursor" && Array.isArray(roots) && typeof roots[0] === "string") return roots[0];
5113
+ if (agent === "antigravity" && Array.isArray(input.workspacePaths) && typeof input.workspacePaths[0] === "string") return input.workspacePaths[0];
5069
5114
  if (typeof input.cwd === "string" && input.cwd) return input.cwd;
5070
- return env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || env.GEMINI_PROJECT_DIR || env.FACTORY_PROJECT_DIR || process.cwd();
5115
+ return env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || env.GEMINI_PROJECT_DIR || env.FACTORY_PROJECT_DIR || env.KIRO_PROJECT_DIR || env.ANTIGRAVITY_PROJECT_DIR || process.cwd();
5071
5116
  }
5072
5117
  function clip(text, limit) {
5073
5118
  const flat = terminalText(text).replace(/\s+/g, " ").trim();
@@ -5131,7 +5176,7 @@ function parseDuration(text) {
5131
5176
  if (!match || Number(match[1]) < 1) throw new ConfigError("Use a duration such as 30m, 2h or 1d.");
5132
5177
  return Number(match[1]) * { m: 6e4, h: 36e5, d: 864e5 }[match[2].toLowerCase()];
5133
5178
  }
5134
- var outcome = (action, stdout = "", stderr = "", result) => ({ action, stdout, stderr, exitCode: 0, result });
5179
+ var outcome = (action, stdout = "", stderr = "", result, exitCode = 0) => ({ action, stdout, stderr, exitCode, result });
5135
5180
  async function runHook(rawInput, options = {}) {
5136
5181
  const env = options.env ?? process.env;
5137
5182
  let input = {};
@@ -5162,16 +5207,25 @@ async function runHook(rawInput, options = {}) {
5162
5207
  } finally {
5163
5208
  clearTimeout(timer);
5164
5209
  }
5165
- if (!result.failedRules.length) return outcome("pass", agent === "cursor" || agent === "gemini" ? "{}\n" : "", "", result);
5210
+ if (!result.failedRules.length) {
5211
+ const fine = agent === "cursor" || agent === "gemini" ? "{}\n" : agent === "antigravity" ? '{"decision":"stop"}\n' : "";
5212
+ return outcome("pass", fine, "", result);
5213
+ }
5166
5214
  const alreadyAsked = input.stop_hook_active === true || typeof input.loop_count === "number" && input.loop_count > 0;
5167
5215
  if (alreadyAsked || advisory) {
5168
5216
  const warning = formatUserWarning(result);
5217
+ if (agent === "kiro") return outcome("warn", "", advisory ? `${warning}
5218
+ ` : "", result);
5219
+ if (agent === "antigravity") return outcome("warn", '{"decision":"stop"}\n', advisory ? `${warning}
5220
+ ` : "", result);
5169
5221
  const notice = agent === "copilot" ? { type: "progress", message: warning } : { systemMessage: warning };
5170
5222
  return outcome("warn", agent === "cursor" ? "{}\n" : JSON.stringify(notice) + "\n", agent === "cursor" && advisory ? `${warning}
5171
5223
  ` : "", result);
5172
5224
  }
5173
5225
  const message = formatAgentFeedback(result);
5174
- const answer = agent === "cursor" ? { followup_message: message } : { decision: agent === "gemini" ? "deny" : "block", reason: message };
5226
+ if (agent === "kiro") return outcome("block", "", `${message}
5227
+ `, result, 1);
5228
+ const answer = agent === "cursor" ? { followup_message: message } : agent === "antigravity" ? { decision: "continue", reason: message } : { decision: agent === "gemini" ? "deny" : "block", reason: message };
5175
5229
  return outcome("block", JSON.stringify(answer) + "\n", "", result);
5176
5230
  }
5177
5231
 
@@ -5182,9 +5236,18 @@ import path9 from "path";
5182
5236
  import { randomUUID as randomUUID4 } from "crypto";
5183
5237
  import { createRequire as createRequire2 } from "module";
5184
5238
  import { spawnSync as spawnSync3 } from "child_process";
5185
- var AGENT_LABELS = { claude: "Claude Code", codex: "Codex", cursor: "Cursor", gemini: "Gemini CLI", copilot: "GitHub Copilot", droid: "Factory Droid" };
5239
+ var AGENT_LABELS = {
5240
+ claude: "Claude Code",
5241
+ codex: "Codex",
5242
+ cursor: "Cursor",
5243
+ gemini: "Gemini CLI",
5244
+ copilot: "GitHub Copilot",
5245
+ droid: "Factory Droid",
5246
+ kiro: "Kiro",
5247
+ antigravity: "Antigravity CLI"
5248
+ };
5186
5249
  var TIMEOUT_SECONDS = 60;
5187
- var FINISH_EVENT = { claude: "Stop", codex: "Stop", cursor: "stop", gemini: "AfterAgent", copilot: "Stop", droid: "Stop" };
5250
+ var FINISH_EVENT = { claude: "Stop", codex: "Stop", cursor: "stop", gemini: "AfterAgent", copilot: "Stop", droid: "Stop", kiro: "Stop", antigravity: "Stop" };
5188
5251
  function packageVersion() {
5189
5252
  const require3 = createRequire2(import.meta.url);
5190
5253
  return String(require3("../package.json").version);
@@ -5214,6 +5277,8 @@ function hookConfigPath(agent, scope, root, home, env = process.env) {
5214
5277
  if (agent === "gemini") return path9.join(scope === "global" ? home : root, ".gemini", "settings.json");
5215
5278
  if (agent === "copilot") return path9.join(scope === "global" ? env.COPILOT_HOME || path9.join(home, ".copilot") : path9.join(root, ".github"), "hooks", "agent-roast.json");
5216
5279
  if (agent === "droid") return path9.join(scope === "global" ? home : root, ".factory", "hooks.json");
5280
+ if (agent === "kiro") return path9.join(scope === "global" ? home : root, ".kiro", "hooks", "agent-roast.json");
5281
+ if (agent === "antigravity") return path9.join(scope === "global" ? path9.join(home, ".gemini", "config") : path9.join(root, ".agents"), "hooks.json");
5217
5282
  return path9.join(scope === "global" ? env.CODEX_HOME || path9.join(home, ".codex") : path9.join(root, ".codex"), "hooks.json");
5218
5283
  }
5219
5284
  function readJsonFile(file) {
@@ -5250,21 +5315,34 @@ function writeJsonFile(link, json) {
5250
5315
  }
5251
5316
  var asArray = (value) => Array.isArray(value) ? value : [];
5252
5317
  var isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
5318
+ var containerKey = (agent) => agent === "antigravity" ? "agent-roast" : "hooks";
5253
5319
  function eventMap(agent, data, create) {
5254
5320
  if (agent === "droid") return data;
5255
- if (create && !isObject(data.hooks)) data.hooks = {};
5256
- return isObject(data.hooks) ? data.hooks : void 0;
5321
+ const key = containerKey(agent);
5322
+ if (create && !isObject(data[key])) data[key] = {};
5323
+ return isObject(data[key]) ? data[key] : void 0;
5257
5324
  }
5258
- var isFlat = (agent) => agent === "cursor" || agent === "copilot";
5325
+ var isFlat = (agent) => agent === "cursor" || agent === "copilot" || agent === "antigravity";
5259
5326
  function addStopHook(agent, data, command) {
5260
5327
  const before = JSON.stringify(data);
5328
+ if (agent === "kiro") {
5329
+ data.version ??= "v1";
5330
+ const hooks = asArray(data.hooks);
5331
+ const ours = hooks.find((hook) => isOurs(hook?.action?.command));
5332
+ if (ours) ours.action.command = command;
5333
+ else hooks.push({ name: "agent-roast", trigger: FINISH_EVENT.kiro, action: { type: "command", command }, timeout: TIMEOUT_SECONDS });
5334
+ data.hooks = hooks;
5335
+ return JSON.stringify(data) !== before;
5336
+ }
5261
5337
  const events = eventMap(agent, data, true), event = FINISH_EVENT[agent];
5262
5338
  if (isFlat(agent)) {
5263
- data.version ??= 1;
5339
+ if (agent !== "antigravity") data.version ??= 1;
5264
5340
  const handlers = asArray(events[event]);
5265
5341
  const ours = handlers.find((handler) => isOurs(handler?.command));
5266
5342
  if (ours) ours.command = command;
5267
- else handlers.push(agent === "cursor" ? { command, timeout: TIMEOUT_SECONDS, loop_limit: 2 } : { type: "command", command, timeoutSec: TIMEOUT_SECONDS });
5343
+ else handlers.push(
5344
+ agent === "cursor" ? { command, timeout: TIMEOUT_SECONDS, loop_limit: 2 } : agent === "antigravity" ? { type: "command", command, timeout: TIMEOUT_SECONDS } : { type: "command", command, timeoutSec: TIMEOUT_SECONDS }
5345
+ );
5268
5346
  events[event] = handlers;
5269
5347
  } else {
5270
5348
  const groups = asArray(events[event]);
@@ -5277,6 +5355,12 @@ function addStopHook(agent, data, command) {
5277
5355
  }
5278
5356
  function removeStopHook(agent, data) {
5279
5357
  const before = JSON.stringify(data);
5358
+ if (agent === "kiro") {
5359
+ const kept = asArray(data.hooks).filter((hook) => !isOurs(hook?.action?.command));
5360
+ if (kept.length) data.hooks = kept;
5361
+ else delete data.hooks;
5362
+ return JSON.stringify(data) !== before;
5363
+ }
5280
5364
  const events = eventMap(agent, data, false), event = FINISH_EVENT[agent];
5281
5365
  if (!events) return false;
5282
5366
  if (isFlat(agent)) {
@@ -5289,7 +5373,7 @@ function removeStopHook(agent, data) {
5289
5373
  if (kept.length) events[event] = kept;
5290
5374
  else delete events[event];
5291
5375
  }
5292
- if (events !== data && !Object.keys(events).length) delete data.hooks;
5376
+ if (events !== data && !Object.keys(events).length) delete data[containerKey(agent)];
5293
5377
  return JSON.stringify(data) !== before;
5294
5378
  }
5295
5379
  function context(options) {
@@ -5319,16 +5403,19 @@ function uninstallHooks(options) {
5319
5403
  function hookStatus(options) {
5320
5404
  const { root, home, env } = context({ ...options, scope: "project" });
5321
5405
  const rows = [];
5322
- for (const agent of ["claude", "codex", "cursor", "gemini", "copilot", "droid"]) {
5406
+ for (const agent of ["claude", "codex", "cursor", "gemini", "copilot", "droid", "kiro", "antigravity"]) {
5323
5407
  for (const scope of ["project", "local", "global"]) {
5324
5408
  if (scope === "local" && agent !== "claude") continue;
5325
5409
  const file = hookConfigPath(agent, scope, root, home, env);
5326
5410
  let command;
5327
5411
  try {
5328
5412
  const data = readJsonFile(file).data;
5329
- const listed = asArray(eventMap(agent, data, false)?.[FINISH_EVENT[agent]]);
5330
- const handlers = isFlat(agent) ? listed : listed.flatMap((group) => asArray(group?.hooks));
5331
- command = handlers.map((handler) => handler?.command).find(isOurs);
5413
+ if (agent === "kiro") command = asArray(data.hooks).map((hook) => hook?.action?.command).find(isOurs);
5414
+ else {
5415
+ const listed = asArray(eventMap(agent, data, false)?.[FINISH_EVENT[agent]]);
5416
+ const handlers = isFlat(agent) ? listed : listed.flatMap((group) => asArray(group?.hooks));
5417
+ command = handlers.map((handler) => handler?.command).find(isOurs);
5418
+ }
5332
5419
  } catch {
5333
5420
  }
5334
5421
  rows.push({ agent, scope, file, command, installed: Boolean(command), advisory: isAdvisory(command), action: command ? "unchanged" : "absent" });
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  terminalText,
39
39
  uninstallHooks,
40
40
  writeBaseline
41
- } from "./chunk-L4XLCXC2.js";
41
+ } from "./chunk-ELOEHPEC.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, Gemini CLI, GitHub Copilot, Factory Droid)`,
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, Kiro, Antigravity CLI)`,
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, Gemini CLI, GitHub Copilot, Factory Droid)");
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, Kiro, Antigravity CLI)");
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,16 @@ 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, 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) => {
528
+ hook.command("install").description("Add the hook to Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid, Kiro or Antigravity CLI 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", 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).");
533
+ if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(
534
+ root,
535
+ { claude: ".claude", codex: ".codex", cursor: ".cursor", gemini: ".gemini", copilot: ".github/hooks", droid: ".factory", kiro: ".kiro", antigravity: ".agents" }[agent]
536
+ )));
537
+ if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor, gemini, copilot, droid, kiro, antigravity).");
535
538
  const scope = opts.global ? "global" : opts.local ? "local" : "project";
536
539
  const advisory = opts.advisory || program.opts().advisory === true;
537
540
  const changes = installHooks({ cwd: process.cwd(), agents, scope, command: opts.command, advisory, dryRun: opts.dryRun });
@@ -545,6 +548,8 @@ hook.command("install").description("Add the hook to Claude Code, Codex, Cursor,
545
548
  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
549
  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
550
  if (agents.includes("droid")) console.log("\nFactory Droid takes a snapshot of its hooks at startup: restart it, and review the hook with /hooks.");
551
+ if (agents.includes("kiro")) console.log("\nKiro reads hooks when a session starts: restart it. Its Stop hook sends the agent back on any error; there is no documented way to tell a first try from a retry, so every run until the agent fixes it sends the agent back again.");
552
+ if (agents.includes("antigravity")) console.log("\nAntigravity CLI reads hooks when a session starts: restart it, or run /hooks to see what is loaded. --advisory is a one-line warning in its log, not a message to you inside the session: Antigravity CLI documents no channel for that.");
548
553
  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");
549
554
  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");
550
555
  } catch (error) {
@@ -552,7 +557,7 @@ hook.command("install").description("Add the hook to Claude Code, Codex, Cursor,
552
557
  process.exitCode = 2;
553
558
  }
554
559
  });
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) => {
560
+ hook.command("uninstall").description("Remove the hook from Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Factory Droid, Kiro or Antigravity CLI").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) => {
556
561
  try {
557
562
  if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
558
563
  const agents = names.length ? parseAgents(names) : [...HOOK_AGENTS].filter((agent) => !(opts.local && agent !== "claude"));
@@ -572,7 +577,7 @@ hook.command("status").description("Show where the hook is installed").action(as
572
577
  const paused = await hookPausedUntil(process.cwd());
573
578
  if (paused) console.log(`
574
579
  Paused in this repository until ${paused.toLocaleString()} (agent-roast hook resume).`);
575
- if (!rows.some((row) => row.installed)) console.log("\n Install it with: agent-roast hook install claude (or codex, cursor, gemini, copilot, droid)");
580
+ if (!rows.some((row) => row.installed)) console.log("\n Install it with: agent-roast hook install claude (or codex, cursor, gemini, copilot, droid, kiro, antigravity)");
576
581
  } catch (error) {
577
582
  console.error(`agent-roast hook status error: ${errorMessage(error)}`);
578
583
  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" | "gemini" | "copilot" | "droid";
478
+ type HookAgent = "claude" | "codex" | "cursor" | "gemini" | "copilot" | "droid" | "kiro" | "antigravity";
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-L4XLCXC2.js";
95
+ } from "./chunk-ELOEHPEC.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.23.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.",
3
+ "version": "0.25.0",
4
+ "description": "Stops AI agents (and rushed humans) weakening tests, coverage gates or CI to get a green build. Hook for 8 coding agents, pre-commit, GitHub Action. Offline.",
5
5
  "mcpName": "io.github.Nari-Ab/agent-roast",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",