agent-roast 0.20.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 +11 -8
- package/dist/{chunk-BNDNXKBM.js → chunk-HDC5C2D3.js} +181 -71
- package/dist/cli.js +12 -9
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/package.json +4 -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 and
|
|
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
|
|
@@ -80,8 +80,8 @@ Policy is read from the **base** branch, so a pull request cannot switch off the
|
|
|
80
80
|
| --- | --- | :-: |
|
|
81
81
|
| `assertion-weakened` | an exact check is replaced by a weaker one: `toBe(42)` → `toBeDefined()`, `toBe(2)` → `toBeLessThanOrEqual(2)`, `toBe(2)` → `not.toBe(0)`, `toEqual(x)` → `toMatchObject(x)`, `assert x == 1` → `assert x` | blocks |
|
|
82
82
|
| `guard-weakened` | a coverage threshold is lowered, `strict` is switched off, a lint rule is downgraded, a CI step may fail, a workflow stops running tests, a `conftest.py` hook sets a report to `passed` | blocks |
|
|
83
|
-
| `test-skip` | a test that was already there is switched off or expected to fail: `it.skip`, `@pytest.mark.skip`, `@pytest.mark.xfail`, `it.fails`, `test.fixme`, `t.Skip`, `#[ignore]`, `@Disabled`, `[Ignore]`, `xit`, ... (a new test that is skipped is not reported) | blocks |
|
|
84
|
-
| `test-focus`, `trivial-assert` | `.only` / `fit`; `expect(true).toBe(true)` and its relatives in ten languages
|
|
83
|
+
| `test-skip` | a test that was already there is switched off or expected to fail: `it.skip`, `@pytest.mark.skip`, `@pytest.mark.skipif(True)`, `@pytest.mark.xfail`, `it.fails`, `test.fixme`, `t.Skip`, `#[ignore]`, `@Disabled`, `[Ignore]`, `xit`, ... (a new test that is skipped is not reported) | blocks |
|
|
84
|
+
| `test-focus`, `trivial-assert` | `.only` / `fit`; `expect(true).toBe(true)` and its relatives in ten languages, `assert x or True`; a test that asserts on a function it mocked itself | blocks |
|
|
85
85
|
| `assertion-removed`, `test-removed`, `expectation-rewritten` | a test loses assertions, disappears, or its expected value is edited next to production code | warns |
|
|
86
86
|
| `swallowed-error`, `type-escape`, `stub-abandonment` | `catch {}` / `except: pass`, `as any` / `# type: ignore` / `# pragma: no cover` / `istanbul ignore`, `throw new Error("Not implemented")` | warns |
|
|
87
87
|
|
|
@@ -112,15 +112,18 @@ 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
|
+
- **162 of 162** hand-labeled cheating edits are caught and **150 of 150** 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
|
+
- **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,
|
|
119
|
+
an edited expected value as a warning) and none of its six honest solutions. The five that are not (special-casing, swallowed errors, overfitting) live in the code, and a search of 126,486 commits shows why a tool that reads text cannot report them:
|
|
120
|
+
their shape is the shape of ordinary code ([section 13](docs/benchmarks.md#13-a-probe-of-what-no-rule-sees-and-a-catalog-of-cheats-that-someone-else-wrote)).
|
|
118
121
|
- **12,645 commits** that touch tests or CI/config in 166 public repositories: blocking rules fired on 0.3% of AI-signed commits
|
|
119
122
|
(Claude, Copilot, Cursor, Codex, Devin, aider) and on 0.3% of the others. Inside the same repositories the odds for an
|
|
120
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
|
|
121
124
|
checks more often.
|
|
122
|
-
- **On repositories it was not written from, between a quarter and two thirds of the blocked commits were false alarms.**
|
|
123
|
-
or 40 public repositories (
|
|
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
|
|
124
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
|
|
125
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
|
|
126
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
|
|
@@ -128,8 +131,8 @@ two thirds of its blocks were questions with a harmless answer, depending on the
|
|
|
128
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
|
|
129
132
|
earlier measurements too, were deliberate and explained. One reader (an AI assistant) sorted them, and the fixes were made from the same
|
|
130
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
|
|
131
|
-
repository's lint configuration);
|
|
132
|
-
[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)).
|
|
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)).
|
|
133
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
|
|
134
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);
|
|
135
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
|
|
@@ -1167,7 +1167,9 @@ var PYTHON_TEST_SKIP_PATTERNS = [
|
|
|
1167
1167
|
// A test that is expected to fail cannot fail the run, which is what a skip does too.
|
|
1168
1168
|
{ regex: /(?<![\w.])pytest\s*\.\s*xfail\s*\(/, name: "imperative pytest xfail (pytest.xfail)", imperative: true },
|
|
1169
1169
|
// `@unittest.expectedFailure`, and the same decorator under another module's name (`@test.expectedFailure` of a test framework that re-exports it).
|
|
1170
|
-
{ regex: /@(?:[A-Za-z_]\w*\s*\.\s*)*expectedFailure\b/, name: "test expected to fail (@unittest.expectedFailure)", imperative: false }
|
|
1170
|
+
{ regex: /@(?:[A-Za-z_]\w*\s*\.\s*)*expectedFailure\b/, name: "test expected to fail (@unittest.expectedFailure)", imperative: false },
|
|
1171
|
+
// `skipif(True)` is a skip whose condition is always true: it switches the test off wherever it runs, which `skipif` with a condition on the environment does not.
|
|
1172
|
+
{ regex: /(?:@\s*|(?<![\w.])marks\s*=\s*\[?\s*|^[ \t]*pytestmark\s*=\s*\[?\s*)(?:pytest\s*\.\s*mark\s*\.\s*skipif|(?:unittest\s*\.\s*)?skipIf)\s*\(\s*True\s*[,)]/, name: "unconditionally skipped test (skipif(True))", imperative: false }
|
|
1171
1173
|
];
|
|
1172
1174
|
var PYTHON_MARKER = /(@\s*|(?<![\w.])marks\s*=\s*\[?\s*|^[ \t]*pytestmark\s*=\s*\[?\s*)pytest\s*\.\s*mark\s*\.\s*(skip|xfail)\b/gm;
|
|
1173
1175
|
function isConditionalXfail(args) {
|
|
@@ -1178,20 +1180,20 @@ function isConditionalXfail(args) {
|
|
|
1178
1180
|
return !/\bCI\b|\bisCI\b|GITHUB_ACTIONS/.test(condition);
|
|
1179
1181
|
}
|
|
1180
1182
|
function isGuardedSkip(lines) {
|
|
1181
|
-
const
|
|
1183
|
+
const indentOf2 = (text) => text.length - text.trimStart().length;
|
|
1182
1184
|
const mentionsCi = (raw) => /\bCI\b|\bGITHUB_ACTIONS\b/.test(raw);
|
|
1183
1185
|
const own = lines[lines.length - 1];
|
|
1184
1186
|
if (/^\s*(?:if|elif|else)\b.*:\s*\S/.test(own.code)) return !mentionsCi(own.raw);
|
|
1185
|
-
let limit =
|
|
1187
|
+
let limit = indentOf2(own.code);
|
|
1186
1188
|
for (let i = lines.length - 2; i >= 0 && lines.length - 1 - i <= 200; i--) {
|
|
1187
1189
|
if (lines[i].number !== lines[i + 1].number - 1) return false;
|
|
1188
1190
|
const { code, raw } = lines[i];
|
|
1189
|
-
if (!code.trim() ||
|
|
1191
|
+
if (!code.trim() || indentOf2(code) >= limit) continue;
|
|
1190
1192
|
const keyword = /^\s*(async\s+def|def|class|if|elif|else|try|except|finally|with|for|while)\b/.exec(code)?.[1];
|
|
1191
1193
|
if (keyword === "def" || keyword === "async def") return !/^\s*(?:async\s+)?def\s+test/.test(code);
|
|
1192
1194
|
if (keyword === "class") return false;
|
|
1193
1195
|
if (keyword === "if" || keyword === "elif" || keyword === "else" || keyword === "try" || keyword === "except" || keyword === "finally") return !mentionsCi(raw);
|
|
1194
|
-
limit =
|
|
1196
|
+
limit = indentOf2(code) + (keyword ? 0 : 1);
|
|
1195
1197
|
}
|
|
1196
1198
|
return false;
|
|
1197
1199
|
}
|
|
@@ -1289,7 +1291,7 @@ function inTestAttributeList(lines, index) {
|
|
|
1289
1291
|
return false;
|
|
1290
1292
|
}
|
|
1291
1293
|
function isGuardedBlockSkip(lines, profile) {
|
|
1292
|
-
const
|
|
1294
|
+
const indentOf2 = (text) => text.length - text.trimStart().length;
|
|
1293
1295
|
const mentionsCi = (raw) => /\bCI\b|\bGITHUB_ACTIONS\b/.test(raw);
|
|
1294
1296
|
const own = lines[lines.length - 1];
|
|
1295
1297
|
if (profile.modifier?.test(own.code) || profile.conditional.test(own.code)) return !mentionsCi(own.raw);
|
|
@@ -1303,17 +1305,17 @@ function isGuardedBlockSkip(lines, profile) {
|
|
|
1303
1305
|
}
|
|
1304
1306
|
return depth > 0;
|
|
1305
1307
|
};
|
|
1306
|
-
let limit =
|
|
1308
|
+
let limit = indentOf2(own.code);
|
|
1307
1309
|
let i = lines.length - 2;
|
|
1308
1310
|
for (; i >= 0 && lines.length - 1 - i <= 200; i--) {
|
|
1309
1311
|
if (lines[i].number !== lines[i + 1].number - 1) return compiledConditionally(i + 1);
|
|
1310
1312
|
const { code, raw } = lines[i];
|
|
1311
|
-
if (!code.trim() ||
|
|
1313
|
+
if (!code.trim() || indentOf2(code) >= limit) continue;
|
|
1312
1314
|
if (profile.preprocessor && (profile.preprocessor.open.test(code) || profile.preprocessor.close.test(code) || profile.preprocessor.other.test(code))) continue;
|
|
1313
1315
|
if (profile.conditional.test(code)) return !mentionsCi(raw);
|
|
1314
1316
|
if (profile.test.test(code)) return compiledConditionally(i + 1);
|
|
1315
1317
|
if (profile.helper.test(code)) return true;
|
|
1316
|
-
limit =
|
|
1318
|
+
limit = indentOf2(code);
|
|
1317
1319
|
}
|
|
1318
1320
|
return compiledConditionally(i + 1);
|
|
1319
1321
|
}
|
|
@@ -1331,7 +1333,7 @@ function isConditionalSkip(source, raw, open2, start) {
|
|
|
1331
1333
|
var TEST_REGISTRATION = /^(?:await\s+)?(?:test|it|describe|suite|beforeAll|beforeEach|afterAll|afterEach)\b/;
|
|
1332
1334
|
function isUnderRuntimeCondition(source, raw, start) {
|
|
1333
1335
|
const alwaysTrue = (text) => /^\s*(?:true|1|!\s*0|!\s*false)\s*$/.test(text);
|
|
1334
|
-
const
|
|
1336
|
+
const indentOf2 = (text) => text.length - text.trimStart().length;
|
|
1335
1337
|
const lineStartOf = (offset) => Math.max(source.lastIndexOf("\n", offset - 1), source.lastIndexOf("\0", offset - 1)) + 1;
|
|
1336
1338
|
const mentionsCi = (from, to) => MENTIONS_CI.test(raw.slice(from, to));
|
|
1337
1339
|
const lineStart = lineStartOf(start);
|
|
@@ -1351,20 +1353,20 @@ function isUnderRuntimeCondition(source, raw, start) {
|
|
|
1351
1353
|
} else if (/(?:^|[\s}])else$/.test(head)) return true;
|
|
1352
1354
|
else if (head.endsWith("&&")) return !mentionsCi(lineStart, start) && !alwaysTrue(head.slice(0, -2).replace(/^[\s{}]+/, ""));
|
|
1353
1355
|
const lineEnd = source.slice(start).search(/[\n\0]/);
|
|
1354
|
-
let limit =
|
|
1356
|
+
let limit = indentOf2(source.slice(lineStart, lineEnd < 0 ? void 0 : start + lineEnd)), at = lineStart;
|
|
1355
1357
|
for (let steps = 0; steps < 200 && at > 0; steps++) {
|
|
1356
1358
|
if (source[at - 1] === "\0") return false;
|
|
1357
1359
|
const previous = lineStartOf(at - 1), line = source.slice(previous, at - 1);
|
|
1358
1360
|
const lineRaw = raw.slice(previous, at - 1);
|
|
1359
1361
|
at = previous;
|
|
1360
|
-
if (!line.trim() ||
|
|
1362
|
+
if (!line.trim() || indentOf2(line) >= limit) continue;
|
|
1361
1363
|
const header = line.trim();
|
|
1362
1364
|
if (/^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(header)) {
|
|
1363
1365
|
return !MENTIONS_CI.test(lineRaw) && !alwaysTrue(/\bif\s*\((.*)\)\s*\{?\s*$/.exec(header)?.[1] ?? "");
|
|
1364
1366
|
}
|
|
1365
1367
|
if (/^(?:\}\s*)?(?:else|catch)\b|^(?:case\b|default\s*:)|^\)\s*\{?\s*$/.test(header)) return true;
|
|
1366
1368
|
if (TEST_REGISTRATION.test(header)) return false;
|
|
1367
|
-
limit =
|
|
1369
|
+
limit = indentOf2(line);
|
|
1368
1370
|
}
|
|
1369
1371
|
return false;
|
|
1370
1372
|
}
|
|
@@ -1436,6 +1438,27 @@ var PYTHON_TRIVIAL_ASSERT_PATTERNS = [
|
|
|
1436
1438
|
{ regex: /\bself\s*\.\s*assertTrue\s*\(\s*(?:True|1)\s*\)/, name: "trivial assert (self.assertTrue(True))" },
|
|
1437
1439
|
{ regex: /\bself\s*\.\s*assertEqual\s*\(\s*(?:1\s*,\s*1|True\s*,\s*True)\s*\)/, name: "trivial assert (self.assertEqual(1, 1))" }
|
|
1438
1440
|
];
|
|
1441
|
+
function assertsAlwaysTrue(code) {
|
|
1442
|
+
const body = /^\s*assert\b\s*(.*)$/.exec(code)?.[1] ?? /\bself\s*\.\s*assertTrue\s*\((.*)$/.exec(code)?.[1];
|
|
1443
|
+
if (body === void 0) return false;
|
|
1444
|
+
let depth = 0, outside2 = "";
|
|
1445
|
+
for (const character of body) {
|
|
1446
|
+
if ("([{".includes(character)) {
|
|
1447
|
+
outside2 += depth === 0 ? character : " ";
|
|
1448
|
+
depth++;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (")]}".includes(character)) {
|
|
1452
|
+
if (depth === 0) break;
|
|
1453
|
+
depth--;
|
|
1454
|
+
outside2 += depth === 0 ? character : " ";
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (depth === 0 && character === ",") break;
|
|
1458
|
+
outside2 += depth === 0 ? character : " ";
|
|
1459
|
+
}
|
|
1460
|
+
return outside2.split(/\bor\b/).some((operand) => /^\s*(?:True|1)\s*$/.test(operand));
|
|
1461
|
+
}
|
|
1439
1462
|
var JS_STUB_PATTERNS = [
|
|
1440
1463
|
{ regex: /\bthrow\s+new\s+Error\s*\(\s*["'`](?:TODO(?=\s*(?:["'`:]|implement\b))|Not implemented\b|to be implemented\b|stub\b|implement me\b)/i, name: "unimplemented stub (throw new Error('Not implemented'))" }
|
|
1441
1464
|
];
|
|
@@ -1562,19 +1585,17 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
1562
1585
|
foundEscape = true;
|
|
1563
1586
|
}
|
|
1564
1587
|
if (python2 && !isComment && isTestFile(file.path)) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
break;
|
|
1577
|
-
}
|
|
1588
|
+
const trivial = PYTHON_TRIVIAL_ASSERT_PATTERNS.find((rule) => rule.regex.test(cleanedLine))?.name ?? (assertsAlwaysTrue(cleanedLine) ? "assertion that cannot fail (assert ... or True)" : void 0);
|
|
1589
|
+
if (trivial) {
|
|
1590
|
+
infractions.push({
|
|
1591
|
+
type: "trivial-assert",
|
|
1592
|
+
file: file.path,
|
|
1593
|
+
lineNumber: added.lineNumber,
|
|
1594
|
+
lineSnippet: rawLine.trim(),
|
|
1595
|
+
commitHash,
|
|
1596
|
+
commitDate,
|
|
1597
|
+
reason: trivial
|
|
1598
|
+
});
|
|
1578
1599
|
}
|
|
1579
1600
|
}
|
|
1580
1601
|
}
|
|
@@ -3339,7 +3360,7 @@ function javascript(text) {
|
|
|
3339
3360
|
if (!binding || !mock) continue;
|
|
3340
3361
|
const name = binding.namespace ? call[2] : call[2] ? void 0 : binding.name;
|
|
3341
3362
|
if (!name) continue;
|
|
3342
|
-
const identifier = call[1].replace(/\$/g, "\\$");
|
|
3363
|
+
const identifier = (binding.namespace ? `${call[1]}\\s*\\.\\s*${call[2]}` : call[1]).replace(/\$/g, "\\$");
|
|
3343
3364
|
const configures = new RegExp(`(?<![\\w$.])${identifier}\\b[^;]{0,80}?\\.\\s*mock(?:Return|Resolved|Rejected|Implementation)\\w*\\s*\\(`, "g");
|
|
3344
3365
|
const configured = [...raw.matchAll(configures)].some((config) => tests.every((range) => !within(range, config.index) || within(range, match.index)));
|
|
3345
3366
|
if (!mock.fake && !configured) continue;
|
|
@@ -3605,10 +3626,14 @@ function analyzePythonConfig(file, oldText, newText, report) {
|
|
|
3605
3626
|
/^\s*python_version\s*=\s*["']?(\d+(?:\.\d+)*)/gm,
|
|
3606
3627
|
/\bruff\s*(?:(?:===|==|~=|>=|>)\s*["']?|=\s*["'][\^~>=]*)(\d+(?:\.\d+)*)/g
|
|
3607
3628
|
];
|
|
3629
|
+
const effective = (text) => newest(text, versions[1]) ?? newest(text, versions[0]);
|
|
3608
3630
|
const bumped = versions.some((pattern) => {
|
|
3609
3631
|
const was2 = newest(oldText, pattern), now = newest(newText, pattern);
|
|
3610
3632
|
return !!was2 && !!now && higher(was2, now);
|
|
3611
|
-
})
|
|
3633
|
+
}) || (() => {
|
|
3634
|
+
const was2 = effective(oldText), now = effective(newText);
|
|
3635
|
+
return !!was2 && !!now && higher(was2, now);
|
|
3636
|
+
})();
|
|
3612
3637
|
for (const [tool, bucket] of ignored(after)) {
|
|
3613
3638
|
if (!configured.has(tool) || tool === "ruff" && bumped) continue;
|
|
3614
3639
|
const old = [...was.get(tool)?.keys() ?? []];
|
|
@@ -3713,13 +3738,13 @@ function testBodies(text, language) {
|
|
|
3713
3738
|
const match = startPattern.exec(line);
|
|
3714
3739
|
if (match) starts.push({ index, name: match[1], length: match[0].length });
|
|
3715
3740
|
});
|
|
3716
|
-
const
|
|
3741
|
+
const indentOf2 = (line) => /^\s*/.exec(line)[0].length;
|
|
3717
3742
|
return starts.map((start, n) => {
|
|
3718
3743
|
const limit = n + 1 < starts.length ? starts[n + 1].index : lines.length;
|
|
3719
|
-
const header = lines[start.index], indent =
|
|
3744
|
+
const header = lines[start.index], indent = indentOf2(header);
|
|
3720
3745
|
let end = start.index + 1, depth = balance(header);
|
|
3721
3746
|
while (depth > 0 && end < limit) depth += balance(lines[end++]);
|
|
3722
|
-
while (end < limit && (language === "go" ? lines[end - 1] !== "}" : !lines[end].trim() ||
|
|
3747
|
+
while (end < limit && (language === "go" ? lines[end - 1] !== "}" : !lines[end].trim() || indentOf2(lines[end]) > indent)) end++;
|
|
3723
3748
|
let top = start.index;
|
|
3724
3749
|
while (top > 0 && start.index - top < 200 && lines[top - 1].trim() && !/^\s*(?:async\s+)?(?:def|class)\b/.test(lines[top - 1])) top--;
|
|
3725
3750
|
return {
|
|
@@ -4088,7 +4113,9 @@ function isWeaker(rawOld, rawNew, language) {
|
|
|
4088
4113
|
const oldBare = unittestAsAssert(oldAssertion) ?? oldAssertion;
|
|
4089
4114
|
if (WEAK_PY.test(oldAssertion) || BOUND_PY.test(oldAssertion) || WEAK_PY.test(oldBare) || BOUND_PY.test(oldBare) || !PY_SPECIFIC.test(oldAssertion)) return false;
|
|
4090
4115
|
const limit = /(?:>=|<=|>|<)\s*([^\s,)]+)/.exec(oldAssertion)?.[1];
|
|
4091
|
-
|
|
4116
|
+
const outsideBrackets = oldAssertion.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, "").replace(/[([{][^()[\]{}]*[)\]}]/g, " ").replace(/[([{][^()[\]{}]*[)\]}]/g, " ").replace(/[([{][^()[\]{}]*[)\]}]/g, " ");
|
|
4117
|
+
const expectsNothing = EMPTY_PY.test(oldAssertion) && !/\b(?:and|or)\b/.test(outsideBrackets);
|
|
4118
|
+
return WEAK_PY.test(newAssertion) && !expectsNothing || BOUND_PY.test(newAssertion) && !EMPTY_PY.test(oldAssertion) && /==|[<>]=?/.test(oldAssertion) && (limit === void 0 || /^-?\d+(?:\.\d+)?$/.test(limit));
|
|
4092
4119
|
}
|
|
4093
4120
|
return false;
|
|
4094
4121
|
}
|
|
@@ -4346,7 +4373,10 @@ function newTestMarkers(before, after, oldText, newText) {
|
|
|
4346
4373
|
if (nth <= had || had === 0 && (nth > 1 && !test.key.startsWith("js:") || !existed(test))) continue;
|
|
4347
4374
|
found.push(finding2);
|
|
4348
4375
|
}
|
|
4349
|
-
|
|
4376
|
+
const plain = (lines) => lines.map((line) => line.trim().replace(/\.(?:skip|fails|failing|todo|fixme|only)\b/g, ""));
|
|
4377
|
+
const oldPlain = plain(oldLines).join("\n"), newPlain = plain(newLines);
|
|
4378
|
+
const newTable = (finding2) => /\.each\b/.test(finding2.lineSnippet ?? "") && !oldPlain.includes(newPlain.slice((finding2.lineNumber ?? 1) - 1, (finding2.lineNumber ?? 1) + 3).join("\n"));
|
|
4379
|
+
return [...found, ...unnamed.slice(unnamedBefore).filter((finding2) => !newTable(finding2))];
|
|
4350
4380
|
}
|
|
4351
4381
|
var GUARD_FILE = /(?:^|\/)(?:package\.json|tsconfig[^/]*\.json|jsconfig\.json|(?:jest|vitest|vite)\.config\.[cm]?[jt]s|\.eslintrc(?:\.[a-z]+)?|eslint\.config\.[cm]?[jt]s|pyproject\.toml|setup\.cfg|pytest\.ini|tox\.ini|\.?ruff\.toml|\.flake8|\.?mypy\.ini|\.?pylintrc|\.coveragerc|codecov\.ya?ml|\.gitlab-ci\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml)$/i;
|
|
4352
4382
|
function isGuardFile(file) {
|
|
@@ -4384,8 +4414,68 @@ var CI_RUNNER = new RegExp(
|
|
|
4384
4414
|
String.raw`|(?<![\w./-])(?:deno\s+(?:task|run)|just|task|poe|rake|composer(?:\s+run)?|mix|sbt|lein|stack|cabal|(?:\./)?(?:gradlew|mvnw)(?:\.bat|\.cmd)?)\s+(?:-\S+\s+)*test(?![\w/-])` + String.raw`|(?<![\w./-])(?:node|tsx)(?:\s+-\S+)*\s+--test(?![\w-])` + // A project's own test script, run through its interpreter or by path: `python scripts/run_tests.py`, `./scripts/test.sh`, `python tests/test_cli.py`.
|
|
4385
4415
|
String.raw`|(?<![\w./-])(?:(?:python[\d.]*|node|bash|sh|zsh|ruby|pwsh|tsx|ts-node)(?:\s+-\S+)*\s+|\./)(?:[\w.-]+/)*(?:(?:run[_-]?)?tests?(?:[_-](?:all|suite|runner|unit|ci))?|test_[\w-]+|[\w-]+_test)\.(?:py|sh|bash|js|mjs|cjs|ts|rb|ps1)(?![\w-])` + String.raw`|\brun:.*(?<=\s)(?:tests?|unit|e2e)(?![\w/.-])`
|
|
4386
4416
|
);
|
|
4387
|
-
var SHELL_TEST = /(?:^|run:\s*|[;&|({]\s*|\b(?:if|then|do|else|elif|while|until)\s+|!\s+)test\s+!?\s
|
|
4388
|
-
var
|
|
4417
|
+
var SHELL_TEST = /(?:^|run:\s*|[;&|({]\s*|\b(?:if|then|do|else|elif|while|until)\s+|!\s+)test\s+!?\s*(?:-[bcdefghkLnOGprsStuwxzN](?=\s)|["$'])/g;
|
|
4418
|
+
var NOT_TESTS = new RegExp(
|
|
4419
|
+
String.raw`(?<![\w./-])(?:uv\s+(?:sync|add|tool\s+install|pip\s+install)|(?:python[\d.]*\s+-m\s+)?pip[\d.]*\s+install|pipx\s+install|poetry\s+(?:install|add)|pdm\s+(?:install|sync|add)|npm\s+(?:ci|i|install|add)|pnpm\s+(?:i|install|add)|bun\s+(?:i|install|add)|yarn\s+(?:install|add)|bundle\s+install|gem\s+install|cargo\s+install|go\s+(?:install|get)|brew\s+install|apt(?:-get)?\s+install|conda\s+(?:install|create))\b[^&;|\n]*` + // The gap before the environment is bounded, so that a line of a hundred thousand characters with `tox` in it again and again is read in linear time.
|
|
4420
|
+
String.raw`|(?<![\w./-])(?:tox|nox)\b[^&;|\n]{0,300}?\s(?:-e|-s|--env|--session)[= ]\s*(?:lint|linting|format|fmt|docs?|types?|typing|mypy|pyright|ruff|black|flake8|pylint|style|isort|bandit|build|package|clean)(?![,\w-])[^&;|\n]*`,
|
|
4421
|
+
"g"
|
|
4422
|
+
);
|
|
4423
|
+
var ciRuns = (line) => !/^\s*(?:-\s+)?name\s*:/.test(line) && CI_RUNNER.test(line.replace(SHELL_TEST, " ").replace(NOT_TESTS, " ").replace(/\s#.*$/, ""));
|
|
4424
|
+
var COMMAND_BLOCK = /^(\s*(?:-\s+)?)(run|script|before_script|after_script)\s*:\s*([|>])?[+-]?\d*\s*(?:#.*)?$/;
|
|
4425
|
+
var indentOf = (line) => line.length - line.trimStart().length;
|
|
4426
|
+
var PRINTS = /^(?:echo|printf)\b/;
|
|
4427
|
+
var NOT_A_TOOL = "mkdir|rmdir|cd|rm|ls|cat|cp|mv|touch|chmod|chown|echo|printf|export|source|git|docker|find|tar|zip|unzip|grep|sed|awk|diff|head|tail|wc|stat|du|tree|set|test|curl|wget|ssh|scp|rsync|ping|open|sleep|kill|for|while|until|if|elif|else|then|do|done|case|in|assert|return|import|from|def|class|print|raise|yield|with|local|declare|readonly|unset|eval|await|let|const|var|function|new|delete|throw|and|or|not|is|lambda|try|except|finally|pass|del";
|
|
4428
|
+
var TOOL_TEST = new RegExp(
|
|
4429
|
+
String.raw`^(?:(?:sudo|time|exec|command|env)(?:\s+-\S+)*\s+|\w+=(?:"[^"]*"|'[^']*'|[^\s"']*)\s+)*(?!(?:${NOT_A_TOOL})\b)(?!-)[\w./-]+\s+test(?![\w/.=\[(-])`
|
|
4430
|
+
);
|
|
4431
|
+
function blockCommands(lines, at, key) {
|
|
4432
|
+
const keyIndent = key[1].length, found = [];
|
|
4433
|
+
if (key[3] === ">") {
|
|
4434
|
+
for (let next = at + 1; next < lines.length; next++) {
|
|
4435
|
+
const text = lines[next].trim();
|
|
4436
|
+
if (!text) continue;
|
|
4437
|
+
if (indentOf(lines[next]) <= keyIndent) break;
|
|
4438
|
+
if (!text.startsWith("#")) found.push(text);
|
|
4439
|
+
}
|
|
4440
|
+
return [found.join(" ")];
|
|
4441
|
+
}
|
|
4442
|
+
if (key[3] === "|") {
|
|
4443
|
+
for (let next = at + 1; next < lines.length; next++) {
|
|
4444
|
+
const text = lines[next].trim().replace(/\s*\\$/, "");
|
|
4445
|
+
if (!text) continue;
|
|
4446
|
+
if (indentOf(lines[next]) <= keyIndent) break;
|
|
4447
|
+
if (TOOL_TEST.test(text)) found.push(text);
|
|
4448
|
+
}
|
|
4449
|
+
return found;
|
|
4450
|
+
}
|
|
4451
|
+
if (key[3] || key[2] === "run") return [];
|
|
4452
|
+
let itemIndent = -1;
|
|
4453
|
+
for (let next = at + 1; next < lines.length; next++) {
|
|
4454
|
+
const text = lines[next].trim();
|
|
4455
|
+
if (!text || text.startsWith("#")) continue;
|
|
4456
|
+
const indent = indentOf(lines[next]);
|
|
4457
|
+
if (itemIndent < 0) {
|
|
4458
|
+
if (!text.startsWith("- ") || indent < keyIndent) break;
|
|
4459
|
+
itemIndent = indent;
|
|
4460
|
+
}
|
|
4461
|
+
if (indent < itemIndent || indent === itemIndent && !text.startsWith("- ")) break;
|
|
4462
|
+
if (indent === itemIndent) found.push(text.slice(2).trim());
|
|
4463
|
+
}
|
|
4464
|
+
return found;
|
|
4465
|
+
}
|
|
4466
|
+
function ciTestEntries(text) {
|
|
4467
|
+
const lines = text.split(/\r?\n/), found = [];
|
|
4468
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4469
|
+
const line = lines[i];
|
|
4470
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
4471
|
+
if (ciRuns(line)) found.push({ index: i, command: line });
|
|
4472
|
+
const key = COMMAND_BLOCK.exec(line);
|
|
4473
|
+
if (!key) continue;
|
|
4474
|
+
for (const command of blockCommands(lines, i, key)) if (command && !PRINTS.test(command) && !ciRuns(command) && ciRuns(`run: ${command}`)) found.push({ index: i, command });
|
|
4475
|
+
}
|
|
4476
|
+
return found;
|
|
4477
|
+
}
|
|
4478
|
+
var ciTestLines = (text) => ciTestEntries(text).map((entry) => entry.command);
|
|
4389
4479
|
var CI_FILE = /(?:\.github\/workflows\/|\.gitlab-ci)/;
|
|
4390
4480
|
var CI_REUSABLE = /^\s*-?\s*uses:\s*\S*\.github\/workflows\/\S+/;
|
|
4391
4481
|
var CI_GATE = /^\s*(?:pull_request|pull_request_target|push|merge_group)\s*:|^\s*on\s*:\s*(?:\[[^\]]*\b(?:push|pull_request|pull_request_target|merge_group)\b[^\]]*\]|(?:push|pull_request|pull_request_target|merge_group)\s*$)|^\s*-\s*(?:push|pull_request|pull_request_target|merge_group)\s*$/m;
|
|
@@ -4421,10 +4511,14 @@ function triggerSection(text) {
|
|
|
4421
4511
|
while (end < lines.length && (!lines[end].trim() || /^[\s#-]/.test(lines[end]))) end++;
|
|
4422
4512
|
return lines.slice(start, end).join("\n");
|
|
4423
4513
|
}
|
|
4424
|
-
function gatingTestCommands(file, text) {
|
|
4514
|
+
function gatingTestCommands(file, text, skip) {
|
|
4425
4515
|
if (!(/\.gitlab-ci/.test(file) || CI_GATE.test(gatingTriggers(text) ?? text))) return [];
|
|
4426
4516
|
const directories = [...new Set([...text.matchAll(/^\s*(?:-\s+)?working-directory\s*:\s*["']?([^\s"'#]+)/gm)].map((match) => match[1]))].sort().join(",");
|
|
4427
|
-
return text
|
|
4517
|
+
return ciTestEntries(text).filter((entry) => !skip?.(entry.index)).map((entry) => `${entry.command.replace(/\s#.*$/, "").replace(/^\s*-?\s*(?:run\s*:\s*)?/, "").replace(/\s+/g, " ").trim()} @${directories}`);
|
|
4518
|
+
}
|
|
4519
|
+
function enforcedTestCommands(file, text) {
|
|
4520
|
+
const tolerated = [...lenientSteps(text).values()].filter((step) => step.runsChecks).flatMap((step) => step.ranges);
|
|
4521
|
+
return gatingTestCommands(file, text, (line) => tolerated.some(([from, to]) => line >= from && line < to));
|
|
4428
4522
|
}
|
|
4429
4523
|
function matchingLines(text, pattern) {
|
|
4430
4524
|
const matches = typeof pattern === "function" ? pattern : (line) => pattern.test(line);
|
|
@@ -4433,9 +4527,9 @@ function matchingLines(text, pattern) {
|
|
|
4433
4527
|
function ciTestSteps(file, oldText, newText) {
|
|
4434
4528
|
if (!CI_FILE.test(file) || oldText === null && newText === null) return null;
|
|
4435
4529
|
const was = oldText ?? "", now = newText ?? "";
|
|
4436
|
-
const commandsBefore =
|
|
4530
|
+
const commandsBefore = enforcedTestCommands(file, was), commandsAfter = gatingTestCommands(file, now);
|
|
4437
4531
|
const after = commandsAfter.length;
|
|
4438
|
-
return { file, before: commandsBefore.length, after, reusableAfter: matchingLines(now, CI_REUSABLE), ungatedAfter: after === 0 ?
|
|
4532
|
+
return { file, before: commandsBefore.length, after, reusableAfter: matchingLines(now, CI_REUSABLE), ungatedAfter: after === 0 ? ciTestLines(now).length : 0, commandsBefore, commandsAfter };
|
|
4439
4533
|
}
|
|
4440
4534
|
function resolveCiTestSteps(counts, commit, elsewhere = /* @__PURE__ */ new Set()) {
|
|
4441
4535
|
const before = counts.reduce((sum, item) => sum + item.before, 0);
|
|
@@ -4484,7 +4578,6 @@ function scopeCommands(scope) {
|
|
|
4484
4578
|
}
|
|
4485
4579
|
function lenientSteps(text) {
|
|
4486
4580
|
const lines = text.split(/\r?\n/);
|
|
4487
|
-
const indentOf = (line) => line.length - line.trimStart().length;
|
|
4488
4581
|
const found = /* @__PURE__ */ new Map();
|
|
4489
4582
|
lines.forEach((line, i) => {
|
|
4490
4583
|
if (line.trim().startsWith("#") || !LENIENT.test(line)) return;
|
|
@@ -4501,7 +4594,7 @@ function lenientSteps(text) {
|
|
|
4501
4594
|
const start = /^\s*-\s+if:/.test(line) ? i : Math.max(j, 0), level = indentOf(lines[start]);
|
|
4502
4595
|
let end = start + 1;
|
|
4503
4596
|
while (end < lines.length && (!lines[end].trim() || lines[end].trim().startsWith("#") || indentOf(lines[end]) > level)) end++;
|
|
4504
|
-
const scope = lines.slice(start, end), runsChecks =
|
|
4597
|
+
const scope = lines.slice(start, end), runsChecks = ciTestLines(scope.join("\n")).length > 0;
|
|
4505
4598
|
const uses = scope.filter((item) => /^\s*(?:-\s+)?uses:/.test(item));
|
|
4506
4599
|
const nonCheck = !runsChecks && uses.length === 1 && NON_CHECK_ACTION.test(uses[0]) && !scope.some((item) => /^\s*(?:-\s+)?run:/.test(item));
|
|
4507
4600
|
const commands = scopeCommands(scope);
|
|
@@ -4511,7 +4604,8 @@ function lenientSteps(text) {
|
|
|
4511
4604
|
entry.runsChecks ||= runsChecks;
|
|
4512
4605
|
entry.nonCheck &&= nonCheck;
|
|
4513
4606
|
entry.commands.push(...commands);
|
|
4514
|
-
|
|
4607
|
+
entry.ranges.push([start, end]);
|
|
4608
|
+
} else found.set(identity, { count: 1, line: i + 1, runsChecks, nonCheck, commands, ranges: [[start, end]] });
|
|
4515
4609
|
});
|
|
4516
4610
|
return found;
|
|
4517
4611
|
}
|
|
@@ -4800,7 +4894,7 @@ async function compareSnapshot(options, cwd, snapshot) {
|
|
|
4800
4894
|
try {
|
|
4801
4895
|
for (const file of (await ciFilesAt(cwd, tree, options.signal)).filter((candidate) => !touched.has(candidate.path)).slice(0, 100)) {
|
|
4802
4896
|
try {
|
|
4803
|
-
for (const command of
|
|
4897
|
+
for (const command of enforcedTestCommands(file.path, new TextDecoder().decode(await readBlob(cwd, file.oid, options.signal, 512 * 1024)))) elsewhere.add(command);
|
|
4804
4898
|
} catch (error) {
|
|
4805
4899
|
assertNotAborted(options.signal);
|
|
4806
4900
|
}
|
|
@@ -4947,13 +5041,14 @@ function formatReplayTerminal(result, allRules = false) {
|
|
|
4947
5041
|
// src/hook.ts
|
|
4948
5042
|
import path8 from "path";
|
|
4949
5043
|
import * as fs7 from "fs/promises";
|
|
4950
|
-
var HOOK_AGENTS = ["claude", "codex", "cursor"];
|
|
5044
|
+
var HOOK_AGENTS = ["claude", "codex", "cursor", "gemini", "copilot", "droid"];
|
|
4951
5045
|
var DEFAULT_TIMEOUT_MS = 25e3;
|
|
4952
5046
|
var PAUSE_FILE = "agent-roast-hook-paused";
|
|
4953
5047
|
var LISTED = 6;
|
|
4954
5048
|
var LISTED_WARNINGS = 3;
|
|
4955
5049
|
function detectAgent(input) {
|
|
4956
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";
|
|
4957
5052
|
if ("turn_id" in input) return "codex";
|
|
4958
5053
|
return "claude";
|
|
4959
5054
|
}
|
|
@@ -4961,7 +5056,7 @@ function inputCwd(agent, input, env) {
|
|
|
4961
5056
|
const roots = input.workspace_roots;
|
|
4962
5057
|
if (agent === "cursor" && Array.isArray(roots) && typeof roots[0] === "string") return roots[0];
|
|
4963
5058
|
if (typeof input.cwd === "string" && input.cwd) return input.cwd;
|
|
4964
|
-
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();
|
|
4965
5060
|
}
|
|
4966
5061
|
function clip(text, limit) {
|
|
4967
5062
|
const flat = terminalText(text).replace(/\s+/g, " ").trim();
|
|
@@ -5039,8 +5134,8 @@ async function runHook(rawInput, options = {}) {
|
|
|
5039
5134
|
if (["off", "0", "false"].includes(mode)) return outcome("skip");
|
|
5040
5135
|
const advisory = options.advisory === true || mode === "advisory";
|
|
5041
5136
|
const agent = options.agent ?? detectAgent(input);
|
|
5042
|
-
const
|
|
5043
|
-
if (
|
|
5137
|
+
const finishing = agent === "gemini" ? "afteragent" : "stop";
|
|
5138
|
+
if (String(input.hook_event_name ?? finishing).toLowerCase() !== finishing) return outcome("skip");
|
|
5044
5139
|
if (agent === "cursor" && typeof input.status === "string" && input.status !== "completed") return outcome("skip");
|
|
5045
5140
|
const cwd = options.cwd ?? inputCwd(agent, input, env);
|
|
5046
5141
|
if (await hookPausedUntil(cwd, options.now)) return outcome("skip");
|
|
@@ -5056,15 +5151,17 @@ async function runHook(rawInput, options = {}) {
|
|
|
5056
5151
|
} finally {
|
|
5057
5152
|
clearTimeout(timer);
|
|
5058
5153
|
}
|
|
5059
|
-
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);
|
|
5060
5155
|
const alreadyAsked = input.stop_hook_active === true || typeof input.loop_count === "number" && input.loop_count > 0;
|
|
5061
5156
|
if (alreadyAsked || advisory) {
|
|
5062
5157
|
const warning = formatUserWarning(result);
|
|
5063
|
-
|
|
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}
|
|
5064
5160
|
` : "", result);
|
|
5065
5161
|
}
|
|
5066
5162
|
const message = formatAgentFeedback(result);
|
|
5067
|
-
|
|
5163
|
+
const answer = agent === "cursor" ? { followup_message: message } : { decision: agent === "gemini" ? "deny" : "block", reason: message };
|
|
5164
|
+
return outcome("block", JSON.stringify(answer) + "\n", "", result);
|
|
5068
5165
|
}
|
|
5069
5166
|
|
|
5070
5167
|
// src/hook-install.ts
|
|
@@ -5074,8 +5171,9 @@ import path9 from "path";
|
|
|
5074
5171
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5075
5172
|
import { createRequire as createRequire2 } from "module";
|
|
5076
5173
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5077
|
-
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" };
|
|
5078
5175
|
var TIMEOUT_SECONDS = 60;
|
|
5176
|
+
var FINISH_EVENT = { claude: "Stop", codex: "Stop", cursor: "stop", gemini: "AfterAgent", copilot: "Stop", droid: "Stop" };
|
|
5079
5177
|
function packageVersion() {
|
|
5080
5178
|
const require3 = createRequire2(import.meta.url);
|
|
5081
5179
|
return String(require3("../package.json").version);
|
|
@@ -5102,6 +5200,9 @@ function hookConfigPath(agent, scope, root, home, env = process.env) {
|
|
|
5102
5200
|
const claude = path9.join(scope === "global" ? home : root, ".claude");
|
|
5103
5201
|
if (agent === "claude") return path9.join(claude, scope === "local" ? "settings.local.json" : "settings.json");
|
|
5104
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");
|
|
5105
5206
|
return path9.join(scope === "global" ? env.CODEX_HOME || path9.join(home, ".codex") : path9.join(root, ".codex"), "hooks.json");
|
|
5106
5207
|
}
|
|
5107
5208
|
function readJsonFile(file) {
|
|
@@ -5137,40 +5238,47 @@ function writeJsonFile(link, json) {
|
|
|
5137
5238
|
}
|
|
5138
5239
|
}
|
|
5139
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";
|
|
5140
5248
|
function addStopHook(agent, data, command) {
|
|
5141
5249
|
const before = JSON.stringify(data);
|
|
5142
|
-
|
|
5143
|
-
if (agent
|
|
5250
|
+
const events = eventMap(agent, data, true), event = FINISH_EVENT[agent];
|
|
5251
|
+
if (isFlat(agent)) {
|
|
5144
5252
|
data.version ??= 1;
|
|
5145
|
-
const handlers = asArray(
|
|
5253
|
+
const handlers = asArray(events[event]);
|
|
5146
5254
|
const ours = handlers.find((handler) => isOurs(handler?.command));
|
|
5147
5255
|
if (ours) ours.command = command;
|
|
5148
|
-
else handlers.push({ command, timeout: TIMEOUT_SECONDS, loop_limit: 2 });
|
|
5149
|
-
|
|
5256
|
+
else handlers.push(agent === "cursor" ? { command, timeout: TIMEOUT_SECONDS, loop_limit: 2 } : { type: "command", command, timeoutSec: TIMEOUT_SECONDS });
|
|
5257
|
+
events[event] = handlers;
|
|
5150
5258
|
} else {
|
|
5151
|
-
const groups = asArray(
|
|
5259
|
+
const groups = asArray(events[event]);
|
|
5152
5260
|
const ours = groups.flatMap((group) => asArray(group?.hooks)).find((handler) => isOurs(handler?.command));
|
|
5153
5261
|
if (ours) ours.command = command;
|
|
5154
|
-
else groups.push({ hooks: [{ type: "command", command, timeout: TIMEOUT_SECONDS }] });
|
|
5155
|
-
|
|
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;
|
|
5156
5264
|
}
|
|
5157
5265
|
return JSON.stringify(data) !== before;
|
|
5158
5266
|
}
|
|
5159
5267
|
function removeStopHook(agent, data) {
|
|
5160
5268
|
const before = JSON.stringify(data);
|
|
5161
|
-
const
|
|
5162
|
-
if (!
|
|
5163
|
-
if (agent
|
|
5164
|
-
const kept = asArray(
|
|
5165
|
-
if (kept.length)
|
|
5166
|
-
else delete
|
|
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];
|
|
5167
5275
|
} else {
|
|
5168
|
-
const groups = asArray(
|
|
5276
|
+
const groups = asArray(events[event]).map((group) => ({ ...group, hooks: asArray(group?.hooks).filter((handler) => !isOurs(handler?.command)) }));
|
|
5169
5277
|
const kept = groups.filter((group) => group.hooks.length);
|
|
5170
|
-
if (kept.length)
|
|
5171
|
-
else delete
|
|
5278
|
+
if (kept.length) events[event] = kept;
|
|
5279
|
+
else delete events[event];
|
|
5172
5280
|
}
|
|
5173
|
-
if (!Object.keys(
|
|
5281
|
+
if (events !== data && !Object.keys(events).length) delete data.hooks;
|
|
5174
5282
|
return JSON.stringify(data) !== before;
|
|
5175
5283
|
}
|
|
5176
5284
|
function context(options) {
|
|
@@ -5200,14 +5308,15 @@ function uninstallHooks(options) {
|
|
|
5200
5308
|
function hookStatus(options) {
|
|
5201
5309
|
const { root, home, env } = context({ ...options, scope: "project" });
|
|
5202
5310
|
const rows = [];
|
|
5203
|
-
for (const agent of ["claude", "codex", "cursor"]) {
|
|
5311
|
+
for (const agent of ["claude", "codex", "cursor", "gemini", "copilot", "droid"]) {
|
|
5204
5312
|
for (const scope of ["project", "local", "global"]) {
|
|
5205
5313
|
if (scope === "local" && agent !== "claude") continue;
|
|
5206
5314
|
const file = hookConfigPath(agent, scope, root, home, env);
|
|
5207
5315
|
let command;
|
|
5208
5316
|
try {
|
|
5209
5317
|
const data = readJsonFile(file).data;
|
|
5210
|
-
const
|
|
5318
|
+
const listed = asArray(eventMap(agent, data, false)?.[FINISH_EVENT[agent]]);
|
|
5319
|
+
const handlers = isFlat(agent) ? listed : listed.flatMap((group) => asArray(group?.hooks));
|
|
5211
5320
|
command = handlers.map((handler) => handler?.command).find(isOurs);
|
|
5212
5321
|
} catch {
|
|
5213
5322
|
}
|
|
@@ -5760,6 +5869,7 @@ export {
|
|
|
5760
5869
|
GitResolutionError,
|
|
5761
5870
|
readGit,
|
|
5762
5871
|
RULE_GUIDES,
|
|
5872
|
+
assertsAlwaysTrue,
|
|
5763
5873
|
isCodeFile,
|
|
5764
5874
|
isTestFile,
|
|
5765
5875
|
auditFileDiffs,
|
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-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
|
|
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
|
|
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(
|
|
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
|
@@ -257,6 +257,10 @@ declare class GitResolutionError extends Error {
|
|
|
257
257
|
constructor(message: string);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
/** `assert check or True` and `self.assertTrue(check or True)` cannot fail: an operand of the `or` is the constant, and the check beside it decides nothing. Only the operands outside every
|
|
261
|
+
* bracket are read: `lambda paths: seen.append(paths) or True` inside a call is a callback that says it handled the call, and `identity(x or True)` is a value. `code` has its strings and
|
|
262
|
+
* comments blanked. */
|
|
263
|
+
declare function assertsAlwaysTrue(code: string): boolean;
|
|
260
264
|
declare function isCodeFile(filePath: string, customIgnorePaths?: string[]): boolean;
|
|
261
265
|
declare function isTestFile(filePath: string): boolean;
|
|
262
266
|
/**
|
|
@@ -471,7 +475,7 @@ declare function replayHistory(options?: ReplayOptions): Promise<ReplayResult>;
|
|
|
471
475
|
declare function formatReplayTerminal(result: ReplayResult, allRules?: boolean): string;
|
|
472
476
|
|
|
473
477
|
/** Coding agents that can run a command when they are about to finish and feed its answer back. */
|
|
474
|
-
type HookAgent = "claude" | "codex" | "cursor";
|
|
478
|
+
type HookAgent = "claude" | "codex" | "cursor" | "gemini" | "copilot" | "droid";
|
|
475
479
|
declare const HOOK_AGENTS: readonly HookAgent[];
|
|
476
480
|
interface HookOptions {
|
|
477
481
|
/** Which agent is calling. Detected from its input when omitted. */
|
|
@@ -741,4 +745,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
|
|
|
741
745
|
}>;
|
|
742
746
|
declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
|
|
743
747
|
|
|
744
|
-
export { AGENT_LABELS, type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DemoResult, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, HOOK_AGENTS, type HookAgent, type HookInstallChange, type HookInstallOptions, type HookOptions, type HookOutcome, type HookScope, INSTRUCTIONS_BLOCK, INSTRUCTIONS_END, INSTRUCTIONS_START, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, MAX_CONFIG_BYTES, MCP_TOOLS, type McpServer, type McpServerOptions, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReplayCommit, type ReplayOptions, type ReplayResult, type ReplayStatus, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ServeMcpOptions, type ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, createMcpServer, detectAiAttribution, findMergeBase, findingFingerprint, formatAgentFeedback, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReplayTerminal, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, hookCommand, hookConfigPath, hookPausedUntil, hookStatus, initProject, installHooks, installInstructions, isCheckRule, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, mostImportantFirst, packageVersion, parseDiff, parseDuration, parseGitLogStream, pauseHook, replayHistory, repositoryRoot, resolveCommitSha, resumeHook, reviewChanges, reviewStagedChanges, reviewWorktreeChanges, roastRepository, runDemo, runHook, serveMcp, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, uninstallHooks, unquoteGitPath, validateExceptions, writeBaseline };
|
|
748
|
+
export { AGENT_LABELS, type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DemoResult, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, HOOK_AGENTS, type HookAgent, type HookInstallChange, type HookInstallOptions, type HookOptions, type HookOutcome, type HookScope, INSTRUCTIONS_BLOCK, INSTRUCTIONS_END, INSTRUCTIONS_START, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, MAX_CONFIG_BYTES, MCP_TOOLS, type McpServer, type McpServerOptions, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReplayCommit, type ReplayOptions, type ReplayResult, type ReplayStatus, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ServeMcpOptions, type ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, assertsAlwaysTrue, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, createMcpServer, detectAiAttribution, findMergeBase, findingFingerprint, formatAgentFeedback, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReplayTerminal, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, hookCommand, hookConfigPath, hookPausedUntil, hookStatus, initProject, installHooks, installInstructions, isCheckRule, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, mostImportantFirst, packageVersion, parseDiff, parseDuration, parseGitLogStream, pauseHook, replayHistory, repositoryRoot, resolveCommitSha, resumeHook, reviewChanges, reviewStagedChanges, reviewWorktreeChanges, roastRepository, runDemo, runHook, serveMcp, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, uninstallHooks, unquoteGitPath, validateExceptions, writeBaseline };
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
VALID_DETECTORS,
|
|
25
25
|
applyBaseline,
|
|
26
26
|
applyExceptions,
|
|
27
|
+
assertsAlwaysTrue,
|
|
27
28
|
auditFileDiffs,
|
|
28
29
|
auditPullRequest,
|
|
29
30
|
checkProject,
|
|
@@ -91,7 +92,7 @@ import {
|
|
|
91
92
|
unquoteGitPath,
|
|
92
93
|
validateExceptions,
|
|
93
94
|
writeBaseline
|
|
94
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-HDC5C2D3.js";
|
|
95
96
|
export {
|
|
96
97
|
AGENT_LABELS,
|
|
97
98
|
CONFIG_FILENAMES,
|
|
@@ -117,6 +118,7 @@ export {
|
|
|
117
118
|
VALID_DETECTORS,
|
|
118
119
|
applyBaseline,
|
|
119
120
|
applyExceptions,
|
|
121
|
+
assertsAlwaysTrue,
|
|
120
122
|
auditFileDiffs,
|
|
121
123
|
auditPullRequest,
|
|
122
124
|
checkProject,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-roast",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Stops AI
|
|
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",
|