mixdog 0.9.1 → 0.9.2

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 (214) hide show
  1. package/package.json +8 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/task-bench.mjs +207 -0
  23. package/scripts/tool-failures.mjs +6 -6
  24. package/scripts/tool-smoke.mjs +306 -66
  25. package/scripts/toolcall-args-test.mjs +81 -0
  26. package/src/agents/debugger/AGENT.md +4 -4
  27. package/src/agents/heavy-worker/AGENT.md +4 -2
  28. package/src/agents/reviewer/AGENT.md +4 -4
  29. package/src/agents/worker/AGENT.md +4 -2
  30. package/src/app.mjs +10 -6
  31. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  32. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  33. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  34. package/src/headless-role.mjs +14 -14
  35. package/src/help.mjs +1 -0
  36. package/src/lib/rules-builder.cjs +32 -54
  37. package/src/mixdog-session-runtime.mjs +710 -318
  38. package/src/output-styles/default.md +12 -7
  39. package/src/output-styles/minimal.md +25 -0
  40. package/src/output-styles/oneline.md +21 -0
  41. package/src/output-styles/simple.md +10 -9
  42. package/src/repl.mjs +12 -2
  43. package/src/rules/agent/00-common.md +7 -5
  44. package/src/rules/agent/30-explorer.md +7 -8
  45. package/src/rules/lead/01-general.md +3 -1
  46. package/src/rules/lead/lead-tool.md +7 -0
  47. package/src/rules/shared/01-tool.md +17 -12
  48. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  51. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  52. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  53. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  54. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  56. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  57. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  58. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  59. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  62. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
  64. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  65. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  66. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  67. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  69. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
  70. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
  71. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
  72. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  73. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  74. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
  76. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  77. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  78. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  80. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  82. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  83. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  84. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  85. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  87. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  90. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  95. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  99. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  100. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  101. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  102. package/src/runtime/channels/backends/discord.mjs +99 -9
  103. package/src/runtime/channels/backends/telegram.mjs +501 -0
  104. package/src/runtime/channels/index.mjs +441 -1224
  105. package/src/runtime/channels/lib/config.mjs +54 -2
  106. package/src/runtime/channels/lib/format.mjs +4 -2
  107. package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
  108. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  109. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  110. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  111. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  112. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  113. package/src/runtime/channels/lib/webhook.mjs +59 -31
  114. package/src/runtime/channels/tool-defs.mjs +1 -1
  115. package/src/runtime/memory/index.mjs +184 -19
  116. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  117. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  118. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  119. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  120. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  121. package/src/runtime/memory/lib/memory.mjs +101 -4
  122. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  123. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  124. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  125. package/src/runtime/memory/tool-defs.mjs +6 -3
  126. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  127. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  128. package/src/runtime/shared/config.mjs +9 -0
  129. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  130. package/src/runtime/shared/schedules-store.mjs +21 -19
  131. package/src/runtime/shared/tool-surface.mjs +98 -13
  132. package/src/runtime/shared/transcript-writer.mjs +129 -0
  133. package/src/runtime/shared/update-checker.mjs +214 -0
  134. package/src/standalone/agent-tool.mjs +255 -109
  135. package/src/standalone/channel-admin.mjs +133 -40
  136. package/src/standalone/channel-worker.mjs +8 -291
  137. package/src/standalone/explore-tool.mjs +2 -2
  138. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  139. package/src/standalone/provider-admin.mjs +11 -0
  140. package/src/standalone/usage-dashboard.mjs +1 -1
  141. package/src/tui/App.jsx +2096 -732
  142. package/src/tui/components/ConfirmBar.jsx +47 -0
  143. package/src/tui/components/ContextPanel.jsx +5 -3
  144. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  145. package/src/tui/components/Markdown.jsx +22 -98
  146. package/src/tui/components/Message.jsx +14 -35
  147. package/src/tui/components/Picker.jsx +87 -12
  148. package/src/tui/components/PromptInput.jsx +83 -7
  149. package/src/tui/components/QueuedCommands.jsx +1 -1
  150. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  151. package/src/tui/components/Spinner.jsx +7 -7
  152. package/src/tui/components/StatusLine.jsx +40 -21
  153. package/src/tui/components/TextEntryPanel.jsx +51 -7
  154. package/src/tui/components/ToolExecution.jsx +170 -98
  155. package/src/tui/components/TurnDone.jsx +4 -4
  156. package/src/tui/components/UsagePanel.jsx +1 -1
  157. package/src/tui/components/tool-output-format.mjs +159 -21
  158. package/src/tui/components/tool-output-format.test.mjs +87 -0
  159. package/src/tui/display-width.mjs +69 -0
  160. package/src/tui/display-width.test.mjs +35 -0
  161. package/src/tui/dist/index.mjs +6965 -2391
  162. package/src/tui/engine.mjs +287 -126
  163. package/src/tui/index.jsx +117 -7
  164. package/src/tui/keyboard-protocol.mjs +42 -0
  165. package/src/tui/lib/voice-recorder.mjs +453 -0
  166. package/src/tui/markdown/format-token.mjs +129 -76
  167. package/src/tui/markdown/format-token.test.mjs +61 -19
  168. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  169. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  170. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  171. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  172. package/src/tui/markdown/table-layout.mjs +9 -9
  173. package/src/tui/paste-attachments.mjs +0 -11
  174. package/src/tui/prompt-history-store.mjs +129 -0
  175. package/src/tui/prompt-history-store.test.mjs +52 -0
  176. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  177. package/src/tui/theme.mjs +41 -657
  178. package/src/tui/themes/base.mjs +86 -0
  179. package/src/tui/themes/basic.mjs +85 -0
  180. package/src/tui/themes/catppuccin.mjs +72 -0
  181. package/src/tui/themes/dracula.mjs +70 -0
  182. package/src/tui/themes/everforest.mjs +71 -0
  183. package/src/tui/themes/gruvbox.mjs +71 -0
  184. package/src/tui/themes/index.mjs +71 -0
  185. package/src/tui/themes/indigo.mjs +78 -0
  186. package/src/tui/themes/kanagawa.mjs +80 -0
  187. package/src/tui/themes/light.mjs +81 -0
  188. package/src/tui/themes/nord.mjs +72 -0
  189. package/src/tui/themes/onedark.mjs +16 -0
  190. package/src/tui/themes/rosepine.mjs +70 -0
  191. package/src/tui/themes/teal.mjs +81 -0
  192. package/src/tui/themes/tokyonight.mjs +79 -0
  193. package/src/tui/themes/utils.mjs +106 -0
  194. package/src/tui/themes/warm.mjs +79 -0
  195. package/src/tui/transcript-tool-failures.mjs +13 -2
  196. package/src/ui/markdown.mjs +1 -1
  197. package/src/ui/model-display.mjs +2 -2
  198. package/src/ui/statusline.mjs +26 -27
  199. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  200. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  201. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  202. package/src/workflows/default/WORKFLOW.md +39 -12
  203. package/src/workflows/sequential/WORKFLOW.md +46 -0
  204. package/src/workflows/solo/WORKFLOW.md +7 -0
  205. package/vendor/ink/build/display-width.js +62 -0
  206. package/vendor/ink/build/ink.js +100 -12
  207. package/vendor/ink/build/measure-text.js +4 -1
  208. package/vendor/ink/build/output.js +115 -9
  209. package/vendor/ink/build/render-node-to-output.js +4 -1
  210. package/vendor/ink/build/render.js +4 -0
  211. package/src/output-styles/extreme-simple.md +0 -20
  212. package/src/rules/lead/04-workflow.md +0 -51
  213. package/src/workflows/default/workflow.json +0 -13
  214. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ // routing-corpus.mjs — mine the local agent trace for REAL tool-routing
3
+ // patterns and freeze them as a fixed corpus. The corpus (input) is captured
4
+ // ONCE with --save; rule changes are then A/B-evaluated against that frozen
5
+ // set by re-running --eval before/after (input constant, only rules change).
6
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { basename, resolve } from 'node:path';
9
+ import { resolvePluginData } from '../src/runtime/shared/plugin-paths.mjs';
10
+
11
+ function argValue(name, fallback = null) {
12
+ const idx = process.argv.indexOf(name);
13
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
14
+ const pref = `${name}=`;
15
+ const hit = process.argv.find((a) => a.startsWith(pref));
16
+ return hit ? hit.slice(pref.length) : fallback;
17
+ }
18
+ function hasFlag(name) { return process.argv.includes(name); }
19
+
20
+ function parseDuration(value) {
21
+ const raw = String(value || '').trim();
22
+ if (!raw) return null;
23
+ if (/^now$/i.test(raw)) return Date.now();
24
+ if (/^\d+$/.test(raw)) { const n = Number(raw); return n > 10_000_000_000 ? n : n * 1000; }
25
+ const rel = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
26
+ if (rel) {
27
+ const n = Number(rel[1]); const u = rel[2].toLowerCase();
28
+ const mult = u === 'ms' ? 1 : u === 's' ? 1000 : u === 'm' ? 60_000 : u === 'h' ? 3_600_000 : 86_400_000;
29
+ return Date.now() - n * mult;
30
+ }
31
+ const parsed = Date.parse(raw);
32
+ return Number.isFinite(parsed) ? parsed : null;
33
+ }
34
+
35
+ function defaultTracePath() {
36
+ const data = process.env.MIXDOG_DATA_DIR || resolvePluginData() || resolve(homedir(), '.mixdog', 'data');
37
+ return resolve(data, 'history', 'agent-trace.jsonl');
38
+ }
39
+
40
+ function readRows(path) {
41
+ if (!existsSync(path)) return [];
42
+ const out = [];
43
+ for (const line of readFileSync(path, 'utf8').split(/\r?\n/)) {
44
+ if (!line) continue;
45
+ try { out.push(JSON.parse(line)); } catch { /* best-effort */ }
46
+ }
47
+ return out;
48
+ }
49
+ function payload(row) { return row?.payload && typeof row.payload === 'object' ? row.payload : {}; }
50
+ function field(row, name) { if (row && row[name] != null) return row[name]; const p = payload(row); return p[name] != null ? p[name] : null; }
51
+ function sessionId(row) { return String(row?.session_id || row?.sessionId || field(row, 'session_id') || ''); }
52
+ function shortId(id) { const s = String(id || ''); return s.length <= 18 ? s : `${s.slice(0, 10)}…${s.slice(-6)}`; }
53
+ function shortModel(m) { const s = String(m || '-'); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; }
54
+ function groupBy(rows, keyFn) {
55
+ const map = new Map();
56
+ for (const r of rows) { const k = keyFn(r); if (!map.has(k)) map.set(k, []); map.get(k).push(r); }
57
+ return map;
58
+ }
59
+
60
+ function argsSummary(tool, args) {
61
+ if (!args || typeof args !== 'object') return '';
62
+ const clip = (v, n = 60) => { const s = String(v ?? '').replace(/\s+/g, ' ').trim(); return s.length > n ? `${s.slice(0, n - 1)}…` : s; };
63
+ const arr = (v) => Array.isArray(v) ? `[${v.length}:${clip(v[0], 40)}]` : clip(v, 50);
64
+ switch (tool) {
65
+ case 'grep': return clip(Array.isArray(args.pattern) ? `[${args.pattern.length}]${args.pattern[0]}` : args.pattern);
66
+ case 'read': return clip(basename(String(Array.isArray(args.path) ? args.path[0] : args.path || '')));
67
+ case 'code_graph': return clip(`${args.mode || '?'}:${args.symbol || args.file || ''}`);
68
+ case 'explore': return arr(args.query);
69
+ case 'find': return clip(args.query);
70
+ case 'glob': return clip(Array.isArray(args.pattern) ? args.pattern[0] : args.pattern);
71
+ case 'list': return clip(basename(String(args.path || '')));
72
+ case 'apply_patch': return '(edit)';
73
+ case 'shell': return clip(args.command);
74
+ default: return clip(JSON.stringify(args), 40);
75
+ }
76
+ }
77
+
78
+ function normGrep(args) {
79
+ const p = args?.pattern;
80
+ return String(Array.isArray(p) ? p.join('|') : (p ?? '')).trim().toLowerCase();
81
+ }
82
+
83
+ // shell used to INSPECT the filesystem (read/list/search/exists) instead of a
84
+ // dedicated tool — the anti-pattern the shell description targets. Concept, not
85
+ // an exhaustive list: a command whose leading verb only reads/lists/searches
86
+ // files and does not change state or run a program (git/node/npm/build/test).
87
+ const SHELL_INSPECT_VERB = /(?:^|[\s;|&(]|\bforeach-object\s*\{)\s*(get-content|get-childitem|gci|select-string|cat|type|ls|dir|head|tail|find|findstr|grep|rg|wc|test-path|resolve-path|readlink|realpath|stat)\b/i;
88
+ const SHELL_EXEC_VERB = /(?:^|[\s;|&])\s*(git|node|npm|npx|pnpm|yarn|python|cargo|go|make|docker|rm|remove-item|mkdir|new-item|set-content|out-file|move-item|copy-item|>>|>)\b/i;
89
+ function isShellInspect(command) {
90
+ const c = String(command || '');
91
+ if (!c.trim()) return false;
92
+ // If the command also changes state or runs a program, it is legitimate.
93
+ if (SHELL_EXEC_VERB.test(c)) return false;
94
+ return SHELL_INSPECT_VERB.test(c);
95
+ }
96
+
97
+ function buildCase(sid, toolRows) {
98
+ const sorted = [...toolRows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
99
+ const agent = field(sorted[0], 'agent') || field(sorted.find((r) => field(r, 'agent')), 'agent') || null;
100
+ const model = field(sorted[0], 'model') || null;
101
+ const iters = new Set();
102
+ const sequence = [];
103
+ for (const r of sorted) {
104
+ const it = field(r, 'iteration');
105
+ if (it != null) iters.add(it);
106
+ const tool = field(r, 'tool_name') || '?';
107
+ sequence.push({ it: it ?? null, tool, args: argsSummary(tool, field(r, 'tool_args')), rawArgs: field(r, 'tool_args') });
108
+ }
109
+ // per-iteration tool counts for serial detection
110
+ const perIt = new Map();
111
+ for (const s of sequence) { if (s.it == null) continue; perIt.set(s.it, (perIt.get(s.it) || 0) + 1); }
112
+ const itOrder = [...perIt.keys()].sort((a, b) => a - b);
113
+ let longestSingleRun = 0, run = 0;
114
+ for (const it of itOrder) { if (perIt.get(it) === 1) { run += 1; longestSingleRun = Math.max(longestSingleRun, run); } else run = 0; }
115
+
116
+ const names = sequence.map((s) => s.tool);
117
+ const hasAnchor = names.some((n) => n === 'grep' || n === 'code_graph' || n === 'find' || n === 'read');
118
+ const exploreCalls = sequence.filter((s) => s.tool === 'explore');
119
+ const flags = [];
120
+ if (exploreCalls.length && hasAnchor) flags.push('explore_overuse');
121
+ if (exploreCalls.some((s) => Array.isArray(s.rawArgs?.query))) flags.push('explore_multiquery');
122
+ if (names[0] === 'explore') flags.push('explore_first');
123
+ if (sequence.some((s) => s.tool === 'code_graph' && s.rawArgs?.mode === 'find_symbol' && !s.rawArgs?.file)) flags.push('find_symbol_noscope');
124
+ if (longestSingleRun >= 4) flags.push('serial_single_tool');
125
+ // read fragmentation
126
+ const readCounts = new Map();
127
+ for (const s of sequence) { if (s.tool !== 'read') continue; const k = String(s.rawArgs?.path ?? s.args); readCounts.set(k, (readCounts.get(k) || 0) + 1); }
128
+ if ([...readCounts.values()].some((c) => c >= 3)) flags.push('read_fragmentation');
129
+ // grep retry
130
+ const grepCounts = new Map();
131
+ for (const s of sequence) { if (s.tool !== 'grep') continue; const k = normGrep(s.rawArgs); if (!k) continue; grepCounts.set(k, (grepCounts.get(k) || 0) + 1); }
132
+ if ([...grepCounts.values()].some((c) => c >= 2)) flags.push('grep_retry');
133
+ // shell used for filesystem inspection instead of dedicated tools
134
+ if (sequence.some((s) => s.tool === 'shell' && isShellInspect(s.rawArgs?.command))) flags.push('shell_inspect');
135
+
136
+ return {
137
+ session_id: sid,
138
+ short_id: shortId(sid),
139
+ agent, model,
140
+ turns: iters.size || itOrder.length,
141
+ tools: sequence.length,
142
+ first_tool: names[0] || null,
143
+ longest_single_run: longestSingleRun,
144
+ flags,
145
+ max_ts: Math.max(...sorted.map((r) => Number(r.ts || 0))),
146
+ sequence: sequence.map((s) => ({ it: s.it, tool: s.tool, args: s.args })),
147
+ };
148
+ }
149
+
150
+ const FLAG_KEYS = ['explore_overuse', 'explore_multiquery', 'explore_first', 'find_symbol_noscope', 'serial_single_tool', 'read_fragmentation', 'grep_retry', 'shell_inspect'];
151
+
152
+ function buildCorpus(rows, { limit, sinceTs, agentFilter }) {
153
+ let filtered = rows.filter((r) => r.kind === 'tool' && sessionId(r));
154
+ if (sinceTs != null) filtered = filtered.filter((r) => Number(r.ts || 0) >= sinceTs);
155
+ if (agentFilter) { const q = String(agentFilter).toLowerCase(); filtered = filtered.filter((r) => String(field(r, 'agent') || '').toLowerCase().includes(q)); }
156
+ const bySession = groupBy(filtered, sessionId);
157
+ const cases = [...bySession.entries()].map(([sid, tr]) => buildCase(sid, tr))
158
+ .sort((a, b) => Number(b.max_ts || 0) - Number(a.max_ts || 0));
159
+ const emitted = cases.slice(0, limit);
160
+ const summary = {};
161
+ for (const k of FLAG_KEYS) {
162
+ const count = emitted.filter((c) => c.flags.includes(k)).length;
163
+ summary[k] = { count, pct: emitted.length ? Math.round((count / emitted.length) * 100) : 0 };
164
+ }
165
+ return { scanned: bySession.size, emitted: emitted.length, summary, cases: emitted };
166
+ }
167
+
168
+ function scoreCorpus(cases) {
169
+ // A single健全 index: sum of flagged cases per KPI (lower is better).
170
+ const summary = {};
171
+ let flaggedCases = 0;
172
+ for (const k of FLAG_KEYS) summary[k] = cases.filter((c) => c.flags.includes(k)).length;
173
+ for (const c of cases) if (c.flags.length) flaggedCases += 1;
174
+ return { total: cases.length, flagged_cases: flaggedCases, clean_pct: cases.length ? Math.round(((cases.length - flaggedCases) / cases.length) * 100) : 0, by_flag: summary };
175
+ }
176
+
177
+ function renderText(corpus, showCases) {
178
+ const L = [];
179
+ L.push(`routing corpus: scanned ${corpus.scanned} sessions, emitted ${corpus.emitted} cases`);
180
+ L.push('anti-pattern summary:');
181
+ for (const k of FLAG_KEYS) L.push(`- ${k.padEnd(20)} ${String(corpus.summary[k].count).padStart(3)} (${corpus.summary[k].pct}%)`);
182
+ if (showCases) {
183
+ L.push('');
184
+ corpus.cases.forEach((c, i) => {
185
+ L.push(`#${i + 1} agent=${c.agent || '-'} model=${shortModel(c.model)} turns=${c.turns} tools=${c.tools} flags=[${c.flags.join(',')}] sess=${c.short_id}`);
186
+ const shown = c.sequence.slice(0, 8).map((s) => `${s.tool}(${s.args})`).join(' → ');
187
+ const more = c.sequence.length > 8 ? ` +${c.sequence.length - 8} more` : '';
188
+ L.push(` seq: ${shown}${more}`);
189
+ });
190
+ }
191
+ return L.join('\n');
192
+ }
193
+
194
+ // Extract one case by session id (or short-id prefix) from the current trace.
195
+ function extractCaseById(rows, wanted) {
196
+ const q = String(wanted || '');
197
+ const tr = rows.filter((r) => r.kind === 'tool' && sessionId(r) && (sessionId(r) === q || sessionId(r).startsWith(q) || shortId(sessionId(r)).startsWith(q)));
198
+ if (!tr.length) return null;
199
+ const sid = sessionId(tr[0]);
200
+ return buildCase(sid, tr.filter((r) => sessionId(r) === sid));
201
+ }
202
+
203
+ // tool-name -> count for a case (the tool mix).
204
+ function toolMix(cas) {
205
+ const mix = {};
206
+ for (const s of cas.sequence) mix[s.tool] = (mix[s.tool] || 0) + 1;
207
+ return mix;
208
+ }
209
+
210
+ function renderVs(before, after) {
211
+ const L = [];
212
+ const fmtMix = (m) => Object.entries(m).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(' ') || '(none)';
213
+ L.push(`routing A/B before=${before.short_id} (${before.agent || '-'}/${shortModel(before.model)}) after=${after.short_id} (${after.agent || '-'}/${shortModel(after.model)})`);
214
+ L.push('');
215
+ const metric = (label, b, a, lowerBetter = true) => {
216
+ const d = a - b; const arrow = d === 0 ? '=' : (lowerBetter ? (d < 0 ? 'v' : '^') : (d > 0 ? 'v' : '^'));
217
+ L.push(`- ${label.padEnd(14)} ${String(b).padStart(5)} -> ${String(a).padStart(5)} (${d > 0 ? '+' : ''}${d}) ${arrow}`);
218
+ };
219
+ metric('turns', before.turns, after.turns);
220
+ metric('tools', before.tools, after.tools);
221
+ metric('flags', before.flags.length, after.flags.length);
222
+ L.push(` before tool-mix: ${fmtMix(toolMix(before))}`);
223
+ L.push(` after tool-mix: ${fmtMix(toolMix(after))}`);
224
+ const gained = after.flags.filter((f) => !before.flags.includes(f));
225
+ const cleared = before.flags.filter((f) => !after.flags.includes(f));
226
+ if (cleared.length) L.push(` cleared flags: ${cleared.join(', ')}`);
227
+ if (gained.length) L.push(` NEW flags: ${gained.join(', ')}`);
228
+ if (!cleared.length && !gained.length) L.push(` flags unchanged: [${before.flags.join(', ') || 'none'}]`);
229
+ return L.join('\n');
230
+ }
231
+
232
+ // ---- main ----
233
+ const tracePath = argValue('--trace') ? resolve(argValue('--trace')) : defaultTracePath();
234
+ const sinceTs = parseDuration(argValue('--since', null));
235
+ const limit = Number.parseInt(argValue('--limit', '50'), 10) || 50;
236
+ const agentFilter = argValue('--agent', null);
237
+ const jsonMode = hasFlag('--json');
238
+ const savePath = argValue('--save', null); // freeze the corpus (session id list + cases) to a file
239
+ const evalPath = argValue('--eval', null); // re-score a frozen corpus's sessions against CURRENT trace
240
+ const vsIdx = process.argv.indexOf('--vs'); // --vs <before-sid> <after-sid>: routing diff
241
+
242
+ if (vsIdx >= 0) {
243
+ const beforeId = process.argv[vsIdx + 1];
244
+ const afterId = process.argv[vsIdx + 2];
245
+ if (!beforeId || !afterId) { console.error('usage: --vs <before-session> <after-session>'); process.exit(1); }
246
+ const rows = readRows(tracePath);
247
+ const before = extractCaseById(rows, beforeId);
248
+ const after = extractCaseById(rows, afterId);
249
+ if (!before) { console.error(`before session not found: ${beforeId}`); process.exit(1); }
250
+ if (!after) { console.error(`after session not found: ${afterId}`); process.exit(1); }
251
+ if (jsonMode) console.log(JSON.stringify({ before, after }, null, 2));
252
+ else console.log(renderVs(before, after));
253
+ process.exit(0);
254
+ }
255
+
256
+ if (evalPath) {
257
+ // A/B: load frozen corpus, re-extract those exact sessions from the current
258
+ // trace, and score. (Sessions are immutable once finished, so this is stable;
259
+ // the value is comparing two frozen corpora captured before/after a rule change.)
260
+ const frozen = JSON.parse(readFileSync(resolve(evalPath), 'utf8'));
261
+ const wantIds = new Set((frozen.cases || []).map((c) => c.session_id));
262
+ const rows = readRows(tracePath).filter((r) => r.kind === 'tool' && wantIds.has(sessionId(r)));
263
+ const bySession = groupBy(rows, sessionId);
264
+ const cases = [...bySession.entries()].map(([sid, tr]) => buildCase(sid, tr));
265
+ const score = scoreCorpus(cases);
266
+ if (jsonMode) console.log(JSON.stringify({ eval: resolve(evalPath), ...score }, null, 2));
267
+ else {
268
+ console.log(`routing eval of frozen corpus (${resolve(evalPath)})`);
269
+ console.log(`cases=${score.total} flagged=${score.flagged_cases} clean=${score.clean_pct}%`);
270
+ for (const k of FLAG_KEYS) console.log(`- ${k.padEnd(20)} ${score.by_flag[k]}`);
271
+ }
272
+ process.exit(0);
273
+ }
274
+
275
+ const corpus = buildCorpus(readRows(tracePath), { limit, sinceTs, agentFilter });
276
+ if (savePath) {
277
+ writeFileSync(resolve(savePath), JSON.stringify(corpus, null, 2));
278
+ console.error(`saved ${corpus.emitted} cases → ${resolve(savePath)}`);
279
+ }
280
+ if (jsonMode) console.log(JSON.stringify(corpus, null, 2));
281
+ else console.log(renderText(corpus, true));