agent-roast 0.7.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,13 +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,
19
21
  reviewStagedChanges,
20
22
  roastRepository,
23
+ terminalText,
21
24
  writeBaseline
22
- } from "./chunk-YGHNTWHX.js";
25
+ } from "./chunk-C423TSZF.js";
23
26
 
24
27
  // src/cli.ts
25
28
  import { Command } from "commander";
@@ -28,6 +31,7 @@ import * as fs from "fs";
28
31
  import * as path from "path";
29
32
  var require2 = createRequire(import.meta.url);
30
33
  var pkg = require2("../package.json");
34
+ var errorMessage = (error) => terminalText(error instanceof Error ? error.message : String(error));
31
35
  async function runContextReview(repoPath, options, staged) {
32
36
  const controller = new AbortController();
33
37
  const abort = () => controller.abort();
@@ -37,30 +41,37 @@ async function runContextReview(repoPath, options, staged) {
37
41
  const common = {
38
42
  cwd: repoPath,
39
43
  failOn: options.failOn !== void 0 ? options.failOn.split(",").map((rule) => rule.trim()).filter(Boolean) : void 0,
44
+ ignoreExceptions: options.ignoreExceptions,
40
45
  format: options.json ? "json" : options.format,
41
46
  signal: controller.signal
42
47
  };
43
48
  const result = staged ? await reviewStagedChanges(common) : await reviewChanges({ ...common, base: options.base, head: options.head });
44
- if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, formatReviewMarkdown(result) + "\n");
45
- if (process.env.GITHUB_OUTPUT) fs.appendFileSync(
46
- process.env.GITHUB_OUTPUT,
47
- `passed=${result.passed}
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}
48
54
  infractions-count=${result.infractions.length}
49
55
  has-supported-changes=${result.filesInspected > 0}
50
56
  complete=${result.complete}
51
57
  existing-count=${result.unchangedCount}
52
58
  removed-count=${result.removedFindings.length}
53
- `
54
- );
55
- console.log(result.output);
56
- process.exitCode = result.exitCode;
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
+ }
57
68
  } finally {
58
69
  process.removeListener("SIGINT", abort);
59
70
  process.removeListener("SIGTERM", abort);
60
71
  }
61
72
  }
62
73
  var program = new Command();
63
- 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", "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("--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) => {
64
75
  try {
65
76
  if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
66
77
  console.warn(
@@ -75,6 +86,7 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
75
86
  await runContextReview(repoPath, options, true);
76
87
  return;
77
88
  }
89
+ if (options.ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
78
90
  if (!["terminal", "json", "github", "markdown"].includes(options.format)) {
79
91
  throw new ConfigError("--format must be terminal, json, github, or markdown.");
80
92
  }
@@ -87,7 +99,8 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
87
99
  if (!options.base && !options.staged && !options.json && !["terminal", "json"].includes(options.format)) {
88
100
  throw new ConfigError("Repository audits support terminal or json format; use --base or --staged for github/markdown.");
89
101
  }
90
- 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);
91
104
  if (options.base || options.staged) {
92
105
  const failOnRules = options.failOn !== void 0 ? options.failOn.split(",").map((s) => s.trim()).filter(Boolean) : config.failOn;
93
106
  const chosenFormat = options.json ? "json" : options.format || "terminal";
@@ -110,7 +123,7 @@ program.name("agent-roast").description("Audit your git history or PR diffs for
110
123
  formatGithubStepSummary(prResult) + "\n"
111
124
  );
112
125
  } catch (err) {
113
- 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)}`);
114
127
  }
115
128
  }
116
129
  if (process.env.GITHUB_OUTPUT) {
@@ -123,7 +136,7 @@ has-supported-changes=${prResult.hasSupportedChanges}
123
136
  `
124
137
  );
125
138
  } catch (err) {
126
- console.error(`Warning: Failed to write to GITHUB_OUTPUT: ${err.message}`);
139
+ console.error(`Warning: Failed to write to GITHUB_OUTPUT: ${errorMessage(err)}`);
127
140
  }
128
141
  }
129
142
  console.log(prResult.output);
@@ -145,7 +158,9 @@ has-supported-changes=${prResult.hasSupportedChanges}
145
158
  console.log(result.output);
146
159
  }
147
160
  const scoreThreshold = options.failUnder !== void 0 ? Number(options.failUnder) : config.scoreThreshold;
148
- 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) {
149
164
  process.exitCode = 1;
150
165
  } else {
151
166
  process.exitCode = 0;
@@ -153,10 +168,10 @@ has-supported-changes=${prResult.hasSupportedChanges}
153
168
  }
154
169
  } catch (err) {
155
170
  if (err instanceof ConfigError || err instanceof GitResolutionError || err instanceof InvalidDetectorError || err instanceof GitError || err.name === "GitError") {
156
- console.error(`agent-roast error: ${err.message}`);
171
+ console.error(`agent-roast error: ${errorMessage(err)}`);
157
172
  process.exitCode = 2;
158
173
  } else {
159
- console.error(`Error auditing repository: ${err.message}`);
174
+ console.error(`Error auditing repository: ${errorMessage(err)}`);
160
175
  process.exitCode = 1;
161
176
  }
162
177
  }
@@ -172,7 +187,7 @@ program.command("review").description("Compare full changed source at merge-base
172
187
  }
173
188
  await runContextReview(repoPath, options, options.staged);
174
189
  } catch (error) {
175
- console.error(`agent-roast review error: ${error instanceof Error ? error.message : String(error)}`);
190
+ console.error(`agent-roast review error: ${errorMessage(error)}`);
176
191
  process.exitCode = 2;
177
192
  }
178
193
  });
@@ -186,6 +201,7 @@ program.command("check").description("Check current tracked and non-ignored untr
186
201
  const result = await checkProject({
187
202
  cwd: repoPath,
188
203
  baseline: opts.baseline,
204
+ ignoreExceptions: global.ignoreExceptions,
189
205
  failOn: global.failOn !== void 0 ? global.failOn.split(",").map((rule) => rule.trim()).filter(Boolean) : void 0,
190
206
  format: global.json ? "json" : global.format
191
207
  });
@@ -194,22 +210,27 @@ program.command("check").description("Check current tracked and non-ignored untr
194
210
  const basePath = path.resolve(repoPath, opts.writeBaseline);
195
211
  writeBaseline(basePath, result.infractions);
196
212
  const message = `Baseline created: ${basePath}
197
- Accepted ${result.totalFindings} existing findings. The code has not been fixed.
213
+ Accepted ${result.infractions.length} existing findings. The code has not been fixed.
198
214
  Review and commit the baseline, then run check --baseline <file> to catch new findings.`;
199
- 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);
200
216
  process.exitCode = 0;
201
217
  return;
202
218
  }
203
219
  console.log(result.output);
204
220
  process.exitCode = result.exitCode;
205
221
  } catch (err) {
206
- console.error(`agent-roast check error: ${err instanceof Error ? err.message : String(err)}`);
222
+ console.error(`agent-roast check error: ${errorMessage(err)}`);
207
223
  process.exitCode = 2;
208
224
  }
209
225
  });
210
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
+ }
211
232
  if (!VALID_DETECTORS.includes(rule)) {
212
- console.error(`Unknown rule '${rule}'. Valid rules: ${VALID_DETECTORS.join(", ")}`);
233
+ console.error(`Unknown rule '${terminalText(rule)}'. Valid rules: ${VALID_DETECTORS.join(", ")}`);
213
234
  process.exitCode = 2;
214
235
  return;
215
236
  }
@@ -225,6 +246,7 @@ Example: ${guide.example}
225
246
  });
226
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) => {
227
248
  try {
249
+ if (program.opts().ignoreExceptions) throw new ConfigError("--ignore-exceptions applies to check, review and --staged context checks.");
228
250
  const res = await initProject({
229
251
  cwd: targetDir,
230
252
  yes: opts.yes,
@@ -232,31 +254,33 @@ program.command("init").description("Initialize .agentroastrc.json and CI/hook c
232
254
  hook: opts.hook
233
255
  });
234
256
  if (res.created.length > 0) {
235
- console.log(`Initialized agent-roast in ${targetDir}:`);
257
+ console.log(`Initialized agent-roast in ${terminalText(targetDir)}:`);
236
258
  for (const file of res.created) {
237
- console.log(` + Created ${file}`);
259
+ console.log(` + Created ${terminalText(file)}`);
238
260
  }
239
261
  }
240
262
  if (res.skipped.length > 0) {
241
263
  for (const file of res.skipped) {
242
- console.log(` - Skipped ${file} (already exists or not applicable)`);
264
+ console.log(` - Skipped ${terminalText(file)} (already exists or not applicable)`);
243
265
  }
244
266
  }
245
267
  if (res.created.length === 0 && res.skipped.length === 0) {
246
268
  console.log("No files were created.");
247
269
  }
248
270
  } catch (err) {
249
- console.error(`Failed to initialize agent-roast: ${err.message}`);
271
+ console.error(`Failed to initialize agent-roast: ${errorMessage(err)}`);
250
272
  process.exitCode = 1;
251
273
  }
252
274
  });
253
- 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) => {
254
276
  try {
255
277
  const rootOpts = program.opts();
256
278
  const isJson = Boolean(opts.json || rootOpts.json);
257
279
  const isAll = Boolean(opts.all || rootOpts.all);
258
- const since = opts.since || rootOpts.since || "90 days ago";
259
- 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);
260
284
  const result = await roastRepository({
261
285
  cwd: repoPath,
262
286
  since,
@@ -265,6 +289,7 @@ program.command("badge").description("Generate an AI discipline badge (SVG, Mark
265
289
  rules: config.rules,
266
290
  panicLoopWindowMinutes: config.panicLoopWindowMinutes
267
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.");
268
293
  const score = result.summary.score;
269
294
  const label = opts.label || "AI Discipline";
270
295
  if (isJson) {
@@ -278,7 +303,7 @@ program.command("badge").description("Generate an AI discipline badge (SVG, Mark
278
303
  const outPath = path.resolve(opts.out);
279
304
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
280
305
  fs.writeFileSync(outPath, svg, "utf-8");
281
- console.log(`Generated SVG badge at: ${opts.out}`);
306
+ console.log(`Generated SVG badge at: ${terminalText(opts.out)}`);
282
307
  process.exitCode = 0;
283
308
  return;
284
309
  }
@@ -290,8 +315,8 @@ ${markdown}
290
315
  `);
291
316
  process.exitCode = 0;
292
317
  } catch (err) {
293
- console.error(`Error generating badge: ${err.message}`);
294
- 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;
295
320
  }
296
321
  });
297
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;
@@ -12,6 +35,8 @@ interface CommitInfo {
12
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,6 +47,7 @@ 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;
@@ -41,6 +67,7 @@ interface MetricSummary {
41
67
  archetypeDescription: string;
42
68
  skippedLongLines?: number;
43
69
  skippedUnknownFiles?: number;
70
+ skippedBinaryFiles?: number;
44
71
  }
45
72
  interface RoastOptions {
46
73
  cwd?: string;
@@ -79,8 +106,10 @@ interface PrAuditResult {
79
106
  infractions: Infraction[];
80
107
  failedRules: string[];
81
108
  passed: boolean;
109
+ complete?: boolean;
82
110
  skippedLongLines: number;
83
111
  skippedUnknownFiles: number;
112
+ skippedBinaryFiles?: number;
84
113
  output: string;
85
114
  exitCode: number;
86
115
  }
@@ -89,6 +118,7 @@ interface CheckOptions {
89
118
  failOn?: string[];
90
119
  format?: ReviewFormat;
91
120
  baseline?: string;
121
+ ignoreExceptions?: boolean;
92
122
  signal?: AbortSignal;
93
123
  }
94
124
  interface CheckResult {
@@ -104,6 +134,8 @@ interface CheckResult {
104
134
  file: string;
105
135
  reason: string;
106
136
  }[];
137
+ suppressedFindings?: SuppressedFinding[];
138
+ expiredExceptionCount?: number;
107
139
  complete: boolean;
108
140
  failedRules: string[];
109
141
  passed: boolean;
@@ -117,6 +149,7 @@ interface ReviewOptions {
117
149
  head?: string;
118
150
  failOn?: string[];
119
151
  format?: ReviewFormat;
152
+ ignoreExceptions?: boolean;
120
153
  signal?: AbortSignal;
121
154
  }
122
155
  type StagedReviewOptions = Omit<ReviewOptions, "base" | "head">;
@@ -145,6 +178,8 @@ interface ReviewResult {
145
178
  file: string;
146
179
  reason: string;
147
180
  }[];
181
+ suppressedFindings?: SuppressedFinding[];
182
+ expiredExceptionCount?: number;
148
183
  complete: boolean;
149
184
  failedRules: string[];
150
185
  passed: boolean;
@@ -166,6 +201,7 @@ interface ParsedFileDiff {
166
201
  path: string;
167
202
  addedLines: ParsedDiffLine[];
168
203
  deleted?: boolean;
204
+ binary?: boolean;
169
205
  }
170
206
  interface ParsedCommit extends CommitInfo {
171
207
  files: ParsedFileDiff[];
@@ -186,6 +222,9 @@ declare class GitError extends Error {
186
222
  }
187
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"];
188
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;
189
228
  declare function parseGitLogStream(stream: Readable, onCommit: (commit: ParsedCommit, stats: {
190
229
  skippedLongLines: number;
191
230
  skippedUnknownFiles: number;
@@ -251,6 +290,7 @@ declare class MetricsCollector {
251
290
  private stubAbandonments;
252
291
  private skippedLongLines;
253
292
  private skippedUnknownFiles;
293
+ private skippedBinaryFiles;
254
294
  private activePanicChain;
255
295
  private includeAll;
256
296
  private ignorePaths?;
@@ -286,13 +326,17 @@ declare function formatGithubStepSummary(result: PrAuditResult): string;
286
326
  * Generates rich GitHub PR Comment Markdown (for PR comments / sticky bots)
287
327
  */
288
328
  declare function formatPrMarkdownComment(result: PrAuditResult): string;
329
+ declare function terminalText(value: string): string;
289
330
  /** Current-tree report emphasizes next actions and explicitly accounts for baseline debt. */
290
331
  declare function formatCheckTerminal(result: CheckResult): string;
291
332
  declare function formatCheckMarkdown(result: CheckResult): string;
292
333
  declare function formatReviewTerminal(result: ReviewResult): string;
293
334
  declare function formatReviewMarkdown(result: ReviewResult): string;
294
335
 
336
+ declare const MAX_CONFIG_BYTES: number;
295
337
  interface AgentRoastConfig {
338
+ /** Reviewed, occurrence-limited allowances that expire on a calendar date. */
339
+ exceptions?: FindingException[];
296
340
  /** Detector rules to fail CI on in PR mode */
297
341
  failOn?: string[];
298
342
  /** Glob patterns or prefixes to ignore from analysis */
@@ -313,9 +357,6 @@ declare class ConfigError extends Error {
313
357
  declare function configFromText(text: string, source: string): AgentRoastConfig;
314
358
  /** Invalid policy must stop the audit instead of silently changing CI behavior. */
315
359
  declare function loadConfig(cwd?: string): AgentRoastConfig;
316
- /**
317
- * Matches a relative or normalized file path against an array of glob ignore patterns.
318
- */
319
360
  declare function matchIgnorePath(filePath: string, ignorePatterns?: string[]): boolean;
320
361
 
321
362
  interface InitOptions {
@@ -409,4 +450,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
409
450
  }>;
410
451
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
411
452
 
412
- 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, type StagedReviewOptions, 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, reviewStagedChanges, 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,
@@ -56,9 +59,11 @@ import {
56
59
  streamGitDiff,
57
60
  streamGitLog,
58
61
  stripStringAndRegexLiterals,
62
+ terminalText,
59
63
  unquoteGitPath,
64
+ validateExceptions,
60
65
  writeBaseline
61
- } from "./chunk-YGHNTWHX.js";
66
+ } from "./chunk-C423TSZF.js";
62
67
  export {
63
68
  CONFIG_FILENAMES,
64
69
  ConfigError,
@@ -70,12 +75,14 @@ export {
70
75
  GitResolutionError,
71
76
  HERMETIC_GIT_FLAGS,
72
77
  InvalidDetectorError,
78
+ MAX_CONFIG_BYTES,
73
79
  MetricsCollector,
74
80
  PRE_COMMIT_HOOK_TEMPLATE,
75
81
  RECORD_SEPARATOR,
76
82
  RULE_GUIDES,
77
83
  VALID_DETECTORS,
78
84
  applyBaseline,
85
+ applyExceptions,
79
86
  auditFileDiffs,
80
87
  auditPullRequest,
81
88
  checkProject,
@@ -101,6 +108,7 @@ export {
101
108
  generateSvgBadge,
102
109
  getHermeticGitEnv,
103
110
  getScoreColor,
111
+ gitOutputPath,
104
112
  initProject,
105
113
  isCodeFile,
106
114
  isTestFile,
@@ -116,6 +124,8 @@ export {
116
124
  streamGitDiff,
117
125
  streamGitLog,
118
126
  stripStringAndRegexLiterals,
127
+ terminalText,
119
128
  unquoteGitPath,
129
+ validateExceptions,
120
130
  writeBaseline
121
131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.7.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
  }
package/schema.json ADDED
@@ -0,0 +1,138 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/Nari-Ab/agent-roast/main/schema.json",
4
+ "title": "Agent Roast configuration",
5
+ "description": "Offline review policy. Changes are policy changes and require human review.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "failOn": {
13
+ "description": "Enabled rules which block; an empty array is advisory.",
14
+ "type": "array",
15
+ "items": {
16
+ "enum": [
17
+ "test-skip",
18
+ "type-escape",
19
+ "swallowed-error",
20
+ "panic-loop",
21
+ "trivial-assert",
22
+ "stub-abandonment",
23
+ "test-focus"
24
+ ]
25
+ }
26
+ },
27
+ "ignorePaths": {
28
+ "description": "Repository-relative simple glob or directory-prefix exclusions.",
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string",
32
+ "maxLength": 1024
33
+ },
34
+ "maxItems": 1000
35
+ },
36
+ "scoreThreshold": {
37
+ "type": "number",
38
+ "minimum": 0,
39
+ "maximum": 100
40
+ },
41
+ "panicLoopWindowMinutes": {
42
+ "type": "number",
43
+ "exclusiveMinimum": 0
44
+ },
45
+ "rules": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "test-skip": {
50
+ "enum": [
51
+ "error",
52
+ "warn",
53
+ "off"
54
+ ]
55
+ },
56
+ "type-escape": {
57
+ "enum": [
58
+ "error",
59
+ "warn",
60
+ "off"
61
+ ]
62
+ },
63
+ "swallowed-error": {
64
+ "enum": [
65
+ "error",
66
+ "warn",
67
+ "off"
68
+ ]
69
+ },
70
+ "panic-loop": {
71
+ "enum": [
72
+ "error",
73
+ "warn",
74
+ "off"
75
+ ]
76
+ },
77
+ "trivial-assert": {
78
+ "enum": [
79
+ "error",
80
+ "warn",
81
+ "off"
82
+ ]
83
+ },
84
+ "stub-abandonment": {
85
+ "enum": [
86
+ "error",
87
+ "warn",
88
+ "off"
89
+ ]
90
+ },
91
+ "test-focus": {
92
+ "enum": [
93
+ "error",
94
+ "warn",
95
+ "off"
96
+ ]
97
+ }
98
+ }
99
+ },
100
+ "exceptions": {
101
+ "description": "Precise, reviewed allowances. A new duplicate beyond count remains actionable; expiry includes its UTC date.",
102
+ "type": "array",
103
+ "maxItems": 1000,
104
+ "items": {
105
+ "type": "object",
106
+ "additionalProperties": false,
107
+ "required": [
108
+ "fingerprint",
109
+ "count",
110
+ "reason",
111
+ "expires"
112
+ ],
113
+ "properties": {
114
+ "fingerprint": {
115
+ "type": "string",
116
+ "pattern": "^[a-f0-9]{64}$"
117
+ },
118
+ "count": {
119
+ "type": "integer",
120
+ "minimum": 1,
121
+ "maximum": 100000
122
+ },
123
+ "reason": {
124
+ "type": "string",
125
+ "minLength": 1,
126
+ "maxLength": 1000,
127
+ "pattern": "^(?=.*\\S)[^\\u0000-\\u001f\\u007f-\\u009f\\u061c\\u200e\\u200f\\u2028-\\u202e\\u2066-\\u2069]*$"
128
+ },
129
+ "expires": {
130
+ "type": "string",
131
+ "format": "date",
132
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }