devrage 0.5.8 → 0.6.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.
Files changed (36) hide show
  1. package/dist/cli.js +468 -55
  2. package/dist/cli.js.map +4 -4
  3. package/dist/lib/adapters/amp.d.ts.map +1 -1
  4. package/dist/lib/adapters/amp.js +2 -1
  5. package/dist/lib/adapters/amp.js.map +1 -1
  6. package/dist/lib/adapters/claude.d.ts.map +1 -1
  7. package/dist/lib/adapters/claude.js +25 -10
  8. package/dist/lib/adapters/claude.js.map +1 -1
  9. package/dist/lib/adapters/cline.d.ts.map +1 -1
  10. package/dist/lib/adapters/cline.js +2 -1
  11. package/dist/lib/adapters/cline.js.map +1 -1
  12. package/dist/lib/adapters/codex.d.ts.map +1 -1
  13. package/dist/lib/adapters/codex.js +20 -19
  14. package/dist/lib/adapters/codex.js.map +1 -1
  15. package/dist/lib/adapters/cursor.js +17 -12
  16. package/dist/lib/adapters/cursor.js.map +1 -1
  17. package/dist/lib/adapters/index.d.ts +3 -1
  18. package/dist/lib/adapters/index.d.ts.map +1 -1
  19. package/dist/lib/adapters/index.js.map +1 -1
  20. package/dist/lib/adapters/opencode.js +7 -7
  21. package/dist/lib/adapters/opencode.js.map +1 -1
  22. package/dist/lib/adapters/pi.js +7 -2
  23. package/dist/lib/adapters/pi.js.map +1 -1
  24. package/dist/lib/adapters/t3code.js +4 -4
  25. package/dist/lib/adapters/t3code.js.map +1 -1
  26. package/dist/lib/adapters/zed.js +6 -5
  27. package/dist/lib/adapters/zed.js.map +1 -1
  28. package/dist/lib/index.d.ts +2 -1
  29. package/dist/lib/index.d.ts.map +1 -1
  30. package/dist/lib/index.js +1 -0
  31. package/dist/lib/index.js.map +1 -1
  32. package/dist/lib/slop/index.d.ts +19 -0
  33. package/dist/lib/slop/index.d.ts.map +1 -0
  34. package/dist/lib/slop/index.js +271 -0
  35. package/dist/lib/slop/index.js.map +1 -0
  36. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ function ampAdapter() {
17
17
  name: "amp",
18
18
  async *messages(options) {
19
19
  const threadsDir = getAmpThreadsDir();
20
+ const role = options?.role ?? "user";
20
21
  let files;
21
22
  try {
22
23
  files = await readdir(threadsDir);
@@ -36,7 +37,7 @@ function ampAdapter() {
36
37
  continue;
37
38
  }
38
39
  for (const msg of thread.messages) {
39
- if (msg.role !== "user") {
40
+ if (msg.role !== role) {
40
41
  continue;
41
42
  }
42
43
  const text = extractText(msg.content);
@@ -240,7 +241,11 @@ function claudeAdapter() {
240
241
  name: "claude",
241
242
  async *messages(options) {
242
243
  for await (const file of discoverClaudeJsonlFiles()) {
243
- yield* parseClaudeJsonl(file.filePath, { ...file, since: options?.since });
244
+ yield* parseClaudeJsonl(file.filePath, {
245
+ ...file,
246
+ role: options?.role ?? "user",
247
+ since: options?.since
248
+ });
244
249
  }
245
250
  },
246
251
  async *usage(options) {
@@ -295,13 +300,14 @@ async function* parseClaudeJsonl(filePath, context) {
295
300
  input: createReadStream(filePath, { encoding: "utf-8" }),
296
301
  crlfDelay: Infinity
297
302
  });
303
+ const seenAssistantRows = /* @__PURE__ */ new Set();
298
304
  for await (const line of rl) {
299
305
  if (!line.trim()) {
300
306
  continue;
301
307
  }
302
308
  try {
303
309
  const entry = JSON.parse(line);
304
- const text = extractUserText(entry);
310
+ const text = extractMessageText(entry, context.role);
305
311
  if (!text) {
306
312
  continue;
307
313
  }
@@ -312,6 +318,15 @@ async function* parseClaudeJsonl(filePath, context) {
312
318
  continue;
313
319
  }
314
320
  }
321
+ if (context.role === "assistant") {
322
+ const message = asRecord2(entry["message"]);
323
+ const messageId = stringValue(message?.["id"]) ?? stringValue(entry["uuid"]);
324
+ const dedupeKey = `${messageId ?? timestamp ?? ""}\0${text}`;
325
+ if (seenAssistantRows.has(dedupeKey)) {
326
+ continue;
327
+ }
328
+ seenAssistantRows.add(dedupeKey);
329
+ }
315
330
  yield {
316
331
  text,
317
332
  timestamp: timestamp ?? void 0,
@@ -322,22 +337,22 @@ async function* parseClaudeJsonl(filePath, context) {
322
337
  }
323
338
  }
324
339
  }
325
- function extractUserText(entry) {
326
- if (entry["type"] === "user") {
327
- const message = entry["message"];
340
+ function extractMessageText(entry, role) {
341
+ if (entry["type"] === role) {
342
+ const message = asRecord2(entry["message"]);
328
343
  if (!message) {
329
344
  return null;
330
345
  }
331
346
  return contentToString(message["content"]);
332
347
  }
333
- if (entry["type"] === "human") {
334
- const message = entry["message"];
348
+ if (role === "user" && entry["type"] === "human") {
349
+ const message = asRecord2(entry["message"]);
335
350
  if (!message) {
336
351
  return null;
337
352
  }
338
353
  return contentToString(message["content"]);
339
354
  }
340
- if (entry["role"] === "user") {
355
+ if (entry["role"] === role) {
341
356
  return contentToString(entry["content"]);
342
357
  }
343
358
  return null;
@@ -493,6 +508,7 @@ function clineAdapter() {
493
508
  name: "cline",
494
509
  async *messages(options) {
495
510
  const taskDirs = getClineTaskDirs();
511
+ const role = options?.role ?? "user";
496
512
  for (const tasksDir of taskDirs) {
497
513
  let taskIds;
498
514
  try {
@@ -514,7 +530,7 @@ function clineAdapter() {
514
530
  continue;
515
531
  }
516
532
  for (const msg of messages) {
517
- if (msg.role !== "user") {
533
+ if (msg.role !== role) {
518
534
  continue;
519
535
  }
520
536
  const text = extractText2(msg.content);
@@ -565,7 +581,11 @@ function codexAdapter() {
565
581
  name: "codex",
566
582
  async *messages(options) {
567
583
  for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
568
- yield* parseCodexJsonl(file.filePath, { session: file.session, since: options?.since });
584
+ yield* parseCodexJsonl(file.filePath, {
585
+ session: file.session,
586
+ role: options?.role ?? "user",
587
+ since: options?.since
588
+ });
569
589
  }
570
590
  },
571
591
  async *usage(options) {
@@ -623,18 +643,17 @@ async function* parseCodexJsonl(filePath, context) {
623
643
  continue;
624
644
  }
625
645
  const payload = entry.payload;
626
- if (!payload || payload.role !== "user") {
646
+ if (!payload || payload.role !== context.role) {
627
647
  continue;
628
648
  }
629
- const text = extractText3(payload.content);
649
+ const text = extractText3(payload.content, context.role);
630
650
  if (!text) {
631
651
  continue;
632
652
  }
633
- if (text.startsWith("<environment_context>")) {
634
- continue;
635
- }
636
- if (text.startsWith("<permissions instructions>")) {
637
- continue;
653
+ if (context.role === "user") {
654
+ if (text.startsWith("<environment_context>") || text.startsWith("<permissions instructions>")) {
655
+ continue;
656
+ }
638
657
  }
639
658
  if (context.since && entry.timestamp) {
640
659
  const ts = new Date(entry.timestamp);
@@ -651,12 +670,13 @@ async function* parseCodexJsonl(filePath, context) {
651
670
  }
652
671
  }
653
672
  }
654
- function extractText3(content) {
673
+ function extractText3(content, role) {
655
674
  if (!Array.isArray(content)) {
656
675
  return null;
657
676
  }
677
+ const textTypes = role === "assistant" ? /* @__PURE__ */ new Set(["output_text", "text"]) : /* @__PURE__ */ new Set(["input_text"]);
658
678
  const parts = content.filter(
659
- (p) => typeof p === "object" && p !== null && p.type === "input_text" && typeof p.text === "string"
679
+ (p) => typeof p === "object" && p !== null && textTypes.has(p.type) && typeof p.text === "string"
660
680
  ).map((p) => p.text);
661
681
  return parts.length > 0 ? parts.join(" ") : null;
662
682
  }
@@ -1036,7 +1056,7 @@ async function* parseCursorStore(store, options) {
1036
1056
  if (parsed === void 0) {
1037
1057
  continue;
1038
1058
  }
1039
- for (const message of extractCursorMessages(parsed, row.key)) {
1059
+ for (const message of extractCursorMessages(parsed, row.key, options?.role ?? "user")) {
1040
1060
  const text = message.text.trim();
1041
1061
  if (!isLikelyMessageText(text)) {
1042
1062
  continue;
@@ -1177,21 +1197,22 @@ function decodeStateValue(value) {
1177
1197
  }
1178
1198
  return null;
1179
1199
  }
1180
- function extractCursorMessages(root, rowKey) {
1200
+ function extractCursorMessages(root, rowKey, role) {
1181
1201
  if (rowKey.startsWith("bubbleId:")) {
1182
- const message = extractCursorBubbleMessage(root, rowKey);
1202
+ const message = extractCursorBubbleMessage(root, rowKey, role);
1183
1203
  return message ? [message] : [];
1184
1204
  }
1185
1205
  const messages = [];
1186
- collectRoleMessages(root, messages);
1187
- if (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations")) {
1206
+ collectRoleMessages(root, messages, role);
1207
+ if (role === "user" && (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations"))) {
1188
1208
  collectPromptMessages(root, messages);
1189
1209
  }
1190
1210
  return uniqueMessages(messages);
1191
1211
  }
1192
- function extractCursorBubbleMessage(root, rowKey) {
1212
+ function extractCursorBubbleMessage(root, rowKey, role) {
1193
1213
  const record = asRecord4(root);
1194
- if (!record || numberValue3(record["type"]) !== 1) {
1214
+ const expectedType = role === "user" ? 1 : 2;
1215
+ if (!record || numberValue3(record["type"]) !== expectedType) {
1195
1216
  return null;
1196
1217
  }
1197
1218
  const text = firstTextField(record, ["text", "richText"]);
@@ -1287,13 +1308,13 @@ function cursorBubbleSession(rowKey) {
1287
1308
  const [, composerId] = rowKey.split(":");
1288
1309
  return composerId?.trim() || void 0;
1289
1310
  }
1290
- function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
1311
+ function collectRoleMessages(value, messages, role, inheritedSession, depth = 0) {
1291
1312
  if (depth > 12) {
1292
1313
  return;
1293
1314
  }
1294
1315
  if (Array.isArray(value)) {
1295
1316
  for (const item of value) {
1296
- collectRoleMessages(item, messages, inheritedSession, depth + 1);
1317
+ collectRoleMessages(item, messages, role, inheritedSession, depth + 1);
1297
1318
  }
1298
1319
  return;
1299
1320
  }
@@ -1302,15 +1323,15 @@ function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
1302
1323
  return;
1303
1324
  }
1304
1325
  const session = extractSession(record) ?? inheritedSession;
1305
- if (isUserAuthored(record)) {
1306
- const text = extractMessageText(record);
1326
+ if (isAuthoredBy(record, role)) {
1327
+ const text = extractMessageText2(record);
1307
1328
  if (text) {
1308
1329
  messages.push({ text, timestamp: extractTimestamp2(record), session });
1309
1330
  }
1310
1331
  }
1311
1332
  for (const child of Object.values(record)) {
1312
1333
  if (typeof child === "object" && child !== null) {
1313
- collectRoleMessages(child, messages, session, depth + 1);
1334
+ collectRoleMessages(child, messages, role, session, depth + 1);
1314
1335
  }
1315
1336
  }
1316
1337
  }
@@ -1350,6 +1371,9 @@ function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
1350
1371
  }
1351
1372
  }
1352
1373
  }
1374
+ function isAuthoredBy(record, role) {
1375
+ return role === "user" ? isUserAuthored(record) : isAssistantAuthored(record);
1376
+ }
1353
1377
  function isUserAuthored(record) {
1354
1378
  return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
1355
1379
  (field) => actorIsUser(record[field])
@@ -1383,7 +1407,7 @@ function actorString(value) {
1383
1407
  }
1384
1408
  return null;
1385
1409
  }
1386
- function extractMessageText(record) {
1410
+ function extractMessageText2(record) {
1387
1411
  return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
1388
1412
  }
1389
1413
  function firstTextField(record, fields) {
@@ -1514,7 +1538,7 @@ function opencodeAdapter() {
1514
1538
  return;
1515
1539
  }
1516
1540
  try {
1517
- yield* queryUserMessages(db, options);
1541
+ yield* queryMessages(db, options);
1518
1542
  } finally {
1519
1543
  db.close();
1520
1544
  }
@@ -1543,7 +1567,7 @@ async function openOpencodeDb() {
1543
1567
  }
1544
1568
  return db;
1545
1569
  }
1546
- function* queryUserMessages(db, options) {
1570
+ function* queryMessages(db, options) {
1547
1571
  let query = `
1548
1572
  SELECT
1549
1573
  m.session_id,
@@ -1551,10 +1575,10 @@ function* queryUserMessages(db, options) {
1551
1575
  json_extract(p.data, '$.text') as text
1552
1576
  FROM message m
1553
1577
  JOIN part p ON p.message_id = m.id
1554
- WHERE json_extract(m.data, '$.role') = 'user'
1578
+ WHERE json_extract(m.data, '$.role') = ?
1555
1579
  AND json_extract(p.data, '$.type') = 'text'
1556
1580
  `;
1557
- const params = [];
1581
+ const params = [options?.role ?? "user"];
1558
1582
  if (options?.since) {
1559
1583
  query += ` AND m.time_created >= ?`;
1560
1584
  params.push(options.since.getTime());
@@ -1662,7 +1686,12 @@ async function* walkPiSessions(dir, options, project) {
1662
1686
  yield* walkPiSessions(fullPath, options, project ?? entry);
1663
1687
  } else if (entry.endsWith(".jsonl")) {
1664
1688
  const session = entry.replace(".jsonl", "");
1665
- yield* parsePiJsonl(fullPath, { session, project, since: options?.since });
1689
+ yield* parsePiJsonl(fullPath, {
1690
+ session,
1691
+ project,
1692
+ role: options?.role ?? "user",
1693
+ since: options?.since
1694
+ });
1666
1695
  }
1667
1696
  }
1668
1697
  }
@@ -1707,7 +1736,7 @@ async function* parsePiJsonl(filePath, context) {
1707
1736
  continue;
1708
1737
  }
1709
1738
  const message = entry.message;
1710
- if (!message || message.role !== "user") {
1739
+ if (!message || message.role !== context.role) {
1711
1740
  continue;
1712
1741
  }
1713
1742
  const text = contentToString2(message.content);
@@ -1824,7 +1853,7 @@ function t3codeAdapter() {
1824
1853
  continue;
1825
1854
  }
1826
1855
  try {
1827
- yield* queryUserMessages2(db, location, options);
1856
+ yield* queryMessages2(db, location, options);
1828
1857
  } finally {
1829
1858
  db.close();
1830
1859
  }
@@ -1886,7 +1915,7 @@ function resolveHomePath(value) {
1886
1915
  async function openT3Db(dbPath) {
1887
1916
  return openReadonlySqliteDatabase(dbPath);
1888
1917
  }
1889
- function* queryUserMessages2(db, location, options) {
1918
+ function* queryMessages2(db, location, options) {
1890
1919
  if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
1891
1920
  return;
1892
1921
  }
@@ -1894,9 +1923,9 @@ function* queryUserMessages2(db, location, options) {
1894
1923
  let query = `
1895
1924
  SELECT thread_id, created_at, text
1896
1925
  FROM projection_thread_messages
1897
- WHERE role = 'user'
1926
+ WHERE role = ?
1898
1927
  `;
1899
- const params = [];
1928
+ const params = [options?.role ?? "user"];
1900
1929
  if (options?.since) {
1901
1930
  query += ` AND created_at >= ?`;
1902
1931
  params.push(options.since.toISOString());
@@ -2251,7 +2280,7 @@ function zedAdapter() {
2251
2280
  }
2252
2281
  };
2253
2282
  }
2254
- async function* parseTextThreads(dir, _options) {
2283
+ async function* parseTextThreads(dir, options) {
2255
2284
  if (!existsSync5(dir)) {
2256
2285
  return;
2257
2286
  }
@@ -2271,8 +2300,9 @@ async function* parseTextThreads(dir, _options) {
2271
2300
  if (!conversation.messages || !Array.isArray(conversation.messages)) {
2272
2301
  continue;
2273
2302
  }
2303
+ const role = options?.role ?? "user";
2274
2304
  for (const msg of conversation.messages) {
2275
- if (msg.role !== "user") {
2305
+ if (msg.role !== role) {
2276
2306
  continue;
2277
2307
  }
2278
2308
  const text = typeof msg.content === "string" ? msg.content : null;
@@ -2288,7 +2318,7 @@ async function* parseTextThreads(dir, _options) {
2288
2318
  }
2289
2319
  }
2290
2320
  }
2291
- async function* parseAgentThreads(dbDir, _options) {
2321
+ async function* parseAgentThreads(dbDir, options) {
2292
2322
  if (!existsSync5(dbDir)) {
2293
2323
  return;
2294
2324
  }
@@ -2324,8 +2354,8 @@ async function* parseAgentThreads(dbDir, _options) {
2324
2354
  continue;
2325
2355
  }
2326
2356
  const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
2327
- let query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = 'user'`;
2328
- const rows = db.prepare(query).all();
2357
+ const query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = ?`;
2358
+ const rows = db.prepare(query).all(options?.role ?? "user");
2329
2359
  for (const row of rows) {
2330
2360
  if (!row.text?.trim()) {
2331
2361
  continue;
@@ -2960,6 +2990,261 @@ function asRecord7(value) {
2960
2990
  return value;
2961
2991
  }
2962
2992
 
2993
+ // src/slop/index.ts
2994
+ var SLOP_SIGNALS = [
2995
+ // Claude's especially recognizable coding-agent voice.
2996
+ {
2997
+ tell: "load-bearing",
2998
+ category: "claude-ism",
2999
+ pattern: /\bload(?:[-‐‑‒–—\s]+)bearing\b/giu
3000
+ },
3001
+ {
3002
+ tell: "honest take",
3003
+ category: "claude-ism",
3004
+ pattern: /\bhonest\s+(?:take|evaluation|assessment)\b/giu
3005
+ },
3006
+ {
3007
+ tell: "precise mechanism",
3008
+ category: "claude-ism",
3009
+ pattern: /\b(?:the\s+)?precise\s+mechanism\b/giu
3010
+ },
3011
+ {
3012
+ tell: "and that matters",
3013
+ category: "claude-ism",
3014
+ pattern: /\band\s+that\s+matters\b/giu
3015
+ },
3016
+ {
3017
+ tell: "key insight",
3018
+ category: "claude-ism",
3019
+ pattern: /\b(?:the\s+)?key\s+insight\b/giu
3020
+ },
3021
+ {
3022
+ tell: "without ceremony",
3023
+ category: "claude-ism",
3024
+ pattern: /\b(?:without|with\s+no|needs?\s+no|requires?\s+no|no)\s+(?:additional\s+)?ceremony\b/giu
3025
+ },
3026
+ {
3027
+ tell: "earns its keep",
3028
+ category: "claude-ism",
3029
+ pattern: /\bearn(?:s|ed|ing)?\s+(?:its|their|the)\s+keep\b/giu
3030
+ },
3031
+ {
3032
+ tell: "keeps X honest",
3033
+ category: "claude-ism",
3034
+ pattern: /\b(?:keep|keeps|keeping|kept)\s+(?:(?:this|that|us|me|things?)|(?:the|our|your|my)\s+[\w-]+(?:\s+[\w-]+){0,2})\s+honest\b/giu
3035
+ },
3036
+ {
3037
+ tell: "belt-and-suspenders",
3038
+ category: "claude-ism",
3039
+ pattern: /\bbelt(?:[-‐‑‒–—\s]+)and(?:[-‐‑‒–—\s]+)suspenders\b/giu
3040
+ },
3041
+ {
3042
+ tell: "soup to nuts",
3043
+ category: "claude-ism",
3044
+ pattern: /\b(?:from\s+)?soup(?:[-‐‑‒–—\s]+)to(?:[-‐‑‒–—\s]+)nuts\b/giu
3045
+ },
3046
+ {
3047
+ tell: "architectural seam",
3048
+ category: "claude-ism",
3049
+ pattern: /\b(?:(?:clean|natural|architectural|implementation|integration)\s+seam|(?:at|across)\s+the\s+seam\s+between)\b/giu
3050
+ },
3051
+ // The apology/validation loop developers see after correcting an agent.
3052
+ {
3053
+ tell: "you're absolutely right",
3054
+ category: "sycophancy",
3055
+ pattern: /\byou(?:['’]re|\s+are)\s+absolutely\s+right\b/giu
3056
+ },
3057
+ {
3058
+ tell: "you're right to call that out",
3059
+ category: "sycophancy",
3060
+ pattern: /\byou(?:['’]re|\s+are)\s+right\s+to\s+(?:call|point|flag)\s+(?:me|that|this)\s+out\b/giu
3061
+ },
3062
+ {
3063
+ tell: "real gap",
3064
+ category: "sycophancy",
3065
+ pattern: /\b(?:a\s+)?real\s+gap\b/giu
3066
+ },
3067
+ {
3068
+ tell: "error in my framing",
3069
+ category: "sycophancy",
3070
+ pattern: /\b(?:a\s+)?real\s+error\s+in\s+(?:my|the)\s+framing\b/giu
3071
+ },
3072
+ {
3073
+ tell: "I was wrong",
3074
+ category: "sycophancy",
3075
+ pattern: /\bi\s+was\s+wrong\b/giu
3076
+ },
3077
+ {
3078
+ tell: "I overcomplicated it",
3079
+ category: "sycophancy",
3080
+ pattern: /\bi\s+(?:overcomplicated|over-engineered|overthought)\s+(?:this|that|it)\b/giu
3081
+ },
3082
+ // Cross-model corporate/chatbot prose, constrained where a raw word is technical.
3083
+ { tell: "delve", category: "stock prose", pattern: /\b(?:delve(?:d|s)?|delving)\b/giu },
3084
+ { tell: "crucial", category: "stock prose", pattern: /\bcrucial\b/giu },
3085
+ { tell: "pivotal", category: "stock prose", pattern: /\bpivotal\b/giu },
3086
+ { tell: "tapestry", category: "stock prose", pattern: /\btapestr(?:y|ies)\b/giu },
3087
+ {
3088
+ tell: "here's the thing",
3089
+ category: "stock prose",
3090
+ pattern: /\bhere(?:['’]s|\s+is)\s+the\s+thing\b/giu
3091
+ },
3092
+ {
3093
+ tell: "hope this helps",
3094
+ category: "stock prose",
3095
+ pattern: /\bhope\s+(?:this|that)\s+helps\b/giu
3096
+ },
3097
+ {
3098
+ tell: "after careful consideration",
3099
+ category: "stock prose",
3100
+ pattern: /\bafter\s+careful\s+consideration\b/giu
3101
+ },
3102
+ {
3103
+ tell: "quick update",
3104
+ category: "stock prose",
3105
+ pattern: /\bto\s+provide\s+(?:you\s+with\s+)?a\s+quick\s+update\b/giu
3106
+ },
3107
+ {
3108
+ tell: "robust and reliable",
3109
+ category: "stock prose",
3110
+ pattern: /\brobust(?:\s+and|,)\s+reliable\b/giu
3111
+ },
3112
+ { tell: "seamless", category: "stock prose", pattern: /\bseamless(?:ly)?\b/giu },
3113
+ {
3114
+ tell: "in the realm of",
3115
+ category: "stock prose",
3116
+ pattern: /\b(?:in|within)\s+the\s+realm\s+of\b/giu
3117
+ },
3118
+ {
3119
+ tell: "important to note",
3120
+ category: "stock prose",
3121
+ pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?important\s+to\s+note\b/giu
3122
+ },
3123
+ {
3124
+ tell: "let me break this down",
3125
+ category: "stock prose",
3126
+ pattern: /\blet\s+(?:me|us)\s+break\s+(?:this|it)\s+down\b/giu
3127
+ },
3128
+ {
3129
+ tell: "let's dive in",
3130
+ category: "stock prose",
3131
+ pattern: /\blet(?:['’]s|\s+us)\s+dive\s+(?:in|into)\b/giu
3132
+ },
3133
+ {
3134
+ tell: "let's unpack this",
3135
+ category: "stock prose",
3136
+ pattern: /\blet(?:['’]s|\s+us)\s+unpack\s+(?:this|that|it)\b/giu
3137
+ },
3138
+ { tell: "at its core", category: "stock prose", pattern: /\bat\s+its\s+core\b/giu },
3139
+ {
3140
+ tell: "successfully implemented",
3141
+ category: "stock prose",
3142
+ pattern: /\bsuccessfully\s+(?:added|completed|created|fixed|implemented|resolved|updated)\b/giu
3143
+ },
3144
+ {
3145
+ tell: "Certainly.",
3146
+ category: "stock prose",
3147
+ pattern: /(?:^|\n)[\t ]*certainly\b/gimu
3148
+ },
3149
+ {
3150
+ tell: "I'd be happy to",
3151
+ category: "stock prose",
3152
+ pattern: /\bi(?:['’]d|\s+would)\s+be\s+happy\s+to\b/giu
3153
+ },
3154
+ // Corrective juxtaposition and fake-deep contrast templates.
3155
+ {
3156
+ tell: "it's not X, it's Y",
3157
+ category: "rhetorical template",
3158
+ pattern: /\b(?:it|this|that)(?:['’]s|\s+is)\s+not\s+[^.!?\n]{1,100}?(?:,|;|—)\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
3159
+ },
3160
+ {
3161
+ tell: "it's not X, it's Y",
3162
+ category: "rhetorical template",
3163
+ pattern: /\b(?:it|this|that)\s+isn(?:['’]t)\s+[^.!?\n]{1,100}[.!;—]\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
3164
+ },
3165
+ {
3166
+ tell: "not just X, but Y",
3167
+ category: "rhetorical template",
3168
+ pattern: /\bnot\s+just\s+[^.!?\n]{1,100}?,?\s+(?:but(?:\s+also)?|(?:it|this|that)(?:['’]s|\s+is))\s+[^.!?\n]{1,100}/giu
3169
+ }
3170
+ ];
3171
+ function detectSlop(text) {
3172
+ const prose = maskMarkdownCode(text);
3173
+ const candidates = SLOP_SIGNALS.flatMap((signal) => findSignalMatches(prose, signal));
3174
+ addCheckmarkWall(prose, candidates);
3175
+ const matches = longestNonOverlapping(candidates).map(({ end: _end, ...match }) => match);
3176
+ return { count: matches.length, matches };
3177
+ }
3178
+ function findSignalMatches(text, signal) {
3179
+ const matches = [];
3180
+ signal.pattern.lastIndex = 0;
3181
+ let match;
3182
+ while ((match = signal.pattern.exec(text)) !== null) {
3183
+ const bounds = trimmedBounds(match[0]);
3184
+ if (bounds.text) {
3185
+ const index = match.index + bounds.start;
3186
+ matches.push({
3187
+ tell: signal.tell,
3188
+ text: bounds.text,
3189
+ index,
3190
+ end: index + bounds.text.length,
3191
+ category: signal.category
3192
+ });
3193
+ }
3194
+ if (match[0].length === 0) {
3195
+ signal.pattern.lastIndex++;
3196
+ }
3197
+ }
3198
+ return matches;
3199
+ }
3200
+ function trimmedBounds(value) {
3201
+ const start = value.search(/\S/u);
3202
+ if (start === -1) {
3203
+ return { text: "", start: 0 };
3204
+ }
3205
+ return { text: value.trim(), start };
3206
+ }
3207
+ function maskMarkdownCode(text) {
3208
+ return text.replace(/(?:```|~~~)[\s\S]*?(?:(?:```|~~~)|$)/gu, preserveNewlines).replace(/`[^`\n]+`/gu, preserveNewlines);
3209
+ }
3210
+ function preserveNewlines(value) {
3211
+ return value.replace(/[^\n]/gu, " ");
3212
+ }
3213
+ function addCheckmarkWall(text, candidates) {
3214
+ const checkmarks = Array.from(text.matchAll(/^[\t ]*(?:[-*]\s*)?[✅✓]\s+/gmu));
3215
+ if (checkmarks.length < 3) {
3216
+ return;
3217
+ }
3218
+ const first = checkmarks[0];
3219
+ if (!first) {
3220
+ return;
3221
+ }
3222
+ const offset = first[0].search(/[✅✓]/u);
3223
+ const index = first.index + Math.max(offset, 0);
3224
+ candidates.push({
3225
+ tell: "checkmark wall",
3226
+ text: `\u2705 \xD7${checkmarks.length}`,
3227
+ index,
3228
+ end: index + 2,
3229
+ category: "formatting"
3230
+ });
3231
+ }
3232
+ function longestNonOverlapping(candidates) {
3233
+ const longestFirst = [...candidates].sort(
3234
+ (left, right) => right.end - right.index - (left.end - left.index) || left.index - right.index || left.tell.localeCompare(right.tell)
3235
+ );
3236
+ const accepted = [];
3237
+ for (const candidate of longestFirst) {
3238
+ const overlaps = accepted.some(
3239
+ (match) => candidate.index < match.end && candidate.end > match.index
3240
+ );
3241
+ if (!overlaps) {
3242
+ accepted.push(candidate);
3243
+ }
3244
+ }
3245
+ return accepted.sort((left, right) => left.index - right.index);
3246
+ }
3247
+
2963
3248
  // src/commands/scan.ts
2964
3249
  var c = {
2965
3250
  reset: "\x1B[0m",
@@ -2994,6 +3279,14 @@ var COST_SPINNER_MESSAGES = [
2994
3279
  "Crunching token counts",
2995
3280
  "Still working through local history"
2996
3281
  ];
3282
+ var SLOP_SPINNER_MESSAGES = [
3283
+ "Auditing agent prose",
3284
+ "Counting load-bearing insights",
3285
+ "Inspecting suspiciously honest takes",
3286
+ "Measuring corrective juxtaposition",
3287
+ "Checking what earned its keep",
3288
+ "Cataloging chatbot tics"
3289
+ ];
2997
3290
  var DAY_MS = 24 * 60 * 60 * 1e3;
2998
3291
  function createSpinner(messages = SPINNER_MESSAGES) {
2999
3292
  let messageIdx = 0;
@@ -3033,7 +3326,7 @@ function createSpinner(messages = SPINNER_MESSAGES) {
3033
3326
  }
3034
3327
  };
3035
3328
  }
3036
- function parseArgs(args) {
3329
+ function parseArgs(args, command = "scan") {
3037
3330
  const options = {};
3038
3331
  for (let i = 0; i < args.length; i++) {
3039
3332
  const arg = args[i];
@@ -3055,7 +3348,18 @@ function parseArgs(args) {
3055
3348
  } else if (arg === "--month") {
3056
3349
  setRelativeRange(options, 30);
3057
3350
  } else if (arg === "--help" || arg === "-h") {
3058
- console.log(`devrage scan \u2014 scan sessions for profanity
3351
+ if (command === "slop") {
3352
+ console.log(`devrage slop \u2014 scan coding-agent responses for AI-isms
3353
+
3354
+ Options:
3355
+ --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
3356
+ --since, -s <date> Only scan responses after this date (ISO 8601)
3357
+ --day, --days [n] Only scan the last n days (default: 1)
3358
+ --week Only scan the last 7 days
3359
+ --month Only scan the last 30 days
3360
+ --help, -h Show this help`);
3361
+ } else {
3362
+ console.log(`devrage scan \u2014 scan sessions for profanity
3059
3363
 
3060
3364
  Options:
3061
3365
  --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
@@ -3064,6 +3368,7 @@ Options:
3064
3368
  --week Only scan the last 7 days
3065
3369
  --month Only scan the last 30 days
3066
3370
  --help, -h Show this help`);
3371
+ }
3067
3372
  process.exit(0);
3068
3373
  }
3069
3374
  }
@@ -3208,6 +3513,98 @@ async function scan(args) {
3208
3513
  console.log("");
3209
3514
  }
3210
3515
  }
3516
+ async function slop(args) {
3517
+ const options = parseArgs(args, "slop");
3518
+ const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
3519
+ const spinner = createSpinner(SLOP_SPINNER_MESSAGES);
3520
+ const tellTally = {};
3521
+ const categoryTally = {};
3522
+ const perAgent = {};
3523
+ let totalMessages = 0;
3524
+ let totalHits = 0;
3525
+ let taintedMessages = 0;
3526
+ spinner.start();
3527
+ try {
3528
+ for (const adapter of adapters) {
3529
+ let agentMessages = 0;
3530
+ let agentHits = 0;
3531
+ let agentTainted = 0;
3532
+ spinner.update();
3533
+ for await (const message of adapter.messages({
3534
+ role: "assistant",
3535
+ since: options.since
3536
+ })) {
3537
+ totalMessages++;
3538
+ agentMessages++;
3539
+ const result = detectSlop(message.text);
3540
+ if (result.count === 0) {
3541
+ continue;
3542
+ }
3543
+ totalHits += result.count;
3544
+ agentHits += result.count;
3545
+ taintedMessages++;
3546
+ agentTainted++;
3547
+ for (const match of result.matches) {
3548
+ tellTally[match.tell] = (tellTally[match.tell] ?? 0) + 1;
3549
+ categoryTally[match.category] = (categoryTally[match.category] ?? 0) + 1;
3550
+ }
3551
+ }
3552
+ if (agentMessages > 0) {
3553
+ perAgent[adapter.name] = {
3554
+ messages: agentMessages,
3555
+ hits: agentHits,
3556
+ tainted: agentTainted
3557
+ };
3558
+ }
3559
+ }
3560
+ } finally {
3561
+ spinner.stop();
3562
+ }
3563
+ const activeAgents = Object.entries(perAgent);
3564
+ console.log("");
3565
+ printReportHeader(options, "slop");
3566
+ printSlopOverview(totalMessages, totalHits, taintedMessages);
3567
+ if (activeAgents.length > 1) {
3568
+ console.log("");
3569
+ console.log(` ${sectionTitle("agent slop")}`);
3570
+ for (const [name, stats] of activeAgents) {
3571
+ const rate = stats.messages > 0 ? stats.tainted / stats.messages : 0;
3572
+ console.log(
3573
+ ` ${colorText(name.padEnd(10), agentColor(name))} ${c.bold}${String(stats.hits).padStart(4)}${c.reset} ${c.dim}hits \xB7 ${stats.tainted}/${stats.messages} messages (${formatPercent(rate)})${c.reset}`
3574
+ );
3575
+ }
3576
+ }
3577
+ if (totalHits > 0) {
3578
+ const tells = Object.entries(tellTally).sort(
3579
+ ([left, leftCount], [right, rightCount]) => rightCount - leftCount || left.localeCompare(right)
3580
+ );
3581
+ console.log("");
3582
+ console.log(` ${sectionTitle("top tells")}`);
3583
+ for (const [tell, count] of tells.slice(0, 15)) {
3584
+ console.log(
3585
+ ` ${c.yellow}${tell.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
3586
+ );
3587
+ }
3588
+ const categories = Object.entries(categoryTally).sort(
3589
+ ([left, leftCount], [right, rightCount]) => (rightCount ?? 0) - (leftCount ?? 0) || left.localeCompare(right)
3590
+ );
3591
+ console.log("");
3592
+ console.log(` ${sectionTitle("slop flavors")}`);
3593
+ for (const [category, count] of categories) {
3594
+ console.log(
3595
+ ` ${c.cyan}${category.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
3596
+ );
3597
+ }
3598
+ }
3599
+ console.log("");
3600
+ if (totalMessages === 0) {
3601
+ console.log(` ${c.gray}no assistant messages found.${c.reset}`);
3602
+ console.log("");
3603
+ } else if (totalHits === 0) {
3604
+ console.log(` ${c.green}suspiciously human. not a single slop tell found.${c.reset}`);
3605
+ console.log("");
3606
+ }
3607
+ }
3211
3608
  async function cost(args) {
3212
3609
  const options = parseCostArgs(args);
3213
3610
  const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
@@ -3601,13 +3998,25 @@ function renderCostHtmlReport(data) {
3601
3998
  function jsonForScript(value) {
3602
3999
  return JSON.stringify(value).replace(/</g, "\\u003c");
3603
4000
  }
3604
- function printReportHeader(options) {
4001
+ function printReportHeader(options, label = "report") {
3605
4002
  const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
3606
4003
  const agent = options.agent ? ` \xB7 ${options.agent}` : "";
3607
- console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
4004
+ console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}${label}${c.reset}`);
3608
4005
  console.log(` ${c.dim}${scope}${agent}${c.reset}`);
3609
4006
  console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
3610
4007
  }
4008
+ function printSlopOverview(totalMessages, totalHits, taintedMessages) {
4009
+ const rate = totalMessages > 0 ? taintedMessages / totalMessages : 0;
4010
+ console.log(
4011
+ ` ${c.dim}assistant messages${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
4012
+ );
4013
+ console.log(
4014
+ ` ${c.dim}slop hits${c.reset} ${c.bold}${c.yellow}${formatNumber(totalHits)}${c.reset}`
4015
+ );
4016
+ console.log(
4017
+ ` ${c.dim}messages with slop${c.reset} ${c.bold}${formatNumber(taintedMessages)}${c.reset} ${c.dim}(${formatPercent(rate)})${c.reset}`
4018
+ );
4019
+ }
3611
4020
  function printBasicOverview(totalMessages, totalSwears) {
3612
4021
  console.log(
3613
4022
  ` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
@@ -3735,11 +4144,12 @@ function formatDate(value) {
3735
4144
  // src/cli.ts
3736
4145
  var COMMANDS = {
3737
4146
  cost,
3738
- scan
4147
+ scan,
4148
+ slop
3739
4149
  };
3740
4150
  var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
3741
4151
  function usage() {
3742
- console.log(`devrage \u2014 count how many times you swear at your coding agents
4152
+ console.log(`devrage \u2014 measure rage, cost, and AI slop in coding-agent sessions
3743
4153
 
3744
4154
  Usage:
3745
4155
  devrage <command> [options]
@@ -3747,6 +4157,7 @@ Usage:
3747
4157
  Commands:
3748
4158
  cost Show API-equivalent coding agent cost
3749
4159
  scan Scan sessions for profanity
4160
+ slop Scan agent responses for common AI-isms
3750
4161
 
3751
4162
  Options:
3752
4163
  --help, -h Show this help message
@@ -3755,7 +4166,9 @@ Options:
3755
4166
  Examples:
3756
4167
  devrage cost
3757
4168
  devrage scan
4169
+ devrage slop
3758
4170
  devrage scan --agent claude
4171
+ devrage slop --agent codex
3759
4172
  devrage scan --since 2025-01-01`);
3760
4173
  }
3761
4174
  async function main() {
@@ -3766,7 +4179,7 @@ async function main() {
3766
4179
  process.exit(0);
3767
4180
  }
3768
4181
  if (command === "--version") {
3769
- console.log("0.5.8");
4182
+ console.log("0.6.1");
3770
4183
  process.exit(0);
3771
4184
  }
3772
4185
  const parsed = parseCommand(args);