muonroi-cli 1.8.5 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  3. package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
  4. package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
  7. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
  9. package/dist/src/agent-harness/mock-model.d.ts +10 -0
  10. package/dist/src/agent-harness/mock-model.js +6 -2
  11. package/dist/src/chat/chat-keychain.d.ts +7 -12
  12. package/dist/src/chat/chat-keychain.js +19 -86
  13. package/dist/src/cli/keys.d.ts +8 -45
  14. package/dist/src/cli/keys.js +18 -324
  15. package/dist/src/council/clarifier.d.ts +4 -2
  16. package/dist/src/council/clarifier.js +158 -36
  17. package/dist/src/council/debate-planner.js +3 -2
  18. package/dist/src/council/debate.js +59 -5
  19. package/dist/src/council/index.d.ts +23 -0
  20. package/dist/src/council/index.js +37 -2
  21. package/dist/src/council/llm.d.ts +62 -0
  22. package/dist/src/council/llm.js +123 -23
  23. package/dist/src/council/panel-select.js +13 -3
  24. package/dist/src/council/planner.js +17 -0
  25. package/dist/src/council/preflight.d.ts +10 -0
  26. package/dist/src/council/preflight.js +36 -0
  27. package/dist/src/council/prompts.d.ts +9 -1
  28. package/dist/src/council/prompts.js +23 -6
  29. package/dist/src/council/types.d.ts +10 -0
  30. package/dist/src/ee/auth.d.ts +19 -0
  31. package/dist/src/ee/auth.js +39 -0
  32. package/dist/src/ee/client.js +28 -3
  33. package/dist/src/ee/ee-onboarding.js +6 -26
  34. package/dist/src/flow/compaction/compress.d.ts +2 -2
  35. package/dist/src/flow/compaction/compress.js +21 -8
  36. package/dist/src/flow/compaction/extract.d.ts +3 -3
  37. package/dist/src/flow/compaction/extract.js +6 -6
  38. package/dist/src/flow/compaction/index.d.ts +2 -1
  39. package/dist/src/flow/compaction/index.js +29 -3
  40. package/dist/src/flow/compaction/progress.d.ts +35 -0
  41. package/dist/src/flow/compaction/progress.js +35 -0
  42. package/dist/src/generated/version.d.ts +1 -1
  43. package/dist/src/generated/version.js +1 -1
  44. package/dist/src/gsd/flags.d.ts +11 -0
  45. package/dist/src/gsd/flags.js +19 -0
  46. package/dist/src/gsd/plan-council.js +104 -72
  47. package/dist/src/gsd/verdict-schema.d.ts +1 -1
  48. package/dist/src/headless/council-answers.js +4 -0
  49. package/dist/src/index.js +129 -260
  50. package/dist/src/lsp/builtins.js +3 -1
  51. package/dist/src/lsp/manager.d.ts +5 -1
  52. package/dist/src/lsp/manager.js +249 -3
  53. package/dist/src/lsp/npm-cache.d.ts +11 -1
  54. package/dist/src/lsp/npm-cache.js +17 -1
  55. package/dist/src/lsp/runtime.d.ts +6 -1
  56. package/dist/src/lsp/runtime.js +17 -1
  57. package/dist/src/lsp/types.d.ts +83 -1
  58. package/dist/src/lsp/types.js +10 -0
  59. package/dist/src/mcp/client-pool.js +43 -15
  60. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  61. package/dist/src/mcp/lsp-tools.js +93 -2
  62. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  63. package/dist/src/mcp/mcp-keychain.js +9 -49
  64. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  65. package/dist/src/mcp/setup-guide-text.js +22 -2
  66. package/dist/src/mcp/tools-server.d.ts +10 -0
  67. package/dist/src/mcp/tools-server.js +10 -2
  68. package/dist/src/models/catalog.json +19 -19
  69. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  70. package/dist/src/orchestrator/ask-user.js +65 -0
  71. package/dist/src/orchestrator/compaction.d.ts +2 -3
  72. package/dist/src/orchestrator/compaction.js +8 -8
  73. package/dist/src/orchestrator/council-manager.js +9 -8
  74. package/dist/src/orchestrator/council-request.d.ts +49 -0
  75. package/dist/src/orchestrator/council-request.js +62 -0
  76. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  77. package/dist/src/orchestrator/interactive-pause.js +36 -0
  78. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  79. package/dist/src/orchestrator/message-processor.js +26 -8
  80. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  81. package/dist/src/orchestrator/orchestrator.js +204 -50
  82. package/dist/src/orchestrator/preprocessor.js +2 -2
  83. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  84. package/dist/src/orchestrator/safety-askcard.js +5 -2
  85. package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
  86. package/dist/src/orchestrator/safety-intercept.js +7 -0
  87. package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
  88. package/dist/src/orchestrator/stall-watchdog.js +24 -3
  89. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  90. package/dist/src/orchestrator/stream-runner.js +54 -21
  91. package/dist/src/orchestrator/tool-engine.d.ts +19 -0
  92. package/dist/src/orchestrator/tool-engine.js +241 -25
  93. package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
  94. package/dist/src/orchestrator/turn-watchdog.js +38 -9
  95. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  96. package/dist/src/pil/agent-operating-contract.js +6 -4
  97. package/dist/src/pil/discovery.d.ts +1 -1
  98. package/dist/src/pil/discovery.js +2 -2
  99. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  100. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  101. package/dist/src/pil/llm-classify.d.ts +78 -3
  102. package/dist/src/pil/llm-classify.js +351 -111
  103. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  104. package/dist/src/pil/native-capabilities-workbook.js +7 -0
  105. package/dist/src/pil/pipeline.js +2 -0
  106. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  107. package/dist/src/pil/repo-grounding-probe.js +136 -0
  108. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  109. package/dist/src/pil/repo-structure-hints.js +45 -0
  110. package/dist/src/product-loop/artifact-io.js +4 -0
  111. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  112. package/dist/src/product-loop/criteria-seed.js +200 -0
  113. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  114. package/dist/src/product-loop/discovery-interview.js +37 -18
  115. package/dist/src/product-loop/discovery-recommender.js +2 -1
  116. package/dist/src/product-loop/discovery-schema.js +14 -1
  117. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  118. package/dist/src/product-loop/discovery-triage.js +109 -0
  119. package/dist/src/product-loop/gather.js +150 -2
  120. package/dist/src/product-loop/index.js +7 -0
  121. package/dist/src/product-loop/loop-driver.js +21 -8
  122. package/dist/src/product-loop/phase-plan.d.ts +16 -0
  123. package/dist/src/product-loop/phase-plan.js +42 -4
  124. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  125. package/dist/src/product-loop/phase-rituals.js +8 -3
  126. package/dist/src/product-loop/phase-runner.js +30 -11
  127. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  128. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  129. package/dist/src/product-loop/sprint-runner.d.ts +62 -0
  130. package/dist/src/product-loop/sprint-runner.js +309 -8
  131. package/dist/src/product-loop/types.d.ts +25 -0
  132. package/dist/src/providers/anthropic.d.ts +9 -8
  133. package/dist/src/providers/anthropic.js +13 -47
  134. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  135. package/dist/src/providers/auth/grok-oauth.js +30 -5
  136. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  137. package/dist/src/providers/auth/openai-oauth.js +14 -0
  138. package/dist/src/providers/auth/token-store.d.ts +9 -9
  139. package/dist/src/providers/auth/token-store.js +8 -70
  140. package/dist/src/providers/auth/types.d.ts +8 -0
  141. package/dist/src/providers/env-store.d.ts +17 -0
  142. package/dist/src/providers/env-store.js +228 -0
  143. package/dist/src/providers/keychain.d.ts +21 -17
  144. package/dist/src/providers/keychain.js +124 -135
  145. package/dist/src/providers/runtime.d.ts +24 -9
  146. package/dist/src/providers/runtime.js +48 -37
  147. package/dist/src/providers/strategies/thinking-mode.js +9 -1
  148. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  149. package/dist/src/providers/warm.d.ts +65 -0
  150. package/dist/src/providers/warm.js +145 -0
  151. package/dist/src/self-qa/agentic-loop.js +3 -2
  152. package/dist/src/storage/transcript.js +56 -2
  153. package/dist/src/tools/git-safety.d.ts +19 -0
  154. package/dist/src/tools/git-safety.js +168 -0
  155. package/dist/src/tools/native-tools.d.ts +1 -1
  156. package/dist/src/tools/native-tools.js +76 -1
  157. package/dist/src/tools/registry.d.ts +17 -0
  158. package/dist/src/tools/registry.js +116 -1
  159. package/dist/src/types/index.d.ts +29 -1
  160. package/dist/src/ui/app.js +67 -4
  161. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  162. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  163. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  164. package/dist/src/ui/components/compact-progress-card.js +42 -0
  165. package/dist/src/ui/components/council-phase-timeline.js +17 -2
  166. package/dist/src/ui/components/council-question-card.js +1 -0
  167. package/dist/src/ui/components/message-view.d.ts +15 -0
  168. package/dist/src/ui/components/message-view.js +50 -1
  169. package/dist/src/ui/components/tool-group.d.ts +15 -3
  170. package/dist/src/ui/components/tool-group.js +69 -11
  171. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  172. package/dist/src/ui/containers/modals-layer.js +2 -2
  173. package/dist/src/ui/council-harness-event.d.ts +57 -0
  174. package/dist/src/ui/council-harness-event.js +46 -0
  175. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  176. package/dist/src/ui/heartbeat-debug.js +45 -0
  177. package/dist/src/ui/modals/api-key-modal.js +1 -1
  178. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  179. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  180. package/dist/src/ui/slash/ee.js +81 -0
  181. package/dist/src/ui/slash/menu-items.js +11 -2
  182. package/dist/src/ui/use-app-logic.js +354 -224
  183. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  184. package/dist/src/ui/utils/agent-activities.js +96 -0
  185. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  186. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  187. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  188. package/dist/src/ui/utils/tool-summary.js +91 -0
  189. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  190. package/dist/src/utils/event-loop-monitor.js +107 -0
  191. package/dist/src/utils/llm-deadline.d.ts +14 -0
  192. package/dist/src/utils/llm-deadline.js +19 -0
  193. package/dist/src/utils/loop-profiler.d.ts +102 -0
  194. package/dist/src/utils/loop-profiler.js +202 -0
  195. package/dist/src/utils/settings.d.ts +27 -0
  196. package/dist/src/utils/settings.js +38 -2
  197. package/dist/src/utils/side-question.d.ts +1 -2
  198. package/dist/src/utils/side-question.js +2 -2
  199. package/dist/src/verify/entrypoint.js +51 -16
  200. package/dist/src/verify/orchestrator.d.ts +1 -1
  201. package/dist/src/verify/orchestrator.js +20 -3
  202. package/package.json +1 -2
  203. package/dist/src/cli/bw-vault.d.ts +0 -55
  204. package/dist/src/cli/bw-vault.js +0 -133
  205. package/dist/src/mcp/ee-tools.d.ts +0 -46
  206. package/dist/src/mcp/ee-tools.js +0 -194
@@ -1,3 +1,5 @@
1
+ import type { LanguageModel, ToolSet } from "ai";
2
+ import { stepCountIs } from "ai";
1
3
  import type { BashTool } from "../tools/bash.js";
2
4
  import type { AgentMode, CouncilStatusPhase, StreamChunk } from "../types/index.js";
3
5
  import type { CouncilLLM, CouncilStats } from "./types.js";
@@ -25,6 +27,56 @@ export declare function recordCouncilUsage(sessionId: string | undefined, modelI
25
27
  declare global {
26
28
  var __muonroiMockLlm: any;
27
29
  }
30
+ /** Record `chars` of streamed output (text OR reasoning) against the live window. */
31
+ export declare function noteCouncilStreamDelta(chars: number): void;
32
+ /**
33
+ * Open a liveness window and return a reader for it.
34
+ *
35
+ * `streamedChars` counts only what streamed SINCE the window opened, and
36
+ * `lastDeltaAgeMs` is measured from the window's own start until the first
37
+ * delta lands — so a cold stall reads as an age growing from 0, never as a
38
+ * bogus epoch-sized number. Reasoning-delta counts toward both: a reasoning
39
+ * model emits reasoning tokens for minutes before any text, and that is
40
+ * exactly the window an operator must not mistake for a hang.
41
+ */
42
+ export declare function councilStreamLivenessReader(): () => {
43
+ streamedChars: number;
44
+ lastDeltaAgeMs: number;
45
+ };
46
+ declare function collectStreamText(args: {
47
+ model: LanguageModel;
48
+ system: string;
49
+ prompt: string;
50
+ maxOutputTokens?: number;
51
+ temperature?: number;
52
+ providerOptions?: Record<string, unknown>;
53
+ abortSignal?: AbortSignal;
54
+ tools?: ToolSet;
55
+ stopWhen?: ReturnType<typeof stepCountIs>;
56
+ prepareStep?: (opts: {
57
+ stepNumber: number;
58
+ messages: readonly unknown[];
59
+ }) => unknown;
60
+ /** Called with the char length of every text/reasoning delta as it arrives. */
61
+ onDelta?: (chars: number) => void;
62
+ }): Promise<{
63
+ text: string;
64
+ usage?: unknown;
65
+ finishReason?: string;
66
+ reasoningText?: string;
67
+ toolCalls: Array<{
68
+ toolName: string;
69
+ input?: unknown;
70
+ result?: unknown;
71
+ }>;
72
+ }>;
73
+ /**
74
+ * Test-only handle on the private stream collector, so `onDelta` (the liveness
75
+ * signal that tells a slow reasoning call from a hung one) is directly
76
+ * assertable without standing up a full council run. Not for production use.
77
+ * @internal
78
+ */
79
+ export declare const __testCollectStreamText: typeof collectStreamText;
28
80
  export declare function createCouncilLLM(bash: BashTool, mode: AgentMode, sessionId: string | undefined, stats: CouncilStats): CouncilLLM;
29
81
  interface TracedGenerateArgs {
30
82
  phase: CouncilStatusPhase;
@@ -62,6 +114,16 @@ interface TracedAsyncArgs {
62
114
  detail?: string;
63
115
  role?: string;
64
116
  tickIntervalMs?: number;
117
+ /**
118
+ * Optional push-based stream liveness reader (see {@link councilStreamLivenessReader}).
119
+ * When supplied, every tick/done carries the chars streamed inside this window
120
+ * and the age of the last delta — so a frozen `elapsedMs` (generator not being
121
+ * pumped) can still be told apart from a genuinely stuck call.
122
+ */
123
+ liveness?: () => {
124
+ streamedChars: number;
125
+ lastDeltaAgeMs: number;
126
+ };
65
127
  }
66
128
  /**
67
129
  * Generic version of {@link tracedGenerate} for arbitrary async work
@@ -7,7 +7,8 @@ import { buildMcpToolSet } from "../mcp/runtime.js";
7
7
  import { getModelInfo } from "../models/registry.js";
8
8
  import { getProviderCapabilities, resolveTemperature } from "../providers/capabilities.js";
9
9
  import { loadKeyForProvider, ProviderKeyMissingError } from "../providers/keychain.js";
10
- import { createProviderFactoryAsync, detectProviderForModel, resolveModelRuntime } from "../providers/runtime.js";
10
+ import { createProviderFactoryAsync, detectProviderForModel, resolveModelRuntime, shouldDropParam, } from "../providers/runtime.js";
11
+ import { wireDebug } from "../providers/wire-debug.js";
11
12
  import { statusBarStore } from "../state/status-bar-store.js";
12
13
  import { recordUsageEvent } from "../storage/index.js";
13
14
  import { createBuiltinTools as createTools } from "../tools/registry.js";
@@ -20,7 +21,11 @@ import { withVisibleRetry } from "../utils/visible-retry.js";
20
21
  import { buildResearchSystemPrompt } from "./prompts.js";
21
22
  import { stripThinkBlocks } from "./strip-think.js";
22
23
  /**
23
- * Resolve a provider factory for a council sub-call, OAuth-aware.
24
+ * Register a provider factory for a council sub-call, OAuth-aware.
25
+ *
26
+ * A council roster routes stances to providers the session itself never built,
27
+ * so their factory must exist in the registry before `resolveModelRuntime` can
28
+ * derive it from the model id.
24
29
  *
25
30
  * The council reachability gate (`isProviderReachable` in leader.ts) counts
26
31
  * OAuth-authenticated providers as usable (via `getConfiguredProviders`), so a
@@ -33,7 +38,7 @@ import { stripThinkBlocks } from "./strip-think.js";
33
38
  * refreshes the stored OAuth bearer token. Only the expected missing-key case
34
39
  * is swallowed; unexpected errors propagate.
35
40
  */
36
- async function resolveCouncilFactory(providerId) {
41
+ async function ensureCouncilFactory(providerId) {
37
42
  let apiKey;
38
43
  try {
39
44
  apiKey = await loadKeyForProvider(providerId);
@@ -43,8 +48,7 @@ async function resolveCouncilFactory(providerId) {
43
48
  throw err;
44
49
  // OAuth-only provider — createProviderFactoryAsync injects the bearer token.
45
50
  }
46
- const { factory } = await createProviderFactoryAsync(providerId, apiKey ? { apiKey } : {});
47
- return factory;
51
+ await createProviderFactoryAsync(providerId, apiKey ? { apiKey } : {});
48
52
  }
49
53
  function getDebugLogPath() {
50
54
  const p = process.env.MUONROI_COUNCIL_DEBUG_LOG;
@@ -297,13 +301,64 @@ const COUNCIL_LLM_TIMEOUT_MS = (() => {
297
301
  * `generateText` result exposed. A debater on the codex/oauth endpoint hit the
298
302
  * exact same non-stream 400 as the eval path — streaming fixes it uniformly.
299
303
  */
304
+ /**
305
+ * Spread for the `maxOutputTokens` param, omitted when the resolved runtime
306
+ * rejects it. The ChatGPT Codex OAuth endpoint (chatgpt.com/backend-api/codex/
307
+ * responses) 400s with `{"detail":"Unsupported parameter: max_output_tokens"}`
308
+ * on EVERY council sub-call (clarify / debate / research) — every council
309
+ * generate on a Codex-OAuth session was failing wholesale. Mirrors the
310
+ * orchestrator + classify paths, which already gate this param via
311
+ * `shouldDropParam`.
312
+ */
313
+ function maxOutSpread(runtime, n) {
314
+ return shouldDropParam(runtime, "maxOutputTokens") ? {} : { maxOutputTokens: n };
315
+ }
316
+ /**
317
+ * Push-based stream liveness — module-level because the only readers
318
+ * (`tracedAsync` wrappers in debate.ts) sit OUTSIDE the `CouncilLLM` interface
319
+ * and cannot reach the in-flight `collectStreamText` any other way.
320
+ *
321
+ * Why it exists. `tracedAsync`'s 1s tick only materializes when the CONSUMER
322
+ * pulls `.next()`; a round that awaits its pairs via `Promise.all` stops
323
+ * pumping the tick generator, so `elapsedMs` FREEZES and a slow-but-alive run
324
+ * is indistinguishable from a hung one (observed: tick frozen at 33142ms for
325
+ * 8+ minutes while status was still "tick"). Token deltas are PUSHED here as
326
+ * they arrive, so liveness survives any back-pressure on the generator.
327
+ */
328
+ let councilStreamedCharsTotal = 0;
329
+ let councilLastDeltaAt = 0;
330
+ /** Record `chars` of streamed output (text OR reasoning) against the live window. */
331
+ export function noteCouncilStreamDelta(chars) {
332
+ if (chars <= 0)
333
+ return;
334
+ councilStreamedCharsTotal += chars;
335
+ councilLastDeltaAt = Date.now();
336
+ }
337
+ /**
338
+ * Open a liveness window and return a reader for it.
339
+ *
340
+ * `streamedChars` counts only what streamed SINCE the window opened, and
341
+ * `lastDeltaAgeMs` is measured from the window's own start until the first
342
+ * delta lands — so a cold stall reads as an age growing from 0, never as a
343
+ * bogus epoch-sized number. Reasoning-delta counts toward both: a reasoning
344
+ * model emits reasoning tokens for minutes before any text, and that is
345
+ * exactly the window an operator must not mistake for a hang.
346
+ */
347
+ export function councilStreamLivenessReader() {
348
+ const baseChars = councilStreamedCharsTotal;
349
+ const windowStart = Date.now();
350
+ return () => ({
351
+ streamedChars: councilStreamedCharsTotal - baseChars,
352
+ lastDeltaAgeMs: Date.now() - (councilLastDeltaAt > windowStart ? councilLastDeltaAt : windowStart),
353
+ });
354
+ }
300
355
  async function collectStreamText(args) {
301
356
  const hasTools = !!args.tools && Object.keys(args.tools).length > 0;
302
357
  const result = streamText({
303
358
  model: args.model,
304
359
  system: args.system,
305
360
  prompt: args.prompt,
306
- maxOutputTokens: args.maxOutputTokens,
361
+ ...(args.maxOutputTokens === undefined ? {} : { maxOutputTokens: args.maxOutputTokens }),
307
362
  maxRetries: 0,
308
363
  ...(args.temperature === undefined ? {} : { temperature: args.temperature }),
309
364
  ...(args.providerOptions ? { providerOptions: args.providerOptions } : {}),
@@ -318,12 +373,21 @@ async function collectStreamText(args) {
318
373
  const byId = new Map();
319
374
  for await (const part of result.fullStream) {
320
375
  switch (part.type) {
321
- case "text-delta":
322
- text += part.text ?? "";
376
+ case "text-delta": {
377
+ const d = part.text ?? "";
378
+ text += d;
379
+ args.onDelta?.(d.length);
323
380
  break;
324
- case "reasoning-delta":
325
- reasoningText += part.text ?? "";
381
+ }
382
+ case "reasoning-delta": {
383
+ // Reasoning deltas count as liveness: a reasoning model streams these
384
+ // for minutes before its first text-delta, and that silent-on-text
385
+ // window is precisely what gets misread as a hang.
386
+ const d = part.text ?? "";
387
+ reasoningText += d;
388
+ args.onDelta?.(d.length);
326
389
  break;
390
+ }
327
391
  case "tool-call": {
328
392
  const p = part;
329
393
  const tc = { toolName: p.toolName, input: p.input };
@@ -352,6 +416,13 @@ async function collectStreamText(args) {
352
416
  }
353
417
  return { text, usage, finishReason, reasoningText: reasoningText || undefined, toolCalls };
354
418
  }
419
+ /**
420
+ * Test-only handle on the private stream collector, so `onDelta` (the liveness
421
+ * signal that tells a slow reasoning call from a hung one) is directly
422
+ * assertable without standing up a full council run. Not for production use.
423
+ * @internal
424
+ */
425
+ export const __testCollectStreamText = collectStreamText;
355
426
  export function createCouncilLLM(bash, mode, sessionId, stats) {
356
427
  return {
357
428
  async generate(modelId, system, prompt, maxTokens = 4096, onUsage, signal) {
@@ -362,8 +433,8 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
362
433
  return stripThinkBlocks(result.text);
363
434
  }
364
435
  const providerId = detectProviderForModel(modelId);
365
- const factory = await resolveCouncilFactory(providerId);
366
- const runtime = resolveModelRuntime(factory, modelId);
436
+ await ensureCouncilFactory(providerId);
437
+ const runtime = resolveModelRuntime(modelId);
367
438
  const t0 = Date.now();
368
439
  // Combine the user-abort signal (when threaded from runCouncil) with the
369
440
  // per-call wall-clock deadline. Without the parent signal, an Esc/Ctrl-C
@@ -381,7 +452,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
381
452
  model: runtime.model,
382
453
  system,
383
454
  prompt,
384
- maxOutputTokens: maxTokens,
455
+ ...maxOutSpread(runtime, maxTokens),
385
456
  // Never hardcode temperature: some upstreams (Moonshot/Kimi via
386
457
  // opencode-go) reject any value but their pinned one, which
387
458
  // failed every clarify/spec call on a Kimi session. resolveTemperature
@@ -389,6 +460,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
389
460
  temperature: resolveTemperature(providerId, runtime.modelInfo, 0.7),
390
461
  providerOptions: runtime.providerOptions,
391
462
  abortSignal: timedSignal,
463
+ onDelta: noteCouncilStreamDelta,
392
464
  }), { label: "council.generate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.generate", signal);
393
465
  cleanupTimeout();
394
466
  stats.calls++;
@@ -426,6 +498,12 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
426
498
  }
427
499
  catch (err) {
428
500
  cleanupTimeout();
501
+ // Capture the provider-side detail (status code + response body +
502
+ // request param shape) that `err.message` alone drops — a generic
503
+ // "Bad Request" on the council path was previously undiagnosable
504
+ // because collectStreamText re-throws only the message. PII-safe,
505
+ // no-op unless MUONROI_DEBUG_LLM_WIRE=1. (No-Silent-Catch.)
506
+ wireDebug.logError(providerId, err);
429
507
  writeDebugRecord({
430
508
  ts: new Date().toISOString(),
431
509
  kind: "generate",
@@ -452,8 +530,8 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
452
530
  return { text: stripThinkBlocks(result.text), toolCalls: [] };
453
531
  }
454
532
  const providerId = detectProviderForModel(modelId);
455
- const factory = await resolveCouncilFactory(providerId);
456
- const runtime = resolveModelRuntime(factory, modelId);
533
+ await ensureCouncilFactory(providerId);
534
+ const runtime = resolveModelRuntime(modelId);
457
535
  // Verification tools — re-introduced after the no-tools fix (session
458
536
  // a7a5690d2049). The original failure was stepCountIs(4) + full toolset
459
537
  // letting reasoning models exhaust steps on exploratory tool chains and
@@ -484,13 +562,16 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
484
562
  }
485
563
  }
486
564
  }
487
- catch {
488
- /* MCP optional — debate continues with builtins only */
565
+ catch (err) {
566
+ // MCP is optional here — debate continues with builtins only — but a
567
+ // silent swallow hid which server failed and why. (No-Silent-Catch.)
568
+ console.error(`[council/llm] debate MCP tool discovery failed, continuing with builtins only: ${err instanceof Error ? err.message : String(err)}`, { modelId, provider: providerId });
489
569
  }
490
570
  verificationTools = wrapToolsWithEeCheck(filtered, sessionId ?? "council-debate");
491
571
  }
492
- catch {
493
- /* fail-open: no tools */
572
+ catch (err) {
573
+ // fail-open: the debate turn still runs, just without verification tools.
574
+ console.error(`[council/llm] debate verification toolset build failed, running without tools: ${err instanceof Error ? err.message : String(err)}`, { modelId, provider: providerId });
494
575
  }
495
576
  }
496
577
  // sanitizeHistory is identity for every provider (kept as a hook for
@@ -515,11 +596,15 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
515
596
  // text. E2E showed 2048 caused finishReason=length on 3KB debate
516
597
  // prompts. 6144 leaves ~4000 tokens for text after typical reasoning
517
598
  // overhead and avoids cuts mid-thought.
518
- maxOutputTokens: 6144,
599
+ ...maxOutSpread(runtime, 6144),
519
600
  // See generate(): capability-aware temperature (omit / clamp).
520
601
  temperature: resolveTemperature(providerId, runtime.modelInfo, 0.7),
521
602
  providerOptions: runtime.providerOptions,
522
603
  abortSignal: timedSignal,
604
+ // Push liveness for the round wrapper: a debate turn on a
605
+ // reasoning model runs near the 5-min ceiling, and the tick
606
+ // generator is not being pumped while Promise.all awaits.
607
+ onDelta: noteCouncilStreamDelta,
523
608
  ...(verificationTools && Object.keys(verificationTools).length > 0
524
609
  ? {
525
610
  tools: verificationTools,
@@ -611,8 +696,8 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
611
696
  return stripThinkBlocks(result.text);
612
697
  }
613
698
  const providerId = detectProviderForModel(modelId);
614
- const factory = await resolveCouncilFactory(providerId);
615
- const runtime = resolveModelRuntime(factory, modelId);
699
+ await ensureCouncilFactory(providerId);
700
+ const runtime = resolveModelRuntime(modelId);
616
701
  const builtinTools = createTools(bash, mode);
617
702
  // CQ-03: Lazy MCP bundle per research call — fail-open so builtins remain available
618
703
  let mcpBundle = null;
@@ -668,7 +753,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
668
753
  const stripped = researchCaps.sanitizeHistory(messages);
669
754
  return stripped === messages ? {} : { messages: stripped };
670
755
  },
671
- maxOutputTokens: 4096,
756
+ ...maxOutSpread(runtime, 4096),
672
757
  // See generate(): capability-aware temperature (omit / clamp).
673
758
  ...(() => {
674
759
  const t = resolveTemperature(providerId, runtime.modelInfo, 0.3);
@@ -894,6 +979,19 @@ export async function* tracedAsync(fn, args) {
894
979
  : `status-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
895
980
  const start = Date.now();
896
981
  const tickInterval = args.tickIntervalMs ?? 1000;
982
+ /** Never let a liveness read break the heartbeat it is meant to enrich. */
983
+ const livenessFields = () => {
984
+ if (!args.liveness)
985
+ return {};
986
+ try {
987
+ const l = args.liveness();
988
+ return { streamedChars: l.streamedChars, lastDeltaAgeMs: l.lastDeltaAgeMs };
989
+ }
990
+ catch (err) {
991
+ console.error(`[council/llm] tracedAsync liveness read failed: ${err instanceof Error ? err.message : String(err)}`, { phase: args.phase, label: args.label });
992
+ return {};
993
+ }
994
+ };
897
995
  yield {
898
996
  type: "council_status",
899
997
  councilStatus: {
@@ -939,6 +1037,7 @@ export async function* tracedAsync(fn, args) {
939
1037
  detail: args.detail,
940
1038
  role: args.role,
941
1039
  elapsedMs: Date.now() - start,
1040
+ ...livenessFields(),
942
1041
  },
943
1042
  };
944
1043
  }
@@ -970,6 +1069,7 @@ export async function* tracedAsync(fn, args) {
970
1069
  detail: args.detail,
971
1070
  role: args.role,
972
1071
  elapsedMs: Date.now() - start,
1072
+ ...livenessFields(),
973
1073
  },
974
1074
  };
975
1075
  return result;
@@ -42,17 +42,27 @@ export async function* selectTaskAwarePanel(opts) {
42
42
  maxTokens: 512,
43
43
  });
44
44
  }
45
- catch {
45
+ catch (err) {
46
+ // Fail-open: caller keeps its default roster. Log so a provider outage on
47
+ // the leader model is diagnosable instead of silently degrading panel
48
+ // selection to the prompt-blind roster. (No-Silent-Catch.)
49
+ console.error(`[council/panel-select] leader panel selection call failed, keeping default roster: ${err instanceof Error ? err.message : String(err)}`, { leaderModelId, poolSize: pool.length });
46
50
  return null;
47
51
  }
48
52
  const match = raw.match(/\{[\s\S]*\}/);
49
- if (!match)
53
+ if (!match) {
54
+ console.error("[council/panel-select] leader returned no JSON object, keeping default roster", {
55
+ leaderModelId,
56
+ rawHead: raw.slice(0, 200),
57
+ });
50
58
  return null;
59
+ }
51
60
  let parsed;
52
61
  try {
53
62
  parsed = JSON.parse(match[0]);
54
63
  }
55
- catch {
64
+ catch (err) {
65
+ console.error(`[council/panel-select] leader selection JSON parse failed, keeping default roster: ${err instanceof Error ? err.message : String(err)}`, { leaderModelId, rawHead: match[0].slice(0, 200) });
56
66
  return null;
57
67
  }
58
68
  const valid = new Set(pool.map((c) => c.model));
@@ -1,6 +1,7 @@
1
1
  import { getCouncilLanguage } from "../utils/settings.js";
2
2
  import { tracedGenerate } from "./llm.js";
3
3
  import { phaseDone, phaseError, phaseStart } from "./phase-events.js";
4
+ import { emitPreflightHarnessEvent } from "./preflight.js";
4
5
  import { buildSynthesisPrompt } from "./prompts.js";
5
6
  export async function* runPlanning(debateState, spec, participants, leaderModelId, respondToPreflight, llm, debatePlan,
6
7
  // CQ-18: PIL outputStyle from runCouncil
@@ -145,6 +146,15 @@ planEmphasis) {
145
146
  yield { type: "content", content: `- [${step.priority}] ${step.description}\n` };
146
147
  }
147
148
  const preflightId = crypto.randomUUID();
149
+ emitPreflightHarnessEvent({
150
+ t: "event",
151
+ kind: "askcard-open",
152
+ questionId: preflightId,
153
+ question: `Approve action plan for: ${spec.problemStatement}`,
154
+ phase: "plan-confirm",
155
+ optionCount: 2,
156
+ defaultIndex: 0,
157
+ });
148
158
  yield {
149
159
  type: "council_preflight",
150
160
  content: "Review the action plan above. Approve to proceed with execution, or reject.",
@@ -159,6 +169,13 @@ planEmphasis) {
159
169
  },
160
170
  };
161
171
  const approved = await respondToPreflight(preflightId);
172
+ emitPreflightHarnessEvent({
173
+ t: "event",
174
+ kind: "askcard-answered",
175
+ questionId: preflightId,
176
+ answerKind: "choice",
177
+ answerText: approved ? "approve" : "reject",
178
+ });
162
179
  yield phaseDone({
163
180
  phaseId: "phase:action-plan",
164
181
  kind: "action_plan",
@@ -1,5 +1,15 @@
1
1
  import type { StreamChunk } from "../types/index.js";
2
2
  import type { ClarifiedSpec, PreflightResponder } from "./types.js";
3
+ /**
4
+ * Emit a harness LiveEvent for a preflight/plan-confirm approval gate.
5
+ *
6
+ * These gates are surfaced as `council_preflight` StreamChunks (a UI render path),
7
+ * NOT as LiveEvents — so a monitor on MUONROI_HARNESS_EVENT_LOG is structurally
8
+ * blind to them, unlike the clarify gate which flows through askcard-open. Emitting
9
+ * askcard-open/answered here lets a wake-at-milestone monitor react to the
10
+ * human-approval pause. Observe-only: never throws, no-op when no agent runtime.
11
+ */
12
+ export declare function emitPreflightHarnessEvent(event: Record<string, unknown>): void;
3
13
  export interface RunPreflightOptions {
4
14
  repoEmpty?: boolean;
5
15
  researchOverridable?: boolean;
@@ -1,4 +1,24 @@
1
1
  import { phaseDone, phaseStart } from "./phase-events.js";
2
+ /**
3
+ * Emit a harness LiveEvent for a preflight/plan-confirm approval gate.
4
+ *
5
+ * These gates are surfaced as `council_preflight` StreamChunks (a UI render path),
6
+ * NOT as LiveEvents — so a monitor on MUONROI_HARNESS_EVENT_LOG is structurally
7
+ * blind to them, unlike the clarify gate which flows through askcard-open. Emitting
8
+ * askcard-open/answered here lets a wake-at-milestone monitor react to the
9
+ * human-approval pause. Observe-only: never throws, no-op when no agent runtime.
10
+ */
11
+ export function emitPreflightHarnessEvent(event) {
12
+ try {
13
+ const ar = globalThis.__muonroiAgentRuntime;
14
+ if (!ar || typeof ar.emitEvent !== "function")
15
+ return;
16
+ ar.emitEvent(event);
17
+ }
18
+ catch {
19
+ /* observe-only */
20
+ }
21
+ }
2
22
  export async function* runPreflight(spec, participants, researchNeeded, respondToPreflight, options) {
3
23
  const preflightId = crypto.randomUUID();
4
24
  const startedAt = Date.now();
@@ -50,6 +70,15 @@ export async function* runPreflight(spec, participants, researchNeeded, respondT
50
70
  });
51
71
  return true;
52
72
  }
73
+ emitPreflightHarnessEvent({
74
+ t: "event",
75
+ kind: "askcard-open",
76
+ questionId: preflightId,
77
+ question: `Approve discussion plan for: ${spec.problemStatement}`,
78
+ phase: "preflight",
79
+ optionCount: 2,
80
+ defaultIndex: 0,
81
+ });
53
82
  yield {
54
83
  type: "council_preflight",
55
84
  content: "Review the discussion brief above. Approve to start debate, or reject to revise.",
@@ -66,6 +95,13 @@ export async function* runPreflight(spec, participants, researchNeeded, respondT
66
95
  },
67
96
  };
68
97
  const approved = await respondToPreflight(preflightId);
98
+ emitPreflightHarnessEvent({
99
+ t: "event",
100
+ kind: "askcard-answered",
101
+ questionId: preflightId,
102
+ answerKind: "choice",
103
+ answerText: approved ? "approve" : "reject",
104
+ });
69
105
  yield phaseDone({
70
106
  phaseId: "phase:preflight",
71
107
  kind: "preflight",
@@ -2,7 +2,15 @@ import type { ClarifiedSpec, DebatePlan, DebateStance, OutputShape } from "./typ
2
2
  export declare function buildClarificationPrompt(topic: string, conversationContext: string, previousQA?: Array<{
3
3
  question: string;
4
4
  answer: string;
5
- }>): {
5
+ }>,
6
+ /**
7
+ * Interview language, already resolved by the caller (runClarification): a
8
+ * concrete language name ("vietnamese", "english", a pinned locale) or "auto".
9
+ * Feeds the shared `buildLanguageRule` so the cards follow the SAME language
10
+ * policy as the debate — the user's conversation language, not English-by-
11
+ * default. Defaults to "auto" so existing no-arg callers keep auto-detection.
12
+ */
13
+ lang?: string): {
6
14
  system: string;
7
15
  prompt: string;
8
16
  };
@@ -1,6 +1,14 @@
1
1
  import { buildStackLockSection } from "./decisions-lock.js";
2
2
  // ── Clarification prompts ────────────────────────────────────────────────────
3
- export function buildClarificationPrompt(topic, conversationContext, previousQA) {
3
+ export function buildClarificationPrompt(topic, conversationContext, previousQA,
4
+ /**
5
+ * Interview language, already resolved by the caller (runClarification): a
6
+ * concrete language name ("vietnamese", "english", a pinned locale) or "auto".
7
+ * Feeds the shared `buildLanguageRule` so the cards follow the SAME language
8
+ * policy as the debate — the user's conversation language, not English-by-
9
+ * default. Defaults to "auto" so existing no-arg callers keep auto-detection.
10
+ */
11
+ lang = "auto") {
4
12
  const qaSection = previousQA?.length
5
13
  ? `\n\n## Already Clarified\n${previousQA.map((qa) => `Q: ${qa.question}\nA: ${qa.answer}`).join("\n\n")}`
6
14
  : "";
@@ -12,6 +20,16 @@ export function buildClarificationPrompt(topic, conversationContext, previousQA)
12
20
  `- SCOPE: what is in/out of scope for THIS change?\n` +
13
21
  `- CONSTRAINTS: hard technical/time/business constraints not already implied by the context.\n` +
14
22
  `- SUCCESS CRITERIA: how "done" is judged, when it isn't already obvious.\n\n` +
23
+ `## Ground your questions in the research (IMPORTANT)\n` +
24
+ `If a "## Scope Research" section is present, it is evidence gathered about THIS topic before you ` +
25
+ `were asked to clarify. Read it first, then use it to ask SHARPER, better-grounded questions — ` +
26
+ `research makes your questions more pointed, it does NOT reduce the need to ask. Treat the research's ` +
27
+ `own guesses/speculation as UNVERIFIED, never as the user's answer: if the research had to assume ` +
28
+ `what the user actually wants (their real goal, the concrete task, the system of record, the target ` +
29
+ `user), that assumption IS the gray area you must confirm with them — do not adopt it silently. ` +
30
+ `In particular, when the request is vague and the research could only speculate about the core goal, ` +
31
+ `asking the user to name that goal/pain concretely is the single highest-value question — ask it. ` +
32
+ `Only skip a question when the research (or the topic/context) settles it with real evidence, not a guess.\n\n` +
15
33
  `## How many questions\n` +
16
34
  `Ask the minimum that unblocks a focused discussion — typically 0-2. A well-scoped topic, or one ` +
17
35
  `whose context already answers the gaps, needs ZERO questions: return []. Do NOT pad to a quota, ` +
@@ -36,11 +54,10 @@ export function buildClarificationPrompt(topic, conversationContext, previousQA)
36
54
  `the project IS the one described in the "## Current Project" section of the context. DO NOT ask which project.\n` +
37
55
  `- Only ask about project identity when the topic mentions multiple distinct projects or external products.\n` +
38
56
  `- Use the project's package.json name and description as implicit context for follow-up questions.\n\n` +
39
- `## Language Rule (mandatory)\n` +
40
- `Write the "question", "why", and every option "label"/"description" in the SAME ` +
41
- `language the user used in the Topic below detect it; if the user wrote Vietnamese, write ` +
42
- `Vietnamese; if English, English. The user reads and answers these on a card, so they must ` +
43
- `NOT default to English. Keep code identifiers, file paths, tech/product names, and JSON keys in English.\n\n` +
57
+ buildLanguageRule(lang) +
58
+ `The user reads and answers these on a card, so the "question", "why", and every option ` +
59
+ `"label"/"description" MUST be in that languagenever default to English for card text, and ` +
60
+ `do NOT mirror the language of any "## Scope Research"/context block; follow the user's language.\n\n` +
44
61
  `Output ONLY a JSON array (no markdown, no preamble):\n` +
45
62
  `[{"question": "...", "why": "why this matters for a focused discussion", "options": [` +
46
63
  `{"label": "option A", "description": "what picking this means and its trade-off — one line", "recommended": true}, ` +
@@ -313,6 +313,16 @@ export interface CouncilConfig {
313
313
  * responder the clarifier + post-debate askcards use.
314
314
  */
315
315
  respondToQuestion?: QuestionResponder;
316
+ /**
317
+ * convene_council path — when true, the mid-debate escalation askcard
318
+ * (runEscalationPrompt) is auto-accepted WITHOUT emitting a blocking
319
+ * council_question card. The convene tool runs the council autonomously
320
+ * mid-agent-turn: there is no interactive user answering the escalation, so a
321
+ * card would hang the tool call. Auto-accept = conclude with the best
322
+ * synthesis so far. No decision is hardcoded post-synthesis — the calling
323
+ * agent decides what to do with the returned conclusion.
324
+ */
325
+ convenePath?: boolean;
316
326
  /**
317
327
  * C (mid-debate checkpoint) — directory to persist the per-round debate
318
328
  * checkpoint (`debate-checkpoint.json`), normally the run dir
@@ -25,6 +25,25 @@ export declare function refreshAuthToken(opts?: {
25
25
  export declare function writeExperienceConfig(patch: Partial<ExperienceConfig>, opts?: {
26
26
  home?: string;
27
27
  }): Promise<void>;
28
+ /** Where the EE config lives, for display in setup/status output. */
29
+ export declare function getExperienceConfigPath(opts?: {
30
+ home?: string;
31
+ }): string;
32
+ /**
33
+ * Best-effort reachability probe against an EE server's /health.
34
+ *
35
+ * Returns a reason on failure rather than a bare boolean: "saved but not
36
+ * reachable" and "saved, server says 401" are different problems for the user
37
+ * to act on, and collapsing them to `false` hides which one they have.
38
+ * Never throws — a probe is diagnostics, and must not fail a config write that
39
+ * already succeeded.
40
+ */
41
+ export declare function probeEEHealth(baseUrl: string, token?: string, opts?: {
42
+ timeoutMs?: number;
43
+ }): Promise<{
44
+ ok: boolean;
45
+ detail: string;
46
+ }>;
28
47
  export declare function getCachedAuthToken(): string | null;
29
48
  export declare function getEmbeddingModelVersion(): string;
30
49
  export declare function getCachedServerBaseUrl(): string | null;