agent-roast 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/chunk-RFCXDN22.js +555 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +28 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +21 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nari-Ab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# agent-roast 🤖🔥
|
|
2
|
+
|
|
3
|
+
> **Zero-telemetry CLI scorecard auditing your git history for AI coding agent infractions, shortcuts, and panic loops.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/agent-roast)
|
|
6
|
+
[-1f9d55)](#privacy--security)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Did your AI coding agent (Cursor, Claude Code, Aider) write clean software, or did it silently delete tests and slap `as any` everywhere to pretend everything compiles?
|
|
10
|
+
|
|
11
|
+
Run in any git repository (100% local, zero tokens, zero telemetry):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx agent-roast
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### What it looks like:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
╔══════════════════════════════════════════════════════════╗
|
|
23
|
+
║ 🤖 AGENT ROAST DOSSIER ║
|
|
24
|
+
╚══════════════════════════════════════════════════════════╝
|
|
25
|
+
|
|
26
|
+
Repository: my-app
|
|
27
|
+
Commits: 18 AI-attributed / 42 total
|
|
28
|
+
Code Added: 4,210 lines (AI) / 12,480 lines (total)
|
|
29
|
+
|
|
30
|
+
AGENT ARCHETYPE: The Any Architect
|
|
31
|
+
Leans on compiler bypasses (`as any`, `@ts-ignore`) rather than strict type modeling.
|
|
32
|
+
|
|
33
|
+
DISCIPLINE SCORE: 68 / 100
|
|
34
|
+
(100 = spotless discipline, 0 = complete shortcut addiction)
|
|
35
|
+
|
|
36
|
+
📊 INFRACTIONS BREAKDOWN:
|
|
37
|
+
───────────────────────────────────────────────────────
|
|
38
|
+
✖ Skipped tests: 4 (it.skip, describe.skip, xit)
|
|
39
|
+
▲ Type escapes: 16 (as any, @ts-ignore, eslint-disable)
|
|
40
|
+
✖ Swallowed errors: 3 (empty catch {}, except: pass)
|
|
41
|
+
↻ Panic fix loops: 2 (quick fixes < 15m apart)
|
|
42
|
+
|
|
43
|
+
───────────────────────────────────────────────────────
|
|
44
|
+
📢 Share your score on X: https://twitter.com/intent/tweet?text=...
|
|
45
|
+
Tip: Run with --verbose to see exact commit SHAs and lines
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### What it detects (only added lines, zero false alarms from file renames):
|
|
51
|
+
|
|
52
|
+
1. **Skipped Tests:** `it.skip`, `describe.skip`, `xit`, `test.todo` added when tests fail.
|
|
53
|
+
2. **Type Escapes:** `as any`, `// @ts-ignore`, `// @ts-expect-error`, `# type: ignore`, `eslint-disable`.
|
|
54
|
+
3. **Swallowed Errors:** Empty `catch {}` or `except: pass` blocks hiding production bugs.
|
|
55
|
+
4. **Panic Loops:** Consecutive quickfix/revert commits within 15 minutes by the agent.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Options
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx agent-roast # Audit AI-attributed commits (Co-Authored-By: Claude, Cursor, Aider, etc.)
|
|
63
|
+
npx agent-roast --all # Audit ALL commits across the repository
|
|
64
|
+
npx agent-roast --verbose # Print exact commit SHAs and code lines as proof
|
|
65
|
+
npx agent-roast --since 30d # Audit last 30 days of git history
|
|
66
|
+
npx agent-roast --json # Output raw JSON metrics for CI pipelines
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Privacy & Security
|
|
72
|
+
|
|
73
|
+
`agent-roast` is strictly **local and deterministic**:
|
|
74
|
+
- Runs streaming `git log` on your machine in milliseconds.
|
|
75
|
+
- **Zero network calls**, zero LLM APIs, zero tracking or analytics.
|
|
76
|
+
- Your source code never leaves your terminal.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### License
|
|
81
|
+
|
|
82
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// src/git.ts
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
|
|
9
|
+
// src/detector.ts
|
|
10
|
+
var TRAILER_AI_PATTERNS = [
|
|
11
|
+
{ regex: /Co-Authored-By:.*(?:Claude|Anthropic)/i, name: "trailer:claude" },
|
|
12
|
+
{ regex: /Co-Authored-By:.*(?:Cursor|Anysphere)/i, name: "trailer:cursor" },
|
|
13
|
+
{ regex: /Co-Authored-By:.*(?:Copilot|GitHub Copilot)/i, name: "trailer:copilot" },
|
|
14
|
+
{ regex: /Co-Authored-By:.*(?:Aider|aider)/i, name: "trailer:aider" },
|
|
15
|
+
{ regex: /Co-Authored-By:.*(?:Devin|cognition)/i, name: "trailer:devin" },
|
|
16
|
+
{ regex: /Co-Authored-By:.*(?:Windsurf|Codeium)/i, name: "trailer:windsurf" },
|
|
17
|
+
{ regex: /Co-Authored-By:.*(?:ChatGPT|OpenAI|GPT)/i, name: "trailer:gpt" }
|
|
18
|
+
];
|
|
19
|
+
var MESSAGE_AI_PATTERNS = [
|
|
20
|
+
{ regex: /(?:Generated|Assisted|Created|Written) by Cursor/i, name: "message:cursor" },
|
|
21
|
+
{ regex: /(?:Generated|Assisted|Created|Written) by Claude/i, name: "message:claude" },
|
|
22
|
+
{ regex: /(?:Generated|Assisted|Created|Written) by Aider/i, name: "message:aider" },
|
|
23
|
+
{ regex: /(?:Generated|Assisted|Created|Written) by Copilot/i, name: "message:copilot" },
|
|
24
|
+
{ regex: /(?:Generated|Assisted|Created|Written) by Devin/i, name: "message:devin" }
|
|
25
|
+
];
|
|
26
|
+
var AUTHOR_AI_PATTERNS = [
|
|
27
|
+
{ regex: /(?:claude|anthropic)/i, name: "author:claude" },
|
|
28
|
+
{ regex: /(?:cursor|anysphere)/i, name: "author:cursor" },
|
|
29
|
+
{ regex: /(?:copilot)/i, name: "author:copilot" },
|
|
30
|
+
{ regex: /(?:aider)/i, name: "author:aider" },
|
|
31
|
+
{ regex: /(?:devin)/i, name: "author:devin" },
|
|
32
|
+
{ regex: /(?:\[bot\]|bot@|automation)/i, name: "author:bot" }
|
|
33
|
+
];
|
|
34
|
+
function detectAiAttribution(authorName, authorEmail, message) {
|
|
35
|
+
const signatures = [];
|
|
36
|
+
for (const { regex, name } of TRAILER_AI_PATTERNS) {
|
|
37
|
+
if (regex.test(message)) {
|
|
38
|
+
signatures.push(name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const { regex, name } of MESSAGE_AI_PATTERNS) {
|
|
42
|
+
if (regex.test(message)) {
|
|
43
|
+
signatures.push(name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const authorIdentity = `${authorName} <${authorEmail}>`;
|
|
47
|
+
for (const { regex, name } of AUTHOR_AI_PATTERNS) {
|
|
48
|
+
if (regex.test(authorIdentity)) {
|
|
49
|
+
signatures.push(name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
isAiAttributed: signatures.length > 0,
|
|
54
|
+
signatures
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/git.ts
|
|
59
|
+
async function parseGitLogStream(stream, onCommit) {
|
|
60
|
+
let buffer = "";
|
|
61
|
+
const processBlock = (block) => {
|
|
62
|
+
if (!block.trim()) return;
|
|
63
|
+
const parts = block.split("");
|
|
64
|
+
if (parts.length < 5) return;
|
|
65
|
+
const hash = parts[0].trim();
|
|
66
|
+
const date = parts[1].trim();
|
|
67
|
+
const authorName = parts[2].trim();
|
|
68
|
+
const authorEmail = parts[3].trim();
|
|
69
|
+
const message = parts[4].trim();
|
|
70
|
+
const rawDiff = parts.slice(5).join("");
|
|
71
|
+
const attribution = detectAiAttribution(authorName, authorEmail, message);
|
|
72
|
+
const files = [];
|
|
73
|
+
let currentFile = null;
|
|
74
|
+
let currentLineNum = 0;
|
|
75
|
+
let totalAdded = 0;
|
|
76
|
+
const diffLines = rawDiff.split("\n");
|
|
77
|
+
for (const dLine of diffLines) {
|
|
78
|
+
if (dLine.startsWith("diff --git")) {
|
|
79
|
+
const match = dLine.match(/b\/(.+)$/);
|
|
80
|
+
const filePath = match ? match[1] : "unknown";
|
|
81
|
+
currentFile = { path: filePath, addedLines: [] };
|
|
82
|
+
files.push(currentFile);
|
|
83
|
+
} else if (dLine.startsWith("@@")) {
|
|
84
|
+
const match = dLine.match(/\+(\d+)/);
|
|
85
|
+
if (match) {
|
|
86
|
+
currentLineNum = parseInt(match[1], 10);
|
|
87
|
+
}
|
|
88
|
+
} else if (dLine.startsWith("+") && !dLine.startsWith("+++")) {
|
|
89
|
+
if (currentFile) {
|
|
90
|
+
currentFile.addedLines.push({
|
|
91
|
+
line: dLine.slice(1),
|
|
92
|
+
lineNumber: currentLineNum
|
|
93
|
+
});
|
|
94
|
+
totalAdded++;
|
|
95
|
+
}
|
|
96
|
+
currentLineNum++;
|
|
97
|
+
} else if (!dLine.startsWith("-")) {
|
|
98
|
+
currentLineNum++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
onCommit({
|
|
102
|
+
hash,
|
|
103
|
+
date,
|
|
104
|
+
authorName,
|
|
105
|
+
authorEmail,
|
|
106
|
+
message,
|
|
107
|
+
isAiAttributed: attribution.isAiAttributed,
|
|
108
|
+
aiSignatures: attribution.signatures,
|
|
109
|
+
files,
|
|
110
|
+
totalAddedCount: totalAdded
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
stream.on("data", (chunk) => {
|
|
115
|
+
buffer += chunk.toString();
|
|
116
|
+
const records = buffer.split("");
|
|
117
|
+
while (records.length > 1) {
|
|
118
|
+
const completeBlock = records.shift();
|
|
119
|
+
if (completeBlock !== void 0) {
|
|
120
|
+
processBlock(completeBlock);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
buffer = records[0] || "";
|
|
124
|
+
});
|
|
125
|
+
stream.on("end", () => {
|
|
126
|
+
if (buffer.trim()) {
|
|
127
|
+
processBlock(buffer);
|
|
128
|
+
}
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
stream.on("error", reject);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function streamGitLog(cwd = process.cwd(), since = "90 days ago") {
|
|
135
|
+
const gitArgs = [
|
|
136
|
+
"--no-pager",
|
|
137
|
+
"log",
|
|
138
|
+
"-p",
|
|
139
|
+
"-U0",
|
|
140
|
+
"--no-color",
|
|
141
|
+
`--since=${since}`,
|
|
142
|
+
"--format=%x1e%H%x1f%aI%x1f%an%x1f%ae%x1f%B%x1f",
|
|
143
|
+
"--",
|
|
144
|
+
".",
|
|
145
|
+
":(exclude)*.lock",
|
|
146
|
+
":(exclude)*-lock.json",
|
|
147
|
+
":(exclude)*.lockb",
|
|
148
|
+
":(exclude)dist",
|
|
149
|
+
":(exclude)node_modules",
|
|
150
|
+
":(exclude).drift_venv"
|
|
151
|
+
];
|
|
152
|
+
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
153
|
+
const child = spawn("git", gitArgs, {
|
|
154
|
+
cwd,
|
|
155
|
+
env: {
|
|
156
|
+
...process.env,
|
|
157
|
+
GIT_PAGER: "cat",
|
|
158
|
+
GIT_CONFIG_GLOBAL: process.env.GIT_CONFIG_GLOBAL || nullDevice,
|
|
159
|
+
GIT_CONFIG_SYSTEM: process.env.GIT_CONFIG_SYSTEM || nullDevice
|
|
160
|
+
},
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
windowsHide: true
|
|
163
|
+
});
|
|
164
|
+
child.stderr.on("data", (chunk) => {
|
|
165
|
+
const errText = chunk.toString();
|
|
166
|
+
if (errText.includes("fatal:") || errText.includes("error:")) {
|
|
167
|
+
child.stdout.emit("error", new Error(errText.trim()));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
child.on("error", (err) => {
|
|
171
|
+
child.stdout.emit("error", err);
|
|
172
|
+
});
|
|
173
|
+
return child.stdout;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/metrics.ts
|
|
177
|
+
var TEST_SKIP_PATTERNS = [
|
|
178
|
+
{ regex: /(?:it|test|describe)\.skip\b/, name: "skipped test (it.skip / describe.skip)" },
|
|
179
|
+
{ regex: /\b(?:xit|xtest)\(/, name: "disabled test (xit / xtest)" },
|
|
180
|
+
{ regex: /@pytest\.mark\.skip\b/, name: "skipped pytest (@pytest.mark.skip)" },
|
|
181
|
+
{ regex: /\bpytest\.skip\(/, name: "imperative pytest skip (pytest.skip)" },
|
|
182
|
+
{ regex: /\btest\.todo\(/, name: "todo placeholder test" }
|
|
183
|
+
];
|
|
184
|
+
var COMMENT_DIRECTIVE_PATTERNS = [
|
|
185
|
+
{ regex: /\/\/\s*@ts-ignore\b/, name: "compiler ignore (@ts-ignore)" },
|
|
186
|
+
{ regex: /\/\/\s*@ts-expect-error\b/, name: "compiler suppression (@ts-expect-error)" },
|
|
187
|
+
{ regex: /#\s*type:\s*ignore\b/, name: "python type ignore" },
|
|
188
|
+
{ regex: /\/\/\s*eslint-disable(?:-next-line)?\b/, name: "linter suppression (eslint-disable)" }
|
|
189
|
+
];
|
|
190
|
+
var CODE_TYPE_ESCAPE_PATTERNS = [
|
|
191
|
+
{ regex: /\bas\s+any\b/, name: "type bypass (as any)" }
|
|
192
|
+
];
|
|
193
|
+
var SWALLOWED_ERROR_PATTERNS = [
|
|
194
|
+
{ regex: /catch\s*(?:\([^)]*\))?\s*\{\s*\}/, name: "empty catch block (catch {})" },
|
|
195
|
+
{ regex: /except(?:\s+[^:]*)?:\s*pass\b/, name: "silent python error swallow (except: pass)" }
|
|
196
|
+
];
|
|
197
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
198
|
+
".ts",
|
|
199
|
+
".tsx",
|
|
200
|
+
".js",
|
|
201
|
+
".jsx",
|
|
202
|
+
".mjs",
|
|
203
|
+
".cjs",
|
|
204
|
+
".py",
|
|
205
|
+
".go",
|
|
206
|
+
".rs",
|
|
207
|
+
".java",
|
|
208
|
+
".kt",
|
|
209
|
+
".vue",
|
|
210
|
+
".svelte",
|
|
211
|
+
".php",
|
|
212
|
+
".rb",
|
|
213
|
+
".cs",
|
|
214
|
+
".cpp",
|
|
215
|
+
".c"
|
|
216
|
+
]);
|
|
217
|
+
var IGNORED_PATH_REGEX = /(?:^|\/)(?:fixtures|__fixtures__|testdata|mocks|test_data|__snapshots__|sample[s]?)\//i;
|
|
218
|
+
function isCodeFile(filePath) {
|
|
219
|
+
if (IGNORED_PATH_REGEX.test(filePath)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
223
|
+
return CODE_EXTENSIONS.has(ext);
|
|
224
|
+
}
|
|
225
|
+
function stripStringAndRegexLiterals(line) {
|
|
226
|
+
return line.replace(/(["'`])(?:\\.|(?!\1)[^\\\r\n])*\1/g, '""').replace(/\/(?=[^\/\s])(?:\\\/|[^\/\r\n])+\/[gimsuy]*/g, '""');
|
|
227
|
+
}
|
|
228
|
+
function isCommentOnlyLine(trimmedLine) {
|
|
229
|
+
return trimmedLine.startsWith("//") || trimmedLine.startsWith("/*") || trimmedLine.startsWith("*") || trimmedLine.startsWith("#");
|
|
230
|
+
}
|
|
231
|
+
var PANIC_COMMIT_REGEX = /\b(?:fix|quickfix|revert|hotfix|retry|patch|undo|workaround)\b/i;
|
|
232
|
+
var MAX_INFRACTIONS_PER_COMMIT = 10;
|
|
233
|
+
var MIN_SAMPLE_LOC = 200;
|
|
234
|
+
var MetricsCollector = class {
|
|
235
|
+
totalLinesAdded = 0;
|
|
236
|
+
aiLinesAdded = 0;
|
|
237
|
+
totalCommits = 0;
|
|
238
|
+
aiCommits = 0;
|
|
239
|
+
testSkips = [];
|
|
240
|
+
typeEscapes = [];
|
|
241
|
+
swallowedErrors = [];
|
|
242
|
+
panicLoops = [];
|
|
243
|
+
lastFixCommit = null;
|
|
244
|
+
includeAll;
|
|
245
|
+
constructor(options = {}) {
|
|
246
|
+
this.includeAll = options.all ?? false;
|
|
247
|
+
}
|
|
248
|
+
processCommit(commit) {
|
|
249
|
+
this.totalCommits++;
|
|
250
|
+
this.totalLinesAdded += commit.totalAddedCount;
|
|
251
|
+
const isTarget = this.includeAll || commit.isAiAttributed;
|
|
252
|
+
if (commit.isAiAttributed) {
|
|
253
|
+
this.aiCommits++;
|
|
254
|
+
this.aiLinesAdded += commit.totalAddedCount;
|
|
255
|
+
}
|
|
256
|
+
if (!isTarget) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const commitDateMs = Date.parse(commit.date);
|
|
260
|
+
if (PANIC_COMMIT_REGEX.test(commit.message)) {
|
|
261
|
+
if (this.lastFixCommit && !isNaN(commitDateMs) && !isNaN(this.lastFixCommit.date) && this.lastFixCommit.author.toLowerCase() === commit.authorEmail.toLowerCase()) {
|
|
262
|
+
const diffMinutes = Math.abs(commitDateMs - this.lastFixCommit.date) / (1e3 * 60);
|
|
263
|
+
if (diffMinutes > 0 && diffMinutes <= 15) {
|
|
264
|
+
const attributionTag = commit.isAiAttributed ? "AI panic loop" : "Panic loop (unverified / --all mode)";
|
|
265
|
+
this.panicLoops.push({
|
|
266
|
+
type: "panic-loop",
|
|
267
|
+
commitHash: commit.hash,
|
|
268
|
+
commitDate: commit.date,
|
|
269
|
+
reason: `${attributionTag}: consecutive quick fix within ${Math.max(1, Math.round(diffMinutes))}m (${this.lastFixCommit.hash.slice(0, 7)} -> ${commit.hash.slice(0, 7)})`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.lastFixCommit = {
|
|
274
|
+
hash: commit.hash,
|
|
275
|
+
date: commitDateMs,
|
|
276
|
+
message: commit.message,
|
|
277
|
+
author: commit.authorEmail
|
|
278
|
+
};
|
|
279
|
+
} else {
|
|
280
|
+
this.lastFixCommit = null;
|
|
281
|
+
}
|
|
282
|
+
let commitSkips = 0;
|
|
283
|
+
let commitEscapes = 0;
|
|
284
|
+
let commitSwallowed = 0;
|
|
285
|
+
for (const file of commit.files) {
|
|
286
|
+
if (!isCodeFile(file.path)) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for (const added of file.addedLines) {
|
|
290
|
+
const rawLine = added.line;
|
|
291
|
+
const cleanedLine = stripStringAndRegexLiterals(rawLine);
|
|
292
|
+
const trimmedCleaned = cleanedLine.trim();
|
|
293
|
+
const isComment = isCommentOnlyLine(trimmedCleaned);
|
|
294
|
+
if (!isComment && commitSkips < MAX_INFRACTIONS_PER_COMMIT) {
|
|
295
|
+
for (const rule of TEST_SKIP_PATTERNS) {
|
|
296
|
+
if (rule.regex.test(cleanedLine)) {
|
|
297
|
+
this.testSkips.push({
|
|
298
|
+
type: "test-skip",
|
|
299
|
+
file: file.path,
|
|
300
|
+
lineSnippet: rawLine.trim(),
|
|
301
|
+
commitHash: commit.hash,
|
|
302
|
+
commitDate: commit.date,
|
|
303
|
+
reason: rule.name
|
|
304
|
+
});
|
|
305
|
+
commitSkips++;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (commitEscapes < MAX_INFRACTIONS_PER_COMMIT) {
|
|
311
|
+
for (const rule of COMMENT_DIRECTIVE_PATTERNS) {
|
|
312
|
+
if (rule.regex.test(cleanedLine)) {
|
|
313
|
+
this.typeEscapes.push({
|
|
314
|
+
type: "type-escape",
|
|
315
|
+
file: file.path,
|
|
316
|
+
lineSnippet: rawLine.trim(),
|
|
317
|
+
commitHash: commit.hash,
|
|
318
|
+
commitDate: commit.date,
|
|
319
|
+
reason: rule.name
|
|
320
|
+
});
|
|
321
|
+
commitEscapes++;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!isComment && commitEscapes < MAX_INFRACTIONS_PER_COMMIT) {
|
|
326
|
+
for (const rule of CODE_TYPE_ESCAPE_PATTERNS) {
|
|
327
|
+
if (rule.regex.test(cleanedLine)) {
|
|
328
|
+
this.typeEscapes.push({
|
|
329
|
+
type: "type-escape",
|
|
330
|
+
file: file.path,
|
|
331
|
+
lineSnippet: rawLine.trim(),
|
|
332
|
+
commitHash: commit.hash,
|
|
333
|
+
commitDate: commit.date,
|
|
334
|
+
reason: rule.name
|
|
335
|
+
});
|
|
336
|
+
commitEscapes++;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!isComment && commitSwallowed < MAX_INFRACTIONS_PER_COMMIT) {
|
|
343
|
+
for (const rule of SWALLOWED_ERROR_PATTERNS) {
|
|
344
|
+
if (rule.regex.test(cleanedLine)) {
|
|
345
|
+
this.swallowedErrors.push({
|
|
346
|
+
type: "swallowed-error",
|
|
347
|
+
file: file.path,
|
|
348
|
+
lineSnippet: rawLine.trim(),
|
|
349
|
+
commitHash: commit.hash,
|
|
350
|
+
commitDate: commit.date,
|
|
351
|
+
reason: rule.name
|
|
352
|
+
});
|
|
353
|
+
commitSwallowed++;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
getSummary() {
|
|
362
|
+
const linesSample = this.includeAll ? this.totalLinesAdded : this.aiLinesAdded;
|
|
363
|
+
const isSufficientData = linesSample >= MIN_SAMPLE_LOC;
|
|
364
|
+
const totalInfractionPoints = this.testSkips.length * 15 + this.swallowedErrors.length * 10 + this.panicLoops.length * 8 + this.typeEscapes.length * 3;
|
|
365
|
+
const normalizedKLoc = linesSample / 1e3;
|
|
366
|
+
const factor = normalizedKLoc < 1 ? Math.max(0.5, 0.7 + 0.3 * normalizedKLoc) : 1 + 1.2 * Math.log(normalizedKLoc);
|
|
367
|
+
const normalizedPenalty = Math.round(totalInfractionPoints / factor);
|
|
368
|
+
const score = isSufficientData ? Math.max(0, Math.min(100, 100 - normalizedPenalty)) : 100;
|
|
369
|
+
let archetype = "The Clean Coder";
|
|
370
|
+
let archetypeDescription = "Spotless discipline. No skipped tests, zero panic loops, and clean type safety.";
|
|
371
|
+
if (isSufficientData) {
|
|
372
|
+
if (score >= 90) {
|
|
373
|
+
if (totalInfractionPoints === 0) {
|
|
374
|
+
archetype = "The Clean Coder";
|
|
375
|
+
archetypeDescription = "Spotless discipline. No skipped tests, zero panic loops, and clean type safety.";
|
|
376
|
+
} else {
|
|
377
|
+
archetype = "The Pragmatic Builder";
|
|
378
|
+
archetypeDescription = "Overwhelmingly disciplined. Only rare, isolated shortcuts across thousands of lines.";
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
const testSkipPoints = this.testSkips.length * 15;
|
|
382
|
+
const panicPoints = this.panicLoops.length * 8;
|
|
383
|
+
const escapePoints = this.typeEscapes.length * 3;
|
|
384
|
+
const swallowPoints = this.swallowedErrors.length * 10;
|
|
385
|
+
const maxPoints = Math.max(testSkipPoints, panicPoints, escapePoints, swallowPoints);
|
|
386
|
+
if (maxPoints === panicPoints && this.panicLoops.length >= 2) {
|
|
387
|
+
archetype = "The Panic Looper";
|
|
388
|
+
archetypeDescription = "Prone to rapid-fire fix and revert thrashing cycles when wrestling stubborn bugs.";
|
|
389
|
+
} else if (maxPoints === testSkipPoints && this.testSkips.length >= 3) {
|
|
390
|
+
archetype = "The Silent Vandal";
|
|
391
|
+
archetypeDescription = "When test suites push back, tends to disable or skip tests to keep pipelines green.";
|
|
392
|
+
} else if (maxPoints === escapePoints && this.typeEscapes.length >= 5) {
|
|
393
|
+
archetype = "The Any Architect";
|
|
394
|
+
archetypeDescription = "Leans on compiler bypasses (`as any`, `@ts-ignore`) rather than strict type modeling.";
|
|
395
|
+
} else if (maxPoints === swallowPoints && this.swallowedErrors.length >= 2) {
|
|
396
|
+
archetype = "The Secret Keeper";
|
|
397
|
+
archetypeDescription = "Tends to silence errors with empty catch blocks, obscuring runtime failures.";
|
|
398
|
+
} else if (score < 50) {
|
|
399
|
+
archetype = "The Chaos Gremlin";
|
|
400
|
+
archetypeDescription = "Mixes multiple shortcuts: skipped tests, type bypasses, and quick patches under pressure.";
|
|
401
|
+
} else {
|
|
402
|
+
archetype = "The Shortcut Specialist";
|
|
403
|
+
archetypeDescription = "Frequently cuts corners on edge cases, prioritizing delivery speed over rigour.";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
totalLinesAdded: this.totalLinesAdded,
|
|
409
|
+
aiLinesAdded: this.aiLinesAdded,
|
|
410
|
+
totalCommits: this.totalCommits,
|
|
411
|
+
aiCommits: this.aiCommits,
|
|
412
|
+
testSkips: this.testSkips,
|
|
413
|
+
typeEscapes: this.typeEscapes,
|
|
414
|
+
swallowedErrors: this.swallowedErrors,
|
|
415
|
+
panicLoops: this.panicLoops,
|
|
416
|
+
rawScore: totalInfractionPoints,
|
|
417
|
+
score,
|
|
418
|
+
isSufficientData,
|
|
419
|
+
archetype,
|
|
420
|
+
archetypeDescription
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/renderer.ts
|
|
426
|
+
import pc from "picocolors";
|
|
427
|
+
function generateShareUrl(summary) {
|
|
428
|
+
const text = `My AI coding agent scored ${summary.score}/100 on @agent_roast! \u{1F916}\u{1F525}
|
|
429
|
+
|
|
430
|
+
Archetype: "${summary.archetype}"
|
|
431
|
+
|
|
432
|
+
Infractions:
|
|
433
|
+
\u2022 ${summary.testSkips.length} skipped tests
|
|
434
|
+
\u2022 ${summary.typeEscapes.length} type escapes
|
|
435
|
+
\u2022 ${summary.panicLoops.length} panic revert loops
|
|
436
|
+
|
|
437
|
+
Audit your repo with: npx agent-roast`;
|
|
438
|
+
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`;
|
|
439
|
+
}
|
|
440
|
+
function formatTerminalCard(summary, options) {
|
|
441
|
+
const lines = [];
|
|
442
|
+
const boxW = 60;
|
|
443
|
+
const hLine = "\u2550".repeat(boxW - 2);
|
|
444
|
+
lines.push(pc.cyan(`\u2554${hLine}\u2557`));
|
|
445
|
+
lines.push(
|
|
446
|
+
pc.cyan("\u2551") + pc.bold(pc.white(" \u{1F916} AGENT ROAST DOSSIER ")) + pc.cyan("\u2551")
|
|
447
|
+
);
|
|
448
|
+
lines.push(pc.cyan(`\u255A${hLine}\u255D`));
|
|
449
|
+
lines.push("");
|
|
450
|
+
lines.push(` ${pc.dim("Repository:")} ${pc.bold(pc.white(options.repoName))}`);
|
|
451
|
+
lines.push(
|
|
452
|
+
` ${pc.dim("Commits:")} ${pc.yellow(summary.aiCommits)} AI-attributed / ${summary.totalCommits} total`
|
|
453
|
+
);
|
|
454
|
+
lines.push(
|
|
455
|
+
` ${pc.dim("Code Added:")} ${pc.green(summary.aiLinesAdded)} lines (AI) / ${summary.totalLinesAdded} lines (total)`
|
|
456
|
+
);
|
|
457
|
+
lines.push("");
|
|
458
|
+
if (!summary.isSufficientData) {
|
|
459
|
+
lines.push(pc.yellow(` \u26A0 INSUFFICIENT DATA TO ROAST (< 200 lines added)`));
|
|
460
|
+
lines.push(pc.dim(" Commit more AI code and run `npx agent-roast` again."));
|
|
461
|
+
lines.push("");
|
|
462
|
+
return lines.join("\n");
|
|
463
|
+
}
|
|
464
|
+
let scoreColor = pc.green;
|
|
465
|
+
if (summary.score < 60) scoreColor = pc.red;
|
|
466
|
+
else if (summary.score < 80) scoreColor = pc.yellow;
|
|
467
|
+
lines.push(` ${pc.bold("AGENT ARCHETYPE:")} ${pc.magenta(pc.bold(summary.archetype))}`);
|
|
468
|
+
lines.push(` ${pc.italic(summary.archetypeDescription)}`);
|
|
469
|
+
lines.push("");
|
|
470
|
+
lines.push(` ${pc.bold("DISCIPLINE SCORE:")} ${scoreColor(pc.bold(`${summary.score} / 100`))}`);
|
|
471
|
+
lines.push(
|
|
472
|
+
pc.dim(" (100 = spotless discipline, 0 = complete shortcut addiction)")
|
|
473
|
+
);
|
|
474
|
+
lines.push("");
|
|
475
|
+
lines.push(pc.bold(" \u{1F4CA} INFRACTIONS BREAKDOWN:"));
|
|
476
|
+
lines.push(pc.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
477
|
+
const skipIcon = summary.testSkips.length > 0 ? pc.red("\u2716") : pc.green("\u2714");
|
|
478
|
+
lines.push(
|
|
479
|
+
` ${skipIcon} ${pc.bold("Skipped tests:")} ${summary.testSkips.length} ${pc.dim("(it.skip, describe.skip, xit)")}`
|
|
480
|
+
);
|
|
481
|
+
const escapeIcon = summary.typeEscapes.length > 0 ? pc.yellow("\u25B2") : pc.green("\u2714");
|
|
482
|
+
lines.push(
|
|
483
|
+
` ${escapeIcon} ${pc.bold("Type escapes:")} ${summary.typeEscapes.length} ${pc.dim("(as any, @ts-ignore, eslint-disable)")}`
|
|
484
|
+
);
|
|
485
|
+
const errorIcon = summary.swallowedErrors.length > 0 ? pc.red("\u2716") : pc.green("\u2714");
|
|
486
|
+
lines.push(
|
|
487
|
+
` ${errorIcon} ${pc.bold("Swallowed errors:")} ${summary.swallowedErrors.length} ${pc.dim("(empty catch {}, except: pass)")}`
|
|
488
|
+
);
|
|
489
|
+
const panicIcon = summary.panicLoops.length > 0 ? pc.magenta("\u21BB") : pc.green("\u2714");
|
|
490
|
+
lines.push(
|
|
491
|
+
` ${panicIcon} ${pc.bold("Panic fix loops:")} ${summary.panicLoops.length} ${pc.dim("(quick fixes < 15m apart)")}`
|
|
492
|
+
);
|
|
493
|
+
lines.push("");
|
|
494
|
+
if (options.verbose) {
|
|
495
|
+
const allInfractions = [
|
|
496
|
+
...summary.testSkips,
|
|
497
|
+
...summary.typeEscapes,
|
|
498
|
+
...summary.swallowedErrors,
|
|
499
|
+
...summary.panicLoops
|
|
500
|
+
];
|
|
501
|
+
if (allInfractions.length > 0) {
|
|
502
|
+
lines.push(pc.bold(" \u{1F50D} VERBOSE PROOFS (First 10 items):"));
|
|
503
|
+
lines.push(pc.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
504
|
+
for (const inf of allInfractions.slice(0, 10)) {
|
|
505
|
+
const fileLoc = inf.file ? `${inf.file}` : "git commit";
|
|
506
|
+
lines.push(
|
|
507
|
+
` \u2022 ${pc.cyan(inf.commitHash.slice(0, 7))} [${inf.type}] ${pc.white(fileLoc)}`
|
|
508
|
+
);
|
|
509
|
+
if (inf.lineSnippet) {
|
|
510
|
+
lines.push(` ${pc.dim(inf.lineSnippet)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
lines.push("");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const shareUrl = generateShareUrl(summary);
|
|
517
|
+
lines.push(pc.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
518
|
+
lines.push(
|
|
519
|
+
` ${pc.cyan("\u{1F4E2} Share your score on X:")} ${pc.underline(pc.cyan(shareUrl))}`
|
|
520
|
+
);
|
|
521
|
+
lines.push(
|
|
522
|
+
` ${pc.dim("Tip: Run with --verbose to see exact commit SHAs and lines")}`
|
|
523
|
+
);
|
|
524
|
+
lines.push("");
|
|
525
|
+
return lines.join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/index.ts
|
|
529
|
+
async function roastRepository(options = {}) {
|
|
530
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
531
|
+
const since = options.since || "90 days ago";
|
|
532
|
+
const repoName = path.basename(cwd);
|
|
533
|
+
const collector = new MetricsCollector({ all: options.all });
|
|
534
|
+
const gitStream = streamGitLog(cwd, since);
|
|
535
|
+
await parseGitLogStream(gitStream, (commit) => {
|
|
536
|
+
collector.processCommit(commit);
|
|
537
|
+
});
|
|
538
|
+
const summary = collector.getSummary();
|
|
539
|
+
const output = formatTerminalCard(summary, {
|
|
540
|
+
repoName,
|
|
541
|
+
verbose: options.verbose
|
|
542
|
+
});
|
|
543
|
+
return { summary, output };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export {
|
|
547
|
+
detectAiAttribution,
|
|
548
|
+
parseGitLogStream,
|
|
549
|
+
streamGitLog,
|
|
550
|
+
stripStringAndRegexLiterals,
|
|
551
|
+
MetricsCollector,
|
|
552
|
+
generateShareUrl,
|
|
553
|
+
formatTerminalCard,
|
|
554
|
+
roastRepository
|
|
555
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
roastRepository
|
|
4
|
+
} from "./chunk-RFCXDN22.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
var program = new Command();
|
|
9
|
+
program.name("agent-roast").description("Audit your git history for AI coding agent infractions and panic loops").version("0.1.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).action(async (repoPath, options) => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await roastRepository({
|
|
12
|
+
cwd: repoPath,
|
|
13
|
+
since: options.since,
|
|
14
|
+
all: options.all,
|
|
15
|
+
verbose: options.verbose,
|
|
16
|
+
json: options.json
|
|
17
|
+
});
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify(result.summary, null, 2));
|
|
20
|
+
} else {
|
|
21
|
+
console.log(result.output);
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`Error auditing repository: ${err.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
program.parse(process.argv);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
|
|
3
|
+
interface CommitInfo {
|
|
4
|
+
hash: string;
|
|
5
|
+
date: string;
|
|
6
|
+
authorName: string;
|
|
7
|
+
authorEmail: string;
|
|
8
|
+
message: string;
|
|
9
|
+
isAiAttributed: boolean;
|
|
10
|
+
aiSignatures: string[];
|
|
11
|
+
}
|
|
12
|
+
interface Infraction {
|
|
13
|
+
type: "test-skip" | "type-escape" | "swallowed-error" | "panic-loop";
|
|
14
|
+
file?: string;
|
|
15
|
+
lineSnippet?: string;
|
|
16
|
+
commitHash: string;
|
|
17
|
+
commitDate: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
interface MetricSummary {
|
|
21
|
+
totalLinesAdded: number;
|
|
22
|
+
aiLinesAdded: number;
|
|
23
|
+
totalCommits: number;
|
|
24
|
+
aiCommits: number;
|
|
25
|
+
testSkips: Infraction[];
|
|
26
|
+
typeEscapes: Infraction[];
|
|
27
|
+
swallowedErrors: Infraction[];
|
|
28
|
+
panicLoops: Infraction[];
|
|
29
|
+
rawScore: number;
|
|
30
|
+
score: number;
|
|
31
|
+
isSufficientData: boolean;
|
|
32
|
+
archetype: string;
|
|
33
|
+
archetypeDescription: string;
|
|
34
|
+
}
|
|
35
|
+
interface RoastOptions {
|
|
36
|
+
cwd?: string;
|
|
37
|
+
since?: string;
|
|
38
|
+
all?: boolean;
|
|
39
|
+
verbose?: boolean;
|
|
40
|
+
json?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface AiAttributionResult {
|
|
44
|
+
isAiAttributed: boolean;
|
|
45
|
+
signatures: string[];
|
|
46
|
+
}
|
|
47
|
+
declare function detectAiAttribution(authorName: string, authorEmail: string, message: string): AiAttributionResult;
|
|
48
|
+
|
|
49
|
+
interface ParsedDiffLine {
|
|
50
|
+
line: string;
|
|
51
|
+
lineNumber: number;
|
|
52
|
+
}
|
|
53
|
+
interface ParsedFileDiff {
|
|
54
|
+
path: string;
|
|
55
|
+
addedLines: ParsedDiffLine[];
|
|
56
|
+
}
|
|
57
|
+
interface ParsedCommit extends CommitInfo {
|
|
58
|
+
files: ParsedFileDiff[];
|
|
59
|
+
totalAddedCount: number;
|
|
60
|
+
}
|
|
61
|
+
declare function parseGitLogStream(stream: Readable, onCommit: (commit: ParsedCommit) => void): Promise<void>;
|
|
62
|
+
declare function streamGitLog(cwd?: string, since?: string): Readable;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Strips quoted string literals and regex literals from a line so that
|
|
66
|
+
* test fixtures and string constants (e.g. { line: "it.skip" }) are not flagged.
|
|
67
|
+
*/
|
|
68
|
+
declare function stripStringAndRegexLiterals(line: string): string;
|
|
69
|
+
declare class MetricsCollector {
|
|
70
|
+
private totalLinesAdded;
|
|
71
|
+
private aiLinesAdded;
|
|
72
|
+
private totalCommits;
|
|
73
|
+
private aiCommits;
|
|
74
|
+
private testSkips;
|
|
75
|
+
private typeEscapes;
|
|
76
|
+
private swallowedErrors;
|
|
77
|
+
private panicLoops;
|
|
78
|
+
private lastFixCommit;
|
|
79
|
+
private includeAll;
|
|
80
|
+
constructor(options?: {
|
|
81
|
+
all?: boolean;
|
|
82
|
+
});
|
|
83
|
+
processCommit(commit: ParsedCommit): void;
|
|
84
|
+
getSummary(): MetricSummary;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare function generateShareUrl(summary: MetricSummary): string;
|
|
88
|
+
declare function formatTerminalCard(summary: MetricSummary, options: {
|
|
89
|
+
repoName: string;
|
|
90
|
+
verbose?: boolean;
|
|
91
|
+
}): string;
|
|
92
|
+
|
|
93
|
+
declare function roastRepository(options?: RoastOptions): Promise<{
|
|
94
|
+
summary: MetricSummary;
|
|
95
|
+
output: string;
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
export { type AiAttributionResult, type CommitInfo, type Infraction, type MetricSummary, MetricsCollector, type ParsedCommit, type ParsedDiffLine, type ParsedFileDiff, type RoastOptions, detectAiAttribution, formatTerminalCard, generateShareUrl, parseGitLogStream, roastRepository, streamGitLog, stripStringAndRegexLiterals };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MetricsCollector,
|
|
4
|
+
detectAiAttribution,
|
|
5
|
+
formatTerminalCard,
|
|
6
|
+
generateShareUrl,
|
|
7
|
+
parseGitLogStream,
|
|
8
|
+
roastRepository,
|
|
9
|
+
streamGitLog,
|
|
10
|
+
stripStringAndRegexLiterals
|
|
11
|
+
} from "./chunk-RFCXDN22.js";
|
|
12
|
+
export {
|
|
13
|
+
MetricsCollector,
|
|
14
|
+
detectAiAttribution,
|
|
15
|
+
formatTerminalCard,
|
|
16
|
+
generateShareUrl,
|
|
17
|
+
parseGitLogStream,
|
|
18
|
+
roastRepository,
|
|
19
|
+
streamGitLog,
|
|
20
|
+
stripStringAndRegexLiterals
|
|
21
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-roast",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-telemetry CLI scorecard analyzing git history for AI coding agent infractions and panic loops",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agent-roast": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai",
|
|
24
|
+
"agents",
|
|
25
|
+
"cursor",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"roast",
|
|
28
|
+
"git",
|
|
29
|
+
"code-quality",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "Nari-Ab",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/Nari-Ab/agent-roast.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Nari-Ab/agent-roast/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/Nari-Ab/agent-roast#readme",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^12.0.0",
|
|
44
|
+
"picocolors": "^1.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"typescript": "^5.0.0",
|
|
50
|
+
"vitest": "^3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
}
|
|
55
|
+
}
|