agent-roast 0.12.2 → 0.14.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,13 +40,14 @@ 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
47
48
  warning rather than an endless loop. Anything agent-roast cannot check lets the agent finish: it never breaks a session. An
48
- intended change is accepted by committing it. Setup for each agent, what it does not do, and how to pause it:
49
- [docs/integrations.md](docs/integrations.md).
49
+ intended change is accepted by committing it. Not sure yet? `npx agent-roast hook install --advisory` only warns you and never
50
+ sends the agent back. Setup for each agent, what it does not do, and how to pause it: [docs/integrations.md](docs/integrations.md).
50
51
 
51
52
  > The agent hooks follow each agent's documented hook protocol and are tested with simulated hook input, not yet in long live
52
53
  > sessions with every agent. Reports of what you see are welcome.
@@ -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";
@@ -3711,10 +3711,10 @@ var plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
3711
3711
  function clipEvidence(text, limit = 200) {
3712
3712
  if (text.length <= limit) return text;
3713
3713
  const arrow = text.indexOf(" \u2192 ");
3714
- const clip2 = (part, size) => part.length > size ? `${part.slice(0, size - 1)}\u2026` : part;
3715
- 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);
3716
3716
  const side = Math.floor((limit - 3) / 2);
3717
- 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)}`;
3718
3718
  }
3719
3719
  async function replayHistory(options = {}) {
3720
3720
  assertNotAborted(options.signal);
@@ -3848,11 +3848,11 @@ function describe(finding2, index) {
3848
3848
  const evidence = clip(finding2.lineSnippet || finding2.reason, 220);
3849
3849
  return [`${index}. ${where} [${finding2.type}]`, ` ${evidence}`];
3850
3850
  }
3851
- function formatAgentFeedback(result) {
3851
+ function formatAgentFeedback(result, subject = "the uncommitted changes") {
3852
3852
  const ranked = mostImportantFirst(result.infractions, result.failedRules);
3853
3853
  const blocking = ranked.filter((finding2) => result.failedRules.includes(finding2.type));
3854
3854
  const notes = ranked.filter((finding2) => !result.failedRules.includes(finding2.type) && isCheckRule(finding2.type));
3855
- 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.`, ""];
3856
3856
  blocking.slice(0, LISTED).forEach((finding2, index) => lines.push(...describe(finding2, index + 1)));
3857
3857
  if (blocking.length > LISTED) lines.push(` \u2026 and ${blocking.length - LISTED} more.`);
3858
3858
  if (notes.length) {
@@ -3911,7 +3911,9 @@ async function runHook(rawInput, options = {}) {
3911
3911
  } catch {
3912
3912
  return outcome("skip", "", "agent-roast hook: input was not JSON; nothing was checked.\n");
3913
3913
  }
3914
- if (["off", "0", "false"].includes((env.AGENT_ROAST_HOOK ?? "").toLowerCase())) return outcome("skip");
3914
+ const mode = (env.AGENT_ROAST_HOOK ?? "").toLowerCase();
3915
+ if (["off", "0", "false"].includes(mode)) return outcome("skip");
3916
+ const advisory = options.advisory === true || mode === "advisory";
3915
3917
  const agent = options.agent ?? detectAgent(input);
3916
3918
  const event = String(input.hook_event_name ?? "Stop");
3917
3919
  if (event.toLowerCase() !== "stop") return outcome("skip");
@@ -3932,8 +3934,10 @@ async function runHook(rawInput, options = {}) {
3932
3934
  }
3933
3935
  if (!result.failedRules.length) return outcome("pass", agent === "cursor" ? "{}\n" : "", "", result);
3934
3936
  const alreadyAsked = input.stop_hook_active === true || typeof input.loop_count === "number" && input.loop_count > 0;
3935
- if (alreadyAsked) {
3936
- return outcome("warn", agent === "cursor" ? "{}\n" : JSON.stringify({ systemMessage: formatUserWarning(result) }) + "\n", "", result);
3937
+ if (alreadyAsked || advisory) {
3938
+ const warning = formatUserWarning(result);
3939
+ return outcome("warn", agent === "cursor" ? "{}\n" : JSON.stringify({ systemMessage: warning }) + "\n", agent === "cursor" && advisory ? `${warning}
3940
+ ` : "", result);
3937
3941
  }
3938
3942
  const message = formatAgentFeedback(result);
3939
3943
  return outcome("block", JSON.stringify(agent === "cursor" ? { followup_message: message } : { decision: "block", reason: message }) + "\n", "", result);
@@ -3952,9 +3956,10 @@ function packageVersion() {
3952
3956
  const require3 = createRequire2(import.meta.url);
3953
3957
  return String(require3("../package.json").version);
3954
3958
  }
3955
- function hookCommand(agent, command = `npx --yes agent-roast@${packageVersion()}`) {
3956
- return `${command.trim()} hook run --agent ${agent}`;
3959
+ function hookCommand(agent, command = `npx --yes agent-roast@${packageVersion()}`, advisory = false) {
3960
+ return `${command.trim()} hook run --agent ${agent}${advisory ? " --advisory" : ""}`;
3957
3961
  }
3962
+ var isAdvisory = (command) => Boolean(command && /(^|\s)--advisory(\s|$)/.test(command));
3958
3963
  var isOurs = (command) => typeof command === "string" && /\bagent-roast\b.*\bhook\s+run\b/.test(command);
3959
3964
  function repositoryRoot(cwd) {
3960
3965
  const absolute = fs8.realpathSync(path9.resolve(cwd));
@@ -4051,7 +4056,7 @@ function installHooks(options) {
4051
4056
  const { root, scope, home, env } = context(options);
4052
4057
  return options.agents.map((agent) => {
4053
4058
  const file = hookConfigPath(agent, scope, root, home, env);
4054
- const command = hookCommand(agent, options.command);
4059
+ const command = hookCommand(agent, options.command, options.advisory);
4055
4060
  const json = readJsonFile(file);
4056
4061
  const changed = addStopHook(agent, json.data, command);
4057
4062
  if (changed && !options.dryRun) writeJsonFile(file, json);
@@ -4082,7 +4087,7 @@ function hookStatus(options) {
4082
4087
  command = handlers.map((handler) => handler?.command).find(isOurs);
4083
4088
  } catch {
4084
4089
  }
4085
- rows.push({ agent, scope, file, command, installed: Boolean(command), action: command ? "unchanged" : "absent" });
4090
+ rows.push({ agent, scope, file, command, installed: Boolean(command), advisory: isAdvisory(command), action: command ? "unchanged" : "absent" });
4086
4091
  }
4087
4092
  }
4088
4093
  return rows;
@@ -4116,10 +4121,278 @@ function installInstructions(options = {}) {
4116
4121
  });
4117
4122
  }
4118
4123
 
4124
+ // src/mcp.ts
4125
+ import * as fs9 from "fs/promises";
4126
+ import path10 from "path";
4127
+ import readline from "readline";
4128
+ var MODERN_VERSIONS = ["2026-07-28"];
4129
+ var LEGACY_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
4130
+ var VERSION_KEY = "io.modelcontextprotocol/protocolVersion";
4131
+ var TOOL_TIMEOUT_MS = 25e3;
4132
+ var CACHE_MS = 36e5;
4133
+ var MAX_TEXT = 6e3;
4134
+ var LISTED_NOTES = 3;
4135
+ 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.";
4136
+ var READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
4137
+ var PATH_SCHEMA = { type: "string", description: "A directory inside the project. Defaults to the project the server was started in." };
4138
+ var MCP_TOOLS = [
4139
+ {
4140
+ name: "review_worktree",
4141
+ title: "Review uncommitted changes",
4142
+ 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.",
4143
+ inputSchema: { type: "object", properties: { path: PATH_SCHEMA }, additionalProperties: false },
4144
+ annotations: { title: "Review uncommitted changes", ...READ_ONLY }
4145
+ },
4146
+ {
4147
+ name: "review_changes",
4148
+ title: "Review a range of commits",
4149
+ 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.",
4150
+ inputSchema: {
4151
+ type: "object",
4152
+ properties: {
4153
+ base: { type: "string", description: "The revision to compare against, for example main or origin/main." },
4154
+ head: { type: "string", description: "The revision under review. Defaults to HEAD." },
4155
+ path: PATH_SCHEMA
4156
+ },
4157
+ required: ["base"],
4158
+ additionalProperties: false
4159
+ },
4160
+ annotations: { title: "Review a range of commits", ...READ_ONLY }
4161
+ },
4162
+ {
4163
+ name: "stop_hook",
4164
+ title: "Stop hook adapter",
4165
+ 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.",
4166
+ inputSchema: {
4167
+ type: "object",
4168
+ properties: {
4169
+ cwd: PATH_SCHEMA,
4170
+ stop_hook_active: { type: "string", description: '"true" when the agent is already continuing because of a Stop hook.' },
4171
+ loop_count: { type: "string", description: "How many times the agent was already sent back, for agents that count." }
4172
+ },
4173
+ additionalProperties: true
4174
+ },
4175
+ annotations: { title: "Stop hook adapter", ...READ_ONLY }
4176
+ }
4177
+ ];
4178
+ var ToolInputError = class extends Error {
4179
+ };
4180
+ var ok = (id, result) => ({ jsonrpc: "2.0", id, result });
4181
+ var fail = (id, code, message, data) => ({ jsonrpc: "2.0", id, error: data === void 0 ? { code, message } : { code, message, data } });
4182
+ var clip2 = (text) => text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT - 1)}\u2026` : text;
4183
+ function optionalText(value, name) {
4184
+ if (value === void 0 || value === null || value === "") return void 0;
4185
+ if (typeof value !== "string" || value.length > 300 || value.includes("\0")) throw new ToolInputError(`${name} must be a revision name of at most 300 characters.`);
4186
+ return value;
4187
+ }
4188
+ function summarize(result, subject) {
4189
+ const blocking = result.infractions.filter((finding2) => result.failedRules.includes(finding2.type));
4190
+ const lines = [];
4191
+ if (blocking.length) lines.push(formatAgentFeedback(result, subject));
4192
+ else {
4193
+ 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.`);
4194
+ const notes = result.infractions.filter((finding2) => isCheckRule(finding2.type));
4195
+ if (notes.length) {
4196
+ lines.push("Worth a second look (these do not block):");
4197
+ for (const note of notes.slice(0, LISTED_NOTES)) {
4198
+ const where = `${terminalText(note.file ?? "unknown location")}${note.lineNumber ? `:${note.lineNumber}` : ""}`;
4199
+ lines.push(`- ${where} [${note.type}] ${terminalText(note.lineSnippet || note.reason).replace(/\s+/g, " ").trim().slice(0, 120)}`);
4200
+ }
4201
+ if (notes.length > LISTED_NOTES) lines.push(`- \u2026 and ${notes.length - LISTED_NOTES} more.`);
4202
+ }
4203
+ }
4204
+ if (!result.complete) {
4205
+ const first = result.skippedFiles[0];
4206
+ 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.`);
4207
+ }
4208
+ return lines.join("\n");
4209
+ }
4210
+ function createMcpServer(options) {
4211
+ const env = options.env ?? process.env;
4212
+ const timeoutMs = options.timeoutMs ?? TOOL_TIMEOUT_MS;
4213
+ const serverInfo = { name: "agent-roast", title: "Agent Roast", version: options.version };
4214
+ const inflight = /* @__PURE__ */ new Map();
4215
+ const cancelled = /* @__PURE__ */ new Set();
4216
+ let allowed;
4217
+ const startDirectory = () => path10.resolve(options.cwd ?? (env.CLAUDE_PROJECT_DIR || env.CURSOR_PROJECT_DIR || process.cwd()));
4218
+ const roots = () => allowed ??= Promise.all(
4219
+ (options.roots?.length ? options.roots : [startDirectory()]).map((root) => fs9.realpath(path10.resolve(root)).catch(() => path10.resolve(root)))
4220
+ );
4221
+ async function gitCommonDirectory(directory) {
4222
+ try {
4223
+ const output = gitOutputPath((await readGit(directory, ["rev-parse", "--git-common-dir"], void 0, 8192)).toString());
4224
+ return await fs9.realpath(path10.resolve(directory, output));
4225
+ } catch {
4226
+ return null;
4227
+ }
4228
+ }
4229
+ async function permitted(real, list) {
4230
+ for (const root of list) {
4231
+ const relative2 = path10.relative(root, real);
4232
+ if (relative2 === "" || relative2 !== ".." && !relative2.startsWith(".." + path10.sep) && !path10.isAbsolute(relative2)) return true;
4233
+ }
4234
+ const common = await gitCommonDirectory(real);
4235
+ if (!common) return false;
4236
+ for (const root of list) if (common === await gitCommonDirectory(root)) return true;
4237
+ return false;
4238
+ }
4239
+ async function project(requested) {
4240
+ if (requested !== void 0 && requested !== null && requested !== "" && (typeof requested !== "string" || requested.includes("\0"))) throw new ToolInputError("path must be a directory path.");
4241
+ const list = await roots();
4242
+ if (!options.roots?.length && list.every((root) => path10.parse(root).root === root)) {
4243
+ 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.");
4244
+ }
4245
+ const target = typeof requested === "string" && requested ? path10.resolve(list[0], requested) : list[0];
4246
+ let real;
4247
+ try {
4248
+ real = await fs9.realpath(target);
4249
+ } catch {
4250
+ throw new ToolInputError(`${terminalText(typeof requested === "string" && requested ? requested : target)} does not exist.`);
4251
+ }
4252
+ 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.");
4253
+ return real;
4254
+ }
4255
+ async function callTool(name, args, signal) {
4256
+ switch (name) {
4257
+ case "review_worktree":
4258
+ return summarize(await reviewWorktreeChanges({ cwd: await project(args.path), signal }), "the uncommitted changes");
4259
+ case "review_changes": {
4260
+ const base = optionalText(args.base, "base");
4261
+ if (!base) throw new ToolInputError("base is required, for example main or origin/main.");
4262
+ const head = optionalText(args.head, "head");
4263
+ const cwd = await project(args.path);
4264
+ return summarize(await reviewChanges({ cwd, base, head, signal }), `the changes between ${terminalText(base)} and ${terminalText(head ?? "HEAD")}`);
4265
+ }
4266
+ case "stop_hook": {
4267
+ const cwd = await project(args.cwd);
4268
+ const input = { hook_event_name: "Stop", cwd, stop_hook_active: args.stop_hook_active === true || args.stop_hook_active === "true" };
4269
+ const loops = Number(args.loop_count);
4270
+ if (args.loop_count !== void 0 && args.loop_count !== "" && Number.isFinite(loops)) input.loop_count = loops;
4271
+ const outcome2 = await runHook(JSON.stringify(input), { agent: "claude", cwd, env, timeoutMs });
4272
+ if (outcome2.stderr) process.stderr.write(outcome2.stderr);
4273
+ return outcome2.stdout.trim() || "{}";
4274
+ }
4275
+ default:
4276
+ throw new ToolInputError(`Unknown tool: ${terminalText(name).slice(0, 100)}`);
4277
+ }
4278
+ }
4279
+ async function toolResult(id, name, rawArguments) {
4280
+ const controller = new AbortController();
4281
+ inflight.set(id, controller);
4282
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4283
+ try {
4284
+ const args = rawArguments ?? {};
4285
+ if (typeof args !== "object" || Array.isArray(args)) throw new ToolInputError("arguments must be an object.");
4286
+ return { text: clip2(await callTool(name, args, controller.signal)), isError: false };
4287
+ } catch (error) {
4288
+ if (cancelled.has(id)) return null;
4289
+ if (controller.signal.aborted) {
4290
+ const limit = timeoutMs >= 1e3 ? `${Math.round(timeoutMs / 1e3)} seconds` : `${timeoutMs} ms`;
4291
+ return { text: `agent-roast: the check took longer than ${limit} and was stopped. Nothing was changed.`, isError: true };
4292
+ }
4293
+ const message = terminalText(error instanceof Error ? error.message : String(error));
4294
+ return { text: clip2(error instanceof ToolInputError ? message : `agent-roast: ${message}`), isError: true };
4295
+ } finally {
4296
+ clearTimeout(timer);
4297
+ inflight.delete(id);
4298
+ cancelled.delete(id);
4299
+ }
4300
+ }
4301
+ async function dispatch(id, method, params) {
4302
+ const meta = params._meta && typeof params._meta === "object" ? params._meta : {};
4303
+ const requested = meta[VERSION_KEY];
4304
+ const modern = typeof requested === "string";
4305
+ if (modern && !MODERN_VERSIONS.includes(requested)) {
4306
+ return fail(id, -32022, "Unsupported protocol version", { supported: [...MODERN_VERSIONS, ...LEGACY_VERSIONS], requested });
4307
+ }
4308
+ const capabilities = { tools: { listChanged: false } };
4309
+ switch (method) {
4310
+ case "initialize": {
4311
+ const asked = params.protocolVersion;
4312
+ const protocolVersion = typeof asked === "string" && LEGACY_VERSIONS.includes(asked) ? asked : LEGACY_VERSIONS[0];
4313
+ return ok(id, { protocolVersion, capabilities, serverInfo, instructions: INSTRUCTIONS });
4314
+ }
4315
+ case "ping":
4316
+ return ok(id, {});
4317
+ case "server/discover":
4318
+ return ok(id, { resultType: "complete", supportedVersions: [...MODERN_VERSIONS, ...LEGACY_VERSIONS], capabilities, serverInfo, instructions: INSTRUCTIONS, ttlMs: CACHE_MS, cacheScope: "public" });
4319
+ case "tools/list":
4320
+ return ok(id, modern ? { resultType: "complete", tools: MCP_TOOLS, ttlMs: CACHE_MS, cacheScope: "public" } : { tools: MCP_TOOLS });
4321
+ case "tools/call": {
4322
+ const name = params.name;
4323
+ if (typeof name !== "string" || !MCP_TOOLS.some((tool) => tool.name === name)) {
4324
+ return fail(id, -32602, `Unknown tool: ${typeof name === "string" ? terminalText(name).slice(0, 100) : "(no name)"}`);
4325
+ }
4326
+ const outcome2 = await toolResult(id, name, params.arguments);
4327
+ if (!outcome2) return void 0;
4328
+ const content = [{ type: "text", text: outcome2.text }];
4329
+ return ok(id, modern ? { resultType: "complete", content, isError: outcome2.isError } : { content, isError: outcome2.isError });
4330
+ }
4331
+ default:
4332
+ return fail(id, -32601, `Method not found: ${terminalText(method).slice(0, 100)}`);
4333
+ }
4334
+ }
4335
+ async function handleOne(message) {
4336
+ if (!message || typeof message !== "object" || Array.isArray(message) || message.jsonrpc !== "2.0") return fail(null, -32600, "Invalid Request");
4337
+ const request = message;
4338
+ if (typeof request.method !== "string") return void 0;
4339
+ const params = request.params && typeof request.params === "object" && !Array.isArray(request.params) ? request.params : {};
4340
+ if (typeof request.id !== "string" && typeof request.id !== "number") {
4341
+ const target = params.requestId;
4342
+ if (request.method === "notifications/cancelled" && (typeof target === "string" || typeof target === "number") && inflight.has(target)) {
4343
+ cancelled.add(target);
4344
+ inflight.get(target)?.abort();
4345
+ }
4346
+ return void 0;
4347
+ }
4348
+ return dispatch(request.id, request.method, params);
4349
+ }
4350
+ return {
4351
+ async handle(message) {
4352
+ if (!Array.isArray(message)) return handleOne(message);
4353
+ if (!message.length) return fail(null, -32600, "Invalid Request");
4354
+ const responses = (await Promise.all(message.map(handleOne))).filter((response) => response !== void 0);
4355
+ return responses.length ? responses : void 0;
4356
+ }
4357
+ };
4358
+ }
4359
+ async function serveMcp(options) {
4360
+ const server = createMcpServer(options);
4361
+ const output = options.output ?? process.stdout;
4362
+ output.on("error", () => {
4363
+ });
4364
+ const write = (message) => {
4365
+ output.write(JSON.stringify(message) + "\n");
4366
+ };
4367
+ const lines = readline.createInterface({ input: options.input ?? process.stdin, crlfDelay: Infinity });
4368
+ const running = /* @__PURE__ */ new Set();
4369
+ lines.on("line", (line) => {
4370
+ if (!line.trim()) return;
4371
+ let message;
4372
+ try {
4373
+ message = JSON.parse(line);
4374
+ } catch {
4375
+ write(fail(null, -32700, "Parse error"));
4376
+ return;
4377
+ }
4378
+ const task = server.handle(message).then((response) => {
4379
+ if (response !== void 0) write(response);
4380
+ }).catch((error) => {
4381
+ process.stderr.write(`agent-roast mcp: ${terminalText(error instanceof Error ? error.message : String(error))}
4382
+ `);
4383
+ }).finally(() => {
4384
+ running.delete(task);
4385
+ });
4386
+ running.add(task);
4387
+ });
4388
+ await new Promise((resolve2) => lines.once("close", () => resolve2()));
4389
+ await Promise.allSettled([...running]);
4390
+ }
4391
+
4119
4392
  // src/demo.ts
4120
- import * as fs9 from "fs";
4393
+ import * as fs10 from "fs";
4121
4394
  import os2 from "os";
4122
- import path10 from "path";
4395
+ import path11 from "path";
4123
4396
  import { spawnSync as spawnSync4 } from "child_process";
4124
4397
  import pc2 from "picocolors";
4125
4398
  var CART = `export function total(prices: number[], discount = 0): number {
@@ -4161,7 +4434,7 @@ jobs:
4161
4434
  ${lenient ? " continue-on-error: true\n" : ""} run: npm test
4162
4435
  `;
4163
4436
  async function runDemo(options = {}) {
4164
- const dir = fs9.mkdtempSync(path10.join(os2.tmpdir(), "agent-roast-demo-"));
4437
+ const dir = fs10.mkdtempSync(path11.join(os2.tmpdir(), "agent-roast-demo-"));
4165
4438
  try {
4166
4439
  const git2 = (...args) => {
4167
4440
  const run = spawnSync4(
@@ -4175,7 +4448,7 @@ async function runDemo(options = {}) {
4175
4448
  "-c",
4176
4449
  "commit.gpgsign=false",
4177
4450
  "-c",
4178
- `core.hooksPath=${path10.join(dir, ".no-hooks")}`,
4451
+ `core.hooksPath=${path11.join(dir, ".no-hooks")}`,
4179
4452
  ...args
4180
4453
  ],
4181
4454
  { cwd: dir, env: getHermeticGitEnv(), encoding: "utf8", timeout: 3e4, windowsHide: true }
@@ -4183,9 +4456,9 @@ async function runDemo(options = {}) {
4183
4456
  if (run.error || run.status !== 0) throw new ConfigError(`The demo needs Git to create a temporary repository: ${run.error?.message ?? run.stderr.trim()}`);
4184
4457
  };
4185
4458
  const write = (file, text) => {
4186
- const destination = path10.join(dir, file);
4187
- fs9.mkdirSync(path10.dirname(destination), { recursive: true });
4188
- fs9.writeFileSync(destination, text);
4459
+ const destination = path11.join(dir, file);
4460
+ fs10.mkdirSync(path11.dirname(destination), { recursive: true });
4461
+ fs10.writeFileSync(destination, text);
4189
4462
  };
4190
4463
  git2("init", "-q", "-b", "main");
4191
4464
  write("src/cart.ts", CART);
@@ -4219,7 +4492,7 @@ async function runDemo(options = {}) {
4219
4492
  ].join("\n");
4220
4493
  return { exitCode: result.failedRules.length ? 0 : 1, output, result };
4221
4494
  } finally {
4222
- fs9.rmSync(dir, { recursive: true, force: true });
4495
+ fs10.rmSync(dir, { recursive: true, force: true });
4223
4496
  }
4224
4497
  }
4225
4498
 
@@ -4231,9 +4504,9 @@ var InvalidDetectorError = class extends Error {
4231
4504
  }
4232
4505
  };
4233
4506
  async function roastRepository(options = {}) {
4234
- const cwd = path11.resolve(options.cwd || process.cwd());
4507
+ const cwd = path12.resolve(options.cwd || process.cwd());
4235
4508
  const since = options.since || "90 days ago";
4236
- const repoName = path11.basename(cwd);
4509
+ const repoName = path12.basename(cwd);
4237
4510
  const collector = new MetricsCollector({ all: options.all, ignorePaths: options.ignorePaths, rules: options.rules, panicLoopWindowMinutes: options.panicLoopWindowMinutes });
4238
4511
  const gitStream = streamGitLog(cwd, since);
4239
4512
  await parseGitLogStream(gitStream, (commit, stats) => {
@@ -4248,7 +4521,7 @@ async function roastRepository(options = {}) {
4248
4521
  return { summary, output };
4249
4522
  }
4250
4523
  async function auditPullRequest(options) {
4251
- const cwd = path11.resolve(options.cwd || process.cwd());
4524
+ const cwd = path12.resolve(options.cwd || process.cwd());
4252
4525
  const format = options.format || "terminal";
4253
4526
  const failOn = (options.failOn || ["test-skip"]).map((s) => s.trim()).filter(Boolean);
4254
4527
  for (const det of failOn) {
@@ -4416,6 +4689,9 @@ export {
4416
4689
  INSTRUCTIONS_END,
4417
4690
  INSTRUCTIONS_BLOCK,
4418
4691
  installInstructions,
4692
+ MCP_TOOLS,
4693
+ createMcpServer,
4694
+ serveMcp,
4419
4695
  runDemo,
4420
4696
  InvalidDetectorError,
4421
4697
  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-FA47T742.js";
41
+ } from "./chunk-MZBP537U.js";
41
42
 
42
43
  // src/cli.ts
43
44
  import { Command } from "commander";
@@ -505,7 +506,7 @@ program.command("demo").description('See what agent-roast reports for a scripted
505
506
  });
506
507
  program.command("audit").description("Score the Git history by the shortcuts it adds (the original agent-roast report)").argument("[path]", "Path to git repository (default: current directory)", ".").action((repoPath) => rootAction(repoPath, { ...program.opts(), forceLegacy: true }));
507
508
  var hook = program.command("hook").description("Guard a coding agent: send it back to fix the code when it weakens a check (Claude Code, Codex, Cursor)");
508
- hook.command("run").description("Run by the agent when it is about to finish; reads the agent's JSON on stdin and answers in its dialect").option("--agent <agent>", `${HOOK_AGENTS.join(", ")} (detected from the input when omitted)`).action(async (opts) => {
509
+ hook.command("run").description("Run by the agent when it is about to finish; reads the agent's JSON on stdin and answers in its dialect").option("--agent <agent>", `${HOOK_AGENTS.join(", ")} (detected from the input when omitted)`).option("--advisory", "only warn the person; never send the agent back", false).action(async (opts) => {
509
510
  let agent;
510
511
  try {
511
512
  agent = opts.agent === void 0 ? void 0 : parseAgents([opts.agent])[0];
@@ -515,7 +516,7 @@ hook.command("run").description("Run by the agent when it is about to finish; re
515
516
  return;
516
517
  }
517
518
  try {
518
- const outcome = await runHook(await readStdin(), { agent });
519
+ const outcome = await runHook(await readStdin(), { agent, advisory: opts.advisory || program.opts().advisory === true });
519
520
  if (outcome.stdout) process.stdout.write(outcome.stdout);
520
521
  if (outcome.stderr) process.stderr.write(outcome.stderr);
521
522
  process.exitCode = outcome.exitCode;
@@ -524,7 +525,7 @@ hook.command("run").description("Run by the agent when it is about to finish; re
524
525
  process.exitCode = 0;
525
526
  }
526
527
  });
527
- hook.command("install").description("Add the hook to Claude Code, Codex or Cursor in this repository").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: the agents that already have a folder in this repository)`).option("--global", "install for every repository of this user instead", false).option("--local", "Claude Code only: use .claude/settings.local.json, which stays out of Git", false).option("--command <program>", "program to run instead of 'npx --yes agent-roast@<version>', for example 'agent-roast' when installed globally").option("--instructions", "also write a short 'keep the checks honest' paragraph into AGENTS.md / CLAUDE.md", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
528
+ hook.command("install").description("Add the hook to Claude Code, Codex or Cursor in this repository").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: the agents that already have a folder in this repository)`).option("--global", "install for every repository of this user instead", false).option("--local", "Claude Code only: use .claude/settings.local.json, which stays out of Git", false).option("--command <program>", "program to run instead of 'npx --yes agent-roast@<version>', for example 'agent-roast' when installed globally").option("--advisory", "try it without risk: the hook only warns you and never sends the agent back", false).option("--instructions", "also write a short 'keep the checks honest' paragraph into AGENTS.md / CLAUDE.md", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
528
529
  try {
529
530
  if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
530
531
  let agents = parseAgents(names);
@@ -532,15 +533,17 @@ hook.command("install").description("Add the hook to Claude Code, Codex or Curso
532
533
  if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(root, { claude: ".claude", codex: ".codex", cursor: ".cursor" }[agent])));
533
534
  if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor).");
534
535
  const scope = opts.global ? "global" : opts.local ? "local" : "project";
535
- const changes = installHooks({ cwd: process.cwd(), agents, scope, command: opts.command, dryRun: opts.dryRun });
536
- console.log(`${opts.dryRun ? "Would install" : "Installed"} the agent-roast hook (${scope}):`);
536
+ const advisory = opts.advisory || program.opts().advisory === true;
537
+ const changes = installHooks({ cwd: process.cwd(), agents, scope, command: opts.command, advisory, dryRun: opts.dryRun });
538
+ console.log(`${opts.dryRun ? "Would install" : "Installed"} the agent-roast hook${advisory ? " in advisory mode" : ""} (${scope}):`);
537
539
  for (const change of changes) console.log(` ${change.action === "unchanged" ? "=" : "+"} ${AGENT_LABELS[change.agent]}: ${displayPath(change.file)} (${change.action})
538
540
  ${change.command}`);
539
541
  if (opts.instructions) {
540
542
  for (const change of installInstructions({ cwd: process.cwd(), dryRun: opts.dryRun })) console.log(` + ${displayPath(change.file)} (${change.action}): keep-the-checks-honest paragraph`);
541
543
  }
542
544
  if (agents.includes("codex")) console.log("\nCodex runs a new hook only after you trust it: open Codex and review it with /hooks.");
543
- if (!opts.dryRun) console.log("\nWhen the agent is about to finish with weakened checks it is sent back once to fix the code.\nTurn it off for a while: agent-roast hook pause \xB7 remove it: agent-roast hook uninstall");
545
+ if (!opts.dryRun && advisory) console.log("\nAdvisory mode: when the agent finishes with weakened checks you get a one-line warning and the agent is never sent back.\nStart sending it back: agent-roast hook install (without --advisory) \xB7 remove it: agent-roast hook uninstall");
546
+ else if (!opts.dryRun) console.log("\nWhen the agent is about to finish with weakened checks it is sent back once to fix the code.\nTurn it off for a while: agent-roast hook pause \xB7 only warn instead: agent-roast hook install --advisory \xB7 remove it: agent-roast hook uninstall");
544
547
  } catch (error) {
545
548
  console.error(`agent-roast hook install error: ${errorMessage(error)}`);
546
549
  process.exitCode = 2;
@@ -562,7 +565,7 @@ hook.command("uninstall").description("Remove the hook from Claude Code, Codex o
562
565
  hook.command("status").description("Show where the hook is installed").action(async () => {
563
566
  try {
564
567
  const rows = hookStatus({ cwd: process.cwd() });
565
- for (const row of rows) console.log(` ${row.installed ? pc.green("installed") : pc.dim("not set ")} ${AGENT_LABELS[row.agent].padEnd(11)} ${row.scope.padEnd(8)} ${displayPath(row.file)}`);
568
+ for (const row of rows) console.log(` ${row.installed ? pc.green("installed") : pc.dim("not set ")} ${AGENT_LABELS[row.agent].padEnd(11)} ${row.scope.padEnd(8)} ${displayPath(row.file)}${row.advisory ? pc.yellow(" (advisory: warns, never sends the agent back)") : ""}`);
566
569
  const paused = await hookPausedUntil(process.cwd());
567
570
  if (paused) console.log(`
568
571
  Paused in this repository until ${paused.toLocaleString()} (agent-roast hook resume).`);
@@ -590,4 +593,12 @@ hook.command("resume").description("End a pause").action(async () => {
590
593
  process.exitCode = 2;
591
594
  }
592
595
  });
596
+ 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) => {
597
+ try {
598
+ await serveMcp({ version: pkg.version, roots: opts.root });
599
+ } catch (error) {
600
+ console.error(`agent-roast mcp: ${errorMessage(error)}`);
601
+ process.exitCode = 1;
602
+ }
603
+ });
593
604
  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. */
@@ -478,6 +478,8 @@ interface HookOptions {
478
478
  agent?: HookAgent;
479
479
  /** Repository to review. Defaults to the directory the agent reports. */
480
480
  cwd?: string;
481
+ /** Only tell the person: the agent is never sent back. AGENT_ROAST_HOOK=advisory does the same. */
482
+ advisory?: boolean;
481
483
  env?: NodeJS.ProcessEnv;
482
484
  /** The agent is waiting, so a slow review gives up and lets it finish. */
483
485
  timeoutMs?: number;
@@ -493,9 +495,9 @@ interface HookOutcome {
493
495
  }
494
496
  /**
495
497
  * 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.
498
+ * checks. Short on purpose, since it is added to the agent's context. `subject` names what was reviewed.
497
499
  */
498
- declare function formatAgentFeedback(result: ReviewResult): string;
500
+ declare function formatAgentFeedback(result: ReviewResult, subject?: string): string;
499
501
  /** The time a pause lasts until, or null when the hook is active. Expired pauses are cleaned up. */
500
502
  declare function hookPausedUntil(cwd: string, now?: Date): Promise<Date | null>;
501
503
  declare function pauseHook(cwd: string, until: Date): Promise<string>;
@@ -506,7 +508,7 @@ declare function parseDuration(text: string): number;
506
508
  * The command an agent runs when it is about to finish. It reviews everything uncommitted against HEAD and, when that
507
509
  * weakens the checks, sends the agent back once with a plain account of what to undo. It never fails the agent's run:
508
510
  * anything it cannot check lets the agent finish, and an agent that already had its chance is not blocked a second time,
509
- * so the person decides instead of the two looping.
511
+ * so the person decides instead of the two looping. In advisory mode the agent is never sent back at all.
510
512
  */
511
513
  declare function runHook(rawInput: string, options?: HookOptions): Promise<HookOutcome>;
512
514
 
@@ -520,6 +522,8 @@ interface HookInstallOptions {
520
522
  dryRun?: boolean;
521
523
  /** Replaces `npx --yes agent-roast@<version>`, for example `agent-roast` when it is installed globally. */
522
524
  command?: string;
525
+ /** Install the hook in advisory mode: it warns the person and never sends the agent back. */
526
+ advisory?: boolean;
523
527
  home?: string;
524
528
  env?: NodeJS.ProcessEnv;
525
529
  }
@@ -535,7 +539,7 @@ declare const AGENT_LABELS: Record<HookAgent, string>;
535
539
  /** Version of this package, which the hook pins so an agent never runs unreviewed newer code. */
536
540
  declare function packageVersion(): string;
537
541
  /** The command an agent runs for this agent's Stop event. */
538
- declare function hookCommand(agent: HookAgent, command?: string): string;
542
+ declare function hookCommand(agent: HookAgent, command?: string, advisory?: boolean): string;
539
543
  /** The top level of the repository containing `cwd`, or `cwd` itself outside a repository. */
540
544
  declare function repositoryRoot(cwd: string): string;
541
545
  /** Where an agent reads its hooks from. */
@@ -545,6 +549,7 @@ declare function uninstallHooks(options: Omit<HookInstallOptions, "command">): H
545
549
  /** Where the hook is installed right now, for every agent and scope that can hold one. */
546
550
  declare function hookStatus(options: Pick<HookInstallOptions, "cwd" | "home" | "env">): (HookInstallChange & {
547
551
  installed: boolean;
552
+ advisory: boolean;
548
553
  })[];
549
554
  declare const INSTRUCTIONS_START = "<!-- agent-roast:start -->";
550
555
  declare const INSTRUCTIONS_END = "<!-- agent-roast:end -->";
@@ -558,6 +563,129 @@ declare function installInstructions(options?: {
558
563
  action: "created" | "updated" | "unchanged";
559
564
  }[];
560
565
 
566
+ /**
567
+ * 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
568
+ * done. It is read-only and offline like the rest of agent-roast, and written without a dependency on purpose: the built files
569
+ * contain no network code, which a test checks. It answers both protocol eras: the initialize handshake of 2025-11-25 and
570
+ * earlier, and the per-request metadata of 2026-07-28 (modelcontextprotocol.io/specification/2026-07-28/basic/versioning).
571
+ */
572
+ type RpcId = string | number;
573
+ type RpcResponse = {
574
+ jsonrpc: "2.0";
575
+ id: RpcId | null;
576
+ } & ({
577
+ result: unknown;
578
+ } | {
579
+ error: {
580
+ code: number;
581
+ message: string;
582
+ data?: unknown;
583
+ };
584
+ });
585
+ declare const MCP_TOOLS: readonly [{
586
+ readonly name: "review_worktree";
587
+ readonly title: "Review uncommitted changes";
588
+ readonly description: string;
589
+ readonly inputSchema: {
590
+ readonly type: "object";
591
+ readonly properties: {
592
+ readonly path: {
593
+ readonly type: "string";
594
+ readonly description: "A directory inside the project. Defaults to the project the server was started in.";
595
+ };
596
+ };
597
+ readonly additionalProperties: false;
598
+ };
599
+ readonly annotations: {
600
+ readonly readOnlyHint: true;
601
+ readonly destructiveHint: false;
602
+ readonly idempotentHint: true;
603
+ readonly openWorldHint: false;
604
+ readonly title: "Review uncommitted changes";
605
+ };
606
+ }, {
607
+ readonly name: "review_changes";
608
+ readonly title: "Review a range of commits";
609
+ readonly description: string;
610
+ readonly inputSchema: {
611
+ readonly type: "object";
612
+ readonly properties: {
613
+ readonly base: {
614
+ readonly type: "string";
615
+ readonly description: "The revision to compare against, for example main or origin/main.";
616
+ };
617
+ readonly head: {
618
+ readonly type: "string";
619
+ readonly description: "The revision under review. Defaults to HEAD.";
620
+ };
621
+ readonly path: {
622
+ readonly type: "string";
623
+ readonly description: "A directory inside the project. Defaults to the project the server was started in.";
624
+ };
625
+ };
626
+ readonly required: readonly ["base"];
627
+ readonly additionalProperties: false;
628
+ };
629
+ readonly annotations: {
630
+ readonly readOnlyHint: true;
631
+ readonly destructiveHint: false;
632
+ readonly idempotentHint: true;
633
+ readonly openWorldHint: false;
634
+ readonly title: "Review a range of commits";
635
+ };
636
+ }, {
637
+ readonly name: "stop_hook";
638
+ readonly title: "Stop hook adapter";
639
+ readonly description: string;
640
+ readonly inputSchema: {
641
+ readonly type: "object";
642
+ readonly properties: {
643
+ readonly cwd: {
644
+ readonly type: "string";
645
+ readonly description: "A directory inside the project. Defaults to the project the server was started in.";
646
+ };
647
+ readonly stop_hook_active: {
648
+ readonly type: "string";
649
+ readonly description: "\"true\" when the agent is already continuing because of a Stop hook.";
650
+ };
651
+ readonly loop_count: {
652
+ readonly type: "string";
653
+ readonly description: "How many times the agent was already sent back, for agents that count.";
654
+ };
655
+ };
656
+ readonly additionalProperties: true;
657
+ };
658
+ readonly annotations: {
659
+ readonly readOnlyHint: true;
660
+ readonly destructiveHint: false;
661
+ readonly idempotentHint: true;
662
+ readonly openWorldHint: false;
663
+ readonly title: "Stop hook adapter";
664
+ };
665
+ }];
666
+ interface McpServerOptions {
667
+ /** The version reported to clients. */
668
+ version: string;
669
+ /** Directories the tools may read. Defaults to the project the server was started in. */
670
+ roots?: string[];
671
+ /** The project directory when the server was started; defaults to CLAUDE_PROJECT_DIR, CURSOR_PROJECT_DIR, then the working directory. */
672
+ cwd?: string;
673
+ env?: NodeJS.ProcessEnv;
674
+ /** How long one tool call may take before it is stopped. */
675
+ timeoutMs?: number;
676
+ }
677
+ interface McpServer {
678
+ /** Answers one JSON-RPC message (or a batch), or nothing for a notification. */
679
+ handle(message: unknown): Promise<RpcResponse | RpcResponse[] | undefined>;
680
+ }
681
+ declare function createMcpServer(options: McpServerOptions): McpServer;
682
+ interface ServeMcpOptions extends McpServerOptions {
683
+ input?: Readable;
684
+ output?: Writable;
685
+ }
686
+ /** Reads newline-delimited JSON-RPC from the input and writes responses to the output until the input closes. */
687
+ declare function serveMcp(options: ServeMcpOptions): Promise<void>;
688
+
561
689
  interface DemoResult {
562
690
  exitCode: number;
563
691
  output: string;
@@ -613,4 +741,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
613
741
  }>;
614
742
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
615
743
 
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 };
744
+ export { AGENT_LABELS, type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DemoResult, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, HOOK_AGENTS, type HookAgent, type HookInstallChange, type HookInstallOptions, type HookOptions, type HookOutcome, type HookScope, INSTRUCTIONS_BLOCK, INSTRUCTIONS_END, INSTRUCTIONS_START, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, MAX_CONFIG_BYTES, MCP_TOOLS, type McpServer, type McpServerOptions, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReplayCommit, type ReplayOptions, type ReplayResult, type ReplayStatus, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ServeMcpOptions, type ShieldsEndpointResponse, type StagedReviewOptions, type SuppressedFinding, VALID_DETECTORS, applyBaseline, applyExceptions, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, createMcpServer, detectAiAttribution, findMergeBase, findingFingerprint, formatAgentFeedback, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReplayTerminal, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, hookCommand, hookConfigPath, hookPausedUntil, hookStatus, initProject, installHooks, installInstructions, isCheckRule, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, mostImportantFirst, packageVersion, parseDiff, parseDuration, parseGitLogStream, pauseHook, replayHistory, repositoryRoot, resolveCommitSha, resumeHook, reviewChanges, reviewStagedChanges, reviewWorktreeChanges, roastRepository, runDemo, runHook, serveMcp, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, uninstallHooks, unquoteGitPath, validateExceptions, writeBaseline };
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-FA47T742.js";
94
+ } from "./chunk-MZBP537U.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.2",
3
+ "version": "0.14.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",