agent-roast 0.3.0 → 0.4.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
@@ -35,6 +35,12 @@ npx agent-roast --fail-under 70 # exit code 1 if score is below 70
35
35
  # Audit pull request diffs (CI gate)
36
36
  npx agent-roast --base origin/main --head HEAD
37
37
  npx agent-roast --base origin/main --head HEAD --fail-on test-skip,trivial-assert,stub-abandonment --format github
38
+ npx agent-roast --base origin/main --format markdown # rich markdown for PR comments
39
+
40
+ # Generate AI discipline badge for your README
41
+ npx agent-roast badge # prints Shields.io Markdown snippet
42
+ npx agent-roast badge --out ./badge.svg # exports standalone SVG badge
43
+ npx agent-roast badge --json # Shields.io dynamic endpoint payload
38
44
  ```
39
45
 
40
46
  What a report looks like (a real repository audit, trimmed):
@@ -77,7 +83,7 @@ Configure custom ignore paths, rule severities, and CI thresholds with a `.agent
77
83
 
78
84
  ```json
79
85
  {
80
- "$schema": "https://raw.githubusercontent.com/sidzuya/agent-roast/main/schema.json",
86
+ "$schema": "https://raw.githubusercontent.com/Nari-Ab/agent-roast/main/schema.json",
81
87
  "ignorePaths": [
82
88
  "**/*.min.js",
83
89
  "**/vendor/**",
@@ -1397,6 +1397,49 @@ function formatGithubStepSummary(result) {
1397
1397
  }
1398
1398
  return lines.join("\n");
1399
1399
  }
1400
+ function formatPrMarkdownComment(result) {
1401
+ const lines = [];
1402
+ const titleEmoji = result.passed ? "\u2705" : "\u274C";
1403
+ const titleStatus = result.passed ? "PASSED" : "BLOCKED";
1404
+ lines.push(`## \u{1F6E1}\uFE0F agent-roast audit: ${titleStatus} ${titleEmoji}`);
1405
+ lines.push("");
1406
+ if (!result.hasSupportedChanges) {
1407
+ lines.push("No supported source file changes detected in this pull request.");
1408
+ lines.push("");
1409
+ lines.push("---");
1410
+ lines.push("*Audited 100% offline with [agent-roast](https://github.com/Nari-Ab/agent-roast)*");
1411
+ return lines.join("\n");
1412
+ }
1413
+ const statusText = result.passed ? "\u2705 Passed" : `\u274C Blocked on ${result.failedRules.join(", ")}`;
1414
+ lines.push("| Status | Defect Density | Files Inspected | Lines Added | Infractions |");
1415
+ lines.push("| :--- | :--- | :--- | :--- | :--- |");
1416
+ lines.push(
1417
+ `| ${statusText} | ${result.defectDensityPer100Lines} / 100 LOC | ${result.filesInspected} | +${result.linesAdded} | ${result.infractions.length} |`
1418
+ );
1419
+ lines.push("");
1420
+ if (result.infractions.length > 0) {
1421
+ lines.push("<details>");
1422
+ lines.push(`<summary><b>View Detected Infractions (${result.infractions.length})</b></summary>`);
1423
+ lines.push("");
1424
+ lines.push("| Type | File:Line | Description | Snippet |");
1425
+ lines.push("| :--- | :--- | :--- | :--- |");
1426
+ const sorted = sortInfractionsDeterministic(result.infractions);
1427
+ for (const inf of sorted) {
1428
+ const loc = inf.file ? `\`${inf.file}:${inf.lineNumber || 1}\`` : "*history*";
1429
+ const snippet = inf.lineSnippet ? `\`${inf.lineSnippet.replace(/\|/g, "\\|")}\`` : "-";
1430
+ lines.push(`| \`${inf.type}\` | ${loc} | ${inf.reason} | ${snippet} |`);
1431
+ }
1432
+ lines.push("");
1433
+ lines.push("</details>");
1434
+ lines.push("");
1435
+ } else {
1436
+ lines.push("\u2728 **Clean pull request**: Zero AI shortcuts, bypassed tests, or compiler type escapes detected.");
1437
+ lines.push("");
1438
+ }
1439
+ lines.push("---");
1440
+ lines.push("*Audited 100% offline with [agent-roast](https://github.com/Nari-Ab/agent-roast)*");
1441
+ return lines.join("\n");
1442
+ }
1400
1443
 
1401
1444
  // src/types.ts
1402
1445
  var VALID_DETECTORS = [
@@ -1412,7 +1455,7 @@ var VALID_DETECTORS = [
1412
1455
  import * as fs2 from "fs";
1413
1456
  import * as path2 from "path";
1414
1457
  var DEFAULT_CONFIG_TEMPLATE = {
1415
- $schema: "https://raw.githubusercontent.com/sidzuya/agent-roast/main/schema.json",
1458
+ $schema: "https://raw.githubusercontent.com/Nari-Ab/agent-roast/main/schema.json",
1416
1459
  ignorePaths: [
1417
1460
  "**/*.min.js",
1418
1461
  "**/vendor/**",
@@ -1512,6 +1555,64 @@ async function initProject(options = {}) {
1512
1555
  return { created, skipped };
1513
1556
  }
1514
1557
 
1558
+ // src/badge.ts
1559
+ function getScoreColor(score) {
1560
+ if (score === null || isNaN(score)) {
1561
+ return { name: "lightgrey", hex: "#9f9f9f" };
1562
+ }
1563
+ if (score >= 90) return { name: "brightgreen", hex: "#4c1" };
1564
+ if (score >= 80) return { name: "green", hex: "#97ca00" };
1565
+ if (score >= 70) return { name: "yellowgreen", hex: "#a4a61d" };
1566
+ if (score >= 60) return { name: "yellow", hex: "#dfb317" };
1567
+ return { name: "red", hex: "#e05d44" };
1568
+ }
1569
+ function generateSvgBadge(score, label = "AI Discipline") {
1570
+ const value = score !== null && !isNaN(score) ? `${score}/100` : "N/A";
1571
+ const color = getScoreColor(score);
1572
+ const labelWidth = Math.round(label.length * 6.8 + 14);
1573
+ const valueWidth = Math.round(value.length * 7.2 + 14);
1574
+ const totalWidth = labelWidth + valueWidth;
1575
+ const labelX = Math.round(labelWidth / 2);
1576
+ const valueX = Math.round(labelWidth + valueWidth / 2);
1577
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${value}">
1578
+ <linearGradient id="s" x2="0" y2="100%">
1579
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
1580
+ <stop offset="1" stop-opacity=".1"/>
1581
+ </linearGradient>
1582
+ <clipPath id="r">
1583
+ <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
1584
+ </clipPath>
1585
+ <g clip-path="url(#r)">
1586
+ <rect width="${labelWidth}" height="20" fill="#555"/>
1587
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color.hex}"/>
1588
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
1589
+ </g>
1590
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
1591
+ <text aria-hidden="true" x="${labelX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${(labelWidth - 14) * 10}">${label}</text>
1592
+ <text x="${labelX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${(labelWidth - 14) * 10}">${label}</text>
1593
+ <text aria-hidden="true" x="${valueX * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${(valueWidth - 14) * 10}">${value}</text>
1594
+ <text x="${valueX * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${(valueWidth - 14) * 10}">${value}</text>
1595
+ </g>
1596
+ </svg>`;
1597
+ }
1598
+ function generateShieldsMarkdown(score, label = "AI Discipline", repoUrl = "https://github.com/Nari-Ab/agent-roast") {
1599
+ const value = score !== null && !isNaN(score) ? `${score}%2F100` : "N%2FA";
1600
+ const color = getScoreColor(score);
1601
+ const encodedLabel = encodeURIComponent(label);
1602
+ const badgeUrl = `https://img.shields.io/badge/${encodedLabel}-${value}-${color.name}?style=flat-square`;
1603
+ return `[![${label}](${badgeUrl})](${repoUrl})`;
1604
+ }
1605
+ function generateShieldsJson(score, label = "AI Discipline") {
1606
+ const value = score !== null && !isNaN(score) ? `${score}/100` : "N/A";
1607
+ const color = getScoreColor(score);
1608
+ return {
1609
+ schemaVersion: 1,
1610
+ label,
1611
+ message: value,
1612
+ color: color.name
1613
+ };
1614
+ }
1615
+
1515
1616
  // src/index.ts
1516
1617
  var InvalidDetectorError = class extends Error {
1517
1618
  constructor(message) {
@@ -1589,6 +1690,8 @@ async function auditPullRequest(options) {
1589
1690
  result.output = JSON.stringify(result, null, 2);
1590
1691
  } else if (format === "github") {
1591
1692
  result.output = formatGithubAnnotations(result);
1693
+ } else if (format === "markdown") {
1694
+ result.output = formatPrMarkdownComment(result);
1592
1695
  } else {
1593
1696
  result.output = formatPrTerminalCard(result);
1594
1697
  }
@@ -1623,11 +1726,16 @@ export {
1623
1726
  formatPrTerminalCard,
1624
1727
  formatGithubAnnotations,
1625
1728
  formatGithubStepSummary,
1729
+ formatPrMarkdownComment,
1626
1730
  VALID_DETECTORS,
1627
1731
  DEFAULT_CONFIG_TEMPLATE,
1628
1732
  GITHUB_WORKFLOW_TEMPLATE,
1629
1733
  PRE_COMMIT_HOOK_TEMPLATE,
1630
1734
  initProject,
1735
+ getScoreColor,
1736
+ generateSvgBadge,
1737
+ generateShieldsMarkdown,
1738
+ generateShieldsJson,
1631
1739
  InvalidDetectorError,
1632
1740
  roastRepository,
1633
1741
  auditPullRequest
package/dist/cli.js CHANGED
@@ -5,19 +5,23 @@ import {
5
5
  InvalidDetectorError,
6
6
  auditPullRequest,
7
7
  formatGithubStepSummary,
8
+ generateShieldsJson,
9
+ generateShieldsMarkdown,
10
+ generateSvgBadge,
8
11
  initProject,
9
12
  loadConfig,
10
13
  roastRepository
11
- } from "./chunk-SMZZAGFS.js";
14
+ } from "./chunk-O4SB2FCT.js";
12
15
 
13
16
  // src/cli.ts
14
17
  import { Command } from "commander";
15
18
  import { createRequire } from "module";
16
19
  import * as fs from "fs";
20
+ import * as path from "path";
17
21
  var require2 = createRequire(import.meta.url);
18
22
  var pkg = require2("../package.json");
19
23
  var program = new Command();
20
- program.name("agent-roast").description("Audit your git history or PR diffs for AI coding shortcuts and panic loops").version(pkg.version || "0.3.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("--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", "terminal").option("--authors", "Display author names and emails in report", false).action(async (repoPath, options) => {
24
+ 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("--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) => {
21
25
  try {
22
26
  if (process.env.GITHUB_EVENT_NAME === "pull_request_target") {
23
27
  console.warn(
@@ -127,4 +131,46 @@ program.command("init").description("Initialize .agentroastrc.json and CI/hook c
127
131
  process.exitCode = 1;
128
132
  }
129
133
  });
134
+ 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) => {
135
+ try {
136
+ const rootOpts = program.opts();
137
+ const isJson = Boolean(opts.json || rootOpts.json);
138
+ const isAll = Boolean(opts.all || rootOpts.all);
139
+ const since = opts.since || rootOpts.since || "90 days ago";
140
+ const config = await loadConfig(repoPath);
141
+ const result = await roastRepository({
142
+ cwd: repoPath,
143
+ since,
144
+ all: isAll,
145
+ ignorePaths: config.ignorePaths
146
+ });
147
+ const score = result.summary.score;
148
+ const label = opts.label || "AI Discipline";
149
+ if (isJson) {
150
+ const payload = generateShieldsJson(score, label);
151
+ console.log(JSON.stringify(payload, null, 2));
152
+ process.exitCode = 0;
153
+ return;
154
+ }
155
+ if (opts.out) {
156
+ const svg = generateSvgBadge(score, label);
157
+ const outPath = path.resolve(opts.out);
158
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
159
+ fs.writeFileSync(outPath, svg, "utf-8");
160
+ console.log(`Generated SVG badge at: ${opts.out}`);
161
+ process.exitCode = 0;
162
+ return;
163
+ }
164
+ const markdown = generateShieldsMarkdown(score, label);
165
+ console.log(`
166
+ Badge for README.md:
167
+
168
+ ${markdown}
169
+ `);
170
+ process.exitCode = 0;
171
+ } catch (err) {
172
+ console.error(`Error generating badge: ${err.message}`);
173
+ process.exitCode = 1;
174
+ }
175
+ });
130
176
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ interface PrAuditOptions {
55
55
  base: string;
56
56
  head: string;
57
57
  failOn?: string[];
58
- format?: "terminal" | "json" | "github";
58
+ format?: "terminal" | "json" | "github" | "markdown";
59
59
  verbose?: boolean;
60
60
  authors?: boolean;
61
61
  ignorePaths?: string[];
@@ -206,6 +206,10 @@ declare function formatGithubAnnotations(result: PrAuditResult): string;
206
206
  * Generates GitHub Step Summary Markdown
207
207
  */
208
208
  declare function formatGithubStepSummary(result: PrAuditResult): string;
209
+ /**
210
+ * Generates rich GitHub PR Comment Markdown (for PR comments / sticky bots)
211
+ */
212
+ declare function formatPrMarkdownComment(result: PrAuditResult): string;
209
213
 
210
214
  interface AgentRoastConfig {
211
215
  /** Detector rules to fail CI on in PR mode */
@@ -256,6 +260,20 @@ declare const GITHUB_WORKFLOW_TEMPLATE = "name: Agent Roast\non:\n pull_request
256
260
  declare const PRE_COMMIT_HOOK_TEMPLATE = "#!/bin/sh\n# Run agent-roast pre-commit verification\nnpx agent-roast --fail-under 70\n";
257
261
  declare function initProject(options?: InitOptions): Promise<InitResult>;
258
262
 
263
+ interface ShieldsEndpointResponse {
264
+ schemaVersion: 1;
265
+ label: string;
266
+ message: string;
267
+ color: string;
268
+ }
269
+ declare function getScoreColor(score: number | null): {
270
+ name: string;
271
+ hex: string;
272
+ };
273
+ declare function generateSvgBadge(score: number | null, label?: string): string;
274
+ declare function generateShieldsMarkdown(score: number | null, label?: string, repoUrl?: string): string;
275
+ declare function generateShieldsJson(score: number | null, label?: string): ShieldsEndpointResponse;
276
+
259
277
  declare class InvalidDetectorError extends Error {
260
278
  constructor(message: string);
261
279
  }
@@ -265,4 +283,4 @@ declare function roastRepository(options?: RoastOptions): Promise<{
265
283
  }>;
266
284
  declare function auditPullRequest(options: PrAuditOptions): Promise<PrAuditResult>;
267
285
 
268
- export { type AgentRoastConfig, type AiAttributionResult, type CommitInfo, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, 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, type RoastOptions, VALID_DETECTORS, auditFileDiffs, auditPullRequest, detectAiAttribution, findMergeBase, formatGithubAnnotations, formatGithubStepSummary, formatPrTerminalCard, formatTerminalCard, generateShareUrl, getHermeticGitEnv, initProject, isCodeFile, isTestFile, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath };
286
+ export { type AgentRoastConfig, type AiAttributionResult, type CommitInfo, DEFAULT_CONFIG, DEFAULT_CONFIG_TEMPLATE, type DetectorType, FIELD_SEPARATOR, 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, type RoastOptions, type ShieldsEndpointResponse, VALID_DETECTORS, auditFileDiffs, auditPullRequest, detectAiAttribution, findMergeBase, formatGithubAnnotations, formatGithubStepSummary, formatPrMarkdownComment, formatPrTerminalCard, formatTerminalCard, generateShareUrl, generateShieldsJson, generateShieldsMarkdown, generateSvgBadge, getHermeticGitEnv, getScoreColor, initProject, isCodeFile, isTestFile, loadConfig, matchIgnorePath, parseDiff, parseGitLogStream, resolveCommitSha, roastRepository, streamGitDiff, streamGitLog, stripStringAndRegexLiterals, unquoteGitPath };
package/dist/index.js CHANGED
@@ -18,10 +18,15 @@ import {
18
18
  findMergeBase,
19
19
  formatGithubAnnotations,
20
20
  formatGithubStepSummary,
21
+ formatPrMarkdownComment,
21
22
  formatPrTerminalCard,
22
23
  formatTerminalCard,
23
24
  generateShareUrl,
25
+ generateShieldsJson,
26
+ generateShieldsMarkdown,
27
+ generateSvgBadge,
24
28
  getHermeticGitEnv,
29
+ getScoreColor,
25
30
  initProject,
26
31
  isCodeFile,
27
32
  isTestFile,
@@ -35,7 +40,7 @@ import {
35
40
  streamGitLog,
36
41
  stripStringAndRegexLiterals,
37
42
  unquoteGitPath
38
- } from "./chunk-SMZZAGFS.js";
43
+ } from "./chunk-O4SB2FCT.js";
39
44
  export {
40
45
  DEFAULT_CONFIG,
41
46
  DEFAULT_CONFIG_TEMPLATE,
@@ -55,10 +60,15 @@ export {
55
60
  findMergeBase,
56
61
  formatGithubAnnotations,
57
62
  formatGithubStepSummary,
63
+ formatPrMarkdownComment,
58
64
  formatPrTerminalCard,
59
65
  formatTerminalCard,
60
66
  generateShareUrl,
67
+ generateShieldsJson,
68
+ generateShieldsMarkdown,
69
+ generateSvgBadge,
61
70
  getHermeticGitEnv,
71
+ getScoreColor,
62
72
  initProject,
63
73
  isCodeFile,
64
74
  isTestFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-telemetry CLI scorecard analyzing git history for AI coding agent infractions and panic loops",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",