muonroi-cli 1.7.2 → 1.8.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 (322) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +55 -60
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.js +43 -43
  253. package/dist/src/storage/transcript-view.js +7 -1
  254. package/dist/src/storage/transcript.d.ts +1 -0
  255. package/dist/src/storage/transcript.js +55 -0
  256. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  257. package/dist/src/storage/usage.js +14 -14
  258. package/dist/src/storage/workspaces.js +12 -12
  259. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  260. package/dist/src/tools/bash.d.ts +36 -23
  261. package/dist/src/tools/bash.js +59 -301
  262. package/dist/src/tools/bash.test.js +2 -257
  263. package/dist/src/tools/file-tracker.d.ts +5 -0
  264. package/dist/src/tools/file-tracker.js +14 -0
  265. package/dist/src/tools/file.js +2 -0
  266. package/dist/src/tools/git-safety.d.ts +38 -1
  267. package/dist/src/tools/git-safety.js +94 -4
  268. package/dist/src/tools/git-safety.test.js +45 -1
  269. package/dist/src/tools/native-tools.d.ts +1 -1
  270. package/dist/src/tools/native-tools.js +56 -0
  271. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  272. package/dist/src/tools/registry-git-safety.test.js +17 -6
  273. package/dist/src/tools/registry.d.ts +5 -0
  274. package/dist/src/tools/registry.js +185 -11
  275. package/dist/src/types/index.d.ts +7 -1
  276. package/dist/src/ui/app.d.ts +1 -0
  277. package/dist/src/ui/app.js +0 -0
  278. package/dist/src/ui/components/council-question-card.js +1 -0
  279. package/dist/src/ui/components/message-view.js +37 -3
  280. package/dist/src/ui/components/structured-response-view.js +7 -0
  281. package/dist/src/ui/components/task-list-panel.js +3 -12
  282. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  283. package/dist/src/ui/containers/chat-feed.js +66 -0
  284. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  285. package/dist/src/ui/containers/modals-layer.js +18 -0
  286. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  287. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  288. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  289. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  290. package/dist/src/ui/slash/council-inspect.js +4 -4
  291. package/dist/src/ui/slash/export.js +16 -1
  292. package/dist/src/ui/slash/menu-items.js +1 -0
  293. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  294. package/dist/src/ui/slash/ponytail.js +19 -0
  295. package/dist/src/ui/status-bar/store.js +0 -1
  296. package/dist/src/ui/status-bar/store.test.js +2 -2
  297. package/dist/src/ui/use-app-logic.d.ts +26 -0
  298. package/dist/src/ui/use-app-logic.js +0 -0
  299. package/dist/src/ui/utils/relaunch.js +1 -1
  300. package/dist/src/ui/utils/text.d.ts +11 -0
  301. package/dist/src/ui/utils/text.js +21 -1
  302. package/dist/src/ui/utils/text.test.js +25 -1
  303. package/dist/src/ui/utils/tools.js +3 -1
  304. package/dist/src/usage/cost-log.js +2 -2
  305. package/dist/src/usage/estimator.d.ts +14 -0
  306. package/dist/src/usage/estimator.js +22 -0
  307. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  308. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  309. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  311. package/dist/src/utils/clipboard-image.js +23 -23
  312. package/dist/src/utils/llm-deadline.d.ts +9 -1
  313. package/dist/src/utils/llm-deadline.js +31 -2
  314. package/dist/src/utils/permission-mode.d.ts +33 -0
  315. package/dist/src/utils/permission-mode.js +164 -3
  316. package/dist/src/utils/permission-mode.test.js +92 -1
  317. package/dist/src/utils/settings.d.ts +27 -31
  318. package/dist/src/utils/settings.js +100 -109
  319. package/dist/src/utils/side-question.js +2 -2
  320. package/dist/src/utils/skills.js +3 -3
  321. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  322. package/package.json +1 -1
@@ -33,10 +33,14 @@ let _cachedScope = null;
33
33
  // Latch: stores the last PreToolUse warning response so PostToolUse can build judgeCtx.
34
34
  // Follows the _cachedScope module-level variable pattern.
35
35
  let _lastWarningResponse = null;
36
+ // Fix 2: throttle recall-feedback reminder to once per unique pending-set.
37
+ // Tracks the sha of pending IDs so identical reminders don't repeat on every tool call.
38
+ let _lastRecallReminderSha = null;
36
39
  /** Reset hook module state — for test teardown only. */
37
40
  export function resetHookState() {
38
41
  _lastWarningResponse = null;
39
42
  _cachedScope = null;
43
+ _lastRecallReminderSha = null;
40
44
  }
41
45
  function emptyResult() {
42
46
  return {
@@ -200,13 +204,51 @@ export async function executeEventHooks(input, cwd, _signal) {
200
204
  why: m.why,
201
205
  confidence: m.confidence,
202
206
  }));
207
+ // Hard EE recall-feedback gate: inject mandatory reminder when there are
208
+ // pending unrated hints. The agent MUST clear all pending ee_feedback before
209
+ // proceeding with the task — system prompt in message-processor.ts enforces
210
+ // this as a first-before-anything directive.
211
+ //
212
+ // Fix 2: throttle to once per unique pending-set. The same 9 hints repeating
213
+ // after each of 10+ tool calls is pure UI noise — the model can't even see
214
+ // yield-content, and the user must scroll past identical blocks. Show the
215
+ // reminder ONCE when the set first appears or changes (e.g. after ee_feedback
216
+ // clears some entries), then stay silent until the set changes again.
217
+ let recallReminder = null;
218
+ try {
219
+ const { sessionRecallLedger, isRecallLedgerEnabled, formatPendingReminder } = await import("../ee/recall-ledger.js");
220
+ if (isRecallLedgerEnabled()) {
221
+ const pending = sessionRecallLedger.pending();
222
+ if (pending.length > 0) {
223
+ const pendingSha = pending
224
+ .map((p) => p.id)
225
+ .sort()
226
+ .join(",");
227
+ if (pendingSha !== _lastRecallReminderSha) {
228
+ _lastRecallReminderSha = pendingSha;
229
+ recallReminder = `⚠️ MANDATORY: ${pending.length} unrated EE recall(s) — call ee_feedback NOW before continuing.\n${formatPendingReminder(pending, { max: 5 })}`;
230
+ }
231
+ }
232
+ else {
233
+ // All cleared — reset so next batch shows fresh
234
+ _lastRecallReminderSha = null;
235
+ }
236
+ }
237
+ }
238
+ catch {
239
+ /* fail-open */
240
+ }
203
241
  if (r.decision === "block") {
204
242
  return {
205
243
  blocked: true,
206
244
  blockingErrors: [{ command: "ee:intercept", stderr: r.reason ?? "ee-blocked" }],
207
245
  preventContinuation: true,
208
246
  stopReason: r.reason ?? "ee-blocked",
209
- additionalContexts: [...capturedWarnings, ...(r.suggestions ?? [])],
247
+ additionalContexts: [
248
+ ...capturedWarnings,
249
+ ...(r.suggestions ?? []),
250
+ ...(recallReminder ? [recallReminder] : []),
251
+ ],
210
252
  decision: "block",
211
253
  results: [],
212
254
  eeMatches,
@@ -216,7 +258,11 @@ export async function executeEventHooks(input, cwd, _signal) {
216
258
  blocked: false,
217
259
  blockingErrors: [],
218
260
  preventContinuation: false,
219
- additionalContexts: [...capturedWarnings, ...(r.suggestions ?? [])],
261
+ additionalContexts: [
262
+ ...capturedWarnings,
263
+ ...(r.suggestions ?? []),
264
+ ...(recallReminder ? [recallReminder] : []),
265
+ ],
220
266
  decision: "approve",
221
267
  results: [],
222
268
  eeMatches,
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
  import type { StreamChunk } from "./types/index.js";
3
+ export declare function setTuiActive(active: boolean): void;
4
+ export declare function appendCrashLog(label: string, msg: string): void;
3
5
  export declare function setActiveEeYield(fn: ((chunk: StreamChunk) => void) | null): void;
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@ const packageJson = { version: PACKAGE_VERSION, description: PACKAGE_DESCRIPTION
17
17
  import { hydrateChatEnvFromKeychain } from "./chat/chat-keychain.js";
18
18
  import { setRenderSink } from "./ee/render.js";
19
19
  import { createHeadlessCouncilAutoAnswerer, handleCouncilChunk, parseCouncilAnswersFile, } from "./headless/council-answers.js";
20
- import { createHeadlessJsonlEmitter, isHeadlessOutputFormat, renderHeadlessChunk, renderHeadlessPrelude, } from "./headless/output.js";
20
+ import { createHeadlessJsonlEmitter, createHeadlessTextEmitter, isHeadlessOutputFormat, renderHeadlessPrelude, } from "./headless/output.js";
21
21
  import { loadCatalog, normalizeModelId } from "./models/registry.js";
22
22
  // Plan 00-07: boot-order modules — AbortContext + PendingCallsLog (TUI-01, TUI-03, TUI-04).
23
23
  import { createAbortContext } from "./orchestrator/abort.js";
@@ -32,7 +32,7 @@ import { loadUsage } from "./storage/usage-cap.js";
32
32
  import { startScheduleDaemon } from "./tools/schedule.js";
33
33
  import { processAtMentions } from "./utils/at-mentions.js";
34
34
  import { runScriptManagedUninstall } from "./utils/install-manager.js";
35
- import { getApiKey, getBaseURL, getCurrentModel, getCurrentSandboxMode, getCurrentSandboxSettings, mergeSandboxSettings, saveUserSettings, } from "./utils/settings.js";
35
+ import { getApiKey, getBaseURL, getCurrentModel, saveUserSettings } from "./utils/settings.js";
36
36
  import { runUpdate } from "./utils/update-checker.js";
37
37
  import { buildVerifyPrompt, getVerifyCliError } from "./verify/entrypoint.js";
38
38
  // Hydrate chat secrets from OS keychain before CLI bootstrap
@@ -41,20 +41,40 @@ const exitCleanlyOnSigterm = () => {
41
41
  process.exit(0);
42
42
  };
43
43
  process.on("SIGTERM", exitCleanlyOnSigterm);
44
- process.on("uncaughtException", (err) => {
44
+ // Set true while the interactive TUI owns the terminal (raw mode + alternate
45
+ // screen). When it is mounted, writing to stdout/stderr corrupts OpenTUI's
46
+ // framebuffer and a process.exit(1) tears the whole UI down — surfacing to the
47
+ // user as "kicked out of the TUI". A single stray unhandledRejection (e.g. a
48
+ // slash handler whose DB read throws and rejects with no .catch) must not be
49
+ // allowed to do that; while _tuiActive is set we log to crash.log and keep the
50
+ // process alive. Headless / CLI paths keep the original fail-fast behaviour.
51
+ let _tuiActive = false;
52
+ export function setTuiActive(active) {
53
+ _tuiActive = active;
54
+ }
55
+ export function appendCrashLog(label, msg) {
45
56
  try {
46
- require("fs").appendFileSync(require("path").join(require("os").homedir(), ".muonroi-cli", "crash.log"), `[${new Date().toISOString()}] UNCAUGHT: ${err.stack || err.message}\n`);
57
+ require("fs").appendFileSync(require("path").join(require("os").homedir(), ".muonroi-cli", "crash.log"), `[${new Date().toISOString()}] ${label}: ${msg}\n`);
58
+ }
59
+ catch {
60
+ /* crash.log is best-effort diagnostics; the logger itself must never throw */
47
61
  }
48
- catch { }
62
+ }
63
+ process.on("uncaughtException", (err) => {
64
+ appendCrashLog("UNCAUGHT", err.stack || err.message);
49
65
  console.error("Fatal:", err.message);
50
66
  process.exit(1);
51
67
  });
52
68
  process.on("unhandledRejection", (reason) => {
53
69
  const msg = reason instanceof Error ? reason.stack || reason.message : String(reason);
54
- try {
55
- require("fs").appendFileSync(require("path").join(require("os").homedir(), ".muonroi-cli", "crash.log"), `[${new Date().toISOString()}] REJECTION: ${msg}\n`);
70
+ appendCrashLog("REJECTION", msg);
71
+ // TUI mounted do NOT corrupt the framebuffer with console.error and do NOT
72
+ // exit. The rejection is logged; the renderer stays up so the user keeps
73
+ // their session (see slash-dispatch .catch handlers in app.tsx, which also
74
+ // surface the failure in-band).
75
+ if (_tuiActive) {
76
+ return;
56
77
  }
57
- catch { }
58
78
  if (reason instanceof Error) {
59
79
  console.error("Unhandled rejection:", reason.stack || reason.message);
60
80
  }
@@ -314,7 +334,7 @@ async function firstRunWizard(currentModel) {
314
334
  return null;
315
335
  }
316
336
  }
317
- async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi, sandboxMode, sandboxSettings, session, initialMessage, permissionMode = "safe", injectHalt = false) {
337
+ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi, session, initialMessage, permissionMode = "safe", injectHalt = false) {
318
338
  // ── Plan 00-07 boot order ──────────────────────────────────────────────────
319
339
  // 1. redactor.installGlobalPatches() — already at top of file (line 6).
320
340
  // 2. loadConfig + loadUsage (validates storage paths, logs usage cap state).
@@ -377,10 +397,11 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
377
397
  if (reconciled.abandoned > 0) {
378
398
  console.warn(`[muonroi-cli] reconciled ${reconciled.abandoned} abandoned tool calls from prior session`);
379
399
  }
400
+ const { warmMcpClients } = await import("./mcp/client-pool.js");
401
+ const { loadMcpServers } = await import("./utils/settings.js");
402
+ await warmMcpClients(loadMcpServers(), !session);
380
403
  const agent = new Agent(apiKey, baseURL, model, maxToolRounds, {
381
404
  session,
382
- sandboxMode,
383
- sandboxSettings,
384
405
  batchApi,
385
406
  abortContext: orchestratorAbort,
386
407
  pendingCalls,
@@ -404,6 +425,10 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
404
425
  alternateKeys: true,
405
426
  },
406
427
  });
428
+ // The TUI now owns the terminal — route fatal-handler behaviour away from
429
+ // process.exit(1) (see the unhandledRejection handler above). Cleared in
430
+ // restoreTerminalSync() the moment we begin tearing the terminal back down.
431
+ setTuiActive(true);
407
432
  /**
408
433
  * Restore terminal to main-screen mode before the process exits.
409
434
  *
@@ -418,6 +443,9 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
418
443
  * flush delay before process.exit().
419
444
  */
420
445
  function restoreTerminalSync() {
446
+ // Terminal is being handed back — restore fail-fast crash behaviour so a
447
+ // throw during/after teardown still exits cleanly instead of hanging.
448
+ setTuiActive(false);
421
449
  try {
422
450
  // 1. Disable Kitty keyboard protocol if enabled.
423
451
  if (typeof renderer.disableKittyKeyboard === "function") {
@@ -582,9 +610,9 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
582
610
  apiKey,
583
611
  baseURL,
584
612
  model: agent.getModel(),
613
+ sandboxMode: "off",
614
+ sandboxSettings: {},
585
615
  maxToolRounds,
586
- sandboxMode,
587
- sandboxSettings,
588
616
  version: packageJson.version,
589
617
  injectHalt,
590
618
  },
@@ -593,11 +621,9 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
593
621
  onRelaunch,
594
622
  }));
595
623
  }
596
- async function runHeadless(prompt, apiKey, baseURL, model, maxToolRounds, batchApi, sandboxMode, sandboxSettings, format, session, permissionMode = "safe", councilAutoAnswer) {
624
+ async function runHeadless(prompt, apiKey, baseURL, model, maxToolRounds, batchApi, format, session, permissionMode = "safe", councilAutoAnswer) {
597
625
  const agent = new Agent(apiKey, baseURL, model, maxToolRounds, {
598
626
  session,
599
- sandboxMode,
600
- sandboxSettings,
601
627
  batchApi,
602
628
  permissionMode,
603
629
  });
@@ -647,14 +673,20 @@ async function runHeadless(prompt, apiKey, baseURL, model, maxToolRounds, batchA
647
673
  writeSafe(process.stderr, tail.stderr ?? "");
648
674
  return;
649
675
  }
676
+ const textEmitter = createHeadlessTextEmitter();
650
677
  for await (const chunk of agent.processMessage(enhancedMessage)) {
651
678
  maybeAutoAnswer(chunk);
652
- const writes = renderHeadlessChunk(chunk);
679
+ const writes = textEmitter.consumeChunk(chunk);
653
680
  if (writes.stdout)
654
681
  writeSafe(process.stdout, writes.stdout);
655
682
  if (writes.stderr)
656
683
  writeSafe(process.stderr, writes.stderr);
657
684
  }
685
+ const textTail = textEmitter.flush();
686
+ if (textTail.stdout)
687
+ writeSafe(process.stdout, textTail.stdout);
688
+ if (textTail.stderr)
689
+ writeSafe(process.stderr, textTail.stderr ?? "");
658
690
  }
659
691
  finally {
660
692
  await agent.cleanup();
@@ -690,13 +722,6 @@ function stringOption(value) {
690
722
  function collect(value, prev) {
691
723
  return [...prev, value];
692
724
  }
693
- function resolveCliSandboxMode(value) {
694
- if (value === true)
695
- return "shuru";
696
- if (value === false)
697
- return "off";
698
- return undefined;
699
- }
700
725
  async function runBackgroundDelegation(jobPath, options) {
701
726
  let output = "";
702
727
  let agent;
@@ -724,12 +749,8 @@ async function runBackgroundDelegation(jobPath, options) {
724
749
  "or run 'muonroi-cli keys login <provider>' for subscription OAuth.");
725
750
  }
726
751
  const maxToolRounds = parseInt(stringOption(options.maxToolRounds) || String(delegation.maxToolRounds), 10) || delegation.maxToolRounds;
727
- const sandboxMode = resolveCliSandboxMode(options.sandbox) || delegation.sandboxMode || getCurrentSandboxMode();
728
- const sandboxSettings = mergeSandboxSettings(getCurrentSandboxSettings(), delegation.sandboxSettings);
729
752
  agent = new Agent(apiKey, baseURL, model, maxToolRounds, {
730
753
  persistSession: false,
731
- sandboxMode,
732
- sandboxSettings,
733
754
  batchApi: Boolean(delegation.batchApi ?? options.batchApi === true),
734
755
  });
735
756
  const result = await agent.runTaskRequest({
@@ -778,22 +799,7 @@ function resolveConfig(options) {
778
799
  // work; runaway loops abort with the "Reached max tool rounds" error
779
800
  // surface so the user can adjust scope. Override via --max-tool-rounds CLI
780
801
  // flag or MUONROI_MAX_TOOL_ROUNDS env.
781
- const maxToolRounds = parseInt(stringOption(options.maxToolRounds) || "12", 10) || 12;
782
- const sandboxMode = resolveCliSandboxMode(options.sandbox) || getCurrentSandboxMode();
783
- const cliOverrides = {};
784
- if (options.allowNet === true)
785
- cliOverrides.allowNet = true;
786
- const allowHostValue = options.allowHost;
787
- if (Array.isArray(allowHostValue) && allowHostValue.length > 0) {
788
- cliOverrides.allowedHosts = allowHostValue;
789
- if (!cliOverrides.allowNet)
790
- cliOverrides.allowNet = true;
791
- }
792
- const portValue = options.port;
793
- if (Array.isArray(portValue) && portValue.length > 0) {
794
- cliOverrides.ports = portValue;
795
- }
796
- const sandboxSettings = mergeSandboxSettings(getCurrentSandboxSettings(), cliOverrides);
802
+ const maxToolRounds = parseInt(stringOption(options.maxToolRounds) || "100", 10) || 100;
797
803
  if (typeof options.apiKey === "string" && process.env.MUONROI_TEST_NO_PERSIST !== "1") {
798
804
  // Persist to OS keychain (per-provider) instead of plaintext settings.json.
799
805
  // Fire-and-forget: keychain write is async; if it fails (no keytar), the key still
@@ -802,13 +808,7 @@ function resolveConfig(options) {
802
808
  }
803
809
  if (typeof options.model === "string")
804
810
  saveUserSettings({ defaultModel: normalizeModelId(options.model) });
805
- if (options.sandbox !== undefined) {
806
- const resolved = resolveCliSandboxMode(options.sandbox);
807
- if (resolved) {
808
- saveUserSettings({ sandboxMode: resolved });
809
- }
810
- }
811
- return { apiKey, baseURL, model, maxToolRounds, sandboxMode, sandboxSettings };
811
+ return { apiKey, baseURL, model, maxToolRounds };
812
812
  }
813
813
  function requireApiKey(apiKey) {
814
814
  if (!apiKey) {
@@ -835,11 +835,6 @@ program
835
835
  .option("-p, --prompt <prompt>", "Run a single prompt headlessly")
836
836
  .option("--verify", "Run the built-in verify flow headlessly")
837
837
  .option("--format <format>", "Headless output format: text or json", parseHeadlessOutputFormat, "text")
838
- .option("--sandbox", "Run agent shell commands inside a Shuru sandbox")
839
- .option("--no-sandbox", "Run agent shell commands directly on the host")
840
- .option("--allow-net", "Enable network access inside the Shuru sandbox")
841
- .option("--allow-host <pattern>", "Restrict sandbox network to specific hosts (repeatable)", collect, [])
842
- .option("--port <mapping>", "Forward a host port to sandbox guest (HOST:GUEST, repeatable)", collect, [])
843
838
  .option("-s, --session <id>", "Continue a saved session by id, or use 'latest'")
844
839
  .option("--background-task-file <path>", "Run a persisted background delegation")
845
840
  .option("--max-tool-rounds <n>", "Max tool execution rounds (ultimate runaway safety net)", "120")
@@ -1125,15 +1120,15 @@ program
1125
1120
  console.error(verifyError);
1126
1121
  process.exit(1);
1127
1122
  }
1128
- await runHeadless(buildVerifyPrompt(process.cwd(), config.sandboxMode), requireApiKey(config.apiKey), config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, config.sandboxMode, config.sandboxSettings, options.format, options.session, options.permission, councilAutoAnswer);
1123
+ await runHeadless(buildVerifyPrompt(process.cwd()), requireApiKey(config.apiKey), config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, options.format, options.session, options.permission, councilAutoAnswer);
1129
1124
  return;
1130
1125
  }
1131
1126
  if (options.prompt) {
1132
- await runHeadless(options.prompt, requireApiKey(config.apiKey), config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, config.sandboxMode, config.sandboxSettings, options.format, options.session, options.permission, councilAutoAnswer);
1127
+ await runHeadless(options.prompt, requireApiKey(config.apiKey), config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, options.format, options.session, options.permission, councilAutoAnswer);
1133
1128
  return;
1134
1129
  }
1135
1130
  const initialMessage = message.length > 0 ? message.join(" ") : undefined;
1136
- await startInteractive(config.apiKey, config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, config.sandboxMode, config.sandboxSettings, options.session, initialMessage, options.permission, options.injectHalt === true);
1131
+ await startInteractive(config.apiKey, config.baseURL, config.model, config.maxToolRounds, options.batchApi === true, options.session, initialMessage, options.permission, options.injectHalt === true);
1137
1132
  });
1138
1133
  program
1139
1134
  .command("models")
@@ -1257,7 +1252,7 @@ keys
1257
1252
  });
1258
1253
  keys
1259
1254
  .command("login <provider>")
1260
- .description("Log in to a provider via OAuth subscription (supported: openai, google, xai)")
1255
+ .description("Log in to a provider via OAuth subscription (supported: openai, google/agy, xai). 'google'/'agy' now uses agy client creds.")
1261
1256
  .action(async (provider) => {
1262
1257
  const { runKeysLogin } = await import("./cli/keys.js");
1263
1258
  await runKeysLogin(provider);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * src/lsp/client-normalize.test.ts
3
+ *
4
+ * Regression guard for the Windows diagnostics-cache key mismatch: the LSP
5
+ * client stores publishDiagnostics under normalizeUriPath(uri) (a LOWERCASE
6
+ * drive, decoded from `file:///c%3A/…`) but retrieves under
7
+ * normalizeFsPath(path.resolve(...)) (the cwd's UPPERCASE drive). Without
8
+ * canonicalizing the drive letter the keys never matched, so diagnostics were
9
+ * permanently [] on Windows — silently breaking write-time LSP feedback AND the
10
+ * commit gate. normalizeFsPath must produce the SAME key for both casings.
11
+ */
12
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * src/lsp/client-normalize.test.ts
3
+ *
4
+ * Regression guard for the Windows diagnostics-cache key mismatch: the LSP
5
+ * client stores publishDiagnostics under normalizeUriPath(uri) (a LOWERCASE
6
+ * drive, decoded from `file:///c%3A/…`) but retrieves under
7
+ * normalizeFsPath(path.resolve(...)) (the cwd's UPPERCASE drive). Without
8
+ * canonicalizing the drive letter the keys never matched, so diagnostics were
9
+ * permanently [] on Windows — silently breaking write-time LSP feedback AND the
10
+ * commit gate. normalizeFsPath must produce the SAME key for both casings.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import { normalizeFsPath } from "./client.js";
14
+ describe("normalizeFsPath", () => {
15
+ it("converts backslashes to forward slashes", () => {
16
+ expect(normalizeFsPath("C:\\Users\\x\\bug.ts")).toBe("c:/Users/x/bug.ts");
17
+ });
18
+ it("lowercases the Windows drive letter so store/retrieve keys converge", () => {
19
+ // The whole point: an UPPERCASE-drive query path and a LOWERCASE-drive
20
+ // publishDiagnostics URI path must normalize to the identical cache key.
21
+ expect(normalizeFsPath("C:/Users/x/bug.ts")).toBe(normalizeFsPath("c:/Users/x/bug.ts"));
22
+ expect(normalizeFsPath("C:/Users/x/bug.ts")).toBe("c:/Users/x/bug.ts");
23
+ expect(normalizeFsPath("D:\\proj\\a.ts")).toBe("d:/proj/a.ts");
24
+ });
25
+ it("leaves POSIX absolute paths untouched (no drive letter)", () => {
26
+ expect(normalizeFsPath("/home/u/bug.ts")).toBe("/home/u/bug.ts");
27
+ });
28
+ it("does not mangle a non-drive leading segment", () => {
29
+ // Only a single leading letter + ":/" is a drive; a path segment that
30
+ // merely starts with a letter must be preserved.
31
+ expect(normalizeFsPath("src/lsp/client.ts")).toBe("src/lsp/client.ts");
32
+ });
33
+ });
34
+ //# sourceMappingURL=client-normalize.test.js.map
@@ -18,3 +18,4 @@ export interface LspClientSession {
18
18
  stop(): Promise<void>;
19
19
  }
20
20
  export declare function createLspClientSession(options: LspClientOptions): Promise<LspClientSession>;
21
+ export declare function normalizeFsPath(filePath: string): string;
@@ -267,8 +267,16 @@ function normalizeUriPath(uri) {
267
267
  }
268
268
  return normalizeFsPath(uri);
269
269
  }
270
- function normalizeFsPath(filePath) {
271
- return filePath.replace(/\\/g, "/");
270
+ export function normalizeFsPath(filePath) {
271
+ const slashed = filePath.replace(/\\/g, "/");
272
+ // Windows drive letters are case-insensitive, but the two sides of the
273
+ // diagnostics cache disagree on case: publishDiagnostics URIs decode to a
274
+ // LOWERCASE drive (tsserver emits `file:///c%3A/…`), while query paths come
275
+ // from `path.resolve(cwd, …)` which yields the cwd's (usually UPPERCASE)
276
+ // drive. Without canonicalizing the drive, diagnostics stored under `c:/…`
277
+ // are never found under `C:/…` → the cache always returns [] on Windows
278
+ // (silently breaking both write-time LSP feedback and the commit gate).
279
+ return slashed.replace(/^([a-zA-Z]):\//, (_m, d) => `${d.toLowerCase()}:/`);
272
280
  }
273
281
  function ensureActive(stopped, serverId) {
274
282
  if (stopped) {
@@ -11,7 +11,7 @@ interface WorkspaceLspManagerOptions {
11
11
  }
12
12
  export interface WorkspaceLspManager {
13
13
  touchFile(filePath: string, waitForDiagnostics?: boolean): Promise<LspDiagnosticFile[]>;
14
- syncFile(filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean): Promise<LspDiagnosticFile[]>;
14
+ syncFile(filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean, diagnosticsTimeoutMs?: number): Promise<LspDiagnosticFile[]>;
15
15
  query(input: LspQueryInput): Promise<LspToolResponse>;
16
16
  close(): Promise<void>;
17
17
  }
@@ -67,7 +67,7 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
67
67
  const content = await readFile(filePath, "utf8");
68
68
  return syncFile(filePath, content, false, waitForDiagnostics);
69
69
  }
70
- async function syncFile(filePath, content, save = true, waitForDiagnostics = true) {
70
+ async function syncFile(filePath, content, save = true, waitForDiagnostics = true, diagnosticsTimeoutMs) {
71
71
  const records = await getClientsForFile(filePath);
72
72
  if (records.length === 0)
73
73
  return [];
@@ -80,7 +80,7 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
80
80
  await client.saveFile(filePath);
81
81
  }
82
82
  if (waitForDiagnostics) {
83
- await client.waitForDiagnostics(filePath);
83
+ await client.waitForDiagnostics(filePath, diagnosticsTimeoutMs);
84
84
  }
85
85
  return {
86
86
  filePath,
@@ -60,7 +60,7 @@ describe("createWorkspaceLspManager", () => {
60
60
  expect(result.success).toBe(true);
61
61
  expect(result.output).toContain("file:///demo.ts");
62
62
  expect(client.openOrChangeFile).toHaveBeenCalledWith(filePath, "typescript", "const demo = 1;\n");
63
- expect(client.waitForDiagnostics).toHaveBeenCalledWith(filePath);
63
+ expect(client.waitForDiagnostics).toHaveBeenCalledWith(filePath, undefined);
64
64
  await manager.close();
65
65
  expect(client.stop).toHaveBeenCalled();
66
66
  });
@@ -92,7 +92,7 @@ describe("createWorkspaceLspManager", () => {
92
92
  const result = await manager.syncFile(filePath, "const broken = true;\n", true, true);
93
93
  expect(result).toEqual(diagnostics);
94
94
  expect(client.saveFile).toHaveBeenCalledWith(filePath);
95
- expect(client.waitForDiagnostics).toHaveBeenCalledWith(filePath);
95
+ expect(client.waitForDiagnostics).toHaveBeenCalledWith(filePath, undefined);
96
96
  await manager.close();
97
97
  });
98
98
  it("times out a hanging request without dropping the client", async () => {
@@ -1,6 +1,6 @@
1
1
  import type { LspDiagnosticFile, LspQueryInput, LspToolResponse } from "./types.js";
2
2
  export declare function queryLsp(cwd: string, input: LspQueryInput): Promise<LspToolResponse>;
3
- export declare function syncFileWithLsp(cwd: string, filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean): Promise<LspDiagnosticFile[]>;
3
+ export declare function syncFileWithLsp(cwd: string, filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean, diagnosticsTimeoutMs?: number): Promise<LspDiagnosticFile[]>;
4
4
  export declare function isLspToolEnabled(_cwd: string): boolean;
5
5
  export declare function summarizeDiagnostics(diagnostics: LspDiagnosticFile[]): string | null;
6
6
  export declare function shutdownWorkspaceLspManager(cwd: string): Promise<void>;
@@ -10,9 +10,9 @@ export async function queryLsp(cwd, input) {
10
10
  filePath: path.isAbsolute(input.filePath) ? input.filePath : path.resolve(cwd, input.filePath),
11
11
  });
12
12
  }
13
- export async function syncFileWithLsp(cwd, filePath, content, save = true, waitForDiagnostics = true) {
13
+ export async function syncFileWithLsp(cwd, filePath, content, save = true, waitForDiagnostics = true, diagnosticsTimeoutMs) {
14
14
  const manager = getOrCreateManager(cwd);
15
- return manager.syncFile(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), content, save, waitForDiagnostics);
15
+ return manager.syncFile(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), content, save, waitForDiagnostics, diagnosticsTimeoutMs);
16
16
  }
17
17
  export function isLspToolEnabled(_cwd) {
18
18
  const settings = getCurrentLspSettings();
@@ -12,9 +12,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
12
  // ── Mocks ──────────────────────────────────────────────────────────────────
13
13
  // vi.hoisted ensures the factory variable is available when vi.mock is hoisted.
14
14
  const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
15
- vi.mock("node:child_process", () => ({
16
- execFile: mockExecFile,
17
- }));
15
+ vi.mock("node:child_process", async () => {
16
+ const actual = await vi.importActual("node:child_process");
17
+ return {
18
+ ...actual,
19
+ execFile: mockExecFile,
20
+ };
21
+ });
18
22
  vi.mock("node:fs", async (importOriginal) => {
19
23
  const actual = (await importOriginal());
20
24
  return {
@@ -11,9 +11,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
11
  // ── Mocks ──────────────────────────────────────────────────────────────────
12
12
  // vi.hoisted ensures the factory variable is available when vi.mock is hoisted.
13
13
  const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
14
- vi.mock("node:child_process", () => ({
15
- execFile: mockExecFile,
16
- }));
14
+ vi.mock("node:child_process", async () => {
15
+ const actual = await vi.importActual("node:child_process");
16
+ return {
17
+ ...actual,
18
+ execFile: mockExecFile,
19
+ };
20
+ });
17
21
  vi.mock("../../council/leader.js", () => ({
18
22
  pickCouncilTaskModel: vi.fn((_task, leaderId) => leaderId),
19
23
  }));
@@ -54,7 +54,7 @@ describe("acquireMcpTools — cross-turn client pool", () => {
54
54
  client: { close: async () => { } },
55
55
  }));
56
56
  const b1 = await acquireMcpTools([srv("fs")]);
57
- await expect(b1.tools["mcp_fs__boom"].execute({}, {})).rejects.toThrow(/transport closed/);
57
+ await expect(b1.tools.mcp_fs__boom.execute({}, {})).rejects.toThrow(/transport closed/);
58
58
  // Initial connect + exactly ONE in-turn reconnect — the retry is not looped.
59
59
  expect(connectOneServer).toHaveBeenCalledTimes(2);
60
60
  });
@@ -77,7 +77,7 @@ describe("acquireMcpTools — cross-turn client pool", () => {
77
77
  };
78
78
  });
79
79
  const b = await acquireMcpTools([srv("docs")]);
80
- const result = await b.tools["mcp_docs__ping"].execute({}, {});
80
+ const result = await b.tools.mcp_docs__ping.execute({}, {});
81
81
  expect(result).toBe("pong"); // recovered within the SAME turn
82
82
  expect(connectOneServer).toHaveBeenCalledTimes(2); // drop + one reconnect
83
83
  });
@@ -104,7 +104,7 @@ describe("acquireMcpTools — cross-turn client pool", () => {
104
104
  };
105
105
  });
106
106
  const b = await acquireMcpTools([srv("docs")]);
107
- const tool = b.tools["mcp_docs__ping"];
107
+ const tool = b.tools.mcp_docs__ping;
108
108
  const results = await Promise.all(Array.from({ length: 14 }, () => tool.execute({}, {})));
109
109
  expect(results.every((r) => r === "pong")).toBe(true);
110
110
  expect(connectOneServer).toHaveBeenCalledTimes(2); // 14 failures → exactly ONE shared reconnect