agent-roast 0.1.4 → 0.2.1
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 +72 -15
- package/dist/chunk-XUNF5YCA.js +1312 -0
- package/dist/cli.js +70 -14
- package/dist/index.d.ts +114 -5
- package/dist/index.js +41 -3
- package/package.json +1 -1
- package/dist/chunk-EPPMO4JE.js +0 -560
package/dist/chunk-EPPMO4JE.js
DELETED
|
@@ -1,560 +0,0 @@
|
|
|
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.
|
|
429
|
-
|
|
430
|
-
Archetype: ${summary.archetype}
|
|
431
|
-
Infractions:
|
|
432
|
-
\u2022 ${summary.testSkips.length} skipped tests
|
|
433
|
-
\u2022 ${summary.typeEscapes.length} type escapes
|
|
434
|
-
\u2022 ${summary.panicLoops.length} panic revert loops
|
|
435
|
-
|
|
436
|
-
Audit your repo: npx agent-roast`;
|
|
437
|
-
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`;
|
|
438
|
-
}
|
|
439
|
-
function formatTerminalCard(summary, options) {
|
|
440
|
-
const lines = [];
|
|
441
|
-
lines.push("");
|
|
442
|
-
lines.push(
|
|
443
|
-
`${pc.bold(pc.cyan("agent-roast"))} ${pc.dim("\u2014 git discipline audit")} ${pc.white(options.repoName)}`
|
|
444
|
-
);
|
|
445
|
-
lines.push(
|
|
446
|
-
` ${pc.dim("commits:")} ${pc.yellow(summary.aiCommits)} AI / ${summary.totalCommits} total ${pc.dim("lines added:")} ${pc.green(summary.aiLinesAdded)} AI / ${summary.totalLinesAdded} total`
|
|
447
|
-
);
|
|
448
|
-
lines.push("");
|
|
449
|
-
if (!summary.isSufficientData) {
|
|
450
|
-
lines.push(pc.yellow(` insufficient data (< 200 lines added by AI)`));
|
|
451
|
-
lines.push(pc.dim(" commit more code and run `npx agent-roast` again."));
|
|
452
|
-
lines.push("");
|
|
453
|
-
return lines.join("\n");
|
|
454
|
-
}
|
|
455
|
-
let scoreColor = pc.green;
|
|
456
|
-
if (summary.score < 60) scoreColor = pc.red;
|
|
457
|
-
else if (summary.score < 80) scoreColor = pc.yellow;
|
|
458
|
-
lines.push(
|
|
459
|
-
` ${pc.bold("SCORE:")} ${scoreColor(pc.bold(`${summary.score} / 100`))} ${pc.dim("\xB7")} ${pc.magenta(pc.bold(summary.archetype))}`
|
|
460
|
-
);
|
|
461
|
-
lines.push(` ${pc.dim(summary.archetypeDescription)}`);
|
|
462
|
-
lines.push("");
|
|
463
|
-
const totalInfractions = summary.testSkips.length + summary.typeEscapes.length + summary.swallowedErrors.length + summary.panicLoops.length;
|
|
464
|
-
lines.push(pc.bold(` INFRACTIONS (${totalInfractions})`));
|
|
465
|
-
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\u2500\u2500\u2500"));
|
|
466
|
-
if (summary.panicLoops.length > 0) {
|
|
467
|
-
lines.push(
|
|
468
|
-
` ${pc.magenta("\u25CF")} ${pc.bold("panic-loop")} (${summary.panicLoops.length}) ${pc.dim("consecutive quick fixes < 15m apart")}`
|
|
469
|
-
);
|
|
470
|
-
const sample = summary.panicLoops[0];
|
|
471
|
-
lines.push(` ${pc.dim(sample.reason)}`);
|
|
472
|
-
}
|
|
473
|
-
if (summary.typeEscapes.length > 0) {
|
|
474
|
-
lines.push(
|
|
475
|
-
` ${pc.yellow("\u25CF")} ${pc.bold("type-escape")} (${summary.typeEscapes.length}) ${pc.dim("compiler bypasses (as any, @ts-ignore)")}`
|
|
476
|
-
);
|
|
477
|
-
const sample = summary.typeEscapes[0];
|
|
478
|
-
if (sample.file) {
|
|
479
|
-
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (summary.testSkips.length > 0) {
|
|
483
|
-
lines.push(
|
|
484
|
-
` ${pc.red("\u25CF")} ${pc.bold("test-skip")} (${summary.testSkips.length}) ${pc.dim("skipped or disabled tests")}`
|
|
485
|
-
);
|
|
486
|
-
const sample = summary.testSkips[0];
|
|
487
|
-
if (sample.file) {
|
|
488
|
-
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (summary.swallowedErrors.length > 0) {
|
|
492
|
-
lines.push(
|
|
493
|
-
` ${pc.red("\u25CF")} ${pc.bold("swallowed-error")} (${summary.swallowedErrors.length}) ${pc.dim("empty catch blocks or except: pass")}`
|
|
494
|
-
);
|
|
495
|
-
const sample = summary.swallowedErrors[0];
|
|
496
|
-
if (sample.file) {
|
|
497
|
-
lines.push(` ${pc.dim(`${sample.file}: ${sample.lineSnippet || ""}`)}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
if (totalInfractions === 0) {
|
|
501
|
-
lines.push(` ${pc.green("\u25CF")} ${pc.dim("zero infractions detected across all inspected lines.")}`);
|
|
502
|
-
}
|
|
503
|
-
lines.push("");
|
|
504
|
-
if (options.verbose) {
|
|
505
|
-
const allInfractions = [
|
|
506
|
-
...summary.testSkips,
|
|
507
|
-
...summary.typeEscapes,
|
|
508
|
-
...summary.swallowedErrors,
|
|
509
|
-
...summary.panicLoops
|
|
510
|
-
];
|
|
511
|
-
if (allInfractions.length > 0) {
|
|
512
|
-
lines.push(pc.bold(" EVIDENCE (First 10 occurrences):"));
|
|
513
|
-
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\u2500\u2500\u2500"));
|
|
514
|
-
for (const inf of allInfractions.slice(0, 10)) {
|
|
515
|
-
const fileLoc = inf.file ? `${inf.file}` : "commit history";
|
|
516
|
-
lines.push(
|
|
517
|
-
` \u25CF ${pc.cyan(inf.commitHash.slice(0, 7))} [${inf.type}] ${pc.white(fileLoc)}`
|
|
518
|
-
);
|
|
519
|
-
if (inf.lineSnippet) {
|
|
520
|
-
lines.push(` ${pc.dim(`\u2514\u2500 ${inf.lineSnippet}`)}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
lines.push("");
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
lines.push(
|
|
527
|
-
` ${pc.dim("Share score:")} ${pc.cyan(generateShareUrl(summary))}`
|
|
528
|
-
);
|
|
529
|
-
lines.push("");
|
|
530
|
-
return lines.join("\n");
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/index.ts
|
|
534
|
-
async function roastRepository(options = {}) {
|
|
535
|
-
const cwd = path.resolve(options.cwd || process.cwd());
|
|
536
|
-
const since = options.since || "90 days ago";
|
|
537
|
-
const repoName = path.basename(cwd);
|
|
538
|
-
const collector = new MetricsCollector({ all: options.all });
|
|
539
|
-
const gitStream = streamGitLog(cwd, since);
|
|
540
|
-
await parseGitLogStream(gitStream, (commit) => {
|
|
541
|
-
collector.processCommit(commit);
|
|
542
|
-
});
|
|
543
|
-
const summary = collector.getSummary();
|
|
544
|
-
const output = formatTerminalCard(summary, {
|
|
545
|
-
repoName,
|
|
546
|
-
verbose: options.verbose
|
|
547
|
-
});
|
|
548
|
-
return { summary, output };
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
export {
|
|
552
|
-
detectAiAttribution,
|
|
553
|
-
parseGitLogStream,
|
|
554
|
-
streamGitLog,
|
|
555
|
-
stripStringAndRegexLiterals,
|
|
556
|
-
MetricsCollector,
|
|
557
|
-
generateShareUrl,
|
|
558
|
-
formatTerminalCard,
|
|
559
|
-
roastRepository
|
|
560
|
-
};
|