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,16 +1,26 @@
1
1
  import { readFile } from "fs/promises";
2
2
  import path from "path";
3
- import { pathToFileURL } from "url";
3
+ import { fileURLToPath, pathToFileURL } from "url";
4
4
  import { createRuntimeLspDefinitions } from "./builtins.js";
5
5
  import { createLspClientSession } from "./client.js";
6
6
  export function createWorkspaceLspManager(cwd, settings, options = {}) {
7
7
  const definitions = createRuntimeLspDefinitions(cwd, settings);
8
8
  const clients = new Map();
9
+ // Servers we have already reported as not installed. Without this the "no
10
+ // command" path is silent: a Go/Rust/C++/Java file just gets no LSP and the
11
+ // caller sees "unavailable" with no way to learn WHICH server is missing.
12
+ const reportedMissing = new Set();
9
13
  const createClient = options.createClient ??
10
14
  (async ({ serverId, root, definition, settings: normalizedSettings }) => {
11
15
  const launch = await definition.resolveLaunch(root, normalizedSettings);
12
- if (!launch?.command)
16
+ if (!launch?.command) {
17
+ if (!reportedMissing.has(serverId)) {
18
+ reportedMissing.add(serverId);
19
+ console.error(`[lsp] no language server found for "${serverId}" (${definition.extensions.join(", ")}). ` +
20
+ `Install it and make sure it is on PATH, or set lsp.builtins.${serverId}.command in settings.`);
21
+ }
13
22
  return null;
23
+ }
14
24
  return createLspClientSession({
15
25
  serverId,
16
26
  root,
@@ -96,7 +106,10 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
96
106
  diagnostics: client.getDiagnostics(filePath),
97
107
  };
98
108
  }
99
- catch {
109
+ catch (err) {
110
+ // Dropping the client silently here is what let a dead language server
111
+ // look like "no diagnostics" for the whole session.
112
+ console.error(`[lsp:${definition.id}] sync failed for ${filePath}, dropping client: ${err instanceof Error ? err.message : String(err)}`);
100
113
  clients.delete(key);
101
114
  return null;
102
115
  }
@@ -167,11 +180,238 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
167
180
  await Promise.allSettled(active.map((entry) => entry.client.stop()));
168
181
  clients.clear();
169
182
  }
183
+ // ── Sprint 1: impact/readiness contract (SLICE1-BUILD-NOTE.md) ─────────────
184
+ const TOKEN_BUDGET_CAP = 500; // hard cap (spec §49-54)
185
+ const REF_TOKEN_COST = 30; // per-reference estimate (spec §53)
186
+ const MAX_REFS = Math.floor(TOKEN_BUDGET_CAP / REF_TOKEN_COST); // 16 — truncation threshold
187
+ /** Error-level = severity ≤ 1 (Error). Warnings/infos ignored (spec §21-25). */
188
+ function errorLevel(diags) {
189
+ return diags.filter((d) => (d.severity ?? 1) <= 1);
190
+ }
191
+ /** tokenBudgetUsed = elapsed estimate + refs×30, clamped ≤500 (spec §49-54). */
192
+ function tokenBudget(elapsedMs, refCount) {
193
+ const elapsedTokens = Math.min(200, Math.round(elapsedMs / 25));
194
+ return Math.min(TOKEN_BUDGET_CAP, elapsedTokens + refCount * REF_TOKEN_COST);
195
+ }
196
+ /** Top-2 distinct error messages joined "; ", ≤120 chars; "none" when clean (spec §42-47). */
197
+ function suggestedGuardFrom(errs) {
198
+ if (errs.length === 0)
199
+ return "none";
200
+ const seen = new Set();
201
+ const messages = [];
202
+ for (const d of errs) {
203
+ const cat = (d.source ?? d.code ?? d.message.split(/[:.]/)[0] ?? "error").toString().trim();
204
+ const key = `${cat}:${d.message.trim()}`;
205
+ if (seen.has(key))
206
+ continue;
207
+ seen.add(key);
208
+ messages.push(d.message.trim());
209
+ if (messages.length === 2)
210
+ break;
211
+ }
212
+ return messages.join("; ").slice(0, 120) || "none";
213
+ }
214
+ /**
215
+ * didOpen `filePath` on every client that is about to be asked for its
216
+ * diagnostics, so the server actually analyses it. Returns true when the file
217
+ * cannot be read — nothing to diagnose, and reporting it "clean" would lie.
218
+ */
219
+ async function openForDiagnostics(filePath, records) {
220
+ let content;
221
+ try {
222
+ content = await readFile(filePath, "utf8");
223
+ }
224
+ catch (err) {
225
+ console.error(`[lsp] cannot read ${filePath} for diagnostics: ${err instanceof Error ? err.message : String(err)}`);
226
+ return true;
227
+ }
228
+ const extension = path.extname(filePath).toLowerCase();
229
+ await Promise.all(records.map(async ({ key, definition, client }) => {
230
+ const languageId = definition.languageIds[extension] ?? (extension.slice(1) || "plaintext");
231
+ try {
232
+ await client.openOrChangeFile(filePath, languageId, content);
233
+ }
234
+ catch (err) {
235
+ // Drop the client so the next call re-spawns it; the diagnostics wait
236
+ // below will surface this as a failure rather than a clean verdict.
237
+ console.error(`[lsp:${definition.id}] didOpen failed for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
238
+ clients.delete(key);
239
+ }
240
+ }));
241
+ return false;
242
+ }
243
+ async function waitForDiagnostics(filePath, timeout) {
244
+ const clampedTimeout = Math.min(Math.max(timeout, 0), 5000);
245
+ const t0 = Date.now();
246
+ const records = await getClientsForFile(filePath);
247
+ if (records.length === 0) {
248
+ return {
249
+ diagnostics: [],
250
+ lspStatus: "unavailable",
251
+ clean: false,
252
+ metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
253
+ };
254
+ }
255
+ // A language server only publishes diagnostics for files it has been told
256
+ // about. Without this didOpen, waiting on a file the server has never seen
257
+ // waits out the full timeout, gets nothing, and reports lspStatus "ok" /
258
+ // clean: true — a green verdict on a file that was never analysed at all.
259
+ // (Proven: a file with two tsc type errors reported clean.) Callers that
260
+ // already synced the buffer (syncFile) are unaffected: openOrChangeFile is
261
+ // idempotent, and diagnostics already cached short-circuit the wait below.
262
+ const unreadable = await openForDiagnostics(filePath, records);
263
+ if (unreadable) {
264
+ return {
265
+ diagnostics: [],
266
+ lspStatus: "unavailable",
267
+ clean: false,
268
+ metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
269
+ };
270
+ }
271
+ let anyTimedOut = false;
272
+ let anyFailed = false;
273
+ const allDiagnostics = [];
274
+ for (const record of records) {
275
+ try {
276
+ const diags = await withRequestTimeout(record.client.waitForDiagnostics(filePath, clampedTimeout), clampedTimeout);
277
+ allDiagnostics.push(...diags);
278
+ }
279
+ catch (err) {
280
+ if (err instanceof LspRequestTimeoutError) {
281
+ anyTimedOut = true;
282
+ allDiagnostics.push(...record.client.getDiagnostics(filePath));
283
+ }
284
+ else {
285
+ anyFailed = true;
286
+ }
287
+ }
288
+ }
289
+ const lspStatus = anyTimedOut
290
+ ? "partial"
291
+ : anyFailed && allDiagnostics.length === 0
292
+ ? "unavailable"
293
+ : "ok";
294
+ // clean: true only when we actually have data and zero error-level diags (spec §85).
295
+ const clean = lspStatus !== "unavailable" && errorLevel(allDiagnostics).length === 0;
296
+ return {
297
+ diagnostics: allDiagnostics,
298
+ lspStatus,
299
+ clean,
300
+ metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
301
+ };
302
+ }
303
+ async function impactOfChange(filePath, _query, timeout) {
304
+ const t0 = Date.now();
305
+ const diag = await waitForDiagnostics(filePath, timeout ?? 1500);
306
+ if (diag.lspStatus === "unavailable") {
307
+ const errs = errorLevel(diag.diagnostics);
308
+ return {
309
+ references: [],
310
+ diagnostics: diag.diagnostics,
311
+ referencesComplete: false,
312
+ safeToRename: false,
313
+ clean: false,
314
+ suggestedGuard: suggestedGuardFrom(errs),
315
+ degraded: "lsp_unavailable",
316
+ lspStatus: "unavailable",
317
+ metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
318
+ };
319
+ }
320
+ const records = await getClientsForFile(filePath);
321
+ const rawReferences = [];
322
+ for (const record of records) {
323
+ try {
324
+ const uri = pathToFileURL(path.resolve(filePath)).href;
325
+ const params = {
326
+ textDocument: { uri },
327
+ position: { line: 0, character: 0 },
328
+ context: { includeDeclaration: true },
329
+ };
330
+ const refs = await record.client.sendRequest("textDocument/references", params);
331
+ if (Array.isArray(refs)) {
332
+ for (const ref of refs) {
333
+ const r = ref;
334
+ if (r?.uri && r?.range) {
335
+ rawReferences.push({
336
+ uri: r.uri,
337
+ range: {
338
+ start: { line: r.range.start.line + 1, character: r.range.start.character + 1 },
339
+ end: { line: r.range.end.line + 1, character: r.range.end.character + 1 },
340
+ },
341
+ });
342
+ }
343
+ }
344
+ }
345
+ }
346
+ catch {
347
+ // Individual reference query failure: continue with partial results
348
+ }
349
+ }
350
+ // Truncate so references×30 stays within budget headroom (spec §38, §53).
351
+ const truncated = rawReferences.length > MAX_REFS;
352
+ const references = truncated ? rawReferences.slice(0, MAX_REFS) : rawReferences;
353
+ const referencesComplete = !truncated;
354
+ // Frozen union: the symbol file's waited diagnostics + each unique referenced
355
+ // file's already-published diagnostics (cheap getDiagnostics, no extra waits).
356
+ // clean is computed over this union of error-level diagnostics (spec §19-25).
357
+ const unionDiags = [...diag.diagnostics];
358
+ const seenFiles = new Set([path.resolve(filePath)]);
359
+ for (const ref of references) {
360
+ let refPath;
361
+ try {
362
+ refPath = fileURLToPath(ref.uri);
363
+ }
364
+ catch {
365
+ continue;
366
+ }
367
+ if (seenFiles.has(refPath))
368
+ continue;
369
+ seenFiles.add(refPath);
370
+ for (const record of records)
371
+ unionDiags.push(...record.client.getDiagnostics(refPath));
372
+ }
373
+ const unionErrors = errorLevel(unionDiags);
374
+ const clean = unionErrors.length === 0;
375
+ // Precedence: lsp_unavailable (handled above) > diagnostics_timeout > refs_truncated > none.
376
+ const degraded = diag.lspStatus === "partial" ? "diagnostics_timeout" : truncated ? "refs_truncated" : "none";
377
+ // safeToRename only when refs are complete, the union is clean, and nothing degraded (spec §27-31).
378
+ const safeToRename = referencesComplete && clean && degraded === "none" && diag.lspStatus === "ok";
379
+ return {
380
+ references,
381
+ diagnostics: diag.diagnostics,
382
+ referencesComplete,
383
+ safeToRename,
384
+ clean,
385
+ suggestedGuard: suggestedGuardFrom(unionErrors),
386
+ degraded,
387
+ lspStatus: diag.lspStatus,
388
+ metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, references.length) },
389
+ };
390
+ }
391
+ async function lspMutationPreview(_filePath, _change) {
392
+ // Fixed stub schema — no workspaceEdit, no apply path (spec §55-71).
393
+ return { op: "allowlist", dryRunResult: { proposedEdits: [], tokenEstimate: 0 }, schemaVersion: "1.0" };
394
+ }
395
+ async function lspBeforeGrep(filePath, query) {
396
+ const result = await impactOfChange(filePath, query);
397
+ // Policy keys on lspStatus: grep fallback allowed unless the LSP was fully ok (spec §73-77).
398
+ if (result.lspStatus !== "ok") {
399
+ return { kind: "allow", reason: `LSP ${result.lspStatus}; fall back to grep` };
400
+ }
401
+ if (result.safeToRename) {
402
+ return { kind: "allow", reason: "LSP ok and rename is safe" };
403
+ }
404
+ return { kind: "enrich", enrichWith: result };
405
+ }
170
406
  return {
171
407
  touchFile,
172
408
  syncFile,
173
409
  query,
174
410
  close,
411
+ waitForDiagnostics,
412
+ impactOfChange,
413
+ lspMutationPreview,
414
+ lspBeforeGrep,
175
415
  };
176
416
  }
177
417
  class LspRequestTimeoutError extends Error {
@@ -211,6 +451,8 @@ function getOperationMethod(operation) {
211
451
  case "incomingCalls":
212
452
  case "outgoingCalls":
213
453
  return "textDocument/prepareCallHierarchy";
454
+ case "waitForDiagnostics":
455
+ return "textDocument/diagnostic";
214
456
  }
215
457
  }
216
458
  function createOperationParams(input, absolutePath) {
@@ -244,6 +486,10 @@ function createOperationParams(input, absolutePath) {
244
486
  return {
245
487
  query: input.query ?? "",
246
488
  };
489
+ case "waitForDiagnostics":
490
+ return {
491
+ textDocument: { uri },
492
+ };
247
493
  }
248
494
  }
249
495
  export function summarizeLspDiagnostics(diagnostics) {
@@ -1,2 +1,12 @@
1
- export declare function lspNpmWhich(pkg: string): Promise<string | null>;
1
+ /**
2
+ * Absolute path to a language-server binary in the cache, installing the package
3
+ * on first use.
4
+ *
5
+ * `preferredBin` is the binary the CALLER needs, which is not always the one
6
+ * named after the package: `pyright` ships both `pyright` (the batch CLI) and
7
+ * `pyright-langserver` (the LSP server). Choosing by package name alone launched
8
+ * the CLI with `--stdio`, which exits 4 with "pyright --help for usage" — so
9
+ * Python LSP never started for anyone relying on auto-install.
10
+ */
11
+ export declare function lspNpmWhich(pkg: string, preferredBin?: string): Promise<string | null>;
2
12
  export declare function lspNpmAdd(pkg: string): Promise<string>;
@@ -10,13 +10,27 @@ function packageDir(pkg) {
10
10
  : pkg;
11
11
  return path.join(CACHE_ROOT, sanitized);
12
12
  }
13
- export async function lspNpmWhich(pkg) {
13
+ /**
14
+ * Absolute path to a language-server binary in the cache, installing the package
15
+ * on first use.
16
+ *
17
+ * `preferredBin` is the binary the CALLER needs, which is not always the one
18
+ * named after the package: `pyright` ships both `pyright` (the batch CLI) and
19
+ * `pyright-langserver` (the LSP server). Choosing by package name alone launched
20
+ * the CLI with `--stdio`, which exits 4 with "pyright --help for usage" — so
21
+ * Python LSP never started for anyone relying on auto-install.
22
+ */
23
+ export async function lspNpmWhich(pkg, preferredBin) {
14
24
  const dir = packageDir(pkg);
15
25
  const binDir = path.join(dir, "node_modules", ".bin");
16
26
  const pick = async () => {
17
27
  const files = await readdir(binDir).catch(() => []);
18
28
  if (files.length === 0)
19
29
  return undefined;
30
+ // The caller's choice wins whenever the package actually ships it. Match the
31
+ // extension-less shim: Windows also lists .cmd/.ps1 wrappers for each bin.
32
+ if (preferredBin && files.includes(preferredBin))
33
+ return preferredBin;
20
34
  if (files.length === 1)
21
35
  return files[0];
22
36
  const pkgJsonPath = path.join(dir, "node_modules", pkg, "package.json");
@@ -29,6 +43,8 @@ export async function lspNpmWhich(pkg) {
29
43
  const keys = Object.keys(bin);
30
44
  if (keys.length === 1)
31
45
  return keys[0];
46
+ if (preferredBin && bin[preferredBin])
47
+ return preferredBin;
32
48
  return bin[unscoped] ? unscoped : keys[0];
33
49
  }
34
50
  return files[0];
@@ -1,6 +1,11 @@
1
- import type { LspDiagnosticFile, LspQueryInput, LspToolResponse } from "./types.js";
1
+ import { type WorkspaceLspManager } from "./manager.js";
2
+ import type { ImpactOfChangeResult, LspDiagnosticFile, LspQueryInput, LspQueryResult, LspToolResponse, MutationPreviewResult } from "./types.js";
2
3
  export declare function queryLsp(cwd: string, input: LspQueryInput): Promise<LspToolResponse>;
4
+ export declare function waitForDiagnosticsLsp(cwd: string, filePath: string, timeout?: number): Promise<LspQueryResult>;
5
+ export declare function impactOfChangeLsp(cwd: string, filePath: string, query?: string): Promise<ImpactOfChangeResult>;
6
+ export declare function lspMutationPreview(cwd: string, filePath: string, change: string): Promise<MutationPreviewResult>;
3
7
  export declare function syncFileWithLsp(cwd: string, filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean, diagnosticsTimeoutMs?: number): Promise<LspDiagnosticFile[]>;
4
8
  export declare function isLspToolEnabled(_cwd: string): boolean;
5
9
  export declare function summarizeDiagnostics(diagnostics: LspDiagnosticFile[]): string | null;
6
10
  export declare function shutdownWorkspaceLspManager(cwd: string): Promise<void>;
11
+ export declare function getOrCreateManager(cwd: string): WorkspaceLspManager;
@@ -10,6 +10,22 @@ export async function queryLsp(cwd, input) {
10
10
  filePath: path.isAbsolute(input.filePath) ? input.filePath : path.resolve(cwd, input.filePath),
11
11
  });
12
12
  }
13
+ // Default timeout (ms) when a caller omits one. manager.waitForDiagnostics now
14
+ // takes a REQUIRED timeout (Sprint-1 readiness contract, commit 4af6efb1) and no
15
+ // longer defaults internally, so the public wrapper supplies the documented 1500ms.
16
+ const DEFAULT_WAIT_DIAGNOSTICS_MS = 1500;
17
+ export async function waitForDiagnosticsLsp(cwd, filePath, timeout) {
18
+ const manager = getOrCreateManager(cwd);
19
+ return manager.waitForDiagnostics(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), timeout ?? DEFAULT_WAIT_DIAGNOSTICS_MS);
20
+ }
21
+ export async function impactOfChangeLsp(cwd, filePath, query) {
22
+ const manager = getOrCreateManager(cwd);
23
+ return manager.impactOfChange(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), query);
24
+ }
25
+ export async function lspMutationPreview(cwd, filePath, change) {
26
+ const manager = getOrCreateManager(cwd);
27
+ return manager.lspMutationPreview(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), change);
28
+ }
13
29
  export async function syncFileWithLsp(cwd, filePath, content, save = true, waitForDiagnostics = true, diagnosticsTimeoutMs) {
14
30
  const manager = getOrCreateManager(cwd);
15
31
  return manager.syncFile(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), content, save, waitForDiagnostics, diagnosticsTimeoutMs);
@@ -29,7 +45,7 @@ export async function shutdownWorkspaceLspManager(cwd) {
29
45
  managers.delete(key);
30
46
  await manager.close();
31
47
  }
32
- function getOrCreateManager(cwd) {
48
+ export function getOrCreateManager(cwd) {
33
49
  const key = resolveManagerKey(cwd);
34
50
  const existing = managers.get(key);
35
51
  if (existing)
@@ -1,4 +1,4 @@
1
- export declare const LSP_TOOL_OPERATIONS: readonly ["goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol", "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls"];
1
+ export declare const LSP_TOOL_OPERATIONS: readonly ["goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol", "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls", "waitForDiagnostics"];
2
2
  export type LspToolOperation = (typeof LSP_TOOL_OPERATIONS)[number];
3
3
  export type LspBuiltInServerId = "typescript" | "pyright" | "gopls" | "rust-analyzer" | "bash-language-server" | "yaml-language-server" | "clangd" | "jdtls" | "sourcekit-lsp" | "csharp-ls";
4
4
  export interface LspPosition {
@@ -78,6 +78,88 @@ export interface LspQueryInput {
78
78
  character?: number;
79
79
  query?: string;
80
80
  }
81
+ /**
82
+ * LSP readiness status (SLICE1-BUILD-NOTE.md §79-86, §73-77).
83
+ * - `ok` — diagnostics resolved within the ≤5s budget
84
+ * - `partial` — one or more waits timed out; stale diagnostics were used
85
+ * - `unavailable` — no server could be reached or the query threw
86
+ * The lsp-before-grep policy keys on this: grep fallback is allowed when
87
+ * `lspStatus !== 'ok'`.
88
+ */
89
+ export type LspStatus = "ok" | "partial" | "unavailable";
90
+ /**
91
+ * Reason a composite result is degraded (SLICE1-BUILD-NOTE.md §33-40).
92
+ * Precedence when several apply: lsp_unavailable > diagnostics_timeout >
93
+ * refs_truncated > none.
94
+ */
95
+ export type DegradedReason = "none" | "refs_truncated" | "diagnostics_timeout" | "lsp_unavailable";
96
+ /** Per-call token-budget accounting; `tokenBudgetUsed` is hard-capped ≤500. */
97
+ export interface LspResultMetadata {
98
+ /** Elapsed + reference estimate, clamped to ≤500 (SLICE1-BUILD-NOTE.md §49-54). */
99
+ tokenBudgetUsed: number;
100
+ }
101
+ /** `waitForDiagnostics` result (SLICE1-BUILD-NOTE.md §79-86). */
102
+ export interface LspQueryResult {
103
+ diagnostics: LspDiagnostic[];
104
+ lspStatus: LspStatus;
105
+ /** True only when zero error-level (severity ≤ 1) diagnostics exist for the file. */
106
+ clean: boolean;
107
+ metadata: LspResultMetadata;
108
+ }
109
+ /** `impact_of_change` composite result (SLICE1-BUILD-NOTE.md §10, §19-54). */
110
+ export interface ImpactOfChangeResult {
111
+ references: LspLocation[];
112
+ diagnostics: LspDiagnostic[];
113
+ /** False when the reference list was truncated to stay within token budget. */
114
+ referencesComplete: boolean;
115
+ /**
116
+ * True only when `referencesComplete === true` AND the frozen union (symbol
117
+ * file + every referenced file) has zero error-level diagnostics AND the LSP
118
+ * was fully available. `false` is the safe default → agents grep-fallback.
119
+ */
120
+ safeToRename: boolean;
121
+ /** Zero error-level diagnostics on the union of the symbol file + reference files. */
122
+ clean: boolean;
123
+ /** Top-2 error category messages joined with "; ", ≤120 chars; "none" when clean. */
124
+ suggestedGuard: string;
125
+ degraded: DegradedReason;
126
+ lspStatus: LspStatus;
127
+ metadata: LspResultMetadata;
128
+ }
129
+ /**
130
+ * `lsp_mutation_preview` stub (SLICE1-BUILD-NOTE.md §55-71). Fixed schema, no
131
+ * `workspaceEdit`, no apply path — real diff computation is Slice 2.
132
+ */
133
+ export interface MutationPreviewResult {
134
+ readonly op: "allowlist";
135
+ readonly dryRunResult: {
136
+ readonly proposedEdits: [];
137
+ readonly tokenEstimate: number;
138
+ };
139
+ readonly schemaVersion: "1.0";
140
+ }
141
+ /**
142
+ * Tagged error kind for LSP operation failures.
143
+ * Throw-only canonical failure for slice 1 ── every failure flows
144
+ * through a single syntactic choke point (the projection's try/catch).
145
+ */
146
+ export type LspErrorKind = "no_server" | "request_timeout" | "request_failed" | "lsp_disabled" | "unknown";
147
+ /** Tagged‑union Error used by manager methods. */
148
+ export declare class LspError extends Error {
149
+ readonly kind: LspErrorKind;
150
+ constructor(kind: LspErrorKind, message: string);
151
+ }
152
+ /** Policy decision the lsp‑before‑grep gate returns. */
153
+ export type PolicyAction = {
154
+ kind: "allow";
155
+ reason: string;
156
+ } | {
157
+ kind: "block";
158
+ reason: string;
159
+ } | {
160
+ kind: "enrich";
161
+ enrichWith: ImpactOfChangeResult;
162
+ };
81
163
  export interface LspToolResponse {
82
164
  success: boolean;
83
165
  output: string;
@@ -8,5 +8,15 @@ export const LSP_TOOL_OPERATIONS = [
8
8
  "prepareCallHierarchy",
9
9
  "incomingCalls",
10
10
  "outgoingCalls",
11
+ "waitForDiagnostics",
11
12
  ];
13
+ /** Tagged‑union Error used by manager methods. */
14
+ export class LspError extends Error {
15
+ kind;
16
+ constructor(kind, message) {
17
+ super(message);
18
+ this.name = "LspError";
19
+ this.kind = kind;
20
+ }
21
+ }
12
22
  //# sourceMappingURL=types.js.map
@@ -238,29 +238,57 @@ export async function acquireMcpTools(servers, opts) {
238
238
  */
239
239
  export async function warmMcpClients(servers, syncAndLog = false) {
240
240
  const validServers = servers.filter((s) => s.enabled && validateMcpServerConfig(s).ok);
241
- if (syncAndLog && validServers.length > 0) {
242
- console.log(`[MCP] Warming up ${validServers.length} server(s)...`);
241
+ // Fire-and-forget background warmup no user-facing output.
242
+ if (!syncAndLog) {
243
+ void Promise.all(validServers.map((s) => getOrConnect(s).catch((e) => {
244
+ // Intentionally non-fatal: getOrConnect evicts the failed entry so a
245
+ // real turn retries. Surface at debug level only (No Silent Catch).
246
+ if (process.env.MUONROI_DEBUG_MCP) {
247
+ console.error(`[mcp:warm] background warmup failed for ${s.id}: ${e instanceof Error ? e.message : String(e)}`);
248
+ }
249
+ })));
250
+ return;
243
251
  }
244
- const promises = validServers.map(async (s) => {
252
+ if (validServers.length === 0)
253
+ return;
254
+ // Synchronous warmup (fresh start): render ONE compact in-place progress line
255
+ // instead of a line per server — e.g. "⏳ Starting MCP servers (0/4): a, b, c".
256
+ const total = validServers.length;
257
+ const names = validServers.map((s) => s.id).join(", ");
258
+ const isTTY = Boolean(process.stdout.isTTY);
259
+ let done = 0;
260
+ const failures = [];
261
+ const render = () => {
262
+ // \r + clear-line (\x1b[2K) rewrites the same terminal row each tick.
263
+ process.stdout.write(`\r\x1b[2K⏳ Starting MCP servers (${done}/${total}): ${names}`);
264
+ };
265
+ if (isTTY)
266
+ render();
267
+ else
268
+ console.log(`[MCP] Starting ${total} server(s): ${names}`);
269
+ await Promise.all(validServers.map(async (s) => {
245
270
  try {
246
271
  await getOrConnect(s);
247
- if (syncAndLog) {
248
- console.log(`[MCP] ✅ ${s.id}: healthy`);
249
- }
250
272
  }
251
273
  catch (e) {
252
- if (syncAndLog) {
253
- const msg = e instanceof Error ? e.message : String(e);
254
- console.log(`[MCP] ❌ ${s.id}: failed - ${msg}`);
255
- }
274
+ failures.push(`${s.id}: ${e instanceof Error ? e.message : String(e)}`);
256
275
  }
257
- });
258
- if (syncAndLog) {
259
- await Promise.all(promises);
276
+ finally {
277
+ done += 1;
278
+ if (isTTY)
279
+ render();
280
+ }
281
+ }));
282
+ const ok = total - failures.length;
283
+ if (isTTY) {
284
+ process.stdout.write(`\r\x1b[2K✅ MCP servers ready (${ok}/${total})\n`);
260
285
  }
261
286
  else {
262
- // Fire-and-forget for background warmup
263
- void Promise.all(promises);
287
+ console.log(`[MCP] ${ok}/${total} server(s) ready`);
288
+ }
289
+ // Compact failure summary (one line, not per-server spam) — keeps errors visible.
290
+ if (failures.length > 0) {
291
+ console.warn(`[MCP] ⚠️ ${failures.length} unavailable: ${failures.join(" | ")}`);
264
292
  }
265
293
  }
266
294
  /** Tear down every pooled client. Call on orchestrator/process shutdown. */
@@ -8,9 +8,13 @@
8
8
  * deps are injected so unit tests never spawn a real language server.
9
9
  */
10
10
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- import { type LspQueryInput, type LspToolResponse } from "../lsp/types.js";
11
+ import { type ImpactOfChangeResult, type LspQueryInput, type LspQueryResult, type LspToolResponse, type MutationPreviewResult, type PolicyAction } from "../lsp/types.js";
12
12
  export interface LspToolDeps {
13
13
  query?: (cwd: string, input: LspQueryInput) => Promise<LspToolResponse>;
14
14
  enabled?: (cwd: string) => boolean | Promise<boolean>;
15
+ waitForDiagnostics?: (cwd: string, filePath: string, timeout?: number) => Promise<LspQueryResult>;
16
+ impactOfChange?: (cwd: string, filePath: string, query?: string) => Promise<ImpactOfChangeResult>;
17
+ lspMutationPreview?: (cwd: string, filePath: string, change: string) => Promise<MutationPreviewResult>;
18
+ lspBeforeGrep?: (cwd: string, filePath: string, query?: string) => Promise<PolicyAction>;
15
19
  }
16
20
  export declare function registerLspTools(server: McpServer, deps?: LspToolDeps): void;