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
@@ -71,6 +71,45 @@ export async function writeExperienceConfig(patch, opts = {}) {
71
71
  await fs.mkdir(path.dirname(p), { recursive: true, mode: 0o700 });
72
72
  await fs.writeFile(p, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
73
73
  }
74
+ /** Where the EE config lives, for display in setup/status output. */
75
+ export function getExperienceConfigPath(opts = {}) {
76
+ return configPath(opts.home);
77
+ }
78
+ /**
79
+ * Best-effort reachability probe against an EE server's /health.
80
+ *
81
+ * Returns a reason on failure rather than a bare boolean: "saved but not
82
+ * reachable" and "saved, server says 401" are different problems for the user
83
+ * to act on, and collapsing them to `false` hides which one they have.
84
+ * Never throws — a probe is diagnostics, and must not fail a config write that
85
+ * already succeeded.
86
+ */
87
+ export async function probeEEHealth(baseUrl, token, opts = {}) {
88
+ const ac = new AbortController();
89
+ const timer = setTimeout(() => ac.abort(), opts.timeoutMs ?? 4000);
90
+ try {
91
+ const res = await fetch(`${baseUrl}/health`, {
92
+ signal: ac.signal,
93
+ headers: token ? { authorization: `Bearer ${token}` } : undefined,
94
+ });
95
+ if (res.ok)
96
+ return { ok: true, detail: `HTTP ${res.status}` };
97
+ return {
98
+ ok: false,
99
+ detail: res.status === 401 || res.status === 403
100
+ ? `HTTP ${res.status} — the server rejected this auth token`
101
+ : `HTTP ${res.status}`,
102
+ };
103
+ }
104
+ catch (err) {
105
+ const message = err?.name === "AbortError" ? "timed out" : (err?.message ?? String(err));
106
+ console.error(`[ee/auth] /health probe failed for ${baseUrl}: ${message}`);
107
+ return { ok: false, detail: message };
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ }
112
+ }
74
113
  export function getCachedAuthToken() {
75
114
  return _token;
76
115
  }
@@ -207,13 +207,23 @@ export function createEEClient(opts = {}) {
207
207
  }
208
208
  return {
209
209
  async health() {
210
+ const startedAt = Date.now();
210
211
  try {
211
212
  const resp = await f(`${baseUrl}/health`, {
212
213
  signal: AbortSignal.timeout(DEFAULT_HEALTH_TIMEOUT_MS),
213
214
  });
214
215
  return { ok: resp.ok, status: resp.status };
215
216
  }
216
- catch {
217
+ catch (err) {
218
+ // status:0 means "no response" and says nothing about WHY. Timeout, DNS,
219
+ // refused connection and TLS failure all degrade to the same shape, so a
220
+ // bare catch here leaves an agent with an unfalsifiable symptom — exactly
221
+ // what happened on 2026-07-17, when status:0 was really the server
222
+ // blocking its event loop.
223
+ logEeFailure("client.health", classifyEeError(err), err, {
224
+ elapsedMs: Date.now() - startedAt,
225
+ budgetMs: DEFAULT_HEALTH_TIMEOUT_MS,
226
+ });
217
227
  return { ok: false, status: 0 };
218
228
  }
219
229
  },
@@ -599,6 +609,7 @@ export function createEEClient(opts = {}) {
599
609
  // races the server and loses → spurious ee_unavailable. 15s gives margin
600
610
  // so a slow-but-valid recall still lands rather than being dropped.
601
611
  const timeoutMs = opts.timeoutMs ?? 15000;
612
+ const startedAt = Date.now();
602
613
  try {
603
614
  const resp = await f(`${baseUrl}/api/recall`, {
604
615
  method: "POST",
@@ -606,11 +617,25 @@ export function createEEClient(opts = {}) {
606
617
  body: JSON.stringify(redactedBody),
607
618
  signal: opts.signal ?? AbortSignal.timeout(timeoutMs),
608
619
  });
609
- if (!resp.ok)
620
+ if (!resp.ok) {
621
+ // Keep returning null — callers map it to "EE unavailable" — but never
622
+ // discard the status. Collapsing 429/401/500 into the same null a
623
+ // timeout produces is what made the 2026-07-17 incident unfalsifiable
624
+ // from the client: "is the server rate-limiting us?" could not be
625
+ // answered without reading the server's source.
626
+ logEeFailure("client.recall", "error", new Error(`HTTP ${resp.status}`), {
627
+ status: resp.status,
628
+ elapsedMs: Date.now() - startedAt,
629
+ });
610
630
  return null;
631
+ }
611
632
  return (await resp.json());
612
633
  }
613
- catch {
634
+ catch (err) {
635
+ logEeFailure("client.recall", classifyEeError(err), err, {
636
+ elapsedMs: Date.now() - startedAt,
637
+ budgetMs: timeoutMs,
638
+ });
614
639
  return null;
615
640
  }
616
641
  },
@@ -8,27 +8,7 @@
8
8
  * health probe is reported, not hidden, and never blocks setup.
9
9
  */
10
10
  import { createInterface } from "node:readline";
11
- import { writeExperienceConfig } from "./auth.js";
12
- /** Best-effort reachability probe — returns true/false, never throws. */
13
- async function probeHealth(baseUrl, token) {
14
- try {
15
- const ac = new AbortController();
16
- const timer = setTimeout(() => ac.abort(), 4000);
17
- try {
18
- const res = await fetch(`${baseUrl}/health`, {
19
- signal: ac.signal,
20
- headers: token ? { authorization: `Bearer ${token}` } : undefined,
21
- });
22
- return res.ok;
23
- }
24
- finally {
25
- clearTimeout(timer);
26
- }
27
- }
28
- catch {
29
- return false;
30
- }
31
- }
11
+ import { probeEEHealth, writeExperienceConfig } from "./auth.js";
32
12
  /**
33
13
  * Returns true when a config was written (so the caller can reload EE auth).
34
14
  * Returns false when skipped or invalid.
@@ -38,8 +18,8 @@ export async function firstRunEESetup() {
38
18
  const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a)));
39
19
  try {
40
20
  process.stderr.write("\nExperience Engine (optional) — a shared brain that recalls past decisions, gotchas,\n" +
41
- "and recipes so the agent works like a senior on your stack. You can set this up later\n" +
42
- "by editing ~/.experience/config.json or setting MUONROI_EE_BASE_URL.\n\n");
21
+ "and recipes so the agent works like a senior on your stack. Skipping is fine: you can\n" +
22
+ "connect one at any time with `/ee config <url> [token]`.\n\n");
43
23
  const url = (await ask("EE server URL (blank to skip): ")).trim();
44
24
  if (!url) {
45
25
  process.stderr.write("Skipped Experience Engine setup.\n");
@@ -59,10 +39,10 @@ export async function firstRunEESetup() {
59
39
  ...(token ? { serverAuthToken: token } : {}),
60
40
  });
61
41
  process.stderr.write(`Wrote Experience Engine config → ~/.experience/config.json (serverBaseUrl=${normalized}).\n`);
62
- const reachable = await probeHealth(normalized, token || undefined);
63
- process.stderr.write(reachable
42
+ const health = await probeEEHealth(normalized, token || undefined);
43
+ process.stderr.write(health.ok
64
44
  ? " ✓ EE server reachable.\n"
65
- : " ⚠ Could not reach the EE server right now (saved anyway run 'muonroi-cli doctor' to recheck).\n");
45
+ : ` ⚠ Could not reach the EE server right now — ${health.detail} (saved anyway; recheck with '/ee config').\n`);
66
46
  return true;
67
47
  }
68
48
  catch (err) {
@@ -17,6 +17,6 @@ export interface CompressResult {
17
17
  * 1. Serialize messages via serializeConversation().
18
18
  * 2. Extract preserved blocks.
19
19
  * 3. If under budget, return as-is (with preserved blocks restored).
20
- * 4. If over budget, compress via LLM (if provider given) or truncate, and restore blocks.
20
+ * 4. If over budget, compress via LLM (if modelId given) or truncate, and restore blocks.
21
21
  */
22
- export declare function compressChat(messages: ModelMessage[], _systemPrompt: string, tokenBudget: number, provider?: unknown, modelId?: string, customInstructions?: string): Promise<CompressResult>;
22
+ export declare function compressChat(messages: ModelMessage[], _systemPrompt: string, tokenBudget: number, modelId?: string, customInstructions?: string, onFraction?: (fraction: number) => void): Promise<CompressResult>;
@@ -4,9 +4,9 @@
4
4
  * Serializes messages, extracts preserved blocks, compresses if over budget,
5
5
  * then restores preserved blocks into the output.
6
6
  */
7
- import { generateText } from "ai";
7
+ import { streamText } from "ai";
8
8
  import { serializeConversation } from "../../orchestrator/compaction.js";
9
- import { resolveModelRuntime, resolveTemperatureParam, } from "../../providers/runtime.js";
9
+ import { resolveModelRuntime, resolveTemperatureParam } from "../../providers/runtime.js";
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { capCompactionInput } from "./input-guard.js";
12
12
  import { extractPreservedBlocks, restorePreservedBlocks } from "./preserve.js";
@@ -16,9 +16,9 @@ import { extractPreservedBlocks, restorePreservedBlocks } from "./preserve.js";
16
16
  * 1. Serialize messages via serializeConversation().
17
17
  * 2. Extract preserved blocks.
18
18
  * 3. If under budget, return as-is (with preserved blocks restored).
19
- * 4. If over budget, compress via LLM (if provider given) or truncate, and restore blocks.
19
+ * 4. If over budget, compress via LLM (if modelId given) or truncate, and restore blocks.
20
20
  */
21
- export async function compressChat(messages, _systemPrompt, tokenBudget, provider, modelId, customInstructions) {
21
+ export async function compressChat(messages, _systemPrompt, tokenBudget, modelId, customInstructions, onFraction) {
22
22
  const serialized = serializeConversation(messages);
23
23
  const { cleaned, blocks } = extractPreservedBlocks(serialized);
24
24
  // Estimate tokens of cleaned text
@@ -38,21 +38,34 @@ export async function compressChat(messages, _systemPrompt, tokenBudget, provide
38
38
  const availableChars = availableTokens * 4;
39
39
  let compressedContent = cleaned;
40
40
  let usedLLM = false;
41
- if (provider && modelId) {
41
+ if (modelId) {
42
42
  try {
43
- const runtime = resolveModelRuntime(provider, modelId);
43
+ const runtime = resolveModelRuntime(modelId);
44
44
  const extraPrompt = customInstructions ? `\n\nUSER FOCUS/INSTRUCTIONS:\n${customInstructions}\n` : "";
45
45
  // Guard the compaction INPUT against the model's own context window —
46
46
  // compaction fires when history is large, which is exactly when the full
47
47
  // serialized text can overflow the summarizer. Keep head + tail.
48
48
  const guardedInput = capCompactionInput(cleaned, runtime.modelInfo?.contextWindow ?? 0);
49
- const result = await generateText({
49
+ // Streamed rather than awaited whole so the caller can report real
50
+ // progress: this pass is the bulk of a /compact's wall-clock, and a bar
51
+ // that freezes for a minute reads as a hang. The summary text is still
52
+ // only used once complete.
53
+ const result = streamText({
50
54
  model: runtime.model,
51
55
  system: "You are an AI context compaction agent. Your job is to heavily summarize a chat history. Keep the core outcomes, the final state, and the technical context. Remove verbose pleasantries, step-by-step thinking, and irrelevant intermediate steps. Do NOT wrap in markdown unless it's code.",
52
56
  prompt: `The following conversation exceeds the token budget. Please summarize it concisely, maintaining the essence of what was discussed, what code was written, and what decisions were reached:${extraPrompt}\n\n${guardedInput}`,
53
57
  ...resolveTemperatureParam(runtime, 0.1),
54
58
  });
55
- compressedContent = result.text.trim();
59
+ let streamed = "";
60
+ for await (const delta of result.textStream) {
61
+ streamed += delta;
62
+ // availableChars is the size the model was ASKED to fit under, so it is
63
+ // the only honest denominator available — a concise summary finishes
64
+ // early and the caller jumps the bar to done rather than overshooting.
65
+ if (onFraction && availableChars > 0)
66
+ onFraction(Math.min(1, streamed.length / availableChars));
67
+ }
68
+ compressedContent = streamed.trim();
56
69
  usedLLM = true;
57
70
  }
58
71
  catch (e) {
@@ -14,7 +14,7 @@ export interface ExtractedDecisions {
14
14
  /**
15
15
  * Extract decisions, facts, and constraints from messages.
16
16
  *
17
- * Scans serialized conversation text for explicit markers, but if provider
18
- * and modelId are given, it asks the LLM to intelligently extract them.
17
+ * Scans serialized conversation text for explicit markers, but if modelId is
18
+ * given, it asks the LLM to intelligently extract them.
19
19
  */
20
- export declare function extractDecisions(messages: ModelMessage[], provider?: unknown, modelId?: string, customInstructions?: string): Promise<ExtractedDecisions>;
20
+ export declare function extractDecisions(messages: ModelMessage[], modelId?: string, customInstructions?: string): Promise<ExtractedDecisions>;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { generateText } from "ai";
9
9
  import { serializeConversation } from "../../orchestrator/compaction.js";
10
- import { resolveModelRuntime, resolveTemperatureParam, } from "../../providers/runtime.js";
10
+ import { resolveModelRuntime, resolveTemperatureParam } from "../../providers/runtime.js";
11
11
  import { logger } from "../../utils/logger.js";
12
12
  import { capCompactionInput } from "./input-guard.js";
13
13
  import { extractPreservedBlocks } from "./preserve.js";
@@ -27,10 +27,10 @@ function matchAll(text, re) {
27
27
  /**
28
28
  * Extract decisions, facts, and constraints from messages.
29
29
  *
30
- * Scans serialized conversation text for explicit markers, but if provider
31
- * and modelId are given, it asks the LLM to intelligently extract them.
30
+ * Scans serialized conversation text for explicit markers, but if modelId is
31
+ * given, it asks the LLM to intelligently extract them.
32
32
  */
33
- export async function extractDecisions(messages, provider, modelId, customInstructions) {
33
+ export async function extractDecisions(messages, modelId, customInstructions) {
34
34
  if (messages.length === 0) {
35
35
  return { decisions: [], facts: [], constraints: [] };
36
36
  }
@@ -42,9 +42,9 @@ export async function extractDecisions(messages, provider, modelId, customInstru
42
42
  const facts = [];
43
43
  const constraints = [];
44
44
  let usedLLM = false;
45
- if (provider && modelId) {
45
+ if (modelId) {
46
46
  try {
47
- const runtime = resolveModelRuntime(provider, modelId);
47
+ const runtime = resolveModelRuntime(modelId);
48
48
  const extraPrompt = customInstructions ? `\n\nUSER FOCUS/INSTRUCTIONS:\n${customInstructions}\n` : "";
49
49
  // Guard input against the summarizer's context window (see input-guard).
50
50
  const guardedInput = capCompactionInput(serialized, runtime.modelInfo?.contextWindow ?? 0);
@@ -6,6 +6,7 @@
6
6
  * Snapshot full chat to history/<timestamp>.md before compressing.
7
7
  */
8
8
  import type { ModelMessage } from "ai";
9
+ import { type CompactProgressFn } from "./progress.js";
9
10
  /**
10
11
  * Anti-mù for the deliberate (/compact) path — parity with the auto/tool-loop
11
12
  * compaction which persists every elided tool output via persistArtifact.
@@ -33,4 +34,4 @@ export interface CompactionResult {
33
34
  * 3. Pass 2: compressChat -> compressed output
34
35
  * 4. Return metrics
35
36
  */
36
- export declare function deliberateCompact(flowDir: string, messages: ModelMessage[], systemPrompt: string, tokenBudget: number, provider?: unknown, modelId?: string, customInstructions?: string): Promise<CompactionResult>;
37
+ export declare function deliberateCompact(flowDir: string, messages: ModelMessage[], systemPrompt: string, tokenBudget: number, modelId?: string, customInstructions?: string, onProgress?: CompactProgressFn): Promise<CompactionResult>;
@@ -15,6 +15,7 @@ import { logger } from "../../utils/logger.js";
15
15
  import { readArtifact, writeArtifact } from "../artifact-io.js";
16
16
  import { compressChat } from "./compress.js";
17
17
  import { extractDecisions } from "./extract.js";
18
+ import { stageProgress } from "./progress.js";
18
19
  /**
19
20
  * Anti-mù for the deliberate (/compact) path — parity with the auto/tool-loop
20
21
  * compaction which persists every elided tool output via persistArtifact.
@@ -83,13 +84,35 @@ export function recordToolArtifactsForRehydrate(messages, projectPath) {
83
84
  * 3. Pass 2: compressChat -> compressed output
84
85
  * 4. Return metrics
85
86
  */
86
- export async function deliberateCompact(flowDir, messages, systemPrompt, tokenBudget, provider, modelId, customInstructions) {
87
+ export async function deliberateCompact(flowDir, messages, systemPrompt, tokenBudget, modelId, customInstructions, onProgress) {
88
+ // Callers reach this through @ts-nocheck UI code, so an argument-order drift
89
+ // is invisible to tsc. It is NOT invisible at runtime: a non-string modelId
90
+ // makes resolveModelRuntime throw, compressChat catches it, and /compact
91
+ // silently degrades to truncation — which is how commit e2100fb7 shipped a
92
+ // broken /compact. Fail loudly instead of summarizing nothing.
93
+ if (modelId !== undefined && typeof modelId !== "string") {
94
+ throw new TypeError(`deliberateCompact: modelId must be a model id string, got ${typeof modelId}. ` +
95
+ `Check the argument order at the call site.`);
96
+ }
97
+ const report = (stage, fraction) => {
98
+ if (!onProgress)
99
+ return;
100
+ try {
101
+ onProgress(stageProgress(stage, fraction));
102
+ }
103
+ catch (err) {
104
+ // A broken progress sink must never fail the compaction it is reporting.
105
+ logger.error("orchestrator", "compact progress callback threw", { stage, message: err?.message });
106
+ }
107
+ };
108
+ report("artifacts");
87
109
  // Anti-mù parity: persist every tool result to the artifact cache (+ EE)
88
110
  // BEFORE we summarize the history away, so the model can still rehydrate a
89
111
  // specific tool output via ee_query "tool-artifact id=<id>" after /compact.
90
112
  recordToolArtifactsForRehydrate(messages, flowDir);
91
113
  // Pass 1: Extract decisions/facts/constraints
92
- const extracted = await extractDecisions(messages, provider, modelId, customInstructions);
114
+ report("extract");
115
+ const extracted = await extractDecisions(messages, modelId, customInstructions);
93
116
  const totalExtracted = extracted.decisions.length + extracted.facts.length + extracted.constraints.length;
94
117
  // Append to decisions.md
95
118
  if (totalExtracted > 0) {
@@ -110,6 +133,7 @@ export async function deliberateCompact(flowDir, messages, systemPrompt, tokenBu
110
133
  await writeArtifact(flowDir, "decisions.md", existing, ["Decisions", "Facts", "Constraints"]);
111
134
  }
112
135
  // Snapshot full chat to history/
136
+ report("snapshot");
113
137
  const historyDir = path.join(flowDir, "history");
114
138
  await fs.mkdir(historyDir, { recursive: true });
115
139
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
@@ -122,7 +146,9 @@ export async function deliberateCompact(flowDir, messages, systemPrompt, tokenBu
122
146
  // Token estimation before
123
147
  const tokensBefore = estimateConversationTokens(systemPrompt, messages);
124
148
  // Pass 2: Compress
125
- const compressed = await compressChat(messages, systemPrompt, tokenBudget, provider, modelId, customInstructions);
149
+ report("compress");
150
+ const compressed = await compressChat(messages, systemPrompt, tokenBudget, modelId, customInstructions, (f) => report("compress", f));
151
+ report("done");
126
152
  return {
127
153
  decisionsExtracted: totalExtracted,
128
154
  tokensBeforeCompress: tokensBefore,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * src/flow/compaction/progress.ts
3
+ *
4
+ * Progress model for deliberate (/compact) compaction.
5
+ *
6
+ * A compaction is a fixed pipeline of stages (see index.ts), so the percentage
7
+ * is stage-weighted rather than invented: each stage owns a span of the bar and
8
+ * only advances when that stage really advances. The two LLM passes dominate the
9
+ * wall-clock, which is why they own most of the span; the compress pass streams,
10
+ * so it reports a genuine within-stage fraction instead of freezing.
11
+ *
12
+ * Weights are ordered spans covering 0..100 with no gaps — asserted in tests so
13
+ * a new stage can't silently make the bar jump or stall.
14
+ */
15
+ export type CompactStage = "artifacts" | "extract" | "snapshot" | "compress" | "done";
16
+ export interface CompactProgress {
17
+ stage: CompactStage;
18
+ /** 0..100, monotonic across the run. */
19
+ percent: number;
20
+ /** Human-readable current step, e.g. "Compressing history…". */
21
+ label: string;
22
+ }
23
+ export type CompactProgressFn = (progress: CompactProgress) => void;
24
+ interface StageSpan {
25
+ from: number;
26
+ to: number;
27
+ label: string;
28
+ }
29
+ export declare const COMPACT_STAGE_SPANS: Record<CompactStage, StageSpan>;
30
+ /**
31
+ * Percent for a stage at `fraction` (0..1) of its own work. Callers that cannot
32
+ * measure within-stage progress pass 0 and land on the stage's start.
33
+ */
34
+ export declare function stageProgress(stage: CompactStage, fraction?: number): CompactProgress;
35
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * src/flow/compaction/progress.ts
3
+ *
4
+ * Progress model for deliberate (/compact) compaction.
5
+ *
6
+ * A compaction is a fixed pipeline of stages (see index.ts), so the percentage
7
+ * is stage-weighted rather than invented: each stage owns a span of the bar and
8
+ * only advances when that stage really advances. The two LLM passes dominate the
9
+ * wall-clock, which is why they own most of the span; the compress pass streams,
10
+ * so it reports a genuine within-stage fraction instead of freezing.
11
+ *
12
+ * Weights are ordered spans covering 0..100 with no gaps — asserted in tests so
13
+ * a new stage can't silently make the bar jump or stall.
14
+ */
15
+ export const COMPACT_STAGE_SPANS = {
16
+ artifacts: { from: 0, to: 5, label: "Preserving tool artifacts…" },
17
+ extract: { from: 5, to: 40, label: "Extracting decisions…" },
18
+ snapshot: { from: 40, to: 50, label: "Snapshotting history…" },
19
+ compress: { from: 50, to: 100, label: "Compressing history…" },
20
+ done: { from: 100, to: 100, label: "Compacted" },
21
+ };
22
+ /**
23
+ * Percent for a stage at `fraction` (0..1) of its own work. Callers that cannot
24
+ * measure within-stage progress pass 0 and land on the stage's start.
25
+ */
26
+ export function stageProgress(stage, fraction = 0) {
27
+ const span = COMPACT_STAGE_SPANS[stage];
28
+ const clamped = Math.max(0, Math.min(1, Number.isFinite(fraction) ? fraction : 0));
29
+ return {
30
+ stage,
31
+ percent: Math.round(span.from + (span.to - span.from) * clamped),
32
+ label: span.label,
33
+ };
34
+ }
35
+ //# sourceMappingURL=progress.js.map
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.8.5";
1
+ export declare const PACKAGE_VERSION = "1.9.0";
2
2
  export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
2
2
  // Sourced from package.json at build time so it survives bun --compile bundling.
3
- export const PACKAGE_VERSION = "1.8.5";
3
+ export const PACKAGE_VERSION = "1.9.0";
4
4
  export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
5
5
  //# sourceMappingURL=version.js.map
@@ -53,3 +53,14 @@ export declare function isRoundGroupsEnabled(): boolean;
53
53
  * MUONROI_TASK_AWARE_PANEL=0.
54
54
  */
55
55
  export declare function isTaskAwarePanelEnabled(): boolean;
56
+ /**
57
+ * Plan-review debate retry budget. The council debate can return an EMPTY
58
+ * synthesis on any of its fail-open paths (provider unreachable, sub-phase
59
+ * catch, user/watchdog abort) — a silent null that plan-council previously
60
+ * collapsed into a permanent forced-`revise`, bricking an autonomous /ideal
61
+ * heavy run (0 code, no human to re-trigger plan-review). On empty synthesis
62
+ * we retry the debate up to this many times before falling back to the
63
+ * perspective path. Default 1 (up to 2 debate attempts). Env override:
64
+ * MUONROI_PLAN_REVIEW_DEBATE_RETRIES. Clamped to [0, 5].
65
+ */
66
+ export declare function getPlanReviewDebateRetries(): number;
@@ -80,4 +80,23 @@ export function isTaskAwarePanelEnabled() {
80
80
  const raw = process.env.MUONROI_TASK_AWARE_PANEL;
81
81
  return raw !== "0" && raw?.toLowerCase() !== "false";
82
82
  }
83
+ /**
84
+ * Plan-review debate retry budget. The council debate can return an EMPTY
85
+ * synthesis on any of its fail-open paths (provider unreachable, sub-phase
86
+ * catch, user/watchdog abort) — a silent null that plan-council previously
87
+ * collapsed into a permanent forced-`revise`, bricking an autonomous /ideal
88
+ * heavy run (0 code, no human to re-trigger plan-review). On empty synthesis
89
+ * we retry the debate up to this many times before falling back to the
90
+ * perspective path. Default 1 (up to 2 debate attempts). Env override:
91
+ * MUONROI_PLAN_REVIEW_DEBATE_RETRIES. Clamped to [0, 5].
92
+ */
93
+ export function getPlanReviewDebateRetries() {
94
+ const raw = process.env.MUONROI_PLAN_REVIEW_DEBATE_RETRIES;
95
+ if (raw === undefined)
96
+ return 1;
97
+ const n = Number.parseInt(raw, 10);
98
+ if (Number.isNaN(n) || n < 0)
99
+ return 1;
100
+ return Math.min(n, 5);
101
+ }
83
102
  //# sourceMappingURL=flags.js.map