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
@@ -1,10 +1,31 @@
1
1
  import type { ModelMessage } from "ai";
2
2
  import type { StreamChunk } from "../types/index.js";
3
3
  import type { CouncilLLM, CouncilStats, PreflightResponder, QuestionResponder } from "./types.js";
4
+ /**
5
+ * Wrap a CouncilLLM so every `generate` call inherits the council-wide abort
6
+ * signal. The whole generate-based call path (clarifier, research-need eval,
7
+ * leader round-eval, opening statements, round summary, spec/plan synthesis,
8
+ * and the debate-planner retry) calls `llm.generate(...)` with NO signal arg —
9
+ * none of those sites thread one. Injecting it here in ONE place makes them all
10
+ * cancellable without touching each signature. `debate`/`research` already get
11
+ * `config.signal` explicitly, so they pass through unchanged.
12
+ *
13
+ * An explicit per-call signal (none exist today, but the param is there) wins
14
+ * over the injected one. Returns the original llm untouched when no signal is
15
+ * configured (e.g. the sprint-planner path, which has no user-abort signal).
16
+ */
17
+ export declare function withCouncilSignal(llm: CouncilLLM, signal: AbortSignal | undefined): CouncilLLM;
4
18
  export interface RunCouncilOptions {
5
19
  skipClarification?: boolean;
6
20
  userModelMessage?: ModelMessage;
7
21
  signal?: AbortSignal;
22
+ /**
23
+ * Hard cap on clarification rounds for the explicit /council path. Defaults to
24
+ * EXPLICIT_COUNCIL_CLARIFY_ROUNDS (1). Callers that genuinely want the full
25
+ * multi-round ready-gate can raise it; auto-council/sprint pass
26
+ * skipClarification:true and never reach the clarifier regardless.
27
+ */
28
+ clarifyMaxRounds?: number;
8
29
  /** Working directory used to resolve the "current project" snapshot. */
9
30
  cwd?: string;
10
31
  /** Shared stats object from orchestrator — when provided, runCouncil uses it instead of a local one so stats.calls is accurate (Phase 14 CQ-01). */
@@ -15,8 +36,32 @@ export interface RunCouncilOptions {
15
36
  */
16
37
  runDir?: string;
17
38
  }
39
+ export type PostDebateAction = "save_exit" | "generate_plan" | "refine" | "ask_followup" | "retry_synthesis";
40
+ /**
41
+ * Decide the DEFAULT post-debate action surfaced as the recommended option.
42
+ * Extracted as a pure function so the policy is unit-testable.
43
+ *
44
+ * Issue #3 (post-debate default mismatch): when synthesis succeeded and no plan
45
+ * exists yet, only an `implementation_plan`-shaped debate should default to
46
+ * "generate_plan" (Lock plan & execute Sprint 1). For a `decision`, `evaluation`,
47
+ * `investigation`, or `exploration` debate the synthesis IS the deliverable — the
48
+ * user asked a question, not for code — so the default is `save_exit`. The
49
+ * generate_plan OPTION is still offered downstream; it's just no longer the
50
+ * pre-selected default for non-build topics.
51
+ */
52
+ export declare function pickPostDebateRecommendation(input: {
53
+ synthesisFailed: boolean;
54
+ hasEmptySections: boolean;
55
+ refinementTopics: string[];
56
+ confidenceLevel: "high" | "medium" | "low";
57
+ hasPlan: boolean;
58
+ outputKind: string;
59
+ }): {
60
+ value: PostDebateAction;
61
+ reason: string;
62
+ };
18
63
  export declare function runCouncil(topic: string, sessionModelId: string, messages: Array<{
19
64
  role: string;
20
65
  content: string | unknown;
21
- }>, sessionId: string | undefined, llm: CouncilLLM, respondToQuestion: QuestionResponder, respondToPreflight: PreflightResponder, processMessageFn: (message: string) => AsyncGenerator<StreamChunk, void, unknown>, options?: RunCouncilOptions): AsyncGenerator<StreamChunk, string | null, unknown>;
66
+ }>, sessionId: string | undefined, rawLlm: CouncilLLM, respondToQuestion: QuestionResponder, respondToPreflight: PreflightResponder, processMessageFn: (message: string) => AsyncGenerator<StreamChunk, void, unknown>, options?: RunCouncilOptions): AsyncGenerator<StreamChunk, string | null, unknown>;
22
67
  export type { ClarifiedSpec, CouncilLLM, CouncilParticipant, CouncilStats } from "./types.js";
@@ -15,9 +15,91 @@ import { resolveLeaderModelDetailed, resolveParticipants } from "./leader.js";
15
15
  import { phaseDone, phaseStart } from "./phase-events.js";
16
16
  import { runPlanning } from "./planner.js";
17
17
  import { runPreflight } from "./preflight.js";
18
- export async function* runCouncil(topic, sessionModelId, messages, sessionId, llm, respondToQuestion, respondToPreflight, processMessageFn, options) {
18
+ /**
19
+ * Wrap a CouncilLLM so every `generate` call inherits the council-wide abort
20
+ * signal. The whole generate-based call path (clarifier, research-need eval,
21
+ * leader round-eval, opening statements, round summary, spec/plan synthesis,
22
+ * and the debate-planner retry) calls `llm.generate(...)` with NO signal arg —
23
+ * none of those sites thread one. Injecting it here in ONE place makes them all
24
+ * cancellable without touching each signature. `debate`/`research` already get
25
+ * `config.signal` explicitly, so they pass through unchanged.
26
+ *
27
+ * An explicit per-call signal (none exist today, but the param is there) wins
28
+ * over the injected one. Returns the original llm untouched when no signal is
29
+ * configured (e.g. the sprint-planner path, which has no user-abort signal).
30
+ */
31
+ export function withCouncilSignal(llm, signal) {
32
+ if (!signal)
33
+ return llm;
34
+ return {
35
+ ...llm,
36
+ generate: (modelId, system, prompt, maxTokens, onUsage, sig) => llm.generate(modelId, system, prompt, maxTokens, onUsage, sig ?? signal),
37
+ };
38
+ }
39
+ /**
40
+ * Explicit `/council …` is the ONLY caller that runs the clarifier — auto-council
41
+ * (message-processor) and the sprint-planner both pass `skipClarification: true`.
42
+ * Capping it to a single round (down from the ready-gate's MAX_CLARIFY_ROUNDS=12)
43
+ * stops the 2-3 rounds of follow-up askcards users hit on already-detailed topics
44
+ * (see project-council-subsystem memory). The per-round ready-gate has no
45
+ * production behavioural effect anyway — `spec.ready`/`confidenceScore`/
46
+ * `remainingGaps` are write-only — so its only real cost is the extra rounds.
47
+ * Round 0 still runs, so the user is asked the key questions exactly once.
48
+ */
49
+ const EXPLICIT_COUNCIL_CLARIFY_ROUNDS = 1;
50
+ /**
51
+ * Decide the DEFAULT post-debate action surfaced as the recommended option.
52
+ * Extracted as a pure function so the policy is unit-testable.
53
+ *
54
+ * Issue #3 (post-debate default mismatch): when synthesis succeeded and no plan
55
+ * exists yet, only an `implementation_plan`-shaped debate should default to
56
+ * "generate_plan" (Lock plan & execute Sprint 1). For a `decision`, `evaluation`,
57
+ * `investigation`, or `exploration` debate the synthesis IS the deliverable — the
58
+ * user asked a question, not for code — so the default is `save_exit`. The
59
+ * generate_plan OPTION is still offered downstream; it's just no longer the
60
+ * pre-selected default for non-build topics.
61
+ */
62
+ export function pickPostDebateRecommendation(input) {
63
+ if (input.synthesisFailed) {
64
+ return {
65
+ value: "retry_synthesis",
66
+ reason: "Re-run synthesis with a compact prompt — usually clears provider-timeout failures.",
67
+ };
68
+ }
69
+ if (input.hasEmptySections) {
70
+ return { value: "refine", reason: `Fill in ${input.refinementTopics.length} section(s) the debate left empty.` };
71
+ }
72
+ if (input.confidenceLevel === "low") {
73
+ return {
74
+ value: "ask_followup",
75
+ reason: "Press the council on the weakest claims rather than accepting a thin synthesis.",
76
+ };
77
+ }
78
+ if (!input.hasPlan) {
79
+ return input.outputKind === "implementation_plan"
80
+ ? { value: "generate_plan", reason: "Convert the agreed outcome into concrete steps." }
81
+ : {
82
+ value: "save_exit",
83
+ reason: `This was a ${input.outputKind} debate — the synthesis above is the deliverable; save it.`,
84
+ };
85
+ }
86
+ return { value: "save_exit", reason: "Outcome looks solid — save and move on." };
87
+ }
88
+ export async function* runCouncil(topic, sessionModelId, messages, sessionId, rawLlm, respondToQuestion, respondToPreflight, processMessageFn, options) {
19
89
  const stats = options?.councilStats ?? { calls: 0, startMs: Date.now(), phases: [] };
20
90
  const costAware = isCouncilCostAware();
91
+ // Inject the user-abort signal into every generate-based sub-call (clarify,
92
+ // research-need, leader-eval, opening, summary, synthesis, debate-plan retry).
93
+ // No-op passthrough when options.signal is undefined.
94
+ const llm = withCouncilSignal(rawLlm, options?.signal);
95
+ // Hard-stop guard. Threading the signal into LLM calls makes them abortable,
96
+ // but every council sub-phase wraps its work in fail-open try/catch that
97
+ // swallows the resulting AbortError and returns normally — so without an
98
+ // explicit check at each phase boundary the loop would march on to the next
99
+ // phase after a cancel. `userAborted()` is checked between phases; when true
100
+ // the run stops cleanly rather than burning the remaining (debate, synthesis)
101
+ // LLM budget. Cancellation latency is bounded by one in-flight sub-call.
102
+ const userAborted = () => options?.signal?.aborted === true;
21
103
  // ── Resolve models ──────────────────────────────────────────────────────────
22
104
  const leaderResolution = await resolveLeaderModelDetailed(sessionModelId);
23
105
  const leaderModelId = leaderResolution.modelId;
@@ -50,6 +132,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
50
132
  : baseContext;
51
133
  const internetFirst = projectInfo.isEmpty;
52
134
  const active = participants.map((p) => ({ ...p, position: "" }));
135
+ if (userAborted()) {
136
+ yield { type: "content", content: "\n> Council cancelled by user.\n" };
137
+ yield { type: "done" };
138
+ return null;
139
+ }
53
140
  // ── Phase A + B loop: Clarify → Confirm ─────────────────────────────────────
54
141
  let spec = buildSpecFromTopic(topic, conversationContext);
55
142
  let approved = false;
@@ -76,7 +163,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
76
163
  content: `\n> Clarification seeded by PIL (${pilSeed.length} gray-area question${pilSeed.length === 1 ? "" : "s"}).\n`,
77
164
  };
78
165
  }
79
- const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, undefined, undefined, costAware);
166
+ const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, options?.clarifyMaxRounds ?? EXPLICIT_COUNCIL_CLARIFY_ROUNDS, undefined, costAware);
80
167
  let clarifyResult;
81
168
  do {
82
169
  clarifyResult = await clarifyGen.next();
@@ -90,6 +177,9 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
90
177
  spec = buildSpecFromTopic(topic, conversationContext);
91
178
  yield { type: "content", content: `\n> Auto-council: skipping clarification (PIL pre-classified).\n` };
92
179
  }
180
+ // Cancelled during clarification — don't pop the preflight approval card.
181
+ if (userAborted())
182
+ break;
93
183
  const researchNeeded = true;
94
184
  const preflightGen = runPreflight(spec, participants, researchNeeded, respondToPreflight, {
95
185
  repoEmpty: internetFirst,
@@ -105,6 +195,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
105
195
  approved = preflightResult.value;
106
196
  }
107
197
  stats.phases.push({ name: "clarify+preflight", durationMs: Date.now() - phaseAStart });
198
+ if (userAborted()) {
199
+ yield { type: "content", content: "\n> Council cancelled by user.\n" };
200
+ yield { type: "done" };
201
+ return null;
202
+ }
108
203
  // ── Research-need check + user override ────────────────────────────────────
109
204
  // Leader-LLM decides if research is required. If yes, give the user a chance
110
205
  // to skip — research is the slowest part of council and trivial questions
@@ -171,6 +266,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
171
266
  content: `\n> [Experience] ${eeResult.warnings.length} past warning(s) loaded — Experience Auditor will calibrate debate.\n`,
172
267
  };
173
268
  }
269
+ if (userAborted()) {
270
+ yield { type: "content", content: "\n> Council cancelled by user.\n" };
271
+ yield { type: "done" };
272
+ return null;
273
+ }
174
274
  // ── Phase B.5: Leader plans the debate (stances + output shape) ─────────────
175
275
  const planStartMs = Date.now();
176
276
  yield phaseStart({
@@ -179,7 +279,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
179
279
  label: "Debate plan",
180
280
  detail: "stances + output shape",
181
281
  });
182
- const planGenerator = planDebate(spec, leaderModelId, llm, eeResult.warnings, experienceMode, pilCtx?.taskType ?? undefined, pilCtx?.complexityTier ?? undefined);
282
+ const planGenerator = planDebate(spec, leaderModelId, llm, eeResult.warnings, experienceMode, pilCtx?.taskType ?? undefined, pilCtx?.complexityTier ?? undefined, options?.signal);
183
283
  let planStep;
184
284
  do {
185
285
  planStep = await planGenerator.next();
@@ -222,6 +322,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
222
322
  active.length = debatePlan.stances.length;
223
323
  }
224
324
  stats.phases.push({ name: "plan_debate", durationMs: Date.now() - planStartMs });
325
+ if (userAborted()) {
326
+ yield { type: "content", content: "\n> Council cancelled by user.\n" };
327
+ yield { type: "done" };
328
+ return null;
329
+ }
225
330
  // ── Phase C: Dynamic Debate ─────────────────────────────────────────────────
226
331
  const debateStart = Date.now();
227
332
  const debateGen = runDebate(spec, {
@@ -235,6 +340,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
235
340
  leaderNeedsResearch,
236
341
  internetFirst,
237
342
  costAware,
343
+ runId: sessionId,
238
344
  }, llm);
239
345
  let debateResult;
240
346
  do {
@@ -270,6 +376,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
270
376
  durationMs: Date.now() - debateStart,
271
377
  data: { topic, roundCount: debateState.roundCount },
272
378
  });
379
+ if (userAborted()) {
380
+ yield { type: "content", content: "\n> Council cancelled by user — skipping synthesis.\n" };
381
+ yield { type: "done" };
382
+ return null;
383
+ }
273
384
  // ── Phase D: Plan ───────────────────────────────────────────────────────────
274
385
  const planStart = Date.now();
275
386
  const planGen = runPlanning(debateState, spec, debateState.active, leaderModelId, respondToPreflight, llm, debatePlan, pilCtx?.outputStyle ?? undefined);
@@ -338,22 +449,17 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
338
449
  : confidenceLevel === "medium"
339
450
  ? `⚠ Medium confidence (evidence density ${evidenceDensity.toFixed(2)})`
340
451
  : `⚠ Low confidence (evidence density ${evidenceDensity.toFixed(2)})`;
341
- // Recommendation surfaced to the user as the default action.
342
- const recommendation = synthesisFailed
343
- ? {
344
- value: "retry_synthesis",
345
- reason: "Re-run synthesis with a compact prompt — usually clears provider-timeout failures.",
346
- }
347
- : hasEmptySections
348
- ? { value: "refine", reason: `Fill in ${refinementTopics.length} section(s) the debate left empty.` }
349
- : confidenceLevel === "low"
350
- ? {
351
- value: "ask_followup",
352
- reason: "Press the council on the weakest claims rather than accepting a thin synthesis.",
353
- }
354
- : !hasPlan
355
- ? { value: "generate_plan", reason: "Convert the agreed outcome into concrete steps." }
356
- : { value: "save_exit", reason: "Outcome looks solid — save and move on." };
452
+ // Recommendation surfaced to the user as the default action. The
453
+ // implementation_plan-vs-decision/evaluation split lives in
454
+ // pickPostDebateRecommendation (issue #3 — see its doc comment).
455
+ const recommendation = pickPostDebateRecommendation({
456
+ synthesisFailed,
457
+ hasEmptySections,
458
+ refinementTopics,
459
+ confidenceLevel,
460
+ hasPlan: !!hasPlan,
461
+ outputKind: debatePlan.outputShape.kind,
462
+ });
357
463
  const baseOptions = [];
358
464
  if (synthesisFailed) {
359
465
  baseOptions.push({
@@ -4,8 +4,8 @@ import { getDefaultEEClient } from "../ee/intercept.js";
4
4
  import { emitMatches } from "../ee/render.js";
5
5
  import { buildMcpToolSet } from "../mcp/runtime.js";
6
6
  import { getProviderCapabilities } from "../providers/capabilities.js";
7
- import { loadKeyForProvider } from "../providers/keychain.js";
8
- import { createProviderFactory, detectProviderForModel, resolveModelRuntime } from "../providers/runtime.js";
7
+ import { loadKeyForProvider, ProviderKeyMissingError } from "../providers/keychain.js";
8
+ import { createProviderFactoryAsync, detectProviderForModel, resolveModelRuntime } from "../providers/runtime.js";
9
9
  import { createBuiltinTools as createTools } from "../tools/registry.js";
10
10
  import { appendCostLog } from "../usage/cost-log.js";
11
11
  import { projectCostUSD } from "../usage/estimator.js";
@@ -13,6 +13,33 @@ import { withDeadlineRace, withTimeoutSignal } from "../utils/llm-deadline.js";
13
13
  import { loadMcpServers } from "../utils/settings.js";
14
14
  import { withVisibleRetry } from "../utils/visible-retry.js";
15
15
  import { buildResearchSystemPrompt } from "./prompts.js";
16
+ /**
17
+ * Resolve a provider factory for a council sub-call, OAuth-aware.
18
+ *
19
+ * The council reachability gate (`isProviderReachable` in leader.ts) counts
20
+ * OAuth-authenticated providers as usable (via `getConfiguredProviders`), so a
21
+ * multi-provider roster can route a stance to an OAuth-only provider such as
22
+ * xai/grok. `loadKeyForProvider` is API-key-only and throws
23
+ * `ProviderKeyMissingError` for those — which previously surfaced as
24
+ * "[Error: No API key found for provider 'xai'.]" on every call of that stance,
25
+ * even though the provider was fully authenticated. Mirror the main
26
+ * orchestrator path: fall back to `createProviderFactoryAsync`, which loads +
27
+ * refreshes the stored OAuth bearer token. Only the expected missing-key case
28
+ * is swallowed; unexpected errors propagate.
29
+ */
30
+ async function resolveCouncilFactory(providerId) {
31
+ let apiKey;
32
+ try {
33
+ apiKey = await loadKeyForProvider(providerId);
34
+ }
35
+ catch (err) {
36
+ if (!(err instanceof ProviderKeyMissingError))
37
+ throw err;
38
+ // OAuth-only provider — createProviderFactoryAsync injects the bearer token.
39
+ }
40
+ const { factory } = await createProviderFactoryAsync(providerId, apiKey ? { apiKey } : {});
41
+ return factory;
42
+ }
16
43
  function getDebugLogPath() {
17
44
  const p = process.env.MUONROI_COUNCIL_DEBUG_LOG;
18
45
  return p && p.length > 0 ? p : null;
@@ -184,7 +211,7 @@ const COUNCIL_LLM_TIMEOUT_MS = (() => {
184
211
  // implementation. Imported at the top of this file.
185
212
  export function createCouncilLLM(bash, mode, sessionId, stats) {
186
213
  return {
187
- async generate(modelId, system, prompt, maxTokens = 4096, onUsage) {
214
+ async generate(modelId, system, prompt, maxTokens = 4096, onUsage, signal) {
188
215
  const mock = getMockLlm();
189
216
  if (mock) {
190
217
  stats.calls++;
@@ -192,11 +219,14 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
192
219
  return result.text;
193
220
  }
194
221
  const providerId = detectProviderForModel(modelId);
195
- const key = await loadKeyForProvider(providerId);
196
- const { factory } = createProviderFactory(providerId, { apiKey: key });
222
+ const factory = await resolveCouncilFactory(providerId);
197
223
  const runtime = resolveModelRuntime(factory, modelId);
198
224
  const t0 = Date.now();
199
- const { signal: timedSignal, cleanup: cleanupTimeout } = withTimeoutSignal(undefined, COUNCIL_LLM_TIMEOUT_MS);
225
+ // Combine the user-abort signal (when threaded from runCouncil) with the
226
+ // per-call wall-clock deadline. Without the parent signal, an Esc/Ctrl-C
227
+ // during the longest generate calls (8192-token synthesis, clarify, leader
228
+ // eval) was a no-op — the call ran to completion or hit the 5-min timeout.
229
+ const { signal: timedSignal, cleanup: cleanupTimeout } = withTimeoutSignal(signal, COUNCIL_LLM_TIMEOUT_MS);
200
230
  try {
201
231
  const result = await withDeadlineRace(() => withVisibleRetry(() => generateText({
202
232
  model: runtime.model,
@@ -211,7 +241,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
211
241
  maxRetries: 0,
212
242
  ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
213
243
  abortSignal: timedSignal,
214
- }), { label: "council.generate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.generate");
244
+ }), { label: "council.generate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.generate", signal);
215
245
  cleanupTimeout();
216
246
  stats.calls++;
217
247
  const durMs = Date.now() - t0;
@@ -273,8 +303,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
273
303
  return { text: result.text, toolCalls: [] };
274
304
  }
275
305
  const providerId = detectProviderForModel(modelId);
276
- const key = await loadKeyForProvider(providerId);
277
- const { factory } = createProviderFactory(providerId, { apiKey: key });
306
+ const factory = await resolveCouncilFactory(providerId);
278
307
  const runtime = resolveModelRuntime(factory, modelId);
279
308
  // Verification tools — re-introduced after the no-tools fix (session
280
309
  // a7a5690d2049). The original failure was stepCountIs(4) + full toolset
@@ -352,7 +381,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
352
381
  maxRetries: 0,
353
382
  ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
354
383
  abortSignal: timedSignal,
355
- }), { label: "council.debate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.debate");
384
+ }), { label: "council.debate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.debate", signal);
356
385
  cleanupTimeout();
357
386
  stats.calls++;
358
387
  // No tool calls expected, but the AI SDK shape still has the field —
@@ -430,8 +459,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
430
459
  return result.text;
431
460
  }
432
461
  const providerId = detectProviderForModel(modelId);
433
- const key = await loadKeyForProvider(providerId);
434
- const { factory } = createProviderFactory(providerId, { apiKey: key });
462
+ const factory = await resolveCouncilFactory(providerId);
435
463
  const runtime = resolveModelRuntime(factory, modelId);
436
464
  const builtinTools = createTools(bash, mode);
437
465
  // CQ-03: Lazy MCP bundle per research call — fail-open so builtins remain available
@@ -488,7 +516,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
488
516
  maxRetries: 0,
489
517
  ...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
490
518
  abortSignal: timedSignal,
491
- }), { label: "council.research" }), researchTimeoutMs + 5_000, "council.research");
519
+ }), { label: "council.research" }), researchTimeoutMs + 5_000, "council.research", signal);
492
520
  cleanupTimeout();
493
521
  const researchUsage = logCouncilCost({
494
522
  callsite: "council.research",
@@ -28,6 +28,11 @@ export function buildClarificationPrompt(topic, conversationContext, previousQA)
28
28
  `the project IS the one described in the "## Current Project" section of the context. DO NOT ask which project.\n` +
29
29
  `- Only ask about project identity when the topic mentions multiple distinct projects or external products.\n` +
30
30
  `- Use the project's package.json name and description as implicit context for follow-up questions.\n\n` +
31
+ `## Language Rule (mandatory)\n` +
32
+ `Write the "question", "why", and every "suggestions"/"recommended" option in the SAME ` +
33
+ `language the user used in the Topic below — detect it; if the user wrote Vietnamese, write ` +
34
+ `Vietnamese; if English, English. The user reads and answers these on a card, so they must ` +
35
+ `NOT default to English. Keep code identifiers, file paths, tech/product names, and JSON keys in English.\n\n` +
31
36
  `Output ONLY a JSON array (no markdown, no preamble):\n` +
32
37
  `[{"question": "...", "why": "why this matters for a focused discussion", "suggestions": ["option A", "option B"], "recommended": "option A", "isRequired": true}]\n\n` +
33
38
  `Rules for "recommended" (be decisive — the user should never face an unranked list):\n` +
@@ -49,7 +54,15 @@ export function buildSpecSynthesisPrompt(topic, conversationContext, qa) {
49
54
  ` "constraints": ["constraint 1", "constraint 2"],\n` +
50
55
  ` "successCriteria": ["criterion 1", "criterion 2"],\n` +
51
56
  ` "scope": "what is in and out of scope"\n` +
52
- `}`,
57
+ `}\n\n` +
58
+ `## Language Rule (mandatory)\n` +
59
+ `Write EVERY field (problemStatement, constraints, successCriteria, scope) in the SAME ` +
60
+ `language the user used — detect it from the Original Topic and Clarification Q&A below. ` +
61
+ `If the user wrote Vietnamese, write Vietnamese; if English, English. Do NOT default to ` +
62
+ `English. This brief is shown to the user on the approval card AND the final synthesis ` +
63
+ `detects its output language from this problemStatement, so writing it in the wrong ` +
64
+ `language drags the whole council's output off the user's language. Keep code identifiers, ` +
65
+ `tech/product names, and JSON keys in English.`,
53
66
  prompt: `## Original Topic\n${topic}\n\n` +
54
67
  (conversationContext ? `## Context\n${conversationContext}\n\n` : "") +
55
68
  `## Clarification Q&A\n${qa.map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`).join("\n\n")}`,
@@ -146,6 +159,25 @@ const EVIDENCE_RULE_FOLLOWUP = `\n## Evidence Rule\n` +
146
159
  `- Most follow-up turns will not need any tool call.\n` +
147
160
  `Tag verified results: \`[CONFIRMED via <tool>:<evidence>]\` or \`[REFUTED via <tool>:<evidence>]\`.\n` +
148
161
  `Uncited numbers you cannot verify: \`[UNVERIFIED: <claim>]\`.\n`;
162
+ /**
163
+ * Length discipline injected into every debate turn (F13 token-thrift). The
164
+ * debate-turn ceilings are generous (4096-6144 maxTokens) and reasoning models
165
+ * left unbounded emit ~400-600 words/turn — ~17 turns/debate is token-heavy and
166
+ * directly fights the "tiết kiệm token" goal. A concrete word cap + "density
167
+ * over length" cuts filler / preamble / brief-restating while keeping the
168
+ * analytical core (position, reasoning bullets, the cross-question). This does
169
+ * NOT lower maxTokens: reasoning models need that headroom to think without
170
+ * empty-turn truncation (see llm.ts) — the saving comes from the model writing
171
+ * less, not from a hard output ceiling. The text deliberately contains none of
172
+ * the prompt substrings the council mocks route on (e.g. "responding to",
173
+ * "continuing a discussion", "team lead", "Summarize this discussion").
174
+ */
175
+ function concisenessRule(maxWords) {
176
+ return (`\n## Length (be token-thrifty)\n` +
177
+ `Keep this turn under ~${maxWords} words. Open with your position in one line, ` +
178
+ `then 2-4 bullets of your strongest reasoning. No preamble, no restating the ` +
179
+ `brief, no filler — density over length. A tight argument beats a long one.\n`);
180
+ }
149
181
  /** Resolve the persona label used inside debate prompts. Stance wins; role is fallback. */
150
182
  function personaOf(role, stance) {
151
183
  if (stance) {
@@ -171,6 +203,7 @@ export function buildOpeningPrompt(ctx) {
171
203
  focusLine +
172
204
  ENGLISH_ONLY_RULE +
173
205
  EVIDENCE_RULE_OPENING +
206
+ concisenessRule(220) +
174
207
  (stackLock ? `\n${stackLock}\n` : "") +
175
208
  guardrails +
176
209
  (ctx.conversationContext ? `\n## Conversation Context\n${ctx.conversationContext}\n\n---\n\n` : "\n") +
@@ -192,6 +225,7 @@ export function buildResponsePrompt(ctx) {
192
225
  system: `You are the "${me.label}" (lens: ${me.lens}) responding to the "${them.label}" (lens: ${them.lens}).\n` +
193
226
  ENGLISH_ONLY_RULE +
194
227
  EVIDENCE_RULE_RESPONSE +
228
+ concisenessRule(160) +
195
229
  (stackLock ? `\n${stackLock}\n` : "") +
196
230
  `\n## Discussion Brief\n` +
197
231
  `Problem: ${ctx.spec.problemStatement}\n` +
@@ -216,6 +250,7 @@ export function buildFollowupPrompt(ctx) {
216
250
  system: `You are the "${me.label}" (lens: ${me.lens}) continuing a discussion (round ${ctx.round}) with the "${them.label}" (lens: ${them.lens}).\n` +
217
251
  ENGLISH_ONLY_RULE +
218
252
  EVIDENCE_RULE_FOLLOWUP +
253
+ concisenessRule(140) +
219
254
  (stackLock ? `\n${stackLock}\n` : "") +
220
255
  `\n` +
221
256
  (ctx.runningSummary
@@ -211,6 +211,13 @@ export interface CouncilConfig {
211
211
  internetFirst?: boolean;
212
212
  /** When true, leader sub-tasks downshift to cheaper tier models on the same provider. */
213
213
  costAware?: boolean;
214
+ /**
215
+ * Enclosing council run id (= sessionId). Used only as the correlationId on the
216
+ * observe-only `council-turn-length` harness event so per-turn length samples can
217
+ * be grouped by run. Optional: direct callers/tests may omit it (falls back to a
218
+ * stable literal); has no effect on debate behaviour.
219
+ */
220
+ runId?: string;
214
221
  }
215
222
  /**
216
223
  * JSON shape persisted as `[Council Memory] {...}` in the session messages.
@@ -270,7 +277,13 @@ export interface CouncilCallUsage {
270
277
  }
271
278
  export type UsageCallback = (usage: CouncilCallUsage) => void;
272
279
  export interface CouncilLLM {
273
- generate(modelId: string, system: string, prompt: string, maxTokens?: number, onUsage?: UsageCallback): Promise<string>;
280
+ generate(modelId: string, system: string, prompt: string, maxTokens?: number, onUsage?: UsageCallback,
281
+ /**
282
+ * User-abort signal. Threaded so a mid-council cancel (Esc/Ctrl-C) aborts
283
+ * the in-flight generate call. Trailing+optional for back-compat — existing
284
+ * positional callers/mocks are unaffected. See withCouncilSignal in index.ts.
285
+ */
286
+ signal?: AbortSignal): Promise<string>;
274
287
  research(modelId: string, topic: string, conversationContext: string, signal?: AbortSignal, persistTrace?: ToolTraceEmitter, options?: {
275
288
  internetFirst?: boolean;
276
289
  }, onUsage?: UsageCallback): Promise<string>;
@@ -49,7 +49,7 @@ function bootstrapDb(sessions) {
49
49
  db.close();
50
50
  }
51
51
  beforeEach(() => {
52
- tmpHome = join(tmpdir(), "muonroi-test-export-" + Date.now());
52
+ tmpHome = join(tmpdir(), `muonroi-test-export-${Date.now()}`);
53
53
  const muonroiDir = join(tmpHome, ".muonroi-cli");
54
54
  const experienceDir = join(tmpHome, ".experience");
55
55
  mkdirSync(muonroiDir, { recursive: true });
@@ -1,4 +1,6 @@
1
1
  import { type PilContextResponse } from "../pil/schema.js";
2
+ export type { WhoAmIDim, WhoAmIDimName, WhoAmIProfile } from "./who-am-i.js";
3
+ export { getWhoAmIProfile, outputStyleFromProfile, resetWhoAmICache } from "./who-am-i.js";
2
4
  interface EEPoint {
3
5
  id: string | number;
4
6
  score?: number;
@@ -37,7 +39,7 @@ export type { EEPoint, EERouteResult, EERouteTaskResult };
37
39
  * 2. Fat clients (local experience-core.js present) → in-process call.
38
40
  * 3. Otherwise → null (caller falls back to regex/keyword paths).
39
41
  */
40
- export declare function classifyViaBrain(prompt: string, timeoutMs?: number): Promise<string | null>;
42
+ export declare function classifyViaBrain(prompt: string, timeoutMs?: number, options?: import("./types.js").BrainProxyOptions): Promise<string | null>;
41
43
  /**
42
44
  * Search a named Qdrant collection by embedding vector.
43
45
  * Falls back to FileStore inside experience-core.js if Qdrant is unavailable.
@@ -11,6 +11,12 @@ import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { PilContextResponseSchema } from "../pil/schema.js";
13
13
  import { classifyEeError, logEeFailure, readTimeoutEnv, withEeTimeout } from "../utils/ee-logger.js";
14
+ // "Who Am I" v4.0 profile access. Re-exported through the bridge so PIL layers
15
+ // reach it via the single sanctioned EE entry point (see the no-network-in-pil
16
+ // arch guard: layer1/types may import `../ee/bridge` only). The provider is
17
+ // local-only (no network), cached, and fail-open — the same contract as the rest
18
+ // of this bridge.
19
+ export { getWhoAmIProfile, outputStyleFromProfile, resetWhoAmICache } from "./who-am-i.js";
14
20
  /**
15
21
  * Phase 21.5 — `routeModel` / `routeTask` budget. EE server unreachability used
16
22
  * to hang `/ideal` for the OS-level fetch connect timeout (~5 min on Windows).
@@ -82,7 +88,7 @@ async function silentCall(fn) {
82
88
  * 2. Fat clients (local experience-core.js present) → in-process call.
83
89
  * 3. Otherwise → null (caller falls back to regex/keyword paths).
84
90
  */
85
- export async function classifyViaBrain(prompt, timeoutMs = 5000) {
91
+ export async function classifyViaBrain(prompt, timeoutMs = 5000, options) {
86
92
  // Thin-client HTTP path — required for CLIs without local experience-core.js.
87
93
  try {
88
94
  const { getCachedEEClientMode } = await import("./client-mode.js");
@@ -92,7 +98,7 @@ export async function classifyViaBrain(prompt, timeoutMs = 5000) {
92
98
  : !!(await import("./auth.js")).getCachedServerBaseUrl();
93
99
  if (useRemote) {
94
100
  const { getDefaultEEClient } = await import("./intercept.js");
95
- const result = await getDefaultEEClient().brainProxy(prompt, timeoutMs);
101
+ const result = await getDefaultEEClient().brainProxy(prompt, timeoutMs, options);
96
102
  if (typeof result === "string")
97
103
  return result;
98
104
  // Server unreachable — fall through to in-process path so a fat-client
@@ -110,7 +116,7 @@ export async function classifyViaBrain(prompt, timeoutMs = 5000) {
110
116
  try {
111
117
  // Race the in-process call against `timeoutMs + 200ms` so a buggy core that
112
118
  // ignores its own timeout argument cannot hang the host CLI.
113
- return await withEeTimeout(silentCall(() => core.classifyViaBrain(prompt, timeoutMs)), timeoutMs + 200);
119
+ return await withEeTimeout(silentCall(() => core.classifyViaBrain(prompt, timeoutMs, options)), timeoutMs + 200);
114
120
  }
115
121
  catch (err) {
116
122
  logEeFailure("bridge.classifyViaBrain", classifyEeError(err), err, {
@@ -52,7 +52,7 @@ describe("bridge — loaded core", () => {
52
52
  it("Test 1: classifyViaBrain returns classification string when core loaded", async () => {
53
53
  const result = await classifyViaBrain("write a function to sort an array");
54
54
  expect(result).toBe("generate");
55
- expect(mockCore.classifyViaBrain).toHaveBeenCalledWith("write a function to sort an array", 5000);
55
+ expect(mockCore.classifyViaBrain).toHaveBeenCalledWith("write a function to sort an array", 5000, undefined);
56
56
  });
57
57
  it("Test 2: searchCollection returns EEPoint[] when core loaded", async () => {
58
58
  const vector = [0.1, 0.2, 0.3];
@@ -210,7 +210,6 @@ describe("bridge — config isolation (BRIDGE-03)", () => {
210
210
  const _validCall = () => classifyViaBrain("prompt");
211
211
  const _validCallWithTimeout = () => classifyViaBrain("prompt", 3000);
212
212
  // These would cause @ts-expect-error if uncommented:
213
- // @ts-expect-error — qdrantUrl is not a valid parameter
214
213
  const _invalidCall = () => classifyViaBrain("prompt", 3000, "http://localhost:6333");
215
214
  expect(_validCall).toBeDefined();
216
215
  expect(_validCallWithTimeout).toBeDefined();
@@ -568,7 +568,7 @@ export function createEEClient(opts = {}) {
568
568
  return null;
569
569
  }
570
570
  },
571
- async brainProxy(prompt, timeoutMs = 2000) {
571
+ async brainProxy(prompt, timeoutMs = 2000, options) {
572
572
  try {
573
573
  // Tight client-side ceiling (~150ms grace over caller's intent budget)
574
574
  // so we never wait longer than the PIL pipeline can use the result.
@@ -576,10 +576,23 @@ export function createEEClient(opts = {}) {
576
576
  // we forward in the body — both ends abort together.
577
577
  const ctrl = new AbortController();
578
578
  const t = setTimeout(() => ctrl.abort(), timeoutMs + 150);
579
+ const body = { prompt, timeoutMs };
580
+ if (options) {
581
+ if (options.systemPrompt)
582
+ body.systemPrompt = options.systemPrompt;
583
+ if (options.responseFormat)
584
+ body.responseFormat = options.responseFormat;
585
+ if (options.model)
586
+ body.model = options.model;
587
+ if (options.maxTokens != null)
588
+ body.maxTokens = options.maxTokens;
589
+ if (options.provider)
590
+ body.provider = options.provider;
591
+ }
579
592
  const resp = await f(`${baseUrl}/api/brain`, {
580
593
  method: "POST",
581
594
  headers: headers(),
582
- body: JSON.stringify({ prompt, timeoutMs }),
595
+ body: JSON.stringify(body),
583
596
  signal: ctrl.signal,
584
597
  });
585
598
  clearTimeout(t);