devrage 0.5.7 → 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 +527 -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 +62 -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
  }
@@ -670,6 +690,7 @@ async function* parseCodexUsageJsonl(filePath, context) {
670
690
  let previousUsageSignature = null;
671
691
  let session = context.session;
672
692
  let sawSessionMeta = false;
693
+ let forkReplayStartedAt = null;
673
694
  for await (const line of rl) {
674
695
  if (!line.trim()) {
675
696
  continue;
@@ -682,6 +703,15 @@ async function* parseCodexUsageJsonl(filePath, context) {
682
703
  if (metaSession && !sawSessionMeta) {
683
704
  session = metaSession;
684
705
  sawSessionMeta = true;
706
+ if (payload?.["thread_source"] === "subagent") {
707
+ forkReplayStartedAt = uuidV7Timestamp(metaSession) ?? timestampMilliseconds(entry["timestamp"]) ?? timestampMilliseconds(payload["timestamp"]);
708
+ }
709
+ }
710
+ continue;
711
+ }
712
+ if (forkReplayStartedAt !== null) {
713
+ if (isLiveForkTaskStart(entry, payload, forkReplayStartedAt)) {
714
+ forkReplayStartedAt = null;
685
715
  }
686
716
  continue;
687
717
  }
@@ -813,6 +843,35 @@ function numberValue2(value) {
813
843
  function stringValue2(value) {
814
844
  return typeof value === "string" && value.trim() ? value : void 0;
815
845
  }
846
+ function isLiveForkTaskStart(entry, payload, forkStartedAt) {
847
+ if (entry["type"] !== "event_msg" || payload?.["type"] !== "task_started") {
848
+ return false;
849
+ }
850
+ const taskIdStartedAt = uuidV7Timestamp(stringValue2(payload["turn_id"]));
851
+ if (taskIdStartedAt !== null) {
852
+ return taskIdStartedAt >= forkStartedAt;
853
+ }
854
+ const taskStartedAt = timestampMilliseconds(payload["started_at"]);
855
+ return taskStartedAt !== null && taskStartedAt >= Math.floor(forkStartedAt / 1e3) * 1e3;
856
+ }
857
+ function uuidV7Timestamp(value) {
858
+ const normalized = value?.replaceAll("-", "");
859
+ if (!normalized || !/^[0-9a-f]{12}7/i.test(normalized)) {
860
+ return null;
861
+ }
862
+ const timestamp = Number.parseInt(normalized.slice(0, 12), 16);
863
+ return Number.isSafeInteger(timestamp) ? timestamp : null;
864
+ }
865
+ function timestampMilliseconds(value) {
866
+ if (typeof value === "number" && Number.isFinite(value)) {
867
+ return value >= 1e12 ? value : value * 1e3;
868
+ }
869
+ if (typeof value === "string") {
870
+ const timestamp = Date.parse(value);
871
+ return Number.isFinite(timestamp) ? timestamp : null;
872
+ }
873
+ return null;
874
+ }
816
875
  function asRecord3(value) {
817
876
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
818
877
  return null;
@@ -997,7 +1056,7 @@ async function* parseCursorStore(store, options) {
997
1056
  if (parsed === void 0) {
998
1057
  continue;
999
1058
  }
1000
- for (const message of extractCursorMessages(parsed, row.key)) {
1059
+ for (const message of extractCursorMessages(parsed, row.key, options?.role ?? "user")) {
1001
1060
  const text = message.text.trim();
1002
1061
  if (!isLikelyMessageText(text)) {
1003
1062
  continue;
@@ -1138,21 +1197,22 @@ function decodeStateValue(value) {
1138
1197
  }
1139
1198
  return null;
1140
1199
  }
1141
- function extractCursorMessages(root, rowKey) {
1200
+ function extractCursorMessages(root, rowKey, role) {
1142
1201
  if (rowKey.startsWith("bubbleId:")) {
1143
- const message = extractCursorBubbleMessage(root, rowKey);
1202
+ const message = extractCursorBubbleMessage(root, rowKey, role);
1144
1203
  return message ? [message] : [];
1145
1204
  }
1146
1205
  const messages = [];
1147
- collectRoleMessages(root, messages);
1148
- 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"))) {
1149
1208
  collectPromptMessages(root, messages);
1150
1209
  }
1151
1210
  return uniqueMessages(messages);
1152
1211
  }
1153
- function extractCursorBubbleMessage(root, rowKey) {
1212
+ function extractCursorBubbleMessage(root, rowKey, role) {
1154
1213
  const record = asRecord4(root);
1155
- if (!record || numberValue3(record["type"]) !== 1) {
1214
+ const expectedType = role === "user" ? 1 : 2;
1215
+ if (!record || numberValue3(record["type"]) !== expectedType) {
1156
1216
  return null;
1157
1217
  }
1158
1218
  const text = firstTextField(record, ["text", "richText"]);
@@ -1248,13 +1308,13 @@ function cursorBubbleSession(rowKey) {
1248
1308
  const [, composerId] = rowKey.split(":");
1249
1309
  return composerId?.trim() || void 0;
1250
1310
  }
1251
- function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
1311
+ function collectRoleMessages(value, messages, role, inheritedSession, depth = 0) {
1252
1312
  if (depth > 12) {
1253
1313
  return;
1254
1314
  }
1255
1315
  if (Array.isArray(value)) {
1256
1316
  for (const item of value) {
1257
- collectRoleMessages(item, messages, inheritedSession, depth + 1);
1317
+ collectRoleMessages(item, messages, role, inheritedSession, depth + 1);
1258
1318
  }
1259
1319
  return;
1260
1320
  }
@@ -1263,15 +1323,15 @@ function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
1263
1323
  return;
1264
1324
  }
1265
1325
  const session = extractSession(record) ?? inheritedSession;
1266
- if (isUserAuthored(record)) {
1267
- const text = extractMessageText(record);
1326
+ if (isAuthoredBy(record, role)) {
1327
+ const text = extractMessageText2(record);
1268
1328
  if (text) {
1269
1329
  messages.push({ text, timestamp: extractTimestamp2(record), session });
1270
1330
  }
1271
1331
  }
1272
1332
  for (const child of Object.values(record)) {
1273
1333
  if (typeof child === "object" && child !== null) {
1274
- collectRoleMessages(child, messages, session, depth + 1);
1334
+ collectRoleMessages(child, messages, role, session, depth + 1);
1275
1335
  }
1276
1336
  }
1277
1337
  }
@@ -1311,6 +1371,9 @@ function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
1311
1371
  }
1312
1372
  }
1313
1373
  }
1374
+ function isAuthoredBy(record, role) {
1375
+ return role === "user" ? isUserAuthored(record) : isAssistantAuthored(record);
1376
+ }
1314
1377
  function isUserAuthored(record) {
1315
1378
  return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
1316
1379
  (field) => actorIsUser(record[field])
@@ -1344,7 +1407,7 @@ function actorString(value) {
1344
1407
  }
1345
1408
  return null;
1346
1409
  }
1347
- function extractMessageText(record) {
1410
+ function extractMessageText2(record) {
1348
1411
  return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
1349
1412
  }
1350
1413
  function firstTextField(record, fields) {
@@ -1475,7 +1538,7 @@ function opencodeAdapter() {
1475
1538
  return;
1476
1539
  }
1477
1540
  try {
1478
- yield* queryUserMessages(db, options);
1541
+ yield* queryMessages(db, options);
1479
1542
  } finally {
1480
1543
  db.close();
1481
1544
  }
@@ -1504,7 +1567,7 @@ async function openOpencodeDb() {
1504
1567
  }
1505
1568
  return db;
1506
1569
  }
1507
- function* queryUserMessages(db, options) {
1570
+ function* queryMessages(db, options) {
1508
1571
  let query = `
1509
1572
  SELECT
1510
1573
  m.session_id,
@@ -1512,10 +1575,10 @@ function* queryUserMessages(db, options) {
1512
1575
  json_extract(p.data, '$.text') as text
1513
1576
  FROM message m
1514
1577
  JOIN part p ON p.message_id = m.id
1515
- WHERE json_extract(m.data, '$.role') = 'user'
1578
+ WHERE json_extract(m.data, '$.role') = ?
1516
1579
  AND json_extract(p.data, '$.type') = 'text'
1517
1580
  `;
1518
- const params = [];
1581
+ const params = [options?.role ?? "user"];
1519
1582
  if (options?.since) {
1520
1583
  query += ` AND m.time_created >= ?`;
1521
1584
  params.push(options.since.getTime());
@@ -1623,7 +1686,12 @@ async function* walkPiSessions(dir, options, project) {
1623
1686
  yield* walkPiSessions(fullPath, options, project ?? entry);
1624
1687
  } else if (entry.endsWith(".jsonl")) {
1625
1688
  const session = entry.replace(".jsonl", "");
1626
- 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
+ });
1627
1695
  }
1628
1696
  }
1629
1697
  }
@@ -1668,7 +1736,7 @@ async function* parsePiJsonl(filePath, context) {
1668
1736
  continue;
1669
1737
  }
1670
1738
  const message = entry.message;
1671
- if (!message || message.role !== "user") {
1739
+ if (!message || message.role !== context.role) {
1672
1740
  continue;
1673
1741
  }
1674
1742
  const text = contentToString2(message.content);
@@ -1785,7 +1853,7 @@ function t3codeAdapter() {
1785
1853
  continue;
1786
1854
  }
1787
1855
  try {
1788
- yield* queryUserMessages2(db, location, options);
1856
+ yield* queryMessages2(db, location, options);
1789
1857
  } finally {
1790
1858
  db.close();
1791
1859
  }
@@ -1847,7 +1915,7 @@ function resolveHomePath(value) {
1847
1915
  async function openT3Db(dbPath) {
1848
1916
  return openReadonlySqliteDatabase(dbPath);
1849
1917
  }
1850
- function* queryUserMessages2(db, location, options) {
1918
+ function* queryMessages2(db, location, options) {
1851
1919
  if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
1852
1920
  return;
1853
1921
  }
@@ -1855,9 +1923,9 @@ function* queryUserMessages2(db, location, options) {
1855
1923
  let query = `
1856
1924
  SELECT thread_id, created_at, text
1857
1925
  FROM projection_thread_messages
1858
- WHERE role = 'user'
1926
+ WHERE role = ?
1859
1927
  `;
1860
- const params = [];
1928
+ const params = [options?.role ?? "user"];
1861
1929
  if (options?.since) {
1862
1930
  query += ` AND created_at >= ?`;
1863
1931
  params.push(options.since.toISOString());
@@ -2212,7 +2280,7 @@ function zedAdapter() {
2212
2280
  }
2213
2281
  };
2214
2282
  }
2215
- async function* parseTextThreads(dir, _options) {
2283
+ async function* parseTextThreads(dir, options) {
2216
2284
  if (!existsSync5(dir)) {
2217
2285
  return;
2218
2286
  }
@@ -2232,8 +2300,9 @@ async function* parseTextThreads(dir, _options) {
2232
2300
  if (!conversation.messages || !Array.isArray(conversation.messages)) {
2233
2301
  continue;
2234
2302
  }
2303
+ const role = options?.role ?? "user";
2235
2304
  for (const msg of conversation.messages) {
2236
- if (msg.role !== "user") {
2305
+ if (msg.role !== role) {
2237
2306
  continue;
2238
2307
  }
2239
2308
  const text = typeof msg.content === "string" ? msg.content : null;
@@ -2249,7 +2318,7 @@ async function* parseTextThreads(dir, _options) {
2249
2318
  }
2250
2319
  }
2251
2320
  }
2252
- async function* parseAgentThreads(dbDir, _options) {
2321
+ async function* parseAgentThreads(dbDir, options) {
2253
2322
  if (!existsSync5(dbDir)) {
2254
2323
  return;
2255
2324
  }
@@ -2285,8 +2354,8 @@ async function* parseAgentThreads(dbDir, _options) {
2285
2354
  continue;
2286
2355
  }
2287
2356
  const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
2288
- let query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = 'user'`;
2289
- 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");
2290
2359
  for (const row of rows) {
2291
2360
  if (!row.text?.trim()) {
2292
2361
  continue;
@@ -2921,6 +2990,281 @@ function asRecord7(value) {
2921
2990
  return value;
2922
2991
  }
2923
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
+
2924
3268
  // src/commands/scan.ts
2925
3269
  var c = {
2926
3270
  reset: "\x1B[0m",
@@ -2955,6 +3299,14 @@ var COST_SPINNER_MESSAGES = [
2955
3299
  "Crunching token counts",
2956
3300
  "Still working through local history"
2957
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
+ ];
2958
3310
  var DAY_MS = 24 * 60 * 60 * 1e3;
2959
3311
  function createSpinner(messages = SPINNER_MESSAGES) {
2960
3312
  let messageIdx = 0;
@@ -2994,7 +3346,7 @@ function createSpinner(messages = SPINNER_MESSAGES) {
2994
3346
  }
2995
3347
  };
2996
3348
  }
2997
- function parseArgs(args) {
3349
+ function parseArgs(args, command = "scan") {
2998
3350
  const options = {};
2999
3351
  for (let i = 0; i < args.length; i++) {
3000
3352
  const arg = args[i];
@@ -3016,7 +3368,18 @@ function parseArgs(args) {
3016
3368
  } else if (arg === "--month") {
3017
3369
  setRelativeRange(options, 30);
3018
3370
  } else if (arg === "--help" || arg === "-h") {
3019
- 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
3020
3383
 
3021
3384
  Options:
3022
3385
  --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
@@ -3025,6 +3388,7 @@ Options:
3025
3388
  --week Only scan the last 7 days
3026
3389
  --month Only scan the last 30 days
3027
3390
  --help, -h Show this help`);
3391
+ }
3028
3392
  process.exit(0);
3029
3393
  }
3030
3394
  }
@@ -3169,6 +3533,98 @@ async function scan(args) {
3169
3533
  console.log("");
3170
3534
  }
3171
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
+ }
3172
3628
  async function cost(args) {
3173
3629
  const options = parseCostArgs(args);
3174
3630
  const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
@@ -3562,13 +4018,25 @@ function renderCostHtmlReport(data) {
3562
4018
  function jsonForScript(value) {
3563
4019
  return JSON.stringify(value).replace(/</g, "\\u003c");
3564
4020
  }
3565
- function printReportHeader(options) {
4021
+ function printReportHeader(options, label = "report") {
3566
4022
  const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
3567
4023
  const agent = options.agent ? ` \xB7 ${options.agent}` : "";
3568
- 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}`);
3569
4025
  console.log(` ${c.dim}${scope}${agent}${c.reset}`);
3570
4026
  console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
3571
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
+ }
3572
4040
  function printBasicOverview(totalMessages, totalSwears) {
3573
4041
  console.log(
3574
4042
  ` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
@@ -3696,11 +4164,12 @@ function formatDate(value) {
3696
4164
  // src/cli.ts
3697
4165
  var COMMANDS = {
3698
4166
  cost,
3699
- scan
4167
+ scan,
4168
+ slop
3700
4169
  };
3701
4170
  var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
3702
4171
  function usage() {
3703
- 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
3704
4173
 
3705
4174
  Usage:
3706
4175
  devrage <command> [options]
@@ -3708,6 +4177,7 @@ Usage:
3708
4177
  Commands:
3709
4178
  cost Show API-equivalent coding agent cost
3710
4179
  scan Scan sessions for profanity
4180
+ slop Scan agent responses for common AI-isms
3711
4181
 
3712
4182
  Options:
3713
4183
  --help, -h Show this help message
@@ -3716,7 +4186,9 @@ Options:
3716
4186
  Examples:
3717
4187
  devrage cost
3718
4188
  devrage scan
4189
+ devrage slop
3719
4190
  devrage scan --agent claude
4191
+ devrage slop --agent codex
3720
4192
  devrage scan --since 2025-01-01`);
3721
4193
  }
3722
4194
  async function main() {
@@ -3727,7 +4199,7 @@ async function main() {
3727
4199
  process.exit(0);
3728
4200
  }
3729
4201
  if (command === "--version") {
3730
- console.log("0.5.7");
4202
+ console.log("0.6.0");
3731
4203
  process.exit(0);
3732
4204
  }
3733
4205
  const parsed = parseCommand(args);