devrage 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli.js +488 -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 +291 -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,281 @@ 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: "good question",
3064
+ category: "sycophancy",
3065
+ pattern: /\b(?:(?:that(?:['’]s|\s+is)|this\s+is)\s+)?(?:a\s+)?(?:good|great|excellent)\s+question\b/giu
3066
+ },
3067
+ {
3068
+ tell: "good catch",
3069
+ category: "sycophancy",
3070
+ pattern: /\b(?:(?:that(?:['’]s|\s+is)|this\s+is)\s+)?(?:a\s+)?(?:good|great|excellent)\s+catch\b/giu
3071
+ },
3072
+ {
3073
+ tell: "real gap",
3074
+ category: "sycophancy",
3075
+ pattern: /\b(?:a\s+)?real\s+gap\b/giu
3076
+ },
3077
+ {
3078
+ tell: "error in my framing",
3079
+ category: "sycophancy",
3080
+ pattern: /\b(?:a\s+)?real\s+error\s+in\s+(?:my|the)\s+framing\b/giu
3081
+ },
3082
+ {
3083
+ tell: "I was wrong",
3084
+ category: "sycophancy",
3085
+ pattern: /\bi\s+was\s+wrong\b/giu
3086
+ },
3087
+ {
3088
+ tell: "I overcomplicated it",
3089
+ category: "sycophancy",
3090
+ pattern: /\bi\s+(?:overcomplicated|over-engineered|overthought)\s+(?:this|that|it)\b/giu
3091
+ },
3092
+ // Cross-model corporate/chatbot prose, constrained where a raw word is technical.
3093
+ { tell: "delve", category: "stock prose", pattern: /\b(?:delve(?:d|s)?|delving)\b/giu },
3094
+ { tell: "crucial", category: "stock prose", pattern: /\bcrucial\b/giu },
3095
+ { tell: "pivotal", category: "stock prose", pattern: /\bpivotal\b/giu },
3096
+ { tell: "tapestry", category: "stock prose", pattern: /\btapestr(?:y|ies)\b/giu },
3097
+ {
3098
+ tell: "here's the thing",
3099
+ category: "stock prose",
3100
+ pattern: /\bhere(?:['’]s|\s+is)\s+the\s+thing\b/giu
3101
+ },
3102
+ {
3103
+ tell: "hope this helps",
3104
+ category: "stock prose",
3105
+ pattern: /\bhope\s+(?:this|that)\s+helps\b/giu
3106
+ },
3107
+ {
3108
+ tell: "after careful consideration",
3109
+ category: "stock prose",
3110
+ pattern: /\bafter\s+careful\s+consideration\b/giu
3111
+ },
3112
+ {
3113
+ tell: "quick update",
3114
+ category: "stock prose",
3115
+ pattern: /\bto\s+provide\s+(?:you\s+with\s+)?a\s+quick\s+update\b/giu
3116
+ },
3117
+ {
3118
+ tell: "robust and reliable",
3119
+ category: "stock prose",
3120
+ pattern: /\brobust(?:\s+and|,)\s+reliable\b/giu
3121
+ },
3122
+ { tell: "seamless", category: "stock prose", pattern: /\bseamless(?:ly)?\b/giu },
3123
+ {
3124
+ tell: "in the realm of",
3125
+ category: "stock prose",
3126
+ pattern: /\b(?:in|within)\s+the\s+realm\s+of\b/giu
3127
+ },
3128
+ {
3129
+ tell: "important to note",
3130
+ category: "stock prose",
3131
+ pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?important\s+to\s+note\b/giu
3132
+ },
3133
+ {
3134
+ tell: "worth noting",
3135
+ category: "stock prose",
3136
+ pattern: /\b(?:(?:it\s+is|it(?:['’]s))\s+)?worth\s+noting\b/giu
3137
+ },
3138
+ {
3139
+ tell: "let me break this down",
3140
+ category: "stock prose",
3141
+ pattern: /\blet\s+(?:me|us)\s+break\s+(?:this|it)\s+down\b/giu
3142
+ },
3143
+ {
3144
+ tell: "let's dive in",
3145
+ category: "stock prose",
3146
+ pattern: /\blet(?:['’]s|\s+us)\s+dive\s+(?:in|into)\b/giu
3147
+ },
3148
+ {
3149
+ tell: "let's unpack this",
3150
+ category: "stock prose",
3151
+ pattern: /\blet(?:['’]s|\s+us)\s+unpack\s+(?:this|that|it)\b/giu
3152
+ },
3153
+ { tell: "at its core", category: "stock prose", pattern: /\bat\s+its\s+core\b/giu },
3154
+ {
3155
+ tell: "comprehensive",
3156
+ category: "stock prose",
3157
+ pattern: /\bcomprehensive\s+(?:analysis|approach|coverage|error\s+handling|guide|implementation|overview|solution|suite)\b/giu
3158
+ },
3159
+ {
3160
+ tell: "successfully implemented",
3161
+ category: "stock prose",
3162
+ pattern: /\bsuccessfully\s+(?:added|completed|created|fixed|implemented|resolved|updated)\b/giu
3163
+ },
3164
+ {
3165
+ tell: "Certainly.",
3166
+ category: "stock prose",
3167
+ pattern: /(?:^|\n)[\t ]*certainly\b/gimu
3168
+ },
3169
+ {
3170
+ tell: "I'd be happy to",
3171
+ category: "stock prose",
3172
+ pattern: /\bi(?:['’]d|\s+would)\s+be\s+happy\s+to\b/giu
3173
+ },
3174
+ // Corrective juxtaposition and fake-deep contrast templates.
3175
+ {
3176
+ tell: "it's not X, it's Y",
3177
+ category: "rhetorical template",
3178
+ 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
3179
+ },
3180
+ {
3181
+ tell: "it's not X, it's Y",
3182
+ category: "rhetorical template",
3183
+ pattern: /\b(?:it|this|that)\s+isn(?:['’]t)\s+[^.!?\n]{1,100}[.!;—]\s*(?:it|this|that)(?:['’]s|\s+is)\s+[^.!?\n]{1,100}/giu
3184
+ },
3185
+ {
3186
+ tell: "not just X, but Y",
3187
+ category: "rhetorical template",
3188
+ pattern: /\bnot\s+just\s+[^.!?\n]{1,100}?,?\s+(?:but(?:\s+also)?|(?:it|this|that)(?:['’]s|\s+is))\s+[^.!?\n]{1,100}/giu
3189
+ }
3190
+ ];
3191
+ function detectSlop(text) {
3192
+ const prose = maskMarkdownCode(text);
3193
+ const candidates = SLOP_SIGNALS.flatMap((signal) => findSignalMatches(prose, signal));
3194
+ addCheckmarkWall(prose, candidates);
3195
+ const matches = longestNonOverlapping(candidates).map(({ end: _end, ...match }) => match);
3196
+ return { count: matches.length, matches };
3197
+ }
3198
+ function findSignalMatches(text, signal) {
3199
+ const matches = [];
3200
+ signal.pattern.lastIndex = 0;
3201
+ let match;
3202
+ while ((match = signal.pattern.exec(text)) !== null) {
3203
+ const bounds = trimmedBounds(match[0]);
3204
+ if (bounds.text) {
3205
+ const index = match.index + bounds.start;
3206
+ matches.push({
3207
+ tell: signal.tell,
3208
+ text: bounds.text,
3209
+ index,
3210
+ end: index + bounds.text.length,
3211
+ category: signal.category
3212
+ });
3213
+ }
3214
+ if (match[0].length === 0) {
3215
+ signal.pattern.lastIndex++;
3216
+ }
3217
+ }
3218
+ return matches;
3219
+ }
3220
+ function trimmedBounds(value) {
3221
+ const start = value.search(/\S/u);
3222
+ if (start === -1) {
3223
+ return { text: "", start: 0 };
3224
+ }
3225
+ return { text: value.trim(), start };
3226
+ }
3227
+ function maskMarkdownCode(text) {
3228
+ return text.replace(/(?:```|~~~)[\s\S]*?(?:(?:```|~~~)|$)/gu, preserveNewlines).replace(/`[^`\n]+`/gu, preserveNewlines);
3229
+ }
3230
+ function preserveNewlines(value) {
3231
+ return value.replace(/[^\n]/gu, " ");
3232
+ }
3233
+ function addCheckmarkWall(text, candidates) {
3234
+ const checkmarks = Array.from(text.matchAll(/^[\t ]*(?:[-*]\s*)?[✅✓]\s+/gmu));
3235
+ if (checkmarks.length < 3) {
3236
+ return;
3237
+ }
3238
+ const first = checkmarks[0];
3239
+ if (!first) {
3240
+ return;
3241
+ }
3242
+ const offset = first[0].search(/[✅✓]/u);
3243
+ const index = first.index + Math.max(offset, 0);
3244
+ candidates.push({
3245
+ tell: "checkmark wall",
3246
+ text: `\u2705 \xD7${checkmarks.length}`,
3247
+ index,
3248
+ end: index + 2,
3249
+ category: "formatting"
3250
+ });
3251
+ }
3252
+ function longestNonOverlapping(candidates) {
3253
+ const longestFirst = [...candidates].sort(
3254
+ (left, right) => right.end - right.index - (left.end - left.index) || left.index - right.index || left.tell.localeCompare(right.tell)
3255
+ );
3256
+ const accepted = [];
3257
+ for (const candidate of longestFirst) {
3258
+ const overlaps = accepted.some(
3259
+ (match) => candidate.index < match.end && candidate.end > match.index
3260
+ );
3261
+ if (!overlaps) {
3262
+ accepted.push(candidate);
3263
+ }
3264
+ }
3265
+ return accepted.sort((left, right) => left.index - right.index);
3266
+ }
3267
+
2963
3268
  // src/commands/scan.ts
2964
3269
  var c = {
2965
3270
  reset: "\x1B[0m",
@@ -2994,6 +3299,14 @@ var COST_SPINNER_MESSAGES = [
2994
3299
  "Crunching token counts",
2995
3300
  "Still working through local history"
2996
3301
  ];
3302
+ var SLOP_SPINNER_MESSAGES = [
3303
+ "Auditing agent prose",
3304
+ "Counting load-bearing insights",
3305
+ "Inspecting suspiciously honest takes",
3306
+ "Measuring corrective juxtaposition",
3307
+ "Checking what earned its keep",
3308
+ "Cataloging chatbot tics"
3309
+ ];
2997
3310
  var DAY_MS = 24 * 60 * 60 * 1e3;
2998
3311
  function createSpinner(messages = SPINNER_MESSAGES) {
2999
3312
  let messageIdx = 0;
@@ -3033,7 +3346,7 @@ function createSpinner(messages = SPINNER_MESSAGES) {
3033
3346
  }
3034
3347
  };
3035
3348
  }
3036
- function parseArgs(args) {
3349
+ function parseArgs(args, command = "scan") {
3037
3350
  const options = {};
3038
3351
  for (let i = 0; i < args.length; i++) {
3039
3352
  const arg = args[i];
@@ -3055,7 +3368,18 @@ function parseArgs(args) {
3055
3368
  } else if (arg === "--month") {
3056
3369
  setRelativeRange(options, 30);
3057
3370
  } else if (arg === "--help" || arg === "-h") {
3058
- console.log(`devrage scan \u2014 scan sessions for profanity
3371
+ if (command === "slop") {
3372
+ console.log(`devrage slop \u2014 scan coding-agent responses for AI-isms
3373
+
3374
+ Options:
3375
+ --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
3376
+ --since, -s <date> Only scan responses after this date (ISO 8601)
3377
+ --day, --days [n] Only scan the last n days (default: 1)
3378
+ --week Only scan the last 7 days
3379
+ --month Only scan the last 30 days
3380
+ --help, -h Show this help`);
3381
+ } else {
3382
+ console.log(`devrage scan \u2014 scan sessions for profanity
3059
3383
 
3060
3384
  Options:
3061
3385
  --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
@@ -3064,6 +3388,7 @@ Options:
3064
3388
  --week Only scan the last 7 days
3065
3389
  --month Only scan the last 30 days
3066
3390
  --help, -h Show this help`);
3391
+ }
3067
3392
  process.exit(0);
3068
3393
  }
3069
3394
  }
@@ -3208,6 +3533,98 @@ async function scan(args) {
3208
3533
  console.log("");
3209
3534
  }
3210
3535
  }
3536
+ async function slop(args) {
3537
+ const options = parseArgs(args, "slop");
3538
+ const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
3539
+ const spinner = createSpinner(SLOP_SPINNER_MESSAGES);
3540
+ const tellTally = {};
3541
+ const categoryTally = {};
3542
+ const perAgent = {};
3543
+ let totalMessages = 0;
3544
+ let totalHits = 0;
3545
+ let taintedMessages = 0;
3546
+ spinner.start();
3547
+ try {
3548
+ for (const adapter of adapters) {
3549
+ let agentMessages = 0;
3550
+ let agentHits = 0;
3551
+ let agentTainted = 0;
3552
+ spinner.update();
3553
+ for await (const message of adapter.messages({
3554
+ role: "assistant",
3555
+ since: options.since
3556
+ })) {
3557
+ totalMessages++;
3558
+ agentMessages++;
3559
+ const result = detectSlop(message.text);
3560
+ if (result.count === 0) {
3561
+ continue;
3562
+ }
3563
+ totalHits += result.count;
3564
+ agentHits += result.count;
3565
+ taintedMessages++;
3566
+ agentTainted++;
3567
+ for (const match of result.matches) {
3568
+ tellTally[match.tell] = (tellTally[match.tell] ?? 0) + 1;
3569
+ categoryTally[match.category] = (categoryTally[match.category] ?? 0) + 1;
3570
+ }
3571
+ }
3572
+ if (agentMessages > 0) {
3573
+ perAgent[adapter.name] = {
3574
+ messages: agentMessages,
3575
+ hits: agentHits,
3576
+ tainted: agentTainted
3577
+ };
3578
+ }
3579
+ }
3580
+ } finally {
3581
+ spinner.stop();
3582
+ }
3583
+ const activeAgents = Object.entries(perAgent);
3584
+ console.log("");
3585
+ printReportHeader(options, "slop");
3586
+ printSlopOverview(totalMessages, totalHits, taintedMessages);
3587
+ if (activeAgents.length > 1) {
3588
+ console.log("");
3589
+ console.log(` ${sectionTitle("agent slop")}`);
3590
+ for (const [name, stats] of activeAgents) {
3591
+ const rate = stats.messages > 0 ? stats.tainted / stats.messages : 0;
3592
+ console.log(
3593
+ ` ${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}`
3594
+ );
3595
+ }
3596
+ }
3597
+ if (totalHits > 0) {
3598
+ const tells = Object.entries(tellTally).sort(
3599
+ ([left, leftCount], [right, rightCount]) => rightCount - leftCount || left.localeCompare(right)
3600
+ );
3601
+ console.log("");
3602
+ console.log(` ${sectionTitle("top tells")}`);
3603
+ for (const [tell, count] of tells.slice(0, 15)) {
3604
+ console.log(
3605
+ ` ${c.yellow}${tell.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
3606
+ );
3607
+ }
3608
+ const categories = Object.entries(categoryTally).sort(
3609
+ ([left, leftCount], [right, rightCount]) => (rightCount ?? 0) - (leftCount ?? 0) || left.localeCompare(right)
3610
+ );
3611
+ console.log("");
3612
+ console.log(` ${sectionTitle("slop flavors")}`);
3613
+ for (const [category, count] of categories) {
3614
+ console.log(
3615
+ ` ${c.cyan}${category.padEnd(28)}${c.reset} ${c.bold}${String(count).padStart(4)}${c.reset}`
3616
+ );
3617
+ }
3618
+ }
3619
+ console.log("");
3620
+ if (totalMessages === 0) {
3621
+ console.log(` ${c.gray}no assistant messages found.${c.reset}`);
3622
+ console.log("");
3623
+ } else if (totalHits === 0) {
3624
+ console.log(` ${c.green}suspiciously human. not a single slop tell found.${c.reset}`);
3625
+ console.log("");
3626
+ }
3627
+ }
3211
3628
  async function cost(args) {
3212
3629
  const options = parseCostArgs(args);
3213
3630
  const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
@@ -3601,13 +4018,25 @@ function renderCostHtmlReport(data) {
3601
4018
  function jsonForScript(value) {
3602
4019
  return JSON.stringify(value).replace(/</g, "\\u003c");
3603
4020
  }
3604
- function printReportHeader(options) {
4021
+ function printReportHeader(options, label = "report") {
3605
4022
  const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
3606
4023
  const agent = options.agent ? ` \xB7 ${options.agent}` : "";
3607
- console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
4024
+ console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}${label}${c.reset}`);
3608
4025
  console.log(` ${c.dim}${scope}${agent}${c.reset}`);
3609
4026
  console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
3610
4027
  }
4028
+ function printSlopOverview(totalMessages, totalHits, taintedMessages) {
4029
+ const rate = totalMessages > 0 ? taintedMessages / totalMessages : 0;
4030
+ console.log(
4031
+ ` ${c.dim}assistant messages${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
4032
+ );
4033
+ console.log(
4034
+ ` ${c.dim}slop hits${c.reset} ${c.bold}${c.yellow}${formatNumber(totalHits)}${c.reset}`
4035
+ );
4036
+ console.log(
4037
+ ` ${c.dim}messages with slop${c.reset} ${c.bold}${formatNumber(taintedMessages)}${c.reset} ${c.dim}(${formatPercent(rate)})${c.reset}`
4038
+ );
4039
+ }
3611
4040
  function printBasicOverview(totalMessages, totalSwears) {
3612
4041
  console.log(
3613
4042
  ` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
@@ -3735,11 +4164,12 @@ function formatDate(value) {
3735
4164
  // src/cli.ts
3736
4165
  var COMMANDS = {
3737
4166
  cost,
3738
- scan
4167
+ scan,
4168
+ slop
3739
4169
  };
3740
4170
  var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
3741
4171
  function usage() {
3742
- console.log(`devrage \u2014 count how many times you swear at your coding agents
4172
+ console.log(`devrage \u2014 measure rage, cost, and AI slop in coding-agent sessions
3743
4173
 
3744
4174
  Usage:
3745
4175
  devrage <command> [options]
@@ -3747,6 +4177,7 @@ Usage:
3747
4177
  Commands:
3748
4178
  cost Show API-equivalent coding agent cost
3749
4179
  scan Scan sessions for profanity
4180
+ slop Scan agent responses for common AI-isms
3750
4181
 
3751
4182
  Options:
3752
4183
  --help, -h Show this help message
@@ -3755,7 +4186,9 @@ Options:
3755
4186
  Examples:
3756
4187
  devrage cost
3757
4188
  devrage scan
4189
+ devrage slop
3758
4190
  devrage scan --agent claude
4191
+ devrage slop --agent codex
3759
4192
  devrage scan --since 2025-01-01`);
3760
4193
  }
3761
4194
  async function main() {
@@ -3766,7 +4199,7 @@ async function main() {
3766
4199
  process.exit(0);
3767
4200
  }
3768
4201
  if (command === "--version") {
3769
- console.log("0.5.8");
4202
+ console.log("0.6.0");
3770
4203
  process.exit(0);
3771
4204
  }
3772
4205
  const parsed = parseCommand(args);