agent-roast 0.6.0 → 0.8.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
@@ -13,12 +13,16 @@ import {
13
13
  generateShieldsJson,
14
14
  generateShieldsMarkdown,
15
15
  generateSvgBadge,
16
+ gitOutputPath,
16
17
  initProject,
17
18
  loadConfig,
19
+ readGit,
18
20
  reviewChanges,
21
+ reviewStagedChanges,
19
22
  roastRepository,
23
+ terminalText,
20
24
  writeBaseline
21
- } from "./chunk-RU5OYHLR.js";
25
+ } from "./chunk-C423TSZF.js";
22
26
 
23
27
  // src/cli.ts
24
28
  import { Command } from "commander";
@@ -27,8 +31,47 @@ import * as fs from "fs";
27
31
  import * as path from "path";
28
32
  var require2 = createRequire(import.meta.url);
29
33
  var pkg = require2("../package.json");
34
+ var errorMessage = (error) => terminalText(error instanceof Error ? error.message : String(error));
35
+ async function runContextReview(repoPath, options, staged) {
36
+ const controller = new AbortController();
37
+ const abort = () => controller.abort();
38
+ process.once("SIGINT", abort);
39
+ process.once("SIGTERM", abort);
40
+ try {
41
+ const common = {
42
+ cwd: repoPath,
43
+ failOn: options.failOn !== void 0 ? options.failOn.split(",").map((rule) => rule.trim()).filter(Boolean) : void 0,
44
+ ignoreExceptions: options.ignoreExceptions,
45
+ format: options.json ? "json" : options.format,
46
+ signal: controller.signal
47
+ };
48
+ const result = staged ? await reviewStagedChanges(common) : await reviewChanges({ ...common, base: options.base, head: options.head });
49
+ console.log(result.output);
50
+ process.exitCode = result.exitCode;
51
+ for (const [filename, content] of [
52
+ [process.env.GITHUB_STEP_SUMMARY, formatReviewMarkdown(result) + "\n"],
53
+ [process.env.GITHUB_OUTPUT, `passed=${result.passed}
54
+ infractions-count=${result.infractions.length}
55
+ has-supported-changes=${result.filesInspected > 0}
56
+ complete=${result.complete}
57
+ existing-count=${result.unchangedCount}
58
+ removed-count=${result.removedFindings.length}
59
+ `]
60
+ ]) {
61
+ if (!filename) continue;
62
+ try {
63
+ fs.appendFileSync(filename, content);
64
+ } catch (error) {
65
+ console.error(`Warning: could not write an optional GitHub report artifact: ${errorMessage(error)}`);
66
+ }
67
+ }
68
+ } finally {
69
+ process.removeListener("SIGINT", abort);
70
+ process.removeListener("SIGTERM", abort);
71
+ }
72
+ }
30
73
  var program = new Command();
31
- program.name("agent-roast").description("Audit your git history or PR diffs for AI coding shortcuts and panic loops").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", "Audit staged changes before committing (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("--fail-on <rules>", "Comma-separated detectors that trigger CI failure").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) => {
74
+ 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("--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) => {
32
75
  try {
33
76
  if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
34
77
  console.warn(
@@ -36,6 +79,14 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
36
79
  );
37
80
  }
38
81
  if (options.staged && options.base) throw new ConfigError("--staged cannot be combined with --base.");
82
+ if (options.staged) {
83
+ if (options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli" || program.getOptionValueSource("head") === "cli") {
84
+ throw new ConfigError("--staged compares HEAD and index; --all, --since, --head and --fail-under do not apply.");
85
+ }
86
+ await runContextReview(repoPath, options, true);
87
+ return;
88
+ }
89
+ if (options.ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
39
90
  if (!["terminal", "json", "github", "markdown"].includes(options.format)) {
40
91
  throw new ConfigError("--format must be terminal, json, github, or markdown.");
41
92
  }
@@ -48,7 +99,8 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
48
99
  if (!options.base && !options.staged && !options.json && !["terminal", "json"].includes(options.format)) {
49
100
  throw new ConfigError("Repository audits support terminal or json format; use --base or --staged for github/markdown.");
50
101
  }
51
- const config = await loadConfig(repoPath);
102
+ const configRoot = gitOutputPath((await readGit(path.resolve(repoPath), ["rev-parse", "--show-toplevel"])).toString("utf8"));
103
+ const config = loadConfig(configRoot);
52
104
  if (options.base || options.staged) {
53
105
  const failOnRules = options.failOn !== void 0 ? options.failOn.split(",").map((s) => s.trim()).filter(Boolean) : config.failOn;
54
106
  const chosenFormat = options.json ? "json" : options.format || "terminal";
@@ -71,7 +123,7 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
71
123
  formatGithubStepSummary(prResult) + "\n"
72
124
  );
73
125
  } catch (err) {
74
- console.error(`Warning: Failed to write to GITHUB_STEP_SUMMARY: ${err.message}`);
126
+ console.error(`Warning: Failed to write to GITHUB_STEP_SUMMARY: ${errorMessage(err)}`);
75
127
  }
76
128
  }
77
129
  if (process.env.GITHUB_OUTPUT) {
@@ -84,7 +136,7 @@ has-supported-changes=${prResult.hasSupportedChanges}
84
136
  `
85
137
  );
86
138
  } catch (err) {
87
- console.error(`Warning: Failed to write to GITHUB_OUTPUT: ${err.message}`);
139
+ console.error(`Warning: Failed to write to GITHUB_OUTPUT: ${errorMessage(err)}`);
88
140
  }
89
141
  }
90
142
  console.log(prResult.output);
@@ -106,7 +158,9 @@ has-supported-changes=${prResult.hasSupportedChanges}
106
158
  console.log(result.output);
107
159
  }
108
160
  const scoreThreshold = options.failUnder !== void 0 ? Number(options.failUnder) : config.scoreThreshold;
109
- if (scoreThreshold !== void 0 && !isNaN(scoreThreshold) && result.summary.score !== null && result.summary.score < scoreThreshold) {
161
+ if (result.summary.skippedLongLines || result.summary.skippedUnknownFiles || result.summary.skippedBinaryFiles) {
162
+ process.exitCode = 2;
163
+ } else if (scoreThreshold !== void 0 && !isNaN(scoreThreshold) && result.summary.score !== null && result.summary.score < scoreThreshold) {
110
164
  process.exitCode = 1;
111
165
  } else {
112
166
  process.exitCode = 0;
@@ -114,53 +168,26 @@ has-supported-changes=${prResult.hasSupportedChanges}
114
168
  }
115
169
  } catch (err) {
116
170
  if (err instanceof ConfigError || err instanceof GitResolutionError || err instanceof InvalidDetectorError || err instanceof GitError || err.name === "GitError") {
117
- console.error(`agent-roast error: ${err.message}`);
171
+ console.error(`agent-roast error: ${errorMessage(err)}`);
118
172
  process.exitCode = 2;
119
173
  } else {
120
- console.error(`Error auditing repository: ${err.message}`);
174
+ console.error(`Error auditing repository: ${errorMessage(err)}`);
121
175
  process.exitCode = 1;
122
176
  }
123
177
  }
124
178
  });
125
- 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)\nOutput: --format terminal|json|github|markdown|sarif, --json, --fail-on <rules>\nExample: agent-roast review --base origin/main --format markdown").action(async (repoPath) => {
179
+ 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>\nExample: agent-roast review --base origin/main --format markdown").action(async (repoPath) => {
126
180
  try {
127
181
  const options = program.opts();
128
- if (!options.base) throw new ConfigError("review requires --base <trusted-ref>.");
129
- if (options.staged || options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli") {
130
- throw new ConfigError("review compares committed refs; --staged, --all, --since and --fail-under do not apply.");
182
+ if (options.staged ? options.base || program.getOptionValueSource("head") === "cli" : !options.base) {
183
+ throw new ConfigError("Use review --base <trusted-ref> or review --staged; --base/--head cannot be combined with --staged.");
131
184
  }
132
- const controller = new AbortController();
133
- const abort = () => controller.abort();
134
- process.once("SIGINT", abort);
135
- process.once("SIGTERM", abort);
136
- try {
137
- const result = await reviewChanges({
138
- cwd: repoPath,
139
- base: options.base,
140
- head: options.head,
141
- failOn: options.failOn !== void 0 ? options.failOn.split(",").map((rule) => rule.trim()).filter(Boolean) : void 0,
142
- format: options.json ? "json" : options.format,
143
- signal: controller.signal
144
- });
145
- if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, formatReviewMarkdown(result) + "\n");
146
- if (process.env.GITHUB_OUTPUT) fs.appendFileSync(
147
- process.env.GITHUB_OUTPUT,
148
- `passed=${result.passed}
149
- infractions-count=${result.infractions.length}
150
- has-supported-changes=${result.filesInspected > 0}
151
- complete=${result.complete}
152
- existing-count=${result.unchangedCount}
153
- removed-count=${result.removedFindings.length}
154
- `
155
- );
156
- console.log(result.output);
157
- process.exitCode = result.exitCode;
158
- } finally {
159
- process.removeListener("SIGINT", abort);
160
- process.removeListener("SIGTERM", abort);
185
+ if (options.all || options.failUnder !== void 0 || program.getOptionValueSource("since") === "cli") {
186
+ throw new ConfigError("review compares source snapshots; --all, --since and --fail-under do not apply.");
161
187
  }
188
+ await runContextReview(repoPath, options, options.staged);
162
189
  } catch (error) {
163
- console.error(`agent-roast review error: ${error instanceof Error ? error.message : String(error)}`);
190
+ console.error(`agent-roast review error: ${errorMessage(error)}`);
164
191
  process.exitCode = 2;
165
192
  }
166
193
  });
@@ -174,6 +201,7 @@ program.command("check").description("Check current tracked and non-ignored untr
174
201
  const result = await checkProject({
175
202
  cwd: repoPath,
176
203
  baseline: opts.baseline,
204
+ ignoreExceptions: global.ignoreExceptions,
177
205
  failOn: global.failOn !== void 0 ? global.failOn.split(",").map((rule) => rule.trim()).filter(Boolean) : void 0,
178
206
  format: global.json ? "json" : global.format
179
207
  });
@@ -182,22 +210,27 @@ program.command("check").description("Check current tracked and non-ignored untr
182
210
  const basePath = path.resolve(repoPath, opts.writeBaseline);
183
211
  writeBaseline(basePath, result.infractions);
184
212
  const message = `Baseline created: ${basePath}
185
- Accepted ${result.totalFindings} existing findings. The code has not been fixed.
213
+ Accepted ${result.infractions.length} existing findings. The code has not been fixed.
186
214
  Review and commit the baseline, then run check --baseline <file> to catch new findings.`;
187
- console.log(global.json || global.format === "json" ? JSON.stringify({ operation: "write-baseline", baseline: basePath, acceptedFindings: result.totalFindings }, null, 2) : message);
215
+ console.log(global.json || global.format === "json" ? JSON.stringify({ operation: "write-baseline", baseline: basePath, acceptedFindings: result.infractions.length }, null, 2) : message);
188
216
  process.exitCode = 0;
189
217
  return;
190
218
  }
191
219
  console.log(result.output);
192
220
  process.exitCode = result.exitCode;
193
221
  } catch (err) {
194
- console.error(`agent-roast check error: ${err instanceof Error ? err.message : String(err)}`);
222
+ console.error(`agent-roast check error: ${errorMessage(err)}`);
195
223
  process.exitCode = 2;
196
224
  }
197
225
  });
198
226
  program.command("explain <rule>").description("Explain a detector, its limits, and how to address a finding").action((rule) => {
227
+ if (program.opts().ignoreExceptions) {
228
+ console.error("--ignore-exceptions applies to check, review and --staged context checks.");
229
+ process.exitCode = 2;
230
+ return;
231
+ }
199
232
  if (!VALID_DETECTORS.includes(rule)) {
200
- console.error(`Unknown rule '${rule}'. Valid rules: ${VALID_DETECTORS.join(", ")}`);
233
+ console.error(`Unknown rule '${terminalText(rule)}'. Valid rules: ${VALID_DETECTORS.join(", ")}`);
201
234
  process.exitCode = 2;
202
235
  return;
203
236
  }
@@ -213,6 +246,7 @@ Example: ${guide.example}
213
246
  });
214
247
  program.command("init").description("Initialize .agentroastrc.json and CI/hook configurations").argument("[dir]", "Directory to initialize", ".").option("-y, --yes", "Skip interactive prompts and use defaults", false).option("--ci", "Generate GitHub Actions workflow file", false).option("--hook", "Install Git pre-commit hook", false).action(async (targetDir, opts) => {
215
248
  try {
249
+ if (program.opts().ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
216
250
  const res = await initProject({
217
251
  cwd: targetDir,
218
252
  yes: opts.yes,
@@ -220,31 +254,33 @@ program.command("init").description("Initialize .agentroastrc.json and CI/hook c
220
254
  hook: opts.hook
221
255
  });
222
256
  if (res.created.length > 0) {
223
- console.log(`Initialized agent-roast in ${targetDir}:`);
257
+ console.log(`Initialized agent-roast in ${terminalText(targetDir)}:`);
224
258
  for (const file of res.created) {
225
- console.log(` + Created ${file}`);
259
+ console.log(` + Created ${terminalText(file)}`);
226
260
  }
227
261
  }
228
262
  if (res.skipped.length > 0) {
229
263
  for (const file of res.skipped) {
230
- console.log(` - Skipped ${file} (already exists or not applicable)`);
264
+ console.log(` - Skipped ${terminalText(file)} (already exists or not applicable)`);
231
265
  }
232
266
  }
233
267
  if (res.created.length === 0 && res.skipped.length === 0) {
234
268
  console.log("No files were created.");
235
269
  }
236
270
  } catch (err) {
237
- console.error(`Failed to initialize agent-roast: ${err.message}`);
271
+ console.error(`Failed to initialize agent-roast: ${errorMessage(err)}`);
238
272
  process.exitCode = 1;
239
273
  }
240
274
  });
241
- program.command("badge").description("Generate an AI discipline badge (SVG, Markdown, or Shields JSON)").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("--label <text>", "Badge label text", "AI Discipline").option("--out <file>", "Output SVG file path (e.g. ./badge.svg)").action(async (repoPath, opts) => {
275
+ program.command("badge").description("Generate an AI discipline badge (SVG, Markdown, or Shields JSON)").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("--label <text>", "Badge label text", "AI Discipline").option("--out <file>", "Output SVG file path (e.g. ./badge.svg)").action(async (repoPath, opts, command) => {
242
276
  try {
243
277
  const rootOpts = program.opts();
244
278
  const isJson = Boolean(opts.json || rootOpts.json);
245
279
  const isAll = Boolean(opts.all || rootOpts.all);
246
- const since = opts.since || rootOpts.since || "90 days ago";
247
- const config = await loadConfig(repoPath);
280
+ if (rootOpts.ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
281
+ const since = command.getOptionValueSource("since") === "cli" ? opts.since : rootOpts.since || "90 days ago";
282
+ const configRoot = gitOutputPath((await readGit(path.resolve(repoPath), ["rev-parse", "--show-toplevel"])).toString("utf8"));
283
+ const config = loadConfig(configRoot);
248
284
  const result = await roastRepository({
249
285
  cwd: repoPath,
250
286
  since,
@@ -253,6 +289,7 @@ program.command("badge").description("Generate an AI discipline badge (SVG, Mark
253
289
  rules: config.rules,
254
290
  panicLoopWindowMinutes: config.panicLoopWindowMinutes
255
291
  });
292
+ if (result.summary.skippedLongLines || result.summary.skippedUnknownFiles || result.summary.skippedBinaryFiles) throw new ConfigError("Cannot generate a badge from an incomplete historical audit; resolve skipped source first.");
256
293
  const score = result.summary.score;
257
294
  const label = opts.label || "AI Discipline";
258
295
  if (isJson) {
@@ -266,7 +303,7 @@ program.command("badge").description("Generate an AI discipline badge (SVG, Mark
266
303
  const outPath = path.resolve(opts.out);
267
304
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
268
305
  fs.writeFileSync(outPath, svg, "utf-8");
269
- console.log(`Generated SVG badge at: ${opts.out}`);
306
+ console.log(`Generated SVG badge at: ${terminalText(opts.out)}`);
270
307
  process.exitCode = 0;
271
308
  return;
272
309
  }
@@ -278,8 +315,8 @@ ${markdown}
278
315
  `);
279
316
  process.exitCode = 0;
280
317
  } catch (err) {
281
- console.error(`Error generating badge: ${err.message}`);
282
- process.exitCode = 1;
318
+ console.error(`Error generating badge: ${errorMessage(err)}`);
319
+ process.exitCode = err instanceof ConfigError || err instanceof GitError || err instanceof GitResolutionError ? 2 : 1;
283
320
  }
284
321
  });
285
322
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,28 @@
1
1
  import { Readable } from 'stream';
2
2
 
3
+ /** A reviewed allowance for a precise pattern and number of occurrences.
4
+ * No wildcard, inline bypass, source snippet or executable policy is needed. */
5
+ interface FindingException {
6
+ fingerprint: string;
7
+ count: number;
8
+ reason: string;
9
+ expires: string;
10
+ }
11
+ interface SuppressedFinding extends Infraction {
12
+ suppression: {
13
+ reason: string;
14
+ expires: string;
15
+ };
16
+ }
17
+ declare function validateExceptions(value: unknown): asserts value is FindingException[] | undefined;
18
+ /** Line shifts are tolerated; duplicate occurrences beyond the approved count
19
+ * remain actionable. Expiry uses UTC calendar days, inclusive through that day. */
20
+ declare function applyExceptions(findings: Infraction[], exceptions?: FindingException[], now?: Date, priorOccurrences?: ReadonlyMap<string, number>): {
21
+ findings: Infraction[];
22
+ suppressedFindings: SuppressedFinding[];
23
+ expiredExceptionCount: number;
24
+ };
25
+
3
26
  interface CommitInfo {
4
27
  hash: string;
5
28
  date: string;
@@ -9,9 +32,11 @@ interface CommitInfo {
9
32
  isAiAttributed: boolean;
10
33
  aiSignatures: string[];
11
34
  }
12
- type DetectorType = "test-skip" | "type-escape" | "swallowed-error" | "panic-loop" | "trivial-assert" | "stub-abandonment";
35
+ type DetectorType = "test-skip" | "type-escape" | "swallowed-error" | "panic-loop" | "trivial-assert" | "stub-abandonment" | "test-focus";
13
36
  declare const VALID_DETECTORS: readonly DetectorType[];
14
37
  interface Infraction {
38
+ fingerprint?: string;
39
+ occurrence?: number;
15
40
  type: DetectorType;
16
41
  file?: string;
17
42
  lineNumber?: number;
@@ -22,12 +47,14 @@ interface Infraction {
22
47
  suggestion?: string;
23
48
  }
24
49
  interface MetricSummary {
50
+ complete?: boolean;
25
51
  schemaVersion: string;
26
52
  totalLinesAdded: number;
27
53
  aiLinesAdded: number;
28
54
  totalCommits: number;
29
55
  aiCommits: number;
30
56
  testSkips: Infraction[];
57
+ focusedTests?: Infraction[];
31
58
  typeEscapes: Infraction[];
32
59
  swallowedErrors: Infraction[];
33
60
  panicLoops: Infraction[];
@@ -40,6 +67,7 @@ interface MetricSummary {
40
67
  archetypeDescription: string;
41
68
  skippedLongLines?: number;
42
69
  skippedUnknownFiles?: number;
70
+ skippedBinaryFiles?: number;
43
71
  }
44
72
  interface RoastOptions {
45
73
  cwd?: string;
@@ -78,8 +106,10 @@ interface PrAuditResult {
78
106
  infractions: Infraction[];
79
107
  failedRules: string[];
80
108
  passed: boolean;
109
+ complete?: boolean;
81
110
  skippedLongLines: number;
82
111
  skippedUnknownFiles: number;
112
+ skippedBinaryFiles?: number;
83
113
  output: string;
84
114
  exitCode: number;
85
115
  }
@@ -88,6 +118,7 @@ interface CheckOptions {
88
118
  failOn?: string[];
89
119
  format?: ReviewFormat;
90
120
  baseline?: string;
121
+ ignoreExceptions?: boolean;
91
122
  signal?: AbortSignal;
92
123
  }
93
124
  interface CheckResult {
@@ -103,6 +134,8 @@ interface CheckResult {
103
134
  file: string;
104
135
  reason: string;
105
136
  }[];
137
+ suppressedFindings?: SuppressedFinding[];
138
+ expiredExceptionCount?: number;
106
139
  complete: boolean;
107
140
  failedRules: string[];
108
141
  passed: boolean;
@@ -116,11 +149,14 @@ interface ReviewOptions {
116
149
  head?: string;
117
150
  failOn?: string[];
118
151
  format?: ReviewFormat;
152
+ ignoreExceptions?: boolean;
119
153
  signal?: AbortSignal;
120
154
  }
155
+ type StagedReviewOptions = Omit<ReviewOptions, "base" | "head">;
121
156
  interface ReviewResult {
122
157
  schemaVersion: string;
123
158
  mode: "review";
159
+ source?: "commits" | "index";
124
160
  baseSha: string;
125
161
  headSha: string;
126
162
  mergeBaseSha: string;
@@ -142,6 +178,8 @@ interface ReviewResult {
142
178
  file: string;
143
179
  reason: string;
144
180
  }[];
181
+ suppressedFindings?: SuppressedFinding[];
182
+ expiredExceptionCount?: number;
145
183
  complete: boolean;
146
184
  failedRules: string[];
147
185
  passed: boolean;
@@ -163,6 +201,7 @@ interface ParsedFileDiff {
163
201
  path: string;
164
202
  addedLines: ParsedDiffLine[];
165
203
  deleted?: boolean;
204
+ binary?: boolean;
166
205
  }
167
206
  interface ParsedCommit extends CommitInfo {
168
207
  files: ParsedFileDiff[];
@@ -183,6 +222,9 @@ declare class GitError extends Error {
183
222
  }
184
223
  declare const HERMETIC_GIT_FLAGS: readonly ["-c", "core.quotepath=false", "-c", "core.fsmonitor=false", "-c", "diff.noprefix=false", "-c", "diff.mnemonicPrefix=false", "--no-pager"];
185
224
  declare function getHermeticGitEnv(extraEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
225
+ /** Git path output has one record-terminating LF. Spaces, CR and extra LFs may
226
+ * belong to the actual pathname and must not be removed with trim(). */
227
+ declare function gitOutputPath(value: string): string;
186
228
  declare function parseGitLogStream(stream: Readable, onCommit: (commit: ParsedCommit, stats: {
187
229
  skippedLongLines: number;
188
230
  skippedUnknownFiles: number;
@@ -240,6 +282,7 @@ declare class MetricsCollector {
240
282
  private totalCommits;
241
283
  private aiCommits;
242
284
  private testSkips;
285
+ private focusedTests;
243
286
  private typeEscapes;
244
287
  private swallowedErrors;
245
288
  private panicLoops;
@@ -247,6 +290,7 @@ declare class MetricsCollector {
247
290
  private stubAbandonments;
248
291
  private skippedLongLines;
249
292
  private skippedUnknownFiles;
293
+ private skippedBinaryFiles;
250
294
  private activePanicChain;
251
295
  private includeAll;
252
296
  private ignorePaths?;
@@ -282,13 +326,17 @@ declare function formatGithubStepSummary(result: PrAuditResult): string;
282
326
  * Generates rich GitHub PR Comment Markdown (for PR comments / sticky bots)
283
327
  */
284
328
  declare function formatPrMarkdownComment(result: PrAuditResult): string;
329
+ declare function terminalText(value: string): string;
285
330
  /** Current-tree report emphasizes next actions and explicitly accounts for baseline debt. */
286
331
  declare function formatCheckTerminal(result: CheckResult): string;
287
332
  declare function formatCheckMarkdown(result: CheckResult): string;
288
333
  declare function formatReviewTerminal(result: ReviewResult): string;
289
334
  declare function formatReviewMarkdown(result: ReviewResult): string;
290
335
 
336
+ declare const MAX_CONFIG_BYTES: number;
291
337
  interface AgentRoastConfig {
338
+ /** Reviewed, occurrence-limited allowances that expire on a calendar date. */
339
+ exceptions?: FindingException[];
292
340
  /** Detector rules to fail CI on in PR mode */
293
341
  failOn?: string[];
294
342
  /** Glob patterns or prefixes to ignore from analysis */
@@ -309,9 +357,6 @@ declare class ConfigError extends Error {
309
357
  declare function configFromText(text: string, source: string): AgentRoastConfig;
310
358
  /** Invalid policy must stop the audit instead of silently changing CI behavior. */
311
359
  declare function loadConfig(cwd?: string): AgentRoastConfig;
312
- /**
313
- * Matches a relative or normalized file path against an array of glob ignore patterns.
314
- */
315
360
  declare function matchIgnorePath(filePath: string, ignorePatterns?: string[]): boolean;
316
361
 
317
362
  interface InitOptions {
@@ -361,6 +406,7 @@ declare function compareFindings(before: Infraction[], after: Infraction[], newP
361
406
  };
362
407
  /** Compare full source blobs in changed files without executing or checking out the candidate. */
363
408
  declare function reviewChanges(options: ReviewOptions): Promise<ReviewResult>;
409
+ declare function reviewStagedChanges(options?: StagedReviewOptions): Promise<ReviewResult>;
364
410
 
365
411
  /** Offline SARIF 2.1.0, with relative URIs and explicit incomplete-run status.
366
412
  * Snippets are omitted to avoid embedding source code in integration artifacts. */
@@ -404,4 +450,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
404
450
  }>;
405
451
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
406
452
 
407
- 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, GITHUB_WORKFLOW_TEMPLATE, GitError, GitResolutionError, HERMETIC_GIT_FLAGS, type Infraction, type InitOptions, type InitResult, InvalidDetectorError, type MetricSummary, MetricsCollector, PRE_COMMIT_HOOK_TEMPLATE, type ParseDiffResult, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type PrAuditOptions, type PrAuditResult, RECORD_SEPARATOR, RULE_GUIDES, type ReviewFormat, type ReviewOptions, type ReviewResult, type RoastOptions, type RuleGuide, type ShieldsEndpointResponse, VALID_DETECTORS, applyBaseline, auditFileDiffs, auditPullRequest, checkProject, compareFindings, configFromText, createBaseline, detectAiAttribution, findMergeBase, findingFingerprint, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, initProject, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, reviewChanges, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath, writeBaseline };
453
+ 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 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, formatReviewMarkdown, formatReviewTerminal, formatSarif, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, gitOutputPath, initProject, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, reviewChanges, reviewStagedChanges, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, terminalText, unquoteGitPath, validateExceptions, writeBaseline };
package/dist/index.js CHANGED
@@ -10,12 +10,14 @@ import {
10
10
  GitResolutionError,
11
11
  HERMETIC_GIT_FLAGS,
12
12
  InvalidDetectorError,
13
+ MAX_CONFIG_BYTES,
13
14
  MetricsCollector,
14
15
  PRE_COMMIT_HOOK_TEMPLATE,
15
16
  RECORD_SEPARATOR,
16
17
  RULE_GUIDES,
17
18
  VALID_DETECTORS,
18
19
  applyBaseline,
20
+ applyExceptions,
19
21
  auditFileDiffs,
20
22
  auditPullRequest,
21
23
  checkProject,
@@ -41,6 +43,7 @@ import {
41
43
  generateSvgBadge,
42
44
  getHermeticGitEnv,
43
45
  getScoreColor,
46
+ gitOutputPath,
44
47
  initProject,
45
48
  isCodeFile,
46
49
  isTestFile,
@@ -51,13 +54,16 @@ import {
51
54
  parseGitLogStream,
52
55
  resolveCommitSha,
53
56
  reviewChanges,
57
+ reviewStagedChanges,
54
58
  roastRepository,
55
59
  streamGitDiff,
56
60
  streamGitLog,
57
61
  stripStringAndRegexLiterals,
62
+ terminalText,
58
63
  unquoteGitPath,
64
+ validateExceptions,
59
65
  writeBaseline
60
- } from "./chunk-RU5OYHLR.js";
66
+ } from "./chunk-C423TSZF.js";
61
67
  export {
62
68
  CONFIG_FILENAMES,
63
69
  ConfigError,
@@ -69,12 +75,14 @@ export {
69
75
  GitResolutionError,
70
76
  HERMETIC_GIT_FLAGS,
71
77
  InvalidDetectorError,
78
+ MAX_CONFIG_BYTES,
72
79
  MetricsCollector,
73
80
  PRE_COMMIT_HOOK_TEMPLATE,
74
81
  RECORD_SEPARATOR,
75
82
  RULE_GUIDES,
76
83
  VALID_DETECTORS,
77
84
  applyBaseline,
85
+ applyExceptions,
78
86
  auditFileDiffs,
79
87
  auditPullRequest,
80
88
  checkProject,
@@ -100,6 +108,7 @@ export {
100
108
  generateSvgBadge,
101
109
  getHermeticGitEnv,
102
110
  getScoreColor,
111
+ gitOutputPath,
103
112
  initProject,
104
113
  isCodeFile,
105
114
  isTestFile,
@@ -110,10 +119,13 @@ export {
110
119
  parseGitLogStream,
111
120
  resolveCommitSha,
112
121
  reviewChanges,
122
+ reviewStagedChanges,
113
123
  roastRepository,
114
124
  streamGitDiff,
115
125
  streamGitLog,
116
126
  stripStringAndRegexLiterals,
127
+ terminalText,
117
128
  unquoteGitPath,
129
+ validateExceptions,
118
130
  writeBaseline
119
131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Offline code checks for skipped tests, swallowed errors, unfinished stubs and AI coding shortcuts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,7 +11,8 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
14
- "LICENSE"
14
+ "LICENSE",
15
+ "schema.json"
15
16
  ],
16
17
  "scripts": {
17
18
  "build": "tsup",
@@ -51,9 +52,13 @@
51
52
  "gifenc": "^1.0.3",
52
53
  "tsup": "^8.0.0",
53
54
  "typescript": "^5.0.0",
54
- "vitest": "^3.0.0"
55
+ "vitest": "^4.1.12"
55
56
  },
56
57
  "engines": {
57
58
  "node": ">=18"
59
+ },
60
+ "overrides": {
61
+ "esbuild": "^0.28.1",
62
+ "vite": "^7.3.7"
58
63
  }
59
64
  }