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 +4 -2
- package/dist/{chunk-L4XLCXC2.js → chunk-ELOEHPEC.js} +108 -21
- package/dist/cli.js +13 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
- **
|
|
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) · [](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)
|
|
4841
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
5256
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
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-
|
|
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
|
|
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(
|
|
534
|
-
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-roast",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Stops AI agents (and rushed humans) weakening tests, coverage gates or CI to get a green build. Hook for
|
|
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",
|