mixdog 0.9.0 → 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 (240) hide show
  1. package/package.json +10 -3
  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/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. 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));