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
@@ -13,7 +13,7 @@ import { getModelInfo, normalizeModelId } from "../models/registry.js";
13
13
  import { getProviderCapabilities } from "../providers/capabilities.js";
14
14
  import { apiBaseFor } from "../providers/endpoints.js";
15
15
  import { loadKeyForProvider } from "../providers/keychain.js";
16
- import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
16
+ import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
17
17
  import { ALL_PROVIDER_IDS } from "../providers/types.js";
18
18
  import { statusBarStore } from "../state/status-bar-store.js";
19
19
  import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionChainInfos, getSessionTotalTokens, loadSessionChainTranscriptState, logInteraction, markMessageCompleted, persistApprovedPlan, recordUsageEvent, SessionStore, } from "../storage/index.js";
@@ -30,6 +30,7 @@ import { runSideQuestion } from "../utils/side-question.js";
30
30
  import { buildVerifyDetectPrompt, inferVerifyProjectProfile, normalizeVerifyRecipe, shouldTrustDeterministicRecipe, } from "../verify/entrypoint.js";
31
31
  import { runVerifyOrchestration } from "../verify/orchestrator.js";
32
32
  import { COUNCIL_COLOR_BG, COUNCIL_COLOR_RESET, COUNCIL_ROLE_COLORS, } from "./agent-options.js";
33
+ import { ASK_USER_DISMISSED } from "./ask-user.js";
33
34
  import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
34
35
  import { BatchTurnRunner } from "./batch-turn-runner.js";
35
36
  import { accumulateUsage, buildAssistantBatchMessage, buildBatchChatCompletionRequest, buildBatchName, buildToolBatchMessage, extractJsonObject, getBatchUsage, hasUsage, parseToolArgumentsOrRaw, toLocalToolCall, } from "./batch-utils.js";
@@ -38,6 +39,7 @@ import { CouncilManager } from "./council-manager.js";
38
39
  import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
39
40
  import { DelegationManager } from "./delegations.js";
40
41
  import { loadFlowResumeDigest } from "./flow-resume.js";
42
+ import { beginInteractivePause, endInteractivePause, isInteractivePaused } from "./interactive-pause.js";
41
43
  import { MessageProcessor } from "./message-processor.js";
42
44
  import { lastPersistedSeq } from "./message-seq.js";
43
45
  import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
@@ -109,10 +111,10 @@ function sanitizeTitle(raw) {
109
111
  * Generate a session title with a single lightweight LLM call on the current
110
112
  * session model. Falls back to a truncated first-message title on any failure.
111
113
  */
112
- async function genTitle(provider, userMessage, modelId) {
114
+ async function genTitle(userMessage, modelId) {
113
115
  try {
114
116
  const { generateText } = await import("ai");
115
- const runtime = resolveModelRuntime(provider, modelId);
117
+ const runtime = resolveModelRuntime(modelId);
116
118
  const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
117
119
  const { text, usage } = await generateText({
118
120
  model: runtime.model,
@@ -133,13 +135,6 @@ async function genTitle(provider, userMessage, modelId) {
133
135
  }
134
136
  return { title: fallbackTitle(userMessage), modelId };
135
137
  }
136
- /**
137
- * Resolve a model ID to a runnable AI SDK LanguageModel.
138
- * Uses the Anthropic provider factory created by createProvider().
139
- */
140
- function resolveModelRuntime(provider, modelId) {
141
- return resolveRuntime(provider, modelId);
142
- }
143
138
  async function toolSetToBatchTools(_tools) {
144
139
  // Batch API not supported with Anthropic in Phase 0. Phase 1 may add this.
145
140
  throw new Error("Batch API not available in Phase 0. Use standard streaming mode.");
@@ -403,7 +398,22 @@ export class Agent {
403
398
  getActiveRunId() {
404
399
  return this._activeRunId;
405
400
  }
406
- setModel(model) {
401
+ /**
402
+ * Point the agent at `model`, re-resolving the provider when the new model
403
+ * belongs to a different one.
404
+ *
405
+ * EVERY path that changes `this.modelId` must go through here. Changing the
406
+ * id alone leaves `providerId` / `provider` / `baseURL` / `apiKey` describing
407
+ * the PREVIOUS provider, so the next turn POSTs the new model to the old
408
+ * provider's endpoint — which rejects it as unknown (session 0c6728ba1a25:
409
+ * `gpt-5.4` sent to api.x.ai → 404 "The model gpt-5.4 does not exist", a
410
+ * wiring bug that reads like a bad model name). `setMode` used to do exactly
411
+ * that; it now calls this too.
412
+ *
413
+ * Session persistence is NOT done here — callers own that, because setMode
414
+ * writes mode+model together.
415
+ */
416
+ _applyModelId(model) {
407
417
  this.modelId = normalizeModelId(model);
408
418
  const newProviderId = detectProviderForModel(this.modelId);
409
419
  if (newProviderId !== this.providerId) {
@@ -437,6 +447,9 @@ export class Agent {
437
447
  this.provider = null;
438
448
  this._oauthInitDone = false;
439
449
  }
450
+ }
451
+ setModel(model) {
452
+ this._applyModelId(model);
440
453
  if (this.sessionStore && this.session) {
441
454
  this.sessionStore.setModel(this.session.id, this.modelId);
442
455
  this.session = this.sessionStore.getRequiredSession(this.session.id);
@@ -464,7 +477,11 @@ export class Agent {
464
477
  this.mode = mode;
465
478
  const modeModel = getModeSpecificModel(mode);
466
479
  if (modeModel) {
467
- this.modelId = normalizeModelId(modeModel);
480
+ // Via _applyModelId, NOT a bare `this.modelId = ...`: a mode-specific
481
+ // model can belong to a different provider than the session's, and
482
+ // assigning the id alone would leave provider/baseURL/apiKey pointing at
483
+ // the old one — the cross-wire class in _applyModelId's docstring.
484
+ this._applyModelId(modeModel);
468
485
  }
469
486
  if (this.sessionStore && this.session) {
470
487
  this.sessionStore.setMode(this.session.id, mode);
@@ -565,7 +582,7 @@ export class Agent {
565
582
  if (!provider) {
566
583
  return "New session";
567
584
  }
568
- const generated = await genTitle(provider, userMessage, this.modelId);
585
+ const generated = await genTitle(userMessage, this.modelId);
569
586
  this.recordUsage(generated.usage, "title", generated.modelId);
570
587
  if (this.sessionStore && this.session && !this.session.title && generated.title) {
571
588
  this.sessionStore.setTitle(this.session.id, generated.title);
@@ -598,7 +615,7 @@ export class Agent {
598
615
  charBudget -= snippet.length;
599
616
  }
600
617
  const conversationContext = contextParts.join("\n\n");
601
- const result = await runSideQuestion(question, this.provider, this.modelId, conversationContext, signal);
618
+ const result = await runSideQuestion(question, this.modelId, conversationContext, signal);
602
619
  this.recordUsage(result.usage, "other");
603
620
  return result;
604
621
  }
@@ -660,6 +677,15 @@ export class Agent {
660
677
  setSafetyOverrideHandler(fn) {
661
678
  this._safetyOverrideHandler = fn;
662
679
  }
680
+ // ask_user handler — set by the UI (app.tsx). Invoked when the model calls the
681
+ // `ask_user` tool: the UI surfaces an agent-authored card and resolves with the
682
+ // human's answer string, which becomes the tool result. When unset (headless
683
+ // without an answerer), the tool returns the dismissed sentinel so the agent
684
+ // still continues from its own judgment.
685
+ _askUserHandler = null;
686
+ setAskUserHandler(fn) {
687
+ this._askUserHandler = fn;
688
+ }
663
689
  respondToToolApproval(approvalId, approved) {
664
690
  const toolApprovalResponse = {
665
691
  role: "tool",
@@ -1190,7 +1216,6 @@ export class Agent {
1190
1216
  async runTaskRequest(request, onActivity, abortSignal) {
1191
1217
  const provider = this.requireProvider();
1192
1218
  const deps = {
1193
- getProvider: () => provider,
1194
1219
  resolveModelForTask: (task) => this._resolveModelForTask(task),
1195
1220
  getModelId: () => this.modelId,
1196
1221
  getProviderId: () => this.providerId,
@@ -1422,7 +1447,7 @@ export class Agent {
1422
1447
  // Phase 1: ask the compaction proposer model whether to compact and what to keep/drop.
1423
1448
  // Only compact if the model says yes. On error/skip, fall back to heuristic.
1424
1449
  const compactModelId = this._resolveCompactModel();
1425
- const proposal = await proposeCompaction(provider, compactModelId, this.messages, signal);
1450
+ const proposal = await proposeCompaction(compactModelId, this.messages, signal);
1426
1451
  if (proposal !== null) {
1427
1452
  // Model decided — compact only if model says shouldCompact
1428
1453
  if (force) {
@@ -1485,7 +1510,7 @@ export class Agent {
1485
1510
  const customInstructions = isSubSession
1486
1511
  ? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
1487
1512
  : undefined;
1488
- const { summary, usage: compactUsage } = await generateCompactionSummary(provider, compactModelId, preparation, customInstructions, signal);
1513
+ const { summary, usage: compactUsage } = await generateCompactionSummary(compactModelId, preparation, customInstructions, signal);
1489
1514
  // Record compaction call in cost-log — bypasses recordUsage because
1490
1515
  // compaction returns usage separately and isn't routed through the
1491
1516
  // status-bar / usage event pipeline (intentional: it's overhead, not user spend).
@@ -1652,7 +1677,7 @@ export class Agent {
1652
1677
  // Council v2 — Clarify → Confirm → Debate → Plan → Execute
1653
1678
  // ========================================================================
1654
1679
  async *runCouncilV2(topic, options) {
1655
- const { runCouncil, postDebateContinuation } = await import("../council/index.js");
1680
+ const { runCouncil, buildNeutralPostCouncilContinuation } = await import("../council/index.js");
1656
1681
  const { createCouncilLLM } = await import("../council/llm.js");
1657
1682
  const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
1658
1683
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
@@ -1702,6 +1727,9 @@ export class Agent {
1702
1727
  cwd: this.bash.getCwd(),
1703
1728
  councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
1704
1729
  signal,
1730
+ // convene_council path — suppress ALL hardcoded post-debate decision
1731
+ // surface; the agent decides what happens after the synthesis.
1732
+ convenePath: options?.convenePath,
1705
1733
  // When the Context Rail is active it carries leader/panel/cost as
1706
1734
  // ambient sidebar rows, so suppress the duplicate inline summary.
1707
1735
  suppressInlineMeta: isContextRailEnabled(),
@@ -1753,7 +1781,12 @@ export class Agent {
1753
1781
  // fires ONLY on the top-level slash path, never when nested inside
1754
1782
  // processMessage (auto-council) or drained by the runDebate tool — those
1755
1783
  // callers manage their own continuation.
1756
- const continuationPrompt = ownsController && synthesis ? postDebateContinuation(chosenAction, synthesis) : null;
1784
+ // convenePath suppresses the hardcoded card (chosenAction stays undefined),
1785
+ // so always hand the synthesis to a normal agent turn via the neutral
1786
+ // continuation and let the agent decide. ownsController scopes this to the
1787
+ // top-level /council slash path (auto-council nests with ownsController
1788
+ // false and continues in tool-engine instead).
1789
+ const continuationPrompt = ownsController && synthesis ? buildNeutralPostCouncilContinuation(synthesis) || null : null;
1757
1790
  const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
1758
1791
  if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
1759
1792
  // #1 — build the council decision in an ISOLATED sub-agent instead of
@@ -1839,6 +1872,7 @@ export class Agent {
1839
1872
  idleMs,
1840
1873
  totalMs,
1841
1874
  label: "council continuation turn",
1875
+ shouldSuppressFire: isInteractivePaused,
1842
1876
  });
1843
1877
  }
1844
1878
  catch (err) {
@@ -1881,12 +1915,45 @@ export class Agent {
1881
1915
  phases: [],
1882
1916
  };
1883
1917
  const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
1884
- const processMessageFn = (m) => this.processMessage(m, options?.observer);
1918
+ // Autonomous-execution permission for the product loop. /ideal's consent
1919
+ // boundary is the preflight plan-approval askcard; once the PO approves the
1920
+ // plan, the sprint IMPLEMENT turn must apply its own file-op mutations without
1921
+ // a per-tool approval prompt. In `safe` mode a Write/Edit surfaces a
1922
+ // tool_approval_request and awaits respondToToolApproval — but in the driven
1923
+ // product-loop context nothing answers it and no approval askcard renders, so
1924
+ // the impl turn wedges forever right after finishReason:tool-calls (observed
1925
+ // live 2026-07-14: 0 files written across grok/opencode/deepseek + isolated &
1926
+ // streamed paths — tool-engine.ts:2997). Elevating safe→auto-edit for the turn
1927
+ // auto-approves file ops (yolo stays yolo); catastrophic bash stays hard-blocked
1928
+ // by permission-mode's CATASTROPHIC_PATTERNS regardless of mode.
1929
+ const self = this;
1930
+ const processMessageFn = (m) => (async function* () {
1931
+ const prev = self.permissionMode;
1932
+ if (self.permissionMode === "safe")
1933
+ self.permissionMode = "auto-edit";
1934
+ try {
1935
+ yield* self.processMessage(m, options?.observer);
1936
+ }
1937
+ finally {
1938
+ self.permissionMode = prev;
1939
+ }
1940
+ })();
1885
1941
  // Isolated bounded task-runner bridge for the sprint implement stage: a fresh
1886
1942
  // child context (getSubAgentBudgetChars cap, independent compaction) that does
1887
1943
  // NOT inherit this turn's council-debate history — the root fix for the live
1888
1944
  // ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
1889
- const runIsolatedTask = (request) => this.runTaskRequest(request, undefined, this.abortController?.signal);
1945
+ // Same autonomous-permission elevation as the streamed path above.
1946
+ const runIsolatedTask = async (request) => {
1947
+ const prev = self.permissionMode;
1948
+ if (self.permissionMode === "safe")
1949
+ self.permissionMode = "auto-edit";
1950
+ try {
1951
+ return await this.runTaskRequest(request, undefined, this.abortController?.signal);
1952
+ }
1953
+ finally {
1954
+ self.permissionMode = prev;
1955
+ }
1956
+ };
1890
1957
  const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
1891
1958
  // P2.7 (LLM-first — no-regex routing): the work-depth tier that decides
1892
1959
  // /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
@@ -1903,7 +1970,15 @@ export class Agent {
1903
1970
  let depth = "standard";
1904
1971
  try {
1905
1972
  const { createLlmClassifier } = await import("../pil/llm-classify.js");
1906
- const classify = createLlmClassifier(this.requireProvider(), this.modelId);
1973
+ const { probeRepoGrounding } = await import("../pil/repo-grounding-probe.js");
1974
+ const { getRepoStructureHints } = await import("../pil/repo-structure-hints.js");
1975
+ const classify = createLlmClassifier(this.modelId, {
1976
+ routeFastTier: true,
1977
+ // /ideal routing needs a reliable depth verdict; allow cross-provider
1978
+ // fallback so an agentic/no-fast-tier session model (e.g. xai) doesn't
1979
+ // strand the classify at fail-open. Bounded by CLASSIFY_TOTAL_BUDGET_MS.
1980
+ crossProviderFallback: true,
1981
+ });
1907
1982
  const res = await classify(payload.idea);
1908
1983
  if (res?.depthTier) {
1909
1984
  depth = res.depthTier;
@@ -1916,32 +1991,72 @@ export class Agent {
1916
1991
  // LLM-first clarity signal — feeds the downstream interview as a hint.
1917
1992
  if (res?.needsClarification === true)
1918
1993
  needsClarification = true;
1994
+ // Design B — deterministic repo-grounding probe. Trigger on Layer-1
1995
+ // OUTPUT (ambiguity), NOT a prompt regex: run only when the verdict is
1996
+ // non-trivial (depth !== "quick") OR low-confidence, so a well-specified
1997
+ // trivial task still hot-paths on one cheap call. The probe itself is
1998
+ // deterministic (REPO_DEEP_MAP index + bounded on-disk reads, no LLM).
1999
+ // Once it runs its MEASURED facts drive routing — confidence is
2000
+ // trigger-only and can no longer override grounding.
2001
+ const lowConfidence = (res?.confidence ?? 1) < 0.7;
2002
+ if (depth !== "quick" || lowConfidence) {
2003
+ try {
2004
+ const hints = getRepoStructureHints(process.cwd());
2005
+ const probe = probeRepoGrounding(payload.idea, hints, { cwd: process.cwd() });
2006
+ if (probe.ran) {
2007
+ // Invariant: probe ran AND zero matched files, or a symbol
2008
+ // collision → route INTO Council (the arbiter). Never direct-
2009
+ // dispatch from weak grounding.
2010
+ if (probe.groundingUncertainty) {
2011
+ needsClarification = true;
2012
+ depth = depth === "quick" ? "standard" : depth;
2013
+ }
2014
+ // Measured size escalation: a large grounded surface routes heavy
2015
+ // regardless of the sentence-level classify verdict.
2016
+ if (probe.bucket === "large")
2017
+ depth = "heavy";
2018
+ console.error(`[ideal/route] grounding probe: files=${probe.matchedFiles} loc=${probe.totalLoc} ` +
2019
+ `dirs=${probe.matchedDirs} collision=${probe.collision} bucket=${probe.bucket} ` +
2020
+ `uncertainty=${probe.groundingUncertainty} → depth=${depth}`);
2021
+ }
2022
+ }
2023
+ catch (err) {
2024
+ // Fail-open: a probe hiccup must never block /ideal (No-Silent-Catch).
2025
+ console.error(`[ideal/route] grounding probe failed, ignoring: ${err?.message}`);
2026
+ }
2027
+ }
1919
2028
  }
1920
2029
  catch (err) {
1921
2030
  // Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
1922
2031
  console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
1923
2032
  }
1924
- // The cheap 8-word classify above is only a HINT for the downstream GSD
1925
- // depth pipeline — it is NOT a routing gate. It was too noisy to gate the
1926
- // council decision: the SAME architectural prompt scored `heavy` on one run
1927
- // and `standard` the next, so routing flip-flopped between full Council and
1928
- // a straight-to-Edit maint-edit that skipped the debate. /ideal is the
1929
- // DELIBERATE path it must ALWAYS run the full Council/loop-driver
1930
- // pipeline, in this order:
1931
- // 1. scan the source (discovery/scoping) to understand what the user's
1932
- // request actually touches,
1933
- // 2. interview the user (AskCard adaptive: only asks what the source
1934
- // can't answer, so an existing repo isn't dragged through 6 questions),
1935
- // 3. leader-tier complexity assessment + debate INSIDE that flow, AFTER
1936
- // scope is understood — never a "no debate" verdict from a bare-prompt
1937
- // guess.
1938
- // So we force Council for every /ideal start; the fast classify only
1939
- // seeds depth/clarity hints for that flow. Explicit `--maintain` still opts
1940
- // out of Council (handled at the top of runProductLoop dispatch).
2033
+ // Council routing gate (model-signal, no hardcoded depth→council table).
2034
+ //
2035
+ // History: /ideal used to force Council UNCONDITIONALLY because the cheap
2036
+ // classify was too noisy to gate on the SAME architectural prompt scored
2037
+ // `heavy` on one run and `standard` the next, so routing flip-flopped. That
2038
+ // noise was root-caused (2026-07-15, harness-measured): the session model
2039
+ // could be an agentic model (e.g. grok-composer) that ignores the terse
2040
+ // 8-word classify contract and emits task-planning prose null → fail-open
2041
+ // "standard"; and stream errors were swallowed into the same null. Both are
2042
+ // now fixed in llm-classify.ts (cross-provider route to a keyed instruction-
2043
+ // follower + No-Silent-Catch error surfacing), so the depth/clarity signal
2044
+ // is reliable enough to gate on.
2045
+ //
2046
+ // Gate: only a genuinely TRIVIAL /ideal — the model says depth=quick AND
2047
+ // not-underspecified skips Council and takes the product-loop hot-path
2048
+ // (single sprint, no debate; index.ts complexity==="low" branch). Anything
2049
+ // else (standard, heavy, or any underspecified request) still runs the full
2050
+ // Council/loop-driver pipeline: scan source → adaptive interview → leader-
2051
+ // tier assessment + debate AFTER scope is understood. When the model is
2052
+ // uncertain (null depth → "medium") it routes INTO Council, the arbiter —
2053
+ // never a hardcoded default. Explicit `--maintain` still opts out entirely.
1941
2054
  complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
1942
2055
  sufficiencyMissing = undefined;
1943
2056
  if (payload.flags.mode !== "maintain") {
1944
- routeForceCouncil = true;
2057
+ const trivial = depth === "quick" && needsClarification !== true;
2058
+ if (!trivial)
2059
+ routeForceCouncil = true;
1945
2060
  }
1946
2061
  }
1947
2062
  const gen = runProductLoop({
@@ -2552,7 +2667,7 @@ export class Agent {
2552
2667
  // continuation ("ok làm phần đó đi") is judged in isolation and can be
2553
2668
  // mis-routed as a fresh/unrelated task. Passing contextInfo also turns
2554
2669
  // on the session-size metadata block the ROTATE_SESSION rule relies on.
2555
- const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage, {
2670
+ const routeResult = await classifySubSessionAction(this.modelId, userMessage, {
2556
2671
  currentChars,
2557
2672
  threshold,
2558
2673
  recentTurns: this._buildRecentTurnsSummary(),
@@ -2596,7 +2711,7 @@ export class Agent {
2596
2711
  const { deliberateCompact } = await import("../flow/compaction/index.js");
2597
2712
  const { getDatabase } = await import("../storage/db.js");
2598
2713
  const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
2599
- const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
2714
+ const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.modelId);
2600
2715
  const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
2601
2716
  const db = getDatabase();
2602
2717
  this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
@@ -2753,6 +2868,7 @@ export class Agent {
2753
2868
  idleMs: turnIdleMs,
2754
2869
  totalMs: turnTotalMs,
2755
2870
  label: "assistant turn",
2871
+ shouldSuppressFire: isInteractivePaused,
2756
2872
  });
2757
2873
  }
2758
2874
  catch (stallErr) {
@@ -3031,9 +3147,8 @@ export class Agent {
3031
3147
  `${serializedParent}\n\n` +
3032
3148
  `Provide clear, actionable guidance to resolve the child's query.`;
3033
3149
  const { generateText } = await import("ai");
3034
- const provider = self.requireProvider();
3035
3150
  const modelId = self.modelId;
3036
- const runtime = resolveModelRuntime(provider, modelId);
3151
+ const runtime = resolveModelRuntime(modelId);
3037
3152
  const result = await generateText({
3038
3153
  model: runtime.model,
3039
3154
  system: systemPrompt,
@@ -3090,6 +3205,25 @@ export class Agent {
3090
3205
  return { action: "block" };
3091
3206
  }
3092
3207
  },
3208
+ askUser: async (info) => {
3209
+ const h = self._askUserHandler;
3210
+ if (!h) {
3211
+ logger.warn("orchestrator", "askUser called but no handler registered — returning dismissed sentinel");
3212
+ return ASK_USER_DISMISSED;
3213
+ }
3214
+ // Hold both watchdogs open while the human answers (see interactive-pause.ts).
3215
+ beginInteractivePause();
3216
+ try {
3217
+ return await h(info);
3218
+ }
3219
+ catch (err) {
3220
+ logger.error("orchestrator", "askUser crashed", { error: err });
3221
+ return ASK_USER_DISMISSED;
3222
+ }
3223
+ finally {
3224
+ endInteractivePause();
3225
+ }
3226
+ },
3093
3227
  runCouncilV2: (msg, opts) => self.runCouncilV2(msg, opts),
3094
3228
  processMessage: (msg, obs, imgs) => self.processMessage(msg, obs, imgs),
3095
3229
  processMessageBatchTurn: (args) => self.processMessageBatchTurn(args),
@@ -3213,14 +3347,34 @@ export class Agent {
3213
3347
  // and must be re-resolved from the NEW provider's own credentials.
3214
3348
  const providerDeferred = this.provider === null;
3215
3349
  const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
3216
- if (!providerDeferred && !keyIsSentinelOrEmpty)
3217
- return;
3218
3350
  try {
3219
- const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
3351
+ const { listOAuthProviderIds, getOAuthProviderConfig } = await import("../providers/auth/registry.js");
3220
3352
  const ids = await listOAuthProviderIds();
3221
- // OAuth path: the provider supports OAuth AND we don't hold a real API
3222
- // key, so inject subscription tokens as Bearer headers.
3223
- if (ids.includes(this.providerId) && keyIsSentinelOrEmpty) {
3353
+ const oauthCapable = ids.includes(this.providerId);
3354
+ // One auth mode per provider (OAuth XOR API key): if this OAuth-capable
3355
+ // provider has a live token, OAuth wins OUTRIGHT — even over a real held
3356
+ // API key. This is the shadowing-bug fix: a stale sk-proj key must not
3357
+ // beat a valid ChatGPT OAuth login. Check token presence BEFORE the
3358
+ // early-out so a held key can no longer suppress OAuth.
3359
+ let hasOAuthTokens = false;
3360
+ if (oauthCapable) {
3361
+ try {
3362
+ const cfg = await getOAuthProviderConfig(this.providerId);
3363
+ const tokens = cfg ? await cfg.loadTokensWithRefresh().catch(() => null) : null;
3364
+ hasOAuthTokens = !!tokens?.accessToken;
3365
+ }
3366
+ catch {
3367
+ hasOAuthTokens = false;
3368
+ }
3369
+ }
3370
+ // Nothing to do only when: no OAuth token, a real key is held, and the
3371
+ // provider was not deferred by a model switch.
3372
+ if (!hasOAuthTokens && !providerDeferred && !keyIsSentinelOrEmpty)
3373
+ return;
3374
+ // OAuth path: inject subscription tokens as Bearer headers. Taken when a
3375
+ // token exists (override a held key), OR when no real key is held for an
3376
+ // OAuth-capable provider.
3377
+ if (oauthCapable && (hasOAuthTokens || keyIsSentinelOrEmpty)) {
3224
3378
  const effectiveBaseURL = this.baseURL &&
3225
3379
  this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
3226
3380
  ? this.baseURL
@@ -29,7 +29,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
29
29
  let llmFallback;
30
30
  try {
31
31
  const { createLlmClassifier } = await import("../pil/llm-classify.js");
32
- llmFallback = createLlmClassifier(deps.requireProvider(), deps.modelId);
32
+ llmFallback = createLlmClassifier(deps.modelId, { routeFastTier: true });
33
33
  }
34
34
  catch (err) {
35
35
  logger.error("pil", "LLM fallback wiring failed", { error: err });
@@ -40,7 +40,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
40
40
  let clarificationProposer;
41
41
  try {
42
42
  const { createModelClarificationProposer } = await import("../pil/discovery.js");
43
- clarificationProposer = createModelClarificationProposer(deps.requireProvider(), deps.modelId);
43
+ clarificationProposer = createModelClarificationProposer(deps.modelId);
44
44
  }
45
45
  catch (err) {
46
46
  logger.error("pil", "clarification proposer wiring failed", { error: err });
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Pure — no React, no side effects. Unit-testable in isolation.
10
10
  */
11
- export type SafetyBlockKind = "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
11
+ export type SafetyBlockKind = "catastrophic" | "destructive-revert" | "dangerous" | "git-safety" | "empty-bash";
12
12
  export interface SafetyBlockInfo {
13
13
  kind: SafetyBlockKind;
14
14
  /** The blocked command or tool call description */
@@ -10,6 +10,7 @@
10
10
  */
11
11
  const CATEGORY_LABELS = {
12
12
  catastrophic: "Lỗi bảo mật nghiêm trọng",
13
+ "destructive-revert": "Hủy thay đổi chưa lưu (không thể hoàn tác)",
13
14
  dangerous: "Lệnh nguy hiểm",
14
15
  "git-safety": "Git safety gate",
15
16
  "empty-bash": "Bash call trống",
@@ -38,8 +39,10 @@ export function planSafetyAskcard(info) {
38
39
  description: "Không chạy lệnh này, trả về lỗi cho agent",
39
40
  },
40
41
  ];
41
- // Allow-session only for less severe blocks
42
- if (info.kind !== "catastrophic") {
42
+ // Allow-session only for less severe blocks. A destructive-revert (discards
43
+ // uncommitted work irreversibly) is treated like catastrophic: allow-once
44
+ // only, never a blanket session allow.
45
+ if (info.kind !== "catastrophic" && info.kind !== "destructive-revert") {
43
46
  options.splice(1, 0, {
44
47
  label: "Cho phép cả phiên (Allow session)",
45
48
  value: "allow-session",
@@ -38,6 +38,11 @@ export declare function parseSafetyBlock(outputText: string): ParsedSafetyBlock
38
38
  * the "don't ask me" mode actually stops asking for routine guardrails.
39
39
  * - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
40
40
  * destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
41
+ * - `destructive-revert` ALWAYS shows the askcard, even in yolo — discarding
42
+ * uncommitted work (git checkout --/restore/reset --hard/clean, rm of a
43
+ * tracked file) is irreversible; headless it hard-blocks. This is the guard
44
+ * that stops an autonomous /ideal sub-agent from silently reverting the
45
+ * user's unrelated working-tree changes.
41
46
  * - `empty-bash` is handled separately (auto-block, no card) and never
42
47
  * reaches this policy.
43
48
  * - `safe` / `auto-edit` always show the card for any real block.
@@ -39,6 +39,11 @@ export function parseSafetyBlock(outputText) {
39
39
  * the "don't ask me" mode actually stops asking for routine guardrails.
40
40
  * - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
41
41
  * destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
42
+ * - `destructive-revert` ALWAYS shows the askcard, even in yolo — discarding
43
+ * uncommitted work (git checkout --/restore/reset --hard/clean, rm of a
44
+ * tracked file) is irreversible; headless it hard-blocks. This is the guard
45
+ * that stops an autonomous /ideal sub-agent from silently reverting the
46
+ * user's unrelated working-tree changes.
42
47
  * - `empty-bash` is handled separately (auto-block, no card) and never
43
48
  * reaches this policy.
44
49
  * - `safe` / `auto-edit` always show the card for any real block.
@@ -48,6 +53,8 @@ export function shouldAutoAllowYolo(kind, mode) {
48
53
  return false;
49
54
  if (kind === "catastrophic")
50
55
  return false;
56
+ if (kind === "destructive-revert")
57
+ return false;
51
58
  if (kind === "empty-bash")
52
59
  return false;
53
60
  return true;
@@ -130,4 +130,11 @@ export declare function shouldContinueAfterMidLoopStall(s: MidLoopStallState): b
130
130
  * (1-based): 500 → 1000 → 2000 → 4000 → 4000.
131
131
  */
132
132
  export declare function stallRepromptBackoffMs(attempt: number): number;
133
- export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void, progressOpts?: StallWatchdogProgressOpts): StallWatchdog;
133
+ export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void, progressOpts?: StallWatchdogProgressOpts,
134
+ /**
135
+ * Optional gate consulted the instant a timer would fire. When it returns
136
+ * true the watchdog RE-ARMS instead of aborting — used to hold the stream
137
+ * open while a blocking interactive card (`ask_user`) awaits a human, without
138
+ * counting that wait as a provider stall.
139
+ */
140
+ shouldSuppressFire?: () => boolean): StallWatchdog;
@@ -70,7 +70,14 @@ export function shouldContinueAfterMidLoopStall(s) {
70
70
  export function stallRepromptBackoffMs(attempt) {
71
71
  return Math.min(500 * 2 ** (Math.max(1, attempt) - 1), 4_000);
72
72
  }
73
- export function createStallWatchdog(timeoutMs, onFire, progressOpts) {
73
+ export function createStallWatchdog(timeoutMs, onFire, progressOpts,
74
+ /**
75
+ * Optional gate consulted the instant a timer would fire. When it returns
76
+ * true the watchdog RE-ARMS instead of aborting — used to hold the stream
77
+ * open while a blocking interactive card (`ask_user`) awaits a human, without
78
+ * counting that wait as a provider stall.
79
+ */
80
+ shouldSuppressFire) {
74
81
  const controller = new AbortController();
75
82
  let firedFlag = false;
76
83
  let timer = null;
@@ -99,14 +106,28 @@ export function createStallWatchdog(timeoutMs, onFire, progressOpts) {
99
106
  const arm = () => {
100
107
  if (!enabled)
101
108
  return;
102
- timer = setTimeout(() => fire(onFire), timeoutMs);
109
+ timer = setTimeout(() => {
110
+ // Hold open (re-arm) while an interactive card blocks the turn — the
111
+ // human is thinking, not a stalled provider.
112
+ if (shouldSuppressFire?.()) {
113
+ arm();
114
+ return;
115
+ }
116
+ fire(onFire);
117
+ }, timeoutMs);
103
118
  // Don't keep the event loop alive solely for the watchdog (Node).
104
119
  timer.unref?.();
105
120
  };
106
121
  const armProgress = () => {
107
122
  if (!progressEnabled)
108
123
  return;
109
- progressTimer = setTimeout(() => fire(progressOpts?.onProgressFire), progressTimeoutMs);
124
+ progressTimer = setTimeout(() => {
125
+ if (shouldSuppressFire?.()) {
126
+ armProgress();
127
+ return;
128
+ }
129
+ fire(progressOpts?.onProgressFire);
130
+ }, progressTimeoutMs);
110
131
  progressTimer.unref?.();
111
132
  };
112
133
  const clear = () => {
@@ -3,7 +3,6 @@ import { type ResolvedModelRuntime } from "../providers/runtime.js";
3
3
  import type { ProviderId } from "../providers/types.js";
4
4
  import { BashTool } from "../tools/bash.js";
5
5
  import type { AgentMode, TaskRequest, ToolResult } from "../types/index.js";
6
- import type { LegacyProvider } from "./agent-options.js";
7
6
  import type { CrossTurnDedup } from "./cross-turn-dedup.js";
8
7
  import type { ReadPathBudget } from "./read-path-budget.js";
9
8
  /**
@@ -11,8 +10,6 @@ import type { ReadPathBudget } from "./read-path-budget.js";
11
10
  * without holding a circular reference. Mirrors the CouncilManager DI pattern.
12
11
  */
13
12
  export interface StreamRunnerDeps {
14
- /** Current top-level provider instance (already validated). */
15
- getProvider(): LegacyProvider;
16
13
  /** Resolve a specific task tier's model id (uses Agent's role config). */
17
14
  resolveModelForTask(task: "compact" | "explore" | "general" | "title" | "verify"): string;
18
15
  /** Top-level model id (for routed_from telemetry). */
@@ -108,6 +105,19 @@ export type SetupOutcome = {
108
105
  kind: "prepared";
109
106
  prepared: PreparedSubAgentCall;
110
107
  } | SetupShortCircuit;
108
+ /**
109
+ * Emit one sub-agent diagnostic line (MUONROI_DEBUG_SUBAGENT=1).
110
+ *
111
+ * Writes to the file named by MUONROI_SUBAGENT_DEBUG_LOG when set, else stderr.
112
+ * The file sink exists because stderr is a dead end for the case this
113
+ * diagnostic was written for: under the MCP harness the TUI runs as a child
114
+ * whose stderr nobody reads (opentui-spawn.ts consumes only the fd3/named-pipe
115
+ * stream), so every line went into a pipe no one drains. G1 — "Task failed: No
116
+ * output generated" — was named, given this flag, and never diagnosed, because
117
+ * the diagnostic could not reach whoever turned it on. Mirrors the working
118
+ * MUONROI_COUNCIL_DEBUG_LOG sink in council/llm.ts.
119
+ */
120
+ export declare function writeSubagentDebug(enabled: boolean, line: string): void;
111
121
  /**
112
122
  * StreamRunner — extracted sub-agent stream lifecycle.
113
123
  *