muonroi-cli 1.7.1 → 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 (328) 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 +143 -104
  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.d.ts +6 -0
  253. package/dist/src/storage/sessions.js +49 -42
  254. package/dist/src/storage/transcript-view.js +7 -1
  255. package/dist/src/storage/transcript.d.ts +1 -0
  256. package/dist/src/storage/transcript.js +55 -0
  257. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  258. package/dist/src/storage/usage.js +14 -14
  259. package/dist/src/storage/workspaces.js +12 -12
  260. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  261. package/dist/src/tools/bash.d.ts +36 -23
  262. package/dist/src/tools/bash.js +59 -301
  263. package/dist/src/tools/bash.test.js +2 -257
  264. package/dist/src/tools/file-tracker.d.ts +5 -0
  265. package/dist/src/tools/file-tracker.js +14 -0
  266. package/dist/src/tools/file.js +2 -0
  267. package/dist/src/tools/git-safety.d.ts +38 -1
  268. package/dist/src/tools/git-safety.js +94 -4
  269. package/dist/src/tools/git-safety.test.js +45 -1
  270. package/dist/src/tools/native-tools.d.ts +1 -1
  271. package/dist/src/tools/native-tools.js +56 -0
  272. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  273. package/dist/src/tools/registry-git-safety.test.js +17 -6
  274. package/dist/src/tools/registry.d.ts +5 -0
  275. package/dist/src/tools/registry.js +185 -11
  276. package/dist/src/types/index.d.ts +7 -1
  277. package/dist/src/ui/app.d.ts +2 -1
  278. package/dist/src/ui/app.js +0 -0
  279. package/dist/src/ui/components/council-question-card.js +1 -0
  280. package/dist/src/ui/components/message-view.js +37 -3
  281. package/dist/src/ui/components/structured-response-view.js +7 -0
  282. package/dist/src/ui/components/task-list-panel.js +3 -12
  283. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  284. package/dist/src/ui/containers/chat-feed.js +66 -0
  285. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  286. package/dist/src/ui/containers/modals-layer.js +18 -0
  287. package/dist/src/ui/modals/session-picker-modal.js +14 -9
  288. package/dist/src/ui/modals/update-modal.js +2 -1
  289. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  290. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  291. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  292. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  293. package/dist/src/ui/slash/council-inspect.js +4 -4
  294. package/dist/src/ui/slash/export.js +16 -1
  295. package/dist/src/ui/slash/menu-items.js +1 -0
  296. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  297. package/dist/src/ui/slash/ponytail.js +19 -0
  298. package/dist/src/ui/status-bar/store.js +0 -1
  299. package/dist/src/ui/status-bar/store.test.js +2 -2
  300. package/dist/src/ui/types.d.ts +7 -0
  301. package/dist/src/ui/use-app-logic.d.ts +26 -0
  302. package/dist/src/ui/use-app-logic.js +0 -0
  303. package/dist/src/ui/utils/relaunch.d.ts +17 -0
  304. package/dist/src/ui/utils/relaunch.js +28 -4
  305. package/dist/src/ui/utils/relaunch.test.js +50 -0
  306. package/dist/src/ui/utils/text.d.ts +11 -0
  307. package/dist/src/ui/utils/text.js +21 -1
  308. package/dist/src/ui/utils/text.test.js +25 -1
  309. package/dist/src/ui/utils/tools.js +3 -1
  310. package/dist/src/usage/cost-log.js +2 -2
  311. package/dist/src/usage/estimator.d.ts +14 -0
  312. package/dist/src/usage/estimator.js +22 -0
  313. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  314. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  315. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  316. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  317. package/dist/src/utils/clipboard-image.js +23 -23
  318. package/dist/src/utils/llm-deadline.d.ts +9 -1
  319. package/dist/src/utils/llm-deadline.js +31 -2
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +27 -31
  324. package/dist/src/utils/settings.js +100 -109
  325. package/dist/src/utils/side-question.js +2 -2
  326. package/dist/src/utils/skills.js +3 -3
  327. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  328. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  * const calls = handle.calls; // LanguageModelV3CallOptions[]
23
23
  * expect(calls[0].maxOutputTokens).toBeUndefined();
24
24
  */
25
+ import { APICallError } from "@ai-sdk/provider";
25
26
  import { simulateReadableStream } from "ai";
26
27
  import { MockLanguageModelV3 } from "ai/test";
27
28
  function isNestedArray(v) {
@@ -71,7 +72,7 @@ export function createMockModel(fx) {
71
72
  const text = generates.length > 0 ? generates[Math.min(genIdx, generates.length - 1)] : "{}";
72
73
  genIdx += 1;
73
74
  if (process.env.MUONROI_DEBUG_MOCK_MODEL === "1") {
74
- process.stderr.write(`[mock-model] doGenerate #${genIdx} → ${text.length} chars\n`);
75
+ process.stderr.write(`[mock-model] doGenerate #${genIdx} → ${text.length} chars\nCALLER:\n${new Error().stack}\n`);
75
76
  }
76
77
  return {
77
78
  content: [{ type: "text", text }],
@@ -201,12 +202,40 @@ function normalizeStreamChunks(chunks) {
201
202
  return chunks.map((c) => {
202
203
  if (c && c.type === "error") {
203
204
  const raw = c.error;
204
- const err = raw instanceof Error ? raw : new Error(typeof raw === "string" ? raw : "mock LLM error");
205
- return { ...c, error: err };
205
+ return { ...c, error: buildFixtureError(raw) };
206
206
  }
207
207
  return c;
208
208
  });
209
209
  }
210
+ /**
211
+ * Turn a fixture `error` payload into the right runtime shape:
212
+ * - an `APICallError` when the payload is an object carrying a `statusCode`
213
+ * (or `apiCallError: true`), so harness specs can exercise the orchestrator's
214
+ * status-aware paths (humanizeApiError 5xx canned text, summarizeApiErrorForLog
215
+ * forensics, retry classifier). `APICallError.isInstance` duck-types on a
216
+ * shared marker, so constructing it here satisfies the `isInstance` checks in
217
+ * error-utils.ts. A pure JSON fixture cannot otherwise build one (it would
218
+ * arrive as a plain string).
219
+ * - a plain `Error` for string payloads (the common transient-message case).
220
+ */
221
+ function buildFixtureError(raw) {
222
+ if (raw instanceof Error)
223
+ return raw;
224
+ if (raw && typeof raw === "object") {
225
+ const o = raw;
226
+ if (o.apiCallError === true || typeof o.statusCode === "number") {
227
+ return new APICallError({
228
+ message: o.message ?? `mock API error${o.statusCode ? ` (HTTP ${o.statusCode})` : ""}`,
229
+ url: o.url ?? "https://mock.invalid/v1/chat/completions",
230
+ requestBodyValues: {},
231
+ statusCode: o.statusCode,
232
+ responseBody: o.responseBody,
233
+ isRetryable: o.isRetryable,
234
+ });
235
+ }
236
+ }
237
+ return new Error(typeof raw === "string" ? raw : "mock LLM error");
238
+ }
210
239
  export async function loadMockModelFromDir(dir) {
211
240
  // Diagnostic logging path. Without these messages, every failure mode
212
241
  // (directory missing, no .json files, malformed JSON, no `model` block)
@@ -131,7 +131,9 @@ async function spawnWindows(args, opts) {
131
131
  await waitForHandshake(outSocket, timeoutMs);
132
132
  }
133
133
  catch (err) {
134
- proc.kill();
134
+ if (proc && typeof proc.kill === "function") {
135
+ proc.kill();
136
+ }
135
137
  inServer.close();
136
138
  outServer.close();
137
139
  throw err;
@@ -86,7 +86,15 @@ export async function deleteChatSecret(id) {
86
86
  const kt = await loadKeytar();
87
87
  if (!kt?.deletePassword)
88
88
  return false;
89
- return kt.deletePassword(KEYCHAIN_SERVICE, ACCOUNT_BY_CHAT[id]);
89
+ try {
90
+ return await kt.deletePassword(KEYCHAIN_SERVICE, ACCOUNT_BY_CHAT[id]);
91
+ }
92
+ catch (err) {
93
+ if (process.env.DEBUG || process.env.MUONROI_DEBUG_KEYCHAIN) {
94
+ console.error(`[chat-keychain] deletePassword backend error for ${id}:`, err?.message || err);
95
+ }
96
+ return false;
97
+ }
90
98
  }
91
99
  /**
92
100
  * List all stored chat secret IDs.
@@ -43,21 +43,21 @@ export function collectCostForensics(sessionId) {
43
43
  if (!session)
44
44
  throw new Error(`Session not found: ${sessionId}`);
45
45
  const rows = db
46
- .prepare(`
47
- SELECT id, source, model, message_seq, input_tokens, output_tokens,
48
- cache_read_tokens, cache_creation_tokens, cost_micros, created_at,
49
- provider_options_shape
50
- FROM usage_events
51
- WHERE session_id = ?
52
- ORDER BY id ASC
46
+ .prepare(`
47
+ SELECT id, source, model, message_seq, input_tokens, output_tokens,
48
+ cache_read_tokens, cache_creation_tokens, cost_micros, created_at,
49
+ provider_options_shape
50
+ FROM usage_events
51
+ WHERE session_id = ?
52
+ ORDER BY id ASC
53
53
  `)
54
54
  .all(sessionId);
55
55
  const counts = db
56
- .prepare(`
57
- SELECT event_type, COUNT(*) AS c
58
- FROM interaction_logs
59
- WHERE session_id = ?
60
- GROUP BY event_type
56
+ .prepare(`
57
+ SELECT event_type, COUNT(*) AS c
58
+ FROM interaction_logs
59
+ WHERE session_id = ?
60
+ GROUP BY event_type
61
61
  `)
62
62
  .all(sessionId);
63
63
  const countMap = new Map(counts.map((r) => [r.event_type, r.c]));
@@ -15,8 +15,8 @@
15
15
  * keys delete <provider> — remove a stored key
16
16
  * keys import-bw [providers] — pull from Bitwarden vault, store in keychain
17
17
  * keys cleanup-settings — strip plaintext keys from user-settings.json
18
- * keys login <provider> — OAuth login (openai, google)
19
- * keys logout <provider> — OAuth logout (openai, google)
18
+ * keys login <provider> — OAuth login (openai, google/agy)
19
+ * keys logout <provider> — OAuth logout (openai, google/agy)
20
20
  */
21
21
  import { type ChatSecretId } from "../chat/chat-keychain.js";
22
22
  export interface KeysSetOptions {
@@ -15,8 +15,8 @@
15
15
  * keys delete <provider> — remove a stored key
16
16
  * keys import-bw [providers] — pull from Bitwarden vault, store in keychain
17
17
  * keys cleanup-settings — strip plaintext keys from user-settings.json
18
- * keys login <provider> — OAuth login (openai, google)
19
- * keys logout <provider> — OAuth logout (openai, google)
18
+ * keys login <provider> — OAuth login (openai, google/agy)
19
+ * keys logout <provider> — OAuth logout (openai, google/agy)
20
20
  */
21
21
  import { spawnSync } from "node:child_process";
22
22
  import { promises as fs } from "node:fs";
@@ -49,8 +49,17 @@ function maskKey(key) {
49
49
  return "***";
50
50
  return `${key.slice(0, 6)}…${key.slice(-4)}`;
51
51
  }
52
- function isValidProvider(p) {
53
- return KEYCHAIN_PROVIDER_IDS.includes(p);
52
+ function normalizeProvider(p) {
53
+ const lower = p.toLowerCase();
54
+ if (lower === "agy")
55
+ return "google"; // agy is alias for google (OAuth + key)
56
+ if (KEYCHAIN_PROVIDER_IDS.includes(lower)) {
57
+ return lower;
58
+ }
59
+ return null;
60
+ }
61
+ function _isValidProvider(p) {
62
+ return normalizeProvider(p) !== null;
54
63
  }
55
64
  async function promptHidden(question) {
56
65
  // ASCII control codes used during raw-mode capture.
@@ -104,8 +113,9 @@ async function promptHidden(question) {
104
113
  });
105
114
  }
106
115
  export async function runKeysSet(provider, options = {}) {
107
- if (!isValidProvider(provider)) {
108
- console.error(`Unknown provider '${provider}'. Valid: ${KEYCHAIN_PROVIDER_IDS.join(", ")}`);
116
+ const norm = normalizeProvider(provider);
117
+ if (!norm) {
118
+ console.error(`Unknown provider '${provider}'. Valid: ${KEYCHAIN_PROVIDER_IDS.join(", ")} (agy is alias for google)`);
109
119
  process.exit(1);
110
120
  }
111
121
  const key = (await promptHidden(`Paste ${provider} API key (hidden): `)).trim();
@@ -124,7 +134,7 @@ export async function runKeysSet(provider, options = {}) {
124
134
  console.log(`Bitwarden vault: ${res.action} '${itemName}'.`);
125
135
  }
126
136
  try {
127
- const ok = await setKeyForProvider(provider, key);
137
+ const ok = await setKeyForProvider(norm, key);
128
138
  if (!ok) {
129
139
  console.error("OS keychain unavailable (keytar failed to load or secret service backend unavailable).");
130
140
  console.error("Common on Linux: install the runtime library, e.g.");
@@ -184,13 +194,35 @@ export async function runKeysList() {
184
194
  const chatStored = await listChatSecrets();
185
195
  // Load OAuth tokens for supported providers
186
196
  const { loadTokens } = await import("../providers/auth/token-store.js");
197
+ const { getOAuthProviderConfig } = await import("../providers/auth/registry.js");
187
198
  const oauthRows = [];
188
199
  const oauthIds = await getOAuthProviderIds();
189
200
  for (const p of oauthIds) {
190
201
  try {
191
- const tokens = await loadTokens(p);
202
+ let tokens = await loadTokens(p);
203
+ if (!tokens && p === "google") {
204
+ // Import from agy storage only for fresh (non-expired) tokens.
205
+ // Stale agy tokens should be cleaned by running `keys logout google` + removing old .gemini file.
206
+ const { loadAgyTokensWithRefresh } = await import("../providers/auth/gemini-oauth.js");
207
+ const agyTokens = await loadAgyTokensWithRefresh();
208
+ if (agyTokens && Date.now() < agyTokens.expiresAt) {
209
+ tokens = agyTokens;
210
+ }
211
+ }
192
212
  if (tokens) {
193
- oauthRows.push({ provider: p, email: tokens.email, expiresAt: tokens.expiresAt });
213
+ if (Date.now() < tokens.expiresAt) {
214
+ const cfg = await getOAuthProviderConfig(p);
215
+ const display = cfg?.displayName ?? p;
216
+ oauthRows.push({ provider: p, display, email: tokens.email, expiresAt: tokens.expiresAt });
217
+ }
218
+ else if (p === "google") {
219
+ // auto-clean stale Agy token from our store (imported from old ~/.gemini)
220
+ try {
221
+ const { deleteTokens } = await import("../providers/auth/token-store.js");
222
+ await deleteTokens("google");
223
+ }
224
+ catch { }
225
+ }
194
226
  }
195
227
  }
196
228
  catch {
@@ -202,7 +234,7 @@ export async function runKeysList() {
202
234
  console.log("No keys stored in OS keychain.");
203
235
  console.log("Run 'muonroi-cli keys set <provider>' or 'muonroi-cli keys import-bw' to add some.");
204
236
  console.log("Run 'muonroi-cli keys login openai' to log in with your OpenAI subscription.");
205
- console.log("Run 'muonroi-cli keys login google' to log in with your Google account.");
237
+ console.log("Run 'muonroi-cli keys login google' or 'keys login agy' to log in with Agy (Google OAuth).");
206
238
  console.log("Run 'muonroi-cli keys login xai' to log in with your SuperGrok / X Premium+ subscription.");
207
239
  return;
208
240
  }
@@ -215,7 +247,7 @@ export async function runKeysList() {
215
247
  const account = row.email ?? "(no email)";
216
248
  const expiry = new Date(row.expiresAt).toLocaleString();
217
249
  const expired = Date.now() > row.expiresAt ? " [EXPIRED]" : "";
218
- console.log(`${row.provider.padEnd(12)} ${account.padEnd(26)} ${expiry}${expired}`);
250
+ console.log(`${row.display.padEnd(12)} ${account.padEnd(26)} ${expiry}${expired}`);
219
251
  }
220
252
  console.log("");
221
253
  }
@@ -254,12 +286,37 @@ export async function runKeysList() {
254
286
  }
255
287
  }
256
288
  export async function runKeysDelete(provider) {
257
- if (!isValidProvider(provider)) {
258
- console.error(`Unknown provider '${provider}'. Valid: ${KEYCHAIN_PROVIDER_IDS.join(", ")}`);
289
+ const norm = normalizeProvider(provider);
290
+ if (!norm) {
291
+ console.error(`Unknown provider '${provider}'. Valid: ${KEYCHAIN_PROVIDER_IDS.join(", ")} (agy is alias for google)`);
259
292
  process.exit(1);
260
293
  }
261
- const ok = await deleteKeyForProvider(provider);
294
+ const ok = await deleteKeyForProvider(norm);
262
295
  console.log(ok ? `Deleted ${provider} key from keychain.` : `No ${provider} key was stored.`);
296
+ // If deleting google/agy, also clean the OAuth token (Agy uses the google slot for its OAuth)
297
+ if (norm === "google") {
298
+ try {
299
+ const { loadTokens, deleteTokens } = await import("../providers/auth/token-store.js");
300
+ const { getOAuthProviderConfig } = await import("../providers/auth/registry.js");
301
+ const tokens = await loadTokens("google");
302
+ if (tokens) {
303
+ const cfg = await getOAuthProviderConfig("google");
304
+ if (cfg?.provider) {
305
+ try {
306
+ await cfg.provider.revoke(tokens);
307
+ }
308
+ catch {
309
+ // best effort
310
+ }
311
+ }
312
+ await deleteTokens("google");
313
+ console.log(`Also cleared Agy/Google OAuth token.`);
314
+ }
315
+ }
316
+ catch {
317
+ // ignore secondary errors
318
+ }
319
+ }
263
320
  }
264
321
  export async function runKeysExport(filePath) {
265
322
  const providers = {};
@@ -330,13 +387,14 @@ export async function runKeysImport(filePath) {
330
387
  let imported = 0;
331
388
  let skipped = 0;
332
389
  for (const [provider, key] of entries) {
333
- if (!isValidProvider(provider)) {
390
+ const norm = normalizeProvider(provider);
391
+ if (!norm) {
334
392
  console.warn(`Skipping unknown provider '${provider}' in bundle.`);
335
393
  skipped++;
336
394
  continue;
337
395
  }
338
396
  try {
339
- const ok = await setKeyForProvider(provider, key);
397
+ const ok = await setKeyForProvider(norm, key);
340
398
  if (ok) {
341
399
  imported++;
342
400
  console.log(`✓ ${provider} → keychain (${maskKey(key)})`);
@@ -388,7 +446,8 @@ export async function runKeysImportBw(opts = {}) {
388
446
  let imported = 0;
389
447
  let skipped = 0;
390
448
  for (const provider of requested) {
391
- if (!isValidProvider(provider)) {
449
+ const norm = normalizeProvider(provider);
450
+ if (!norm) {
392
451
  console.warn(`Skip unknown provider: ${provider}`);
393
452
  skipped++;
394
453
  continue;
@@ -407,7 +466,7 @@ export async function runKeysImportBw(opts = {}) {
407
466
  continue;
408
467
  }
409
468
  try {
410
- const ok = await setKeyForProvider(provider, key);
469
+ const ok = await setKeyForProvider(norm, key);
411
470
  if (!ok) {
412
471
  console.warn(`! ${provider} — OS keychain unavailable (keytar or secret service backend).`);
413
472
  console.warn(" Linux fix: sudo dnf install libsecret (Fedora) or sudo apt-get install libsecret-1-0 (Ubuntu)");
@@ -620,18 +679,36 @@ export async function runChatImportBw(opts = {}) {
620
679
  */
621
680
  export async function runKeysLogin(provider) {
622
681
  const oauthIds = await getOAuthProviderIds();
623
- if (!oauthIds.includes(provider)) {
624
- console.error(`OAuth login not supported for '${provider}'. Supported: ${oauthIds.join(", ")}`);
682
+ const norm = provider.toLowerCase() === "agy" ? "google" : provider;
683
+ if (!oauthIds.includes(norm)) {
684
+ const supported = oauthIds.map((id) => (id === "google" ? "google (or agy)" : id)).join(", ");
685
+ console.error(`OAuth login not supported for '${provider}'. Supported: ${supported}`);
625
686
  process.exit(1);
626
687
  }
627
688
  const { getOAuthProviderConfig } = await import("../providers/auth/registry.js");
628
689
  const { saveTokens } = await import("../providers/auth/token-store.js");
629
- const cfg = await getOAuthProviderConfig(provider);
690
+ const cfg = await getOAuthProviderConfig(norm);
630
691
  if (!cfg) {
631
692
  console.error(`OAuth provider '${provider}' is registered but failed to load.`);
632
693
  process.exit(1);
633
694
  }
634
695
  const name = cfg.displayName;
696
+ // Special handling for google: prefer importing/reusing token from agy local config
697
+ // (~/.gemini/oauth_creds.json) so existing agy login "just works" without browser.
698
+ if (provider === "google") {
699
+ const { loadAgyTokensWithRefresh } = await import("../providers/auth/gemini-oauth.js");
700
+ const existing = await loadAgyTokensWithRefresh();
701
+ if (existing?.accessToken) {
702
+ console.log(`Found existing agy token for ${existing.email || "(no email)"}.`);
703
+ console.log("Reusing it (refreshed if near expiry). No browser needed.");
704
+ const expiry = new Date(existing.expiresAt).toLocaleString();
705
+ const isExpired = Date.now() > existing.expiresAt;
706
+ console.log(`Token expires: ${expiry}${isExpired ? " (was expired, refresh attempted)" : ""}`);
707
+ console.log("Run 'muonroi-cli keys list' to verify.");
708
+ return;
709
+ }
710
+ // No existing agy token — fall through to normal browser flow (using agy client)
711
+ }
635
712
  console.log(`Logging in to ${name} via OAuth...`);
636
713
  console.log("A browser window will open. Complete the sign-in to continue.");
637
714
  const tokens = await cfg.provider.login({
@@ -646,12 +723,12 @@ export async function runKeysLogin(provider) {
646
723
  console.log(` User code: ${codeOrUrl}`);
647
724
  }
648
725
  console.log("\n Waiting for authorization...");
649
- if (provider === "xai") {
726
+ if (norm === "xai") {
650
727
  console.log(" (If the xAI page shows a code instead of redirecting, paste it when the terminal prompts.)");
651
728
  }
652
729
  },
653
730
  });
654
- await saveTokens(provider, tokens);
731
+ await saveTokens(norm, tokens);
655
732
  const emailDisplay = tokens.email ? ` (${tokens.email})` : "";
656
733
  const expiry = new Date(tokens.expiresAt).toLocaleString();
657
734
  console.log(`\nLogged in to ${name}${emailDisplay}. Token expires: ${expiry}`);
@@ -665,22 +742,24 @@ export async function runKeysLogin(provider) {
665
742
  */
666
743
  export async function runKeysLogout(provider) {
667
744
  const oauthIds = await getOAuthProviderIds();
668
- if (!oauthIds.includes(provider)) {
669
- console.error(`OAuth logout not supported for '${provider}'. Supported: ${oauthIds.join(", ")}`);
745
+ const norm = provider.toLowerCase() === "agy" ? "google" : provider;
746
+ if (!oauthIds.includes(norm)) {
747
+ const supported = oauthIds.map((id) => (id === "google" ? "google (or agy)" : id)).join(", ");
748
+ console.error(`OAuth logout not supported for '${provider}'. Supported: ${supported}`);
670
749
  process.exit(1);
671
750
  }
672
751
  const { getOAuthProviderConfig } = await import("../providers/auth/registry.js");
673
752
  const { loadTokens, deleteTokens } = await import("../providers/auth/token-store.js");
674
- const tokens = await loadTokens(provider);
753
+ const tokens = await loadTokens(norm);
675
754
  if (!tokens) {
676
755
  console.log(`No OAuth tokens stored for '${provider}'.`);
677
756
  return;
678
757
  }
679
- const cfg = await getOAuthProviderConfig(provider);
758
+ const cfg = await getOAuthProviderConfig(norm);
680
759
  if (cfg) {
681
760
  await cfg.provider.revoke(tokens); // best-effort
682
761
  }
683
- await deleteTokens(provider);
762
+ await deleteTokens(norm);
684
763
  const name = cfg?.displayName ?? provider;
685
764
  console.log(`Logged out of ${name}. OAuth tokens revoked and deleted.`);
686
765
  }
@@ -8,8 +8,8 @@ import { promises as fs } from "node:fs";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { readCostLog } from "../usage/cost-log.js";
11
- import { readProductLedger } from "../usage/product-ledger.js";
12
11
  import { listDecisionLogDates, readDecisionLog } from "../usage/decision-log.js";
12
+ import { readProductLedger } from "../usage/product-ledger.js";
13
13
  function muonroiHome(homeOverride) {
14
14
  return homeOverride ?? process.env.MUONROI_CLI_HOME ?? path.join(os.homedir(), ".muonroi-cli");
15
15
  }
@@ -284,7 +284,7 @@ export async function runSecurityAudit(opts = {}) {
284
284
  const highRiskCmds = overrides
285
285
  .map((d) => {
286
286
  const ctx = d.meta?.context || {};
287
- let cmd = ctx.command ? String(ctx.command) : (ctx.shuru ? "[shuru]" : "");
287
+ let cmd = ctx.command ? String(ctx.command) : ctx.shuru ? "[shuru]" : "";
288
288
  if (cmd && cmd !== "[shuru]") {
289
289
  cmd = cmd.replace(/((?:key|token|secret|pwd|pass|auth|AWS_)[^=]*=)[^\s]+/gi, "$1[REDACTED]");
290
290
  cmd = cmd.replace(/https?:\/\/\S*?(?:key|token|secret)=\S+/gi, "[REDACTED_URL]");
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Abort-signal threading contract for the council subsystem.
3
+ *
4
+ * Bug this guards: a user-abort signal that reaches `runCouncil(options.signal)`
5
+ * was never forwarded to the `generate`-based call path (clarify, research-need,
6
+ * leader-eval, opening statements, round summary, synthesis, debate planning).
7
+ * `llm.generate` had no `signal` parameter at all and `withTimeoutSignal(undefined,…)`
8
+ * was hard-coded — so pressing Esc/Ctrl-C mid-council did nothing and the run
9
+ * continued making LLM calls to completion.
10
+ *
11
+ * The fix:
12
+ * 1. `CouncilLLM.generate` accepts a trailing `signal?` and threads it into the
13
+ * SDK call via `withTimeoutSignal(signal, …)`.
14
+ * 2. `runCouncil` wraps the injected `llm` with `withCouncilSignal(llm, signal)`
15
+ * so EVERY generate call site (none of which pass a signal) inherits it.
16
+ * 3. `planDebate` forwards the signal to its direct `generateObject` attempt.
17
+ */
18
+ import { beforeEach, describe, expect, it, vi } from "vitest";
19
+ import { withCouncilSignal } from "../index.js";
20
+ /** Recording fake — captures the `signal` argument each method received. */
21
+ function recordingLlm(generateReply = () => "{}") {
22
+ const calls = [];
23
+ const llm = {
24
+ generate: async (_modelId, _system, prompt, _maxTokens, _onUsage, signal) => {
25
+ calls.push({ kind: "generate", signal });
26
+ return generateReply(prompt);
27
+ },
28
+ debate: async (_modelId, _system, _prompt, signal) => {
29
+ calls.push({ kind: "debate", signal });
30
+ return { text: "ok", toolCalls: [] };
31
+ },
32
+ research: async (_modelId, _topic, _ctx, signal) => {
33
+ calls.push({ kind: "research", signal });
34
+ return "findings";
35
+ },
36
+ };
37
+ return { llm, calls };
38
+ }
39
+ describe("withCouncilSignal — injects the council abort signal into generate", () => {
40
+ it("forwards the wrapper signal when the caller passes none", async () => {
41
+ const { llm, calls } = recordingLlm();
42
+ const ac = new AbortController();
43
+ const wrapped = withCouncilSignal(llm, ac.signal);
44
+ // tracedGenerate / openingWithRetry / judgeReadiness all call generate with
45
+ // <= 4 positional args — i.e. no signal. The wrapper must inject ours.
46
+ await wrapped.generate("m", "sys", "prompt", 256);
47
+ expect(calls).toHaveLength(1);
48
+ expect(calls[0].signal).toBe(ac.signal);
49
+ });
50
+ it("lets an explicit caller signal win over the injected one", async () => {
51
+ const { llm, calls } = recordingLlm();
52
+ const injected = new AbortController().signal;
53
+ const explicit = new AbortController().signal;
54
+ const wrapped = withCouncilSignal(llm, injected);
55
+ await wrapped.generate("m", "sys", "prompt", 256, undefined, explicit);
56
+ expect(calls[0].signal).toBe(explicit);
57
+ });
58
+ it("is a no-op passthrough when no signal is configured (sprint path)", () => {
59
+ const { llm } = recordingLlm();
60
+ expect(withCouncilSignal(llm, undefined)).toBe(llm);
61
+ });
62
+ });
63
+ describe("createCouncilLLM.generate — honours the parent abort signal", () => {
64
+ beforeEach(() => vi.resetModules());
65
+ it("propagates an already-aborted parent into the SDK abortSignal", async () => {
66
+ const captured = [];
67
+ vi.doMock("ai", () => ({
68
+ generateText: vi.fn().mockImplementation(async (args) => {
69
+ captured.push(args);
70
+ return { text: "done", usage: { inputTokens: 1, outputTokens: 1 }, finishReason: "stop" };
71
+ }),
72
+ stepCountIs: vi.fn().mockReturnValue({ __step: 1 }),
73
+ }));
74
+ vi.doMock("../../providers/keychain.js", () => ({
75
+ loadKeyForProvider: vi.fn().mockResolvedValue("test-key"),
76
+ }));
77
+ vi.doMock("../../providers/runtime.js", () => ({
78
+ detectProviderForModel: vi.fn().mockReturnValue("openai"),
79
+ createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
80
+ createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
81
+ resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
82
+ }));
83
+ const { createCouncilLLM } = await import("../llm.js");
84
+ const stats = { calls: 0, startMs: Date.now(), phases: [] };
85
+ const llm = createCouncilLLM({}, "agent", undefined, stats);
86
+ const ac = new AbortController();
87
+ ac.abort(new Error("user pressed Esc"));
88
+ await llm.generate("gpt-4o", "sys", "prompt", 256, undefined, ac.signal);
89
+ expect(captured).toHaveLength(1);
90
+ const sig = captured[0].abortSignal;
91
+ expect(sig).toBeDefined();
92
+ expect(sig?.aborted).toBe(true);
93
+ });
94
+ it("does NOT abort the SDK call when the parent is live", async () => {
95
+ const captured = [];
96
+ vi.doMock("ai", () => ({
97
+ generateText: vi.fn().mockImplementation(async (args) => {
98
+ captured.push(args);
99
+ return { text: "done", usage: { inputTokens: 1, outputTokens: 1 }, finishReason: "stop" };
100
+ }),
101
+ stepCountIs: vi.fn().mockReturnValue({ __step: 1 }),
102
+ }));
103
+ vi.doMock("../../providers/keychain.js", () => ({
104
+ loadKeyForProvider: vi.fn().mockResolvedValue("test-key"),
105
+ }));
106
+ vi.doMock("../../providers/runtime.js", () => ({
107
+ detectProviderForModel: vi.fn().mockReturnValue("openai"),
108
+ createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
109
+ createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
110
+ resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
111
+ }));
112
+ const { createCouncilLLM } = await import("../llm.js");
113
+ const stats = { calls: 0, startMs: Date.now(), phases: [] };
114
+ const llm = createCouncilLLM({}, "agent", undefined, stats);
115
+ const ac = new AbortController();
116
+ await llm.generate("gpt-4o", "sys", "prompt", 256, undefined, ac.signal);
117
+ const sig = captured[0].abortSignal;
118
+ expect(sig?.aborted).toBe(false);
119
+ });
120
+ });
121
+ describe("evaluateResearchNeed — generate call inherits the wrapped signal", () => {
122
+ it("records the council signal on the underlying generate call", async () => {
123
+ const { evaluateResearchNeed } = await import("../debate.js");
124
+ const { llm, calls } = recordingLlm(() => '{"needsResearch": false}');
125
+ const ac = new AbortController();
126
+ const wrapped = withCouncilSignal(llm, ac.signal);
127
+ const spec = {
128
+ problemStatement: "x",
129
+ constraints: [],
130
+ successCriteria: [],
131
+ scope: "",
132
+ rawQA: [],
133
+ };
134
+ const gen = evaluateResearchNeed(spec, "leader-model", "ctx", wrapped, false);
135
+ let step = await gen.next();
136
+ while (!step.done)
137
+ step = await gen.next();
138
+ const genCalls = calls.filter((c) => c.kind === "generate");
139
+ expect(genCalls.length).toBeGreaterThan(0);
140
+ for (const c of genCalls)
141
+ expect(c.signal).toBe(ac.signal);
142
+ });
143
+ });
144
+ describe("planDebate — forwards signal to the direct generateObject attempt", () => {
145
+ beforeEach(() => vi.resetModules());
146
+ it("passes an abort-linked signal to generateObject", async () => {
147
+ const captured = [];
148
+ vi.doMock("ai", () => ({
149
+ generateObject: vi.fn().mockImplementation(async (args) => {
150
+ captured.push(args);
151
+ return {
152
+ object: {
153
+ intentSummary: "s",
154
+ stances: [
155
+ { name: "A", lens: "l1" },
156
+ { name: "B", lens: "l2" },
157
+ ],
158
+ outputShape: { kind: "decision", sections: [{ key: "k", heading: "H", shape: "list" }], guardrails: [] },
159
+ },
160
+ };
161
+ }),
162
+ }));
163
+ vi.doMock("../../providers/keychain.js", () => ({
164
+ loadKeyForProvider: vi.fn().mockResolvedValue("test-key"),
165
+ }));
166
+ vi.doMock("../../providers/runtime.js", () => ({
167
+ detectProviderForModel: vi.fn().mockReturnValue("openai"),
168
+ createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
169
+ createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
170
+ resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
171
+ }));
172
+ const { planDebate } = await import("../debate-planner.js");
173
+ const spec = {
174
+ problemStatement: "x",
175
+ constraints: [],
176
+ successCriteria: [],
177
+ scope: "",
178
+ rawQA: [],
179
+ };
180
+ const ac = new AbortController();
181
+ ac.abort(new Error("user pressed Esc"));
182
+ const { llm } = recordingLlm();
183
+ const gen = planDebate(spec, "leader-model", llm, undefined, undefined, undefined, undefined, ac.signal);
184
+ let step = await gen.next();
185
+ while (!step.done)
186
+ step = await gen.next();
187
+ expect(captured).toHaveLength(1);
188
+ const sig = captured[0].abortSignal;
189
+ expect(sig).toBeDefined();
190
+ expect(sig?.aborted).toBe(true);
191
+ });
192
+ });
193
+ //# sourceMappingURL=abort-threading.test.js.map
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { buildClarificationPrompt, buildReadinessJudgePrompt } from "../prompts.js";
2
+ import { buildClarificationPrompt, buildReadinessJudgePrompt, buildSpecSynthesisPrompt } from "../prompts.js";
3
3
  // Guards the de-robotized askcard contract (2026-06-14). The council clarifier
4
4
  // used to (a) force "AT LEAST 2 questions" → rambling/over-asking, (b) tell the
5
5
  // model to OMIT a recommendation unless certain → unranked option lists, and
@@ -34,6 +34,26 @@ describe("buildClarificationPrompt — de-robotized askcard", () => {
34
34
  expect(system).toMatch(/Output ONLY a JSON array/);
35
35
  });
36
36
  });
37
+ // Issue A — language drift. buildSpecSynthesisPrompt had NO language rule, so
38
+ // DeepSeek defaulted the brief to English even for a Vietnamese topic. Because
39
+ // the final synthesis detects its output language FROM this problemStatement,
40
+ // an English brief dragged the whole council off the user's language. The fix
41
+ // adds a mandatory "use the user's language" rule. These lock it in.
42
+ describe("buildSpecSynthesisPrompt — user-language rule (issue A)", () => {
43
+ const { system } = buildSpecSynthesisPrompt("Xây dựng REST API quản lý todo", "", [
44
+ { question: "Auth?", answer: "Không cần" },
45
+ ]);
46
+ it("instructs writing every field in the user's language, not defaulting to English", () => {
47
+ expect(system).toMatch(/Language Rule/i);
48
+ expect(system).toMatch(/SAME[\s\S]*language the user used/i);
49
+ expect(system).toMatch(/Do NOT default to[\s\S]*English/i);
50
+ });
51
+ it("keeps the mock-router substring so council-e2e fixtures still match", () => {
52
+ // tests/council/council-e2e.ts routes the spec-synthesis mock on this exact
53
+ // phrase — changing it silently diverges the e2e flow. Guard it.
54
+ expect(system).toContain("synthesizing a discussion brief");
55
+ });
56
+ });
37
57
  describe("buildReadinessJudgePrompt — no JS string-concat leak", () => {
38
58
  const { system } = buildReadinessJudgePrompt("Optimize queries", [], {
39
59
  problemStatement: "p",