mixdog 0.8.0 → 0.9.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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -2,7 +2,7 @@
2
2
  import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- import { BRIDGE_TOOL, createStandaloneBridge } from '../src/standalone/bridge-tool.mjs';
5
+ import { AGENT_TOOL, createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
6
6
  import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
7
7
  import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
8
8
  import { initProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
@@ -28,27 +28,19 @@ function sleep(ms) {
28
28
  return new Promise((resolve) => setTimeout(resolve, ms));
29
29
  }
30
30
 
31
- function jobId(text) {
32
- return String(text).match(/bridge job: (\S+)/)?.[1] || null;
31
+ function taskId(text) {
32
+ return String(text).match(/agent task: (\S+)/)?.[1] || null;
33
33
  }
34
34
 
35
35
  async function main() {
36
- const leadToolRules = readFileSync('src/rules/lead/00-tool-lead.md', 'utf8');
37
- const workflowRules = readFileSync('src/defaults/user-workflow.md', 'utf8');
38
- assert(/Use `bridge` to delegate actual scoped work/i.test(leadToolRules), 'lead rules must direct scoped work to bridge');
39
- assert(/parallelize independent files\/concerns/i.test(leadToolRules), 'lead rules must keep independent work parallel');
40
- assert(/Use bridge workers/i.test(workflowRules) && /parallelizes useful work/i.test(workflowRules), 'workflow rules must recommend bridge parallelism');
41
- assert(/Prefer async by default/i.test(BRIDGE_TOOL.description || '') && /distinct tags/i.test(BRIDGE_TOOL.description || '') && /completion notification/i.test(BRIDGE_TOOL.description || ''), 'bridge tool description must expose async parallel tags');
36
+ const leadToolRules = readFileSync('src/rules/lead/lead-tool.md', 'utf8');
37
+ const workflowRules = readFileSync('src/workflows/default/WORKFLOW.md', 'utf8');
38
+ assert(/Use `agent` for scoped implementation/i.test(leadToolRules), 'lead rules must direct scoped work to agents');
39
+ assert(/delegat(?:es|ing) them concurrently/i.test(workflowRules), 'workflow rules must keep independent work parallel');
40
+ assert(/always start background tasks/i.test(AGENT_TOOL.description || '') && /distinct tags/i.test(AGENT_TOOL.description || '') && /completion notification/i.test(AGENT_TOOL.description || ''), 'agent tool description must expose async parallel tags');
42
41
 
43
42
  mkdirSync(dataDir, { recursive: true });
44
43
  await initProviders({ 'openai-oauth': { enabled: true } });
45
- writeFileSync(join(dataDir, 'user-workflow.json'), JSON.stringify({
46
- roles: [
47
- { name: 'worker', preset: 'fake-worker', permission: 'full', maxLoopIterations: 3, idleTimeoutMs: 5000 },
48
- { name: 'reviewer', preset: 'fake-reviewer', permission: 'read' },
49
- { name: 'debugger', preset: 'fake-debugger', permission: 'read' },
50
- ],
51
- }));
52
44
  writeFileSync(join(root, 'feature.txt'), 'alpha\nbeta\n', 'utf8');
53
45
  writeFileSync(join(root, 'notes.txt'), 'TODO investigate timeout\n', 'utf8');
54
46
 
@@ -56,6 +48,11 @@ async function main() {
56
48
  loadConfig() {
57
49
  return {
58
50
  providers: { 'openai-oauth': { enabled: true } },
51
+ agents: {
52
+ worker: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'medium', fast: true },
53
+ reviewer: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
54
+ debugger: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
55
+ },
59
56
  presets: [
60
57
  { id: 'fake-worker', name: 'Fake Worker', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'full', effort: 'medium', fast: true },
61
58
  { id: 'fake-reviewer', name: 'Fake Reviewer', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'readonly', effort: 'low' },
@@ -64,7 +61,7 @@ async function main() {
64
61
  };
65
62
  },
66
63
  resolveRuntimeSpec(preset, ctx) {
67
- return { lane: 'bridge', scopeKey: `smoke:${ctx.agentId}`, provider: preset.provider, model: preset.model };
64
+ return { lane: 'agent', scopeKey: `smoke:${ctx.agentId}`, provider: preset.provider, model: preset.model };
68
65
  },
69
66
  };
70
67
  const reg = { initProviders };
@@ -130,14 +127,14 @@ async function main() {
130
127
  getSessionLastProgressAt,
131
128
  askSession: fakeAskSession,
132
129
  };
133
- const bridge = createStandaloneBridge({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
130
+ const agentRunner = createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
134
131
 
135
132
  async function waitJob(out, pattern, label) {
136
- const id = jobId(out);
137
- assert(id, `missing job id for ${label}: ${out}`);
133
+ const id = taskId(out);
134
+ assert(id, `missing task ID for ${label}: ${out}`);
138
135
  let last = '';
139
136
  for (let i = 0; i < 40; i += 1) {
140
- last = await bridge.execute({ type: 'read', jobId: id }, { invocationSource: 'model-tool', cwd: root });
137
+ last = await agentRunner.execute({ type: 'read', task_id: id }, { invocationSource: 'model-tool', cwd: root });
141
138
  if (pattern.test(last)) return last;
142
139
  if (/^Error[\s:[]/.test(last) || /status: error/.test(last)) break;
143
140
  await sleep(100);
@@ -145,7 +142,15 @@ async function main() {
145
142
  throw new Error(`${label} did not finish with ${pattern}: ${last}`);
146
143
  }
147
144
 
148
- const spawnOut = await bridge.execute({
145
+ async function waitAgentTag(tag, label) {
146
+ for (let i = 0; i < 20; i += 1) {
147
+ if (listSessions().some((session) => session?.agentTag === tag && !session.closed)) return;
148
+ await sleep(25);
149
+ }
150
+ throw new Error(`${label} did not register agent tag ${tag}`);
151
+ }
152
+
153
+ const spawnOut = await agentRunner.execute({
149
154
  type: 'spawn',
150
155
  role: 'worker',
151
156
  tag: 'impl1',
@@ -154,22 +159,22 @@ async function main() {
154
159
  }, { invocationSource: 'model-tool', cwd: root });
155
160
  await waitJob(spawnOut, /worker wrote feature/, 'worker write');
156
161
 
157
- const sendOut = await bridge.execute({
162
+ const sendOut = await agentRunner.execute({
158
163
  type: 'send',
159
164
  tag: 'impl1',
160
165
  message: 'run verification: inspect feature.txt using bash',
161
166
  }, { invocationSource: 'model-tool', cwd: root });
162
- assert(/bridge job:/.test(sendOut), `completed worker send should be async job, got ${sendOut}`);
167
+ assert(/agent task:/.test(sendOut), `completed worker send should be async task, got ${sendOut}`);
163
168
  await waitJob(sendOut, /beta from worker/, 'worker verify');
164
169
 
165
- const reviewOut = await bridge.execute({
170
+ const reviewOut = await agentRunner.execute({
166
171
  type: 'spawn',
167
172
  role: 'reviewer',
168
173
  tag: 'rev1',
169
174
  cwd: root,
170
175
  prompt: 'review feature.txt for the worker change',
171
176
  }, { invocationSource: 'model-tool', cwd: root });
172
- const debugOut = await bridge.execute({
177
+ const debugOut = await agentRunner.execute({
173
178
  type: 'spawn',
174
179
  role: 'debugger',
175
180
  tag: 'dbg1',
@@ -180,21 +185,21 @@ async function main() {
180
185
  await waitJob(debugOut, /debugger TODO/, 'debugger');
181
186
 
182
187
  const parallelSpawns = await Promise.all([
183
- bridge.execute({
188
+ agentRunner.execute({
184
189
  type: 'spawn',
185
190
  role: 'worker',
186
191
  tag: 'parWorker',
187
192
  cwd: root,
188
193
  prompt: 'parallel slow worker task',
189
194
  }, { invocationSource: 'model-tool', cwd: root }),
190
- bridge.execute({
195
+ agentRunner.execute({
191
196
  type: 'spawn',
192
197
  role: 'reviewer',
193
198
  tag: 'parReviewer',
194
199
  cwd: root,
195
200
  prompt: 'parallel slow review task',
196
201
  }, { invocationSource: 'model-tool', cwd: root }),
197
- bridge.execute({
202
+ agentRunner.execute({
198
203
  type: 'spawn',
199
204
  role: 'debugger',
200
205
  tag: 'parDebugger',
@@ -202,33 +207,34 @@ async function main() {
202
207
  prompt: 'parallel slow debug task',
203
208
  }, { invocationSource: 'model-tool', cwd: root }),
204
209
  ]);
205
- assert(parallelSpawns.every((out) => /bridge job:/.test(out)), `parallel spawns must return jobs: ${parallelSpawns.join('\n---\n')}`);
210
+ assert(parallelSpawns.every((out) => /agent task:/.test(out)), `parallel spawns must return tasks: ${parallelSpawns.join('\n---\n')}`);
206
211
  await Promise.all([
207
212
  waitJob(parallelSpawns[0], /ack worker/, 'parallel worker'),
208
213
  waitJob(parallelSpawns[1], /reviewer ok/, 'parallel reviewer'),
209
214
  waitJob(parallelSpawns[2], /debugger TODO/, 'parallel debugger'),
210
215
  ]);
211
- assert(maxActiveAsks >= 2, `bridge workers did not overlap; maxActiveAsks=${maxActiveAsks}`);
216
+ assert(maxActiveAsks >= 2, `agents did not overlap; maxActiveAsks=${maxActiveAsks}`);
212
217
 
213
- const busyOut = await bridge.execute({
218
+ const busyOut = await agentRunner.execute({
214
219
  type: 'spawn',
215
220
  role: 'worker',
216
221
  tag: 'busy1',
217
222
  cwd: root,
218
223
  prompt: 'long busy worker task',
219
224
  }, { invocationSource: 'model-tool', cwd: root });
220
- const queued = await bridge.execute({
225
+ await waitAgentTag('busy1', 'busy worker');
226
+ const queued = await agentRunner.execute({
221
227
  type: 'send',
222
228
  tag: 'busy1',
223
229
  message: 'follow-up while still busy',
224
230
  }, { invocationSource: 'model-tool', cwd: root });
225
- assert(/bridge message queued/.test(queued), `busy send should queue, got ${queued}`);
231
+ assert(/agent message queued/.test(queued), `busy send should queue, got ${queued}`);
226
232
  await waitJob(busyOut, /ack worker/, 'busy worker');
227
233
 
228
- const missing = await bridge.execute({ type: 'read', jobId: 'job_missing_live_smoke' }, { invocationSource: 'model-tool', cwd: root });
229
- assert(/^Error[\s:[]/.test(missing), 'missing bridge job should be Error result');
234
+ const missing = await agentRunner.execute({ type: 'read', task_id: 'task_missing_live_smoke' }, { invocationSource: 'model-tool', cwd: root });
235
+ assert(/^Error[\s:[]/.test(missing), 'missing agent task should be Error result');
230
236
  assert(readFileSync(join(root, 'feature.txt'), 'utf8').includes('beta from worker'), 'final file missing worker edit');
231
- bridge.closeAll('live-worker-smoke-end');
237
+ agentRunner.closeAll('live-worker-smoke-end');
232
238
  process.stdout.write('live worker smoke passed\n');
233
239
  }
234
240
 
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { resolve } from 'node:path';
5
+
6
+ function argValue(name, fallback = null) {
7
+ const idx = process.argv.indexOf(name);
8
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
9
+ const pref = `${name}=`;
10
+ const hit = process.argv.find((arg) => arg.startsWith(pref));
11
+ return hit ? hit.slice(pref.length) : fallback;
12
+ }
13
+
14
+ function intArg(name, fallback) {
15
+ const n = Number.parseInt(argValue(name, String(fallback)), 10);
16
+ return Number.isFinite(n) && n > 0 ? n : fallback;
17
+ }
18
+
19
+ const pathArg = argValue('--path', null);
20
+ const dataDir = argValue('--data-dir', null);
21
+ const sinceArg = argValue('--since', null);
22
+ const kindFilter = argValue('--kind', null);
23
+ const last = intArg('--last', 5000);
24
+ const limit = intArg('--limit', 20);
25
+ const slowMs = intArg('--slow-ms', 3000);
26
+ const jsonMode = process.argv.includes('--json');
27
+
28
+ const mixdogHome = process.env.MIXDOG_HOME || resolve(homedir(), '.mixdog');
29
+ const mixdogDataDir = process.env.MIXDOG_DATA_DIR || resolve(mixdogHome, 'data');
30
+
31
+ function unique(values) {
32
+ const seen = new Set();
33
+ const out = [];
34
+ for (const value of values) {
35
+ const key = String(value || '');
36
+ if (!key || seen.has(key)) continue;
37
+ seen.add(key);
38
+ out.push(value);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function defaultTraceFiles() {
44
+ if (pathArg) return [resolve(pathArg)];
45
+ const dirs = dataDir
46
+ ? [resolve(dataDir)]
47
+ : [resolve(process.cwd(), '.mixdog', 'data'), mixdogDataDir];
48
+ return unique(dirs.flatMap((dir) => [
49
+ resolve(dir, 'history', 'agent-trace.jsonl.1'),
50
+ resolve(dir, 'history', 'agent-trace.jsonl'),
51
+ resolve(dir, 'history', 'agent-trace.jsonl.1'),
52
+ resolve(dir, 'history', 'agent-trace.jsonl'),
53
+ ]));
54
+ }
55
+
56
+ function parseSince(value) {
57
+ const raw = String(value || '').trim();
58
+ if (!raw) return null;
59
+ if (/^now$/i.test(raw)) return Date.now();
60
+ if (/^\d+$/.test(raw)) {
61
+ const n = Number(raw);
62
+ return n > 10_000_000_000 ? n : n * 1000;
63
+ }
64
+ const rel = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
65
+ if (rel) {
66
+ const n = Number(rel[1]);
67
+ const unit = rel[2].toLowerCase();
68
+ const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
69
+ return Date.now() - n * mult;
70
+ }
71
+ const parsed = Date.parse(raw);
72
+ return Number.isFinite(parsed) ? parsed : null;
73
+ }
74
+
75
+ function readRows(file) {
76
+ if (!existsSync(file)) return [];
77
+ return readFileSync(file, 'utf8')
78
+ .split(/\r?\n/)
79
+ .filter(Boolean)
80
+ .map((line) => {
81
+ try {
82
+ return { file, ...JSON.parse(line) };
83
+ } catch {
84
+ return { file, kind: 'parse_error', payload: { line: line.slice(0, 300) } };
85
+ }
86
+ });
87
+ }
88
+
89
+ function payload(row) {
90
+ return row && row.payload && typeof row.payload === 'object' ? row.payload : {};
91
+ }
92
+
93
+ function field(row, name) {
94
+ if (row && row[name] != null) return row[name];
95
+ const p = payload(row);
96
+ return p[name] != null ? p[name] : null;
97
+ }
98
+
99
+ function numberField(row, name) {
100
+ const n = Number(field(row, name));
101
+ return Number.isFinite(n) ? n : null;
102
+ }
103
+
104
+ function countBy(rows, fn) {
105
+ const map = new Map();
106
+ for (const row of rows) {
107
+ const key = String(fn(row) ?? '(none)');
108
+ map.set(key, (map.get(key) || 0) + 1);
109
+ }
110
+ return Object.fromEntries([...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
111
+ }
112
+
113
+ function values(rows, name) {
114
+ return rows.map((row) => numberField(row, name)).filter((n) => n != null);
115
+ }
116
+
117
+ function percentile(sorted, p) {
118
+ if (sorted.length === 0) return null;
119
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
120
+ return sorted[idx];
121
+ }
122
+
123
+ function stats(nums) {
124
+ const arr = nums.filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
125
+ if (arr.length === 0) return null;
126
+ const sum = arr.reduce((a, b) => a + b, 0);
127
+ return {
128
+ n: arr.length,
129
+ sum,
130
+ avg: Math.round(sum / arr.length),
131
+ p50: percentile(arr, 50),
132
+ p90: percentile(arr, 90),
133
+ p99: percentile(arr, 99),
134
+ max: arr[arr.length - 1],
135
+ };
136
+ }
137
+
138
+ function formatStats(s) {
139
+ if (!s) return 'n=0';
140
+ return `n=${s.n} avg=${s.avg}ms p50=${s.p50}ms p90=${s.p90}ms p99=${s.p99}ms max=${s.max}ms`;
141
+ }
142
+
143
+ function short(value, max = 140) {
144
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
145
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
146
+ }
147
+
148
+ function timeLabel(ts) {
149
+ const n = Number(ts);
150
+ if (!Number.isFinite(n) || n <= 0) return '-';
151
+ try {
152
+ return new Date(n).toISOString();
153
+ } catch {
154
+ return String(ts);
155
+ }
156
+ }
157
+
158
+ function topStatsBy(rows, groupName, valueName, minValue = null) {
159
+ const groups = new Map();
160
+ for (const row of rows) {
161
+ const value = numberField(row, valueName);
162
+ if (value == null) continue;
163
+ if (minValue != null && value < minValue) continue;
164
+ const key = String(field(row, groupName) || '(none)');
165
+ if (!groups.has(key)) groups.set(key, []);
166
+ groups.get(key).push(value);
167
+ }
168
+ return [...groups.entries()]
169
+ .map(([key, nums]) => ({ key, ...stats(nums) }))
170
+ .sort((a, b) => b.max - a.max || b.p90 - a.p90 || b.n - a.n);
171
+ }
172
+
173
+ function topStatsByTotal(rows, groupName, valueName, minValue = null) {
174
+ return topStatsBy(rows, groupName, valueName, minValue)
175
+ .sort((a, b) => b.sum - a.sum || b.max - a.max || b.p90 - a.p90 || b.n - a.n);
176
+ }
177
+
178
+ function printCounts(label, obj, max = 12) {
179
+ const parts = Object.entries(obj).slice(0, max).map(([k, v]) => `${k}:${v}`);
180
+ console.log(`${label}: ${parts.join(', ') || '(none)'}`);
181
+ }
182
+
183
+ const files = defaultTraceFiles();
184
+ const sinceTs = parseSince(sinceArg);
185
+ const allRows = files.flatMap(readRows)
186
+ .filter((row) => sinceTs == null || Number(row.ts || 0) >= sinceTs)
187
+ .filter((row) => !kindFilter || String(row.kind || '') === kindFilter)
188
+ .sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
189
+ const rows = allRows.slice(-last);
190
+
191
+ const kindCounts = countBy(rows, (row) => row.kind || 'unknown');
192
+ const transportRows = rows.filter((row) => row.kind === 'transport');
193
+ const sseRows = rows.filter((row) => row.kind === 'sse');
194
+ const fetchRows = rows.filter((row) => row.kind === 'fetch');
195
+ const toolRows = rows.filter((row) => row.kind === 'tool');
196
+ const toolSlowRows = rows.filter((row) => row.kind === 'tool_slow' || (row.kind === 'tool' && (numberField(row, 'tool_ms') || 0) >= slowMs));
197
+ const cacheSlowRows = rows.filter((row) => row.kind === 'cache_lane_slow');
198
+ const explicitCacheBreakRows = rows.filter((row) => row.kind === 'cache_break');
199
+ const explicitCacheBreakKeys = new Set(explicitCacheBreakRows.map((row) => `${row.session_id || ''}:${row.iteration ?? ''}`));
200
+ const cacheBreakRows = explicitCacheBreakRows
201
+ .concat(transportRows.filter((row) => field(row, 'chain_delta_reason') && !explicitCacheBreakKeys.has(`${row.session_id || ''}:${row.iteration ?? ''}`)));
202
+ const fallbackRows = rows.filter((row) => row.kind === 'transport_fallback');
203
+
204
+ const report = {
205
+ analyzed: rows.length,
206
+ matched: allRows.length,
207
+ since: sinceTs ? new Date(sinceTs).toISOString() : null,
208
+ filters: {
209
+ kind: kindFilter,
210
+ last,
211
+ slow_ms: slowMs,
212
+ },
213
+ sources: files.filter(existsSync),
214
+ kinds: kindCounts,
215
+ transport: {
216
+ modes: countBy(transportRows, (row) => field(row, 'ws_mode') || field(row, 'transport') || '(unknown)'),
217
+ rate_policies: countBy(transportRows.filter((row) => field(row, 'cache_lane_rate_policy')), (row) => field(row, 'cache_lane_rate_policy')),
218
+ delta_reasons: countBy(transportRows.filter((row) => field(row, 'chain_delta_reason')), (row) => field(row, 'chain_delta_reason')),
219
+ cache_lane_rate_wait_ms: stats(values(transportRows, 'cache_lane_rate_wait_ms')),
220
+ cache_lane_rate_reacquire_wait_ms: stats(values(transportRows, 'cache_lane_rate_reacquire_wait_ms')),
221
+ cache_lane_wait_ms: stats(values(transportRows, 'cache_lane_wait_ms')),
222
+ cache_lane_slow: cacheSlowRows.length,
223
+ fallback: countBy(fallbackRows, (row) => field(row, 'reason') || field(row, 'fallback_reason') || '(unknown)'),
224
+ },
225
+ sse: {
226
+ ttft_ms: stats(values(sseRows, 'ttft_ms')),
227
+ sse_parse_ms: stats(values(sseRows, 'sse_parse_ms')),
228
+ },
229
+ fetch: {
230
+ headers_ms: stats(values(fetchRows, 'headers_ms')),
231
+ },
232
+ tools: {
233
+ slow_ms: slowMs,
234
+ by_tool: topStatsBy(toolRows, 'tool_name', 'tool_ms'),
235
+ by_tool_total: topStatsByTotal(toolRows, 'tool_name', 'tool_ms'),
236
+ slow_by_tool: topStatsBy(toolRows, 'tool_name', 'tool_ms', slowMs),
237
+ },
238
+ cache_breaks: {
239
+ count: cacheBreakRows.length,
240
+ reasons: countBy(cacheBreakRows, (row) => field(row, 'reason') || field(row, 'chain_delta_reason') || '(unknown)'),
241
+ },
242
+ samples: {
243
+ slow_tools: toolSlowRows
244
+ .slice()
245
+ .sort((a, b) => Number(numberField(b, 'tool_ms') || 0) - Number(numberField(a, 'tool_ms') || 0))
246
+ .slice(0, limit)
247
+ .map((row) => ({
248
+ ts: timeLabel(row.ts),
249
+ session_id: row.session_id || null,
250
+ iteration: row.iteration ?? null,
251
+ tool_name: field(row, 'tool_name'),
252
+ tool_ms: numberField(row, 'tool_ms'),
253
+ role: field(row, 'role'),
254
+ model: field(row, 'model'),
255
+ args: payload(row).tool_args || row.tool_args || null,
256
+ })),
257
+ cache_lane_slow: cacheSlowRows.slice(-limit).map((row) => ({
258
+ ts: timeLabel(row.ts),
259
+ session_id: row.session_id || null,
260
+ iteration: row.iteration ?? null,
261
+ event: field(row, 'event'),
262
+ provider: field(row, 'provider'),
263
+ model: field(row, 'model'),
264
+ payload: payload(row),
265
+ })),
266
+ cache_breaks: cacheBreakRows.slice(-limit).map((row) => ({
267
+ ts: timeLabel(row.ts),
268
+ session_id: row.session_id || null,
269
+ iteration: row.iteration ?? null,
270
+ reason: field(row, 'reason') || field(row, 'chain_delta_reason'),
271
+ provider: field(row, 'provider'),
272
+ model: field(row, 'model'),
273
+ payload: payload(row),
274
+ })),
275
+ },
276
+ };
277
+
278
+ if (jsonMode) {
279
+ console.log(JSON.stringify(report, null, 2));
280
+ process.exit(0);
281
+ }
282
+
283
+ console.log(`llm trace: ${rows.length}/${allRows.length} rows analyzed`);
284
+ if (sinceTs) console.log(`since: ${new Date(sinceTs).toISOString()}`);
285
+ if (kindFilter) console.log(`filter: kind=${kindFilter}`);
286
+ console.log(`sources: ${report.sources.join(', ') || '(none)'}`);
287
+ printCounts('kinds', report.kinds);
288
+ printCounts('transport modes', report.transport.modes);
289
+ printCounts('cache lane rate policies', report.transport.rate_policies);
290
+ printCounts('delta/full reasons', report.transport.delta_reasons);
291
+ printCounts('transport fallback', report.transport.fallback);
292
+ console.log(`cache lane rate wait: ${formatStats(report.transport.cache_lane_rate_wait_ms)}`);
293
+ console.log(`cache lane rate reacquire wait: ${formatStats(report.transport.cache_lane_rate_reacquire_wait_ms)}`);
294
+ console.log(`cache lane queue wait: ${formatStats(report.transport.cache_lane_wait_ms)}`);
295
+ console.log(`cache lane slow rows: ${report.transport.cache_lane_slow}`);
296
+ console.log(`ttft: ${formatStats(report.sse.ttft_ms)}`);
297
+ console.log(`sse parse: ${formatStats(report.sse.sse_parse_ms)}`);
298
+ console.log(`fetch headers: ${formatStats(report.fetch.headers_ms)}`);
299
+ console.log(`cache breaks: ${report.cache_breaks.count}`);
300
+ printCounts('cache break reasons', report.cache_breaks.reasons);
301
+ console.log('tools by total time:');
302
+ for (const row of report.tools.by_tool_total.slice(0, 12)) {
303
+ console.log(`- ${row.key}: n=${row.n} total=${Math.round(row.sum)}ms avg=${row.avg}ms p90=${row.p90}ms max=${row.max}ms`);
304
+ }
305
+ if (report.tools.by_tool_total.length === 0) console.log('- (none)');
306
+ console.log('slow tools:');
307
+ for (const row of report.tools.slow_by_tool.slice(0, 12)) {
308
+ console.log(`- ${row.key}: n=${row.n} avg=${row.avg}ms p90=${row.p90}ms max=${row.max}ms`);
309
+ }
310
+ if (report.tools.slow_by_tool.length === 0) console.log('- (none)');
311
+ console.log('slow tool samples:');
312
+ for (const row of report.samples.slow_tools) {
313
+ console.log(`- ${row.ts} ${row.tool_name} ${row.tool_ms}ms role=${row.role || '-'} args=${short(JSON.stringify(row.args || {}), 180)}`);
314
+ }
315
+ if (report.samples.slow_tools.length === 0) console.log('- (none)');
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Regression: mergeMetaValue uses jsonb shallow merge (not full replace).
3
+ import test from 'node:test'
4
+ import assert from 'node:assert/strict'
5
+ import { mergeMetaValue } from '../src/runtime/memory/lib/memory.mjs'
6
+
7
+ test('mergeMetaValue issues INSERT ... ON CONFLICT DO UPDATE with jsonb ||', async () => {
8
+ const calls = []
9
+ const db = {
10
+ async query(sql, params) {
11
+ calls.push({ sql, params })
12
+ },
13
+ }
14
+ await mergeMetaValue(db, 'state.cycle_last_run', { cycle1: 100, cycle2_last_error: '' })
15
+ assert.equal(calls.length, 1)
16
+ assert.match(calls[0].sql, /COALESCE\(meta\.value/)
17
+ assert.match(calls[0].sql, /\|\| EXCLUDED\.value/)
18
+ assert.deepEqual(calls[0].params, ['state.cycle_last_run', '{"cycle1":100,"cycle2_last_error":""}'])
19
+ })
20
+
@@ -13,14 +13,19 @@ function assert(condition, message) {
13
13
  if (!condition) throw new Error(message);
14
14
  }
15
15
 
16
- function assertCleanOutput(name, value, { maxLines = 2, maxBullets = 3 } = {}) {
16
+ const DEFAULT_REPORT_LABELS = new Set(['바뀐 점', '확인한 것', '남은 리스크/다음 단계', '다음 단계']);
17
+
18
+ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
17
19
  const text = String(value || '').trim();
18
20
  assert(text, `${name}: empty output`);
19
21
  assert(!/\n\s*\n/.test(text), `${name}: multiple paragraphs are not compact`);
20
22
  assert(!/^\s*#{1,6}\s/m.test(text), `${name}: heading found`);
21
23
  assert(!/^\s*\d+[.)]\s/m.test(text), `${name}: numbered list found`);
22
24
  assert(!/^\s{2,}[-*]\s/m.test(text), `${name}: nested bullet found`);
23
- assert(!/^\s*[\p{L}\p{N}][\p{L}\p{N} _-]{0,30}:\s/um.test(text), `${name}: report label found`);
25
+ for (const line of text.split(/\r?\n/)) {
26
+ const match = line.match(/^\s*(?:[-*]\s+)?([\p{L}\p{N}][\p{L}\p{N} _/-]{0,30}):\s/u);
27
+ if (match) assert(allowedLabels.has(match[1]), `${name}: disallowed report label found`);
28
+ }
24
29
  assert(!/\b(?:tool trace|session metadata|model metadata|searched-path list)\b/i.test(text), `${name}: tool/meta trace found`);
25
30
  assert(!/\b(?:Mapped|Searched|Read|Called) for \d/i.test(text), `${name}: timing/tool status found`);
26
31
  assert(!/^(?:네|예|맞아요|좋아요|알겠습니다|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
@@ -43,15 +48,42 @@ function assertCleanOutput(name, value, { maxLines = 2, maxBullets = 3 } = {}) {
43
48
  const defaultStyle = readFileSync(join(root, 'src', 'output-styles', 'default.md'), 'utf8');
44
49
  for (const required of [
45
50
  'name: default',
46
- 'Uniformity beats local cleverness.',
47
- 'One answer only.',
48
- 'Hard cap user-visible replies at 2 short sentences',
49
- 'Explore/retrieval results are evidence only',
50
- 'Do not use label prefixes',
51
+ 'Concise engineering summaries',
52
+ 'Mixdog default — concise engineering summaries',
53
+ 'Use labels such as',
54
+ '`바뀐 점`, `확인한 것`,',
55
+ 'Synthesize agent or retrieval results',
56
+ 'Do not hide blockers',
51
57
  ]) {
52
58
  assert(defaultStyle.includes(required), `default.md missing: ${required}`);
53
59
  }
54
60
  assert(!defaultStyle.includes('Claude Code-compact'), 'default.md must not reference the old compact style name');
61
+ assert(!defaultStyle.includes('Hard cap user-visible replies'), 'default.md must not hard-cap replies to two short sentences');
62
+ assert(!defaultStyle.includes('Be short and direct.'), 'default.md must not keep the old generic concise preset');
63
+ assert(!defaultStyle.includes('Practical concise — outcome-first'), 'default.md must not use the simple preset body');
64
+
65
+ const simpleStyle = readFileSync(join(root, 'src', 'output-styles', 'simple.md'), 'utf8');
66
+ for (const required of [
67
+ 'name: simple',
68
+ 'Outcome-first concise handoffs for coding work',
69
+ 'Practical concise — outcome-first handoffs',
70
+ 'file_path:line_number',
71
+ 'controlled detail',
72
+ 'Synthesize agent or retrieval results',
73
+ 'Do not hide blockers',
74
+ 'if verification was',
75
+ ]) {
76
+ assert(simpleStyle.includes(required), `simple.md missing: ${required}`);
77
+ }
78
+ assert(!simpleStyle.includes('Mixdog default — concise engineering summaries'), 'simple.md must not duplicate default preset body');
79
+ for (const [name, style] of Object.entries({
80
+ default: defaultStyle,
81
+ simple: simpleStyle,
82
+ extreme: readFileSync(join(root, 'src', 'output-styles', 'extreme-simple.md'), 'utf8'),
83
+ })) {
84
+ assert(!style.includes('pre-tool preamble'), `${name} output style must not own language preamble rules`);
85
+ assert(!style.includes('selected/default user language'), `${name} output style must not duplicate profile language rules`);
86
+ }
55
87
 
56
88
  const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-output-style-smoke-'));
57
89
  try {
@@ -62,23 +94,31 @@ try {
62
94
  assert(outputStyleIndex > leadRules.lastIndexOf('# User Workflow'), 'Lead output style must be injected after workflow/user context');
63
95
 
64
96
  mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
65
- writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({ outputStyle: 'custom-smoke' }));
97
+ writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
98
+ agent: { profile: { title: '재영님', language: 'system' } },
99
+ outputStyle: 'custom-smoke',
100
+ }));
66
101
  writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
67
102
  const customRules = rulesBuilder.buildInjectionContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
68
103
  assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
69
- assert(!customRules.includes('Default concise engineering replies'), 'custom outputStyle should not append default style');
104
+ assert(!customRules.includes('Mixdog default — concise engineering summaries'), 'custom outputStyle should not append default style');
105
+ const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
106
+ assert(profileMeta.includes('Use "재영님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
107
+ assert(profileMeta.includes('Do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
108
+ assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
109
+ assert(profileMeta.includes('including pre-tool preambles'), 'profile language must cover pre-tool preambles');
70
110
  } finally {
71
111
  rmSync(dataDir, { recursive: true, force: true });
72
112
  }
73
113
 
74
114
  const goodOutputs = {
75
- explanation: '기본 출력은 한 문장으로 결론만 말하고, 추가 설명은 사용자가 요청할 때만 붙입니다.',
76
- implementation: 'default 출력 스타일을 문장 우선으로 조였고 `npm run smoke:output`으로 확인했습니다.',
77
- crowded: '- default 스타일은 문장 우선입니다.\n- 상세/후보 요청이 있을 때만 목록을 펼칩니다.',
115
+ explanation: '기본 출력은 결과 줄과 근거 가지로 끝내고, 최종 보고만 짧은 bullet 라벨을 씁니다.',
116
+ implementation: '- 바뀐 점: `src/output-styles/default.md`에 이전 simple 프리셋을 반영했습니다.\n- 확인한 것: `node scripts/output-style-smoke.mjs`를 실행했습니다.',
117
+ crowded: '- 바뀐 점: default 요약 라벨 보고, simple은 handoff용 controlled detail을 씁니다.\n- 확인한 것: smoke 예시가 compact guardrail을 유지합니다.',
78
118
  blocker: '`src/output-styles/default.md`를 찾을 수 없어 변경이 막혔습니다.',
79
- semicolon: '`default.md`를 다듬었고; 출력 스모크가 예시 응답 모양을 검증합니다.',
119
+ semicolon: '`default.md`를 갱신했고; 출력 스모크가 예시 응답 모양을 검증합니다.',
80
120
  };
81
- for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output);
121
+ for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
82
122
 
83
123
  const badOutputs = {
84
124
  heading: '## Summary\nDone.',
@@ -91,10 +131,11 @@ const badOutputs = {
91
131
  timing: 'Mapped for 2m 32s\n\ndefault 스타일을 수정했습니다.',
92
132
  tooManySentences: '수정했습니다. 검증했습니다. 보고했습니다.',
93
133
  mixed: '수정했습니다.\n- 검증했습니다.',
134
+ tooManyBullets: '- 바뀐 점: A\n- 확인한 것: B\n- 다음 단계: C\n- 남은 리스크/다음 단계: D',
94
135
  };
95
136
  for (const [name, output] of Object.entries(badOutputs)) {
96
137
  let failed = false;
97
- try { assertCleanOutput(`bad:${name}`, output); } catch { failed = true; }
138
+ try { assertCleanOutput(`bad:${name}`, output, { allowedLabels: DEFAULT_REPORT_LABELS }); } catch { failed = true; }
98
139
  assert(failed, `bad sample unexpectedly passed: ${name}`);
99
140
  }
100
141