agent-roast 0.13.0 → 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
@@ -46,8 +46,8 @@ MCP can also call agent-roast as a read-only tool, `agent-roast mcp` ([setup](do
46
46
  When the agent is about to finish, agent-roast reviews everything it changed. If a check got weaker, the agent is **sent back
47
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
48
48
  warning rather than an endless loop. Anything agent-roast cannot check lets the agent finish: it never breaks a session. An
49
- intended change is accepted by committing it. Setup for each agent, what it does not do, and how to pause it:
50
- [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).
51
51
 
52
52
  > The agent hooks follow each agent's documented hook protocol and are tested with simulated hook input, not yet in long live
53
53
  > sessions with every agent. Reports of what you see are welcome.
@@ -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;
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  terminalText,
39
39
  uninstallHooks,
40
40
  writeBaseline
41
- } from "./chunk-W6JP5YWF.js";
41
+ } from "./chunk-MZBP537U.js";
42
42
 
43
43
  // src/cli.ts
44
44
  import { Command } from "commander";
@@ -506,7 +506,7 @@ program.command("demo").description('See what agent-roast reports for a scripted
506
506
  });
507
507
  program.command("audit").description("Score the Git history by the shortcuts it adds (the original agent-roast report)").argument("[path]", "Path to git repository (default: current directory)", ".").action((repoPath) => rootAction(repoPath, { ...program.opts(), forceLegacy: true }));
508
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)");
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)`).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) => {
510
510
  let agent;
511
511
  try {
512
512
  agent = opts.agent === void 0 ? void 0 : parseAgents([opts.agent])[0];
@@ -516,7 +516,7 @@ hook.command("run").description("Run by the agent when it is about to finish; re
516
516
  return;
517
517
  }
518
518
  try {
519
- const outcome = await runHook(await readStdin(), { agent });
519
+ const outcome = await runHook(await readStdin(), { agent, advisory: opts.advisory || program.opts().advisory === true });
520
520
  if (outcome.stdout) process.stdout.write(outcome.stdout);
521
521
  if (outcome.stderr) process.stderr.write(outcome.stderr);
522
522
  process.exitCode = outcome.exitCode;
@@ -525,7 +525,7 @@ hook.command("run").description("Run by the agent when it is about to finish; re
525
525
  process.exitCode = 0;
526
526
  }
527
527
  });
528
- hook.command("install").description("Add the hook to Claude Code, Codex or 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) => {
529
529
  try {
530
530
  if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
531
531
  let agents = parseAgents(names);
@@ -533,15 +533,17 @@ hook.command("install").description("Add the hook to Claude Code, Codex or Curso
533
533
  if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(root, { claude: ".claude", codex: ".codex", cursor: ".cursor" }[agent])));
534
534
  if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor).");
535
535
  const scope = opts.global ? "global" : opts.local ? "local" : "project";
536
- const changes = installHooks({ cwd: process.cwd(), agents, scope, command: opts.command, dryRun: opts.dryRun });
537
- 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}):`);
538
539
  for (const change of changes) console.log(` ${change.action === "unchanged" ? "=" : "+"} ${AGENT_LABELS[change.agent]}: ${displayPath(change.file)} (${change.action})
539
540
  ${change.command}`);
540
541
  if (opts.instructions) {
541
542
  for (const change of installInstructions({ cwd: process.cwd(), dryRun: opts.dryRun })) console.log(` + ${displayPath(change.file)} (${change.action}): keep-the-checks-honest paragraph`);
542
543
  }
543
544
  if (agents.includes("codex")) console.log("\nCodex runs a new hook only after you trust it: open Codex and review it with /hooks.");
544
- 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");
545
547
  } catch (error) {
546
548
  console.error(`agent-roast hook install error: ${errorMessage(error)}`);
547
549
  process.exitCode = 2;
@@ -563,7 +565,7 @@ hook.command("uninstall").description("Remove the hook from Claude Code, Codex o
563
565
  hook.command("status").description("Show where the hook is installed").action(async () => {
564
566
  try {
565
567
  const rows = hookStatus({ cwd: process.cwd() });
566
- 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)") : ""}`);
567
569
  const paused = await hookPausedUntil(process.cwd());
568
570
  if (paused) console.log(`
569
571
  Paused in this repository until ${paused.toLocaleString()} (agent-roast hook resume).`);
package/dist/index.d.ts CHANGED
@@ -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;
@@ -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 -->";
package/dist/index.js CHANGED
@@ -91,7 +91,7 @@ import {
91
91
  unquoteGitPath,
92
92
  validateExceptions,
93
93
  writeBaseline
94
- } from "./chunk-W6JP5YWF.js";
94
+ } from "./chunk-MZBP537U.js";
95
95
  export {
96
96
  AGENT_LABELS,
97
97
  CONFIG_FILENAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.13.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
5
  "mcpName": "io.github.Nari-Ab/agent-roast",
6
6
  "type": "module",