mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,515 @@
1
+ // Unified-gate cluster extracted from memory-cycle2.mjs: prompt formatting,
2
+ // pipe-verdict parsing/validation, the rules-digest cache, the single LLM
3
+ // gate pass (runUnifiedGate) and the Sonnet re-judge cascade. Facade
4
+ // (memory-cycle2.mjs) re-exports the public members unchanged.
5
+ import { existsSync, readFileSync } from 'fs'
6
+ import { join } from 'path'
7
+ import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
8
+ import { callAgentDispatch } from './agent-ipc.mjs'
9
+ import { listCore } from './core-memory-store.mjs'
10
+ import { __mixdogMemoryLog, throwIfAborted, resourceDir } from './memory-cycle2-shared.mjs'
11
+
12
+ export const CYCLE2_ACTIVE_TARGET_CAP = 100
13
+
14
+ // Status-based verb whitelist. 3-tier policy: pending → active/archived,
15
+ // active → active/archived/update/merge.
16
+ const STATUS_ALLOWED_VERBS = {
17
+ pending: new Set(['active', 'archived']),
18
+ active: new Set(['active', 'archived', 'update', 'merge']),
19
+ }
20
+ const NON_ARCHIVE_VERBS = new Set(['active', 'update', 'merge'])
21
+ // Union of every primary (status) verb across all statuses, plus the two
22
+ // non-verb line kinds. Used by the stray-index shift guard to decide whether
23
+ // a `idx|id|verb` line had a leading row index prepended by the LLM.
24
+ const ALL_PRIMARY_VERBS = new Set(['active', 'archived', 'update', 'merge'])
25
+ const isShiftFollowToken = (tok) => {
26
+ const v = String(tok ?? '').trim().toLowerCase()
27
+ return ALL_PRIMARY_VERBS.has(v) || v === 'why' || v === 'core'
28
+ }
29
+
30
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
31
+ return await llmCall({
32
+ agent: 'cycle2-agent',
33
+ taskType: 'maintenance',
34
+ mode,
35
+ preset,
36
+ timeout,
37
+ cwd: null,
38
+ }, prompt)
39
+ }
40
+
41
+ function buildPidMap(rowSets) {
42
+ const pids = [...new Set(rowSets.flat().map(r => r.project_id).filter(Boolean))].sort()
43
+ return new Map(pids.map((p, i) => [p, `P${i + 1}`]))
44
+ }
45
+
46
+ function formatEntriesForPromotePrompt(rows, pidMap, opts = {}) {
47
+ if (!rows || rows.length === 0) return '(none)'
48
+ const map = pidMap ?? buildPidMap([rows])
49
+ // When numbered, prefix each row with its 1-based prompt-order ordinal so the
50
+ // gate LLM can echo a row number it can see, instead of inventing one. The
51
+ // ordinal domain (1..N) and the 5-digit batch-id domain must stay disjoint —
52
+ // see the ordinalToId invariant in runUnifiedGate.
53
+ const numbered = opts.numbered === true
54
+ const lines = rows.map((r, i) => {
55
+ const tag = r.project_id ? (map.get(r.project_id) ?? 'C') : 'C'
56
+ const stat = r.status ? `[${r.status}]` : '[?]'
57
+ const prefix = numbered ? `${i + 1}. ` : '- '
58
+ return `${prefix}id:${r.id} ${stat} ${tag} ${r.category} s:${r.score ?? 'n'} el:${r.element} sm:${String(r.summary || '').slice(0, 100)}`
59
+ })
60
+ if (map.size === 0) return lines.join('\n')
61
+ const legend = [...map.entries()].map(([p, t]) => `${t}=${p}`).concat('C=COMMON').join(', ')
62
+ return `# pid: ${legend}\n` + lines.join('\n')
63
+ }
64
+
65
+ // User-curated rows from core_entries — id-less, no status, no score; the
66
+ // LLM only needs element + summary + project tag to detect overlap with
67
+ // candidate entries below. Format kept terse so the prompt budget stays small.
68
+ function formatUserCoreForPrompt(rows, pidMap) {
69
+ if (!rows || rows.length === 0) return '(none)'
70
+ const map = pidMap ?? new Map()
71
+ return rows.map(r => {
72
+ const tag = r.project_id ? (map.get(r.project_id) ?? 'C') : 'C'
73
+ const sm = String(r.summary || '').slice(0, 200)
74
+ return `- ${tag} ${r.category}: ${r.element}${sm && sm !== r.element ? ` — ${sm}` : ''}`
75
+ }).join('\n')
76
+ }
77
+
78
+ // Parse pipe-format unified verdicts. Each line: <id>|<verb> [|...].
79
+ // Verbs validated against the row's current status via STATUS_ALLOWED_VERBS.
80
+ // Returns { actions, rejected } or null when no parseable lines.
81
+ function parseUnifiedFormat(raw, statusById, ordinalToId = null) {
82
+ if (raw == null) return null
83
+ const text = String(raw).trim()
84
+ if (!text) return { actions: [], rejected: new Set() }
85
+ const lines = text.split('\n')
86
+ const actions = []
87
+ const rejected = new Set()
88
+ const support = new Map()
89
+ let sawValid = false
90
+ // Resolve a first-field/merge token to a real batch id. The gate may echo
91
+ // either the exact 5-digit batch id OR the 1-based row ordinal shown in the
92
+ // numbered Entries block. The two domains are disjoint (asserted in
93
+ // runUnifiedGate), so an exact-id hit always wins and an unmatched value
94
+ // falls back to ordinal lookup; anything else is NaN (line treated invalid).
95
+ const resolveId = (tok) => {
96
+ const n = Number(String(tok ?? '').trim())
97
+ if (!Number.isFinite(n)) return NaN
98
+ if (statusById.has(n)) return n
99
+ if (ordinalToId && ordinalToId.has(n)) return ordinalToId.get(n)
100
+ return NaN
101
+ }
102
+ for (const rawLine of lines) {
103
+ const line = rawLine.trim()
104
+ if (!line) continue
105
+ if (line.startsWith('//') || line.startsWith('#')) continue
106
+ if (line.startsWith('```')) continue
107
+ const parts = line.split('|')
108
+ if (parts.length < 2) continue
109
+ // LLM sometimes prefixes a row index, emitting `idx|id|verdict` instead of
110
+ // `id|verdict`; parts[0] (the index) is a stray token and the line must be
111
+ // shifted before parsing. Strict invariant so a real 2-field `id|verdict`
112
+ // is never shifted into a 1-field line (which would throw on parts[1]):
113
+ // parts.length >= 3 AND parts[1] is a known batch id AND parts[2] is a
114
+ // valid primary verb / why / core (the shifted verdict slot).
115
+ // Trigger when EITHER parts[0] is not a known id (classic stray index) OR
116
+ // parts[0] IS known but parts[1] is not itself a valid verb — that covers
117
+ // `1|1|active`, where the stray index collides with a real batch id and the
118
+ // un-shifted reading would verb-reject the wrong row.
119
+ if (
120
+ parts.length >= 3 &&
121
+ statusById.has(Number(parts[1].trim())) &&
122
+ isShiftFollowToken(parts[2]) &&
123
+ (!statusById.has(Number(parts[0].trim())) || !isShiftFollowToken(parts[1]))
124
+ ) {
125
+ parts.shift()
126
+ }
127
+ const entryId = resolveId(parts[0])
128
+ const action = parts[1].trim().toLowerCase()
129
+ if (!Number.isFinite(entryId) || !action) continue
130
+ const status = statusById.get(entryId)
131
+ if (!status) continue
132
+ // Only mark as parse-ok when the id is known to the batch; a response
133
+ // composed entirely of unknown ids would otherwise return parse-ok with
134
+ // zero actions/rejections, leaving the rows un-reviewed and re-queued.
135
+ sawValid = true
136
+ if (action === 'core') {
137
+ actions.push({ entry_id: entryId, action: 'core', core_summary: parts.slice(2).join('|').trim().slice(0, 120) })
138
+ continue
139
+ }
140
+ if (action === 'why') {
141
+ const kind = (parts[2] ?? '').trim().toUpperCase()
142
+ const reason = parts.slice(3).join('|').replace(/\s+/g, ' ').trim().slice(0, 240)
143
+ if ((kind === 'A' || kind === 'B') && reason) {
144
+ support.set(entryId, { kind, reason })
145
+ }
146
+ continue
147
+ }
148
+ const allowed = STATUS_ALLOWED_VERBS[status]
149
+ if (!allowed || !allowed.has(action)) {
150
+ __mixdogMemoryLog(`[cycle2] verb rejected: id=${entryId} status=${status} verb=${action}\n`)
151
+ rejected.add(entryId)
152
+ continue
153
+ }
154
+ if (action === 'update') {
155
+ actions.push({
156
+ entry_id: entryId, action,
157
+ element: (parts[2] ?? '').trim(),
158
+ summary: parts.slice(3).join('|').trim(),
159
+ })
160
+ } else if (action === 'merge') {
161
+ const targetId = resolveId(parts[2])
162
+ const sourceIds = [...new Set((parts[3] ?? '').split(',').map(s => resolveId(s)).filter(Number.isFinite))]
163
+ if (!Number.isFinite(targetId) || sourceIds.length === 0) {
164
+ __mixdogMemoryLog(`[cycle2] merge rejected: id=${entryId} invalid target/sources\n`)
165
+ rejected.add(entryId)
166
+ continue
167
+ }
168
+ if (targetId !== entryId && !sourceIds.includes(entryId)) {
169
+ __mixdogMemoryLog(
170
+ `[cycle2] merge rejected: id=${entryId} must be target or listed source (target=${targetId} sources=${sourceIds.join(',')})\n`,
171
+ )
172
+ rejected.add(entryId)
173
+ continue
174
+ }
175
+ actions.push({
176
+ entry_id: entryId, action,
177
+ target_id: targetId,
178
+ source_ids: sourceIds,
179
+ element: (parts[4] ?? '').trim(),
180
+ summary: parts.slice(5).join('|').trim(),
181
+ })
182
+ } else {
183
+ actions.push({ entry_id: entryId, action })
184
+ }
185
+ }
186
+ if (!sawValid && rejected.size === 0) return null
187
+ return { actions, rejected, support }
188
+ }
189
+
190
+ // ─── Current rules digest cache ──────────────────────────────────────────────
191
+
192
+ let _currentRulesDigest = null
193
+ let _currentRulesDigestTs = 0
194
+ export function loadCurrentRulesDigest() {
195
+ const now = Date.now()
196
+ if (_currentRulesDigest && now - _currentRulesDigestTs < 60_000) return _currentRulesDigest
197
+ const sources = [
198
+ join(resourceDir(), 'rules', 'shared', '01-general.md'),
199
+ join(resourceDir(), 'rules', 'shared', '01-tool.md'),
200
+ join(resourceDir(), 'rules', 'shared', '04-memory.md'),
201
+ join(resourceDir(), 'rules', 'shared', '06-team.md'),
202
+ join(resourceDir(), 'rules', 'shared', '07-workflow.md'),
203
+ ]
204
+ const parts = []
205
+ for (const p of sources) {
206
+ try {
207
+ if (!existsSync(p)) continue
208
+ const txt = readFileSync(p, 'utf8').trim()
209
+ if (txt) parts.push(`# Source: ${p}\n${txt}`)
210
+ } catch {}
211
+ }
212
+ const joined = parts.join('\n\n---\n\n')
213
+ const CAP = 40_000
214
+ _currentRulesDigest = joined.length > CAP ? joined.slice(0, CAP) + '\n…[truncated]' : joined
215
+ _currentRulesDigestTs = now
216
+ return _currentRulesDigest
217
+ }
218
+
219
+ function uniqueIds(values) {
220
+ return [...new Set(values
221
+ .map(id => Number(id))
222
+ .filter(id => Number.isFinite(id)))]
223
+ }
224
+
225
+ function validateUnifiedGate(parsed, statusById) {
226
+ const actions = Array.isArray(parsed?.actions) ? parsed.actions : []
227
+ const primary = actions.filter(a => a?.action !== 'core')
228
+ const verdictCounts = new Map()
229
+ for (const action of primary) {
230
+ const id = Number(action?.entry_id)
231
+ if (!Number.isFinite(id)) continue
232
+ verdictCounts.set(id, (verdictCounts.get(id) || 0) + 1)
233
+ }
234
+ const expectedIds = [...statusById.keys()]
235
+ const missingVerdictIds = expectedIds.filter(id => !verdictCounts.has(id))
236
+ const duplicateVerdictIds = [...verdictCounts.entries()]
237
+ .filter(([, count]) => count > 1)
238
+ .map(([id]) => id)
239
+ const support = parsed?.support instanceof Map ? parsed.support : new Map()
240
+ const coreIds = new Set(actions
241
+ .filter(a => a?.action === 'core')
242
+ .map(a => Number(a.entry_id))
243
+ .filter(id => Number.isFinite(id)))
244
+ const missingSupportIds = []
245
+ const missingCoreIds = []
246
+ for (const action of primary) {
247
+ if (!NON_ARCHIVE_VERBS.has(action?.action)) continue
248
+ const id = Number(action.entry_id)
249
+ if (!Number.isFinite(id)) continue
250
+ const coreId = action.action === 'merge' && Number.isFinite(Number(action.target_id))
251
+ ? Number(action.target_id)
252
+ : id
253
+ const hasSupport = support.has(id) || (action.action === 'merge' && support.has(coreId))
254
+ if (!hasSupport) missingSupportIds.push(id)
255
+ if (!coreIds.has(coreId)) missingCoreIds.push(id)
256
+ }
257
+ return {
258
+ missingVerdictIds: uniqueIds(missingVerdictIds),
259
+ duplicateVerdictIds: uniqueIds(duplicateVerdictIds),
260
+ missingSupportIds: uniqueIds(missingSupportIds),
261
+ missingCoreIds: uniqueIds(missingCoreIds),
262
+ }
263
+ }
264
+
265
+ function gateQualitySummary(quality) {
266
+ const parts = []
267
+ if (quality?.missingVerdictIds?.length) parts.push(`missing verdict ids=${quality.missingVerdictIds.join(',')}`)
268
+ if (quality?.duplicateVerdictIds?.length) parts.push(`duplicate verdict ids=${quality.duplicateVerdictIds.join(',')}`)
269
+ if (quality?.missingSupportIds?.length) parts.push(`missing why ids=${quality.missingSupportIds.join(',')}`)
270
+ if (quality?.missingCoreIds?.length) parts.push(`missing core ids=${quality.missingCoreIds.join(',')}`)
271
+ return parts.join('; ')
272
+ }
273
+
274
+ function stripUnsupportedPromotions(parsed, unsupportedIds) {
275
+ const ids = new Set(uniqueIds(unsupportedIds))
276
+ if (ids.size === 0) return parsed
277
+ const rejected = new Set(parsed?.rejected || [])
278
+ for (const id of ids) rejected.add(id)
279
+ const actions = (parsed?.actions || []).filter(a => {
280
+ if (a?.action === 'core') return true
281
+ return !ids.has(Number(a?.entry_id))
282
+ })
283
+ return { ...parsed, actions, rejected }
284
+ }
285
+
286
+ function requiredCoreIdForAction(action) {
287
+ if (action?.action === 'merge' && Number.isFinite(Number(action.target_id))) {
288
+ return Number(action.target_id)
289
+ }
290
+ return Number(action?.entry_id)
291
+ }
292
+
293
+ // ─── Unified gate ────────────────────────────────────────────────────────────
294
+
295
+ // Single LLM pass over rows whose status is in {pending, active}.
296
+ // Returns { actions, rejected, parseOk } following parseUnifiedFormat shape.
297
+ export async function runUnifiedGate(db, rows, activeContext, config = {}, options = {}) {
298
+ const signal = options?.signal
299
+ throwIfAborted(signal)
300
+ if (!rows || rows.length === 0) return { actions: [], rejected: new Set(), parseOk: true }
301
+ const promptPath = join(resourceDir(), 'defaults', 'memory-promote-prompt.md')
302
+ if (!existsSync(promptPath)) {
303
+ throw new Error(`runCycle2: prompt file missing at ${promptPath}`)
304
+ }
305
+ const template = readFileSync(promptPath, 'utf8')
306
+ const userCoreRows = options.dataDir ? await listCore(options.dataDir, '*').catch(() => []) : []
307
+ throwIfAborted(signal)
308
+ const sharedPidMap = buildPidMap([activeContext ?? [], rows ?? [], userCoreRows ?? []])
309
+ const rulesDigest = loadCurrentRulesDigest() || '(no current rules digest available)'
310
+ const activeCount = activeContext?.length ?? 0
311
+ const activeCap = options.activeCap ?? CYCLE2_ACTIVE_TARGET_CAP
312
+
313
+ const prompt = template
314
+ .replace('{{CURRENT_RULES}}', rulesDigest)
315
+ .replace('{{USER_CORE}}', formatUserCoreForPrompt(userCoreRows, sharedPidMap))
316
+ .replace('{{CORE_MEMORY}}', formatEntriesForPromotePrompt(activeContext, sharedPidMap))
317
+ .replace('{{ITEMS}}', formatEntriesForPromotePrompt(rows, sharedPidMap, { numbered: true }))
318
+ .replace('{{ACTIVE_COUNT}}', String(activeCount))
319
+ .replace('{{ACTIVE_CAP}}', String(activeCap))
320
+
321
+ const preset = options.preset || resolveMaintenancePreset('memory')
322
+ const timeout = Number(config?.cycle2?.timeout ?? 600000)
323
+ const mode = 'cycle2-unified'
324
+
325
+ const previewRaw = (raw) => String(raw ?? '').replace(/\s+/g, ' ').slice(0, 200)
326
+ const callOnce = async (extraTag) => {
327
+ throwIfAborted(signal)
328
+ const p = extraTag ? `${prompt}\n\n[retry:${extraTag}]` : prompt
329
+ const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm)
330
+ throwIfAborted(signal)
331
+ return raw
332
+ }
333
+
334
+ const statusById = new Map(rows.map(r => [Number(r.id), String(r.status)]))
335
+ // Ordinal → batch-id map, keyed by 1-based prompt order (the same order the
336
+ // numbered Entries block uses). The gate may echo either the real 5-digit
337
+ // batch id or the row ordinal 1..N; the parser resolves both. The two domains
338
+ // MUST be disjoint or an ordinal could shadow a real id (ids are 5-digit,
339
+ // ordinals are <= rows.length, so disjointness always holds in practice).
340
+ // On a violation a row-number line is indistinguishable from an exact-id
341
+ // line, so no safe interpretation exists — skip this batch (gate failure)
342
+ // rather than risk applying a verdict to the wrong entry. The cycle itself
343
+ // proceeds; the batch re-queues for a later run.
344
+ const ordinalToId = new Map(rows.map((r, i) => [i + 1, Number(r.id)]))
345
+ const minBatchId = Math.min(...[...statusById.keys()])
346
+ if (Number.isFinite(minBatchId) && minBatchId <= rows.length) {
347
+ __mixdogMemoryLog(`[cycle2] batch id ${minBatchId} collides with ordinal range 1..${rows.length} — skipping batch (no safe id resolution)\n`)
348
+ return { actions: null, rejected: new Set(), parseOk: false }
349
+ }
350
+
351
+ __mixdogMemoryLog(`[cycle2-diag] unified prompt=${prompt.length} bytes; rows=${rows.length}\n`)
352
+
353
+ let raw
354
+ try {
355
+ raw = await callOnce(null)
356
+ } catch (err) {
357
+ if (signal?.aborted) throw signal.reason ?? err
358
+ __mixdogMemoryLog(`[cycle2] unified LLM error: ${err.message}\n`)
359
+ return { actions: null, rejected: new Set(), parseOk: false }
360
+ }
361
+ throwIfAborted(signal)
362
+ __mixdogMemoryLog(`[cycle2-diag] unified raw (first 1500): ${String(raw ?? '').replace(/\n/g, '⏎').slice(0, 1500)}\n`)
363
+
364
+ let parsed = parseUnifiedFormat(raw, statusById, ordinalToId)
365
+ let quality = parsed ? validateUnifiedGate(parsed, statusById) : null
366
+ const qualityIssue = () => gateQualitySummary(quality)
367
+ if (!parsed || qualityIssue()) {
368
+ throwIfAborted(signal)
369
+ const issue = parsed ? qualityIssue() : `unparseable (${previewRaw(raw)})`
370
+ __mixdogMemoryLog(`[cycle2] unified quality retry: ${issue}\n`)
371
+ // Preserve the first pass before retrying. A retry fired for a mere quality
372
+ // issue (e.g. a few missing verdicts) must not throw away an otherwise-valid
373
+ // first-pass parse if the retry comes back unparseable.
374
+ const firstParsed = parsed
375
+ const firstQuality = quality
376
+ try {
377
+ const retryTag = parsed
378
+ ? 'complete-verdicts-with-why-and-core-lines'
379
+ : 'first-field-must-be-the-listed-row-number'
380
+ const raw2 = await callOnce(retryTag)
381
+ const retryParsed = parseUnifiedFormat(raw2, statusById, ordinalToId)
382
+ if (retryParsed) {
383
+ parsed = retryParsed
384
+ quality = validateUnifiedGate(retryParsed, statusById)
385
+ } else if (firstParsed) {
386
+ __mixdogMemoryLog(`[cycle2] unparseable after retry — falling back to first-pass parse (${previewRaw(raw2)})\n`)
387
+ parsed = firstParsed
388
+ quality = firstQuality
389
+ } else {
390
+ __mixdogMemoryLog(`[cycle2] unparseable after retry — skipping batch (${previewRaw(raw2)})\n`)
391
+ return { actions: null, rejected: new Set(), parseOk: false }
392
+ }
393
+ } catch (err) {
394
+ if (signal?.aborted) throw signal.reason ?? err
395
+ if (firstParsed) {
396
+ __mixdogMemoryLog(`[cycle2] retry LLM error: ${err.message} — falling back to first-pass parse\n`)
397
+ parsed = firstParsed
398
+ quality = firstQuality
399
+ } else {
400
+ __mixdogMemoryLog(`[cycle2] retry LLM error: ${err.message}\n`)
401
+ return { actions: null, rejected: new Set(), parseOk: false }
402
+ }
403
+ }
404
+ }
405
+ const finalIssue = gateQualitySummary(quality)
406
+ // duplicateVerdictIds are genuinely ambiguous (the same row got two conflicting
407
+ // verbs) — keep the full-skip. missingVerdictIds, by contrast, used to skip the
408
+ // WHOLE batch, so a handful of persistently-missing poison rows could livelock
409
+ // the gate. Partial-apply instead: keep the valid verdicts we did receive, just
410
+ // log the missing ids and leave those rows for a later run.
411
+ if (quality?.duplicateVerdictIds?.length) {
412
+ __mixdogMemoryLog(`[cycle2] duplicate verdict coverage after retry — skipping batch (${finalIssue})\n`)
413
+ return { actions: null, rejected: new Set(), parseOk: false }
414
+ }
415
+ if (quality?.missingVerdictIds?.length) {
416
+ __mixdogMemoryLog(`[cycle2] missing verdicts after retry — partial apply, leaving ids=${quality.missingVerdictIds.join(',')} for a later run (${finalIssue})\n`)
417
+ }
418
+ // A response made up solely of why/core lines parses "ok" yet carries zero
419
+ // primary (status-verb) verdicts. Without this guard parseOk stays true and
420
+ // the caller treats the batch as a clean no-op, masking the coverage failure
421
+ // and marking the rows reviewed. Fail the parse so the rows are re-queued.
422
+ const primaryCount = (parsed.actions || []).filter(a => a?.action !== 'core').length
423
+ if (rows.length > 0 && primaryCount === 0) {
424
+ __mixdogMemoryLog(`[cycle2] gate produced zero primary verdicts for ${rows.length} rows — failing parse\n`)
425
+ return { actions: null, rejected: new Set(), parseOk: false, missingIds: [...statusById.keys()] }
426
+ }
427
+ const incompletePromotionIds = uniqueIds([
428
+ ...(quality?.missingSupportIds || []),
429
+ ...(quality?.missingCoreIds || []),
430
+ ])
431
+ if (incompletePromotionIds.length > 0) {
432
+ __mixdogMemoryLog(`[cycle2] incomplete non-archive verdicts rejected after retry ids=${incompletePromotionIds.join(',')} (${finalIssue})\n`)
433
+ parsed = stripUnsupportedPromotions(parsed, incompletePromotionIds)
434
+ }
435
+ return {
436
+ actions: parsed.actions,
437
+ rejected: parsed.rejected,
438
+ parseOk: true,
439
+ missingIds: quality?.missingVerdictIds || [],
440
+ }
441
+ }
442
+
443
+ // ─── Sonnet cascade ──────────────────────────────────────────────────────────
444
+
445
+ // Sonnet re-judge over first-pass keep verdicts. Sonnet sees rules + summary
446
+ // and returns binary keep/drop. Failures fail-open (preserve first-pass).
447
+ export async function sonnetCascade(candidates, rulesDigest, options = {}) {
448
+ const signal = options?.signal
449
+ throwIfAborted(signal)
450
+ if (!candidates || candidates.length === 0) return new Map()
451
+ const lines = candidates.map(c =>
452
+ `id:${c.id} status:${c.status} verb:${c.verb} cat:${c.category} el:${c.element} sm:${String(c.summary || '').slice(0, 200)}${c.core ? ` core:${String(c.core).slice(0, 200)}` : ''}`,
453
+ ).join('\n')
454
+ const prompt = [
455
+ `Final gate over first-pass keep verdicts.`,
456
+ `Keep a candidate ONLY if it lands in one of three layers: L1 relationship/communication`,
457
+ `(user identity, address form, reply-style preferences, disliked patterns); L2 behavior rules`,
458
+ `(principles the user corrected/insisted on, hard safety boundaries, quality bars); or L3 current`,
459
+ `map (one-line project-landscape summaries, live long-running goals, environment anchors documented`,
460
+ `nowhere else). For a past decision/failure, keep only the one-line lesson that still constrains`,
461
+ `behavior, else drop. DROP anything whose source of truth is code, rules files, or skill docs, plus`,
462
+ `implementation specs, code-internal constants, measurements, resolved-bug stories, status snapshots,`,
463
+ `and duplicates of source-of-truth rules.`,
464
+ `When a candidate has a core: field, judge THAT extracted one-line lesson (the entry will live as`,
465
+ `that line), not the raw narrative in el:/sm:.`,
466
+ ``,
467
+ `Source-of-truth rules (excerpt — DO NOT duplicate in memory):`,
468
+ String(rulesDigest || '').slice(0, 4000),
469
+ ``,
470
+ `Candidates:`,
471
+ lines,
472
+ ``,
473
+ `Reply one line per id: "<id>|keep" to retain, "<id>|drop" to reject.`,
474
+ `NO prose, NO preamble, NO meta-commentary. First character must be a digit.`,
475
+ ].join('\n')
476
+
477
+ // Hardcoded — resolveMaintenancePreset falls back to first preset (HAIKU)
478
+ // when no binding exists, which would defeat the cascade. SONNET HIGH
479
+ // matches the worker pool's default preset id from agent-config.
480
+ const preset = options.cascadePreset || 'SONNET HIGH'
481
+ const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
482
+ let raw
483
+ try {
484
+ raw = await llmCall({
485
+ agent: 'cycle2-agent',
486
+ taskType: 'maintenance',
487
+ mode: 'cycle2-cascade',
488
+ preset,
489
+ timeout: 600000,
490
+ cwd: null,
491
+ }, prompt)
492
+ } catch (err) {
493
+ if (signal?.aborted) throw signal.reason ?? err
494
+ __mixdogMemoryLog(`[cycle2] cascade error: ${err.message} — fail-open\n`)
495
+ return new Map()
496
+ }
497
+ throwIfAborted(signal)
498
+
499
+ const verdicts = new Map()
500
+ for (const line of String(raw ?? '').split('\n')) {
501
+ throwIfAborted(signal)
502
+ const trimmed = line.trim()
503
+ if (!trimmed) continue
504
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('```')) continue
505
+ const parts = trimmed.split('|')
506
+ if (parts.length < 2) continue
507
+ const id = Number(parts[0].trim())
508
+ const v = parts[1].trim().toLowerCase()
509
+ if (Number.isFinite(id) && (v === 'keep' || v === 'drop')) verdicts.set(id, v)
510
+ }
511
+ __mixdogMemoryLog(`[cycle2] cascade evaluated=${candidates.length} drops=${[...verdicts.values()].filter(v => v === 'drop').length}\n`)
512
+ return verdicts
513
+ }
514
+
515
+ export { NON_ARCHIVE_VERBS, requiredCoreIdForAction }