muonroi-cli 1.8.4 → 1.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 (562) hide show
  1. package/LICENSE +17 -5
  2. package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
  3. package/dist/packages/agent-harness-core/src/driver.js +46 -0
  4. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  5. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  6. package/dist/packages/agent-harness-core/src/event-tee.d.ts +64 -0
  7. package/dist/packages/agent-harness-core/src/event-tee.js +104 -0
  8. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +25 -0
  9. package/dist/packages/agent-harness-core/src/mcp-server.js +169 -21
  10. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  11. package/dist/packages/agent-harness-core/src/protocol.d.ts +90 -4
  12. package/dist/packages/agent-harness-core/src/protocol.js +15 -0
  13. package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
  14. package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
  15. package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
  16. package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
  17. package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
  18. package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
  19. package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
  20. package/dist/packages/agent-harness-opentui/src/install.js +10 -0
  21. package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
  22. package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
  23. package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
  24. package/dist/src/agent-harness/mock-model.d.ts +38 -0
  25. package/dist/src/agent-harness/mock-model.js +69 -3
  26. package/dist/src/agent-harness/test-spawn.js +31 -0
  27. package/dist/src/chat/chat-keychain.d.ts +7 -12
  28. package/dist/src/chat/chat-keychain.js +19 -86
  29. package/dist/src/cli/config/screen-providers.js +1 -1
  30. package/dist/src/cli/cost-forensics.d.ts +10 -0
  31. package/dist/src/cli/cost-forensics.js +18 -3
  32. package/dist/src/cli/keys-bundle.d.ts +1 -1
  33. package/dist/src/cli/keys-bundle.js +1 -1
  34. package/dist/src/cli/keys.d.ts +10 -47
  35. package/dist/src/cli/keys.js +31 -399
  36. package/dist/src/council/clarifier.d.ts +31 -3
  37. package/dist/src/council/clarifier.js +220 -32
  38. package/dist/src/council/context.js +49 -15
  39. package/dist/src/council/debate-checkpoint.d.ts +129 -0
  40. package/dist/src/council/debate-checkpoint.js +176 -0
  41. package/dist/src/council/debate-planner.js +54 -5
  42. package/dist/src/council/debate-summary.d.ts +25 -0
  43. package/dist/src/council/debate-summary.js +85 -0
  44. package/dist/src/council/debate.d.ts +169 -2
  45. package/dist/src/council/debate.js +1265 -135
  46. package/dist/src/council/index.d.ts +108 -1
  47. package/dist/src/council/index.js +670 -197
  48. package/dist/src/council/leader.d.ts +26 -0
  49. package/dist/src/council/leader.js +150 -9
  50. package/dist/src/council/llm.d.ts +94 -0
  51. package/dist/src/council/llm.js +348 -55
  52. package/dist/src/council/panel-select.d.ts +30 -0
  53. package/dist/src/council/panel-select.js +82 -0
  54. package/dist/src/council/planner.js +40 -0
  55. package/dist/src/council/preflight.d.ts +17 -0
  56. package/dist/src/council/preflight.js +50 -2
  57. package/dist/src/council/prompts.d.ts +39 -4
  58. package/dist/src/council/prompts.js +256 -69
  59. package/dist/src/council/stance-recall.d.ts +42 -0
  60. package/dist/src/council/stance-recall.js +57 -0
  61. package/dist/src/council/strip-think.d.ts +17 -0
  62. package/dist/src/council/strip-think.js +33 -0
  63. package/dist/src/council/types.d.ts +138 -0
  64. package/dist/src/ee/artifact-cache.d.ts +16 -0
  65. package/dist/src/ee/artifact-cache.js +32 -0
  66. package/dist/src/ee/auth.d.ts +20 -0
  67. package/dist/src/ee/auth.js +54 -2
  68. package/dist/src/ee/bridge.d.ts +10 -0
  69. package/dist/src/ee/bridge.js +58 -0
  70. package/dist/src/ee/client.js +109 -21
  71. package/dist/src/ee/ee-onboarding.js +6 -26
  72. package/dist/src/ee/export-transcripts.d.ts +1 -0
  73. package/dist/src/ee/export-transcripts.js +8 -10
  74. package/dist/src/ee/extract-session.js +29 -0
  75. package/dist/src/ee/extract-style.d.ts +58 -0
  76. package/dist/src/ee/extract-style.js +270 -0
  77. package/dist/src/ee/recall-ledger.d.ts +9 -0
  78. package/dist/src/ee/recall-ledger.js +3 -0
  79. package/dist/src/ee/scope.d.ts +1 -0
  80. package/dist/src/ee/scope.js +26 -1
  81. package/dist/src/ee/search.d.ts +7 -0
  82. package/dist/src/ee/search.js +24 -0
  83. package/dist/src/ee/transcript-emit.js +2 -0
  84. package/dist/src/ee/types.d.ts +22 -0
  85. package/dist/src/ee/who-am-i-brain.d.ts +35 -0
  86. package/dist/src/ee/who-am-i-brain.js +220 -0
  87. package/dist/src/ee/who-am-i.d.ts +10 -3
  88. package/dist/src/ee/who-am-i.js +12 -0
  89. package/dist/src/ee/workflow-event.d.ts +48 -0
  90. package/dist/src/ee/workflow-event.js +81 -0
  91. package/dist/src/flow/compaction/compress.d.ts +3 -3
  92. package/dist/src/flow/compaction/compress.js +58 -8
  93. package/dist/src/flow/compaction/extract.d.ts +4 -7
  94. package/dist/src/flow/compaction/extract.js +50 -10
  95. package/dist/src/flow/compaction/index.d.ts +14 -1
  96. package/dist/src/flow/compaction/index.js +96 -3
  97. package/dist/src/flow/compaction/input-guard.d.ts +24 -0
  98. package/dist/src/flow/compaction/input-guard.js +43 -0
  99. package/dist/src/flow/compaction/progress.d.ts +35 -0
  100. package/dist/src/flow/compaction/progress.js +35 -0
  101. package/dist/src/flow/fold-planning.d.ts +36 -0
  102. package/dist/src/flow/fold-planning.js +83 -0
  103. package/dist/src/flow/hierarchy.d.ts +146 -0
  104. package/dist/src/flow/hierarchy.js +427 -0
  105. package/dist/src/flow/index.d.ts +1 -0
  106. package/dist/src/flow/index.js +2 -0
  107. package/dist/src/flow/run-artifacts.d.ts +102 -0
  108. package/dist/src/flow/run-artifacts.js +208 -0
  109. package/dist/src/generated/version.d.ts +1 -1
  110. package/dist/src/generated/version.js +1 -1
  111. package/dist/src/gsd/assessment-schema.d.ts +44 -0
  112. package/dist/src/gsd/assessment-schema.js +134 -0
  113. package/dist/src/gsd/capability-registry.d.ts +45 -0
  114. package/dist/src/gsd/capability-registry.js +337 -0
  115. package/dist/src/gsd/complexity-assessor.d.ts +39 -0
  116. package/dist/src/gsd/complexity-assessor.js +152 -0
  117. package/dist/src/gsd/config-bridge.d.ts +7 -0
  118. package/dist/src/gsd/config-bridge.js +114 -0
  119. package/dist/src/gsd/config-loader.d.ts +27 -0
  120. package/dist/src/gsd/config-loader.js +50 -0
  121. package/dist/src/gsd/council-context.d.ts +44 -0
  122. package/dist/src/gsd/council-context.js +114 -0
  123. package/dist/src/gsd/ee-closure.d.ts +28 -0
  124. package/dist/src/gsd/ee-closure.js +49 -0
  125. package/dist/src/gsd/flags.d.ts +66 -0
  126. package/dist/src/gsd/flags.js +102 -0
  127. package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
  128. package/dist/src/gsd/gsd-dispatch.js +131 -0
  129. package/dist/src/gsd/gsd-runtime.d.ts +22 -0
  130. package/dist/src/gsd/gsd-runtime.js +37 -0
  131. package/dist/src/gsd/host-adapter.d.ts +11 -0
  132. package/dist/src/gsd/host-adapter.js +29 -0
  133. package/dist/src/gsd/index.d.ts +24 -1
  134. package/dist/src/gsd/index.js +27 -0
  135. package/dist/src/gsd/loop-host-contract.d.ts +21 -0
  136. package/dist/src/gsd/loop-host-contract.js +39 -0
  137. package/dist/src/gsd/loop-host.d.ts +69 -0
  138. package/dist/src/gsd/loop-host.js +245 -0
  139. package/dist/src/gsd/loop-resolver.d.ts +36 -0
  140. package/dist/src/gsd/loop-resolver.js +79 -0
  141. package/dist/src/gsd/model-tier.d.ts +13 -0
  142. package/dist/src/gsd/model-tier.js +45 -0
  143. package/dist/src/gsd/mutation-gate.d.ts +16 -0
  144. package/dist/src/gsd/mutation-gate.js +41 -0
  145. package/dist/src/gsd/native-roadmap.d.ts +89 -0
  146. package/dist/src/gsd/native-roadmap.js +343 -0
  147. package/dist/src/gsd/native-state.d.ts +47 -0
  148. package/dist/src/gsd/native-state.js +220 -0
  149. package/dist/src/gsd/paths.d.ts +23 -0
  150. package/dist/src/gsd/paths.js +66 -0
  151. package/dist/src/gsd/phase-dag.d.ts +12 -0
  152. package/dist/src/gsd/phase-dag.js +94 -0
  153. package/dist/src/gsd/phase-sync.d.ts +42 -0
  154. package/dist/src/gsd/phase-sync.js +321 -0
  155. package/dist/src/gsd/pil-gate-context.d.ts +13 -0
  156. package/dist/src/gsd/pil-gate-context.js +64 -0
  157. package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
  158. package/dist/src/gsd/pil-gate-critic.js +74 -0
  159. package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
  160. package/dist/src/gsd/plan-council-prompts.js +79 -0
  161. package/dist/src/gsd/plan-council.d.ts +44 -0
  162. package/dist/src/gsd/plan-council.js +283 -0
  163. package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
  164. package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
  165. package/dist/src/gsd/product-workspace.d.ts +13 -0
  166. package/dist/src/gsd/product-workspace.js +124 -0
  167. package/dist/src/gsd/ship-bridge.d.ts +25 -0
  168. package/dist/src/gsd/ship-bridge.js +65 -0
  169. package/dist/src/gsd/state-document.d.ts +40 -0
  170. package/dist/src/gsd/state-document.js +163 -0
  171. package/dist/src/gsd/verdict-schema.d.ts +39 -0
  172. package/dist/src/gsd/verdict-schema.js +144 -0
  173. package/dist/src/gsd/verify-context.d.ts +22 -0
  174. package/dist/src/gsd/verify-context.js +27 -0
  175. package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
  176. package/dist/src/gsd/verify-council-prompts.js +85 -0
  177. package/dist/src/gsd/verify-council.d.ts +25 -0
  178. package/dist/src/gsd/verify-council.js +119 -0
  179. package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
  180. package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
  181. package/dist/src/gsd/workflow-engine.d.ts +60 -0
  182. package/dist/src/gsd/workflow-engine.js +207 -0
  183. package/dist/src/gsd/workflow-tools.d.ts +13 -0
  184. package/dist/src/gsd/workflow-tools.js +277 -0
  185. package/dist/src/headless/council-answers.js +4 -0
  186. package/dist/src/hooks/index.js +1 -1
  187. package/dist/src/index.js +172 -270
  188. package/dist/src/lsp/builtins.js +3 -1
  189. package/dist/src/lsp/manager.d.ts +5 -1
  190. package/dist/src/lsp/manager.js +249 -3
  191. package/dist/src/lsp/npm-cache.d.ts +11 -1
  192. package/dist/src/lsp/npm-cache.js +17 -1
  193. package/dist/src/lsp/runtime.d.ts +6 -1
  194. package/dist/src/lsp/runtime.js +17 -1
  195. package/dist/src/lsp/types.d.ts +83 -1
  196. package/dist/src/lsp/types.js +10 -0
  197. package/dist/src/maintain/pr-builder.js +23 -13
  198. package/dist/src/mcp/auto-setup.js +57 -32
  199. package/dist/src/mcp/client-pool.js +44 -16
  200. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  201. package/dist/src/mcp/lsp-tools.js +93 -2
  202. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  203. package/dist/src/mcp/mcp-keychain.js +9 -49
  204. package/dist/src/mcp/research-onboarding.js +8 -7
  205. package/dist/src/mcp/runtime.js +34 -2
  206. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  207. package/dist/src/mcp/setup-guide-text.js +22 -2
  208. package/dist/src/mcp/tools-server.d.ts +10 -0
  209. package/dist/src/mcp/tools-server.js +10 -2
  210. package/dist/src/models/catalog-client.d.ts +87 -0
  211. package/dist/src/models/catalog-client.js +105 -38
  212. package/dist/src/models/catalog.json +528 -265
  213. package/dist/src/models/registry.d.ts +22 -7
  214. package/dist/src/models/registry.js +73 -10
  215. package/dist/src/ops/doctor.js +1 -1
  216. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  217. package/dist/src/orchestrator/ask-user.js +65 -0
  218. package/dist/src/orchestrator/auto-commit.js +1 -1
  219. package/dist/src/orchestrator/batch-turn-runner.js +2 -2
  220. package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
  221. package/dist/src/orchestrator/cache-prefix.js +83 -0
  222. package/dist/src/orchestrator/compact-request.d.ts +32 -0
  223. package/dist/src/orchestrator/compact-request.js +41 -0
  224. package/dist/src/orchestrator/compaction.d.ts +12 -3
  225. package/dist/src/orchestrator/compaction.js +35 -15
  226. package/dist/src/orchestrator/council-manager.d.ts +12 -3
  227. package/dist/src/orchestrator/council-manager.js +74 -32
  228. package/dist/src/orchestrator/council-request.d.ts +49 -0
  229. package/dist/src/orchestrator/council-request.js +62 -0
  230. package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
  231. package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
  232. package/dist/src/orchestrator/error-utils.d.ts +29 -0
  233. package/dist/src/orchestrator/error-utils.js +132 -24
  234. package/dist/src/orchestrator/grounding-check.js +39 -1
  235. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  236. package/dist/src/orchestrator/interactive-pause.js +36 -0
  237. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  238. package/dist/src/orchestrator/message-processor.js +268 -41
  239. package/dist/src/orchestrator/orchestrator.d.ts +64 -3
  240. package/dist/src/orchestrator/orchestrator.js +823 -120
  241. package/dist/src/orchestrator/preprocessor.js +3 -3
  242. package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
  243. package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
  244. package/dist/src/orchestrator/prompts.js +17 -17
  245. package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
  246. package/dist/src/orchestrator/reactive-delegation.js +59 -0
  247. package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
  248. package/dist/src/orchestrator/retry-classifier.js +46 -2
  249. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  250. package/dist/src/orchestrator/safety-askcard.js +5 -2
  251. package/dist/src/orchestrator/safety-intercept.d.ts +50 -0
  252. package/dist/src/orchestrator/safety-intercept.js +62 -0
  253. package/dist/src/orchestrator/scope-reminder.js +1 -1
  254. package/dist/src/orchestrator/session-experience.d.ts +2 -1
  255. package/dist/src/orchestrator/session-experience.js +2 -1
  256. package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
  257. package/dist/src/orchestrator/should-run-gate.js +18 -0
  258. package/dist/src/orchestrator/stall-watchdog.d.ts +31 -3
  259. package/dist/src/orchestrator/stall-watchdog.js +65 -10
  260. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  261. package/dist/src/orchestrator/stream-runner.js +115 -49
  262. package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
  263. package/dist/src/orchestrator/sub-agent-cap.js +16 -1
  264. package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
  265. package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
  266. package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
  267. package/dist/src/orchestrator/subagent-compactor.js +126 -15
  268. package/dist/src/orchestrator/tool-engine.d.ts +41 -0
  269. package/dist/src/orchestrator/tool-engine.js +846 -66
  270. package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
  271. package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
  272. package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
  273. package/dist/src/orchestrator/turn-watchdog.d.ts +44 -0
  274. package/dist/src/orchestrator/turn-watchdog.js +84 -0
  275. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  276. package/dist/src/pil/agent-operating-contract.js +6 -4
  277. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  278. package/dist/src/pil/cheap-model-playbook.js +5 -1
  279. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  280. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  281. package/dist/src/pil/discovery-types.d.ts +1 -0
  282. package/dist/src/pil/discovery.d.ts +1 -1
  283. package/dist/src/pil/discovery.js +18 -13
  284. package/dist/src/pil/layer1-intent.d.ts +18 -6
  285. package/dist/src/pil/layer1-intent.js +66 -757
  286. package/dist/src/pil/layer15-context-scan.js +15 -1
  287. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  288. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  289. package/dist/src/pil/layer3-ee-injection.js +23 -8
  290. package/dist/src/pil/layer4-gsd.js +69 -16
  291. package/dist/src/pil/layer5-context.js +7 -3
  292. package/dist/src/pil/layer6-output.d.ts +23 -0
  293. package/dist/src/pil/layer6-output.js +5 -1
  294. package/dist/src/pil/llm-classify.d.ts +111 -5
  295. package/dist/src/pil/llm-classify.js +421 -189
  296. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  297. package/dist/src/pil/native-capabilities-workbook.js +8 -0
  298. package/dist/src/pil/pipeline.js +36 -2
  299. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  300. package/dist/src/pil/repo-grounding-probe.js +136 -0
  301. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  302. package/dist/src/pil/repo-structure-hints.js +45 -0
  303. package/dist/src/pil/response-tools.js +5 -3
  304. package/dist/src/pil/schema.d.ts +1 -0
  305. package/dist/src/pil/schema.js +2 -0
  306. package/dist/src/pil/types.d.ts +18 -0
  307. package/dist/src/playbook/directives.d.ts +4 -0
  308. package/dist/src/playbook/directives.js +17 -5
  309. package/dist/src/product-loop/artifact-io.js +4 -0
  310. package/dist/src/product-loop/backlog-builder.d.ts +14 -1
  311. package/dist/src/product-loop/backlog-builder.js +30 -6
  312. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  313. package/dist/src/product-loop/criteria-seed.js +200 -0
  314. package/dist/src/product-loop/discovery-context-format.js +3 -1
  315. package/dist/src/product-loop/discovery-ecosystem.js +4 -1
  316. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  317. package/dist/src/product-loop/discovery-interview.js +60 -12
  318. package/dist/src/product-loop/discovery-recommender.js +2 -1
  319. package/dist/src/product-loop/discovery-schema.js +19 -2
  320. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  321. package/dist/src/product-loop/discovery-triage.js +109 -0
  322. package/dist/src/product-loop/gather.js +150 -2
  323. package/dist/src/product-loop/ideal-trace.d.ts +7 -0
  324. package/dist/src/product-loop/ideal-trace.js +64 -0
  325. package/dist/src/product-loop/index.d.ts +13 -1
  326. package/dist/src/product-loop/index.js +340 -52
  327. package/dist/src/product-loop/loop-driver.d.ts +7 -0
  328. package/dist/src/product-loop/loop-driver.js +330 -106
  329. package/dist/src/product-loop/phase-plan.d.ts +21 -0
  330. package/dist/src/product-loop/phase-plan.js +81 -6
  331. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  332. package/dist/src/product-loop/phase-rituals.js +8 -3
  333. package/dist/src/product-loop/phase-runner.js +39 -12
  334. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  335. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  336. package/dist/src/product-loop/sprint-runner.d.ts +173 -0
  337. package/dist/src/product-loop/sprint-runner.js +863 -19
  338. package/dist/src/product-loop/types.d.ts +61 -5
  339. package/dist/src/providers/adapter.d.ts +1 -1
  340. package/dist/src/providers/adapter.js +3 -4
  341. package/dist/src/providers/anthropic.d.ts +9 -8
  342. package/dist/src/providers/anthropic.js +13 -47
  343. package/dist/src/providers/auth/browser-flow.d.ts +1 -1
  344. package/dist/src/providers/auth/browser-flow.js +1 -1
  345. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  346. package/dist/src/providers/auth/grok-oauth.js +30 -5
  347. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  348. package/dist/src/providers/auth/openai-oauth.js +15 -1
  349. package/dist/src/providers/auth/registry.js +0 -34
  350. package/dist/src/providers/auth/token-store.d.ts +9 -9
  351. package/dist/src/providers/auth/token-store.js +8 -67
  352. package/dist/src/providers/auth/types.d.ts +9 -1
  353. package/dist/src/providers/auth/types.js +1 -1
  354. package/dist/src/providers/capabilities.d.ts +24 -5
  355. package/dist/src/providers/capabilities.js +42 -24
  356. package/dist/src/providers/endpoints.d.ts +2 -2
  357. package/dist/src/providers/endpoints.js +11 -10
  358. package/dist/src/providers/env-store.d.ts +17 -0
  359. package/dist/src/providers/env-store.js +228 -0
  360. package/dist/src/providers/keychain.d.ts +22 -18
  361. package/dist/src/providers/keychain.js +127 -140
  362. package/dist/src/providers/mcp-vision-bridge.js +56 -146
  363. package/dist/src/providers/openai-compatible.js +8 -1
  364. package/dist/src/providers/pricing.d.ts +2 -2
  365. package/dist/src/providers/pricing.js +3 -13
  366. package/dist/src/providers/runtime.d.ts +43 -3
  367. package/dist/src/providers/runtime.js +88 -14
  368. package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
  369. package/dist/src/providers/strategies/base.strategy.js +24 -1
  370. package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
  371. package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
  372. package/dist/src/providers/strategies/registry.js +4 -4
  373. package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
  374. package/dist/src/providers/strategies/thinking-mode.js +288 -1
  375. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  376. package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
  377. package/dist/src/providers/strategies/zai.strategy.js +44 -0
  378. package/dist/src/providers/types.d.ts +5 -6
  379. package/dist/src/providers/types.js +2 -2
  380. package/dist/src/providers/vision-backend.d.ts +47 -0
  381. package/dist/src/providers/vision-backend.js +258 -0
  382. package/dist/src/providers/vision-proxy.d.ts +22 -9
  383. package/dist/src/providers/vision-proxy.js +63 -132
  384. package/dist/src/providers/warm.d.ts +65 -0
  385. package/dist/src/providers/warm.js +145 -0
  386. package/dist/src/providers/wire-debug.js +95 -0
  387. package/dist/src/router/decide.d.ts +13 -0
  388. package/dist/src/router/decide.js +138 -36
  389. package/dist/src/router/peak-hour.d.ts +38 -0
  390. package/dist/src/router/peak-hour.js +107 -0
  391. package/dist/src/router/step-router.js +3 -2
  392. package/dist/src/router/warm.js +4 -5
  393. package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
  394. package/dist/src/scaffold/continuation-prompt.js +26 -0
  395. package/dist/src/scaffold/point-to-existing.d.ts +21 -0
  396. package/dist/src/scaffold/point-to-existing.js +25 -0
  397. package/dist/src/self-qa/agentic-loop.js +6 -5
  398. package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
  399. package/dist/src/{ui/state → state}/active-run.js +21 -0
  400. package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
  401. package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
  402. package/dist/src/state/turn-trace.d.ts +43 -0
  403. package/dist/src/state/turn-trace.js +32 -0
  404. package/dist/src/storage/db.js +2 -1
  405. package/dist/src/storage/index.d.ts +1 -1
  406. package/dist/src/storage/index.js +1 -1
  407. package/dist/src/storage/interaction-log.d.ts +1 -1
  408. package/dist/src/storage/migrations.js +71 -1
  409. package/dist/src/storage/sessions.d.ts +28 -10
  410. package/dist/src/storage/sessions.js +78 -21
  411. package/dist/src/storage/transcript-view.js +1 -1
  412. package/dist/src/storage/transcript.d.ts +51 -0
  413. package/dist/src/storage/transcript.js +340 -15
  414. package/dist/src/tools/file.d.ts +15 -0
  415. package/dist/src/tools/file.js +32 -0
  416. package/dist/src/tools/git-safety.d.ts +19 -0
  417. package/dist/src/tools/git-safety.js +168 -0
  418. package/dist/src/tools/native-tools.d.ts +1 -1
  419. package/dist/src/tools/native-tools.js +81 -1
  420. package/dist/src/tools/registry.d.ts +20 -0
  421. package/dist/src/tools/registry.js +576 -23
  422. package/dist/src/tools/research.d.ts +29 -0
  423. package/dist/src/tools/research.js +233 -0
  424. package/dist/src/types/index.d.ts +147 -4
  425. package/dist/src/ui/app.js +0 -0
  426. package/dist/src/ui/cards/product-status-card.js +1 -1
  427. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  428. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  429. package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
  430. package/dist/src/ui/components/bubble-body-guard.js +50 -0
  431. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  432. package/dist/src/ui/components/compact-progress-card.js +42 -0
  433. package/dist/src/ui/components/context-rail.d.ts +26 -0
  434. package/dist/src/ui/components/context-rail.js +33 -0
  435. package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
  436. package/dist/src/ui/components/council-conclusion-card.js +420 -0
  437. package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
  438. package/dist/src/ui/components/council-debate-pill.js +34 -0
  439. package/dist/src/ui/components/council-info-card.js +2 -2
  440. package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
  441. package/dist/src/ui/components/council-leader-bubble.js +21 -11
  442. package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
  443. package/dist/src/ui/components/council-message-bubble.js +16 -15
  444. package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
  445. package/dist/src/ui/components/council-phase-timeline.js +66 -17
  446. package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
  447. package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
  448. package/dist/src/ui/components/council-question-card.js +13 -12
  449. package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
  450. package/dist/src/ui/components/council-rail-rounds.js +57 -0
  451. package/dist/src/ui/components/council-round-group.d.ts +38 -0
  452. package/dist/src/ui/components/council-round-group.js +88 -0
  453. package/dist/src/ui/components/council-status-list.d.ts +3 -1
  454. package/dist/src/ui/components/council-status-list.js +36 -24
  455. package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
  456. package/dist/src/ui/components/council-synthesis-banner.js +20 -5
  457. package/dist/src/ui/components/halt-recovery-card.js +9 -5
  458. package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
  459. package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
  460. package/dist/src/ui/components/message-view.d.ts +15 -0
  461. package/dist/src/ui/components/message-view.js +50 -1
  462. package/dist/src/ui/components/prompt-box.js +18 -16
  463. package/dist/src/ui/components/session-tree-card.d.ts +14 -0
  464. package/dist/src/ui/components/session-tree-card.js +46 -0
  465. package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
  466. package/dist/src/ui/components/slash-inline-menu.js +26 -5
  467. package/dist/src/ui/components/task-list-panel.d.ts +14 -1
  468. package/dist/src/ui/components/task-list-panel.js +22 -2
  469. package/dist/src/ui/components/tool-group.d.ts +15 -3
  470. package/dist/src/ui/components/tool-group.js +69 -11
  471. package/dist/src/ui/containers/modals-layer.d.ts +4 -2
  472. package/dist/src/ui/containers/modals-layer.js +2 -2
  473. package/dist/src/ui/council-harness-event.d.ts +57 -0
  474. package/dist/src/ui/council-harness-event.js +46 -0
  475. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  476. package/dist/src/ui/heartbeat-debug.js +45 -0
  477. package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
  478. package/dist/src/ui/mcp-modal.js +2 -4
  479. package/dist/src/ui/modals/api-key-modal.js +1 -1
  480. package/dist/src/ui/modals/connect-modal.js +4 -3
  481. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  482. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  483. package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
  484. package/dist/src/ui/modals/session-picker-modal.js +3 -5
  485. package/dist/src/ui/picker-providers.d.ts +1 -1
  486. package/dist/src/ui/picker-providers.js +1 -1
  487. package/dist/src/ui/primitives/index.d.ts +1 -0
  488. package/dist/src/ui/primitives/index.js +2 -0
  489. package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
  490. package/dist/src/ui/primitives/semantic-primitives.js +81 -0
  491. package/dist/src/ui/slash/compact.js +5 -7
  492. package/dist/src/ui/slash/cost.js +1 -1
  493. package/dist/src/ui/slash/council.js +19 -1
  494. package/dist/src/ui/slash/debug.d.ts +3 -31
  495. package/dist/src/ui/slash/debug.js +9 -20
  496. package/dist/src/ui/slash/ee.js +81 -0
  497. package/dist/src/ui/slash/ideal.d.ts +6 -2
  498. package/dist/src/ui/slash/ideal.js +97 -7
  499. package/dist/src/ui/slash/menu-items.d.ts +7 -0
  500. package/dist/src/ui/slash/menu-items.js +23 -20
  501. package/dist/src/ui/slash/registry.d.ts +2 -0
  502. package/dist/src/ui/slash/registry.js +4 -0
  503. package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
  504. package/dist/src/ui/status-bar/cache-hit.js +9 -0
  505. package/dist/src/ui/status-bar/index.d.ts +1 -1
  506. package/dist/src/ui/status-bar/index.js +7 -3
  507. package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
  508. package/dist/src/ui/status-bar/usd-meter.js +6 -4
  509. package/dist/src/ui/theme.d.ts +1 -0
  510. package/dist/src/ui/theme.js +2 -0
  511. package/dist/src/ui/types.d.ts +7 -0
  512. package/dist/src/ui/use-app-logic.js +0 -0
  513. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  514. package/dist/src/ui/utils/agent-activities.js +96 -0
  515. package/dist/src/ui/utils/format.d.ts +14 -0
  516. package/dist/src/ui/utils/format.js +23 -3
  517. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  518. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  519. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  520. package/dist/src/ui/utils/tool-summary.js +91 -0
  521. package/dist/src/usage/downgrade.js +2 -2
  522. package/dist/src/usage/product-ledger.js +2 -2
  523. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  524. package/dist/src/utils/event-loop-monitor.js +107 -0
  525. package/dist/src/utils/install-manager.js +2 -1
  526. package/dist/src/utils/llm-deadline.d.ts +14 -0
  527. package/dist/src/utils/llm-deadline.js +19 -0
  528. package/dist/src/utils/logger.js +2 -2
  529. package/dist/src/utils/loop-profiler.d.ts +102 -0
  530. package/dist/src/utils/loop-profiler.js +202 -0
  531. package/dist/src/utils/permission-mode.js +5 -3
  532. package/dist/src/utils/redactor.js +1 -1
  533. package/dist/src/utils/settings.d.ts +180 -5
  534. package/dist/src/utils/settings.js +271 -31
  535. package/dist/src/utils/side-question.d.ts +1 -2
  536. package/dist/src/utils/side-question.js +2 -2
  537. package/dist/src/utils/visible-retry.d.ts +11 -0
  538. package/dist/src/utils/visible-retry.js +10 -1
  539. package/dist/src/verify/entrypoint.d.ts +1 -1
  540. package/dist/src/verify/entrypoint.js +52 -17
  541. package/dist/src/verify/orchestrator.d.ts +1 -1
  542. package/dist/src/verify/orchestrator.js +20 -3
  543. package/dist/src/verify/recipes.d.ts +13 -0
  544. package/dist/src/verify/recipes.js +15 -0
  545. package/package.json +134 -132
  546. package/dist/src/cli/bw-vault.d.ts +0 -55
  547. package/dist/src/cli/bw-vault.js +0 -133
  548. package/dist/src/mcp/ee-tools.d.ts +0 -46
  549. package/dist/src/mcp/ee-tools.js +0 -193
  550. package/dist/src/providers/auth/gcloud.d.ts +0 -28
  551. package/dist/src/providers/auth/gcloud.js +0 -102
  552. package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
  553. package/dist/src/providers/auth/gemini-oauth.js +0 -472
  554. package/dist/src/providers/gemini.d.ts +0 -11
  555. package/dist/src/providers/gemini.js +0 -45
  556. package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
  557. package/dist/src/providers/siliconflow-sse-repair.js +0 -177
  558. package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
  559. package/dist/src/providers/strategies/google.strategy.js +0 -174
  560. package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
  561. package/dist/src/ui/containers/chat-feed.d.ts +0 -40
  562. package/dist/src/ui/containers/chat-feed.js +0 -66
@@ -5,6 +5,7 @@ import { getTenantId } from "../ee/tenant.js";
5
5
  import { emitTranscriptToDisk } from "../ee/transcript-emit.js";
6
6
  import { createRun, getActiveRunId, setActiveRunId } from "../flow/run-manager.js";
7
7
  import { ensureFlowDir } from "../flow/scaffold.js";
8
+ import { isContextRailEnabled } from "../gsd/flags.js";
8
9
  import { executeEventHooks } from "../hooks/index.js";
9
10
  import { shutdownWorkspaceLspManager } from "../lsp/runtime.js";
10
11
  import { ensureDefaultMcpServers } from "../mcp/auto-setup.js";
@@ -12,39 +13,44 @@ import { getModelInfo, normalizeModelId } from "../models/registry.js";
12
13
  import { getProviderCapabilities } from "../providers/capabilities.js";
13
14
  import { apiBaseFor } from "../providers/endpoints.js";
14
15
  import { loadKeyForProvider } from "../providers/keychain.js";
15
- import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, } from "../providers/runtime.js";
16
+ import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
16
17
  import { ALL_PROVIDER_IDS } from "../providers/types.js";
17
- import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionTotalTokens, loadTranscript, loadTranscriptState, logInteraction, markMessageCompleted, recordUsageEvent, SessionStore, } from "../storage/index.js";
18
+ import { statusBarStore } from "../state/status-bar-store.js";
19
+ import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionChainInfos, getSessionTotalTokens, loadSessionChainTranscriptState, logInteraction, markMessageCompleted, persistApprovedPlan, recordUsageEvent, SessionStore, } from "../storage/index.js";
18
20
  import { BashTool } from "../tools/bash.js";
19
21
  import { createBuiltinTools } from "../tools/registry.js";
20
22
  import { ScheduleManager } from "../tools/schedule.js";
21
23
  import { snapshotFromTodoWriteArgs } from "../tools/todo-write-snapshot.js";
22
- import { statusBarStore } from "../ui/status-bar/store.js";
23
24
  import { appendCostLog } from "../usage/cost-log.js";
24
25
  import { appendDecisionLog } from "../usage/decision-log.js";
25
26
  import { projectCostUSD, sanitizeInputTokens } from "../usage/estimator.js";
26
27
  import { logger } from "../utils/logger.js";
27
- import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
28
+ import { getAutoCompactMinNewTokens, getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
28
29
  import { runSideQuestion } from "../utils/side-question.js";
29
- import { buildVerifyDetectPrompt, normalizeVerifyRecipe } from "../verify/entrypoint.js";
30
+ import { buildVerifyDetectPrompt, inferVerifyProjectProfile, normalizeVerifyRecipe, shouldTrustDeterministicRecipe, } from "../verify/entrypoint.js";
30
31
  import { runVerifyOrchestration } from "../verify/orchestrator.js";
31
32
  import { COUNCIL_COLOR_BG, COUNCIL_COLOR_RESET, COUNCIL_ROLE_COLORS, } from "./agent-options.js";
33
+ import { ASK_USER_DISMISSED } from "./ask-user.js";
32
34
  import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
33
35
  import { BatchTurnRunner } from "./batch-turn-runner.js";
34
36
  import { accumulateUsage, buildAssistantBatchMessage, buildBatchChatCompletionRequest, buildBatchName, buildToolBatchMessage, extractJsonObject, getBatchUsage, hasUsage, parseToolArgumentsOrRaw, toLocalToolCall, } from "./batch-utils.js";
35
- import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
37
+ import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, isCompactionSummaryMessage, isCompactionThrash, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
36
38
  import { CouncilManager } from "./council-manager.js";
37
39
  import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
38
40
  import { DelegationManager } from "./delegations.js";
39
41
  import { loadFlowResumeDigest } from "./flow-resume.js";
42
+ import { beginInteractivePause, endInteractivePause, isInteractivePaused } from "./interactive-pause.js";
40
43
  import { MessageProcessor } from "./message-processor.js";
41
44
  import { lastPersistedSeq } from "./message-seq.js";
42
45
  import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
46
+ import { getReactiveDelegationThresholdChars, shouldReactivelyEscalate } from "./reactive-delegation.js";
43
47
  import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
44
48
  import { withStreamRetry } from "./retry-stream.js";
45
49
  import { StreamRunner } from "./stream-runner.js";
46
50
  import { resolveModelForTask } from "./sub-agent-model-tier.js";
51
+ import { compactSubAgentMessages } from "./subagent-compactor.js";
47
52
  import { setProviderHint } from "./token-counter.js";
53
+ import { getToolLimitAutoRecoverCap } from "./tool-limit-auto-recover.js";
48
54
  import { firstLine, formatSubagentActivity, toToolResult } from "./tool-utils.js";
49
55
  // ---------------------------------------------------------------------------
50
56
  // Provider implementations
@@ -69,22 +75,65 @@ function isAnyProviderApiBase(url) {
69
75
  }
70
76
  return false;
71
77
  }
72
- /**
73
- * Generate a session title using the Anthropic provider.
74
- * Kept as a lightweight stub for Phase 0 — title generation ships in Phase 1.
75
- */
76
- function genTitle(_provider, userMessage) {
77
- // Phase 0 stub: return a truncated version of the first user message as title.
78
- // Phase 1 will replace this with a real LLM-based title generation call.
79
- const title = userMessage.slice(0, 60).trim() || "New session";
80
- return Promise.resolve({ title, modelId: getCurrentModel() });
78
+ const TITLE_SYSTEM_PROMPT = `You are a session-naming assistant. Given the first message a user sent to an AI coding assistant, produce a short session title.
79
+
80
+ Rules:
81
+ - 5-7 words that capture the essence of the request (the goal, not the phrasing).
82
+ - Same language as the user's message (English request → English title, Vietnamese → Vietnamese, etc.).
83
+ - Title Case for languages that use it; natural casing otherwise.
84
+ - No surrounding quotes, no trailing punctuation, no emoji, no markdown.
85
+ - Output ONLY the title text nothing else.`;
86
+ /** Deterministic fallback title: truncated first user message, or "" when the
87
+ * message is empty or a JSON-ish payload (so the caller leaves title = NULL
88
+ * and the picker renders a clean "(untitled)" instead of a literal "{}"). */
89
+ export function fallbackTitle(userMessage) {
90
+ const trimmed = userMessage.trim();
91
+ if (!trimmed)
92
+ return "";
93
+ // JSON object/array payloads (programmatic first messages) make useless titles.
94
+ if (/^[{[]/.test(trimmed) && /[}\]]$/.test(trimmed))
95
+ return "";
96
+ return trimmed.slice(0, 60).trim();
97
+ }
98
+ /** Strip quotes/backticks, collapse whitespace, drop trailing punctuation, cap at ~60 chars. */
99
+ function sanitizeTitle(raw) {
100
+ let t = raw.trim().split("\n")[0]?.trim() ?? "";
101
+ t = t.replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "");
102
+ t = t
103
+ .replace(/\s+/g, " ")
104
+ .replace(/[.,:;!?…]+$/g, "")
105
+ .trim();
106
+ if (t.length > 60)
107
+ t = t.slice(0, 60).trim();
108
+ return t;
81
109
  }
82
110
  /**
83
- * Resolve a model ID to a runnable AI SDK LanguageModel.
84
- * Uses the Anthropic provider factory created by createProvider().
111
+ * Generate a session title with a single lightweight LLM call on the current
112
+ * session model. Falls back to a truncated first-message title on any failure.
85
113
  */
86
- function resolveModelRuntime(provider, modelId) {
87
- return resolveRuntime(provider, modelId);
114
+ async function genTitle(userMessage, modelId) {
115
+ try {
116
+ const { generateText } = await import("ai");
117
+ const runtime = resolveModelRuntime(modelId);
118
+ const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
119
+ const { text, usage } = await generateText({
120
+ model: runtime.model,
121
+ system: TITLE_SYSTEM_PROMPT,
122
+ prompt: `User's first message:\n\n${snippet}\n\nTitle:`,
123
+ ...resolveTemperatureParam(runtime, 0.3),
124
+ ...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: 64 }),
125
+ ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
126
+ });
127
+ const title = sanitizeTitle(text ?? "");
128
+ if (title) {
129
+ return { title, modelId, usage };
130
+ }
131
+ console.error(`[orchestrator/genTitle] title generation returned empty text (model=${modelId}); using fallback`);
132
+ }
133
+ catch (err) {
134
+ console.error(`[orchestrator/genTitle] title generation failed (model=${modelId}): ${err?.message}; using fallback`);
135
+ }
136
+ return { title: fallbackTitle(userMessage), modelId };
88
137
  }
89
138
  async function toolSetToBatchTools(_tools) {
90
139
  // Batch API not supported with Anthropic in Phase 0. Phase 1 may add this.
@@ -110,6 +159,7 @@ function createTools(_bash, _provider, _mode, _opts) {
110
159
  listDelegations: _opts?.listDelegations,
111
160
  killDelegation: _opts?.killDelegation,
112
161
  modelId: _opts?.modelId,
162
+ sessionId: _opts?.sessionId,
113
163
  });
114
164
  }
115
165
  // ---------------------------------------------------------------------------
@@ -138,6 +188,8 @@ export class Agent {
138
188
  steerDrain = null;
139
189
  maxToolRounds;
140
190
  hardMaxToolRounds;
191
+ /** Original hard ceiling at construction — bounds auto-compaction extension. */
192
+ _initialHardMaxToolRounds = 0;
141
193
  mode = "agent";
142
194
  modelId;
143
195
  maxTokens;
@@ -196,6 +248,9 @@ export class Agent {
196
248
  _turnUserGoalExcerpt = "";
197
249
  /** Compaction statistics tracking count and total tokens saved. */
198
250
  _compactionStats = { count: 0, totalSaved: 0 };
251
+ /** Token count immediately after the last successful compaction (or null if none yet this
252
+ * session). Used by the cross-turn anti-thrash guard in postTurnCompact. */
253
+ _lastCompactionTokensAfter = null;
199
254
  /**
200
255
  * Pinned message sequences. A pinned user message is preserved verbatim across
201
256
  * compaction — it is re-injected as a system note immediately after the
@@ -208,6 +263,15 @@ export class Agent {
208
263
  // Phase C3: cross-turn tool-output dedup. One instance per session; bumped
209
264
  // on each user turn. Lazily initialized so disabled-via-env path stays cheap.
210
265
  _crossTurnDedup = isCrossTurnDedupEnabled() ? new CrossTurnDedup() : null;
266
+ // Reactive sub-session escalation: the PREVIOUS turn's cumulative tool-output
267
+ // chars (from the top-level cap), reported by the tool engine at turn end and
268
+ // read at the next turn's route decision. See reactive-delegation.ts.
269
+ _lastTurnToolChars = 0;
270
+ // Instrument (cold-first-turn measurement): ordinal of the top-level turn whose
271
+ // tool load was last reported. Lets telemetry distinguish a turn-1 blow-up
272
+ // (which reactive escalation CANNOT isolate) from a later one (which it can).
273
+ // Gates the decision to build an in-turn checkpoint. See CONTEXT-CONTROL-LAYERS.md.
274
+ _turnLoadOrdinal = 0;
211
275
  // Phase C4 — input-keyed read-path budget. Complements C3 (output hash) by
212
276
  // catching re-reads of files the agent edited between rounds. Disabled
213
277
  // when MUONROI_MAX_READS_PER_PATH=0.
@@ -246,6 +310,10 @@ export class Agent {
246
310
  this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
247
311
  const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
248
312
  this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
313
+ // Baseline captured so auto-compaction ceiling extension (see
314
+ // extendHardCeilingForAutoCompaction) is bounded relative to the ORIGINAL
315
+ // hard cap and cannot grow without limit across a runaway turn.
316
+ this._initialHardMaxToolRounds = this.hardMaxToolRounds;
249
317
  const envMax = Number(process.env.MUONROI_MAX_TOKENS);
250
318
  this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
251
319
  this.batchApi = options.batchApi ?? false;
@@ -280,7 +348,7 @@ export class Agent {
280
348
  this.workspace = this.sessionStore.getWorkspace();
281
349
  this.session = this.sessionStore.openSession(options.session, this.modelId, this.mode, this.bash.getCwd());
282
350
  this.mode = this.session.mode;
283
- const transcript = loadTranscriptState(this.session.id);
351
+ const transcript = loadSessionChainTranscriptState(this.session.id);
284
352
  this.messages = transcript.messages;
285
353
  this.messageSeqs = transcript.seqs;
286
354
  this.sessionStore.setModel(this.session.id, this.modelId);
@@ -330,30 +398,58 @@ export class Agent {
330
398
  getActiveRunId() {
331
399
  return this._activeRunId;
332
400
  }
333
- setModel(model) {
401
+ /**
402
+ * Point the agent at `model`, re-resolving the provider when the new model
403
+ * belongs to a different one.
404
+ *
405
+ * EVERY path that changes `this.modelId` must go through here. Changing the
406
+ * id alone leaves `providerId` / `provider` / `baseURL` / `apiKey` describing
407
+ * the PREVIOUS provider, so the next turn POSTs the new model to the old
408
+ * provider's endpoint — which rejects it as unknown (session 0c6728ba1a25:
409
+ * `gpt-5.4` sent to api.x.ai → 404 "The model gpt-5.4 does not exist", a
410
+ * wiring bug that reads like a bad model name). `setMode` used to do exactly
411
+ * that; it now calls this too.
412
+ *
413
+ * Session persistence is NOT done here — callers own that, because setMode
414
+ * writes mode+model together.
415
+ */
416
+ _applyModelId(model) {
334
417
  this.modelId = normalizeModelId(model);
335
418
  const newProviderId = detectProviderForModel(this.modelId);
336
- if (newProviderId !== this.providerId && this.apiKey) {
419
+ if (newProviderId !== this.providerId) {
337
420
  this.providerId = newProviderId;
338
421
  setProviderHint(this.providerId);
339
422
  // Drop this.baseURL when it points at a DIFFERENT provider's default
340
423
  // apiBase — otherwise the rebuilt factory binds the new provider's
341
424
  // strategy to the OLD provider's URL, sending requests to the wrong
342
425
  // host. Evidence: session 2492d6579b1d — user switched defaultProvider
343
- // siliconflow→ (via UI), this.baseURL was still api.deepseek.com from
344
- // startup, SiliconflowStrategy.createFactory was created with that
345
- // baseURL, requests landed at api.deepseek.com which rejected the SF-
346
- // style model id ("deepseek-ai/DeepSeek-V4-Flash") with "supported API
347
- // model names are deepseek-v4-pro or deepseek-v4-flash".
426
+ // (via UI), this.baseURL was still the old provider's apiBase from
427
+ // startup, the rebuilt strategy factory was created with that stale
428
+ // baseURL, and requests landed at the wrong host which rejected the
429
+ // model id.
348
430
  // A user-supplied custom baseURL is preserved only when it does NOT
349
431
  // match any known provider's apiBase (i.e. it's a real override, not
350
432
  // a stale default).
351
433
  const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
352
- const effectiveBaseURL = staleBaseURL ? undefined : (this.baseURL ?? undefined);
353
434
  if (staleBaseURL)
354
435
  this.baseURL = null;
355
- this.provider = createProvider(this.providerId, this.apiKey, effectiveBaseURL);
436
+ // Provider changed — DEFER factory construction. The current this.apiKey
437
+ // belongs to the PREVIOUS provider (or is the "oauth" OAuth sentinel) and
438
+ // is invalid for the new one. Rebuilding here with it sent provider A's
439
+ // key to provider B → 401 "Authentication Fails, your api key … is
440
+ // invalid" on the next chat, for EVERY TUI provider switch (evidence:
441
+ // routing log routed to "deepseek-v4-flash via deepseek" with a
442
+ // non-deepseek key; and the "oauth" leak in session ff932f8568e8). Clear
443
+ // the stale key, null the provider, and re-arm _initOAuthProvider, which
444
+ // resolves the NEW provider's own key (or OAuth tokens) before the next
445
+ // turn (message-processor calls initOAuthProvider at turn start).
446
+ this.apiKey = null;
447
+ this.provider = null;
448
+ this._oauthInitDone = false;
356
449
  }
450
+ }
451
+ setModel(model) {
452
+ this._applyModelId(model);
357
453
  if (this.sessionStore && this.session) {
358
454
  this.sessionStore.setModel(this.session.id, this.modelId);
359
455
  this.session = this.sessionStore.getRequiredSession(this.session.id);
@@ -381,7 +477,11 @@ export class Agent {
381
477
  this.mode = mode;
382
478
  const modeModel = getModeSpecificModel(mode);
383
479
  if (modeModel) {
384
- this.modelId = normalizeModelId(modeModel);
480
+ // Via _applyModelId, NOT a bare `this.modelId = ...`: a mode-specific
481
+ // model can belong to a different provider than the session's, and
482
+ // assigning the id alone would leave provider/baseURL/apiKey pointing at
483
+ // the old one — the cross-wire class in _applyModelId's docstring.
484
+ this._applyModelId(modeModel);
385
485
  }
386
486
  if (this.sessionStore && this.session) {
387
487
  this.sessionStore.setMode(this.session.id, mode);
@@ -392,6 +492,17 @@ export class Agent {
392
492
  }
393
493
  setPlanContext(ctx) {
394
494
  this.planContext = ctx;
495
+ // Persist so that bare continuation phrases ("tiếp tục", "continue") after
496
+ // interrupt or cross-process resume can re-hydrate the APPROVED PLAN section
497
+ // instead of forgetting context and re-asking for details.
498
+ if (this.session && ctx) {
499
+ try {
500
+ persistApprovedPlan(this.session.id, ctx);
501
+ }
502
+ catch {
503
+ /* best-effort */
504
+ }
505
+ }
395
506
  }
396
507
  setSendTelegramFile(fn) {
397
508
  this.sendTelegramFile = fn;
@@ -403,7 +514,7 @@ export class Agent {
403
514
  this.apiKey = apiKey;
404
515
  // Drop baseURL when it points at a DIFFERENT provider's default apiBase
405
516
  // (e.g. caller passed the legacy anthropic URL while providerId is
406
- // siliconflow — without this we'd send siliconflow requests to
517
+ // deepseek — without this we'd send deepseek requests to
407
518
  // api.anthropic.com or similar). User-supplied custom URLs that don't
408
519
  // match any known provider's apiBase are preserved as real overrides.
409
520
  const stale = isAnyProviderApiBase(baseURL) && baseURL !== apiBaseFor(this.providerId);
@@ -419,6 +530,14 @@ export class Agent {
419
530
  getProviderId() {
420
531
  return this.providerId;
421
532
  }
533
+ /**
534
+ * Expose the provider factory for external callers (e.g., /compact slash command,
535
+ * sub-agent spawning) that need to make LLM calls outside the normal processMessage flow.
536
+ * Throws if no provider is configured.
537
+ */
538
+ getProvider() {
539
+ return this.requireProvider();
540
+ }
422
541
  getCwd() {
423
542
  return this.bash.getCwd();
424
543
  }
@@ -432,6 +551,9 @@ export class Agent {
432
551
  setMessages(messages) {
433
552
  this.messages = messages;
434
553
  this.messageSeqs = messages.map(() => null);
554
+ // Manual compaction (/compact) or /clear replaces the conversation; reset
555
+ // the per-turn auto-compact latch so future tool loops can compact again.
556
+ this._compactedThisTurn = false;
435
557
  }
436
558
  async listSchedules() {
437
559
  return this.schedules.list();
@@ -460,7 +582,7 @@ export class Agent {
460
582
  if (!provider) {
461
583
  return "New session";
462
584
  }
463
- const generated = await genTitle(provider, userMessage);
585
+ const generated = await genTitle(userMessage, this.modelId);
464
586
  this.recordUsage(generated.usage, "title", generated.modelId);
465
587
  if (this.sessionStore && this.session && !this.session.title && generated.title) {
466
588
  this.sessionStore.setTitle(this.session.id, generated.title);
@@ -493,7 +615,7 @@ export class Agent {
493
615
  charBudget -= snippet.length;
494
616
  }
495
617
  const conversationContext = contextParts.join("\n\n");
496
- const result = await runSideQuestion(question, this.provider, this.modelId, conversationContext, signal);
618
+ const result = await runSideQuestion(question, this.modelId, conversationContext, signal);
497
619
  this.recordUsage(result.usage, "other");
498
620
  return result;
499
621
  }
@@ -530,6 +652,22 @@ export class Agent {
530
652
  setToolLoopCapHandler(fn) {
531
653
  this._toolLoopCapHandler = fn;
532
654
  }
655
+ /**
656
+ * Extend the absolute hard-cap ceiling when the turn is being sustained by
657
+ * AUTO-COMPACTION (a productive long task, not a runaway). Rationale: each
658
+ * auto-compacted round resets context to O(N) input, so it is cheap — the
659
+ * hard cap's cost-runaway purpose is already served by the compaction. We
660
+ * grant `maxToolRounds/2` more headroom per compaction, bounded to at most
661
+ * one extra auto-recover budget's worth above the ORIGINAL ceiling so a
662
+ * genuinely wedged turn (which trips the pattern guard, not this path) can
663
+ * never grow the cap without limit. Called from the tool-engine auto-recover
664
+ * branch after a successful compaction.
665
+ */
666
+ extendHardCeilingForAutoCompaction() {
667
+ const bump = Math.max(25, Math.floor(this.maxToolRounds / 2));
668
+ const absoluteMax = this._initialHardMaxToolRounds + bump * getToolLimitAutoRecoverCap();
669
+ this.hardMaxToolRounds = Math.min(this.hardMaxToolRounds + bump, absoluteMax);
670
+ }
533
671
  // Safety-override handler — set by the UI (app.tsx) at startup. Invoked
534
672
  // from the message-processor tool-result pipeline when a safety block
535
673
  // (catastrophic, dangerous, git-safety, empty-bash) is detected. The UI
@@ -539,6 +677,15 @@ export class Agent {
539
677
  setSafetyOverrideHandler(fn) {
540
678
  this._safetyOverrideHandler = fn;
541
679
  }
680
+ // ask_user handler — set by the UI (app.tsx). Invoked when the model calls the
681
+ // `ask_user` tool: the UI surfaces an agent-authored card and resolves with the
682
+ // human's answer string, which becomes the tool result. When unset (headless
683
+ // without an answerer), the tool returns the dismissed sentinel so the agent
684
+ // still continues from its own judgment.
685
+ _askUserHandler = null;
686
+ setAskUserHandler(fn) {
687
+ this._askUserHandler = fn;
688
+ }
542
689
  respondToToolApproval(approvalId, approved) {
543
690
  const toolApprovalResponse = {
544
691
  role: "tool",
@@ -585,6 +732,7 @@ export class Agent {
585
732
  compaction_summary: undefined,
586
733
  });
587
734
  this._compactionStats = { count: 0, totalSaved: 0 };
735
+ this._lastCompactionTokensAfter = null;
588
736
  this._pinnedSeqs.clear();
589
737
  if (!this.sessionStore) {
590
738
  this.messages = [];
@@ -663,9 +811,35 @@ export class Agent {
663
811
  return [];
664
812
  return buildChatEntries(this.session.id);
665
813
  }
814
+ /**
815
+ * The session tree this TUI currently hosts: the root conversation plus every
816
+ * rotation / sub-agent descendant, with per-session metadata. Content from
817
+ * these sessions is already merged into the transcript on resume; this exposes
818
+ * WHICH sessions produced it so the rail can show the multi-session structure.
819
+ */
820
+ getSessionTree() {
821
+ if (!this.session)
822
+ return [];
823
+ try {
824
+ return getSessionChainInfos(this.session.id);
825
+ }
826
+ catch (err) {
827
+ logger.error("orchestrator", "getSessionTree failed", { message: err?.message });
828
+ return [];
829
+ }
830
+ }
666
831
  getLastTodoSnapshot() {
667
832
  if (!this.session)
668
833
  return null;
834
+ try {
835
+ const { getTaskListSnapshotFromGsd } = require("../gsd/phase-sync.js");
836
+ const gsdSnap = getTaskListSnapshotFromGsd(this.bash?.getCwd() ?? process.cwd());
837
+ if (gsdSnap)
838
+ return gsdSnap;
839
+ }
840
+ catch (err) {
841
+ // fail-open to legacy todo_write args
842
+ }
669
843
  const argsJson = getLastTodoWriteArgs(this.session.id);
670
844
  if (!argsJson)
671
845
  return null;
@@ -677,7 +851,7 @@ export class Agent {
677
851
  return {
678
852
  workspace: this.workspace,
679
853
  session: this.session,
680
- messages: loadTranscript(this.session.id),
854
+ messages: loadSessionChainTranscriptState(this.session.id).messages,
681
855
  entries: buildChatEntries(this.session.id),
682
856
  totalTokens: getSessionTotalTokens(this.session.id),
683
857
  };
@@ -796,7 +970,7 @@ export class Agent {
796
970
  // where orchestrator/task/title traffic is actually spending.
797
971
  // Best-effort: failures inside appendCostLog are swallowed (see cost-log.ts).
798
972
  const breakdown = source === "message" ? (this._lastPromptBreakdown ?? undefined) : undefined;
799
- // Sanitize actualInputTokens for providers (e.g. SiliconFlow) that return
973
+ // Sanitize actualInputTokens for providers that return
800
974
  // implausibly low prompt_tokens (e.g. 10) regardless of prompt size.
801
975
  const estIn = breakdown ? Math.ceil(((breakdown.systemChars ?? 0) + (breakdown.messagesChars ?? 0)) / 4) : 0;
802
976
  const actualInput = sanitizeInputTokens(totalInput, estIn);
@@ -919,8 +1093,27 @@ export class Agent {
919
1093
  const totalUsage = {};
920
1094
  let assistantText = "";
921
1095
  let lastActivity = initialDetail;
1096
+ // Phase B3 parity: the streamText sub-agent path compacts older tool
1097
+ // results in prepareStep, but this batch path historically concatenated
1098
+ // `[...childMessages, ...turnMessages]` unbounded every round — on a long
1099
+ // batch loop the re-sent history balloons exactly like the stream path did
1100
+ // before B3. Apply the same compactor here (round >= 1, mirroring the
1101
+ // stream path's `stepNumber >= 1` gate). High-value results stay verbatim.
1102
+ const batchCompactThreshold = getSubAgentCompactThresholdChars();
1103
+ const batchCompactKeepLast = getSubAgentCompactKeepLast();
1104
+ const batchChildCtxWindow = childRuntime.modelInfo?.contextWindow ?? 0;
1105
+ const batchIsReasoningModel = childRuntime.modelInfo?.reasoning === true;
922
1106
  for (let round = 0; round < maxSteps; round++) {
923
1107
  const batchRequestId = `task-${Date.now()}-${round + 1}`;
1108
+ const roundMessages = round < 1
1109
+ ? [...childMessages, ...turnMessages]
1110
+ : compactSubAgentMessages([...childMessages, ...turnMessages], {
1111
+ thresholdChars: batchCompactThreshold,
1112
+ keepLastTurns: batchCompactKeepLast,
1113
+ contextWindowTokens: batchChildCtxWindow,
1114
+ contextFillRatio: batchIsReasoningModel ? 0.3 : undefined,
1115
+ stripOldReasoning: batchIsReasoningModel,
1116
+ });
924
1117
  await addBatchRequests({
925
1118
  ...this.getBatchClientOptions(signal),
926
1119
  batchId: batch.batch_id,
@@ -931,8 +1124,8 @@ export class Agent {
931
1124
  chat_get_completion: buildBatchChatCompletionRequest({
932
1125
  modelId: childRuntime.modelId,
933
1126
  system: childSystem,
934
- messages: [...childMessages, ...turnMessages],
935
- temperature: request.agent === "explore" ? 0.2 : 0.5,
1127
+ messages: roundMessages,
1128
+ temperature: childRuntime.modelInfo?.fixedTemperature ?? (request.agent === "explore" ? 0.2 : 0.5),
936
1129
  maxOutputTokens: !childCaps.acceptsParam("maxOutputTokens", childRuntime.modelInfo)
937
1130
  ? undefined
938
1131
  : Math.min(this.maxTokens, 8_192),
@@ -1023,7 +1216,6 @@ export class Agent {
1023
1216
  async runTaskRequest(request, onActivity, abortSignal) {
1024
1217
  const provider = this.requireProvider();
1025
1218
  const deps = {
1026
- getProvider: () => provider,
1027
1219
  resolveModelForTask: (task) => this._resolveModelForTask(task),
1028
1220
  getModelId: () => this.modelId,
1029
1221
  getProviderId: () => this.providerId,
@@ -1244,7 +1436,10 @@ export class Agent {
1244
1436
  return this._resolveModelForTask("compact");
1245
1437
  }
1246
1438
  _resolveModelForTask(task) {
1247
- return resolveModelForTask(task, this.providerId, this.modelId);
1439
+ const parentTier = getModelInfo(this.modelId)?.tier;
1440
+ return resolveModelForTask(task, this.providerId, this.modelId, undefined, {
1441
+ parentTier,
1442
+ });
1248
1443
  }
1249
1444
  async compactForContext(provider, system, contextWindow, signal, settings = this.getCompactionSettings(contextWindow), force = false) {
1250
1445
  if (!this.session)
@@ -1252,7 +1447,7 @@ export class Agent {
1252
1447
  // Phase 1: ask the compaction proposer model whether to compact and what to keep/drop.
1253
1448
  // Only compact if the model says yes. On error/skip, fall back to heuristic.
1254
1449
  const compactModelId = this._resolveCompactModel();
1255
- const proposal = await proposeCompaction(provider, compactModelId, this.messages, signal);
1450
+ const proposal = await proposeCompaction(compactModelId, this.messages, signal);
1256
1451
  if (proposal !== null) {
1257
1452
  // Model decided — compact only if model says shouldCompact
1258
1453
  if (force) {
@@ -1315,7 +1510,7 @@ export class Agent {
1315
1510
  const customInstructions = isSubSession
1316
1511
  ? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
1317
1512
  : undefined;
1318
- const { summary, usage: compactUsage } = await generateCompactionSummary(provider, compactModelId, preparation, customInstructions, signal);
1513
+ const { summary, usage: compactUsage } = await generateCompactionSummary(compactModelId, preparation, customInstructions, signal);
1319
1514
  // Record compaction call in cost-log — bypasses recordUsage because
1320
1515
  // compaction returns usage separately and isn't routed through the
1321
1516
  // status-bar / usage event pipeline (intentional: it's overhead, not user spend).
@@ -1387,6 +1582,7 @@ export class Agent {
1387
1582
  // grossSaved tells the user how many context tokens were reclaimed — a real benefit that
1388
1583
  // reduces subsequent turn costs.
1389
1584
  const tokensAfter = estimateConversationTokens(system, this.messages);
1585
+ this._lastCompactionTokensAfter = tokensAfter;
1390
1586
  const saved = Math.max(0, preparation.tokensBefore - tokensAfter);
1391
1587
  const pct = preparation.tokensBefore > 0 ? ((saved / preparation.tokensBefore) * 100).toFixed(1) : "0.0";
1392
1588
  this._compactionStats.count++;
@@ -1452,6 +1648,13 @@ export class Agent {
1452
1648
  minMeaningfulTokens,
1453
1649
  });
1454
1650
  }
1651
+ const minNew = getAutoCompactMinNewTokens();
1652
+ if (isCompactionThrash(tokens, this._lastCompactionTokensAfter, minNew)) {
1653
+ return log(false, `anti-thrash (only ${tokens - (this._lastCompactionTokensAfter ?? 0)} new < ${minNew})`, {
1654
+ tokens,
1655
+ lastAfter: this._lastCompactionTokensAfter,
1656
+ });
1657
+ }
1455
1658
  log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
1456
1659
  await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
1457
1660
  }
@@ -1464,8 +1667,8 @@ export class Agent {
1464
1667
  // (respondToCouncilQuestion/Preflight + the internal _create*Responder
1465
1668
  // hooks used by orchestrator.agent.test.ts).
1466
1669
  // ========================================================================
1467
- respondToCouncilQuestion(questionId, answer) {
1468
- this.councilManager.respondToQuestion(questionId, answer);
1670
+ respondToCouncilQuestion(questionId, answer, questionText) {
1671
+ this.councilManager.respondToQuestion(questionId, answer, questionText);
1469
1672
  }
1470
1673
  respondToCouncilPreflight(preflightId, approved) {
1471
1674
  this.councilManager.respondToPreflight(preflightId, approved);
@@ -1474,7 +1677,7 @@ export class Agent {
1474
1677
  // Council v2 — Clarify → Confirm → Debate → Plan → Execute
1475
1678
  // ========================================================================
1476
1679
  async *runCouncilV2(topic, options) {
1477
- const { runCouncil } = await import("../council/index.js");
1680
+ const { runCouncil, buildNeutralPostCouncilContinuation } = await import("../council/index.js");
1478
1681
  const { createCouncilLLM } = await import("../council/llm.js");
1479
1682
  const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
1480
1683
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
@@ -1514,6 +1717,9 @@ export class Agent {
1514
1717
  catch (err) {
1515
1718
  logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
1516
1719
  }
1720
+ // Captures the post-debate action so we can auto-continue on
1721
+ // "continue_session" (user chose to keep working with the debate result).
1722
+ let chosenAction;
1517
1723
  try {
1518
1724
  const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
1519
1725
  skipClarification: options?.skipClarification,
@@ -1521,12 +1727,42 @@ export class Agent {
1521
1727
  cwd: this.bash.getCwd(),
1522
1728
  councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
1523
1729
  signal,
1730
+ // convene_council path — suppress ALL hardcoded post-debate decision
1731
+ // surface; the agent decides what happens after the synthesis.
1732
+ convenePath: options?.convenePath,
1733
+ // When the Context Rail is active it carries leader/panel/cost as
1734
+ // ambient sidebar rows, so suppress the duplicate inline summary.
1735
+ suppressInlineMeta: isContextRailEnabled(),
1524
1736
  runDir, // B1 — persist decisions.lock.md for the /council slash path
1737
+ // #2 — isolated research bridge. Same StreamRunner sub-agent the /ideal
1738
+ // sprint-runner uses (runTaskRequest): a budget-capped, near-empty
1739
+ // explore child runs the debate's research phase so its multi-step tool
1740
+ // clutter never accretes into the council thread/context. Threads the
1741
+ // council abort signal so Esc cancels the research child too.
1742
+ runIsolatedTask: (request) => this.runTaskRequest(request, undefined, signal),
1743
+ onPostDebateAction: (action) => {
1744
+ chosenAction = action;
1745
+ // Relay to the auto-council caller (tool-engine) so nested runs honor
1746
+ // the user's choice instead of always continuing.
1747
+ this.councilManager.setLastPostDebateAction(action);
1748
+ },
1525
1749
  });
1526
1750
  let result;
1751
+ // Hold back runCouncil's terminal `done`. The /council slash consumer
1752
+ // (use-app-logic.tsx) does `for await (chunk of gen) { if (chunk.type ===
1753
+ // "done") break; }` — so forwarding runCouncil's end-of-debate `done`
1754
+ // strands THIS generator before the continue_session auto-continue below
1755
+ // ever runs (the generator is suspended at the yield and never resumes).
1756
+ // Swallow it here and re-emit our own `done` after the optional
1757
+ // continuation so the consumer terminates at the right boundary.
1758
+ let innerDoneSeen = false;
1527
1759
  do {
1528
1760
  result = await gen.next();
1529
1761
  if (!result.done && result.value) {
1762
+ if (result.value.type === "done") {
1763
+ innerDoneSeen = true;
1764
+ continue;
1765
+ }
1530
1766
  yield result.value;
1531
1767
  }
1532
1768
  } while (!result.done);
@@ -1535,6 +1771,130 @@ export class Agent {
1535
1771
  if (options?.userModelMessage && synthesis) {
1536
1772
  this.appendCompletedTurn(options.userModelMessage, [{ role: "assistant", content: synthesis }]);
1537
1773
  }
1774
+ // Keep working in THIS session when the chosen action calls for it
1775
+ // (continue_session → carry the conclusion; generate_plan/implement →
1776
+ // execute action items). postDebateContinuation is the single source of
1777
+ // truth shared with the auto-council caller (tool-engine). Re-entering
1778
+ // processMessage also writes real message rows, which is what makes the
1779
+ // session resumable (the /council slash path otherwise leaves no messages
1780
+ // and is filtered from the resume picker). Guarded by ownsController so it
1781
+ // fires ONLY on the top-level slash path, never when nested inside
1782
+ // processMessage (auto-council) or drained by the runDebate tool — those
1783
+ // callers manage their own continuation.
1784
+ // convenePath suppresses the hardcoded card (chosenAction stays undefined),
1785
+ // so always hand the synthesis to a normal agent turn via the neutral
1786
+ // continuation and let the agent decide. ownsController scopes this to the
1787
+ // top-level /council slash path (auto-council nests with ownsController
1788
+ // false and continues in tool-engine instead).
1789
+ const continuationPrompt = ownsController && synthesis ? buildNeutralPostCouncilContinuation(synthesis) || null : null;
1790
+ const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
1791
+ if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
1792
+ // #1 — build the council decision in an ISOLATED sub-agent instead of
1793
+ // re-entering the full processMessage turn. The flat turn inherited the
1794
+ // entire multi-round debate history and could overflow the context window
1795
+ // (the exact wedge 97bc9d12 fixed for the /ideal sprint-runner but that
1796
+ // /council never got). runTaskRequest starts near-empty and returns a
1797
+ // compact ToolResult. ANTI-MÙ: the child is NOT blind — it is seeded with
1798
+ // (a) the approved synthesis-as-spec (already in continuationPrompt) and
1799
+ // (b) councilManager.buildContext(), the same compaction-summary + Recent
1800
+ // Conversation + [Council Decision]/[Council Memory] "Key Decisions"
1801
+ // checkpoint the anti-mù layer surfaces — so it has the decision + its
1802
+ // rationale without carrying the raw transcript. Opt out with
1803
+ // MUONROI_COUNCIL_ISOLATE_IMPL=0 (falls back to the processMessage path).
1804
+ yield { type: "content", content: "\n[Implementing the council decision in an isolated context…]\n" };
1805
+ this.councilManager.setContinuation(true);
1806
+ try {
1807
+ let councilCheckpoint = "";
1808
+ try {
1809
+ councilCheckpoint = this.councilManager.buildContext();
1810
+ }
1811
+ catch {
1812
+ /* anti-mù bundle is best-effort — the synthesis alone still grounds the build */
1813
+ }
1814
+ const seededPrompt = councilCheckpoint
1815
+ ? `${continuationPrompt}\n\n## Council context (decision + recent session — do NOT re-debate, just build)\n${councilCheckpoint}`
1816
+ : continuationPrompt;
1817
+ let result;
1818
+ try {
1819
+ result = await this.runTaskRequest({ agent: "general", description: "Council implementation", prompt: seededPrompt, modelId: this.modelId }, undefined, signal);
1820
+ }
1821
+ catch (err) {
1822
+ // A throw (vs a returned {success:false}) must not escape and take the
1823
+ // whole /council run down — surface it in-band like the processMessage
1824
+ // path's TurnStallError handling and end the turn cleanly.
1825
+ yield {
1826
+ type: "error",
1827
+ content: `Council implementation failed: ${err instanceof Error ? err.message : String(err)}`,
1828
+ };
1829
+ yield { type: "done" };
1830
+ return;
1831
+ }
1832
+ if (result.success && result.output?.trim()) {
1833
+ yield { type: "content", content: `\n${result.output.trim()}\n` };
1834
+ }
1835
+ else if (result.error) {
1836
+ yield { type: "error", content: `Council implementation failed: ${result.error}` };
1837
+ }
1838
+ // Persist a COMPACT record so the /council slash session stays resumable
1839
+ // (the processMessage path wrote rows for this) WITHOUT re-inheriting the
1840
+ // debate bloat the isolated child deliberately avoided.
1841
+ try {
1842
+ this.appendCompletedTurn({ role: "user", content: "[Council implementation of the debate decision]" }, [{ role: "assistant", content: (result.output ?? result.error ?? "(no output)").trim() }]);
1843
+ }
1844
+ catch {
1845
+ /* non-critical persistence */
1846
+ }
1847
+ yield { type: "done" };
1848
+ }
1849
+ finally {
1850
+ this.councilManager.setContinuation(false);
1851
+ }
1852
+ }
1853
+ else if (continuationPrompt) {
1854
+ yield { type: "content", content: "\n[Continuing with the debate conclusion…]\n" };
1855
+ this.councilManager.setContinuation(true);
1856
+ try {
1857
+ // processMessage emits its own terminal `done`, which becomes the
1858
+ // consumer's break boundary — so the UI stops AFTER the continuation
1859
+ // turn, not before it.
1860
+ //
1861
+ // Guard the continuation with an idle+total watchdog: the per-chunk
1862
+ // stall watchdog only covers streamText byte flow, NOT a turn that
1863
+ // wedges inside a tool call (a `task` sub-agent or long `bash`).
1864
+ // Session 578b2eae7099 hung exactly there — the UI froze at "Council
1865
+ // working… elapsed 0s" with no rescue. On fire we abort the turn and
1866
+ // surface a toast instead of hanging forever.
1867
+ const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
1868
+ const idleMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_IDLE_MS ?? 120_000);
1869
+ const totalMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_TOTAL_MS ?? 600_000);
1870
+ try {
1871
+ yield* withTurnWatchdog(this.processMessage(continuationPrompt, options?.observer), {
1872
+ idleMs,
1873
+ totalMs,
1874
+ label: "council continuation turn",
1875
+ shouldSuppressFire: isInteractivePaused,
1876
+ });
1877
+ }
1878
+ catch (err) {
1879
+ if (err instanceof TurnStallError) {
1880
+ this.abortController?.abort(new DOMException(err.message, "TimeoutError"));
1881
+ yield { type: "error", content: `Council continuation stalled — ${err.message}` };
1882
+ yield { type: "done" };
1883
+ }
1884
+ else {
1885
+ throw err;
1886
+ }
1887
+ }
1888
+ }
1889
+ finally {
1890
+ this.councilManager.setContinuation(false);
1891
+ }
1892
+ }
1893
+ else if (innerDoneSeen) {
1894
+ // Non-continuation paths (save_exit, nested auto-council, sprint drain)
1895
+ // still need the terminal `done` the consumer expects.
1896
+ yield { type: "done" };
1897
+ }
1538
1898
  }
1539
1899
  finally {
1540
1900
  if (ownsController && this.abortController?.signal === signal) {
@@ -1555,26 +1915,149 @@ export class Agent {
1555
1915
  phases: [],
1556
1916
  };
1557
1917
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
1558
- const processMessageFn = (m) => this.processMessage(m, options?.observer);
1918
+ // Autonomous-execution permission for the product loop. /ideal's consent
1919
+ // boundary is the preflight plan-approval askcard; once the PO approves the
1920
+ // plan, the sprint IMPLEMENT turn must apply its own file-op mutations without
1921
+ // a per-tool approval prompt. In `safe` mode a Write/Edit surfaces a
1922
+ // tool_approval_request and awaits respondToToolApproval — but in the driven
1923
+ // product-loop context nothing answers it and no approval askcard renders, so
1924
+ // the impl turn wedges forever right after finishReason:tool-calls (observed
1925
+ // live 2026-07-14: 0 files written across grok/opencode/deepseek + isolated &
1926
+ // streamed paths — tool-engine.ts:2997). Elevating safe→auto-edit for the turn
1927
+ // auto-approves file ops (yolo stays yolo); catastrophic bash stays hard-blocked
1928
+ // by permission-mode's CATASTROPHIC_PATTERNS regardless of mode.
1929
+ const self = this;
1930
+ const processMessageFn = (m) => (async function* () {
1931
+ const prev = self.permissionMode;
1932
+ if (self.permissionMode === "safe")
1933
+ self.permissionMode = "auto-edit";
1934
+ try {
1935
+ yield* self.processMessage(m, options?.observer);
1936
+ }
1937
+ finally {
1938
+ self.permissionMode = prev;
1939
+ }
1940
+ })();
1941
+ // Isolated bounded task-runner bridge for the sprint implement stage: a fresh
1942
+ // child context (getSubAgentBudgetChars cap, independent compaction) that does
1943
+ // NOT inherit this turn's council-debate history — the root fix for the live
1944
+ // ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
1945
+ // Same autonomous-permission elevation as the streamed path above.
1946
+ const runIsolatedTask = async (request) => {
1947
+ const prev = self.permissionMode;
1948
+ if (self.permissionMode === "safe")
1949
+ self.permissionMode = "auto-edit";
1950
+ try {
1951
+ return await this.runTaskRequest(request, undefined, this.abortController?.signal);
1952
+ }
1953
+ finally {
1954
+ self.permissionMode = prev;
1955
+ }
1956
+ };
1559
1957
  const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
1560
- // P2.7: compute complexity from the idea using PIL Layer 1 heuristics (cheap,
1561
- // no LLM calls). Only meaningful for "start"; other subcommands ignore it.
1958
+ // P2.7 (LLM-first no-regex routing): the work-depth tier that decides
1959
+ // /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
1960
+ // classifier emits for chat turns), NOT by a keyword scorer. The old regex
1961
+ // `scoreComplexity` mis-tiered plainly-phrased heavy work — a "merge / vendor
1962
+ // / rename" refactor scored LOW because it lacked the FORCE_HIGH keywords —
1963
+ // which hot-pathed genuinely heavy tasks with zero interview/council. Only
1964
+ // meaningful for "start"; other subcommands ignore it.
1562
1965
  let complexity;
1966
+ let needsClarification;
1563
1967
  let sufficiencyMissing;
1968
+ let routeForceCouncil = payload.flags.forceCouncil;
1564
1969
  if (payload.subcommand === "start" && payload.idea) {
1565
- const { scoreComplexity, scoreSufficiency } = await import("../pil/layer1-intent.js");
1566
- const result = scoreComplexity({
1567
- rawText: payload.idea,
1568
- taskType: null,
1569
- t0HitCount: 0,
1570
- hasMaxSprintsOne: payload.flags.maxSprints === 1,
1571
- });
1572
- complexity = result.complexity;
1573
- // Sufficiency gate vague briefs ("todo app") force Council so the
1574
- // discovery AskCard can fill in persona/MVP/architecture/verify before
1575
- // any code is scaffolded.
1576
- const suff = scoreSufficiency({ rawText: payload.idea });
1577
- sufficiencyMissing = suff.sufficient ? undefined : suff.missing;
1970
+ let depth = "standard";
1971
+ try {
1972
+ const { createLlmClassifier } = await import("../pil/llm-classify.js");
1973
+ const { probeRepoGrounding } = await import("../pil/repo-grounding-probe.js");
1974
+ const { getRepoStructureHints } = await import("../pil/repo-structure-hints.js");
1975
+ const classify = createLlmClassifier(this.modelId, {
1976
+ routeFastTier: true,
1977
+ // /ideal routing needs a reliable depth verdict; allow cross-provider
1978
+ // fallback so an agentic/no-fast-tier session model (e.g. xai) doesn't
1979
+ // strand the classify at fail-open. Bounded by CLASSIFY_TOTAL_BUDGET_MS.
1980
+ crossProviderFallback: true,
1981
+ });
1982
+ const res = await classify(payload.idea);
1983
+ if (res?.depthTier) {
1984
+ depth = res.depthTier;
1985
+ }
1986
+ else {
1987
+ // Same convention as PIL Layer 1: a null/garbled depth defaults to the
1988
+ // safe middle. Logged so a persistently-null classifier is diagnosable.
1989
+ console.error(`[ideal/route] model depth classify returned no depthTier — defaulting to "standard". idea=${JSON.stringify(payload.idea.slice(0, 80))}`);
1990
+ }
1991
+ // LLM-first clarity signal — feeds the downstream interview as a hint.
1992
+ if (res?.needsClarification === true)
1993
+ needsClarification = true;
1994
+ // Design B — deterministic repo-grounding probe. Trigger on Layer-1
1995
+ // OUTPUT (ambiguity), NOT a prompt regex: run only when the verdict is
1996
+ // non-trivial (depth !== "quick") OR low-confidence, so a well-specified
1997
+ // trivial task still hot-paths on one cheap call. The probe itself is
1998
+ // deterministic (REPO_DEEP_MAP index + bounded on-disk reads, no LLM).
1999
+ // Once it runs its MEASURED facts drive routing — confidence is
2000
+ // trigger-only and can no longer override grounding.
2001
+ const lowConfidence = (res?.confidence ?? 1) < 0.7;
2002
+ if (depth !== "quick" || lowConfidence) {
2003
+ try {
2004
+ const hints = getRepoStructureHints(process.cwd());
2005
+ const probe = probeRepoGrounding(payload.idea, hints, { cwd: process.cwd() });
2006
+ if (probe.ran) {
2007
+ // Invariant: probe ran AND zero matched files, or a symbol
2008
+ // collision → route INTO Council (the arbiter). Never direct-
2009
+ // dispatch from weak grounding.
2010
+ if (probe.groundingUncertainty) {
2011
+ needsClarification = true;
2012
+ depth = depth === "quick" ? "standard" : depth;
2013
+ }
2014
+ // Measured size escalation: a large grounded surface routes heavy
2015
+ // regardless of the sentence-level classify verdict.
2016
+ if (probe.bucket === "large")
2017
+ depth = "heavy";
2018
+ console.error(`[ideal/route] grounding probe: files=${probe.matchedFiles} loc=${probe.totalLoc} ` +
2019
+ `dirs=${probe.matchedDirs} collision=${probe.collision} bucket=${probe.bucket} ` +
2020
+ `uncertainty=${probe.groundingUncertainty} → depth=${depth}`);
2021
+ }
2022
+ }
2023
+ catch (err) {
2024
+ // Fail-open: a probe hiccup must never block /ideal (No-Silent-Catch).
2025
+ console.error(`[ideal/route] grounding probe failed, ignoring: ${err?.message}`);
2026
+ }
2027
+ }
2028
+ }
2029
+ catch (err) {
2030
+ // Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
2031
+ console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
2032
+ }
2033
+ // Council routing gate (model-signal, no hardcoded depth→council table).
2034
+ //
2035
+ // History: /ideal used to force Council UNCONDITIONALLY because the cheap
2036
+ // classify was too noisy to gate on — the SAME architectural prompt scored
2037
+ // `heavy` on one run and `standard` the next, so routing flip-flopped. That
2038
+ // noise was root-caused (2026-07-15, harness-measured): the session model
2039
+ // could be an agentic model (e.g. grok-composer) that ignores the terse
2040
+ // 8-word classify contract and emits task-planning prose → null → fail-open
2041
+ // "standard"; and stream errors were swallowed into the same null. Both are
2042
+ // now fixed in llm-classify.ts (cross-provider route to a keyed instruction-
2043
+ // follower + No-Silent-Catch error surfacing), so the depth/clarity signal
2044
+ // is reliable enough to gate on.
2045
+ //
2046
+ // Gate: only a genuinely TRIVIAL /ideal — the model says depth=quick AND
2047
+ // not-underspecified — skips Council and takes the product-loop hot-path
2048
+ // (single sprint, no debate; index.ts complexity==="low" branch). Anything
2049
+ // else (standard, heavy, or any underspecified request) still runs the full
2050
+ // Council/loop-driver pipeline: scan source → adaptive interview → leader-
2051
+ // tier assessment + debate AFTER scope is understood. When the model is
2052
+ // uncertain (null depth → "medium") it routes INTO Council, the arbiter —
2053
+ // never a hardcoded default. Explicit `--maintain` still opts out entirely.
2054
+ complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
2055
+ sufficiencyMissing = undefined;
2056
+ if (payload.flags.mode !== "maintain") {
2057
+ const trivial = depth === "quick" && needsClarification !== true;
2058
+ if (!trivial)
2059
+ routeForceCouncil = true;
2060
+ }
1578
2061
  }
1579
2062
  const gen = runProductLoop({
1580
2063
  subcommand: payload.subcommand,
@@ -1589,16 +2072,18 @@ export class Agent {
1589
2072
  doneThreshold: payload.flags.doneThreshold,
1590
2073
  budgetTokens: payload.flags.budgetTokens,
1591
2074
  stack: payload.flags.stack,
1592
- forceCouncil: payload.flags.forceCouncil,
2075
+ forceCouncil: routeForceCouncil,
1593
2076
  },
1594
2077
  respondToQuestion: this.councilManager.createQuestionResponder(),
1595
2078
  respondToPreflight: this.councilManager.createPreflightResponder(),
1596
2079
  cwd: this.bash.getCwd(),
1597
2080
  processMessageFn,
2081
+ runIsolatedTask,
1598
2082
  // Mode C — wire verify-recipe detector so runProductLoop auto-detect can probe cwd.
1599
2083
  detectVerifyRecipe: () => this.detectVerifyRecipe(),
1600
2084
  skipPriorContext: payload.flags.noPriorContext === true,
1601
2085
  complexity,
2086
+ needsClarification,
1602
2087
  sufficiencyMissing,
1603
2088
  // Mode C explicit override + gh pr create opt-in (see .planning/MAINTAIN-MODE.md).
1604
2089
  mode: payload.flags.mode,
@@ -2064,9 +2549,11 @@ export class Agent {
2064
2549
  setLastProviderOptionsShape: (shape) => {
2065
2550
  self._lastProviderOptionsShape = shape;
2066
2551
  },
2552
+ getCompactionStats: () => self.getCompactionStats(),
2067
2553
  getBatchClientOptions: (signal) => self.getBatchClientOptions(signal),
2068
2554
  getCompactionSettings: (cw) => self.getCompactionSettings(cw),
2069
2555
  compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
2556
+ extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
2070
2557
  postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
2071
2558
  createTools: (bash, provider, mode, opts) => createTools(bash, provider, mode, opts),
2072
2559
  runTask: (request, signal) => self.runTask(request, signal),
@@ -2175,7 +2662,16 @@ export class Agent {
2175
2662
  else {
2176
2663
  try {
2177
2664
  const { classifySubSessionAction } = await import("../pil/llm-classify.js");
2178
- const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage);
2665
+ // Feed the recent-conversation digest so the router can actually honour
2666
+ // its "decide based on the conversation history" contract — otherwise a
2667
+ // continuation ("ok làm phần đó đi") is judged in isolation and can be
2668
+ // mis-routed as a fresh/unrelated task. Passing contextInfo also turns
2669
+ // on the session-size metadata block the ROTATE_SESSION rule relies on.
2670
+ const routeResult = await classifySubSessionAction(this.modelId, userMessage, {
2671
+ currentChars,
2672
+ threshold,
2673
+ recentTurns: this._buildRecentTurnsSummary(),
2674
+ });
2179
2675
  if (routeResult) {
2180
2676
  routeAction = routeResult.action;
2181
2677
  logger.info("orchestrator", "Routing action selected for user message", {
@@ -2189,6 +2685,18 @@ export class Agent {
2189
2685
  logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
2190
2686
  }
2191
2687
  }
2688
+ // Reactive escalation — override a DIRECT_ANSWER route (the router's blind
2689
+ // spot on read-heavy analysis, and its silent-degrade to DIRECT on classify
2690
+ // failure) when the PREVIOUS turn's observed tool load proves the session is
2691
+ // doing heavy multi-tool work. Deterministic, based on real execution, not a
2692
+ // prompt guess. Only rescues DIRECT_ANSWER — never hijacks a deliberate
2693
+ // ROTATE_SESSION. See reactive-delegation.ts.
2694
+ if (routeAction === "DIRECT_ANSWER" && shouldReactivelyEscalate(this._lastTurnToolChars)) {
2695
+ logger.info("orchestrator", "Reactive escalation to sub-session (prior turn tool-heavy)", {
2696
+ prevTurnToolChars: this._lastTurnToolChars,
2697
+ });
2698
+ routeAction = "SPAWN_SUB_SESSION";
2699
+ }
2192
2700
  const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
2193
2701
  if (shouldRotate && this.session && this.sessionStore) {
2194
2702
  logger.info("orchestrator", "Triggering silent session rotation", {
@@ -2203,10 +2711,10 @@ export class Agent {
2203
2711
  const { deliberateCompact } = await import("../flow/compaction/index.js");
2204
2712
  const { getDatabase } = await import("../storage/db.js");
2205
2713
  const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
2206
- const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
2714
+ const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.modelId);
2207
2715
  const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2208
2716
  const db = getDatabase();
2209
- db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2717
+ this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
2210
2718
  const summaryMessage = createCompactionSummaryMessage(cr.summary);
2211
2719
  const nextSeq = getNextMessageSequence(newSession.id);
2212
2720
  appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
@@ -2229,10 +2737,10 @@ export class Agent {
2229
2737
  let parentSessionId = null;
2230
2738
  let subSessionId = null;
2231
2739
  if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
2232
- yield { type: "content", content: "\n⋯ Đang khởi tạo sub-session ngầm để xử lý tác vụ...\n" };
2740
+ yield { type: "toast", toastLevel: "info", content: "Đang khởi tạo sub-session ngầm để xử lý tác vụ..." };
2233
2741
  parentSessionId = this.session.id;
2234
2742
  try {
2235
- const { loadLatestCompaction, getNextMessageSequence, appendCompaction, loadTranscriptState } = await import("../storage/transcript.js");
2743
+ const { loadLatestCompaction, getNextMessageSequence, appendCompaction } = await import("../storage/transcript.js");
2236
2744
  const { getDatabase } = await import("../storage/db.js");
2237
2745
  const db = getDatabase();
2238
2746
  // Check if there is already an active child session for this parent session
@@ -2259,9 +2767,13 @@ export class Agent {
2259
2767
  }
2260
2768
  }
2261
2769
  if (shouldResume && subSessionId) {
2262
- yield { type: "content", content: "\n⋯ Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...\n" };
2770
+ yield {
2771
+ type: "toast",
2772
+ toastLevel: "info",
2773
+ content: "Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...",
2774
+ };
2263
2775
  this.session = this.sessionStore.getRequiredSession(subSessionId);
2264
- const childState = loadTranscriptState(subSessionId);
2776
+ const childState = loadSessionChainTranscriptState(subSessionId);
2265
2777
  this.messages = childState.messages;
2266
2778
  this.messageSeqs = childState.seqs;
2267
2779
  this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
@@ -2274,21 +2786,26 @@ export class Agent {
2274
2786
  else {
2275
2787
  const latest = loadLatestCompaction(parentSessionId);
2276
2788
  const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2277
- db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
2789
+ this.sessionStore.linkChild(newSession.id, parentSessionId, "subagent");
2278
2790
  subSessionId = newSession.id;
2279
- let seedMessages = [];
2280
- let seedSeqs = [];
2791
+ // Seed the child with the parent's CURRENT working set — after a
2792
+ // compaction that is already [summary, ...kept raw tail] (see the
2793
+ // compactForContext assignment). Previously the compaction branch
2794
+ // reseeded ONLY [summary], discarding the kept raw tail — so a forked
2795
+ // council/debate saw a bare generic summary instead of the actual
2796
+ // recent discussion the user pointed at ("debate THESE parts"), and
2797
+ // drifted off-topic. Keep the tail; just guarantee the summary is
2798
+ // present and persist the compaction row for the child's continuity.
2799
+ const seedMessages = [...this.messages];
2800
+ const seedSeqs = [...this.messageSeqs];
2281
2801
  if (latest?.summary) {
2282
- const summaryMsg = createCompactionSummaryMessage(latest.summary);
2283
- seedMessages = [summaryMsg];
2284
- seedSeqs = [null];
2802
+ if (!isCompactionSummaryMessage(seedMessages[0])) {
2803
+ seedMessages.unshift(createCompactionSummaryMessage(latest.summary));
2804
+ seedSeqs.unshift(null);
2805
+ }
2285
2806
  const nextSeq = getNextMessageSequence(subSessionId);
2286
2807
  appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
2287
2808
  }
2288
- else {
2289
- seedMessages = [...this.messages];
2290
- seedSeqs = [...this.messageSeqs];
2291
- }
2292
2809
  // Add sub-session overlay system message
2293
2810
  const overlayMessage = {
2294
2811
  role: "system",
@@ -2322,6 +2839,22 @@ export class Agent {
2322
2839
  const autoCommitOn = isAutoCommitEnabled();
2323
2840
  const cwd = this.bash.getCwd();
2324
2841
  const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
2842
+ // Top-level turn watchdog. The per-chunk stall watchdog only covers
2843
+ // streamText byte flow; it does NOT cover a turn generator that keeps the
2844
+ // socket open but never RETURNS after the model is done. Observed live:
2845
+ // xai/grok-composer-2.5-fast fires onFinish/llm-done (finishReason=stop) but
2846
+ // its stream generator never terminates, so processor.run() hangs, the UI's
2847
+ // `for await` never ends, finalizeActiveTurn never runs, and the TUI stays
2848
+ // frozen in the "processing" state (partial/raw markdown) until the next
2849
+ // message forces a new run. Mirrors the council-continuation guard (idle +
2850
+ // hard total). On fire we abort the turn and emit a terminal `done` so the
2851
+ // consumer finalizes cleanly instead of hanging forever. idleMs resets on
2852
+ // every yielded chunk, so long legitimate tool calls are safe; totalMs is a
2853
+ // hard ceiling (0 = disabled by default, since a big multi-tool turn is not a
2854
+ // hang). Env-overridable.
2855
+ const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
2856
+ const turnIdleMs = Number(process.env.MUONROI_TURN_IDLE_MS ?? 120_000);
2857
+ const turnTotalMs = Number(process.env.MUONROI_TURN_TOTAL_MS ?? 0);
2325
2858
  try {
2326
2859
  let attempts = 0;
2327
2860
  const maxAttempts = 3;
@@ -2330,7 +2863,30 @@ export class Agent {
2330
2863
  while (attempts < maxAttempts) {
2331
2864
  try {
2332
2865
  attempts++;
2333
- yield* processor.run(userMessage, observer, images);
2866
+ try {
2867
+ yield* withTurnWatchdog(processor.run(userMessage, observer, images), {
2868
+ idleMs: turnIdleMs,
2869
+ totalMs: turnTotalMs,
2870
+ label: "assistant turn",
2871
+ shouldSuppressFire: isInteractivePaused,
2872
+ });
2873
+ }
2874
+ catch (stallErr) {
2875
+ // A hung turn is NOT a transient error — retrying it (below) would
2876
+ // just hang again. Abort, surface a toast, and terminate the turn.
2877
+ if (stallErr instanceof TurnStallError) {
2878
+ logger.warn("orchestrator", "Top-level turn watchdog fired — finalizing turn", {
2879
+ kind: stallErr.kind,
2880
+ message: stallErr.message,
2881
+ });
2882
+ this.abortController?.abort(new DOMException(stallErr.message, "TimeoutError"));
2883
+ yield { type: "toast", toastLevel: "warn", content: `Turn ended by watchdog: ${stallErr.message}` };
2884
+ yield { type: "done" };
2885
+ }
2886
+ else {
2887
+ throw stallErr;
2888
+ }
2889
+ }
2334
2890
  break;
2335
2891
  }
2336
2892
  catch (err) {
@@ -2516,6 +3072,42 @@ export class Agent {
2516
3072
  self._compactedThisTurn = v;
2517
3073
  },
2518
3074
  getCompactedThisTurn: () => self._compactedThisTurn,
3075
+ getCompactionStats: () => self.getCompactionStats(),
3076
+ reportTurnToolLoad: (chars) => {
3077
+ self._lastTurnToolChars = Number.isFinite(chars) && chars > 0 ? chars : 0;
3078
+ self._turnLoadOrdinal += 1;
3079
+ // Cold-first-turn instrument: record every turn whose load crosses the
3080
+ // reactive threshold, tagged with its ordinal. A later query counts what
3081
+ // fraction of threshold-crossing turns are ordinal 1 (the un-isolatable
3082
+ // cold turn) — the evidence that gates building an in-turn checkpoint.
3083
+ try {
3084
+ if (self.session && self._lastTurnToolChars >= getReactiveDelegationThresholdChars()) {
3085
+ // isolated = the heavy turn ran in a forked child (router SPAWN,
3086
+ // reactive escalation, or rotation), so the parent never held it.
3087
+ // The cold-first-turn HOLE is specifically coldFirstTurn && !isolated
3088
+ // — a turn-1 blow-up that landed in the parent because neither the
3089
+ // router nor reactive escalation caught it. Recording `isolated`
3090
+ // stops the metric from over-counting router-isolated heavy turns.
3091
+ const kind = self.sessionStore?.getSessionKind(self.session.id) ?? "conversation";
3092
+ const isolated = kind !== "conversation";
3093
+ logInteraction(self.session.id, "turn_tool_load", {
3094
+ data: {
3095
+ chars: self._lastTurnToolChars,
3096
+ ordinal: self._turnLoadOrdinal,
3097
+ coldFirstTurn: self._turnLoadOrdinal === 1,
3098
+ isolated,
3099
+ kind,
3100
+ threshold: getReactiveDelegationThresholdChars(),
3101
+ },
3102
+ });
3103
+ }
3104
+ }
3105
+ catch (err) {
3106
+ logger.debug("orchestrator", "turn_tool_load telemetry failed (best-effort)", {
3107
+ error: err?.message,
3108
+ });
3109
+ }
3110
+ },
2519
3111
  setTurnUserGoalExcerpt: (v) => {
2520
3112
  self._turnUserGoalExcerpt = v;
2521
3113
  },
@@ -2555,14 +3147,13 @@ export class Agent {
2555
3147
  `${serializedParent}\n\n` +
2556
3148
  `Provide clear, actionable guidance to resolve the child's query.`;
2557
3149
  const { generateText } = await import("ai");
2558
- const provider = self.requireProvider();
2559
3150
  const modelId = self.modelId;
2560
- const runtime = resolveModelRuntime(provider, modelId);
3151
+ const runtime = resolveModelRuntime(modelId);
2561
3152
  const result = await generateText({
2562
3153
  model: runtime.model,
2563
3154
  system: systemPrompt,
2564
3155
  prompt: `Child Sub-session is stuck. Question:\n${question}`,
2565
- temperature: 0.2,
3156
+ ...resolveTemperatureParam(runtime, 0.2),
2566
3157
  ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
2567
3158
  });
2568
3159
  return result.text;
@@ -2575,6 +3166,7 @@ export class Agent {
2575
3166
  countFilesTouched: () => self._countFilesTouched(),
2576
3167
  getCompactionSettings: (cw) => self.getCompactionSettings(cw),
2577
3168
  compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
3169
+ extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
2578
3170
  postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
2579
3171
  runTask: (request, signal) => self.runTask(request, signal),
2580
3172
  runDelegation: (request, signal) => self.runDelegation(request, signal),
@@ -2613,6 +3205,25 @@ export class Agent {
2613
3205
  return { action: "block" };
2614
3206
  }
2615
3207
  },
3208
+ askUser: async (info) => {
3209
+ const h = self._askUserHandler;
3210
+ if (!h) {
3211
+ logger.warn("orchestrator", "askUser called but no handler registered — returning dismissed sentinel");
3212
+ return ASK_USER_DISMISSED;
3213
+ }
3214
+ // Hold both watchdogs open while the human answers (see interactive-pause.ts).
3215
+ beginInteractivePause();
3216
+ try {
3217
+ return await h(info);
3218
+ }
3219
+ catch (err) {
3220
+ logger.error("orchestrator", "askUser crashed", { error: err });
3221
+ return ASK_USER_DISMISSED;
3222
+ }
3223
+ finally {
3224
+ endInteractivePause();
3225
+ }
3226
+ },
2616
3227
  runCouncilV2: (msg, opts) => self.runCouncilV2(msg, opts),
2617
3228
  processMessage: (msg, obs, imgs) => self.processMessage(msg, obs, imgs),
2618
3229
  processMessageBatchTurn: (args) => self.processMessageBatchTurn(args),
@@ -2633,20 +3244,31 @@ export class Agent {
2633
3244
  const recent = this.messages.slice(-6);
2634
3245
  const parts = [];
2635
3246
  for (const msg of recent) {
2636
- if (msg.role !== "user" && msg.role !== "assistant")
3247
+ if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool")
2637
3248
  continue;
2638
- const text = typeof msg.content === "string"
2639
- ? msg.content
2640
- : Array.isArray(msg.content)
2641
- ? msg.content
2642
- .filter((p) => p.type === "text")
2643
- .map((p) => p.text ?? "")
2644
- .join("")
2645
- : "";
3249
+ let text = "";
3250
+ if (msg.role === "tool") {
3251
+ text = Array.isArray(msg.content)
3252
+ ? msg.content.map((p) => (typeof p.result === "string" ? p.result : JSON.stringify(p.result))).join(" ")
3253
+ : typeof msg.content === "string"
3254
+ ? msg.content
3255
+ : JSON.stringify(msg.content);
3256
+ }
3257
+ else {
3258
+ text =
3259
+ typeof msg.content === "string"
3260
+ ? msg.content
3261
+ : Array.isArray(msg.content)
3262
+ ? msg.content
3263
+ .filter((p) => p.type === "text")
3264
+ .map((p) => p.text ?? "")
3265
+ .join("")
3266
+ : "";
3267
+ }
2646
3268
  if (!text)
2647
3269
  continue;
2648
- const snippet = text.length > 80 ? `${text.slice(0, 77)}...` : text;
2649
- parts.push(`[${msg.role}]: ${snippet}`);
3270
+ const snippet = text.length > 300 ? `${text.slice(0, 297)}...` : text;
3271
+ parts.push(`[${msg.role}]: ${snippet.replace(/\n/g, " ")}`);
2650
3272
  }
2651
3273
  return parts.length > 0 ? parts.join(" | ") : null;
2652
3274
  }
@@ -2716,48 +3338,129 @@ export class Agent {
2716
3338
  if (this._oauthInitDone)
2717
3339
  return;
2718
3340
  this._oauthInitDone = true;
2719
- // Only upgrade when there is no explicit API key — OAuth is an alternative
2720
- // auth path, not an override when the user deliberately passed a key.
2721
- // The boot wizard in src/index.ts uses the literal "oauth" as a sentinel
2722
- // to signal "no API key but OAuth tokens exist", so treat that as "no
2723
- // key" here.
2724
- if (this.apiKey && this.apiKey !== "oauth")
2725
- return;
3341
+ // Re-resolve auth for the CURRENT provider. Runs when either:
3342
+ // (a) no explicit API key / the "oauth" sentinel is held — OAuth may
3343
+ // apply (the boot wizard in src/index.ts uses the literal "oauth" to
3344
+ // mean "no key but OAuth tokens exist"); OR
3345
+ // (b) setModel deferred provider construction on a provider switch
3346
+ // (this.provider === null) the PREVIOUS provider's key was cleared
3347
+ // and must be re-resolved from the NEW provider's own credentials.
3348
+ const providerDeferred = this.provider === null;
3349
+ const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
2726
3350
  try {
2727
- const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
3351
+ const { listOAuthProviderIds, getOAuthProviderConfig } = await import("../providers/auth/registry.js");
2728
3352
  const ids = await listOAuthProviderIds();
2729
- if (!ids.includes(this.providerId))
3353
+ const oauthCapable = ids.includes(this.providerId);
3354
+ // One auth mode per provider (OAuth XOR API key): if this OAuth-capable
3355
+ // provider has a live token, OAuth wins OUTRIGHT — even over a real held
3356
+ // API key. This is the shadowing-bug fix: a stale sk-proj key must not
3357
+ // beat a valid ChatGPT OAuth login. Check token presence BEFORE the
3358
+ // early-out so a held key can no longer suppress OAuth.
3359
+ let hasOAuthTokens = false;
3360
+ if (oauthCapable) {
3361
+ try {
3362
+ const cfg = await getOAuthProviderConfig(this.providerId);
3363
+ const tokens = cfg ? await cfg.loadTokensWithRefresh().catch(() => null) : null;
3364
+ hasOAuthTokens = !!tokens?.accessToken;
3365
+ }
3366
+ catch {
3367
+ hasOAuthTokens = false;
3368
+ }
3369
+ }
3370
+ // Nothing to do only when: no OAuth token, a real key is held, and the
3371
+ // provider was not deferred by a model switch.
3372
+ if (!hasOAuthTokens && !providerDeferred && !keyIsSentinelOrEmpty)
2730
3373
  return;
2731
- const effectiveBaseURL = this.baseURL &&
2732
- this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
2733
- ? this.baseURL
2734
- : undefined;
2735
- const result = await createProviderFactoryAsync(this.providerId, {
2736
- baseURL: effectiveBaseURL ?? undefined,
2737
- });
2738
- this.provider = result.factory;
3374
+ // OAuth path: inject subscription tokens as Bearer headers. Taken when a
3375
+ // token exists (override a held key), OR when no real key is held for an
3376
+ // OAuth-capable provider.
3377
+ if (oauthCapable && (hasOAuthTokens || keyIsSentinelOrEmpty)) {
3378
+ const effectiveBaseURL = this.baseURL &&
3379
+ this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
3380
+ ? this.baseURL
3381
+ : undefined;
3382
+ const result = await createProviderFactoryAsync(this.providerId, {
3383
+ baseURL: effectiveBaseURL ?? undefined,
3384
+ });
3385
+ this.apiKey = "oauth";
3386
+ this.provider = result.factory;
3387
+ return;
3388
+ }
3389
+ // API-key path: resolve THIS provider's own stored key. Reached when the
3390
+ // provider isn't OAuth-capable, or when a provider switch deferred
3391
+ // construction (we must swap in the NEW provider's key — never reuse the
3392
+ // previous provider's, which 401'd on every TUI provider switch). If no
3393
+ // key exists for a deferred switch, leave the provider null so
3394
+ // requireProvider() surfaces a clear "API key required".
3395
+ try {
3396
+ const key = await loadKeyForProvider(this.providerId);
3397
+ if (key) {
3398
+ this.apiKey = key;
3399
+ const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
3400
+ if (staleBaseURL)
3401
+ this.baseURL = null;
3402
+ this.provider = createProvider(this.providerId, key, this.baseURL ?? undefined);
3403
+ }
3404
+ else if (providerDeferred) {
3405
+ this.provider = null;
3406
+ }
3407
+ }
3408
+ catch (err) {
3409
+ // ProviderKeyMissingError (or keychain failure) — no usable key. For a
3410
+ // deferred provider switch, null the provider so the next turn fails
3411
+ // loudly ("API key required") instead of 401-ing on a wrong/sentinel
3412
+ // key. For a non-deferred empty-key case, leave the existing provider
3413
+ // untouched (fail-open, as before).
3414
+ if (providerDeferred) {
3415
+ console.error(`[orchestrator] no API key for provider '${this.providerId}' after model switch: ${err instanceof Error ? err.message : String(err)}`);
3416
+ this.provider = null;
3417
+ }
3418
+ }
2739
3419
  }
2740
3420
  catch {
2741
- // Fail-open provider remains null; requireProvider() will surface the error
3421
+ // Registry unavailable — fail-open; the existing provider (if any) is
3422
+ // left untouched and requireProvider() will surface any error.
2742
3423
  }
2743
3424
  }
2744
3425
  async detectVerifyRecipe(settings, abortSignal) {
3426
+ const cwd = this.bash.getCwd();
3427
+ const effectiveSettings = settings ?? this.bash.getSandboxSettings();
3428
+ // Primary: LLM verify-detect turn — a codebase-aware, richer recipe.
3429
+ let llmRecipe = null;
2745
3430
  try {
2746
3431
  const result = await this.runTaskRequest({
2747
3432
  agent: "verify-detect",
2748
3433
  description: "Detect verification recipe",
2749
- prompt: buildVerifyDetectPrompt(this.bash.getCwd(), settings ?? this.bash.getSandboxSettings()),
3434
+ prompt: buildVerifyDetectPrompt(cwd, effectiveSettings),
2750
3435
  }, undefined, abortSignal);
2751
- if (!result.success || !result.output)
2752
- return null;
2753
- const maybeJson = extractJsonObject(result.output);
2754
- if (!maybeJson)
2755
- return null;
2756
- return normalizeVerifyRecipe(JSON.parse(maybeJson));
3436
+ if (result.success && result.output) {
3437
+ const maybeJson = extractJsonObject(result.output);
3438
+ if (maybeJson)
3439
+ llmRecipe = normalizeVerifyRecipe(JSON.parse(maybeJson));
3440
+ }
2757
3441
  }
2758
- catch {
2759
- return null;
3442
+ catch (err) {
3443
+ console.error(`[orchestrator] verify-detect turn failed: ${err instanceof Error ? err.message : String(err)}`);
3444
+ }
3445
+ if (llmRecipe)
3446
+ return llmRecipe;
3447
+ // Deterministic fallback: an existing repo with a recognizable manifest
3448
+ // (package.json test script, *.sln, pyproject, …) yields a reliable recipe
3449
+ // WITHOUT an LLM. This stops CB-3 from false-halting an in-place /ideal
3450
+ // migration with the "Recovery options" card purely because the flaky
3451
+ // verify-detect model returned no parseable JSON (root cause of the
3452
+ // existing-repo implementation-reachability halt). Only trusted when the
3453
+ // profiler recognizes the ecosystem AND found real test commands.
3454
+ try {
3455
+ const profile = inferVerifyProjectProfile(cwd, effectiveSettings);
3456
+ if (shouldTrustDeterministicRecipe(profile.recipe)) {
3457
+ return profile.recipe;
3458
+ }
2760
3459
  }
3460
+ catch (err) {
3461
+ console.error(`[orchestrator] deterministic verify-recipe fallback failed: ${err instanceof Error ? err.message : String(err)}`);
3462
+ }
3463
+ return null;
2761
3464
  }
2762
3465
  async runVerify(onProgress, abortSignal) {
2763
3466
  this.abortController = new AbortController();