muonroi-cli 1.7.2 → 1.8.1

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 (326) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  3. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  4. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  5. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  6. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  7. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  8. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  9. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  10. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  11. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  12. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  13. package/dist/src/agent-harness/mock-model.js +143 -6
  14. package/dist/src/agent-harness/test-spawn.js +3 -1
  15. package/dist/src/chat/chat-keychain.js +9 -1
  16. package/dist/src/cli/cost-forensics.js +7 -4
  17. package/dist/src/cli/keys.d.ts +2 -2
  18. package/dist/src/cli/keys.js +107 -28
  19. package/dist/src/cli/usage-report.js +2 -2
  20. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  21. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  22. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  23. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  24. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  25. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  27. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  28. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  29. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  30. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  31. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  32. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  33. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  34. package/dist/src/council/clarifier.d.ts +29 -1
  35. package/dist/src/council/clarifier.js +64 -1
  36. package/dist/src/council/debate-planner.d.ts +2 -1
  37. package/dist/src/council/debate-planner.js +5 -3
  38. package/dist/src/council/debate.d.ts +9 -0
  39. package/dist/src/council/debate.js +88 -8
  40. package/dist/src/council/index.d.ts +46 -1
  41. package/dist/src/council/index.js +125 -19
  42. package/dist/src/council/llm.js +41 -13
  43. package/dist/src/council/prompts.js +36 -1
  44. package/dist/src/council/types.d.ts +14 -1
  45. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  46. package/dist/src/ee/bridge.d.ts +3 -1
  47. package/dist/src/ee/bridge.js +9 -3
  48. package/dist/src/ee/bridge.test.js +1 -2
  49. package/dist/src/ee/client.js +15 -2
  50. package/dist/src/ee/recall-ledger.d.ts +20 -0
  51. package/dist/src/ee/recall-ledger.js +40 -1
  52. package/dist/src/ee/search.d.ts +25 -0
  53. package/dist/src/ee/search.js +73 -0
  54. package/dist/src/ee/types.d.ts +15 -1
  55. package/dist/src/ee/who-am-i.d.ts +41 -0
  56. package/dist/src/ee/who-am-i.js +143 -0
  57. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  58. package/dist/src/ee/who-am-i.test.js +101 -0
  59. package/dist/src/flow/compaction/index.d.ts +1 -0
  60. package/dist/src/flow/compaction/index.js +4 -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 +59 -62
  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 +13 -5
  76. package/dist/src/lsp/manager.test.js +41 -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/auto-setup.js +0 -8
  86. package/dist/src/mcp/client-pool.d.ts +1 -1
  87. package/dist/src/mcp/client-pool.js +25 -9
  88. package/dist/src/mcp/ee-tools.d.ts +10 -0
  89. package/dist/src/mcp/ee-tools.js +34 -0
  90. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/self-verify-runner.js +1 -1
  93. package/dist/src/models/catalog-gemini.test.js +12 -10
  94. package/dist/src/models/catalog.json +106 -38
  95. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  96. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  97. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +3 -0
  98. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  99. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  100. package/dist/src/orchestrator/__tests__/message-processor.test.js +2 -0
  101. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  102. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  105. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  106. package/dist/src/orchestrator/agent.test.js +5 -2
  107. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  108. package/dist/src/orchestrator/auto-commit.js +366 -0
  109. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  110. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  111. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  112. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  113. package/dist/src/orchestrator/compaction.d.ts +22 -0
  114. package/dist/src/orchestrator/compaction.js +42 -0
  115. package/dist/src/orchestrator/delegations.d.ts +1 -0
  116. package/dist/src/orchestrator/delegations.js +41 -0
  117. package/dist/src/orchestrator/error-utils.js +27 -2
  118. package/dist/src/orchestrator/message-processor.d.ts +23 -5
  119. package/dist/src/orchestrator/message-processor.js +226 -2573
  120. package/dist/src/orchestrator/orchestrator.d.ts +11 -2
  121. package/dist/src/orchestrator/orchestrator.js +530 -43
  122. package/dist/src/orchestrator/pending-calls.js +2 -1
  123. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  124. package/dist/src/orchestrator/preprocessor.js +146 -0
  125. package/dist/src/orchestrator/prompts.d.ts +16 -0
  126. package/dist/src/orchestrator/prompts.js +130 -67
  127. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  128. package/dist/src/orchestrator/safety-askcard.js +74 -0
  129. package/dist/src/orchestrator/sandbox.test.js +8 -5
  130. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  131. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  132. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  133. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  134. package/dist/src/orchestrator/stream-runner.js +52 -30
  135. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  136. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  137. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  138. package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
  139. package/dist/src/orchestrator/subagent-compactor.js +148 -24
  140. package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
  141. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  142. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  143. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  144. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  145. package/dist/src/orchestrator/tool-engine.d.ts +153 -0
  146. package/dist/src/orchestrator/tool-engine.js +3138 -0
  147. package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -0
  148. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  149. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  150. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  151. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  152. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  153. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  154. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  155. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  156. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  157. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  158. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  159. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  160. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  161. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  162. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  163. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  164. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  165. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  166. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  167. package/dist/src/pil/agent-operating-contract.js +5 -3
  168. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  169. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  170. package/dist/src/pil/cheap-model-playbook.js +1 -1
  171. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  172. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  173. package/dist/src/pil/config.d.ts +20 -0
  174. package/dist/src/pil/config.js +35 -0
  175. package/dist/src/pil/discovery-types.d.ts +32 -9
  176. package/dist/src/pil/discovery.d.ts +3 -3
  177. package/dist/src/pil/discovery.js +138 -244
  178. package/dist/src/pil/index.d.ts +1 -1
  179. package/dist/src/pil/index.js +1 -1
  180. package/dist/src/pil/layer1-intent.d.ts +9 -1
  181. package/dist/src/pil/layer1-intent.js +62 -7
  182. package/dist/src/pil/layer1-intent.test.js +112 -2
  183. package/dist/src/pil/layer15-context-scan.js +1 -1
  184. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  185. package/dist/src/pil/layer16-clarity.js +52 -88
  186. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  187. package/dist/src/pil/layer18-acceptance.js +1 -75
  188. package/dist/src/pil/layer2-personality.js +7 -8
  189. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  190. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  191. package/dist/src/pil/layer3-ee-injection.js +180 -14
  192. package/dist/src/pil/layer4-gsd.js +22 -8
  193. package/dist/src/pil/layer5-context.d.ts +1 -0
  194. package/dist/src/pil/layer5-context.js +26 -11
  195. package/dist/src/pil/layer6-output.js +23 -5
  196. package/dist/src/pil/llm-classify.d.ts +11 -0
  197. package/dist/src/pil/llm-classify.js +196 -3
  198. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  199. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  200. package/dist/src/pil/pipeline.js +15 -1
  201. package/dist/src/pil/response-tools.d.ts +16 -0
  202. package/dist/src/pil/response-tools.js +41 -0
  203. package/dist/src/pil/schema.d.ts +8 -0
  204. package/dist/src/pil/schema.js +24 -3
  205. package/dist/src/pil/task-tier-map.js +7 -7
  206. package/dist/src/pil/types.d.ts +9 -3
  207. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  208. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  209. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  210. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  211. package/dist/src/product-loop/discovery-interview.js +121 -7
  212. package/dist/src/product-loop/gather.js +19 -5
  213. package/dist/src/product-loop/loop-driver.js +2 -2
  214. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  215. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  216. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  217. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  218. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  219. package/dist/src/providers/auth/gcloud.js +102 -0
  220. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  221. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  222. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  223. package/dist/src/providers/auth/grok-oauth.js +1 -1
  224. package/dist/src/providers/auth/registry.js +28 -6
  225. package/dist/src/providers/gemini.d.ts +1 -1
  226. package/dist/src/providers/gemini.js +10 -3
  227. package/dist/src/providers/keychain.d.ts +2 -2
  228. package/dist/src/providers/keychain.js +20 -2
  229. package/dist/src/providers/openai.js +5 -1
  230. package/dist/src/providers/runtime.d.ts +1 -1
  231. package/dist/src/providers/runtime.js +11 -2
  232. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  233. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  234. package/dist/src/providers/strategies/google.strategy.js +159 -9
  235. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  236. package/dist/src/providers/types.d.ts +1 -1
  237. package/dist/src/router/decide.test.js +40 -8
  238. package/dist/src/router/step-router.d.ts +20 -0
  239. package/dist/src/router/step-router.js +73 -0
  240. package/dist/src/self-qa/agentic-loop.js +30 -21
  241. package/dist/src/self-qa/orchestrator.js +5 -5
  242. package/dist/src/self-qa/scenario-planner.js +1 -1
  243. package/dist/src/self-qa/spec-emitter.js +4 -4
  244. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  245. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  246. package/dist/src/storage/__tests__/sweep-stale-pending.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/migrations.js +13 -1
  250. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  251. package/dist/src/storage/transcript-view.js +7 -1
  252. package/dist/src/storage/transcript.d.ts +3 -0
  253. package/dist/src/storage/transcript.js +254 -73
  254. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  255. package/dist/src/storage/usage.js +22 -5
  256. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  257. package/dist/src/tools/bash.d.ts +36 -23
  258. package/dist/src/tools/bash.js +59 -301
  259. package/dist/src/tools/bash.test.js +2 -257
  260. package/dist/src/tools/file-tracker.d.ts +5 -0
  261. package/dist/src/tools/file-tracker.js +14 -0
  262. package/dist/src/tools/file.js +2 -0
  263. package/dist/src/tools/git-safety.d.ts +38 -1
  264. package/dist/src/tools/git-safety.js +94 -4
  265. package/dist/src/tools/git-safety.test.js +45 -1
  266. package/dist/src/tools/native-tools.d.ts +1 -1
  267. package/dist/src/tools/native-tools.js +66 -1
  268. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  269. package/dist/src/tools/registry-git-safety.test.js +17 -6
  270. package/dist/src/tools/registry.d.ts +6 -0
  271. package/dist/src/tools/registry.js +215 -13
  272. package/dist/src/types/index.d.ts +8 -2
  273. package/dist/src/ui/app.d.ts +1 -0
  274. package/dist/src/ui/app.js +0 -0
  275. package/dist/src/ui/components/council-question-card.js +1 -0
  276. package/dist/src/ui/components/message-view.js +37 -3
  277. package/dist/src/ui/components/structured-response-view.js +7 -0
  278. package/dist/src/ui/components/task-list-panel.js +3 -12
  279. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  280. package/dist/src/ui/containers/chat-feed.js +66 -0
  281. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  282. package/dist/src/ui/containers/modals-layer.js +18 -0
  283. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  284. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  285. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  286. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  287. package/dist/src/ui/slash/expand.js +14 -1
  288. package/dist/src/ui/slash/export.js +16 -1
  289. package/dist/src/ui/slash/menu-items.js +1 -0
  290. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  291. package/dist/src/ui/slash/ponytail.js +19 -0
  292. package/dist/src/ui/status-bar/store.js +0 -1
  293. package/dist/src/ui/status-bar/store.test.js +2 -2
  294. package/dist/src/ui/use-app-logic.d.ts +26 -0
  295. package/dist/src/ui/use-app-logic.js +0 -0
  296. package/dist/src/ui/utils/relaunch.js +1 -1
  297. package/dist/src/ui/utils/text.d.ts +11 -0
  298. package/dist/src/ui/utils/text.js +21 -1
  299. package/dist/src/ui/utils/text.test.js +25 -1
  300. package/dist/src/ui/utils/tools.js +3 -1
  301. package/dist/src/usage/cost-log.js +2 -2
  302. package/dist/src/usage/estimator.d.ts +14 -0
  303. package/dist/src/usage/estimator.js +22 -0
  304. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  305. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  306. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  307. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  308. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  309. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/logger.test.js +115 -0
  311. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  312. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  313. package/dist/src/utils/install-manager.d.ts +1 -0
  314. package/dist/src/utils/install-manager.js +76 -7
  315. package/dist/src/utils/install-manager.test.js +43 -1
  316. package/dist/src/utils/llm-deadline.d.ts +9 -1
  317. package/dist/src/utils/llm-deadline.js +31 -2
  318. package/dist/src/utils/logger.d.ts +28 -0
  319. package/dist/src/utils/logger.js +171 -0
  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 +36 -32
  324. package/dist/src/utils/settings.js +151 -123
  325. package/dist/src/utils/update-checker.test.js +5 -1
  326. package/package.json +1 -1
@@ -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",
@@ -0,0 +1,130 @@
1
+ /**
2
+ * council-turn-length emit contract (G2-b).
3
+ *
4
+ * Drives the REAL `runDebate` generator with a recording CouncilLLM and a
5
+ * capturing `globalThis.__muonroiAgentRuntime`, then asserts the observe-only
6
+ * `council-turn-length` harness event fires for each fully-assembled speaker
7
+ * turn — opening statements (round 0) and discussion turns (round 1+) — carrying
8
+ * the right charCount / wordCount / round / model / correlationId.
9
+ *
10
+ * This exercises the production emit path in src/council/debate.ts end-to-end
11
+ * (not a mocked re-implementation). The MCP harness tool surface only exposes
12
+ * toast/stream.delta via tui_last_event, so this generator-level drive is the
13
+ * faithful wire-level proof that the event fires during a real council run.
14
+ */
15
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
16
+ import { runDebate } from "../debate.js";
17
+ const OPENING = "Opening alpha beta gamma."; // 25 chars, 4 words
18
+ const ROUND = "Round response one two three."; // round-turn text
19
+ function recordingLlm() {
20
+ return {
21
+ // openingWithRetry + leader-eval + summaries all go through generate. Opening
22
+ // statements use the returned text verbatim → that drives the round-0 emit.
23
+ generate: async () => OPENING,
24
+ // debateWithRetry (discussion turns) is the ONLY caller of debate → drives the
25
+ // round-1+ emit with a known, non-failure string.
26
+ debate: async () => ({ text: ROUND, toolCalls: [] }),
27
+ research: async () => "findings",
28
+ };
29
+ }
30
+ function makeParticipants() {
31
+ return [
32
+ { role: "architect", model: "model-arch", position: "", stance: { name: "architect", lens: "design" } },
33
+ { role: "qa", model: "model-qa", position: "", stance: { name: "qa", lens: "risk" } },
34
+ ];
35
+ }
36
+ function makeSpec() {
37
+ return {
38
+ problemStatement: "Decide X vs Y for a small service.",
39
+ constraints: [],
40
+ successCriteria: [],
41
+ scope: "",
42
+ rawQA: [],
43
+ };
44
+ }
45
+ function makeConfig(participants) {
46
+ return {
47
+ topic: "X vs Y",
48
+ conversationContext: "",
49
+ leaderModelId: "model-leader",
50
+ participants,
51
+ debatePlan: {
52
+ intentSummary: "Pick the better option.",
53
+ stances: [
54
+ { name: "architect", lens: "design" },
55
+ { name: "qa", lens: "risk" },
56
+ ],
57
+ outputShape: {
58
+ kind: "decision",
59
+ sections: [{ key: "rec", heading: "Recommendation", shape: "list" }],
60
+ guardrails: [],
61
+ },
62
+ plannedRounds: 1,
63
+ },
64
+ // Skip research so the flow goes straight to opening statements deterministically.
65
+ researchSkipOverride: true,
66
+ runId: "sess-turnlen-test",
67
+ };
68
+ }
69
+ describe("council-turn-length emit (real runDebate)", () => {
70
+ let captured;
71
+ let prevRuntime;
72
+ beforeEach(() => {
73
+ captured = [];
74
+ const g = globalThis;
75
+ prevRuntime = g.__muonroiAgentRuntime;
76
+ g.__muonroiAgentRuntime = { emitEvent: (e) => captured.push(e) };
77
+ });
78
+ afterEach(() => {
79
+ globalThis.__muonroiAgentRuntime = prevRuntime;
80
+ });
81
+ function turnLengthEvents() {
82
+ return captured.filter((e) => e.t === "event" && e.kind === "council-turn-length");
83
+ }
84
+ it("emits a round-0 event per opening speaker with correct counts + correlationId", async () => {
85
+ const participants = makeParticipants();
86
+ const gen = runDebate(makeSpec(), makeConfig(participants), recordingLlm());
87
+ let step = await gen.next();
88
+ while (!step.done)
89
+ step = await gen.next();
90
+ const opening = turnLengthEvents().filter((e) => e.round === 0);
91
+ // One opening statement per participant.
92
+ expect(opening.length).toBe(2);
93
+ for (const e of opening) {
94
+ expect(e.charCount).toBe(OPENING.length);
95
+ expect(e.wordCount).toBe(4);
96
+ expect(e.correlationId).toBe("sess-turnlen-test");
97
+ expect(typeof e.model).toBe("string");
98
+ expect(e.model.length).toBeGreaterThan(0);
99
+ }
100
+ // Roles are the participant stance names / roles.
101
+ const roles = opening.map((e) => e.role);
102
+ expect(roles).toContain("architect");
103
+ expect(roles).toContain("qa");
104
+ });
105
+ it("emits round-1 discussion-turn events with the discussion text's counts", async () => {
106
+ const gen = runDebate(makeSpec(), makeConfig(makeParticipants()), recordingLlm());
107
+ let step = await gen.next();
108
+ while (!step.done)
109
+ step = await gen.next();
110
+ const round1 = turnLengthEvents().filter((e) => e.round === 1);
111
+ expect(round1.length).toBeGreaterThanOrEqual(1);
112
+ for (const e of round1) {
113
+ expect(e.charCount).toBe(ROUND.trim().length);
114
+ expect(e.wordCount).toBe(ROUND.trim().split(/\s+/).filter(Boolean).length);
115
+ expect(e.correlationId).toBe("sess-turnlen-test");
116
+ }
117
+ });
118
+ it("does not emit when no agent runtime is installed (normal user mode)", async () => {
119
+ globalThis.__muonroiAgentRuntime = undefined;
120
+ const localCaptured = [];
121
+ // Re-install a capturer AFTER clearing to prove the helper truly no-ops when
122
+ // the runtime is absent: here it's absent, so localCaptured stays empty.
123
+ const gen = runDebate(makeSpec(), makeConfig(makeParticipants()), recordingLlm());
124
+ let step = await gen.next();
125
+ while (!step.done)
126
+ step = await gen.next();
127
+ expect(localCaptured.length).toBe(0);
128
+ });
129
+ });
130
+ //# sourceMappingURL=council-turn-length-emit.test.js.map