agent-roast 0.11.0 → 0.12.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/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AGENT_LABELS,
3
4
  ConfigError,
4
5
  GitError,
5
6
  GitResolutionError,
7
+ HOOK_AGENTS,
6
8
  InvalidDetectorError,
7
9
  RULE_GUIDES,
8
10
  VALID_DETECTORS,
@@ -14,22 +16,35 @@ import {
14
16
  generateShieldsMarkdown,
15
17
  generateSvgBadge,
16
18
  gitOutputPath,
19
+ hookPausedUntil,
20
+ hookStatus,
17
21
  initProject,
22
+ installHooks,
23
+ installInstructions,
18
24
  loadConfig,
25
+ parseDuration,
26
+ pauseHook,
19
27
  readGit,
20
28
  replayHistory,
29
+ repositoryRoot,
30
+ resumeHook,
21
31
  reviewChanges,
22
32
  reviewStagedChanges,
33
+ reviewWorktreeChanges,
23
34
  roastRepository,
35
+ runDemo,
36
+ runHook,
24
37
  terminalText,
38
+ uninstallHooks,
25
39
  writeBaseline
26
- } from "./chunk-WMHB65AL.js";
40
+ } from "./chunk-AP5Y4CH6.js";
27
41
 
28
42
  // src/cli.ts
29
43
  import { Command } from "commander";
30
44
  import { createRequire } from "module";
31
45
  import * as fs from "fs";
32
46
  import * as path from "path";
47
+ import pc from "picocolors";
33
48
  var require2 = createRequire(import.meta.url);
34
49
  var pkg = require2("../package.json");
35
50
  var errorMessage = (error) => terminalText(error instanceof Error ? error.message : String(error));
@@ -40,7 +55,7 @@ function failOnOption(options) {
40
55
  }
41
56
  return options.failOn?.split(",").map((rule) => rule.trim()).filter(Boolean);
42
57
  }
43
- async function runContextReview(repoPath, options, staged) {
58
+ async function runContextReview(repoPath, options, source) {
44
59
  const controller = new AbortController();
45
60
  const abort = () => controller.abort();
46
61
  process.once("SIGINT", abort);
@@ -53,7 +68,7 @@ async function runContextReview(repoPath, options, staged) {
53
68
  format: options.json ? "json" : options.format,
54
69
  signal: controller.signal
55
70
  };
56
- const result = staged ? await reviewStagedChanges(common) : await reviewChanges({ ...common, base: options.base, head: options.head });
71
+ const result = source === "index" ? await reviewStagedChanges(common) : source === "worktree" ? await reviewWorktreeChanges(common) : await reviewChanges({ ...common, base: options.base, head: options.head });
57
72
  console.log(result.output);
58
73
  process.exitCode = result.exitCode;
59
74
  for (const [filename, content] of [
@@ -78,9 +93,117 @@ removed-count=${result.removedFindings.length}
78
93
  process.removeListener("SIGTERM", abort);
79
94
  }
80
95
  }
96
+ function isDefaultInvocation(options) {
97
+ return !(options.all || options.base || options.staged || options.worktree || options.failUnder !== void 0 || options.verbose || options.authors || program.getOptionValueSource("since") === "cli" || program.getOptionValueSource("head") === "cli");
98
+ }
99
+ var NEXT_STEPS = [
100
+ "",
101
+ pc.bold("Next steps"),
102
+ ` ${pc.cyan("npx agent-roast hook install")} stop your coding agent from weakening checks (Claude Code, Codex, Cursor)`,
103
+ ` ${pc.cyan("npx agent-roast init --ci")} add the pull-request check (GitHub Actions)`,
104
+ ` ${pc.cyan("npx agent-roast demo")} see a scripted example`,
105
+ ""
106
+ ].join("\n");
107
+ async function runDefault(repoPath, options) {
108
+ const controller = new AbortController();
109
+ const abort = () => controller.abort();
110
+ process.once("SIGINT", abort);
111
+ process.once("SIGTERM", abort);
112
+ try {
113
+ const format = options.json ? "json" : options.format;
114
+ if (!["terminal", "json", "github", "markdown", "sarif"].includes(format)) throw new ConfigError("--format must be terminal, json, github, markdown or sarif.");
115
+ try {
116
+ await readGit(path.resolve(repoPath), ["rev-parse", "--show-toplevel"], controller.signal, 8192);
117
+ } catch (error) {
118
+ if (!(error instanceof GitError)) throw error;
119
+ console.error(`agent-roast reviews Git repositories, and ${terminalText(path.resolve(repoPath))} is not inside one.
120
+ To see what it does without one: npx agent-roast demo`);
121
+ process.exitCode = 2;
122
+ return;
123
+ }
124
+ const review = await reviewWorktreeChanges({ cwd: repoPath, failOn: failOnOption(options), ignoreExceptions: options.ignoreExceptions, format, signal: controller.signal });
125
+ if (format === "terminal" && review.filesChanged === 0) {
126
+ console.log(pc.dim("\nNothing is uncommitted. Here is what a pull-request check would have said about your recent commits:"));
127
+ const progress = Boolean(process.stderr.isTTY);
128
+ try {
129
+ const replay = await replayHistory({
130
+ cwd: repoPath,
131
+ last: 50,
132
+ format: "terminal",
133
+ signal: controller.signal,
134
+ onProgress: progress ? (done, total) => process.stderr.write(`\r reviewed ${done}/${total} commits`) : void 0
135
+ });
136
+ if (progress) process.stderr.write("\r\x1B[K");
137
+ console.log(replay.summary.commits ? replay.output.replace(/\s+$/, "") : "\n There is no earlier commit to compare with yet, so there is nothing to replay.");
138
+ console.log(NEXT_STEPS);
139
+ process.exitCode = replay.exitCode;
140
+ } catch (error) {
141
+ if (progress) process.stderr.write("\r\x1B[K");
142
+ if (controller.signal.aborted) throw error;
143
+ console.log(`
144
+ ${errorMessage(error)}
145
+ To see what agent-roast does: npx agent-roast demo
146
+ `);
147
+ process.exitCode = 0;
148
+ }
149
+ return;
150
+ }
151
+ console.log(review.output);
152
+ process.exitCode = review.exitCode;
153
+ } catch (error) {
154
+ console.error(`agent-roast error: ${errorMessage(error)}`);
155
+ process.exitCode = error instanceof ConfigError || error instanceof GitError || error instanceof GitResolutionError || error instanceof InvalidDetectorError ? 2 : 1;
156
+ } finally {
157
+ process.removeListener("SIGINT", abort);
158
+ process.removeListener("SIGTERM", abort);
159
+ }
160
+ }
161
+ function parseAgents(names) {
162
+ return names.map((name) => {
163
+ if (!HOOK_AGENTS.includes(name.toLowerCase())) throw new ConfigError(`Unknown agent '${name}'. Use ${HOOK_AGENTS.join(", ")}.`);
164
+ return name.toLowerCase();
165
+ });
166
+ }
167
+ async function readStdin(timeoutMs = 5e3) {
168
+ if (process.stdin.isTTY) return "";
169
+ return new Promise((resolve2) => {
170
+ const chunks = [];
171
+ let settled = false;
172
+ const finish = () => {
173
+ if (settled) return;
174
+ settled = true;
175
+ clearTimeout(timer);
176
+ process.stdin.pause();
177
+ resolve2(Buffer.concat(chunks).toString("utf8"));
178
+ };
179
+ const timer = setTimeout(finish, timeoutMs);
180
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
181
+ process.stdin.on("end", finish);
182
+ process.stdin.on("error", finish);
183
+ });
184
+ }
185
+ function displayPath(file) {
186
+ const relative2 = path.relative(process.cwd(), file);
187
+ if (relative2 && !relative2.startsWith("..") && !path.isAbsolute(relative2)) return relative2;
188
+ const home = process.env.HOME || process.env.USERPROFILE;
189
+ return home && file.startsWith(home) ? "~" + file.slice(home.length) : file;
190
+ }
81
191
  var program = new Command();
82
- program.name("agent-roast").description("Check current code, review changes and audit Git history for coding shortcuts").version(pkg.version || "0.4.0").argument("[path]", "Path to git repository (default: current directory)", ".").option("-a, --all", "Audit ALL commits regardless of AI attribution tags", false).option("-s, --since <time>", "Time window for git analysis", "90 days ago").option("-v, --verbose", "Show commit SHAs and line snippets for all infractions", false).option("--json", "Output results as raw JSON", false).option("--staged", "Compare full HEAD/index source and gate new staged findings (including the first commit)", false).option("--base <ref>", "Base git ref/branch for PR mode audit (e.g. origin/main)").option("--head <ref>", "Head git ref/SHA for PR mode audit (default: HEAD)", "HEAD").option("--ignore-exceptions", "Report findings even when accepted by reviewed, unexpired exceptions", false).option("--fail-on <rules>", "Comma-separated detectors that trigger CI failure").option("--advisory", "Report findings without failing the run, the same as an empty --fail-on (setup errors and incomplete scans still fail)", false).option("--fail-under <score>", "Fail CI if agent score is below threshold (0-100)").option("--format <format>", "Output format: terminal, json, github, markdown (check/review also support sarif)", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
192
+ program.name("agent-roast").description("Check current code, review changes and audit Git history for coding shortcuts").version(pkg.version || "0.4.0").argument("[path]", "Path to git repository (default: current directory)", ".").option("-a, --all", "Audit ALL commits regardless of AI attribution tags", false).option("-s, --since <time>", "Time window for git analysis", "90 days ago").option("-v, --verbose", "Show commit SHAs and line snippets for all infractions", false).option("--json", "Output results as raw JSON", false).option("--staged", "Compare full HEAD/index source and gate new staged findings (including the first commit)", false).option("--worktree", "Compare HEAD with everything not committed yet: staged, unstaged and untracked files", false).option("--base <ref>", "Base git ref/branch for PR mode audit (e.g. origin/main)").option("--head <ref>", "Head git ref/SHA for PR mode audit (default: HEAD)", "HEAD").option("--ignore-exceptions", "Report findings even when accepted by reviewed, unexpired exceptions", false).option("--fail-on <rules>", "Comma-separated detectors that trigger CI failure").option("--advisory", "Report findings without failing the run, the same as an empty --fail-on (setup errors and incomplete scans still fail)", false).option("--fail-under <score>", "Fail CI if agent score is below threshold (0-100)").option("--format <format>", "Output format: terminal, json, github, markdown (check/review also support sarif)", "terminal").option("--authors", "Display author names and emails in report", false).action((repoPath, options) => rootAction(repoPath, options));
193
+ async function rootAction(repoPath, options) {
83
194
  try {
195
+ if (!options.forceLegacy && isDefaultInvocation(options)) {
196
+ await runDefault(repoPath, options);
197
+ return;
198
+ }
199
+ if (options.worktree && (options.staged || options.base)) throw new ConfigError("--worktree compares HEAD with the working tree; --staged and --base do not apply.");
200
+ if (options.worktree) {
201
+ if (options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli" || program.getOptionValueSource("head") === "cli") {
202
+ throw new ConfigError("--worktree compares HEAD and the working tree; --all, --since, --head and --fail-under do not apply.");
203
+ }
204
+ await runContextReview(repoPath, options, "worktree");
205
+ return;
206
+ }
84
207
  if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
85
208
  console.warn(
86
209
  "Warning: pull_request_target runs with elevated repository privileges. agent-roast recommends running on 'pull_request'."
@@ -91,7 +214,7 @@ program.name("agent-roast").description("Check current code, review changes and
91
214
  if (options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli" || program.getOptionValueSource("head") === "cli") {
92
215
  throw new ConfigError("--staged compares HEAD and index; --all, --since, --head and --fail-under do not apply.");
93
216
  }
94
- await runContextReview(repoPath, options, true);
217
+ await runContextReview(repoPath, options, "index");
95
218
  return;
96
219
  }
97
220
  if (options.ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
@@ -183,17 +306,18 @@ has-supported-changes=${prResult.hasSupportedChanges}
183
306
  process.exitCode = 1;
184
307
  }
185
308
  }
186
- });
187
- program.command("review").description("Compare full changed source at merge-base and head; gate only new findings").argument("[path]", "Path to the Git repository", ".").addHelpText("after", "\nRequired: --base <trusted-ref> Optional: --head <ref> (default HEAD), or use --staged for HEAD/index\nOutput: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>, --advisory\nExamples:\n agent-roast review --base origin/main --format markdown\n agent-roast review --base HEAD~50 --advisory").action(async (repoPath) => {
309
+ }
310
+ program.command("review").description("Compare full changed source at merge-base and head; gate only new findings").argument("[path]", "Path to the Git repository", ".").addHelpText("after", "\nRequired: --base <trusted-ref> Optional: --head <ref> (default HEAD), or use --staged for HEAD/index, or --worktree for HEAD/everything uncommitted\nOutput: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>, --advisory\nExamples:\n agent-roast review --base origin/main --format markdown\n agent-roast review --worktree\n agent-roast review --base HEAD~50 --advisory").action(async (repoPath) => {
188
311
  try {
189
312
  const options = program.opts();
190
- if (options.staged ? options.base || program.getOptionValueSource("head") === "cli" : !options.base) {
191
- throw new ConfigError("Use review --base <trusted-ref> or review --staged; --base/--head cannot be combined with --staged.");
313
+ if (options.worktree && options.staged) throw new ConfigError("--worktree and --staged cannot be combined.");
314
+ if (options.staged || options.worktree ? options.base || program.getOptionValueSource("head") === "cli" : !options.base) {
315
+ throw new ConfigError("Use review --base <trusted-ref>, review --staged or review --worktree; --base/--head cannot be combined with --staged or --worktree.");
192
316
  }
193
317
  if (options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli") {
194
318
  throw new ConfigError("review compares source snapshots; --all, --since and --fail-under do not apply.");
195
319
  }
196
- await runContextReview(repoPath, options, options.staged);
320
+ await runContextReview(repoPath, options, options.staged ? "index" : options.worktree ? "worktree" : "commits");
197
321
  } catch (error) {
198
322
  console.error(`agent-roast review error: ${errorMessage(error)}`);
199
323
  process.exitCode = 2;
@@ -206,8 +330,8 @@ program.command("replay").description("Review each of the newest commits on its
206
330
  process.once("SIGTERM", abort);
207
331
  try {
208
332
  const global = program.opts();
209
- if (global.base || global.staged || global.failOn !== void 0 || global.advisory || global.failUnder !== void 0 || global.all || global.ignoreExceptions || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
210
- throw new ConfigError("replay reviews each commit on its own; --base, --head, --staged, --fail-on, --advisory, --fail-under, --all, --since and --ignore-exceptions do not apply.");
333
+ if (global.base || global.staged || global.worktree || global.failOn !== void 0 || global.advisory || global.failUnder !== void 0 || global.all || global.ignoreExceptions || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
334
+ throw new ConfigError("replay reviews each commit on its own; --base, --head, --staged, --worktree, --fail-on, --advisory, --fail-under, --all, --since and --ignore-exceptions do not apply.");
211
335
  }
212
336
  const format = global.json ? "json" : global.format;
213
337
  if (format !== "terminal" && format !== "json") throw new ConfigError("replay supports --format terminal or json.");
@@ -236,8 +360,8 @@ program.command("replay").description("Review each of the newest commits on its
236
360
  program.command("check").description("Check current tracked and non-ignored untracked source files").argument("[path]", "Path to a git repository", ".").option("--baseline <file>", "Report only findings not accepted in this baseline").option("--write-baseline <file>", "Save current findings for adoption (refuses to overwrite)").addHelpText("after", "\nOutput options: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>, --advisory\nExamples:\n agent-roast check\n agent-roast check --write-baseline .agentroast-baseline.json\n agent-roast check --baseline .agentroast-baseline.json --format json").action(async (repoPath, opts) => {
237
361
  try {
238
362
  const global = program.opts();
239
- if (global.base || global.staged || global.failUnder !== void 0 || global.all || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
240
- throw new ConfigError("check inspects current files; history, --base, --head, --staged and --fail-under options do not apply.");
363
+ if (global.base || global.staged || global.worktree || global.failUnder !== void 0 || global.all || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
364
+ throw new ConfigError("check inspects current files; history, --base, --head, --staged, --worktree and --fail-under options do not apply.");
241
365
  }
242
366
  if (opts.baseline && opts.writeBaseline) throw new ConfigError("--baseline and --write-baseline cannot be combined.");
243
367
  const result = await checkProject({
@@ -361,4 +485,109 @@ ${markdown}
361
485
  process.exitCode = err instanceof ConfigError || err instanceof GitError || err instanceof GitResolutionError ? 2 : 1;
362
486
  }
363
487
  });
488
+ program.command("demo").description('See what agent-roast reports for a scripted "fix" that weakens the checks (a temporary repository; nothing of yours is read)').action(async () => {
489
+ const controller = new AbortController();
490
+ const abort = () => controller.abort();
491
+ process.once("SIGINT", abort);
492
+ process.once("SIGTERM", abort);
493
+ try {
494
+ if (program.opts().worktree || program.opts().staged || program.opts().base) throw new ConfigError("demo takes no review options.");
495
+ const demo = await runDemo({ signal: controller.signal });
496
+ console.log(demo.output);
497
+ process.exitCode = demo.exitCode;
498
+ } catch (error) {
499
+ console.error(`agent-roast demo error: ${errorMessage(error)}`);
500
+ process.exitCode = 2;
501
+ } finally {
502
+ process.removeListener("SIGINT", abort);
503
+ process.removeListener("SIGTERM", abort);
504
+ }
505
+ });
506
+ 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
+ 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
+ let agent;
510
+ try {
511
+ agent = opts.agent === void 0 ? void 0 : parseAgents([opts.agent])[0];
512
+ } catch (error) {
513
+ console.error(`agent-roast hook: ${errorMessage(error)}`);
514
+ process.exitCode = 1;
515
+ return;
516
+ }
517
+ try {
518
+ const outcome = await runHook(await readStdin(), { agent });
519
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
520
+ if (outcome.stderr) process.stderr.write(outcome.stderr);
521
+ process.exitCode = outcome.exitCode;
522
+ } catch (error) {
523
+ console.error(`agent-roast hook: ${errorMessage(error)}`);
524
+ process.exitCode = 0;
525
+ }
526
+ });
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
+ try {
529
+ if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
530
+ let agents = parseAgents(names);
531
+ const root = repositoryRoot(process.cwd());
532
+ if (!agents.length) agents = HOOK_AGENTS.filter((agent) => fs.existsSync(path.join(root, { claude: ".claude", codex: ".codex", cursor: ".cursor" }[agent])));
533
+ if (!agents.length) throw new ConfigError("Name the agent to guard: agent-roast hook install claude (or codex, cursor).");
534
+ 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}):`);
537
+ for (const change of changes) console.log(` ${change.action === "unchanged" ? "=" : "+"} ${AGENT_LABELS[change.agent]}: ${displayPath(change.file)} (${change.action})
538
+ ${change.command}`);
539
+ if (opts.instructions) {
540
+ for (const change of installInstructions({ cwd: process.cwd(), dryRun: opts.dryRun })) console.log(` + ${displayPath(change.file)} (${change.action}): keep-the-checks-honest paragraph`);
541
+ }
542
+ 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");
544
+ } catch (error) {
545
+ console.error(`agent-roast hook install error: ${errorMessage(error)}`);
546
+ process.exitCode = 2;
547
+ }
548
+ });
549
+ hook.command("uninstall").description("Remove the hook from Claude Code, Codex or Cursor").argument("[agents...]", `${HOOK_AGENTS.join(", ")} (default: all)`).option("--global", "remove it from the user-level files", false).option("--local", "Claude Code only: remove it from .claude/settings.local.json", false).option("--dry-run", "show what would change without writing anything", false).action((names, opts) => {
550
+ try {
551
+ if (opts.global && opts.local) throw new ConfigError("Choose --global or --local, not both.");
552
+ const agents = names.length ? parseAgents(names) : [...HOOK_AGENTS].filter((agent) => !(opts.local && agent !== "claude"));
553
+ const scope = opts.global ? "global" : opts.local ? "local" : "project";
554
+ for (const change of uninstallHooks({ cwd: process.cwd(), agents, scope, dryRun: opts.dryRun })) {
555
+ console.log(` ${change.action === "removed" ? "-" : "="} ${AGENT_LABELS[change.agent]}: ${displayPath(change.file)} (${change.action === "removed" ? opts.dryRun ? "would be removed" : "removed" : "no agent-roast hook there"})`);
556
+ }
557
+ } catch (error) {
558
+ console.error(`agent-roast hook uninstall error: ${errorMessage(error)}`);
559
+ process.exitCode = 2;
560
+ }
561
+ });
562
+ hook.command("status").description("Show where the hook is installed").action(async () => {
563
+ try {
564
+ 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)}`);
566
+ const paused = await hookPausedUntil(process.cwd());
567
+ if (paused) console.log(`
568
+ Paused in this repository until ${paused.toLocaleString()} (agent-roast hook resume).`);
569
+ if (!rows.some((row) => row.installed)) console.log("\n Install it with: agent-roast hook install claude (or codex, cursor)");
570
+ } catch (error) {
571
+ console.error(`agent-roast hook status error: ${errorMessage(error)}`);
572
+ process.exitCode = 2;
573
+ }
574
+ });
575
+ hook.command("pause").description("Let the agent finish without the check in this repository for a while (for a change you have approved)").option("--for <duration>", "how long: 30m, 2h, 1d", "4h").action(async (opts) => {
576
+ try {
577
+ const until = new Date(Date.now() + parseDuration(opts.for));
578
+ await pauseHook(process.cwd(), until);
579
+ console.log(`The hook is paused in this repository until ${until.toLocaleString()}. Resume now: agent-roast hook resume`);
580
+ } catch (error) {
581
+ console.error(`agent-roast hook pause error: ${errorMessage(error)}`);
582
+ process.exitCode = 2;
583
+ }
584
+ });
585
+ hook.command("resume").description("End a pause").action(async () => {
586
+ try {
587
+ console.log(await resumeHook(process.cwd()) ? "The hook is active again." : "The hook was not paused.");
588
+ } catch (error) {
589
+ console.error(`agent-roast hook resume error: ${errorMessage(error)}`);
590
+ process.exitCode = 2;
591
+ }
592
+ });
364
593
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -156,7 +156,7 @@ type StagedReviewOptions = Omit<ReviewOptions, "base" | "head">;
156
156
  interface ReviewResult {
157
157
  schemaVersion: string;
158
158
  mode: "review";
159
- source?: "commits" | "index";
159
+ source?: "commits" | "index" | "worktree";
160
160
  baseSha: string;
161
161
  headSha: string;
162
162
  mergeBaseSha: string;
@@ -418,6 +418,8 @@ declare function compareFindings(before: Infraction[], after: Infraction[], newP
418
418
  /** Compare full source blobs in changed files without executing or checking out the candidate. */
419
419
  declare function reviewChanges(options: ReviewOptions): Promise<ReviewResult>;
420
420
  declare function reviewStagedChanges(options?: StagedReviewOptions): Promise<ReviewResult>;
421
+ /** Everything uncommitted (staged, unstaged and untracked files) against HEAD, under the policy committed at HEAD. */
422
+ declare function reviewWorktreeChanges(options?: StagedReviewOptions): Promise<ReviewResult>;
421
423
 
422
424
  type ReplayStatus = "blocked" | "warned" | "noted" | "clean" | "untouched" | "incomplete" | "failed";
423
425
  interface ReplayCommit {
@@ -468,6 +470,107 @@ interface ReplayResult {
468
470
  declare function replayHistory(options?: ReplayOptions): Promise<ReplayResult>;
469
471
  declare function formatReplayTerminal(result: ReplayResult, allRules?: boolean): string;
470
472
 
473
+ /** Coding agents that can run a command when they are about to finish and feed its answer back. */
474
+ type HookAgent = "claude" | "codex" | "cursor";
475
+ declare const HOOK_AGENTS: readonly HookAgent[];
476
+ interface HookOptions {
477
+ /** Which agent is calling. Detected from its input when omitted. */
478
+ agent?: HookAgent;
479
+ /** Repository to review. Defaults to the directory the agent reports. */
480
+ cwd?: string;
481
+ env?: NodeJS.ProcessEnv;
482
+ /** The agent is waiting, so a slow review gives up and lets it finish. */
483
+ timeoutMs?: number;
484
+ now?: Date;
485
+ }
486
+ interface HookOutcome {
487
+ /** pass: nothing to say; block: the agent was sent back; warn: the user is told, the agent is not; skip: nothing could be checked. */
488
+ action: "pass" | "block" | "warn" | "skip";
489
+ stdout: string;
490
+ stderr: string;
491
+ exitCode: number;
492
+ result?: ReviewResult;
493
+ }
494
+ /**
495
+ * What the agent reads when it is sent back: the offending changes, then the instruction to fix the code rather than the
496
+ * checks. Short on purpose, since it is added to the agent's context.
497
+ */
498
+ declare function formatAgentFeedback(result: ReviewResult): string;
499
+ /** The time a pause lasts until, or null when the hook is active. Expired pauses are cleaned up. */
500
+ declare function hookPausedUntil(cwd: string, now?: Date): Promise<Date | null>;
501
+ declare function pauseHook(cwd: string, until: Date): Promise<string>;
502
+ declare function resumeHook(cwd: string): Promise<boolean>;
503
+ /** "30m", "2h", "1d": how long a pause lasts. */
504
+ declare function parseDuration(text: string): number;
505
+ /**
506
+ * The command an agent runs when it is about to finish. It reviews everything uncommitted against HEAD and, when that
507
+ * weakens the checks, sends the agent back once with a plain account of what to undo. It never fails the agent's run:
508
+ * 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.
510
+ */
511
+ declare function runHook(rawInput: string, options?: HookOptions): Promise<HookOutcome>;
512
+
513
+ /** project: shared with the team (committed); local: this checkout only (Claude Code); global: every repository of this user. */
514
+ type HookScope = "project" | "local" | "global";
515
+ interface HookInstallOptions {
516
+ /** Anywhere inside the repository. */
517
+ cwd?: string;
518
+ agents: readonly HookAgent[];
519
+ scope?: HookScope;
520
+ dryRun?: boolean;
521
+ /** Replaces `npx --yes agent-roast@<version>`, for example `agent-roast` when it is installed globally. */
522
+ command?: string;
523
+ home?: string;
524
+ env?: NodeJS.ProcessEnv;
525
+ }
526
+ interface HookInstallChange {
527
+ agent: HookAgent;
528
+ scope: HookScope;
529
+ file: string;
530
+ action: "created" | "updated" | "unchanged" | "removed" | "absent";
531
+ /** The command line the agent runs. */
532
+ command?: string;
533
+ }
534
+ declare const AGENT_LABELS: Record<HookAgent, string>;
535
+ /** Version of this package, which the hook pins so an agent never runs unreviewed newer code. */
536
+ declare function packageVersion(): string;
537
+ /** The command an agent runs for this agent's Stop event. */
538
+ declare function hookCommand(agent: HookAgent, command?: string): string;
539
+ /** The top level of the repository containing `cwd`, or `cwd` itself outside a repository. */
540
+ declare function repositoryRoot(cwd: string): string;
541
+ /** Where an agent reads its hooks from. */
542
+ declare function hookConfigPath(agent: HookAgent, scope: HookScope, root: string, home: string, env?: NodeJS.ProcessEnv): string;
543
+ declare function installHooks(options: HookInstallOptions): HookInstallChange[];
544
+ declare function uninstallHooks(options: Omit<HookInstallOptions, "command">): HookInstallChange[];
545
+ /** Where the hook is installed right now, for every agent and scope that can hold one. */
546
+ declare function hookStatus(options: Pick<HookInstallOptions, "cwd" | "home" | "env">): (HookInstallChange & {
547
+ installed: boolean;
548
+ })[];
549
+ declare const INSTRUCTIONS_START = "<!-- agent-roast:start -->";
550
+ declare const INSTRUCTIONS_END = "<!-- agent-roast:end -->";
551
+ declare const INSTRUCTIONS_BLOCK = "<!-- agent-roast:start -->\n## Keep the checks honest\n\nWhen a test, type check, linter or CI step fails, fix the code. Do not make the build green by skipping or deleting tests,\nweakening assertions, lowering coverage thresholds, switching off `strict`, or letting a CI step fail. If you think a check\nis wrong, say so and ask before changing it. Before you finish, run `npx agent-roast review --worktree`: it lists uncommitted\nchanges that weaken the checks.\n<!-- agent-roast:end -->\n";
552
+ /** Writes the guidance into AGENTS.md and CLAUDE.md (whichever exist; AGENTS.md is created when neither does). Idempotent. */
553
+ declare function installInstructions(options?: {
554
+ cwd?: string;
555
+ dryRun?: boolean;
556
+ }): {
557
+ file: string;
558
+ action: "created" | "updated" | "unchanged";
559
+ }[];
560
+
561
+ interface DemoResult {
562
+ exitCode: number;
563
+ output: string;
564
+ result: ReviewResult;
565
+ }
566
+ /**
567
+ * A scripted example in a throwaway repository: a project with passing tests, then the kind of "fix" that turns a red build
568
+ * green by changing the checks. The review is the real one; nothing of the user's is read, and the repository is deleted.
569
+ */
570
+ declare function runDemo(options?: {
571
+ signal?: AbortSignal;
572
+ }): Promise<DemoResult>;
573
+
471
574
  /** Offline SARIF 2.1.0, with relative URIs and explicit incomplete-run status.
472
575
  * Snippets are omitted to avoid embedding source code in integration artifacts. */
473
576
  declare function formatSarif(report: CheckResult | ReviewResult): string;
@@ -510,4 +613,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
510
613
  }>;
511
614
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
512
615
 
513
- export { type AgentRoastConfig, type AiAttributionResult, CONFIG_FILENAMES, type CheckOptions, type CheckResult, type CommitInfo, ConfigError, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, type FindingBaseline, type FindingException, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, 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, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReplayTerminal, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, initProject, isCheckRule, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, mostImportantFirst, parseDiff, parseGitLogStream, replayHistory, resolveCommitSha, reviewChanges, reviewStagedChanges, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, unquoteGitPath, validateExceptions, writeBaseline };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ AGENT_LABELS,
3
4
  CONFIG_FILENAMES,
4
5
  ConfigError,
5
6
  DEFAULT_CONFIG,
@@ -9,6 +10,10 @@ import {
9
10
  GitError,
10
11
  GitResolutionError,
11
12
  HERMETIC_GIT_FLAGS,
13
+ HOOK_AGENTS,
14
+ INSTRUCTIONS_BLOCK,
15
+ INSTRUCTIONS_END,
16
+ INSTRUCTIONS_START,
12
17
  InvalidDetectorError,
13
18
  MAX_CONFIG_BYTES,
14
19
  MetricsCollector,
@@ -27,6 +32,7 @@ import {
27
32
  detectAiAttribution,
28
33
  findMergeBase,
29
34
  findingFingerprint,
35
+ formatAgentFeedback,
30
36
  formatCheckMarkdown,
31
37
  formatCheckTerminal,
32
38
  formatGithubAnnotations,
@@ -45,7 +51,13 @@ import {
45
51
  getHermeticGitEnv,
46
52
  getScoreColor,
47
53
  gitOutputPath,
54
+ hookCommand,
55
+ hookConfigPath,
56
+ hookPausedUntil,
57
+ hookStatus,
48
58
  initProject,
59
+ installHooks,
60
+ installInstructions,
49
61
  isCheckRule,
50
62
  isCodeFile,
51
63
  isTestFile,
@@ -53,22 +65,32 @@ import {
53
65
  loadConfig,
54
66
  matchIgnorePath,
55
67
  mostImportantFirst,
68
+ packageVersion,
56
69
  parseDiff,
70
+ parseDuration,
57
71
  parseGitLogStream,
72
+ pauseHook,
58
73
  replayHistory,
74
+ repositoryRoot,
59
75
  resolveCommitSha,
76
+ resumeHook,
60
77
  reviewChanges,
61
78
  reviewStagedChanges,
79
+ reviewWorktreeChanges,
62
80
  roastRepository,
81
+ runDemo,
82
+ runHook,
63
83
  streamGitDiff,
64
84
  streamGitLog,
65
85
  stripStringAndRegexLiterals,
66
86
  terminalText,
87
+ uninstallHooks,
67
88
  unquoteGitPath,
68
89
  validateExceptions,
69
90
  writeBaseline
70
- } from "./chunk-WMHB65AL.js";
91
+ } from "./chunk-AP5Y4CH6.js";
71
92
  export {
93
+ AGENT_LABELS,
72
94
  CONFIG_FILENAMES,
73
95
  ConfigError,
74
96
  DEFAULT_CONFIG,
@@ -78,6 +100,10 @@ export {
78
100
  GitError,
79
101
  GitResolutionError,
80
102
  HERMETIC_GIT_FLAGS,
103
+ HOOK_AGENTS,
104
+ INSTRUCTIONS_BLOCK,
105
+ INSTRUCTIONS_END,
106
+ INSTRUCTIONS_START,
81
107
  InvalidDetectorError,
82
108
  MAX_CONFIG_BYTES,
83
109
  MetricsCollector,
@@ -96,6 +122,7 @@ export {
96
122
  detectAiAttribution,
97
123
  findMergeBase,
98
124
  findingFingerprint,
125
+ formatAgentFeedback,
99
126
  formatCheckMarkdown,
100
127
  formatCheckTerminal,
101
128
  formatGithubAnnotations,
@@ -114,7 +141,13 @@ export {
114
141
  getHermeticGitEnv,
115
142
  getScoreColor,
116
143
  gitOutputPath,
144
+ hookCommand,
145
+ hookConfigPath,
146
+ hookPausedUntil,
147
+ hookStatus,
117
148
  initProject,
149
+ installHooks,
150
+ installInstructions,
118
151
  isCheckRule,
119
152
  isCodeFile,
120
153
  isTestFile,
@@ -122,17 +155,26 @@ export {
122
155
  loadConfig,
123
156
  matchIgnorePath,
124
157
  mostImportantFirst,
158
+ packageVersion,
125
159
  parseDiff,
160
+ parseDuration,
126
161
  parseGitLogStream,
162
+ pauseHook,
127
163
  replayHistory,
164
+ repositoryRoot,
128
165
  resolveCommitSha,
166
+ resumeHook,
129
167
  reviewChanges,
130
168
  reviewStagedChanges,
169
+ reviewWorktreeChanges,
131
170
  roastRepository,
171
+ runDemo,
172
+ runHook,
132
173
  streamGitDiff,
133
174
  streamGitLog,
134
175
  stripStringAndRegexLiterals,
135
176
  terminalText,
177
+ uninstallHooks,
136
178
  unquoteGitPath,
137
179
  validateExceptions,
138
180
  writeBaseline
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.11.0",
4
- "description": "Offline review for AI-assisted changes: catches weakened tests, lowered quality gates, skipped tests and swallowed errors",
3
+ "version": "0.12.0",
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
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -26,14 +26,22 @@
26
26
  },
27
27
  "keywords": [
28
28
  "ai",
29
- "agents",
30
29
  "ai-agents",
30
+ "ai-coding",
31
+ "llm",
31
32
  "claude-code",
32
- "cursor",
33
33
  "codex",
34
+ "cursor",
35
+ "copilot",
36
+ "hooks",
37
+ "guardrails",
34
38
  "code-review",
39
+ "testing",
35
40
  "test-quality",
41
+ "github-actions",
42
+ "pre-commit",
36
43
  "ci",
44
+ "static-analysis",
37
45
  "sarif",
38
46
  "git",
39
47
  "cli"