agent-roast 0.12.1 → 0.13.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 +7 -2
- package/dist/{chunk-AP5Y4CH6.js → chunk-W6JP5YWF.js} +335 -27
- package/dist/cli.js +10 -1
- package/dist/index.d.ts +127 -4
- package/dist/index.js +7 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ npx agent-roast # review what you have not committed yet (on a clean tr
|
|
|
40
40
|
npx agent-roast hook install # Claude Code, Codex and Cursor
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
For Claude Code there is also a plugin: `/plugin install agent-roast --marketplace Nari-Ab/agent-roast`.
|
|
43
|
+
For Claude Code there is also a plugin: `/plugin install agent-roast --marketplace Nari-Ab/agent-roast`. Any agent that speaks
|
|
44
|
+
MCP can also call agent-roast as a read-only tool, `agent-roast mcp` ([setup](docs/integrations.md#mcp-server)).
|
|
44
45
|
|
|
45
46
|
When the agent is about to finish, agent-roast reviews everything it changed. If a check got weaker, the agent is **sent back
|
|
46
47
|
once**, with the file, the line and the before and after, and told to fix the code instead. If it tries again, you get a
|
|
@@ -110,7 +111,7 @@ old debt with a baseline: [docs/reference.md](docs/reference.md#configuration).
|
|
|
110
111
|
Measured, with the denominators and the caveats in [docs/benchmarks.md](docs/benchmarks.md). **Weakened checks are rare, mostly
|
|
111
112
|
deliberate, and not specific to AI. agent-roast is a tripwire that stays silent until a check gets weaker.**
|
|
112
113
|
|
|
113
|
-
- **
|
|
114
|
+
- **71 of 71** hand-labeled cheating edits are caught and **51 of 51** harmless look-alikes (reformats, moves, raised
|
|
114
115
|
thresholds, environment-gated skips) stay silent. The cases were written with the detector, so they guard against regressions;
|
|
115
116
|
they do not prove accuracy. Three gaps are documented and still open.
|
|
116
117
|
- **12,645 commits** that touch tests or CI/config in 166 public repositories: blocking rules fired on 0.3% of AI-signed commits
|
|
@@ -120,6 +121,10 @@ deliberate, and not specific to AI. agent-roast is a tripwire that stays silent
|
|
|
120
121
|
- **About one finding in five was a false alarm**, and most of the rest were deliberate, explained changes a reviewer should
|
|
121
122
|
see rather than mistakes. The labels come from one reader (an AI assistant), so treat them as a first estimate.
|
|
122
123
|
- `replay` ran on 179 public repositories that use coding agents: no commit would have been blocked in 149 of them.
|
|
124
|
+
- The rules for **eight more languages** (Go, Rust, Java, Kotlin, C#, Ruby, PHP, Swift) ran on the newest 300 commits of 39
|
|
125
|
+
repositories and every finding was read: 26 of 99 were skips that depend on the environment, now fixed; of the 73 left, 56 are
|
|
126
|
+
tests that really are switched off and 17 are deliberate test data. One reader, and the fixes were made on the same
|
|
127
|
+
repositories, so expect a few new false alarms elsewhere.
|
|
123
128
|
|
|
124
129
|
## How it compares
|
|
125
130
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path12 from "path";
|
|
5
5
|
|
|
6
6
|
// src/git.ts
|
|
7
7
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -1191,7 +1191,8 @@ var GO_GUARD = {
|
|
|
1191
1191
|
var RUBY_GUARD = {
|
|
1192
1192
|
modifier: /^\s*(?:skip|pending)\b.*\b(?:if|unless)\b/,
|
|
1193
1193
|
conditional: /^\s*(?:if|unless|else|elsif|case|when|begin|rescue|ensure|while|until)\b/,
|
|
1194
|
-
|
|
1194
|
+
// Hooks (before, after, around) are not listed: a hook declared under `elsif RUBY_VERSION ...` gates its skip like an example does.
|
|
1195
|
+
test: /^\s*(?:it|specify|example|scenario|test)\b|^\s*def\s+test_/,
|
|
1195
1196
|
helper: /^\s*def\s+\w+/
|
|
1196
1197
|
};
|
|
1197
1198
|
var PHP_GUARD = {
|
|
@@ -1201,9 +1202,10 @@ var PHP_GUARD = {
|
|
|
1201
1202
|
helper: /^\s*(?:(?:public|protected|private|static)\s+)*function\s+\w+\s*\(/
|
|
1202
1203
|
};
|
|
1203
1204
|
var SWIFT_GUARD = {
|
|
1205
|
+
preprocessor: { open: /^\s*#if\b/, close: /^\s*#endif\b/, other: /^\s*#else(?:if)?\b/ },
|
|
1204
1206
|
conditional: /^\s*(?:\}\s*)?(?:if|else|guard|switch|case|for|while|do|catch)\b/,
|
|
1205
|
-
test: /^\s*(?:@\w+\s+)*(?:(?:private|fileprivate|static)\s+)*func\s+test\w*\s*\(/,
|
|
1206
|
-
helper: /^\s*(?:@\w+\s+)*(?:(?:private|fileprivate|static)\s+)*func\s+\w+\s*\(/
|
|
1207
|
+
test: /^\s*(?:@\w+\s+)*(?:(?:public|open|internal|private|fileprivate|static|final|override|class|nonisolated)\s+)*func\s+test\w*\s*\(/,
|
|
1208
|
+
helper: /^\s*(?:@\w+\s+)*(?:(?:public|open|internal|private|fileprivate|static|final|override|class|nonisolated)\s+)*func\s+\w+\s*\(/
|
|
1207
1209
|
};
|
|
1208
1210
|
var LANGUAGE_LINE_RULES = [
|
|
1209
1211
|
{ files: /_test\.go$/i, rules: [
|
|
@@ -1226,7 +1228,9 @@ var LANGUAGE_LINE_RULES = [
|
|
|
1226
1228
|
{ type: "trivial-assert", regex: /\bassertThat\s*\(\s*true\s*\)\s*\.\s*isTrue\s*\(/, name: "trivial assert (assertThat(true).isTrue())" }
|
|
1227
1229
|
] },
|
|
1228
1230
|
{ files: /\.cs$/i, rules: [
|
|
1229
|
-
|
|
1231
|
+
// `[Ignore]` is checked separately: it also names a mapping attribute (AutoMapper) and must sit next to a test attribute.
|
|
1232
|
+
// Skip = with SkipUnless, SkipWhen or SkipType depends on the environment.
|
|
1233
|
+
{ type: "test-skip", regex: /\b(?:Fact|Theory)\s*\(\s*(?:[^)]*,\s*)?Skip\s*=(?![^\]]*\bSkip(?:Unless|When|Type)\b)/, name: "ignored .NET test (Skip =)" },
|
|
1230
1234
|
{ type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:True|IsTrue)\s*\(\s*true\s*\)/, name: "trivial assert (Assert.True(true))" },
|
|
1231
1235
|
{ type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:False|IsFalse)\s*\(\s*false\s*\)/, name: "trivial assert (Assert.False(false))" },
|
|
1232
1236
|
{ type: "trivial-assert", regex: /\bAssert\s*\.\s*(?:AreEqual|Equal)\s*\(\s*(\d+|true|false|null)\s*,\s*\1\s*\)/, name: "trivial assert (Assert.Equal(1, 1))" }
|
|
@@ -1236,7 +1240,9 @@ var LANGUAGE_LINE_RULES = [
|
|
|
1236
1240
|
{ type: "test-focus", regex: /^\s*(?:fit|fdescribe|fcontext|fspecify|fexample|fscenario)\b/, name: "focused RSpec example (fit / fdescribe)" },
|
|
1237
1241
|
{ type: "test-focus", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::focus\b|focus:\s*true\b)/, name: "focused RSpec example (focus: true)" },
|
|
1238
1242
|
{ type: "test-skip", regex: /^\s*(?:xit|xdescribe|xcontext|xspecify|xexample|xscenario|xfeature)\b/, name: "disabled RSpec example (xit / xdescribe)" },
|
|
1239
|
-
|
|
1243
|
+
// `:skip => !feature?` or `skip: RUBY_VERSION < "3"` gates the example on its environment; only a bare flag, true or a reason
|
|
1244
|
+
// disables it. The line is matched with its strings blanked, so a reason shows as nothing between `skip:` and `do`, `{`, `,` or `)`.
|
|
1245
|
+
{ type: "test-skip", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::skip\b(?!\s*=>)|(?::skip\s*=>|skip:)\s*(?:true\b|(?=do\b|\{|,|\)|$)))/, name: "disabled RSpec example (skip: true)" },
|
|
1240
1246
|
{ type: "test-skip", regex: /^\s*skip(?:\s|\(|$)/, name: "skipped Ruby test (skip)", guard: RUBY_GUARD },
|
|
1241
1247
|
{ type: "test-skip", regex: /^\s*pending(?:\s+["'(]|\s*$)/, name: "pending Ruby example (pending)", guard: RUBY_GUARD },
|
|
1242
1248
|
{ type: "trivial-assert", regex: /\bexpect\s*\(\s*(true|false|1|0|nil)\s*\)\s*\.\s*to\s+(?:eq|eql|be|equal)\b\s*\(?\s*\1\b/, name: "trivial expectation (expect(true).to eq(true))" },
|
|
@@ -1251,29 +1257,53 @@ var LANGUAGE_LINE_RULES = [
|
|
|
1251
1257
|
] },
|
|
1252
1258
|
{ files: /\.swift$/i, rules: [
|
|
1253
1259
|
{ type: "test-skip", regex: /\bthrow\s+XCTSkip\s*\(/, name: "skipped XCTest (XCTSkip)", guard: SWIFT_GUARD },
|
|
1254
|
-
|
|
1260
|
+
// `.disabled(if: condition)` depends on the environment; `.disabled()` and `.disabled("reason")` switch the test off.
|
|
1261
|
+
{ type: "test-skip", regex: /@(?:Test|Suite)\s*\([^)]*\.disabled\s*\((?!\s*if\b)/, name: "disabled Swift Testing test (.disabled)" },
|
|
1255
1262
|
{ type: "trivial-assert", regex: /\bXCTAssertTrue\s*\(\s*true\s*\)/, name: "trivial assert (XCTAssertTrue(true))" },
|
|
1256
1263
|
{ type: "trivial-assert", regex: /\bXCTAssertFalse\s*\(\s*false\s*\)/, name: "trivial assert (XCTAssertFalse(false))" },
|
|
1257
1264
|
{ type: "trivial-assert", regex: /\bXCTAssertEqual\s*\(\s*(\d+|true|false|nil)\s*,\s*\1\s*\)/, name: "trivial assert (XCTAssertEqual(1, 1))" },
|
|
1258
1265
|
{ type: "trivial-assert", regex: /#expect\s*\(\s*true\s*\)/, name: "trivial expectation (#expect(true))" }
|
|
1259
1266
|
] }
|
|
1260
1267
|
];
|
|
1268
|
+
function inTestAttributeList(lines, index) {
|
|
1269
|
+
const marker = /\[\s*(?:Test|TestCase|TestCaseSource|TestMethod|DataTestMethod|TestFixture|TestClass|Fact|Theory)\b/;
|
|
1270
|
+
if (marker.test(lines[index].code)) return true;
|
|
1271
|
+
for (const step of [-1, 1]) {
|
|
1272
|
+
for (let i = index + step, seen = 0; i >= 0 && i < lines.length && seen < 6; i += step, seen++) {
|
|
1273
|
+
if (lines[i].number !== lines[i - step].number + step || !/^\s*\[/.test(lines[i].code)) break;
|
|
1274
|
+
if (marker.test(lines[i].code)) return true;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1261
1279
|
function isGuardedBlockSkip(lines, profile) {
|
|
1262
1280
|
const indentOf = (text) => text.length - text.trimStart().length;
|
|
1263
1281
|
const mentionsCi = (raw) => /\bCI\b|\bGITHUB_ACTIONS\b/.test(raw);
|
|
1264
1282
|
const own = lines[lines.length - 1];
|
|
1265
1283
|
if (profile.modifier?.test(own.code) || profile.conditional.test(own.code)) return !mentionsCi(own.raw);
|
|
1284
|
+
const compiledConditionally = (from) => {
|
|
1285
|
+
const directives = profile.preprocessor;
|
|
1286
|
+
if (!directives) return false;
|
|
1287
|
+
let depth = 0;
|
|
1288
|
+
for (let i2 = from; i2 < lines.length - 1; i2++) {
|
|
1289
|
+
if (directives.open.test(lines[i2].code)) depth++;
|
|
1290
|
+
else if (directives.close.test(lines[i2].code)) depth = Math.max(0, depth - 1);
|
|
1291
|
+
}
|
|
1292
|
+
return depth > 0;
|
|
1293
|
+
};
|
|
1266
1294
|
let limit = indentOf(own.code);
|
|
1267
|
-
|
|
1268
|
-
|
|
1295
|
+
let i = lines.length - 2;
|
|
1296
|
+
for (; i >= 0 && lines.length - 1 - i <= 200; i--) {
|
|
1297
|
+
if (lines[i].number !== lines[i + 1].number - 1) return compiledConditionally(i + 1);
|
|
1269
1298
|
const { code, raw } = lines[i];
|
|
1270
1299
|
if (!code.trim() || indentOf(code) >= limit) continue;
|
|
1300
|
+
if (profile.preprocessor && (profile.preprocessor.open.test(code) || profile.preprocessor.close.test(code) || profile.preprocessor.other.test(code))) continue;
|
|
1271
1301
|
if (profile.conditional.test(code)) return !mentionsCi(raw);
|
|
1272
|
-
if (profile.test.test(code)) return
|
|
1302
|
+
if (profile.test.test(code)) return compiledConditionally(i + 1);
|
|
1273
1303
|
if (profile.helper.test(code)) return true;
|
|
1274
1304
|
limit = indentOf(code);
|
|
1275
1305
|
}
|
|
1276
|
-
return
|
|
1306
|
+
return compiledConditionally(i + 1);
|
|
1277
1307
|
}
|
|
1278
1308
|
var JS_TEST_API = /(?<![\w$.])(?:(?:it|test|describe|suite)(?:\s*\.\s*(?:concurrent|serial|parallel|describe|failing|sequential))*\s*\.\s*(skip|todo|only)|((?:xit|xtest|xdescribe|fit|fdescribe)))\b((?:\s*\.\s*(?:each|failing|concurrent|serial|parallel|sequential))*)\s*(?=\(|\u0001)/g;
|
|
1279
1309
|
function isConditionalSkip(source, raw, open2) {
|
|
@@ -1470,6 +1500,13 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
|
|
|
1470
1500
|
}
|
|
1471
1501
|
}
|
|
1472
1502
|
}
|
|
1503
|
+
if (/\.cs$/i.test(file.path)) {
|
|
1504
|
+
maskedLines.forEach((line, index) => {
|
|
1505
|
+
if (/\[\s*Ignore\b/.test(line.code) && inTestAttributeList(maskedLines, index)) {
|
|
1506
|
+
infractions.push({ type: "test-skip", file: file.path, lineNumber: line.number, lineSnippet: line.raw.trim(), commitHash, commitDate, reason: "ignored .NET test ([Ignore])" });
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1473
1510
|
let source = "";
|
|
1474
1511
|
let code = "";
|
|
1475
1512
|
let raw = "";
|
|
@@ -3674,10 +3711,10 @@ var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
|
3674
3711
|
function clipEvidence(text, limit = 200) {
|
|
3675
3712
|
if (text.length <= limit) return text;
|
|
3676
3713
|
const arrow = text.indexOf(" \u2192 ");
|
|
3677
|
-
const
|
|
3678
|
-
if (arrow < 0) return
|
|
3714
|
+
const clip3 = (part, size) => part.length > size ? `${part.slice(0, size - 1)}\u2026` : part;
|
|
3715
|
+
if (arrow < 0) return clip3(text, limit);
|
|
3679
3716
|
const side = Math.floor((limit - 3) / 2);
|
|
3680
|
-
return `${
|
|
3717
|
+
return `${clip3(text.slice(0, arrow), side)} \u2192 ${clip3(text.slice(arrow + 3), side)}`;
|
|
3681
3718
|
}
|
|
3682
3719
|
async function replayHistory(options = {}) {
|
|
3683
3720
|
assertNotAborted(options.signal);
|
|
@@ -3811,11 +3848,11 @@ function describe(finding2, index) {
|
|
|
3811
3848
|
const evidence = clip(finding2.lineSnippet || finding2.reason, 220);
|
|
3812
3849
|
return [`${index}. ${where} [${finding2.type}]`, ` ${evidence}`];
|
|
3813
3850
|
}
|
|
3814
|
-
function formatAgentFeedback(result) {
|
|
3851
|
+
function formatAgentFeedback(result, subject = "the uncommitted changes") {
|
|
3815
3852
|
const ranked = mostImportantFirst(result.infractions, result.failedRules);
|
|
3816
3853
|
const blocking = ranked.filter((finding2) => result.failedRules.includes(finding2.type));
|
|
3817
3854
|
const notes = ranked.filter((finding2) => !result.failedRules.includes(finding2.type) && isCheckRule(finding2.type));
|
|
3818
|
-
const lines = [
|
|
3855
|
+
const lines = [`agent-roast: ${subject} make the checks easier to pass instead of making the code correct.`, ""];
|
|
3819
3856
|
blocking.slice(0, LISTED).forEach((finding2, index) => lines.push(...describe(finding2, index + 1)));
|
|
3820
3857
|
if (blocking.length > LISTED) lines.push(` \u2026 and ${blocking.length - LISTED} more.`);
|
|
3821
3858
|
if (notes.length) {
|
|
@@ -4079,10 +4116,278 @@ function installInstructions(options = {}) {
|
|
|
4079
4116
|
});
|
|
4080
4117
|
}
|
|
4081
4118
|
|
|
4119
|
+
// src/mcp.ts
|
|
4120
|
+
import * as fs9 from "fs/promises";
|
|
4121
|
+
import path10 from "path";
|
|
4122
|
+
import readline from "readline";
|
|
4123
|
+
var MODERN_VERSIONS = ["2026-07-28"];
|
|
4124
|
+
var LEGACY_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
4125
|
+
var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
|
|
4126
|
+
var TOOL_TIMEOUT_MS = 25e3;
|
|
4127
|
+
var CACHE_MS = 36e5;
|
|
4128
|
+
var MAX_TEXT = 6e3;
|
|
4129
|
+
var LISTED_NOTES = 3;
|
|
4130
|
+
var INSTRUCTIONS = "Call review_worktree before you tell the user a task is done. It compares your uncommitted changes with HEAD and reports any test, assertion, coverage threshold, lint rule or CI step that was made weaker instead of fixing the code. If it reports findings, fix the code and leave the checks as they were; if a change to a check is really intended, ask the user first.";
|
|
4131
|
+
var READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
4132
|
+
var PATH_SCHEMA = { type: "string", description: "A directory inside the project. Defaults to the project the server was started in." };
|
|
4133
|
+
var MCP_TOOLS = [
|
|
4134
|
+
{
|
|
4135
|
+
name: "review_worktree",
|
|
4136
|
+
title: "Review uncommitted changes",
|
|
4137
|
+
description: "Compare everything not committed yet (staged, unstaged and untracked files) with HEAD and report changes that make tests, assertions, coverage thresholds, lint rules or CI easier to pass instead of fixing the code. Read-only and offline: nothing is run, written or sent.",
|
|
4138
|
+
inputSchema: { type: "object", properties: { path: PATH_SCHEMA }, additionalProperties: false },
|
|
4139
|
+
annotations: { title: "Review uncommitted changes", ...READ_ONLY }
|
|
4140
|
+
},
|
|
4141
|
+
{
|
|
4142
|
+
name: "review_changes",
|
|
4143
|
+
title: "Review a range of commits",
|
|
4144
|
+
description: "Compare two committed revisions, for example a branch against main, and report changes that weaken tests, assertions, coverage thresholds, lint rules or CI. The rules come from the base revision, so the change under review cannot switch them off. Read-only and offline.",
|
|
4145
|
+
inputSchema: {
|
|
4146
|
+
type: "object",
|
|
4147
|
+
properties: {
|
|
4148
|
+
base: { type: "string", description: "The revision to compare against, for example main or origin/main." },
|
|
4149
|
+
head: { type: "string", description: "The revision under review. Defaults to HEAD." },
|
|
4150
|
+
path: PATH_SCHEMA
|
|
4151
|
+
},
|
|
4152
|
+
required: ["base"],
|
|
4153
|
+
additionalProperties: false
|
|
4154
|
+
},
|
|
4155
|
+
annotations: { title: "Review a range of commits", ...READ_ONLY }
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
4158
|
+
name: "stop_hook",
|
|
4159
|
+
title: "Stop hook adapter",
|
|
4160
|
+
description: "Runs the same check as `agent-roast hook run` for plugin hooks of type mcp_tool and answers with the hook's JSON: it sends an agent back once when its changes weaken checks. Meant to be called by a hook, not by hand.",
|
|
4161
|
+
inputSchema: {
|
|
4162
|
+
type: "object",
|
|
4163
|
+
properties: {
|
|
4164
|
+
cwd: PATH_SCHEMA,
|
|
4165
|
+
stop_hook_active: { type: "string", description: '"true" when the agent is already continuing because of a Stop hook.' },
|
|
4166
|
+
loop_count: { type: "string", description: "How many times the agent was already sent back, for agents that count." }
|
|
4167
|
+
},
|
|
4168
|
+
additionalProperties: true
|
|
4169
|
+
},
|
|
4170
|
+
annotations: { title: "Stop hook adapter", ...READ_ONLY }
|
|
4171
|
+
}
|
|
4172
|
+
];
|
|
4173
|
+
var ToolInputError = class extends Error {
|
|
4174
|
+
};
|
|
4175
|
+
var ok = (id, result) => ({ jsonrpc: "2.0", id, result });
|
|
4176
|
+
var fail = (id, code, message, data) => ({ jsonrpc: "2.0", id, error: data === void 0 ? { code, message } : { code, message, data } });
|
|
4177
|
+
var clip2 = (text) => text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT - 1)}\u2026` : text;
|
|
4178
|
+
function optionalText(value, name) {
|
|
4179
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
4180
|
+
if (typeof value !== "string" || value.length > 300 || value.includes("\0")) throw new ToolInputError(`${name} must be a revision name of at most 300 characters.`);
|
|
4181
|
+
return value;
|
|
4182
|
+
}
|
|
4183
|
+
function summarize(result, subject) {
|
|
4184
|
+
const blocking = result.infractions.filter((finding2) => result.failedRules.includes(finding2.type));
|
|
4185
|
+
const lines = [];
|
|
4186
|
+
if (blocking.length) lines.push(formatAgentFeedback(result, subject));
|
|
4187
|
+
else {
|
|
4188
|
+
lines.push(result.filesChanged ? `agent-roast: no weakened checks in ${subject} (${result.filesInspected} of ${result.filesChanged} changed files compared).` : `agent-roast: nothing changed in ${subject}, so there is nothing to review.`);
|
|
4189
|
+
const notes = result.infractions.filter((finding2) => isCheckRule(finding2.type));
|
|
4190
|
+
if (notes.length) {
|
|
4191
|
+
lines.push("Worth a second look (these do not block):");
|
|
4192
|
+
for (const note of notes.slice(0, LISTED_NOTES)) {
|
|
4193
|
+
const where = `${terminalText(note.file ?? "unknown location")}${note.lineNumber ? `:${note.lineNumber}` : ""}`;
|
|
4194
|
+
lines.push(`- ${where} [${note.type}] ${terminalText(note.lineSnippet || note.reason).replace(/\s+/g, " ").trim().slice(0, 120)}`);
|
|
4195
|
+
}
|
|
4196
|
+
if (notes.length > LISTED_NOTES) lines.push(`- \u2026 and ${notes.length - LISTED_NOTES} more.`);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (!result.complete) {
|
|
4200
|
+
const first = result.skippedFiles[0];
|
|
4201
|
+
lines.push(`agent-roast could not compare ${result.skippedFiles.length} changed file${result.skippedFiles.length === 1 ? "" : "s"}${first ? ` (for example ${terminalText(first.file)}: ${terminalText(first.reason).slice(0, 120)})` : ""}, so this result may be incomplete.`);
|
|
4202
|
+
}
|
|
4203
|
+
return lines.join("\n");
|
|
4204
|
+
}
|
|
4205
|
+
function createMcpServer(options) {
|
|
4206
|
+
const env = options.env ?? process.env;
|
|
4207
|
+
const timeoutMs = options.timeoutMs ?? TOOL_TIMEOUT_MS;
|
|
4208
|
+
const serverInfo = { name: "agent-roast", title: "Agent Roast", version: options.version };
|
|
4209
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
4210
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
4211
|
+
let allowed;
|
|
4212
|
+
const startDirectory = () => path10.resolve(options.cwd ?? (env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || process.cwd()));
|
|
4213
|
+
const roots = () => allowed ??= Promise.all(
|
|
4214
|
+
(options.roots?.length ? options.roots : [startDirectory()]).map((root) => fs9.realpath(path10.resolve(root)).catch(() => path10.resolve(root)))
|
|
4215
|
+
);
|
|
4216
|
+
async function gitCommonDirectory(directory) {
|
|
4217
|
+
try {
|
|
4218
|
+
const output = gitOutputPath((await readGit(directory, ["rev-parse", "--git-common-dir"], void 0, 8192)).toString());
|
|
4219
|
+
return await fs9.realpath(path10.resolve(directory, output));
|
|
4220
|
+
} catch {
|
|
4221
|
+
return null;
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
async function permitted(real, list) {
|
|
4225
|
+
for (const root of list) {
|
|
4226
|
+
const relative2 = path10.relative(root, real);
|
|
4227
|
+
if (relative2 === "" || relative2 !== ".." && !relative2.startsWith(".." + path10.sep) && !path10.isAbsolute(relative2)) return true;
|
|
4228
|
+
}
|
|
4229
|
+
const common = await gitCommonDirectory(real);
|
|
4230
|
+
if (!common) return false;
|
|
4231
|
+
for (const root of list) if (common === await gitCommonDirectory(root)) return true;
|
|
4232
|
+
return false;
|
|
4233
|
+
}
|
|
4234
|
+
async function project(requested) {
|
|
4235
|
+
if (requested !== void 0 && requested !== null && requested !== "" && (typeof requested !== "string" || requested.includes("\0"))) throw new ToolInputError("path must be a directory path.");
|
|
4236
|
+
const list = await roots();
|
|
4237
|
+
if (!options.roots?.length && list.every((root) => path10.parse(root).root === root)) {
|
|
4238
|
+
throw new ToolInputError("The server was started outside a project (its working directory is the filesystem root). Add --root <directory> to the server's command line.");
|
|
4239
|
+
}
|
|
4240
|
+
const target = typeof requested === "string" && requested ? path10.resolve(list[0], requested) : list[0];
|
|
4241
|
+
let real;
|
|
4242
|
+
try {
|
|
4243
|
+
real = await fs9.realpath(target);
|
|
4244
|
+
} catch {
|
|
4245
|
+
throw new ToolInputError(`${terminalText(typeof requested === "string" && requested ? requested : target)} does not exist.`);
|
|
4246
|
+
}
|
|
4247
|
+
if (!await permitted(real, list)) throw new ToolInputError("That path is outside the project this server was started in. Add --root <directory> to the server's command line to allow another location.");
|
|
4248
|
+
return real;
|
|
4249
|
+
}
|
|
4250
|
+
async function callTool(name, args, signal) {
|
|
4251
|
+
switch (name) {
|
|
4252
|
+
case "review_worktree":
|
|
4253
|
+
return summarize(await reviewWorktreeChanges({ cwd: await project(args.path), signal }), "the uncommitted changes");
|
|
4254
|
+
case "review_changes": {
|
|
4255
|
+
const base = optionalText(args.base, "base");
|
|
4256
|
+
if (!base) throw new ToolInputError("base is required, for example main or origin/main.");
|
|
4257
|
+
const head = optionalText(args.head, "head");
|
|
4258
|
+
const cwd = await project(args.path);
|
|
4259
|
+
return summarize(await reviewChanges({ cwd, base, head, signal }), `the changes between ${terminalText(base)} and ${terminalText(head ?? "HEAD")}`);
|
|
4260
|
+
}
|
|
4261
|
+
case "stop_hook": {
|
|
4262
|
+
const cwd = await project(args.cwd);
|
|
4263
|
+
const input = { hook_event_name: "Stop", cwd, stop_hook_active: args.stop_hook_active === true || args.stop_hook_active === "true" };
|
|
4264
|
+
const loops = Number(args.loop_count);
|
|
4265
|
+
if (args.loop_count !== void 0 && args.loop_count !== "" && Number.isFinite(loops)) input.loop_count = loops;
|
|
4266
|
+
const outcome2 = await runHook(JSON.stringify(input), { agent: "claude", cwd, env, timeoutMs });
|
|
4267
|
+
if (outcome2.stderr) process.stderr.write(outcome2.stderr);
|
|
4268
|
+
return outcome2.stdout.trim() || "{}";
|
|
4269
|
+
}
|
|
4270
|
+
default:
|
|
4271
|
+
throw new ToolInputError(`Unknown tool: ${terminalText(name).slice(0, 100)}`);
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
async function toolResult(id, name, rawArguments) {
|
|
4275
|
+
const controller = new AbortController();
|
|
4276
|
+
inflight.set(id, controller);
|
|
4277
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4278
|
+
try {
|
|
4279
|
+
const args = rawArguments ?? {};
|
|
4280
|
+
if (typeof args !== "object" || Array.isArray(args)) throw new ToolInputError("arguments must be an object.");
|
|
4281
|
+
return { text: clip2(await callTool(name, args, controller.signal)), isError: false };
|
|
4282
|
+
} catch (error) {
|
|
4283
|
+
if (cancelled.has(id)) return null;
|
|
4284
|
+
if (controller.signal.aborted) {
|
|
4285
|
+
const limit = timeoutMs >= 1e3 ? `${Math.round(timeoutMs / 1e3)} seconds` : `${timeoutMs} ms`;
|
|
4286
|
+
return { text: `agent-roast: the check took longer than ${limit} and was stopped. Nothing was changed.`, isError: true };
|
|
4287
|
+
}
|
|
4288
|
+
const message = terminalText(error instanceof Error ? error.message : String(error));
|
|
4289
|
+
return { text: clip2(error instanceof ToolInputError ? message : `agent-roast: ${message}`), isError: true };
|
|
4290
|
+
} finally {
|
|
4291
|
+
clearTimeout(timer);
|
|
4292
|
+
inflight.delete(id);
|
|
4293
|
+
cancelled.delete(id);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
async function dispatch(id, method, params) {
|
|
4297
|
+
const meta = params._meta && typeof params._meta === "object" ? params._meta : {};
|
|
4298
|
+
const requested = meta[VERSION_KEY];
|
|
4299
|
+
const modern = typeof requested === "string";
|
|
4300
|
+
if (modern && !MODERN_VERSIONS.includes(requested)) {
|
|
4301
|
+
return fail(id, -32022, "Unsupported protocol version", { supported: [...MODERN_VERSIONS, ...LEGACY_VERSIONS], requested });
|
|
4302
|
+
}
|
|
4303
|
+
const capabilities = { tools: { listChanged: false } };
|
|
4304
|
+
switch (method) {
|
|
4305
|
+
case "initialize": {
|
|
4306
|
+
const asked = params.protocolVersion;
|
|
4307
|
+
const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
|
|
4308
|
+
return ok(id, { protocolVersion, capabilities, serverInfo, instructions: INSTRUCTIONS });
|
|
4309
|
+
}
|
|
4310
|
+
case "ping":
|
|
4311
|
+
return ok(id, {});
|
|
4312
|
+
case "server/discover":
|
|
4313
|
+
return ok(id, { resultType: "complete", supportedVersions: [...MODERN_VERSIONS, ...LEGACY_VERSIONS], capabilities, serverInfo, instructions: INSTRUCTIONS, ttlMs: CACHE_MS, cacheScope: "public" });
|
|
4314
|
+
case "tools/list":
|
|
4315
|
+
return ok(id, modern ? { resultType: "complete", tools: MCP_TOOLS, ttlMs: CACHE_MS, cacheScope: "public" } : { tools: MCP_TOOLS });
|
|
4316
|
+
case "tools/call": {
|
|
4317
|
+
const name = params.name;
|
|
4318
|
+
if (typeof name !== "string" || !MCP_TOOLS.some((tool) => tool.name === name)) {
|
|
4319
|
+
return fail(id, -32602, `Unknown tool: ${typeof name === "string" ? terminalText(name).slice(0, 100) : "(no name)"}`);
|
|
4320
|
+
}
|
|
4321
|
+
const outcome2 = await toolResult(id, name, params.arguments);
|
|
4322
|
+
if (!outcome2) return void 0;
|
|
4323
|
+
const content = [{ type: "text", text: outcome2.text }];
|
|
4324
|
+
return ok(id, modern ? { resultType: "complete", content, isError: outcome2.isError } : { content, isError: outcome2.isError });
|
|
4325
|
+
}
|
|
4326
|
+
default:
|
|
4327
|
+
return fail(id, -32601, `Method not found: ${terminalText(method).slice(0, 100)}`);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
async function handleOne(message) {
|
|
4331
|
+
if (!message || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0") return fail(null, -32600, "Invalid Request");
|
|
4332
|
+
const request = message;
|
|
4333
|
+
if (typeof request.method !== "string") return void 0;
|
|
4334
|
+
const params = request.params && typeof request.params === "object" && !Array.isArray(request.params) ? request.params : {};
|
|
4335
|
+
if (typeof request.id !== "string" && typeof request.id !== "number") {
|
|
4336
|
+
const target = params.requestId;
|
|
4337
|
+
if (request.method === "notifications/cancelled" && (typeof target === "string" || typeof target === "number") && inflight.has(target)) {
|
|
4338
|
+
cancelled.add(target);
|
|
4339
|
+
inflight.get(target)?.abort();
|
|
4340
|
+
}
|
|
4341
|
+
return void 0;
|
|
4342
|
+
}
|
|
4343
|
+
return dispatch(request.id, request.method, params);
|
|
4344
|
+
}
|
|
4345
|
+
return {
|
|
4346
|
+
async handle(message) {
|
|
4347
|
+
if (!Array.isArray(message)) return handleOne(message);
|
|
4348
|
+
if (!message.length) return fail(null, -32600, "Invalid Request");
|
|
4349
|
+
const responses = (await Promise.all(message.map(handleOne))).filter((response) => response !== void 0);
|
|
4350
|
+
return responses.length ? responses : void 0;
|
|
4351
|
+
}
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
async function serveMcp(options) {
|
|
4355
|
+
const server = createMcpServer(options);
|
|
4356
|
+
const output = options.output ?? process.stdout;
|
|
4357
|
+
output.on("error", () => {
|
|
4358
|
+
});
|
|
4359
|
+
const write = (message) => {
|
|
4360
|
+
output.write(JSON.stringify(message) + "\n");
|
|
4361
|
+
};
|
|
4362
|
+
const lines = readline.createInterface({ input: options.input ?? process.stdin, crlfDelay: Infinity });
|
|
4363
|
+
const running = /* @__PURE__ */ new Set();
|
|
4364
|
+
lines.on("line", (line) => {
|
|
4365
|
+
if (!line.trim()) return;
|
|
4366
|
+
let message;
|
|
4367
|
+
try {
|
|
4368
|
+
message = JSON.parse(line);
|
|
4369
|
+
} catch {
|
|
4370
|
+
write(fail(null, -32700, "Parse error"));
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const task = server.handle(message).then((response) => {
|
|
4374
|
+
if (response !== void 0) write(response);
|
|
4375
|
+
}).catch((error) => {
|
|
4376
|
+
process.stderr.write(`agent-roast mcp: ${terminalText(error instanceof Error ? error.message : String(error))}
|
|
4377
|
+
`);
|
|
4378
|
+
}).finally(() => {
|
|
4379
|
+
running.delete(task);
|
|
4380
|
+
});
|
|
4381
|
+
running.add(task);
|
|
4382
|
+
});
|
|
4383
|
+
await new Promise((resolve2) => lines.once("close", () => resolve2()));
|
|
4384
|
+
await Promise.allSettled([...running]);
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4082
4387
|
// src/demo.ts
|
|
4083
|
-
import * as
|
|
4388
|
+
import * as fs10 from "fs";
|
|
4084
4389
|
import os2 from "os";
|
|
4085
|
-
import
|
|
4390
|
+
import path11 from "path";
|
|
4086
4391
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4087
4392
|
import pc2 from "picocolors";
|
|
4088
4393
|
var CART = `export function total(prices: number[], discount = 0): number {
|
|
@@ -4124,7 +4429,7 @@ jobs:
|
|
|
4124
4429
|
${lenient ? " continue-on-error: true\n" : ""} run: npm test
|
|
4125
4430
|
`;
|
|
4126
4431
|
async function runDemo(options = {}) {
|
|
4127
|
-
const dir =
|
|
4432
|
+
const dir = fs10.mkdtempSync(path11.join(os2.tmpdir(), "agent-roast-demo-"));
|
|
4128
4433
|
try {
|
|
4129
4434
|
const git2 = (...args) => {
|
|
4130
4435
|
const run = spawnSync4(
|
|
@@ -4138,7 +4443,7 @@ async function runDemo(options = {}) {
|
|
|
4138
4443
|
"-c",
|
|
4139
4444
|
"commit.gpgsign=false",
|
|
4140
4445
|
"-c",
|
|
4141
|
-
`core.hooksPath=${
|
|
4446
|
+
`core.hooksPath=${path11.join(dir, ".no-hooks")}`,
|
|
4142
4447
|
...args
|
|
4143
4448
|
],
|
|
4144
4449
|
{ cwd: dir, env: getHermeticGitEnv(), encoding: "utf8", timeout: 3e4, windowsHide: true }
|
|
@@ -4146,9 +4451,9 @@ async function runDemo(options = {}) {
|
|
|
4146
4451
|
if (run.error || run.status !== 0) throw new ConfigError(`The demo needs Git to create a temporary repository: ${run.error?.message ?? run.stderr.trim()}`);
|
|
4147
4452
|
};
|
|
4148
4453
|
const write = (file, text) => {
|
|
4149
|
-
const destination =
|
|
4150
|
-
|
|
4151
|
-
|
|
4454
|
+
const destination = path11.join(dir, file);
|
|
4455
|
+
fs10.mkdirSync(path11.dirname(destination), { recursive: true });
|
|
4456
|
+
fs10.writeFileSync(destination, text);
|
|
4152
4457
|
};
|
|
4153
4458
|
git2("init", "-q", "-b", "main");
|
|
4154
4459
|
write("src/cart.ts", CART);
|
|
@@ -4182,7 +4487,7 @@ async function runDemo(options = {}) {
|
|
|
4182
4487
|
].join("\n");
|
|
4183
4488
|
return { exitCode: result.failedRules.length ? 0 : 1, output, result };
|
|
4184
4489
|
} finally {
|
|
4185
|
-
|
|
4490
|
+
fs10.rmSync(dir, { recursive: true, force: true });
|
|
4186
4491
|
}
|
|
4187
4492
|
}
|
|
4188
4493
|
|
|
@@ -4194,9 +4499,9 @@ var InvalidDetectorError = class extends Error {
|
|
|
4194
4499
|
}
|
|
4195
4500
|
};
|
|
4196
4501
|
async function roastRepository(options = {}) {
|
|
4197
|
-
const cwd =
|
|
4502
|
+
const cwd = path12.resolve(options.cwd || process.cwd());
|
|
4198
4503
|
const since = options.since || "90 days ago";
|
|
4199
|
-
const repoName =
|
|
4504
|
+
const repoName = path12.basename(cwd);
|
|
4200
4505
|
const collector = new MetricsCollector({ all: options.all, ignorePaths: options.ignorePaths, rules: options.rules, panicLoopWindowMinutes: options.panicLoopWindowMinutes });
|
|
4201
4506
|
const gitStream = streamGitLog(cwd, since);
|
|
4202
4507
|
await parseGitLogStream(gitStream, (commit, stats) => {
|
|
@@ -4211,7 +4516,7 @@ async function roastRepository(options = {}) {
|
|
|
4211
4516
|
return { summary, output };
|
|
4212
4517
|
}
|
|
4213
4518
|
async function auditPullRequest(options) {
|
|
4214
|
-
const cwd =
|
|
4519
|
+
const cwd = path12.resolve(options.cwd || process.cwd());
|
|
4215
4520
|
const format = options.format || "terminal";
|
|
4216
4521
|
const failOn = (options.failOn || ["test-skip"]).map((s) => s.trim()).filter(Boolean);
|
|
4217
4522
|
for (const det of failOn) {
|
|
@@ -4379,6 +4684,9 @@ export {
|
|
|
4379
4684
|
INSTRUCTIONS_END,
|
|
4380
4685
|
INSTRUCTIONS_BLOCK,
|
|
4381
4686
|
installInstructions,
|
|
4687
|
+
MCP_TOOLS,
|
|
4688
|
+
createMcpServer,
|
|
4689
|
+
serveMcp,
|
|
4382
4690
|
runDemo,
|
|
4383
4691
|
InvalidDetectorError,
|
|
4384
4692
|
roastRepository,
|
package/dist/cli.js
CHANGED
|
@@ -34,10 +34,11 @@ import {
|
|
|
34
34
|
roastRepository,
|
|
35
35
|
runDemo,
|
|
36
36
|
runHook,
|
|
37
|
+
serveMcp,
|
|
37
38
|
terminalText,
|
|
38
39
|
uninstallHooks,
|
|
39
40
|
writeBaseline
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-W6JP5YWF.js";
|
|
41
42
|
|
|
42
43
|
// src/cli.ts
|
|
43
44
|
import { Command } from "commander";
|
|
@@ -590,4 +591,12 @@ hook.command("resume").description("End a pause").action(async () => {
|
|
|
590
591
|
process.exitCode = 2;
|
|
591
592
|
}
|
|
592
593
|
});
|
|
594
|
+
program.command("mcp").description("Run as an MCP server over stdio, so any MCP-capable coding agent can review its own changes (read-only, offline)").option("--root <directory...>", "a directory the tools may review (repeatable); defaults to the project the server was started in").action(async (opts) => {
|
|
595
|
+
try {
|
|
596
|
+
await serveMcp({ version: pkg.version, roots: opts.root });
|
|
597
|
+
} catch (error) {
|
|
598
|
+
console.error(`agent-roast mcp: ${errorMessage(error)}`);
|
|
599
|
+
process.exitCode = 1;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
593
602
|
program.parse(process.argv);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Readable } from 'stream';
|
|
1
|
+
import { Readable, Writable } from 'stream';
|
|
2
2
|
|
|
3
3
|
/** A reviewed allowance for a precise pattern and number of occurrences.
|
|
4
4
|
* No wildcard, inline bypass, source snippet or executable policy is needed. */
|
|
@@ -493,9 +493,9 @@ interface HookOutcome {
|
|
|
493
493
|
}
|
|
494
494
|
/**
|
|
495
495
|
* What the agent reads when it is sent back: the offending changes, then the instruction to fix the code rather than the
|
|
496
|
-
* checks. Short on purpose, since it is added to the agent's context.
|
|
496
|
+
* checks. Short on purpose, since it is added to the agent's context. `subject` names what was reviewed.
|
|
497
497
|
*/
|
|
498
|
-
declare function formatAgentFeedback(result: ReviewResult): string;
|
|
498
|
+
declare function formatAgentFeedback(result: ReviewResult, subject?: string): string;
|
|
499
499
|
/** The time a pause lasts until, or null when the hook is active. Expired pauses are cleaned up. */
|
|
500
500
|
declare function hookPausedUntil(cwd: string, now?: Date): Promise<Date | null>;
|
|
501
501
|
declare function pauseHook(cwd: string, until: Date): Promise<string>;
|
|
@@ -558,6 +558,129 @@ declare function installInstructions(options?: {
|
|
|
558
558
|
action: "created" | "updated" | "unchanged";
|
|
559
559
|
}[];
|
|
560
560
|
|
|
561
|
+
/**
|
|
562
|
+
* A small MCP server over stdio, so any agent that speaks MCP can ask "did my changes weaken a check?" before it says it is
|
|
563
|
+
* done. It is read-only and offline like the rest of agent-roast, and written without a dependency on purpose: the built files
|
|
564
|
+
* contain no network code, which a test checks. It answers both protocol eras: the initialize handshake of 2025-11-25 and
|
|
565
|
+
* earlier, and the per-request metadata of 2026-07-28 (modelcontextprotocol.io/specification/2026-07-28/basic/versioning).
|
|
566
|
+
*/
|
|
567
|
+
type RpcId = string | number;
|
|
568
|
+
type RpcResponse = {
|
|
569
|
+
jsonrpc: "2.0";
|
|
570
|
+
id: RpcId | null;
|
|
571
|
+
} & ({
|
|
572
|
+
result: unknown;
|
|
573
|
+
} | {
|
|
574
|
+
error: {
|
|
575
|
+
code: number;
|
|
576
|
+
message: string;
|
|
577
|
+
data?: unknown;
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
declare const MCP_TOOLS: readonly [{
|
|
581
|
+
readonly name: "review_worktree";
|
|
582
|
+
readonly title: "Review uncommitted changes";
|
|
583
|
+
readonly description: string;
|
|
584
|
+
readonly inputSchema: {
|
|
585
|
+
readonly type: "object";
|
|
586
|
+
readonly properties: {
|
|
587
|
+
readonly path: {
|
|
588
|
+
readonly type: "string";
|
|
589
|
+
readonly description: "A directory inside the project. Defaults to the project the server was started in.";
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
readonly additionalProperties: false;
|
|
593
|
+
};
|
|
594
|
+
readonly annotations: {
|
|
595
|
+
readonly readOnlyHint: true;
|
|
596
|
+
readonly destructiveHint: false;
|
|
597
|
+
readonly idempotentHint: true;
|
|
598
|
+
readonly openWorldHint: false;
|
|
599
|
+
readonly title: "Review uncommitted changes";
|
|
600
|
+
};
|
|
601
|
+
}, {
|
|
602
|
+
readonly name: "review_changes";
|
|
603
|
+
readonly title: "Review a range of commits";
|
|
604
|
+
readonly description: string;
|
|
605
|
+
readonly inputSchema: {
|
|
606
|
+
readonly type: "object";
|
|
607
|
+
readonly properties: {
|
|
608
|
+
readonly base: {
|
|
609
|
+
readonly type: "string";
|
|
610
|
+
readonly description: "The revision to compare against, for example main or origin/main.";
|
|
611
|
+
};
|
|
612
|
+
readonly head: {
|
|
613
|
+
readonly type: "string";
|
|
614
|
+
readonly description: "The revision under review. Defaults to HEAD.";
|
|
615
|
+
};
|
|
616
|
+
readonly path: {
|
|
617
|
+
readonly type: "string";
|
|
618
|
+
readonly description: "A directory inside the project. Defaults to the project the server was started in.";
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
readonly required: readonly ["base"];
|
|
622
|
+
readonly additionalProperties: false;
|
|
623
|
+
};
|
|
624
|
+
readonly annotations: {
|
|
625
|
+
readonly readOnlyHint: true;
|
|
626
|
+
readonly destructiveHint: false;
|
|
627
|
+
readonly idempotentHint: true;
|
|
628
|
+
readonly openWorldHint: false;
|
|
629
|
+
readonly title: "Review a range of commits";
|
|
630
|
+
};
|
|
631
|
+
}, {
|
|
632
|
+
readonly name: "stop_hook";
|
|
633
|
+
readonly title: "Stop hook adapter";
|
|
634
|
+
readonly description: string;
|
|
635
|
+
readonly inputSchema: {
|
|
636
|
+
readonly type: "object";
|
|
637
|
+
readonly properties: {
|
|
638
|
+
readonly cwd: {
|
|
639
|
+
readonly type: "string";
|
|
640
|
+
readonly description: "A directory inside the project. Defaults to the project the server was started in.";
|
|
641
|
+
};
|
|
642
|
+
readonly stop_hook_active: {
|
|
643
|
+
readonly type: "string";
|
|
644
|
+
readonly description: "\"true\" when the agent is already continuing because of a Stop hook.";
|
|
645
|
+
};
|
|
646
|
+
readonly loop_count: {
|
|
647
|
+
readonly type: "string";
|
|
648
|
+
readonly description: "How many times the agent was already sent back, for agents that count.";
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
readonly additionalProperties: true;
|
|
652
|
+
};
|
|
653
|
+
readonly annotations: {
|
|
654
|
+
readonly readOnlyHint: true;
|
|
655
|
+
readonly destructiveHint: false;
|
|
656
|
+
readonly idempotentHint: true;
|
|
657
|
+
readonly openWorldHint: false;
|
|
658
|
+
readonly title: "Stop hook adapter";
|
|
659
|
+
};
|
|
660
|
+
}];
|
|
661
|
+
interface McpServerOptions {
|
|
662
|
+
/** The version reported to clients. */
|
|
663
|
+
version: string;
|
|
664
|
+
/** Directories the tools may read. Defaults to the project the server was started in. */
|
|
665
|
+
roots?: string[];
|
|
666
|
+
/** The project directory when the server was started; defaults to CLAUDE_PROJECT_DIR, CURSOR_PROJECT_DIR, then the working directory. */
|
|
667
|
+
cwd?: string;
|
|
668
|
+
env?: NodeJS.ProcessEnv;
|
|
669
|
+
/** How long one tool call may take before it is stopped. */
|
|
670
|
+
timeoutMs?: number;
|
|
671
|
+
}
|
|
672
|
+
interface McpServer {
|
|
673
|
+
/** Answers one JSON-RPC message (or a batch), or nothing for a notification. */
|
|
674
|
+
handle(message: unknown): Promise<RpcResponse | RpcResponse[] | undefined>;
|
|
675
|
+
}
|
|
676
|
+
declare function createMcpServer(options: McpServerOptions): McpServer;
|
|
677
|
+
interface ServeMcpOptions extends McpServerOptions {
|
|
678
|
+
input?: Readable;
|
|
679
|
+
output?: Writable;
|
|
680
|
+
}
|
|
681
|
+
/** Reads newline-delimited JSON-RPC from the input and writes responses to the output until the input closes. */
|
|
682
|
+
declare function serveMcp(options: ServeMcpOptions): Promise<void>;
|
|
683
|
+
|
|
561
684
|
interface DemoResult {
|
|
562
685
|
exitCode: number;
|
|
563
686
|
output: string;
|
|
@@ -613,4 +736,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
|
|
|
613
736
|
}>;
|
|
614
737
|
declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
|
|
615
738
|
|
|
616
|
-
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, 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 ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, 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, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, uninstallHooks, unquoteGitPath, validateExceptions, writeBaseline };
|
|
739
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
INSTRUCTIONS_START,
|
|
17
17
|
InvalidDetectorError,
|
|
18
18
|
MAX_CONFIG_BYTES,
|
|
19
|
+
MCP_TOOLS,
|
|
19
20
|
MetricsCollector,
|
|
20
21
|
PRE_COMMIT_HOOK_TEMPLATE,
|
|
21
22
|
RECORD_SEPARATOR,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
compareFindings,
|
|
30
31
|
configFromText,
|
|
31
32
|
createBaseline,
|
|
33
|
+
createMcpServer,
|
|
32
34
|
detectAiAttribution,
|
|
33
35
|
findMergeBase,
|
|
34
36
|
findingFingerprint,
|
|
@@ -80,6 +82,7 @@ import {
|
|
|
80
82
|
roastRepository,
|
|
81
83
|
runDemo,
|
|
82
84
|
runHook,
|
|
85
|
+
serveMcp,
|
|
83
86
|
streamGitDiff,
|
|
84
87
|
streamGitLog,
|
|
85
88
|
stripStringAndRegexLiterals,
|
|
@@ -88,7 +91,7 @@ import {
|
|
|
88
91
|
unquoteGitPath,
|
|
89
92
|
validateExceptions,
|
|
90
93
|
writeBaseline
|
|
91
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-W6JP5YWF.js";
|
|
92
95
|
export {
|
|
93
96
|
AGENT_LABELS,
|
|
94
97
|
CONFIG_FILENAMES,
|
|
@@ -106,6 +109,7 @@ export {
|
|
|
106
109
|
INSTRUCTIONS_START,
|
|
107
110
|
InvalidDetectorError,
|
|
108
111
|
MAX_CONFIG_BYTES,
|
|
112
|
+
MCP_TOOLS,
|
|
109
113
|
MetricsCollector,
|
|
110
114
|
PRE_COMMIT_HOOK_TEMPLATE,
|
|
111
115
|
RECORD_SEPARATOR,
|
|
@@ -119,6 +123,7 @@ export {
|
|
|
119
123
|
compareFindings,
|
|
120
124
|
configFromText,
|
|
121
125
|
createBaseline,
|
|
126
|
+
createMcpServer,
|
|
122
127
|
detectAiAttribution,
|
|
123
128
|
findMergeBase,
|
|
124
129
|
findingFingerprint,
|
|
@@ -170,6 +175,7 @@ export {
|
|
|
170
175
|
roastRepository,
|
|
171
176
|
runDemo,
|
|
172
177
|
runHook,
|
|
178
|
+
serveMcp,
|
|
173
179
|
streamGitDiff,
|
|
174
180
|
streamGitLog,
|
|
175
181
|
stripStringAndRegexLiterals,
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-roast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Stops AI coding agents (and rushed humans) from turning a build green by weakening tests, coverage gates or CI. Hook for Claude Code, Codex and Cursor, pre-commit, GitHub Action. Offline.",
|
|
5
|
+
"mcpName": "io.github.Nari-Ab/agent-roast",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/index.js",
|
|
7
8
|
"types": "./dist/index.d.ts",
|