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 +4 -3
- package/dist/{chunk-FA47T742.js → chunk-MZBP537U.js} +300 -24
- package/dist/cli.js +19 -8
- package/dist/index.d.ts +134 -6
- package/dist/index.js +7 -1
- package/package.json +2 -1
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.
|
|
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
|
|
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
|
|
3715
|
-
if (arrow < 0) return
|
|
3714
|
+
const clip3 = (part, size) => part.length > size ? `${part.slice(0, size - 1)}\u2026` : part;
|
|
3715
|
+
if (arrow < 0) return clip3(text, limit);
|
|
3716
3716
|
const side = Math.floor((limit - 3) / 2);
|
|
3717
|
-
return `${
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4393
|
+
import * as fs10 from "fs";
|
|
4121
4394
|
import os2 from "os";
|
|
4122
|
-
import
|
|
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 =
|
|
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=${
|
|
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 =
|
|
4187
|
-
|
|
4188
|
-
|
|
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
|
-
|
|
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 =
|
|
4507
|
+
const cwd = path12.resolve(options.cwd || process.cwd());
|
|
4235
4508
|
const since = options.since || "90 days ago";
|
|
4236
|
-
const repoName =
|
|
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 =
|
|
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-
|
|
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
|
|
536
|
-
|
|
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("\
|
|
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-
|
|
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.
|
|
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",
|