agent-roast 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,67 @@ A finding includes its file and line, the source snippet, and a concrete next st
48
48
  To see the complete machine-readable result, use `check --format json`; terminal
49
49
  output shows the first 20 findings. `check --format markdown` produces a review table.
50
50
 
51
+ ### Context review for pull requests
52
+
53
+ ```bash
54
+ # Compare committed source at the common ancestor and candidate head
55
+ agent-roast review --base origin/main --head HEAD
56
+ agent-roast review --base origin/main --format markdown
57
+
58
+ # Export a report for tools that consume SARIF 2.1.0
59
+ agent-roast review --base origin/main --format sarif > agentroast.sarif
60
+ agent-roast check --format sarif > current-code.sarif
61
+ ```
62
+
63
+ `review` compares **full contents of changed source files**, so deleting the only
64
+ handler inside a `catch` is detectable even when no suspicious line was added.
65
+ It reports new findings, existing findings, and patterns no longer detected. Only
66
+ new blocking findings fail the check; no manually generated baseline is required.
67
+ Deleting a file is reported as disappearance of a pattern, not proof of a correct fix.
68
+
69
+ Source is read directly from immutable Git blobs at the merge-base and head. The
70
+ worktree and index are left untouched, and project code, hooks, diff programs and
71
+ package scripts are never executed. Use `check` for uncommitted working files, and
72
+ `--staged` for the index. Source comparison and policy are pinned to the reported SHAs.
73
+
74
+ Policy comes from the explicitly supplied **base ref tip**; source comparison starts
75
+ at the merge-base. Candidate changes to `.agentroastrc.json`, `.agentroastrc` or
76
+ `agent-roast.config.json` are reported and ignored for this review. Adopt policy
77
+ changes through separate human review. An explicit `--fail-on` overrides blocking
78
+ severity but does not enable rules disabled by base policy.
79
+
80
+ ```bash
81
+ # Advisory pilot: inspect findings before deciding what should block the team
82
+ agent-roast review --base origin/main --fail-on ''
83
+ # Gate the shortcuts the team agrees matter
84
+ agent-roast review --base origin/main --fail-on test-skip,trivial-assert,swallowed-error
85
+ ```
86
+
87
+ Matching uses rule, source snippet and occurrence count within a file pair. Line
88
+ shifts and Git-recognized pure renames do not turn old debt into new debt. A second
89
+ identical occurrence is new. This is not semantic tracking: rewritten snippets or
90
+ language changes can require fresh review, and Git's rename recognition is heuristic.
91
+
92
+ Limits: 1,000 changed files, 2 MiB per source blob, 50 MiB across inspected versions,
93
+ 30 seconds per Git command. Symlinked/binary/oversized source and missing comparison
94
+ objects yield exit 2, never a clean pass. Partial clones and ambiguous multiple
95
+ merge-bases are unsupported; fetch complete local history (`fetch-depth: 0`). The
96
+ report counts excluded and unsupported changes explicitly. Only changed supported
97
+ files are compared; this is not a whole-repository safety certification.
98
+
99
+ SARIF includes rule metadata, relative locations, stable occurrence fingerprints
100
+ and whether the scan completed. Source snippets are omitted from SARIF. JSON and
101
+ Markdown reports contain evidence snippets and should be handled as source code.
102
+ SARIF output is generated offline; uploading it is a separate, optional operation.
103
+ GitHub supports SARIF ingestion, but this release has not been tested with a live
104
+ code-scanning upload. Some findings on unchanged lines (including deletion-only
105
+ regressions) may appear in the report/summary rather than as an inline PR annotation.
106
+ Use a separate SARIF category for introduced-findings review; it does not represent
107
+ a full scan of all outstanding alerts.
108
+
109
+ Install with `npx agent-roast@0.6.0 review --base origin/main`. For local development,
110
+ run `npm run build` and use `node dist/cli.js`.
111
+
51
112
  ### Baseline adoption
52
113
 
53
114
  A baseline explicitly accepts the findings present at adoption time. It does not fix
@@ -238,7 +299,7 @@ Only **added lines** (`+`) in source code files are scanned — deleted lines, f
238
299
  - **Requirements:** Git 2.30+ (for `--end-of-options` argument isolation), Node.js 18+.
239
300
  - **100% Local:** Instant streaming parser runs directly on your machine.
240
301
  - **Zero Telemetry:** No cloud dependencies, no LLM API calls, no network traffic. Verified by static bundle inspection.
241
- - **Hermetic:** Enforces `-c core.quotepath=false -c diff.noprefix=false` to guarantee identical behavior across all environments.
302
+ - **Git isolation:** Forces stable diff paths, disables fsmonitor/external diff execution, and reads review content by object identity.
242
303
 
243
304
  ---
244
305
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import path5 from "path";
4
+ import path6 from "path";
5
5
 
6
6
  // src/git.ts
7
7
  import { spawn, spawnSync } from "child_process";
@@ -754,32 +754,36 @@ function validateConfig(value) {
754
754
  }
755
755
  }
756
756
  }
757
+ function configFromText(text, source) {
758
+ try {
759
+ const parsed = JSON.parse(text);
760
+ validateConfig(parsed);
761
+ const rules = { ...DEFAULT_CONFIG.rules, ...parsed.rules };
762
+ const failOn = parsed.failOn ?? Object.entries(rules).filter(([, level]) => level === "error").map(([rule]) => rule);
763
+ return {
764
+ ...DEFAULT_CONFIG,
765
+ ...parsed,
766
+ failOn,
767
+ ignorePaths: [.../* @__PURE__ */ new Set([...DEFAULT_CONFIG.ignorePaths || [], ...parsed.ignorePaths || []])],
768
+ rules
769
+ };
770
+ } catch (error) {
771
+ throw new ConfigError(`Invalid ${source}: ${error instanceof Error ? error.message : String(error)}`);
772
+ }
773
+ }
757
774
  function loadConfig(cwd = process.cwd()) {
758
775
  for (const filename of CONFIG_FILENAMES) {
759
776
  const filePath = path.join(cwd, filename);
760
- if (!fs.existsSync(filePath)) continue;
761
- try {
762
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
763
- validateConfig(parsed);
764
- const rules = { ...DEFAULT_CONFIG.rules, ...parsed.rules };
765
- const failOn = parsed.failOn ?? Object.entries(rules).filter(([, level]) => level === "error").map(([rule]) => rule);
766
- return {
767
- ...DEFAULT_CONFIG,
768
- ...parsed,
769
- failOn,
770
- ignorePaths: [.../* @__PURE__ */ new Set([...DEFAULT_CONFIG.ignorePaths || [], ...parsed.ignorePaths || []])],
771
- rules
772
- };
773
- } catch (err) {
774
- throw new ConfigError(`Invalid ${filename}: ${err instanceof Error ? err.message : String(err)}`);
777
+ if (fs.existsSync(filePath)) {
778
+ try {
779
+ return configFromText(fs.readFileSync(filePath, "utf8"), filename);
780
+ } catch (error) {
781
+ if (error instanceof ConfigError) throw error;
782
+ throw new ConfigError(`Cannot read ${filename}: ${error instanceof Error ? error.message : String(error)}`);
783
+ }
775
784
  }
776
785
  }
777
- return {
778
- ...DEFAULT_CONFIG,
779
- failOn: [...DEFAULT_CONFIG.failOn || []],
780
- ignorePaths: [...DEFAULT_CONFIG.ignorePaths || []],
781
- rules: { ...DEFAULT_CONFIG.rules }
782
- };
786
+ return configFromText("{}", "default configuration");
783
787
  }
784
788
  function matchIgnorePath(filePath, ignorePatterns) {
785
789
  if (!ignorePatterns || ignorePatterns.length === 0) {
@@ -1623,6 +1627,60 @@ function formatCheckMarkdown(result) {
1623
1627
  for (const skipped of result.skippedFiles) lines.push("", `Not scanned: ${markdownCell(skipped.file)} \u2014 ${markdownCell(skipped.reason)}`);
1624
1628
  return lines.join("\n");
1625
1629
  }
1630
+ function formatReviewTerminal(result) {
1631
+ const status = !result.complete ? "INCOMPLETE" : result.passed ? "PASSED" : "BLOCKED";
1632
+ const lines = [
1633
+ "",
1634
+ pc.bold(`agent-roast \u2014 context review: ${status}`),
1635
+ ` ${result.mergeBaseSha.slice(0, 7)} \u2192 ${result.headSha.slice(0, 7)} \xB7 policy from base ${result.policySha.slice(0, 7)}`,
1636
+ ` ${result.filesInspected} changed files compared \xB7 ${result.linesInspected} source lines inspected across both versions`,
1637
+ ` ${result.infractions.length} new \xB7 ${result.unchangedCount} existing \xB7 ${result.removedFindings.length} no longer detected`
1638
+ ];
1639
+ if (result.policyChanges.length) lines.push(" Candidate policy changes were ignored; adopt them separately after review.");
1640
+ if (result.excludedFiles || result.unsupportedFiles) lines.push(` ${result.excludedFiles} excluded \xB7 ${result.unsupportedFiles} unsupported/non-source changes`);
1641
+ for (const finding of result.infractions.slice(0, 20)) {
1642
+ lines.push(
1643
+ "",
1644
+ ` ${result.failedRules.includes(finding.type) ? "FAIL" : "WARN"} [${finding.type}] ${terminalText(finding.file || "unknown")}:${finding.lineNumber}`,
1645
+ ` ${terminalText(finding.lineSnippet || finding.reason)}`,
1646
+ ` Next: ${finding.suggestion || "Review this occurrence."}`
1647
+ );
1648
+ }
1649
+ if (result.infractions.length > 20) lines.push(` ${result.infractions.length - 20} more new findings; use --format json for all results.`);
1650
+ for (const skipped of result.skippedFiles) lines.push(` NOT COMPARED ${terminalText(skipped.file)}: ${terminalText(skipped.reason)}`);
1651
+ lines.push("", !result.complete ? " INCOMPLETE: some changed source was not compared (exit 2)." : result.passed ? " PASSED: no new blocking findings." : ` BLOCKED: ${result.failedRules.join(", ")}.`, "");
1652
+ if (!result.filesInspected) lines.push(" No supported changed source files compared.", "");
1653
+ return lines.join("\n");
1654
+ }
1655
+ function formatReviewMarkdown(result) {
1656
+ const status = !result.complete ? "INCOMPLETE" : result.passed ? "PASSED" : "BLOCKED";
1657
+ const lines = [
1658
+ `## agent-roast context review: ${status}`,
1659
+ "",
1660
+ `Compared ${result.filesInspected} changed files at \`${result.mergeBaseSha.slice(0, 7)}\` \u2192 \`${result.headSha.slice(0, 7)}\`. Policy: \`${result.policySha.slice(0, 7)}\`.`,
1661
+ "",
1662
+ `**${result.infractions.length} new** \xB7 ${result.unchangedCount} existing \xB7 ${result.removedFindings.length} no longer detected.`,
1663
+ ""
1664
+ ];
1665
+ if (result.policyChanges.length) lines.push("Candidate policy changes were ignored. This review uses the supplied base commit's policy.", "");
1666
+ lines.push("| New finding | Head location | Evidence | Next step |", "| --- | --- | --- | --- |");
1667
+ for (const finding of result.infractions) lines.push(`| ${finding.type} | ${markdownCell(`${finding.file}:${finding.lineNumber}`)} | ${markdownCell(finding.lineSnippet || finding.reason)} | ${markdownCell(finding.suggestion || "Review this occurrence.")} |`);
1668
+ if (result.removedFindings.length) {
1669
+ lines.push(
1670
+ "",
1671
+ "### No longer detected",
1672
+ "",
1673
+ "These patterns disappeared from the compared source. Deleting code is not proof of a behavioral fix.",
1674
+ "",
1675
+ "| Rule | Previous location | Previous evidence |",
1676
+ "| --- | --- | --- |"
1677
+ );
1678
+ for (const finding of result.removedFindings) lines.push(`| ${finding.type} | ${markdownCell(`${finding.file}:${finding.lineNumber}`)} | ${markdownCell(finding.lineSnippet || finding.reason)} |`);
1679
+ }
1680
+ for (const skipped of result.skippedFiles) lines.push("", `Not compared: ${markdownCell(skipped.file)} \u2014 ${markdownCell(skipped.reason)}`);
1681
+ lines.push("", `Scope: changed supported files only; ${result.excludedFiles} excluded and ${result.unsupportedFiles} unsupported/non-source changes.`);
1682
+ return lines.join("\n");
1683
+ }
1626
1684
 
1627
1685
  // src/init.ts
1628
1686
  import * as fs2 from "fs";
@@ -1663,6 +1721,7 @@ jobs:
1663
1721
  base: \${{ github.event.pull_request.base.sha }}
1664
1722
  head: \${{ github.event.pull_request.head.sha }}
1665
1723
  format: github
1724
+ mode: review
1666
1725
  `;
1667
1726
  var PRE_COMMIT_HOOK_TEMPLATE = `#!/bin/sh
1668
1727
  # Run agent-roast pre-commit verification
@@ -1775,11 +1834,8 @@ function generateShieldsJson(score, label = "AI Discipline") {
1775
1834
  };
1776
1835
  }
1777
1836
 
1778
- // src/check.ts
1779
- import { spawnSync as spawnSync2 } from "child_process";
1780
- import * as fs4 from "fs/promises";
1781
- import { constants as constants2 } from "fs";
1782
- import path4 from "path";
1837
+ // src/sarif.ts
1838
+ import { createRequire } from "module";
1783
1839
 
1784
1840
  // src/baseline.ts
1785
1841
  import { createHash, randomUUID } from "crypto";
@@ -1873,7 +1929,78 @@ function writeBaseline(filename, findings) {
1873
1929
  }
1874
1930
  }
1875
1931
 
1932
+ // src/sarif.ts
1933
+ var require2 = createRequire(import.meta.url);
1934
+ var version = require2("../package.json").version;
1935
+ function formatSarif(report) {
1936
+ const occurrences = /* @__PURE__ */ new Map();
1937
+ const results = report.infractions.map((finding) => {
1938
+ const filename = finding.file;
1939
+ if (!filename || filename.startsWith("/") || filename.split("/").includes("..") || /^[a-z]:/i.test(filename)) {
1940
+ throw new ConfigError("SARIF needs repository-relative finding paths.");
1941
+ }
1942
+ const fingerprint = findingFingerprint(finding);
1943
+ const occurrence = (occurrences.get(fingerprint) || 0) + 1;
1944
+ occurrences.set(fingerprint, occurrence);
1945
+ return {
1946
+ ruleId: finding.type,
1947
+ ruleIndex: VALID_DETECTORS.indexOf(finding.type),
1948
+ level: report.failedRules.includes(finding.type) ? "error" : "warning",
1949
+ message: { text: `${finding.reason}. ${finding.suggestion || RULE_GUIDES[finding.type].suggestion}` },
1950
+ locations: [{ physicalLocation: {
1951
+ artifactLocation: { uri: filename.split("/").map((segment) => encodeURIComponent(segment)).join("/") },
1952
+ region: { startLine: Math.max(1, finding.lineNumber || 1) }
1953
+ } }],
1954
+ partialFingerprints: { "agentRoastFinding/v1": `${fingerprint}:${occurrence}` },
1955
+ ...report.mode === "review" ? { baselineState: "new" } : {}
1956
+ };
1957
+ });
1958
+ return JSON.stringify({
1959
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1960
+ version: "2.1.0",
1961
+ runs: [{
1962
+ tool: { driver: {
1963
+ name: "agent-roast",
1964
+ semanticVersion: version,
1965
+ informationUri: "https://github.com/Nari-Ab/agent-roast",
1966
+ rules: VALID_DETECTORS.map((rule) => ({
1967
+ id: rule,
1968
+ name: rule,
1969
+ shortDescription: { text: RULE_GUIDES[rule].title },
1970
+ fullDescription: { text: RULE_GUIDES[rule].why },
1971
+ help: { text: `${RULE_GUIDES[rule].suggestion}
1972
+ Example: ${RULE_GUIDES[rule].example}` },
1973
+ defaultConfiguration: { level: "warning" },
1974
+ properties: { tags: ["maintainability", "code-review"] }
1975
+ }))
1976
+ } },
1977
+ invocations: [{
1978
+ executionSuccessful: report.complete,
1979
+ exitCode: report.exitCode,
1980
+ toolExecutionNotifications: report.skippedFiles.map((skipped) => ({
1981
+ level: "error",
1982
+ message: { text: `Not inspected: ${skipped.file}. ${skipped.reason}` }
1983
+ }))
1984
+ }],
1985
+ results,
1986
+ properties: report.mode === "review" ? {
1987
+ scope: "introduced findings in changed files",
1988
+ baseSha: report.baseSha,
1989
+ headSha: report.headSha,
1990
+ mergeBaseSha: report.mergeBaseSha,
1991
+ policySha: report.policySha,
1992
+ unchangedFindings: report.unchangedCount,
1993
+ removedFindings: report.removedFindings.length
1994
+ } : { scope: "current source", acceptedBaselineFindings: report.baselineCount }
1995
+ }]
1996
+ }, null, 2);
1997
+ }
1998
+
1876
1999
  // src/check.ts
2000
+ import { spawnSync as spawnSync2 } from "child_process";
2001
+ import * as fs4 from "fs/promises";
2002
+ import { constants as constants2 } from "fs";
2003
+ import path4 from "path";
1877
2004
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
1878
2005
  var MAX_SCAN_BYTES = 50 * 1024 * 1024;
1879
2006
  var GENERATED_PATHS = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.min.js", "**/*.map"];
@@ -1939,7 +2066,7 @@ async function checkProject(options = {}) {
1939
2066
  const failOn = options.failOn ?? config.failOn ?? ["test-skip", "trivial-assert"];
1940
2067
  for (const rule of failOn) if (!VALID_DETECTORS.includes(rule)) throw new ConfigError(`Unknown rule '${rule}'.`);
1941
2068
  const format = options.format || "terminal";
1942
- if (!["terminal", "json", "github", "markdown"].includes(format)) throw new ConfigError("Unsupported check output format.");
2069
+ if (!["terminal", "json", "github", "markdown", "sarif"].includes(format)) throw new ConfigError("Unsupported check output format.");
1943
2070
  const baseline = options.baseline ? loadBaseline(path4.resolve(cwd, options.baseline)) : void 0;
1944
2071
  const filenames = [...new Set(git(root, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]).split("\0").filter(Boolean))].sort();
1945
2072
  const infractions = [];
@@ -1992,6 +2119,7 @@ async function checkProject(options = {}) {
1992
2119
  output: ""
1993
2120
  };
1994
2121
  if (format === "json") result.output = JSON.stringify(result, null, 2);
2122
+ else if (format === "sarif") result.output = formatSarif(result);
1995
2123
  else if (format === "markdown") result.output = formatCheckMarkdown(result);
1996
2124
  else if (format === "github") {
1997
2125
  result.output = formatGithubAnnotations(result) + (complete ? "" : "\n::error::Project scan incomplete. See the JSON or terminal report for skipped files.");
@@ -1999,6 +2127,246 @@ async function checkProject(options = {}) {
1999
2127
  return result;
2000
2128
  }
2001
2129
 
2130
+ // src/review.ts
2131
+ import path5 from "path";
2132
+
2133
+ // src/snapshot.ts
2134
+ import { spawn as spawn2 } from "child_process";
2135
+ function assertNotAborted(signal) {
2136
+ if (signal?.aborted) throw new ConfigError("Review cancelled; no complete result was produced.");
2137
+ }
2138
+ async function readGit(cwd, args, signal, limit = 8 * 1024 * 1024) {
2139
+ assertNotAborted(signal);
2140
+ const env = getHermeticGitEnv({ GIT_NO_REPLACE_OBJECTS: "1", GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0" });
2141
+ for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_NAMESPACE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES"]) delete env[key];
2142
+ return new Promise((resolve2, reject) => {
2143
+ const child = spawn2("git", [...HERMETIC_GIT_FLAGS, ...args], {
2144
+ cwd,
2145
+ env,
2146
+ stdio: ["ignore", "pipe", "pipe"],
2147
+ windowsHide: true
2148
+ });
2149
+ const chunks = [];
2150
+ let bytes = 0;
2151
+ let stderr = "";
2152
+ let settled = false;
2153
+ const finish = (error) => {
2154
+ if (settled) return;
2155
+ settled = true;
2156
+ clearTimeout(timer);
2157
+ signal?.removeEventListener("abort", abort);
2158
+ if (error) {
2159
+ child.kill();
2160
+ reject(error);
2161
+ } else resolve2(Buffer.concat(chunks));
2162
+ };
2163
+ const abort = () => finish(new ConfigError("Review cancelled; no complete result was produced."));
2164
+ const timer = setTimeout(() => finish(new ConfigError("Git review read timed out after 30 seconds.")), 3e4);
2165
+ signal?.addEventListener("abort", abort, { once: true });
2166
+ child.stdout.on("data", (chunk) => {
2167
+ if (settled) return;
2168
+ bytes += chunk.length;
2169
+ if (bytes > limit) finish(new ConfigError(`Git read exceeds the ${limit} byte limit.`));
2170
+ else chunks.push(chunk);
2171
+ });
2172
+ child.stderr.on("data", (chunk) => {
2173
+ if (stderr.length < 64 * 1024) stderr += chunk.toString("utf8").slice(0, 64 * 1024 - stderr.length);
2174
+ });
2175
+ child.on("error", (error) => finish(error));
2176
+ child.on("close", (code) => finish(code === 0 ? void 0 : new GitError(stderr, code)));
2177
+ if (signal?.aborted) abort();
2178
+ });
2179
+ }
2180
+ async function reviewRefs(cwd, base, head, signal) {
2181
+ const resolve2 = async (ref) => {
2182
+ if (!ref.trim()) throw new ConfigError("Review refs must not be empty.");
2183
+ try {
2184
+ return (await readGit(cwd, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], signal, 1024)).toString().trim();
2185
+ } catch (error) {
2186
+ assertNotAborted(signal);
2187
+ throw new ConfigError(`Cannot resolve review ref '${ref}'. Use valid commits and fetch complete history (actions/checkout fetch-depth: 0). ${error instanceof Error ? error.message : String(error)}`);
2188
+ }
2189
+ };
2190
+ try {
2191
+ const promises = (await readGit(cwd, ["config", "--type=bool", "--get-regexp", "^remote\\..*\\.promisor$"], signal, 64 * 1024)).toString();
2192
+ if (promises.split("\n").some((line) => /\s+(?:true|yes|on|1)\s*$/i.test(line))) {
2193
+ throw new ConfigError("Offline review requires a non-partial clone with local objects; promisor remotes are unsupported.");
2194
+ }
2195
+ } catch (error) {
2196
+ if (!(error instanceof GitError && error.code === 1)) throw error;
2197
+ }
2198
+ const baseSha = await resolve2(base);
2199
+ const headSha = await resolve2(head);
2200
+ let ancestors;
2201
+ try {
2202
+ ancestors = (await readGit(cwd, ["merge-base", "--all", "--end-of-options", baseSha, headSha], signal, 8192)).toString().trim().split("\n");
2203
+ } catch (error) {
2204
+ assertNotAborted(signal);
2205
+ throw new ConfigError(`Cannot find review merge-base. Fetch full shared history (fetch-depth: 0). ${error instanceof Error ? error.message : String(error)}`);
2206
+ }
2207
+ if (ancestors.length !== 1 || !/^[a-f0-9]{40,64}$/.test(ancestors[0])) throw new ConfigError("Review needs exactly one merge base; ambiguous merge ancestry is unsupported.");
2208
+ return { baseSha, headSha, mergeBaseSha: ancestors[0] };
2209
+ }
2210
+ async function changedTreeFiles(cwd, before, after, signal) {
2211
+ const buffer = await readGit(cwd, ["diff", "--raw", "-z", "--no-relative", "--no-abbrev", "--no-ext-diff", "--no-textconv", "--find-renames=50%", "-l1000", "--end-of-options", before, after, "--"], signal);
2212
+ const tokens = new TextDecoder("utf-8", { fatal: true }).decode(buffer).split("\0");
2213
+ if (tokens.pop() !== "") throw new ConfigError("Truncated Git change inventory.");
2214
+ const changes = [];
2215
+ for (let i = 0; i < tokens.length; ) {
2216
+ const match = /^:(\d{6}) (\d{6}) ([a-f0-9]{40,64}) ([a-f0-9]{40,64}) ([AMDRT]\d*)$/.exec(tokens[i++]);
2217
+ if (!match) throw new ConfigError("Unsupported Git change inventory entry.");
2218
+ const oldPath = tokens[i++];
2219
+ const newPath = match[5].startsWith("R") ? tokens[i++] : oldPath;
2220
+ if (!oldPath || !newPath) throw new ConfigError("Incomplete Git path inventory.");
2221
+ changes.push({ oldMode: match[1], newMode: match[2], oldOid: match[3], newOid: match[4], status: match[5], oldPath, newPath });
2222
+ if (changes.length > 1e3) throw new ConfigError("Review exceeds the 1000 changed-file limit; split the change into smaller reviews.");
2223
+ }
2224
+ return changes;
2225
+ }
2226
+ async function readBlob(cwd, oid, signal, limit = 2 * 1024 * 1024) {
2227
+ if (!/^[a-f0-9]{40,64}$/.test(oid)) throw new ConfigError("Invalid Git blob identity.");
2228
+ const sizeText = (await readGit(cwd, ["cat-file", "-s", oid], signal, 128)).toString().trim();
2229
+ const size = Number(sizeText);
2230
+ if (!Number.isSafeInteger(size) || size < 0 || size > limit) throw new ConfigError(`Source blob exceeds the ${limit} byte limit or has an invalid size.`);
2231
+ const buffer = await readGit(cwd, ["cat-file", "blob", oid], signal, limit);
2232
+ if (buffer.length !== size) throw new ConfigError("Git blob size differs from the recorded object.");
2233
+ return buffer;
2234
+ }
2235
+ async function reviewPolicy(cwd, baseSha, signal) {
2236
+ const raw = await readGit(cwd, ["ls-tree", "--full-tree", "-z", baseSha, "--", ...CONFIG_FILENAMES], signal, 8192);
2237
+ const entries = raw.toString().split("\0").filter(Boolean);
2238
+ for (const filename of CONFIG_FILENAMES) {
2239
+ const entry = entries.find((line) => line.slice(line.indexOf(" ") + 1) === filename);
2240
+ if (!entry) continue;
2241
+ const match = /^(100644|100755) blob ([a-f0-9]{40,64})\t/.exec(entry);
2242
+ if (!match) throw new ConfigError(`Base policy '${filename}' must be a regular Git blob.`);
2243
+ const content = await readBlob(cwd, match[2], signal, 64 * 1024);
2244
+ return { config: configFromText(new TextDecoder("utf-8", { fatal: true }).decode(content), `${filename} in base ${baseSha.slice(0, 7)}`), filename };
2245
+ }
2246
+ return { config: configFromText("{}", "default review policy"), filename: null };
2247
+ }
2248
+
2249
+ // src/review.ts
2250
+ var GENERATED_PATHS2 = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.min.js", "**/*.map"];
2251
+ var MAX_SCAN_BYTES2 = 50 * 1024 * 1024;
2252
+ function compareFindings(before, after, newPath) {
2253
+ const old = /* @__PURE__ */ new Map();
2254
+ for (const finding of before) {
2255
+ const key = findingFingerprint({ ...finding, file: newPath });
2256
+ const bucket = old.get(key) || { findings: [], matched: 0 };
2257
+ bucket.findings.push(finding);
2258
+ old.set(key, bucket);
2259
+ }
2260
+ const introduced = [];
2261
+ let unchanged = 0;
2262
+ for (const finding of after) {
2263
+ const bucket = old.get(findingFingerprint(finding));
2264
+ if (bucket && bucket.matched < bucket.findings.length) {
2265
+ bucket.matched++;
2266
+ unchanged++;
2267
+ } else introduced.push(finding);
2268
+ }
2269
+ return { introduced, removed: [...old.values()].flatMap((bucket) => bucket.findings.slice(bucket.matched)), unchanged };
2270
+ }
2271
+ async function reviewChanges(options) {
2272
+ assertNotAborted(options.signal);
2273
+ const cwd = path5.resolve(options.cwd || process.cwd());
2274
+ const refs = await reviewRefs(cwd, options.base, options.head ?? "HEAD", options.signal);
2275
+ const policy = await reviewPolicy(cwd, refs.baseSha, options.signal);
2276
+ const format = options.format ?? "terminal";
2277
+ if (!["terminal", "json", "github", "markdown", "sarif"].includes(format)) throw new ConfigError("Unsupported review format.");
2278
+ const failOn = options.failOn ?? policy.config.failOn ?? ["test-skip", "trivial-assert"];
2279
+ for (const rule of failOn) if (!VALID_DETECTORS.includes(rule)) throw new ConfigError(`Unknown review rule '${rule}'.`);
2280
+ const changes = await changedTreeFiles(cwd, refs.mergeBaseSha, refs.headSha, options.signal);
2281
+ const policyChanges = changes.filter((change) => CONFIG_FILENAMES.includes(change.oldPath) || CONFIG_FILENAMES.includes(change.newPath)).map((change) => change.newPath);
2282
+ const ignores = [...GENERATED_PATHS2, ...policy.config.ignorePaths || []];
2283
+ const result = {
2284
+ schemaVersion: "1.0.0",
2285
+ mode: "review",
2286
+ ...refs,
2287
+ policySha: refs.baseSha,
2288
+ policyFile: policy.filename,
2289
+ policyChanges,
2290
+ filesChanged: changes.length,
2291
+ filesInspected: 0,
2292
+ unsupportedFiles: 0,
2293
+ excludedFiles: 0,
2294
+ renamedFiles: changes.filter((change) => change.status.startsWith("R")).length,
2295
+ linesInspected: 0,
2296
+ beforeFindings: 0,
2297
+ afterFindings: 0,
2298
+ unchangedCount: 0,
2299
+ infractions: [],
2300
+ removedFindings: [],
2301
+ skippedFiles: [],
2302
+ complete: true,
2303
+ failedRules: [],
2304
+ passed: false,
2305
+ exitCode: 2,
2306
+ output: ""
2307
+ };
2308
+ let totalBytes = 0;
2309
+ for (const change of changes) {
2310
+ assertNotAborted(options.signal);
2311
+ const oldSupported = change.oldMode !== "000000" && isCodeFile(change.oldPath);
2312
+ const newSupported = change.newMode !== "000000" && isCodeFile(change.newPath);
2313
+ if (!oldSupported && !newSupported) {
2314
+ result.unsupportedFiles++;
2315
+ continue;
2316
+ }
2317
+ const oldInScope = oldSupported && isCodeFile(change.oldPath, ignores);
2318
+ const newInScope = newSupported && isCodeFile(change.newPath, ignores);
2319
+ if (!oldInScope && !newInScope) {
2320
+ result.excludedFiles++;
2321
+ continue;
2322
+ }
2323
+ try {
2324
+ let inspectedLines = 0;
2325
+ const scan = async (mode, oid, filename, commit) => {
2326
+ if (mode === "000000") return [];
2327
+ if (!["100644", "100755"].includes(mode)) throw new ConfigError("Changed source is a symlink, submodule or unsupported Git mode; comparison is incomplete.");
2328
+ const content = await readBlob(cwd, oid, options.signal);
2329
+ totalBytes += content.length;
2330
+ if (totalBytes > MAX_SCAN_BYTES2) throw new ConfigError("Review exceeds the 50 MiB source comparison limit.");
2331
+ if (content.includes(0)) throw new ConfigError("Binary content in a source comparison.");
2332
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(content);
2333
+ const lines = text.split(/\r?\n/);
2334
+ if (lines.at(-1) === "") lines.pop();
2335
+ inspectedLines += lines.length;
2336
+ return auditFileDiffs([{ path: filename, addedLines: lines.map((line, index) => ({ line, lineNumber: index + 1 })) }], commit, "", { rules: policy.config.rules }).infractions;
2337
+ };
2338
+ if (oldSupported && !newSupported && change.newMode !== "000000") {
2339
+ throw new ConfigError("Source moved to an unsupported extension; cannot claim its findings were resolved.");
2340
+ }
2341
+ const before = await scan(change.oldMode, change.oldOid, change.oldPath, refs.mergeBaseSha);
2342
+ const after = await scan(change.newMode, change.newOid, change.newPath, refs.headSha);
2343
+ const compared = compareFindings(before, after, change.newPath);
2344
+ result.infractions.push(...compared.introduced);
2345
+ result.removedFindings.push(...compared.removed);
2346
+ result.unchangedCount += compared.unchanged;
2347
+ result.beforeFindings += before.length;
2348
+ result.afterFindings += after.length;
2349
+ result.filesInspected++;
2350
+ result.linesInspected += inspectedLines;
2351
+ } catch (error) {
2352
+ assertNotAborted(options.signal);
2353
+ result.skippedFiles.push({ file: change.newPath, reason: error instanceof Error ? error.message : String(error) });
2354
+ if (totalBytes > MAX_SCAN_BYTES2) break;
2355
+ }
2356
+ }
2357
+ assertNotAborted(options.signal);
2358
+ result.complete = result.skippedFiles.length === 0;
2359
+ result.failedRules = [...new Set(result.infractions.filter((finding) => failOn.includes(finding.type)).map((finding) => finding.type))];
2360
+ result.passed = result.complete && result.failedRules.length === 0;
2361
+ result.exitCode = !result.complete ? 2 : result.passed ? 0 : 1;
2362
+ if (format === "json") result.output = JSON.stringify(result, null, 2);
2363
+ else if (format === "markdown") result.output = formatReviewMarkdown(result);
2364
+ else if (format === "sarif") result.output = formatSarif(result);
2365
+ else if (format === "github") result.output = formatGithubAnnotations(result) + (!result.complete ? "\n::error::Context review incomplete. Inspect skipped source files in the JSON report." : "") + (policyChanges.length ? "\n::notice::Candidate policy changes were ignored; review uses policy from the supplied base commit." : "");
2366
+ else result.output = formatReviewTerminal(result);
2367
+ return result;
2368
+ }
2369
+
2002
2370
  // src/index.ts
2003
2371
  var InvalidDetectorError = class extends Error {
2004
2372
  constructor(message) {
@@ -2007,9 +2375,9 @@ var InvalidDetectorError = class extends Error {
2007
2375
  }
2008
2376
  };
2009
2377
  async function roastRepository(options = {}) {
2010
- const cwd = path5.resolve(options.cwd || process.cwd());
2378
+ const cwd = path6.resolve(options.cwd || process.cwd());
2011
2379
  const since = options.since || "90 days ago";
2012
- const repoName = path5.basename(cwd);
2380
+ const repoName = path6.basename(cwd);
2013
2381
  const collector = new MetricsCollector({ all: options.all, ignorePaths: options.ignorePaths, rules: options.rules, panicLoopWindowMinutes: options.panicLoopWindowMinutes });
2014
2382
  const gitStream = streamGitLog(cwd, since);
2015
2383
  await parseGitLogStream(gitStream, (commit, stats) => {
@@ -2024,7 +2392,7 @@ async function roastRepository(options = {}) {
2024
2392
  return { summary, output };
2025
2393
  }
2026
2394
  async function auditPullRequest(options) {
2027
- const cwd = path5.resolve(options.cwd || process.cwd());
2395
+ const cwd = path6.resolve(options.cwd || process.cwd());
2028
2396
  const format = options.format || "terminal";
2029
2397
  const failOn = (options.failOn || ["test-skip"]).map((s) => s.trim()).filter(Boolean);
2030
2398
  for (const det of failOn) {
@@ -2109,7 +2477,9 @@ export {
2109
2477
  RULE_GUIDES,
2110
2478
  VALID_DETECTORS,
2111
2479
  DEFAULT_CONFIG,
2480
+ CONFIG_FILENAMES,
2112
2481
  ConfigError,
2482
+ configFromText,
2113
2483
  loadConfig,
2114
2484
  matchIgnorePath,
2115
2485
  isCodeFile,
@@ -2125,6 +2495,8 @@ export {
2125
2495
  formatPrMarkdownComment,
2126
2496
  formatCheckTerminal,
2127
2497
  formatCheckMarkdown,
2498
+ formatReviewTerminal,
2499
+ formatReviewMarkdown,
2128
2500
  DEFAULT_CONFIG_TEMPLATE,
2129
2501
  GITHUB_WORKFLOW_TEMPLATE,
2130
2502
  PRE_COMMIT_HOOK_TEMPLATE,
@@ -2138,7 +2510,10 @@ export {
2138
2510
  loadBaseline,
2139
2511
  applyBaseline,
2140
2512
  writeBaseline,
2513
+ formatSarif,
2141
2514
  checkProject,
2515
+ compareFindings,
2516
+ reviewChanges,
2142
2517
  InvalidDetectorError,
2143
2518
  roastRepository,
2144
2519
  auditPullRequest
package/dist/cli.js CHANGED
@@ -9,14 +9,16 @@ import {
9
9
  auditPullRequest,
10
10
  checkProject,
11
11
  formatGithubStepSummary,
12
+ formatReviewMarkdown,
12
13
  generateShieldsJson,
13
14
  generateShieldsMarkdown,
14
15
  generateSvgBadge,
15
16
  initProject,
16
17
  loadConfig,
18
+ reviewChanges,
17
19
  roastRepository,
18
20
  writeBaseline
19
- } from "./chunk-FUAUIURY.js";
21
+ } from "./chunk-RU5OYHLR.js";
20
22
 
21
23
  // src/cli.ts
22
24
  import { Command } from "commander";
@@ -26,7 +28,7 @@ import * as path from "path";
26
28
  var require2 = createRequire(import.meta.url);
27
29
  var pkg = require2("../package.json");
28
30
  var program = new Command();
29
- 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", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
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) => {
30
32
  try {
31
33
  if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
32
34
  console.warn(
@@ -120,7 +122,49 @@ has-supported-changes=${prResult.hasSupportedChanges}
120
122
  }
121
123
  }
122
124
  });
123
- 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, --json, --fail-on <rules>\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) => {
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) => {
126
+ try {
127
+ 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.");
131
+ }
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);
161
+ }
162
+ } catch (error) {
163
+ console.error(`agent-roast review error: ${error instanceof Error ? error.message : String(error)}`);
164
+ process.exitCode = 2;
165
+ }
166
+ });
167
+ 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>\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) => {
124
168
  try {
125
169
  const global = program.opts();
126
170
  if (global.base || global.staged || global.failUnder !== void 0 || global.all || program.getOptionValueSource("head") === "cli" || program.getOptionValueSource("since") === "cli") {
package/dist/index.d.ts CHANGED
@@ -86,7 +86,7 @@ interface PrAuditResult {
86
86
  interface CheckOptions {
87
87
  cwd?: string;
88
88
  failOn?: string[];
89
- format?: "terminal" | "json" | "github" | "markdown";
89
+ format?: ReviewFormat;
90
90
  baseline?: string;
91
91
  signal?: AbortSignal;
92
92
  }
@@ -109,6 +109,45 @@ interface CheckResult {
109
109
  exitCode: number;
110
110
  output: string;
111
111
  }
112
+ type ReviewFormat = "terminal" | "json" | "github" | "markdown" | "sarif";
113
+ interface ReviewOptions {
114
+ cwd?: string;
115
+ base: string;
116
+ head?: string;
117
+ failOn?: string[];
118
+ format?: ReviewFormat;
119
+ signal?: AbortSignal;
120
+ }
121
+ interface ReviewResult {
122
+ schemaVersion: string;
123
+ mode: "review";
124
+ baseSha: string;
125
+ headSha: string;
126
+ mergeBaseSha: string;
127
+ policySha: string;
128
+ policyFile: string | null;
129
+ policyChanges: string[];
130
+ filesChanged: number;
131
+ filesInspected: number;
132
+ unsupportedFiles: number;
133
+ excludedFiles: number;
134
+ renamedFiles: number;
135
+ linesInspected: number;
136
+ beforeFindings: number;
137
+ afterFindings: number;
138
+ unchangedCount: number;
139
+ infractions: Infraction[];
140
+ removedFindings: Infraction[];
141
+ skippedFiles: {
142
+ file: string;
143
+ reason: string;
144
+ }[];
145
+ complete: boolean;
146
+ failedRules: string[];
147
+ passed: boolean;
148
+ exitCode: number;
149
+ output: string;
150
+ }
112
151
 
113
152
  interface AiAttributionResult {
114
153
  isAiAttributed: boolean;
@@ -246,6 +285,8 @@ declare function formatPrMarkdownComment(result: PrAuditResult): string;
246
285
  /** Current-tree report emphasizes next actions and explicitly accounts for baseline debt. */
247
286
  declare function formatCheckTerminal(result: CheckResult): string;
248
287
  declare function formatCheckMarkdown(result: CheckResult): string;
288
+ declare function formatReviewTerminal(result: ReviewResult): string;
289
+ declare function formatReviewMarkdown(result: ReviewResult): string;
249
290
 
250
291
  interface AgentRoastConfig {
251
292
  /** Detector rules to fail CI on in PR mode */
@@ -260,9 +301,12 @@ interface AgentRoastConfig {
260
301
  panicLoopWindowMinutes?: number;
261
302
  }
262
303
  declare const DEFAULT_CONFIG: AgentRoastConfig;
304
+ declare const CONFIG_FILENAMES: string[];
263
305
  declare class ConfigError extends Error {
264
306
  constructor(message: string);
265
307
  }
308
+ /** Parse one explicit policy source; shared by worktree and trusted Git reviews. */
309
+ declare function configFromText(text: string, source: string): AgentRoastConfig;
266
310
  /** Invalid policy must stop the audit instead of silently changing CI behavior. */
267
311
  declare function loadConfig(cwd?: string): AgentRoastConfig;
268
312
  /**
@@ -287,7 +331,7 @@ declare const DEFAULT_CONFIG_TEMPLATE: {
287
331
  };
288
332
  scoreThreshold: number;
289
333
  };
290
- declare const GITHUB_WORKFLOW_TEMPLATE = "name: Agent Roast\non:\n pull_request:\n branches: [main, master]\n\npermissions:\n contents: read\n\njobs:\n audit:\n name: Audit AI Shortcuts\n runs-on: ubuntu-latest\n steps:\n - name: Checkout repository\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Run agent-roast PR check\n if: github.event_name == 'pull_request'\n uses: Nari-Ab/agent-roast@v1\n with:\n base: ${{ github.event.pull_request.base.sha }}\n head: ${{ github.event.pull_request.head.sha }}\n format: github\n";
334
+ declare const GITHUB_WORKFLOW_TEMPLATE = "name: Agent Roast\non:\n pull_request:\n branches: [main, master]\n\npermissions:\n contents: read\n\njobs:\n audit:\n name: Audit AI Shortcuts\n runs-on: ubuntu-latest\n steps:\n - name: Checkout repository\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Run agent-roast PR check\n if: github.event_name == 'pull_request'\n uses: Nari-Ab/agent-roast@v1\n with:\n base: ${{ github.event.pull_request.base.sha }}\n head: ${{ github.event.pull_request.head.sha }}\n format: github\n mode: review\n";
291
335
  declare const PRE_COMMIT_HOOK_TEMPLATE = "#!/bin/sh\n# Run agent-roast pre-commit verification\nnpx --no-install agent-roast --staged\n";
292
336
  declare function initProject(options?: InitOptions): Promise<InitResult>;
293
337
 
@@ -308,6 +352,20 @@ declare function generateShieldsJson(score: number | null, label?: string): Shie
308
352
  /** Read current tracked and non-ignored untracked source; never run project code. */
309
353
  declare function checkProject(options?: CheckOptions): Promise<CheckResult>;
310
354
 
355
+ /** Match only within the same changed-file pair; Git renames remap old paths.
356
+ * Counts matter: an identical second shortcut is still a new finding. */
357
+ declare function compareFindings(before: Infraction[], after: Infraction[], newPath: string): {
358
+ introduced: Infraction[];
359
+ removed: Infraction[];
360
+ unchanged: number;
361
+ };
362
+ /** Compare full source blobs in changed files without executing or checking out the candidate. */
363
+ declare function reviewChanges(options: ReviewOptions): Promise<ReviewResult>;
364
+
365
+ /** Offline SARIF 2.1.0, with relative URIs and explicit incomplete-run status.
366
+ * Snippets are omitted to avoid embedding source code in integration artifacts. */
367
+ declare function formatSarif(report: CheckResult | ReviewResult): string;
368
+
311
369
  interface FindingBaseline {
312
370
  schemaVersion: "1.0.0";
313
371
  fingerprintVersion: 1;
@@ -346,4 +404,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
346
404
  }>;
347
405
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
348
406
 
349
- export { type AgentRoastConfig, type AiAttributionResult, 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 RoastOptions, type RuleGuide, type ShieldsEndpointResponse, VALID_DETECTORS, applyBaseline, auditFileDiffs, auditPullRequest, checkProject, createBaseline, detectAiAttribution, findMergeBase, findingFingerprint, formatCheckMarkdown, formatCheckTerminal, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, initProject, isCodeFile, isTestFile, loadBaseline, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath, writeBaseline };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CONFIG_FILENAMES,
3
4
  ConfigError,
4
5
  DEFAULT_CONFIG,
5
6
  DEFAULT_CONFIG_TEMPLATE,
@@ -18,6 +19,8 @@ import {
18
19
  auditFileDiffs,
19
20
  auditPullRequest,
20
21
  checkProject,
22
+ compareFindings,
23
+ configFromText,
21
24
  createBaseline,
22
25
  detectAiAttribution,
23
26
  findMergeBase,
@@ -28,6 +31,9 @@ import {
28
31
  formatGithubStepSummary,
29
32
  formatPrMarkdownComment,
30
33
  formatPrTerminalCard,
34
+ formatReviewMarkdown,
35
+ formatReviewTerminal,
36
+ formatSarif,
31
37
  formatTerminalCard,
32
38
  generateShareUrl,
33
39
  generateShieldsJson,
@@ -44,14 +50,16 @@ import {
44
50
  parseDiff,
45
51
  parseGitLogStream,
46
52
  resolveCommitSha,
53
+ reviewChanges,
47
54
  roastRepository,
48
55
  streamGitDiff,
49
56
  streamGitLog,
50
57
  stripStringAndRegexLiterals,
51
58
  unquoteGitPath,
52
59
  writeBaseline
53
- } from "./chunk-FUAUIURY.js";
60
+ } from "./chunk-RU5OYHLR.js";
54
61
  export {
62
+ CONFIG_FILENAMES,
55
63
  ConfigError,
56
64
  DEFAULT_CONFIG,
57
65
  DEFAULT_CONFIG_TEMPLATE,
@@ -70,6 +78,8 @@ export {
70
78
  auditFileDiffs,
71
79
  auditPullRequest,
72
80
  checkProject,
81
+ compareFindings,
82
+ configFromText,
73
83
  createBaseline,
74
84
  detectAiAttribution,
75
85
  findMergeBase,
@@ -80,6 +90,9 @@ export {
80
90
  formatGithubStepSummary,
81
91
  formatPrMarkdownComment,
82
92
  formatPrTerminalCard,
93
+ formatReviewMarkdown,
94
+ formatReviewTerminal,
95
+ formatSarif,
83
96
  formatTerminalCard,
84
97
  generateShareUrl,
85
98
  generateShieldsJson,
@@ -96,6 +109,7 @@ export {
96
109
  parseDiff,
97
110
  parseGitLogStream,
98
111
  resolveCommitSha,
112
+ reviewChanges,
99
113
  roastRepository,
100
114
  streamGitDiff,
101
115
  streamGitLog,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",