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
@@ -48,43 +48,52 @@
48
48
  // - A4 (tool_call write-ahead) — persistToolCallWriteAhead
49
49
  // - A5 (message_seq write-ahead) — persistMessageWriteAhead
50
50
  // - O1 (providerOptions shape forensics) — extractProviderOptionsShape
51
- // - siliconflow reasoning-strip — turnCaps.sanitizeHistory
51
+ // - reasoning-strip (provider quirk) — turnCaps.sanitizeHistory
52
52
  import { generateText, stepCountIs, streamText } from "ai";
53
+ import { getEffectiveCouncilRoleCount } from "../council/leader.js";
53
54
  import { recordArtifact } from "../ee/artifact-cache.js";
54
55
  import { routeFeedback } from "../ee/bridge.js";
55
56
  import { getDefaultEEClient } from "../ee/intercept.js";
57
+ import { isGsdHardGateEnabled } from "../gsd/flags.js";
58
+ import { evaluateMutationGate } from "../gsd/mutation-gate.js";
56
59
  import { acquireMcpTools } from "../mcp/client-pool.js";
57
60
  import { dropRedundantFsMcpTools, filterMcpServersByMessage } from "../mcp/smart-filter.js";
58
- import { getModelInfo } from "../models/registry.js";
61
+ import { getModelInfo, isReasoningModel } from "../models/registry.js";
59
62
  import { cheapModelShellLine, injectCheapModelPlaybook, injectCheapModelShellDirective, shouldInjectCheapModelPlaybook, } from "../pil/cheap-model-playbook.js";
60
63
  import { injectCheapModelWorkbook, shouldInjectCheapModelWorkbook } from "../pil/cheap-model-workbooks.js";
61
64
  import { getResponseTaskType, isResponseTool, normalizeStructuredResponseTaskType, shouldHaltOnResponseTool, } from "../pil/index.js";
62
- import { isMetaAnalysisPrompt } from "../pil/layer6-output.js";
65
+ import { isMetaAnalysisPrompt, isSprintPlanExecution } from "../pil/layer6-output.js";
63
66
  import { taskTypeToMaxTokens, taskTypeToReasoningEffort, taskTypeToTier } from "../pil/task-tier-map.js";
64
67
  import { mentionsEcosystemScope } from "../playbook/directives.js";
65
68
  import { getProviderCapabilities } from "../providers/capabilities.js";
66
69
  import { bridgeMcpToolResult, listCachedImages, scrubImagePayloadsInMessages, } from "../providers/mcp-vision-bridge.js";
67
70
  import { captureToolSchemas } from "../providers/patch-zod-schema.js";
68
- import { buildTurnProviderOptions, requireRuntimeProvider, shouldDropParam, } from "../providers/runtime.js";
71
+ import { buildTurnProviderOptions, requireRuntimeProvider, resolveTemperatureParam, shouldDropParam, } from "../providers/runtime.js";
69
72
  import { wireDebug } from "../providers/wire-debug.js";
70
73
  import { reportRouteOutcome } from "../router/decide.js";
74
+ import { statusBarStore } from "../state/status-bar-store.js";
75
+ import { recordTurnTrace } from "../state/turn-trace.js";
71
76
  import { getNextMessageSequence, logInteraction, markMessageErrored, markToolCallErrored, persistToolCallWriteAhead, } from "../storage/index.js";
72
77
  import { persistSessionExperience } from "../storage/session-experience-store.js";
73
78
  import { createBuiltinTools } from "../tools/registry.js";
74
79
  import { snapshotFromTodoWriteArgs } from "../tools/todo-write-snapshot.js";
75
80
  import { visionToolsNeeded } from "../tools/vision-gate.js";
76
- import { recordTurnTrace } from "../ui/slash/debug.js";
77
- import { statusBarStore } from "../ui/status-bar/store.js";
78
81
  import { appendDecisionLog } from "../usage/decision-log.js";
82
+ import { setLoopBreadcrumb } from "../utils/event-loop-monitor.js";
79
83
  import { logger } from "../utils/logger.js";
80
84
  import { openUrl } from "../utils/open-url.js";
81
85
  import { appendAudit, toolNeedsApproval } from "../utils/permission-mode.js";
82
- import { getAutoCouncilConfidence, getAutoCouncilMinRoles, getProviderStallRetries, getProviderStallTimeoutMs, getRoleModels, getSteerInjectionEnabled, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, isAutoCouncilEnabled, loadMcpServers, } from "../utils/settings.js";
86
+ import { getAutoCouncilConfidence, getAutoCouncilMinRoles, getProviderProgressTimeoutMs, getProviderStallRetries, getProviderStallTimeoutMs, getSteerInjectionEnabled, getTopLevelCompactHysteresis, getTopLevelCompactKeepLast, getTopLevelCompactTailBudgetChars, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, isAutoCouncilClarifyEnabled, isAutoCouncilEnabled, loadMcpServers, } from "../utils/settings.js";
87
+ import { isAutoCouncilSkipReasoning } from "../utils/settings.js";
83
88
  import { resolveShell } from "../utils/shell.js";
89
+ import { foldDynamicTailIntoUserMessage, splitFrontAndDynamicTail } from "./cache-prefix.js";
90
+ import { consumeProactiveCompact } from "./compact-request.js";
84
91
  import { relaxCompactionSettings } from "./compaction.js";
92
+ import { consumeCouncilConvene, hasPendingCouncilConvene, peekCouncilConveneToolCallId } from "./council-request.js";
85
93
  import { wrapToolSetWithDedup } from "./cross-turn-dedup.js";
86
94
  import { humanizeApiError, isAuthenticationError, isContextLimitError, summarizeApiErrorForLog } from "./error-utils.js";
87
95
  import { buildGroundingFootnote, findUnverifiedClaims } from "./grounding-check.js";
96
+ import { isInteractivePaused } from "./interactive-pause.js";
88
97
  import { buildInterruptedTurnNote } from "./interrupted-turn.js";
89
98
  import { stableCallId } from "./pending-calls.js";
90
99
  import { buildMcpCapabilityBlock, MAX_LLM_CALLS_PER_TURN, } from "./prompts.js";
@@ -94,6 +103,7 @@ import { containsEncryptedReasoning, sanitizeModelMessages } from "./reasoning.j
94
103
  import { repairToolCallHook } from "./repair-tool-call.js";
95
104
  import { buildRepetitionReminder, recordAssistantBurst, shouldInjectRepetitionReminder, } from "./repetition-detector.js";
96
105
  import { classifyStreamError } from "./retry-classifier.js";
106
+ import { parseSafetyBlock, shouldAutoAllowYolo } from "./safety-intercept.js";
97
107
  import { forcedFinalize, incSessionStep, } from "./scope-ceiling.js";
98
108
  import { attachReminderToMessages, buildCheckpointReminder, buildScopeReminder, cadenceForSize, shouldInjectCeilingCrossing, shouldInjectReminder, shouldInjectSoftWarn, shouldPreWarnCompaction, } from "./scope-reminder.js";
99
109
  import { formatElisionManifest, getSessionExperienceCounts, recordCompaction, recordElision, } from "./session-experience.js";
@@ -101,10 +111,35 @@ import { attemptStallRescue, pushStallToolResult } from "./stall-rescue.js";
101
111
  import { createStallWatchdog, STALL_ERROR_MESSAGE, shouldContinueAfterMidLoopStall, shouldRepromptStall, stallRepromptBackoffMs, } from "./stall-watchdog.js";
102
112
  import { planSteerInjection } from "./steer-inbox.js";
103
113
  import { wrapToolSetWithCap } from "./sub-agent-cap.js";
104
- import { applyAnthropicPromptCaching, compactSubAgentMessages, cumulativeMessageChars } from "./subagent-compactor.js";
114
+ import { applyAnthropicPromptCaching, applyCompactionHysteresis, compactSubAgentMessages, cumulativeMessageChars, initCompactionHysteresisState, } from "./subagent-compactor.js";
105
115
  import { detectTextEmittedToolCall, parseDsmlToolCalls } from "./text-tool-call-detector.js";
116
+ import { getToolLimitAutoRecoverCap, shouldAutoRecoverToolLimit } from "./tool-limit-auto-recover.js";
106
117
  import { createToolLoopCapPredicate } from "./tool-loop-cap.js";
107
118
  import { buildToolRepetitionAbortMessage, recordToolError as recordToolRepetitionError, recordToolSuccess as recordToolRepetitionSuccess, } from "./tool-repetition-detector.js";
119
+ /**
120
+ * Resolve the per-turn `maxOutputTokens` budget.
121
+ *
122
+ * Normally the budget is derived from the PIL-classified `taskType`
123
+ * (`taskTypeToMaxTokens`). But a sprint IMPLEMENTATION turn — the /ideal
124
+ * loop's handoff into the host orchestrator via `processMessageFn`, marked
125
+ * with `SPRINT_EXECUTION_MARKER` — is a KNOWN code-writing task that must not
126
+ * be starved by a noisy classify. Observed live (2026-07-10, gsd-core
127
+ * migration): the impl prompt was classified `analyze`/default → capped at
128
+ * 4_096 output → the model spent the whole budget narrating its plan, hit
129
+ * `finishReason:"length"` mid-word, produced ZERO code, and the turn wedged.
130
+ *
131
+ * Fix: for a sprint-execution turn, floor the budget at the build/generate
132
+ * tier (12_288) regardless of the classified type. Scoped to the marker only
133
+ * (NOT the broad `isImplementationIntent`) so ordinary refactor/debug turns
134
+ * keep their intentionally tighter L6 budgets.
135
+ */
136
+ export function resolveTurnMaxOutputTokens(pilCtx) {
137
+ const base = taskTypeToMaxTokens(pilCtx.taskType);
138
+ if (isSprintPlanExecution(pilCtx.raw ?? "")) {
139
+ return Math.max(base, taskTypeToMaxTokens("build"));
140
+ }
141
+ return base;
142
+ }
108
143
  /**
109
144
  * F2 — approximate the char cost of the FIXED prompt envelope (system +
110
145
  * tools JSON-Schema) that streamText re-sends on every step. Used to feed
@@ -193,6 +228,38 @@ export function rewriteSafetyApprovedToolResults(messages) {
193
228
  return changed ? { ...m, content: newContent } : m;
194
229
  });
195
230
  }
231
+ /**
232
+ * Replace the `result` value of a single tool-result part (matched by
233
+ * toolCallId) in an AI-SDK message history, in place, preserving the
234
+ * tool-call/tool-result pairing. Used by the convene_council path to splice the
235
+ * council synthesis into the placeholder tool_result the model saw, so on the
236
+ * restarted step the model reads the conclusion as that tool's result. Pure +
237
+ * exported for unit testing. Returns the same array reference untouched when the
238
+ * toolCallId is absent (with a caller-visible `replaced` flag).
239
+ */
240
+ export function spliceConveneToolResult(messages, toolCallId, value) {
241
+ if (!toolCallId)
242
+ return { messages, replaced: false };
243
+ let replaced = false;
244
+ const out = messages.map((m) => {
245
+ if (m.role !== "tool" || !Array.isArray(m.content))
246
+ return m;
247
+ let changed = false;
248
+ const newContent = m.content.map((part) => {
249
+ if (part?.type === "tool-result" && part?.toolCallId === toolCallId) {
250
+ changed = true;
251
+ replaced = true;
252
+ // AI SDK v6 tool-result parts carry the value under `output` (typed) or
253
+ // `result` (legacy). Set both so whichever the provider serializer reads
254
+ // sees the synthesis, and clear any error flag.
255
+ return { ...part, isError: false, output: value, result: value };
256
+ }
257
+ return part;
258
+ });
259
+ return changed ? { ...m, content: newContent } : m;
260
+ });
261
+ return { messages: replaced ? out : messages, replaced };
262
+ }
196
263
  export class SimpleMutex {
197
264
  queue = Promise.resolve();
198
265
  async run(fn) {
@@ -212,9 +279,57 @@ export class SimpleMutex {
212
279
  }
213
280
  }
214
281
  import { stripDsmlMarkup } from "./message-processor.js";
282
+ /** Tools that produce zero side-effects — safe for Q&A-only (direct-answer) mode. */
283
+ function stripWriteTools(tools) {
284
+ const readonly = new Set([
285
+ "read_file",
286
+ "grep",
287
+ "bash_output_get",
288
+ "process_list",
289
+ "delegation_read",
290
+ "delegation_list",
291
+ "ee_query",
292
+ "ee_health",
293
+ "usage_forensics",
294
+ "lsp_query",
295
+ "setup_guide",
296
+ "selfverify_status",
297
+ "selfverify_result",
298
+ "selfverify_list",
299
+ "list_vision_cache",
300
+ "ee_feedback",
301
+ "ee_write",
302
+ ]);
303
+ const result = {};
304
+ for (const [name, tool] of Object.entries(tools)) {
305
+ if (readonly.has(name) || name.startsWith("respond_") || name.startsWith("mcp_")) {
306
+ result[name] = tool;
307
+ }
308
+ }
309
+ return result;
310
+ }
215
311
  export async function* executeToolEngine(args) {
216
312
  let { deps, stepRouterPhase, phase2Runtime, runtime, modelInfo, _debugSteps, _ceilingHit, userMessage, pilCtx, turnModelId, turnProvider, _stepCeiling, userModelMessage, userEnrichedMessage, signal, observer, taskHash, provider, system, routerStore, attemptedOverflowRecovery, patternLoopForceHalt, userWriteAheadSeq, streamRetryCount, MAX_STREAM_RETRIES, subagents, systemParts, toolTurnSystem, playwrightGuidance, _hasResponseTools, _pilResponseTools, patternLoopInjectCount, agentLoopDecisionCount, MAX_AGENT_LOOP_DECISIONS, _naturalCeiling, _ceilingTaskType, _ceilingSize, textToolReSteerCount, MAX_TEXT_TOOL_RESTEER, turnStartMs, _debugOn, _debugTurnId, _pilEnrichmentDeltaSnapshot, isChitchat, } = args;
217
313
  // Put all extracted code here:
314
+ // Auto-recover budget for "cap" (tool-round ceiling) halts: compact
315
+ // the history and keep going instead of stopping and asking the user
316
+ // to /compact. Turn-scoped (not per-stream-attempt) so a stream-error
317
+ // retry or stall reprompt (`continue streamAttempt`) cannot reset the
318
+ // counter and exceed the intended cap of auto-compactions per turn
319
+ // (default 6, env MUONROI_TOOL_LIMIT_AUTO_RECOVER_CAP).
320
+ let toolLimitAutoRecoverCount = 0;
321
+ const TOOL_LIMIT_AUTO_RECOVER_CAP = getToolLimitAutoRecoverCap();
322
+ // Convene-council loop guard. The agent may convene the council at most once
323
+ // per turn — a second convene_council call in the SAME turn does NOT re-run a
324
+ // full (5-10min) council; instead its tool-result is a non-binding suggestion
325
+ // to USE the synthesis already above (respond, or ask_user). Turn-scoped (not
326
+ // per-stream-attempt) so a `continue` restart cannot reset it. Live-caught:
327
+ // the model looped convene→synthesis→convene, burning a second council.
328
+ let conveneRunsThisTurn = 0;
329
+ const COUNCIL_MAX_CONVENES_PER_TURN = (() => {
330
+ const raw = Number.parseInt(process.env.MUONROI_MAX_CONVENES_PER_TURN ?? "", 10);
331
+ return Number.isFinite(raw) && raw >= 1 ? raw : 1;
332
+ })();
218
333
  let stallTriggered = false;
219
334
  // Time-to-first-byte stall RE-PROMPT: some providers (observed:
220
335
  // xai/grok-build-0.1) accept the request then never send the first byte —
@@ -243,6 +358,10 @@ export async function* executeToolEngine(args) {
243
358
  // the turn when exceeded. Prevents the session 526a83cf22df pattern
244
359
  // where 3 user messages burnt 82% of 2.44M tokens in 36 LLM calls.
245
360
  let llmCallsThisTurn = 0;
361
+ // Reactive delegation signal: reference to the top-level cap's live state so
362
+ // this turn's cumulative tool-output load can be reported to the Agent at
363
+ // turn end (drives next-turn sub-session escalation). See reactive-delegation.ts.
364
+ let _topLevelCapState = null;
246
365
  // Live-queue steering: messages the user typed mid-turn are drained at a
247
366
  // prepareStep boundary and accumulated here, then re-appended (deduped) to
248
367
  // the messages returned for each subsequent step. Loop-persistent so they
@@ -269,11 +388,20 @@ export async function* executeToolEngine(args) {
269
388
  // and stay productive. `plan` keeps the old behaviour (architectural
270
389
  // decisions deserve debate regardless of length).
271
390
  const autoCouncilTypes = new Set(["plan", "analyze"]);
272
- const councilRoles = getRoleModels();
273
- const configuredRoleCount = Object.values(councilRoles).filter(Boolean).length;
274
- const heavyTier = pilCtx.complexityTier === "heavy";
391
+ const configuredRoleCount = getEffectiveCouncilRoleCount();
392
+ // Task 8 Step 7: prefer the complexity assessor's own auto-council verdict
393
+ // (pilCtx.gsdAutoCouncil, set at message-processor.ts:685 when the assessor ran)
394
+ // over the raw heavy-tier heuristic below — the assessor already reasoned about
395
+ // depth + task shape, so its verdict is the more intelligent router. Fall back to
396
+ // the heuristic when the assessor didn't run (gsdAutoCouncil undefined).
397
+ const assessorAutoCouncil = pilCtx.gsdAutoCouncil;
398
+ const heavyTier = typeof assessorAutoCouncil === "boolean"
399
+ ? assessorAutoCouncil
400
+ : pilCtx.complexityTier === "heavy";
275
401
  const autoCouncilConfidence = getAutoCouncilConfidence();
276
402
  const autoCouncilMinRoles = getAutoCouncilMinRoles();
403
+ const sessionModelIsReasoning = isReasoningModel(deps.modelId);
404
+ const skipReasoningSetting = isAutoCouncilSkipReasoning();
277
405
  const _complexityFromTrace = pilCtx._intentTrace
278
406
  ?.complexity;
279
407
  const _complexityGatePassed = pilCtx.taskType === "plan" || _complexityFromTrace === undefined || _complexityFromTrace !== "low";
@@ -281,9 +409,13 @@ export async function* executeToolEngine(args) {
281
409
  autoCouncilTypes.has(pilCtx.taskType) &&
282
410
  pilCtx.confidence >= autoCouncilConfidence &&
283
411
  _complexityGatePassed;
412
+ // Skip reasoning-model skip for heavy/complex tasks — they benefit from
413
+ // multi-role diversity even when the session model already does extended thinking.
414
+ const shouldSkipForReasoning = sessionModelIsReasoning && skipReasoningSetting && !heavyTier;
284
415
  const shouldAutoCouncil = !deps.councilManager.isContinuation &&
285
416
  isAutoCouncilEnabled() &&
286
417
  configuredRoleCount >= autoCouncilMinRoles &&
418
+ !shouldSkipForReasoning &&
287
419
  (taskTypeMatch || heavyTier);
288
420
  // Always log the auto-council decision (taken or skipped) with the gate
289
421
  // values that decided it. Lets reports answer "why did this turn cost
@@ -295,6 +427,8 @@ export async function* executeToolEngine(args) {
295
427
  return "feature-disabled";
296
428
  if (configuredRoleCount < autoCouncilMinRoles)
297
429
  return `role-count<${autoCouncilMinRoles} (have ${configuredRoleCount})`;
430
+ if (shouldSkipForReasoning)
431
+ return `reasoning-model=${deps.modelId} (internal self-debate active; skip with MUONROI_AUTOCOUNCIL_SKIP_REASONING=0)`;
298
432
  if (!taskTypeMatch && !heavyTier) {
299
433
  if (!pilCtx.taskType || !autoCouncilTypes.has(pilCtx.taskType))
300
434
  return `taskType=${pilCtx.taskType ?? "null"} not in plan|analyze`;
@@ -322,6 +456,8 @@ export async function* executeToolEngine(args) {
322
456
  autoCouncilConfidence,
323
457
  autoCouncilMinRoles,
324
458
  heavyTier,
459
+ sessionModelIsReasoning,
460
+ skipReasoningSetting,
325
461
  isContinuation: deps.councilManager.isContinuation,
326
462
  },
327
463
  }).catch(() => undefined);
@@ -330,14 +466,40 @@ export async function* executeToolEngine(args) {
330
466
  ? `complexity=heavy${pilCtx.taskType ? ` task=${pilCtx.taskType}` : ""}`
331
467
  : `${pilCtx.taskType} task detected with ${(pilCtx.confidence * 100).toFixed(0)}% confidence`;
332
468
  yield { type: "content", content: `\n[Auto-council triggered: ${reason}]\n` };
333
- yield* deps.runCouncilV2(userMessage, { skipClarification: true, observer, userModelMessage });
469
+ // Pre-debate interview: unless disabled, run the model-designed clarification
470
+ // askcards BEFORE the debate so a broadly-scoped "debate mode" request is
471
+ // chốt-ed first (each card's options carry a recommended default + per-option
472
+ // why — see runClarification/buildClarifyOptions). The clarifier is ROI-gated
473
+ // and yields 0 cards on already-detailed topics, so this stays quiet when the
474
+ // prompt is already specific. Skip only when the user turned it off. The
475
+ // clarifier reuses PIL gray-areas as seed questions (no hardcoded questions),
476
+ // and its models come from pickCouncilTaskModel (no hardcoded model/provider).
477
+ yield* deps.runCouncilV2(userMessage, {
478
+ skipClarification: !isAutoCouncilClarifyEnabled(),
479
+ observer,
480
+ userModelMessage,
481
+ // Suppress the CLI-hardcoded post-debate option card. The follow-up is
482
+ // decided by the agent's own intent via the neutral continuation below,
483
+ // not a fixed CLI menu. (Pre-debate clarification is orthogonal and still
484
+ // runs per skipClarification.)
485
+ convenePath: true,
486
+ });
334
487
  const synthesis = deps.councilManager.lastSynthesis;
488
+ const chosenAction = deps.councilManager.lastPostDebateAction;
335
489
  deps.councilManager.setLastSynthesis(null);
336
- if (synthesis) {
490
+ deps.councilManager.setLastPostDebateAction(null);
491
+ // convenePath suppressed the hardcoded card, so there is no chosenAction to
492
+ // branch on. Hand the synthesis to a normal agent turn with a non-binding
493
+ // nudge and let the agent decide the next step (respond / ask_user /
494
+ // implement). Re-entry is guarded by setContinuation(true) below so
495
+ // shouldAutoCouncil (which checks !isContinuation) can't re-fire into a loop.
496
+ const { buildNeutralPostCouncilContinuation } = await import("../council/index.js");
497
+ const continuationPrompt = synthesis ? buildNeutralPostCouncilContinuation(synthesis) || null : null;
498
+ if (continuationPrompt) {
337
499
  yield { type: "content", content: "\n[Auto-continuing with council recommendations...]\n" };
338
500
  deps.councilManager.setContinuation(true);
339
501
  try {
340
- yield* deps.processMessage(`Council debate completed. Synthesis:\n\n${synthesis}\n\nProceed with the recommended action items.`, observer);
502
+ yield* deps.processMessage(continuationPrompt, observer);
341
503
  }
342
504
  finally {
343
505
  deps.councilManager.setContinuation(false);
@@ -345,6 +507,10 @@ export async function* executeToolEngine(args) {
345
507
  }
346
508
  return;
347
509
  }
510
+ // Skipping auto-council is the normal, expected path for a reasoning model —
511
+ // not an event the user needs narrated on every turn. The decision (and the
512
+ // gate values behind it) is still recorded via autoCouncilSkipReason above,
513
+ // so forensics keep the full story without the transcript noise.
348
514
  if (deps.batchApi) {
349
515
  try {
350
516
  yield* deps.processMessageBatchTurn({
@@ -482,14 +648,58 @@ export async function* executeToolEngine(args) {
482
648
  cachedImageCount: listCachedImages().length,
483
649
  priorTurnHadTools: deps.messages.some((m) => m?.role === "tool"),
484
650
  });
651
+ const depthTier = pilCtx.modelDepthTier ??
652
+ pilCtx.complexityTier ??
653
+ "standard";
485
654
  const baseToolsRaw = createBuiltinTools(deps.bash, deps.mode, {
486
655
  runTask: (request, abortSignal) => deps.runTask(request, combineAbortSignals(signal, abortSignal)),
487
656
  runDelegation: (request, abortSignal) => deps.runDelegation(request, combineAbortSignals(signal, abortSignal)),
488
657
  readDelegation: (id) => deps.readDelegation(id),
489
658
  listDelegations: () => deps.listDelegations(),
490
659
  modelId: turnModelId,
660
+ depthTier,
661
+ sessionId: deps.session?.id,
491
662
  includeVisionTools,
492
663
  consultParentSession: deps.consultParentSession,
664
+ // Register convene_council only when the council is actually usable
665
+ // for this session (enough configured roles) — else the model could
666
+ // call a council that can't convene.
667
+ councilConfigured: configuredRoleCount >= autoCouncilMinRoles,
668
+ askUser: deps.askUser,
669
+ runDebate: async (topic) => {
670
+ // Reset before draining so a generator that throws BEFORE setting
671
+ // synthesis (orchestrator.setLastSynthesis) cannot return a STALE
672
+ // synthesis from a prior council run.
673
+ deps.councilManager.setLastSynthesis(null);
674
+ const gen = deps.runCouncilV2(topic, {
675
+ skipClarification: true,
676
+ userModelMessage: { role: "user", content: `/council ${topic}` },
677
+ // Model-callable debate: the synthesis is returned to the model as
678
+ // the tool result and the model decides the follow-up — so suppress
679
+ // the CLI-hardcoded post-debate card, same as convene_council.
680
+ convenePath: true,
681
+ });
682
+ // Capture a tail of content chunks so an empty-synthesis failure
683
+ // (provider unreachable / sub-phase fail-open / abort — all of which
684
+ // yield a content hint then `return null`) surfaces a real reason in
685
+ // the log instead of a silent "".
686
+ let lastContentHint = "";
687
+ for await (const chunk of gen) {
688
+ const text = chunk?.content;
689
+ if (chunk?.type === "content" && typeof text === "string" && text.trim()) {
690
+ lastContentHint = text.trim().slice(-200);
691
+ }
692
+ }
693
+ const synthesis = deps.councilManager.lastSynthesis ?? "";
694
+ if (!synthesis.trim()) {
695
+ // No-Silent-Catch: the debate produced no synthesis. plan-council
696
+ // will retry then fall back to the perspective path — log why here
697
+ // so the failure is diagnosable remotely.
698
+ console.error(`[tool-engine] plan-review runDebate returned empty synthesis` +
699
+ `${lastContentHint ? ` (last council chunk: ${lastContentHint})` : ""}`);
700
+ }
701
+ return synthesis;
702
+ },
493
703
  });
494
704
  // Top-level cumulative cap state. We accumulate the raw tool set
495
705
  // (base + MCP + PIL response tools) across the assembly below,
@@ -513,16 +723,23 @@ export async function* executeToolEngine(args) {
513
723
  // history) is preserved.
514
724
  const turnCaps = getProviderCapabilities(requireRuntimeProvider(runtime));
515
725
  const _priorTurnHadTools = deps.messages.some((m) => m?.role === "tool");
726
+ // Direct-answer mode: purely informational Q&A (no code changes).
727
+ // Strip write tools (bash, edit_file, write_file) AND skip MCP;
728
+ // only readonly tools remain (read_file, grep, ee_query, etc.).
729
+ const isDirectAnswer = pilCtx.directAnswer === true;
516
730
  let rawToolSet = !turnCaps.supportsClientTools(runtime.modelInfo)
517
731
  ? {}
518
732
  : isChitchat && !_priorTurnHadTools
519
733
  ? {}
520
- : baseToolsRaw;
521
- // MCP skip: chitchat / greeting inputs don't need 7 MCP servers'
734
+ : isDirectAnswer && !_priorTurnHadTools
735
+ ? stripWriteTools(baseToolsRaw)
736
+ : baseToolsRaw;
737
+ // MCP skip: chitchat / direct-answer / greeting inputs don't need 7 MCP servers'
522
738
  // worth of tool schemas (~20K input tokens). PIL Layer 1 already
523
739
  // gates this conservatively (≤10 chars + ≤2 words OR brain "none").
524
740
  if (deps.mode === "agent" &&
525
741
  (!isChitchat || _priorTurnHadTools) &&
742
+ !isDirectAnswer &&
526
743
  turnCaps.supportsClientTools(runtime.modelInfo)) {
527
744
  // Smart MCP filter: drop OPTIONAL MCP servers whose category the
528
745
  // current message gives no signal for. Browser/vision servers
@@ -629,11 +846,14 @@ export async function* executeToolEngine(args) {
629
846
  // Apply the top-level cumulative cap once over the fully-assembled
630
847
  // raw tool set. State is per-turn; each turn gets a fresh budget.
631
848
  const topLevelCap = wrapToolSetWithCap(rawToolSet, {
632
- maxCumulativeChars: getTopLevelToolBudgetChars(deps.maxToolRounds),
849
+ maxCumulativeChars: getTopLevelToolBudgetChars(deps.maxToolRounds, contextWindow),
633
850
  midTierRatio: 0.5,
634
851
  highTierRatio: 0.8,
635
852
  label: "top-level",
636
853
  });
854
+ // Expose the cap state so the reactive-delegation signal can read this
855
+ // turn's cumulative tool load at turn end (see report at success exit).
856
+ _topLevelCapState = topLevelCap.state;
637
857
  // Phase C3: layer cross-turn dedup on top of the top-level cap.
638
858
  const tools = wrapToolSetWithReadBudget(wrapToolSetWithDedup(topLevelCap.tools, deps.crossTurnDedup), deps.readBudget);
639
859
  // Wrap non-read-only tools in a turn-scoped mutex to prevent race conditions during parallel execution.
@@ -654,18 +874,56 @@ export async function* executeToolEngine(args) {
654
874
  "selfverify_result",
655
875
  "selfverify_list",
656
876
  "list_vision_cache",
877
+ "ee_feedback",
878
+ "ee_write",
657
879
  ]);
880
+ /**
881
+ * Tools that perform mutations (writes, edits, side-effects).
882
+ * MUTATION_TOOLS are read from this set to route them through the mutation
883
+ * gate and prevent accidental execution when GSD hard gate is active.
884
+ * Registration: add the tool name string to this set and ensure the tool
885
+ * itself is wired in the tool assembly step.
886
+ */
887
+ const MUTATION_TOOLS = new Set(["lsp_mutation_preview"]);
888
+ // Task 8: native GSD mutation gate. Read once per turn (not per call) since
889
+ // hardGateEnabled/directAnswer don't change mid-turn; the gate itself
890
+ // re-reads STATE.md per call (cheap fs read) so it stays live if the
891
+ // model advances phase/verdict mid-turn via gsd_* tools.
892
+ const gsdHardGateEnabled = isGsdHardGateEnabled();
893
+ const gsdDirectAnswer = pilCtx.directAnswer;
658
894
  for (const name of Object.keys(tools)) {
659
895
  const tool = tools[name];
660
- if (tool &&
661
- typeof tool.execute === "function" &&
662
- !READ_ONLY_TOOLS.has(name) &&
663
- !name.startsWith("respond_")) {
664
- const originalExecute = tool.execute;
665
- tool.execute = async (input, context) => {
666
- return writeMutex.run(() => originalExecute(input, context));
667
- };
668
- }
896
+ if (!tool || typeof tool.execute !== "function")
897
+ continue;
898
+ const originalExecute = tool.execute;
899
+ // Read-only tools skip the mutation gate and the write mutex, but ALL
900
+ // tools get a breadcrumb: a freeze can start under any of them, and a
901
+ // block report that cannot name a suspect is much weaker evidence.
902
+ const guarded = !READ_ONLY_TOOLS.has(name) && !name.startsWith("respond_");
903
+ tool.execute = async (input, context) => {
904
+ // Never cleared, only overwritten — a block often starts just AFTER
905
+ // a tool returns (the 2026-07-16 freeze began on the tick after a
906
+ // bash result), so "after:bash" is exactly the clue we want. Also
907
+ // why this is not a stack: with parallel tool calls, last-writer-
908
+ // wins is the honest summary; the CPU profile is the real evidence.
909
+ setLoopBreadcrumb(`tool:${name}`);
910
+ try {
911
+ if (!guarded)
912
+ return await originalExecute(input, context);
913
+ const gate = evaluateMutationGate(deps.bash.getCwd(), {
914
+ toolName: name,
915
+ hardGateEnabled: gsdHardGateEnabled,
916
+ directAnswer: gsdDirectAnswer,
917
+ });
918
+ if (gate.blocked) {
919
+ return { success: false, output: gate.reason, error: gate.reason };
920
+ }
921
+ return await writeMutex.run(() => originalExecute(input, context));
922
+ }
923
+ finally {
924
+ setLoopBreadcrumb(`after-tool:${name}`);
925
+ }
926
+ };
669
927
  }
670
928
  captureToolSchemas(tools);
671
929
  let responseToolCalled = false;
@@ -761,7 +1019,40 @@ export async function* executeToolEngine(args) {
761
1019
  // (non-cached) system message via the slice below.
762
1020
  const mcpCapabilityBlock = buildMcpCapabilityBlock(Object.keys(tools));
763
1021
  const systemWithCaps = mcpCapabilityBlock ? `${systemWithShell}${mcpCapabilityBlock}` : systemWithShell;
764
- const systemForModel = runtime.modelId.startsWith("claude")
1022
+ // Task 3 — non-Claude prompt-cache prefix stability. On non-Claude the
1023
+ // system is a single string; per-turn-dynamic content (dynamicSuffix +
1024
+ // PIL suffix + MCP roster) that sits AFTER the byte-stable staticPrefix
1025
+ // but BEFORE the conversation shifts the cached prefix and nukes the
1026
+ // cache on PIL-active turns (session 47a774d272da: pil_active=1 ⟺
1027
+ // cache_read=0). We keep the front byte-stable and relocate the dynamic
1028
+ // tail into the trailing user message (variant b) — NOT a mid-conversation
1029
+ // system-role message, which OpenAI-compatible providers (DeepSeek/GLM)
1030
+ // do not reliably accept. Claude keeps its untouched two-block split.
1031
+ const _isClaudeModel = runtime.modelId.startsWith("claude");
1032
+ const { front: _nonClaudeFront, dynamicTail: _nonClaudeDynamicTail } = _isClaudeModel
1033
+ ? { front: systemWithCaps, dynamicTail: "" }
1034
+ : splitFrontAndDynamicTail({
1035
+ modelId: runtime.modelId,
1036
+ systemWithCaps,
1037
+ staticPrefix: systemParts.staticPrefix,
1038
+ });
1039
+ // Only relocate when the enriched user message is actually present to
1040
+ // receive the tail; otherwise fall back to the original single string so
1041
+ // no instruction content is dropped.
1042
+ const _willFoldDynamicTail = !_isClaudeModel &&
1043
+ _nonClaudeDynamicTail.trim().length > 0 &&
1044
+ deps.messages.includes(userModelMessage);
1045
+ if (process.env.MUONROI_DEBUG_CACHE_PREFIX === "1") {
1046
+ try {
1047
+ console.error(`[cache-prefix] model=${runtime.modelId} staticPrefixLen=${systemParts.staticPrefix.length} ` +
1048
+ `frontLen=${_nonClaudeFront.length} dynTailLen=${_nonClaudeDynamicTail.length} ` +
1049
+ `fold=${_willFoldDynamicTail} msgCount=${deps.messages.length}`);
1050
+ }
1051
+ catch (err) {
1052
+ console.error(`[cache-prefix] log failed: ${err?.message}`);
1053
+ }
1054
+ }
1055
+ const systemForModel = _isClaudeModel
765
1056
  ? [
766
1057
  {
767
1058
  role: "system",
@@ -773,7 +1064,9 @@ export async function* executeToolEngine(args) {
773
1064
  content: systemWithCaps.slice(systemParts.staticPrefix.length),
774
1065
  },
775
1066
  ]
776
- : systemWithCaps;
1067
+ : _willFoldDynamicTail
1068
+ ? _nonClaudeFront
1069
+ : systemWithCaps;
777
1070
  // Capture prompt-size breakdown so recordUsage can attach it to the
778
1071
  // cost-log entry. Without this, "system prompt is huge" is unfalsifiable.
779
1072
  // chars/4 ≈ tokens for English; reported as chars to keep math obvious.
@@ -837,13 +1130,27 @@ export async function* executeToolEngine(args) {
837
1130
  // stubs. Symmetric to the B3 sub-agent path; reuses the same module
838
1131
  // with `label: "top-level"` so the stub text reflects which loop
839
1132
  // elided the content.
840
- const topLevelCompactThreshold = getTopLevelCompactThresholdChars();
841
- const topLevelCompactKeepLast = getTopLevelCompactKeepLast();
1133
+ const topLevelCompactThreshold = getTopLevelCompactThresholdChars(contextWindow);
1134
+ const topLevelCompactKeepLast = getTopLevelCompactKeepLast(contextWindow);
1135
+ // O2 — byte budget for the verbatim tail; shrinks keepLast on read-heavy
1136
+ // turns that stay large at low fill (the 60-80k-per-call bucket).
1137
+ const topLevelCompactTailBudget = getTopLevelCompactTailBudgetChars(contextWindow);
1138
+ // O3 — compaction hysteresis state (per-turn; this scope runs once per
1139
+ // streamText turn). Once we compact, freeze the compacted prefix and
1140
+ // only append new messages until size grows past the hysteresis ceiling,
1141
+ // so the provider prompt-cache prefix stays byte-stable across steps
1142
+ // instead of breaking every step as the keepLast boundary slides.
1143
+ const compactHysteresis = getTopLevelCompactHysteresis();
1144
+ let hysteresisState = initCompactionHysteresisState();
842
1145
  // Phase O1 — capture providerOptions SHAPE (types only) for forensics.
843
1146
  deps.setLastProviderOptionsShape(Object.keys(providerOpts).length > 0 ? extractProviderOptionsShape(providerOpts) : null);
844
1147
  // Substitute the enriched user message for the current turn so the LLM sees PIL additions,
845
1148
  // while leaving the DB-persisted `deps.messages` clean for future turns.
846
- const _messagesForCall = deps.messages.map((m) => m === userModelMessage ? userEnrichedMessage : m);
1149
+ const _messagesForCall = deps.messages.map((m) => m === userModelMessage
1150
+ ? _willFoldDynamicTail
1151
+ ? foldDynamicTailIntoUserMessage(userEnrichedMessage, _nonClaudeDynamicTail)
1152
+ : userEnrichedMessage
1153
+ : m);
847
1154
  if (wireDebug.enabled) {
848
1155
  wireDebug.logRequest({
849
1156
  providerId: runtime.modelInfo?.provider ?? "unknown",
@@ -865,6 +1172,37 @@ export async function* executeToolEngine(args) {
865
1172
  const _baseDynamicStopWhen = createToolLoopCapPredicate({
866
1173
  initialCap: deps.maxToolRounds,
867
1174
  ask: async (info) => {
1175
+ // Auto-recover a "cap" (tool-round ceiling) halt: compact the history and keep
1176
+ // going, instead of stopping and telling the user to /compact
1177
+ // (prompts.ts). Capped so a runaway turn still terminates;
1178
+ // pattern-loop halts are excluded (agent is stuck).
1179
+ if (shouldAutoRecoverToolLimit(info, toolLimitAutoRecoverCount, TOOL_LIMIT_AUTO_RECOVER_CAP)) {
1180
+ toolLimitAutoRecoverCount++;
1181
+ try {
1182
+ const _cw = runtime.modelInfo?.contextWindow ?? 0;
1183
+ if (_cw > 0) {
1184
+ await deps.compactForContext(provider, system, _cw, signal, deps.getCompactionSettings(_cw), false);
1185
+ // A compacted round resets context to O(N) input (cheap), so the
1186
+ // hard-cap's cost-runaway purpose is served — grant the turn more
1187
+ // headroom instead of letting the 1.5× hard ceiling strand a
1188
+ // genuinely productive long task. Bounded inside the Agent.
1189
+ deps.extendHardCeilingForAutoCompaction?.();
1190
+ }
1191
+ }
1192
+ catch (err) {
1193
+ logger.error("orchestrator", "tool-limit auto-recover compaction failed", {
1194
+ message: err instanceof Error ? err.message : String(err),
1195
+ });
1196
+ }
1197
+ const _ar2 = globalThis.__muonroiAgentRuntime;
1198
+ _ar2?.emitEvent({
1199
+ t: "event",
1200
+ kind: "toast",
1201
+ level: "info",
1202
+ text: `đạt giới hạn bước — đã tự nén ngữ cảnh và tiếp tục (lần ${toolLimitAutoRecoverCount}/${TOOL_LIMIT_AUTO_RECOVER_CAP})`,
1203
+ });
1204
+ return "continue";
1205
+ }
868
1206
  if (info.kind === "pattern") {
869
1207
  if (patternLoopInjectCount < 1) {
870
1208
  patternLoopInjectCount++;
@@ -987,6 +1325,14 @@ export async function* executeToolEngine(args) {
987
1325
  _hardCapHit = true;
988
1326
  return true;
989
1327
  }
1328
+ // convene_council fast-path: if the model queued a council convening
1329
+ // during this step, STOP now so the outer loop runs the council and
1330
+ // splices the synthesis BEFORE the model consumes the placeholder
1331
+ // tool_result. This is a fast-path only — the authoritative
1332
+ // consumption is the outer-loop check after the stream drains (a
1333
+ // phase-1 SAMR step ends on stepCountIs(1) and never reaches here).
1334
+ if (hasPendingCouncilConvene())
1335
+ return true;
990
1336
  // Terminal response tool: a `respond_*` call IS the model's final
991
1337
  // structured answer (its `execute` is identity — the payload lives
992
1338
  // in the tool-call args). `shouldHaltOnResponseTool` decides if the
@@ -1081,9 +1427,27 @@ export async function* executeToolEngine(args) {
1081
1427
  // on every chunk via stall.pet(), so it never kills an actively
1082
1428
  // streaming call. Disposed when the stream ends or errors.
1083
1429
  stallTriggered = false;
1430
+ // Second timer (progressTimeoutMs) is the no-forward-progress guard.
1431
+ // stall.pet() re-arms the any-activity timer on EVERY chunk — including
1432
+ // a reasoning model's reasoning-delta — so an endless chain-of-thought
1433
+ // keeps it alive and it never fires (observed live 2026-07-10: a
1434
+ // deepseek-v4-flash sub-SESSION churned reasoning 30+ min, 1.4M input
1435
+ // tokens, ZERO text/tool output; the 2-min stall watchdog never tripped).
1436
+ // The progress timer is reset ONLY by stall.petProgress() on real output
1437
+ // (text-delta / tool-call), aborting a runaway-reasoning loop while a
1438
+ // legitimately long reasoning burst that DOES emit output survives.
1084
1439
  const stall = createStallWatchdog(getProviderStallTimeoutMs(), () => {
1085
1440
  stallTriggered = true;
1086
- });
1441
+ }, {
1442
+ progressTimeoutMs: getProviderProgressTimeoutMs(),
1443
+ onProgressFire: () => {
1444
+ stallTriggered = true;
1445
+ console.error(`[tool-engine] stream aborted: no text/tool output for ${getProviderProgressTimeoutMs()}ms ` +
1446
+ `(runaway reasoning / no forward progress) model=${runtime.modelId}`);
1447
+ },
1448
+ },
1449
+ // Hold the stream open while a blocking `ask_user` card awaits a human.
1450
+ isInteractivePaused);
1087
1451
  // F3c — hard-cap LLM calls per turn before this streamText()
1088
1452
  if (++llmCallsThisTurn > MAX_LLM_CALLS_PER_TURN) {
1089
1453
  stall.dispose();
@@ -1260,10 +1624,17 @@ export async function* executeToolEngine(args) {
1260
1624
  };
1261
1625
  // T1.1 + T1.2 — reasoning models (DeepSeek V4 Flash, R1) emit 2K-5K
1262
1626
  // CoT tokens per turn that accumulate across the multi-step loop.
1263
- // Strip old reasoning and compact earlier (ratio 0.3 vs 0.5) to
1264
- // cut ~40-60% of cumulative input tokens.
1627
+ // Strip old reasoning and compact earlier to cut cumulative input.
1628
+ // Small-context reasoning models (< 100K) use ratio 0.2 (fire at
1629
+ // 20% fill) because their per-step overhead (system + tools + CoT)
1630
+ // already consumes ~30-40% of the window, leaving little headroom.
1265
1631
  const isReasoningModel = runtime.modelInfo?.reasoning === true;
1266
- const compacted = compactSubAgentMessages(stripped, {
1632
+ const reasoningFillRatio = isReasoningModel
1633
+ ? contextWindowTokens > 0 && contextWindowTokens < 100_000
1634
+ ? 0.2
1635
+ : 0.3
1636
+ : undefined;
1637
+ const runCompaction = () => compactSubAgentMessages(stripped, {
1267
1638
  thresholdChars: topLevelCompactThreshold,
1268
1639
  // Rec #1 (cheap part): on meta/self-eval turns keep a couple more
1269
1640
  // trailing tool turns verbatim — those carry the reasoning the
@@ -1274,28 +1645,120 @@ export async function* executeToolEngine(args) {
1274
1645
  label: "top-level",
1275
1646
  envelopeChars,
1276
1647
  contextWindowTokens,
1277
- contextFillRatio: isReasoningModel ? 0.3 : undefined,
1648
+ contextFillRatio: reasoningFillRatio,
1278
1649
  keepToolIds: keepToolIds.length ? keepToolIds : undefined,
1279
1650
  persistArtifact,
1280
1651
  stripOldReasoning: isReasoningModel,
1652
+ tailBudgetChars: topLevelCompactTailBudget,
1281
1653
  });
1282
- if (compacted !== stripped)
1283
- recordCompaction(sn);
1654
+ // O3 — compaction hysteresis (holds the frozen compacted prefix
1655
+ // between compactions so the provider prompt-cache prefix stays
1656
+ // byte-stable across steps instead of breaking as the keepLast
1657
+ // boundary slides). See applyCompactionHysteresis.
1658
+ const currChars = cumulativeMessageChars(stripped) + envelopeChars;
1659
+ // Proactive compaction (agent called the `compact` tool). Consume the
1660
+ // one-shot request and FORCE a compaction this step, bypassing the
1661
+ // hysteresis threshold — the agent explicitly asked to shed context,
1662
+ // so a one-time cache-prefix break is the intended trade. Re-seed the
1663
+ // hysteresis state to the fresh compacted prefix so the following
1664
+ // steps hold it steady (no per-step churn) until it grows again.
1665
+ const _proactiveCompact = consumeProactiveCompact();
1666
+ let compacted;
1667
+ if (_proactiveCompact) {
1668
+ const _forced = runCompaction();
1669
+ const _didForce = _forced !== stripped;
1670
+ compacted = _forced;
1671
+ hysteresisState = _didForce
1672
+ ? { frozenCompacted: _forced, frozenStrippedLen: stripped.length, lastCompactTriggerChars: currChars }
1673
+ : hysteresisState;
1674
+ if (_didForce)
1675
+ recordCompaction(sn);
1676
+ try {
1677
+ const _arPc = globalThis.__muonroiAgentRuntime;
1678
+ _arPc?.emitEvent({
1679
+ t: "event",
1680
+ kind: "toast",
1681
+ level: "info",
1682
+ text: _didForce
1683
+ ? "đã nén ngữ cảnh theo yêu cầu của agent — tiếp tục tác vụ"
1684
+ : "agent yêu cầu nén nhưng chưa có gì để nén — tiếp tục",
1685
+ });
1686
+ }
1687
+ catch {
1688
+ /* toast best-effort */
1689
+ }
1690
+ }
1691
+ else {
1692
+ const _hyst = applyCompactionHysteresis({
1693
+ stripped,
1694
+ currChars,
1695
+ hysteresis: compactHysteresis,
1696
+ state: hysteresisState,
1697
+ runCompaction,
1698
+ });
1699
+ compacted = _hyst.compacted;
1700
+ hysteresisState = _hyst.state;
1701
+ // Count only ACTUAL (re)compactions, not held-boundary steps — the
1702
+ // compaction counter drives the cache-churn telemetry this fixes.
1703
+ if (_hyst.didRecompact)
1704
+ recordCompaction(sn);
1705
+ }
1706
+ const coalesced = coalesceReadOnlyMessages(compacted);
1707
+ // (recordCompaction already handled per-branch above.)
1284
1708
  // Pre-compaction visibility: give the agent one step of notice
1285
1709
  // before B4 actually rewrites history into stubs. This is the
1286
1710
  // advance warning that was missing — agent can now decide to
1287
1711
  // summarize, finish, or request preservation. Fires when we did
1288
1712
  // NOT compact this step (compacted === stripped, restored by the
1289
- // compactSubAgentMessages no-op ref contract) AND the prompt is
1290
- // approaching the threshold. Must compare CHARS (messages +
1291
- // envelope), not stripped.length (a message count that never
1292
- // exceeds a char-scaled threshold) — session 2b7a10219499.
1293
1713
  const _preWarnChars = cumulativeMessageChars(stripped) + envelopeChars;
1294
- if (compacted === stripped && shouldPreWarnCompaction(_preWarnChars, topLevelCompactThreshold)) {
1714
+ if (coalesced === stripped && shouldPreWarnCompaction(_preWarnChars, topLevelCompactThreshold)) {
1295
1715
  const _cp = buildCheckpointReminder(sn, true);
1296
1716
  const _pre = `[pre-compaction warning at step ${sn} — next step(s) will likely rewrite older tool results to stubs (threshold ${topLevelCompactThreshold}, keepLast=${topLevelCompactKeepLast}). ${_cp} Summarize or finish if possible, or warn the user they can run the "/compact" command if they want a clean compressed history.]`;
1297
1717
  return withSteers({ messages: attachReminderToMessages(stripped, _pre) });
1298
1718
  }
1719
+ // ---- Read-only tool batching interceptor ----
1720
+ // The system prompt already instructs batching (BATCH ALL TOOL
1721
+ // CALLS — HARD RULE), but models like DeepSeek frequently ignore
1722
+ // it and emit 1 read-only call per step. This injects a concrete,
1723
+ // in-context reminder right before the next LLM call when the
1724
+ // previous step had ≤2 all-read-only tool calls. Cost: ~0 extra
1725
+ // context. Effect: reduces tool rounds 2-3x on the same work.
1726
+ if (sn >= 1) {
1727
+ let _lastAsst = null;
1728
+ for (let _i = stepMessages.length - 1; _i >= 0; _i--) {
1729
+ if (stepMessages[_i].role === "assistant") {
1730
+ _lastAsst = stepMessages[_i];
1731
+ break;
1732
+ }
1733
+ }
1734
+ if (_lastAsst && Array.isArray(_lastAsst.content)) {
1735
+ let _total = 0, _ro = 0;
1736
+ for (const _p of _lastAsst.content) {
1737
+ if (_p.type === "tool-call") {
1738
+ _total++;
1739
+ if (READ_ONLY_TOOLS.has(_p.toolName))
1740
+ _ro++;
1741
+ }
1742
+ }
1743
+ if (_total > 0 && _ro === _total && _total <= 2) {
1744
+ // Prefer the SINGLE-CALL multi-path form (read_file file_paths=[…])
1745
+ // over "parallel tool_calls": a batch of parallel tool_calls is
1746
+ // reshaped into sequential single-call turns for the kimi/glm/
1747
+ // deepseek-go cohort (splitParallelToolCalls) — which re-inflates
1748
+ // history and defeats the batching win. One read_file carrying N
1749
+ // paths is ONE tool_call → never split → the token cut holds on
1750
+ // every provider.
1751
+ const _b = `[Tool batching: you called ${_total} read-only tool(s) one-at-a-time — each extra call re-sends the whole conversation, so N single reads cost O(N²) tokens. To read MULTIPLE files, call read_file ONCE with file_paths=["a","b","c"] — a SINGLE tool_call that no provider splits. For other read-only tools (grep, bash_output_get), emit all pending calls in ONE assistant turn. Do NOT sequence reads across steps.]`;
1752
+ // Attach to `coalesced` (the B4-compacted history), NOT `stripped`:
1753
+ // read-only tools (read_file/grep/…) are exactly what triggers this
1754
+ // reminder, and returning `stripped` here silently discarded the
1755
+ // compaction computed above — so on any loop where the model emits
1756
+ // ≤2 read-only calls per step (DeepSeek does this constantly) older
1757
+ // tool results were never elided and cumulative input grew unbounded.
1758
+ return withSteers({ messages: attachReminderToMessages(coalesced, _b) });
1759
+ }
1760
+ }
1761
+ }
1299
1762
  // Phase 4A — scope reminder injection (REQ-005).
1300
1763
  // Cadence K = 3/5/8 for small/medium/large. Soft-warn fires
1301
1764
  // ONCE per session at floor(ceiling*0.7). Reminder lives in
@@ -1355,10 +1818,10 @@ export async function* executeToolEngine(args) {
1355
1818
  ? `${buildRepetitionReminder(_ceilingSessionId)}\n${_scopePart}`
1356
1819
  : buildRepetitionReminder(_ceilingSessionId)
1357
1820
  : _scopePart;
1358
- const withReminder = attachReminderToMessages(compacted, _reminder);
1821
+ const withReminder = attachReminderToMessages(coalesced, _reminder);
1359
1822
  return withSteers({ messages: withReminder });
1360
1823
  }
1361
- if (compacted === stripped && stripped === stepMessages)
1824
+ if (coalesced === stripped && stripped === stepMessages)
1362
1825
  return withSteers({});
1363
1826
  // Self-awareness note: tell the model compaction happened so it
1364
1827
  // knows earlier context was elided and can adjust its behavior.
@@ -1377,12 +1840,12 @@ export async function* executeToolEngine(args) {
1377
1840
  })()
1378
1841
  : null;
1379
1842
  if (_compactNote) {
1380
- return withSteers({ messages: attachReminderToMessages(compacted, _compactNote) });
1843
+ return withSteers({ messages: attachReminderToMessages(coalesced, _compactNote) });
1381
1844
  }
1382
- return withSteers({ messages: compacted });
1845
+ return withSteers({ messages: coalesced });
1383
1846
  },
1384
- ...(dropParam("temperature") ? {} : { temperature: 0.7 }),
1385
- ...(dropParam("maxOutputTokens") ? {} : { maxOutputTokens: taskTypeToMaxTokens(pilCtx.taskType) }),
1847
+ ...resolveTemperatureParam(runtime, 0.7),
1848
+ ...(dropParam("maxOutputTokens") ? {} : { maxOutputTokens: resolveTurnMaxOutputTokens(pilCtx) }),
1386
1849
  ...(Object.keys(providerOpts).length > 0 ? { providerOptions: providerOpts } : {}),
1387
1850
  experimental_onStepStart: (event) => {
1388
1851
  stepNumber = getStepNumber(event, stepNumber + 1);
@@ -1401,6 +1864,10 @@ export async function* executeToolEngine(args) {
1401
1864
  finishReason: getFinishReason(event),
1402
1865
  usage: stepUsage,
1403
1866
  });
1867
+ // Pull any completed background delegations so their results can be
1868
+ // injected (as system messages) for the *next* LLM step in this same turn.
1869
+ // This improves "self wake" for background jobs without waiting for a new user turn.
1870
+ void deps.consumeBackgroundNotifications?.().catch(() => { });
1404
1871
  // Realtime status bar update per step
1405
1872
  if (stepUsage.inputTokens || stepUsage.outputTokens) {
1406
1873
  // O1 — thread THIS turn's providerOptions shape per step so every
@@ -1451,6 +1918,9 @@ export async function* executeToolEngine(args) {
1451
1918
  const _wireProviderIdTop = runtime.modelInfo?.provider ?? "unknown";
1452
1919
  for await (const part of result.fullStream) {
1453
1920
  stall.pet(); // chunk arrived — reset the stall watchdog
1921
+ // Breadcrumb the chunk type: if the loop blocks while draining the
1922
+ // stream, this says which part kind we were handling when it froze.
1923
+ setLoopBreadcrumb(`stream:${String(part.type ?? "unknown")}`);
1454
1924
  // Count only real content parts. The watchdog abort itself surfaces
1455
1925
  // as an "abort" part — counting it would defeat the TTFB-stall gate
1456
1926
  // (a frozen-before-first-byte stall yields ONLY the abort part).
@@ -1475,8 +1945,23 @@ export async function* executeToolEngine(args) {
1475
1945
  wireDebug.logError(_wireProviderIdTop, part.error);
1476
1946
  }
1477
1947
  }
1948
+ // Terminal part of the ENTIRE multi-step turn (AI SDK v6 emits exactly
1949
+ // one `finish` after every step's `finish-step`; it carries the final
1950
+ // finishReason/totalUsage). onFinish has already run by now (usage
1951
+ // recorded + llm-done emitted, and result.response is resolved), so
1952
+ // there is nothing left to drain. Some providers — observed live:
1953
+ // xai/grok-composer-2.5-fast — emit `finish` but then never CLOSE the
1954
+ // fullStream async iterator, so `for await` would block on the next
1955
+ // `.next()` until the turn watchdog fires (up to MUONROI_TURN_IDLE_MS).
1956
+ // Breaking on `finish` finalizes the turn immediately instead. Safe:
1957
+ // `finish` is strictly last, so this can never truncate a multi-step
1958
+ // turn (per-step boundaries are `finish-step`, handled by fall-through).
1959
+ if (part.type === "finish") {
1960
+ break;
1961
+ }
1478
1962
  switch (part.type) {
1479
1963
  case "text-delta":
1964
+ stall.petProgress(); // real forward progress — reset the no-progress guard
1480
1965
  assistantText += part.text;
1481
1966
  // Task 2.6b — emit llm-token (agent-mode only; high-volume, default-off per Phase 4).
1482
1967
  try {
@@ -1508,6 +1993,7 @@ export async function* executeToolEngine(args) {
1508
1993
  yield { type: "reasoning", content: part.text };
1509
1994
  break;
1510
1995
  case "tool-call": {
1996
+ stall.petProgress(); // real forward progress — reset the no-progress guard
1511
1997
  const tc = toToolCall(part);
1512
1998
  activeToolCalls.push(tc);
1513
1999
  // SAMR: track that Phase 1 produced tool calls → transition to Phase 2
@@ -1728,10 +2214,10 @@ export async function* executeToolEngine(args) {
1728
2214
  // the global __muonroiSafetyApproved map so registry.ts's
1729
2215
  // bash.execute bypasses the block on the retry.
1730
2216
  const _outputText = [tr.output, tr.error].filter((x) => typeof x === "string").join("\n");
1731
- const _blockMatch = _outputText.match(/^BLOCKED \(([^)]+)\):\s*(.*)/);
1732
- if (_blockMatch && part.toolName === "bash") {
1733
- const _blockKind = _blockMatch[1];
1734
- const _blockReason = _blockMatch[2];
2217
+ const _parsedBlock = parseSafetyBlock(_outputText);
2218
+ if (_parsedBlock && part.toolName === "bash") {
2219
+ const _blockKind = _parsedBlock.kind;
2220
+ const _blockReason = _parsedBlock.reason;
1735
2221
  const _command = typeof part.input === "object" && part.input != null
1736
2222
  ? String(part.input.command ?? "")
1737
2223
  : "";
@@ -1744,15 +2230,33 @@ export async function* executeToolEngine(args) {
1744
2230
  tr = { ...tr, success: false, error: _outputText, output: _outputText };
1745
2231
  }
1746
2232
  else {
1747
- const _verdict = deps.askSafetyOverride
1748
- ? await deps.askSafetyOverride({
1749
- kind: _blockKind,
1750
- toolName: part.toolName,
1751
- blockedItem: _command,
1752
- reason: _blockReason,
1753
- source: "bash.execute",
1754
- })
1755
- : { action: "block" };
2233
+ // yolo mode auto-approves lower-severity blocks (git-safety /
2234
+ // dangerous) so "don't ask me" actually stops asking — but a
2235
+ // catastrophic, irreversible command STILL shows the askcard,
2236
+ // even in yolo. Everything else surfaces the interactive
2237
+ // safety-override card via deps.askSafetyOverride (registered
2238
+ // by the TUI); when no handler is wired (headless / batch),
2239
+ // that resolves to { action: "block" }, preserving the
2240
+ // backward-compatible hard-stop for non-interactive runs.
2241
+ let _verdict;
2242
+ if (shouldAutoAllowYolo(_blockKind, deps.permissionMode)) {
2243
+ _verdict = { action: "allow-once" };
2244
+ yield {
2245
+ type: "content",
2246
+ content: `[yolo: auto-approved blocked command: ${_blockKind}]\n`,
2247
+ };
2248
+ }
2249
+ else {
2250
+ _verdict = deps.askSafetyOverride
2251
+ ? await deps.askSafetyOverride({
2252
+ kind: _blockKind,
2253
+ toolName: part.toolName,
2254
+ blockedItem: _command,
2255
+ reason: _blockReason,
2256
+ source: "bash.execute",
2257
+ })
2258
+ : { action: "block" };
2259
+ }
1756
2260
  if (_verdict.action === "allow-once" || _verdict.action === "allow-session") {
1757
2261
  // Store approval so registry.ts can bypass the block on retry.
1758
2262
  const _globalSafety = globalThis;
@@ -1886,10 +2390,26 @@ export async function* executeToolEngine(args) {
1886
2390
  // dedicated chunk so the sticky checklist panel can re-render
1887
2391
  // without parsing tool args itself. Skipped when the snapshot
1888
2392
  // doesn't parse (malformed args) so the UI is never poisoned.
1889
- if (tc.function.name === "todo_write" && tr.success) {
1890
- const snap = snapshotFromTodoWriteArgs(tc.function.arguments);
1891
- if (snap)
1892
- yield { type: "task_list_update", taskListSnapshot: snap };
2393
+ if (tr.success && (tc.function.name === "todo_write" || tc.function.name.startsWith("gsd_"))) {
2394
+ try {
2395
+ const { getTaskListSnapshotFromGsd } = require("../gsd/phase-sync.js");
2396
+ const snap = getTaskListSnapshotFromGsd(deps.bash.getCwd());
2397
+ if (snap) {
2398
+ yield { type: "task_list_update", taskListSnapshot: snap };
2399
+ }
2400
+ else if (tc.function.name === "todo_write") {
2401
+ const snapLegacy = snapshotFromTodoWriteArgs(tc.function.arguments);
2402
+ if (snapLegacy)
2403
+ yield { type: "task_list_update", taskListSnapshot: snapLegacy };
2404
+ }
2405
+ }
2406
+ catch (err) {
2407
+ if (tc.function.name === "todo_write") {
2408
+ const snapLegacy = snapshotFromTodoWriteArgs(tc.function.arguments);
2409
+ if (snapLegacy)
2410
+ yield { type: "task_list_update", taskListSnapshot: snapLegacy };
2411
+ }
2412
+ }
1893
2413
  }
1894
2414
  break;
1895
2415
  }
@@ -2454,6 +2974,109 @@ export async function* executeToolEngine(args) {
2454
2974
  }
2455
2975
  }
2456
2976
  stall.dispose(); // stream drained normally — stop the stall watchdog
2977
+ // ─── convene_council consumption ───────────────────────────────
2978
+ // The convene_council tool queued a request during THIS step's execute().
2979
+ // Consume it here in the OUTER loop after every stream drain — NOT solely
2980
+ // via dynamicStopWhen, because a phase-1 SAMR step ends on stepCountIs(1)
2981
+ // and never evaluates the stop hook (design-debate BUG 2). Runs the
2982
+ // council autonomously (convenePath suppresses ALL post-debate decision
2983
+ // surface — no card, no continuation), splices the synthesis into the
2984
+ // convene tool_result, grafts into deps.messages, and restarts the step
2985
+ // so the model reads the conclusion as the tool result and continues.
2986
+ if (hasPendingCouncilConvene()) {
2987
+ if (responseToolCalled) {
2988
+ // The same step also emitted a terminal respond_* answer — the model
2989
+ // is done; discard the convene request rather than override the
2990
+ // answer (design-debate BUG 1: never let the flag leak across turns).
2991
+ consumeCouncilConvene();
2992
+ logger.warn("orchestrator", "convene: discarded — step also emitted a terminal response tool");
2993
+ }
2994
+ else {
2995
+ const pendingId = peekCouncilConveneToolCallId();
2996
+ let conveneResponse = null;
2997
+ try {
2998
+ conveneResponse = await result.response;
2999
+ }
3000
+ catch (err) {
3001
+ logger.error("orchestrator", "convene: failed to read response.messages", {
3002
+ error: err?.message,
3003
+ });
3004
+ }
3005
+ // BUG 3 guard: only run council when the pending convene toolCallId is
3006
+ // a recorded tool-result in THIS drain's messages — else a nested
3007
+ // frame's convene call would be wrongly consumed by this loop.
3008
+ const belongsHere = !!conveneResponse &&
3009
+ !!pendingId &&
3010
+ conveneResponse.messages.some((m) => m?.role === "tool" &&
3011
+ Array.isArray(m.content) &&
3012
+ m.content.some((p) => p?.type === "tool-result" && p?.toolCallId === pendingId));
3013
+ if (belongsHere && conveneResponse) {
3014
+ const req = consumeCouncilConvene();
3015
+ // Loop guard: a second convene in the SAME turn short-circuits —
3016
+ // do NOT pay for another council. Splice a non-binding suggestion
3017
+ // pointing the model at the synthesis already in the transcript so
3018
+ // it responds (or asks the user) instead of re-convening forever.
3019
+ if (conveneRunsThisTurn >= COUNCIL_MAX_CONVENES_PER_TURN) {
3020
+ logger.warn("orchestrator", "convene: loop guard hit — suppressing re-convene", {
3021
+ conveneRunsThisTurn,
3022
+ cap: COUNCIL_MAX_CONVENES_PER_TURN,
3023
+ });
3024
+ const suggestion = "[The council already convened this turn — its synthesis is in the transcript above. " +
3025
+ "Do NOT convene again for the same question. Use that synthesis now: give the user your " +
3026
+ "recommendation, or call ask_user if you need their go-ahead before implementing.]";
3027
+ const { messages: guardSpliced, replaced: guardReplaced } = spliceConveneToolResult(conveneResponse.messages, req?.toolCallId ?? null, suggestion);
3028
+ if (!guardReplaced) {
3029
+ logger.warn("orchestrator", "convene: loop-guard splice found no matching toolCallId", {
3030
+ toolCallId: req?.toolCallId ?? null,
3031
+ });
3032
+ }
3033
+ const guardNewMsgs = guardSpliced.slice(deps.messages.length);
3034
+ for (const msg of guardNewMsgs)
3035
+ deps.messages.push(msg);
3036
+ continue; // re-enter with the suggestion as the tool result
3037
+ }
3038
+ conveneRunsThisTurn++;
3039
+ yield { type: "content", content: "\n[Convening the council…]\n" };
3040
+ // Filter the terminal `done` runCouncilV2 emits on its
3041
+ // non-continuation path (orchestrator.ts ~2274): letting it through
3042
+ // would finalize the UI turn BEFORE we restart streamText below, so
3043
+ // the model's post-council continuation would be orphaned/invisible
3044
+ // (live-caught: council ran, synthesis produced, but no final answer
3045
+ // rendered). We continue the SAME turn ourselves via the splice +
3046
+ // `continue` restart, so the council's `done` must not propagate.
3047
+ for await (const chunk of deps.runCouncilV2(userMessage, {
3048
+ convenePath: true,
3049
+ skipClarification: true,
3050
+ observer,
3051
+ userModelMessage,
3052
+ })) {
3053
+ if (chunk.type === "done")
3054
+ continue;
3055
+ yield chunk;
3056
+ }
3057
+ const synthesis = deps.councilManager.lastSynthesis;
3058
+ const spliceValue = synthesis && synthesis.trim().length > 0
3059
+ ? synthesis
3060
+ : `[Council could not produce a conclusion${req?.reason ? ` for: ${req.reason}` : ""}. Proceed using your own judgment.]`;
3061
+ if (!synthesis || synthesis.trim().length === 0) {
3062
+ logger.warn("orchestrator", "convene: council returned no synthesis", { reason: req?.reason ?? null });
3063
+ }
3064
+ const { messages: spliced, replaced } = spliceConveneToolResult(conveneResponse.messages, req?.toolCallId ?? null, spliceValue);
3065
+ if (!replaced) {
3066
+ logger.warn("orchestrator", "convene: tool_result splice found no matching toolCallId", {
3067
+ toolCallId: req?.toolCallId ?? null,
3068
+ });
3069
+ }
3070
+ // Graft the new messages (assistant tool-call + spliced tool-result)
3071
+ // into deps.messages, then restart the step so the model reads the
3072
+ // synthesis as the convene tool's result (SAMR restart precedent).
3073
+ const newMsgs = spliced.slice(deps.messages.length);
3074
+ for (const msg of newMsgs)
3075
+ deps.messages.push(msg);
3076
+ continue; // re-enter the loop with the spliced history
3077
+ }
3078
+ }
3079
+ }
2457
3080
  // ─── SAMR Phase 1 → Phase 2 transition ─────────────────────────
2458
3081
  // Phase 1 (premium model) produced tool calls but the SDK stopped
2459
3082
  // before executing them (stopWhen: stepCountIs(1)). Append the
@@ -3021,6 +3644,10 @@ export async function* executeToolEngine(args) {
3021
3644
  if (modelInfo?.contextWindow) {
3022
3645
  void deps.postTurnCompact(provider, system, modelInfo.contextWindow, signal).catch(() => { });
3023
3646
  }
3647
+ // Reactive delegation: report this turn's observed tool-output load so
3648
+ // the next turn can escalate to an isolated sub-session when it proves
3649
+ // heavy — independent of the fragile upfront router. See reactive-delegation.ts.
3650
+ deps.reportTurnToolLoad?.(_topLevelCapState?.cumulative ?? 0);
3024
3651
  yield { type: "done" };
3025
3652
  return;
3026
3653
  }
@@ -3167,6 +3794,10 @@ export async function* executeToolEngine(args) {
3167
3794
  if (modelInfo?.contextWindow) {
3168
3795
  void deps.postTurnCompact(provider, system, modelInfo.contextWindow, signal).catch(() => { });
3169
3796
  }
3797
+ // Reactive delegation: report this turn's observed tool-output load so
3798
+ // the next turn can escalate to an isolated sub-session when it proves
3799
+ // heavy — independent of the fragile upfront router. See reactive-delegation.ts.
3800
+ deps.reportTurnToolLoad?.(_topLevelCapState?.cumulative ?? 0);
3170
3801
  yield { type: "done" };
3171
3802
  return;
3172
3803
  }
@@ -3184,4 +3815,153 @@ export async function* executeToolEngine(args) {
3184
3815
  }
3185
3816
  }
3186
3817
  }
3818
+ export function coalesceReadOnlyMessages(messages) {
3819
+ if (!messages || messages.length === 0)
3820
+ return messages;
3821
+ const READ_ONLY_TOOLS = new Set([
3822
+ "read_file",
3823
+ "grep",
3824
+ "bash_output_get",
3825
+ "process_list",
3826
+ "delegation_read",
3827
+ "delegation_list",
3828
+ "ee_query",
3829
+ "ee_health",
3830
+ "usage_forensics",
3831
+ "lsp_query",
3832
+ "setup_guide",
3833
+ "selfverify_status",
3834
+ "selfverify_result",
3835
+ "selfverify_list",
3836
+ "list_vision_cache",
3837
+ "ee_feedback",
3838
+ "ee_write",
3839
+ ]);
3840
+ const result = [];
3841
+ let lastUserIdx = -1;
3842
+ for (let k = messages.length - 1; k >= 0; k--) {
3843
+ if (messages[k].role === "user") {
3844
+ lastUserIdx = k;
3845
+ break;
3846
+ }
3847
+ }
3848
+ if (lastUserIdx === -1)
3849
+ return messages;
3850
+ for (let k = 0; k <= lastUserIdx; k++) {
3851
+ result.push(messages[k]);
3852
+ }
3853
+ const postUser = messages.slice(lastUserIdx + 1);
3854
+ const groups = [];
3855
+ let currentAsst = null;
3856
+ for (const msg of postUser) {
3857
+ if (msg.role === "assistant") {
3858
+ if (currentAsst) {
3859
+ groups.push({ assistant: currentAsst, tool: null });
3860
+ }
3861
+ currentAsst = msg;
3862
+ }
3863
+ else if (msg.role === "tool") {
3864
+ if (currentAsst) {
3865
+ groups.push({ assistant: currentAsst, tool: msg });
3866
+ currentAsst = null;
3867
+ }
3868
+ else {
3869
+ result.push(msg);
3870
+ }
3871
+ }
3872
+ else {
3873
+ if (currentAsst) {
3874
+ groups.push({ assistant: currentAsst, tool: null });
3875
+ currentAsst = null;
3876
+ }
3877
+ result.push(msg);
3878
+ }
3879
+ }
3880
+ if (currentAsst) {
3881
+ groups.push({ assistant: currentAsst, tool: null });
3882
+ }
3883
+ const coalescedGroups = [];
3884
+ for (const group of groups) {
3885
+ const isReadOnly = (() => {
3886
+ if (!group.tool || !group.assistant)
3887
+ return false;
3888
+ const toolCalls = getToolCalls(group.assistant);
3889
+ if (toolCalls.length === 0)
3890
+ return false;
3891
+ return toolCalls.every((tc) => {
3892
+ const name = tc.toolName || tc.function?.name;
3893
+ return name && READ_ONLY_TOOLS.has(name);
3894
+ });
3895
+ })();
3896
+ if (isReadOnly) {
3897
+ const prev = coalescedGroups[coalescedGroups.length - 1];
3898
+ const prevIsReadOnly = prev &&
3899
+ (() => {
3900
+ if (!prev.tool || !prev.assistant)
3901
+ return false;
3902
+ const toolCalls = getToolCalls(prev.assistant);
3903
+ if (toolCalls.length === 0)
3904
+ return false;
3905
+ return toolCalls.every((tc) => {
3906
+ const name = tc.toolName || tc.function?.name;
3907
+ return name && READ_ONLY_TOOLS.has(name);
3908
+ });
3909
+ })();
3910
+ if (prevIsReadOnly) {
3911
+ prev.assistant = mergeAssistantMessages(prev.assistant, group.assistant);
3912
+ prev.tool = mergeToolMessages(prev.tool, group.tool);
3913
+ }
3914
+ else {
3915
+ coalescedGroups.push({ ...group });
3916
+ }
3917
+ }
3918
+ else {
3919
+ coalescedGroups.push(group);
3920
+ }
3921
+ }
3922
+ for (const group of coalescedGroups) {
3923
+ if (group.assistant)
3924
+ result.push(group.assistant);
3925
+ if (group.tool)
3926
+ result.push(group.tool);
3927
+ }
3928
+ return result;
3929
+ }
3930
+ function getToolCalls(msg) {
3931
+ if (Array.isArray(msg.toolCalls))
3932
+ return msg.toolCalls;
3933
+ if (Array.isArray(msg.content)) {
3934
+ return msg.content.filter((p) => p && p.type === "tool-call");
3935
+ }
3936
+ return [];
3937
+ }
3938
+ function mergeAssistantMessages(msg1, msg2) {
3939
+ const parts1 = Array.isArray(msg1.content)
3940
+ ? msg1.content
3941
+ : typeof msg1.content === "string" && msg1.content
3942
+ ? [{ type: "text", text: msg1.content }]
3943
+ : [];
3944
+ const parts2 = Array.isArray(msg2.content)
3945
+ ? msg2.content
3946
+ : typeof msg2.content === "string" && msg2.content
3947
+ ? [{ type: "text", text: msg2.content }]
3948
+ : [];
3949
+ const mergedContent = [...parts1, ...parts2];
3950
+ const res = {
3951
+ role: "assistant",
3952
+ content: mergedContent,
3953
+ };
3954
+ if (Array.isArray(msg1.toolCalls) || Array.isArray(msg2.toolCalls)) {
3955
+ res.toolCalls = [...(msg1.toolCalls ?? []), ...(msg2.toolCalls ?? [])];
3956
+ }
3957
+ return res;
3958
+ }
3959
+ function mergeToolMessages(msg1, msg2) {
3960
+ const parts1 = Array.isArray(msg1.content) ? msg1.content : [];
3961
+ const parts2 = Array.isArray(msg2.content) ? msg2.content : [];
3962
+ return {
3963
+ role: "tool",
3964
+ content: [...parts1, ...parts2],
3965
+ };
3966
+ }
3187
3967
  //# sourceMappingURL=tool-engine.js.map