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 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
- - **64 of 64** hand-labeled cheating edits are caught and **44 of 44** harmless look-alikes (reformats, moves, raised
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 path11 from "path";
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
- test: /^\s*(?:it|specify|example|scenario|test|before|after|around)\b|^\s*def\s+test_/,
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
- { type: "test-skip", regex: /\[\s*Ignore\b|\b(?:Fact|Theory)\s*\(\s*(?:[^)]*,\s*)?Skip\s*=/, name: "ignored .NET test ([Ignore] / Skip =)" },
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
- { type: "test-skip", regex: /\b(?:it|describe|context|specify|scenario)\b[^\n]*[,(]\s*(?::skip\b|skip:\s*(?:true\b|["']))/, name: "disabled RSpec example (skip: true)" },
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
- { type: "test-skip", regex: /@(?:Test|Suite)\s*\([^)]*\.disabled\b/, name: "disabled Swift Testing test (.disabled)" },
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
- for (let i = lines.length - 2; i >= 0 && lines.length - 1 - i <= 200; i--) {
1268
- if (lines[i].number !== lines[i + 1].number - 1) return false;
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 false;
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 false;
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 clip2 = (part, size) => part.length > size ? `${part.slice(0, size - 1)}\u2026` : part;
3678
- if (arrow < 0) return clip2(text, limit);
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 `${clip2(text.slice(0, arrow), side)} \u2192 ${clip2(text.slice(arrow + 3), side)}`;
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 = ["agent-roast: the uncommitted changes make the checks easier to pass instead of making the code correct.", ""];
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 fs9 from "fs";
4388
+ import * as fs10 from "fs";
4084
4389
  import os2 from "os";
4085
- import path10 from "path";
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 = fs9.mkdtempSync(path10.join(os2.tmpdir(), "agent-roast-demo-"));
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=${path10.join(dir, ".no-hooks")}`,
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 = path10.join(dir, file);
4150
- fs9.mkdirSync(path10.dirname(destination), { recursive: true });
4151
- fs9.writeFileSync(destination, text);
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
- fs9.rmSync(dir, { recursive: true, force: true });
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 = path11.resolve(options.cwd || process.cwd());
4502
+ const cwd = path12.resolve(options.cwd || process.cwd());
4198
4503
  const since = options.since || "90 days ago";
4199
- const repoName = path11.basename(cwd);
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 = path11.resolve(options.cwd || process.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-AP5Y4CH6.js";
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-AP5Y4CH6.js";
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.12.1",
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",