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
@@ -24,6 +24,7 @@
24
24
  *
25
25
  * Override the push gate with `MUONROI_ALLOW_PUSH_ON_RED=1`.
26
26
  */
27
+ import { spawnSync } from "node:child_process";
27
28
  import * as fs from "node:fs";
28
29
  import * as path from "node:path";
29
30
  import { isVerificationCommand } from "../orchestrator/tool-args-hash.js";
@@ -216,6 +217,173 @@ export function checkSensitiveStaging(cwd) {
216
217
  "To bypass this gate (not recommended), set MUONROI_ALLOW_BROAD_STAGE=1.";
217
218
  return { blocked: true, sensitive, message };
218
219
  }
220
+ // ── Destructive-op guard (2026-07-12) ──────────────────────────────────────
221
+ //
222
+ // Motivation: an autonomous /ideal plan-adherence fix sub-agent ran
223
+ // `git checkout -- <files>` + committed the result, silently discarding the
224
+ // user's UNCOMMITTED, unrelated working-tree changes (a whole set of harness
225
+ // fixes). No existing guard caught it. This guard classifies commands that
226
+ // irreversibly discard uncommitted work and blocks them PRE-EXECUTION when
227
+ // there is actually work at risk. Routed as the `destructive-revert` safety
228
+ // kind: interactive → askcard (allow-once/block, never auto-allow even in
229
+ // yolo); headless/autonomous → hard-block. Escape hatch:
230
+ // MUONROI_ALLOW_DESTRUCTIVE_REVERT=1 (user-set, logged like the other gates).
231
+ // `git checkout … -- ` (discard paths) OR `git checkout .` (discard all).
232
+ const CHECKOUT_DISCARD_RE = /\bgit\b[^|&;\n]*[ \t]checkout\b(?:[^|&;\n]*[ \t]--(?:[ \t]|$)|[ \t]+\.(?=[ \t]|$))/;
233
+ // `git restore …` discards the working tree by default. `git restore --staged`
234
+ // WITHOUT `--worktree`/`-W` only unstages (non-destructive) — exempt that.
235
+ const RESTORE_RE = /\bgit\b[^|&;\n]*[ \t]restore\b/;
236
+ const RESTORE_STAGED_RE = /[ \t]--staged\b/;
237
+ const RESTORE_WORKTREE_RE = /[ \t](?:--worktree\b|-[a-z]*W)/;
238
+ // `git reset --hard` throws away all uncommitted changes.
239
+ const RESET_HARD_RE = /\bgit\b[^|&;\n]*[ \t]reset\b[^|&;\n]*[ \t]--hard\b/;
240
+ // `git clean -f[d][x]` deletes untracked files.
241
+ const CLEAN_FORCE_RE = /\bgit\b[^|&;\n]*[ \t]clean\b[^|&;\n]*[ \t]-[a-eg-wyz]*f/;
242
+ // `git stash drop` / `git stash clear` destroy stashed work.
243
+ const STASH_DROP_RE = /\bgit\b[^|&;\n]*[ \t]stash[ \t]+(?:drop|clear)\b/;
244
+ // `rm` (not rmdir) — targets checked against git tracking below.
245
+ const RM_RE = /(?:^|[|&;\n]|\s)rm\b(?!dir)/;
246
+ export function analyzeDestructiveGit(command) {
247
+ const c = stripQuoted(command);
248
+ if (RESET_HARD_RE.test(c))
249
+ return { kind: "reset-hard", untrackedRisk: false };
250
+ if (CHECKOUT_DISCARD_RE.test(c))
251
+ return { kind: "checkout-discard", untrackedRisk: false };
252
+ if (RESTORE_RE.test(c)) {
253
+ const stagedOnly = RESTORE_STAGED_RE.test(c) && !RESTORE_WORKTREE_RE.test(c);
254
+ if (!stagedOnly)
255
+ return { kind: "restore", untrackedRisk: false };
256
+ }
257
+ if (CLEAN_FORCE_RE.test(c))
258
+ return { kind: "clean", untrackedRisk: true };
259
+ if (STASH_DROP_RE.test(c))
260
+ return { kind: "stash-drop", untrackedRisk: false };
261
+ return { kind: null, untrackedRisk: false };
262
+ }
263
+ function git(cwd, args) {
264
+ try {
265
+ const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 8000, maxBuffer: 8 * 1024 * 1024 });
266
+ return (r.stdout ?? "").trim();
267
+ }
268
+ catch {
269
+ return "";
270
+ }
271
+ }
272
+ /** Working-tree paths that a destructive command would discard (best-effort). */
273
+ function atRiskPaths(cwd, untrackedRisk) {
274
+ // NOTE: read raw stdout — do NOT trim the whole output. Porcelain v1 lines
275
+ // are "XY<space>PATH" where XY is 2 chars; a global trim would eat the leading
276
+ // space of a " M file" line and shift the path parse by one character.
277
+ let raw;
278
+ try {
279
+ const r = spawnSync("git", ["status", "--porcelain"], {
280
+ cwd,
281
+ encoding: "utf8",
282
+ timeout: 8000,
283
+ maxBuffer: 8 * 1024 * 1024,
284
+ });
285
+ raw = r.stdout ?? "";
286
+ }
287
+ catch {
288
+ return [];
289
+ }
290
+ const out = [];
291
+ for (const line of raw.split("\n")) {
292
+ if (line.length < 4)
293
+ continue;
294
+ const xy = line.slice(0, 2);
295
+ const file = line.slice(3).trim();
296
+ const isUntracked = xy === "??";
297
+ if (untrackedRisk ? isUntracked : !isUntracked && xy !== "!!")
298
+ out.push(file);
299
+ }
300
+ return out;
301
+ }
302
+ /** Extract rm target tokens (non-flag args) from a single-clause rm command. */
303
+ function rmTargets(command) {
304
+ const c = stripQuoted(command);
305
+ const m = c.match(/(?:^|[|&;\n]|\s)rm\b(?!dir)([^|&;\n]*)/);
306
+ if (!m)
307
+ return [];
308
+ return m[1]
309
+ .split(/\s+/)
310
+ .map((t) => t.trim())
311
+ .filter((t) => t.length > 0 && !t.startsWith("-") && t !== '""' && t !== "''");
312
+ }
313
+ /** rm targets that are git-tracked (file directly, or a dir containing tracked files). */
314
+ function rmTrackedTargets(cwd, command) {
315
+ const targets = rmTargets(command);
316
+ const tracked = [];
317
+ for (const t of targets) {
318
+ // Tracked file?
319
+ const asFile = spawnSync("git", ["ls-files", "--error-unmatch", t], {
320
+ cwd,
321
+ encoding: "utf8",
322
+ timeout: 6000,
323
+ });
324
+ if (asFile.status === 0) {
325
+ tracked.push(t);
326
+ continue;
327
+ }
328
+ // Directory containing tracked files?
329
+ const underDir = git(cwd, ["ls-files", "--", t]);
330
+ if (underDir)
331
+ tracked.push(t);
332
+ }
333
+ return tracked;
334
+ }
335
+ /**
336
+ * Pre-execution guard: block a working-tree-destroying command when it would
337
+ * actually discard uncommitted work. Returns blocked=false (no message) when
338
+ * the command is non-destructive, when nothing is at risk, or when the user set
339
+ * the escape hatch. Deterministic + cheap (a couple of git plumbing calls).
340
+ */
341
+ export function checkDestructiveOp(command, cwd) {
342
+ if (process.env.MUONROI_ALLOW_DESTRUCTIVE_REVERT === "1") {
343
+ return { blocked: false, kind: null, message: "" };
344
+ }
345
+ const shape = analyzeDestructiveGit(command);
346
+ if (shape.kind) {
347
+ let atRisk;
348
+ if (shape.kind === "stash-drop") {
349
+ atRisk = git(cwd, ["stash", "list"]).split("\n").filter(Boolean);
350
+ }
351
+ else {
352
+ atRisk = atRiskPaths(cwd, shape.untrackedRisk);
353
+ }
354
+ if (atRisk.length === 0)
355
+ return { blocked: false, kind: shape.kind, message: "" };
356
+ return { blocked: true, kind: shape.kind, message: destructiveMessage(shape.kind, atRisk) };
357
+ }
358
+ if (RM_RE.test(stripQuoted(command))) {
359
+ const tracked = rmTrackedTargets(cwd, command);
360
+ if (tracked.length > 0) {
361
+ return { blocked: true, kind: "rm-tracked", message: destructiveMessage("rm-tracked", tracked) };
362
+ }
363
+ }
364
+ return { blocked: false, kind: null, message: "" };
365
+ }
366
+ function destructiveMessage(kind, items) {
367
+ const list = items
368
+ .slice(0, 30)
369
+ .map((n) => ` • ${n}`)
370
+ .join("\n");
371
+ const more = items.length > 30 ? `\n … and ${items.length - 30} more` : "";
372
+ const verb = {
373
+ "checkout-discard": "`git checkout` discarding working-tree changes",
374
+ restore: "`git restore` discarding working-tree changes",
375
+ "reset-hard": "`git reset --hard` discarding ALL uncommitted changes",
376
+ clean: "`git clean` deleting untracked files",
377
+ "stash-drop": "`git stash drop/clear` destroying stashed work",
378
+ "rm-tracked": "`rm` deleting git-tracked file(s)",
379
+ };
380
+ return (`BLOCKED: refusing ${verb[kind]} — this irreversibly discards uncommitted work:\n` +
381
+ `${list}${more}\n\n` +
382
+ "These changes are NOT committed and cannot be recovered once discarded. If you did " +
383
+ "not intend to destroy them, leave them alone (they may be the user's unrelated work). " +
384
+ "To keep them, `git add` + `git commit` (or `git stash`) FIRST, then retry. To bypass " +
385
+ "this guard for a genuine, intentional discard, set MUONROI_ALLOW_DESTRUCTIVE_REVERT=1.");
386
+ }
219
387
  /**
220
388
  * @deprecated Use checkSensitiveStaging() for hard-blocking.
221
389
  * Kept for call sites that emit a trailing warning (non-broad-stage contexts).
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { type ToolSet } from "ai";
22
22
  /** The native tool names this module registers — used by the MCP-twin dedup. */
23
- export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "ee_write", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
23
+ export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "ee_write", "usage_forensics", "lsp_query", "wait_for_diagnostics", "impact_of_change", "lsp_mutation_preview", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
24
24
  export interface NativeToolOpts {
25
25
  /** Workspace cwd for lsp_query. Defaults to process.cwd(). */
26
26
  cwd?: string;
@@ -19,9 +19,22 @@
19
19
  * identical across the two surfaces.
20
20
  */
21
21
  import { dynamicTool, jsonSchema } from "ai";
22
- import { LSP_TOOL_OPERATIONS } from "../lsp/types.js";
22
+ import { LSP_TOOL_OPERATIONS, LspError } from "../lsp/types.js";
23
23
  import { getSelfVerifyJobManager } from "../mcp/self-verify-runner.js";
24
24
  import { SETUP_GUIDE_TEXT } from "../mcp/setup-guide-text.js";
25
+ /** Map a thrown error to the LspError tagged-union shape. */
26
+ function mapLspError(e) {
27
+ if (e instanceof LspError)
28
+ return e;
29
+ if (e && typeof e === "object" && "kind" in e && "message" in e) {
30
+ return new LspError(e.kind, e.message);
31
+ }
32
+ return new LspError("unknown", e instanceof Error ? e.message : String(e));
33
+ }
34
+ function lspErrLine(e) {
35
+ const err = mapLspError(e);
36
+ return JSON.stringify({ error: err.kind, message: err.message });
37
+ }
25
38
  /** The native tool names this module registers — used by the MCP-twin dedup. */
26
39
  export const NATIVE_MUONROI_TOOL_NAMES = [
27
40
  "ee_health",
@@ -29,6 +42,9 @@ export const NATIVE_MUONROI_TOOL_NAMES = [
29
42
  "ee_write",
30
43
  "usage_forensics",
31
44
  "lsp_query",
45
+ "wait_for_diagnostics",
46
+ "impact_of_change",
47
+ "lsp_mutation_preview",
32
48
  "setup_guide",
33
49
  "selfverify_start",
34
50
  "selfverify_status",
@@ -219,6 +235,65 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
219
235
  }
220
236
  },
221
237
  });
238
+ // ── Sprint 1: readiness contract tools ─────────────────────────────────────
239
+ tools.wait_for_diagnostics = dynamicTool({
240
+ description: "Wait for LSP diagnostics on a file, with timeout clamping (default 1500ms, max 5000ms). " +
241
+ "Returns { diagnostics: LspDiagnostic[], lspStatus: 'ok'|'partial'|'unavailable', clean: boolean, metadata: { tokenBudgetUsed } }. " +
242
+ "clean=true means zero error-level diagnostics. Grep fallback is allowed when lspStatus !== 'ok'.",
243
+ inputSchema: jsonSchema({
244
+ type: "object",
245
+ properties: {
246
+ filePath: { type: "string", description: "Absolute or workspace-relative path" },
247
+ timeout: { type: "number", description: "Timeout in ms (default 1500, max 5000)", optional: true },
248
+ },
249
+ required: ["filePath"],
250
+ }),
251
+ execute: async (input) => {
252
+ const cwd = opts.cwd ?? process.cwd();
253
+ try {
254
+ const { getOrCreateManager } = await import("../lsp/runtime.js");
255
+ return json(await getOrCreateManager(cwd).waitForDiagnostics(input.filePath, input.timeout ?? 1500));
256
+ }
257
+ catch (e) {
258
+ return lspErrLine(e);
259
+ }
260
+ },
261
+ });
262
+ tools.impact_of_change = dynamicTool({
263
+ description: "Analyse impact of a change: returns { references, diagnostics, referencesComplete, safeToRename, clean, " +
264
+ "suggestedGuard, degraded, lspStatus, metadata }. Safe for use as lsp-before-grep gate: grep fallback is " +
265
+ "allowed when lspStatus !== 'ok'; safeToRename is true only when refs are complete and the union is clean.",
266
+ inputSchema: jsonSchema({
267
+ type: "object",
268
+ properties: {
269
+ filePath: { type: "string", description: "Absolute or workspace-relative path" },
270
+ query: { type: "string", description: "Optional symbol query", optional: true },
271
+ },
272
+ required: ["filePath"],
273
+ }),
274
+ execute: async (input) => {
275
+ const cwd = opts.cwd ?? process.cwd();
276
+ const { getOrCreateManager } = await import("../lsp/runtime.js");
277
+ return json(await getOrCreateManager(cwd).impactOfChange(input.filePath, input.query));
278
+ },
279
+ });
280
+ tools.lsp_mutation_preview = dynamicTool({
281
+ description: "Preview a mutation (stub — no real edits). Returns { op: 'allowlist', dryRunResult: { proposedEdits: [], " +
282
+ "tokenEstimate: 0 }, schemaVersion: '1.0' }. MUTATION_TOOLS gating applies; no apply path in Sprint 1.",
283
+ inputSchema: jsonSchema({
284
+ type: "object",
285
+ properties: {
286
+ filePath: { type: "string", description: "File path" },
287
+ change: { type: "string", description: "Change specification (stub)" },
288
+ },
289
+ required: ["filePath", "change"],
290
+ }),
291
+ execute: async (input) => {
292
+ const cwd = opts.cwd ?? process.cwd();
293
+ const { getOrCreateManager } = await import("../lsp/runtime.js");
294
+ return json(await getOrCreateManager(cwd).lspMutationPreview(input.filePath, input.change));
295
+ },
296
+ });
222
297
  // ── Onboarding: setup_guide ───────────────────────────────────────────────
223
298
  tools.setup_guide = dynamicTool({
224
299
  description: "Returns the up-to-date setup / install / first-run / MCP wiring / verify guide for muonroi-cli. Call this " +
@@ -6,6 +6,7 @@
6
6
  * is sent to the API for all providers (including DeepSeek, Gemini, etc.).
7
7
  */
8
8
  import { type ToolSet } from "ai";
9
+ import type { AskUserAskInfo } from "../orchestrator/ask-user.js";
9
10
  import type { AgentMode, TaskRequest, ToolResult } from "../types/index.js";
10
11
  import type { BashTool } from "./bash.js";
11
12
  interface ToolRegistryOpts {
@@ -34,6 +35,22 @@ interface ToolRegistryOpts {
34
35
  */
35
36
  sessionId?: string;
36
37
  runDebate?: (topic: string) => Promise<string>;
38
+ /**
39
+ * When true, the `convene_council` tool is registered so the agent can
40
+ * convene the multi-model council on demand mid-turn. Set by the tool-engine
41
+ * only when the council is actually usable for this session
42
+ * (`configuredRoleCount >= getAutoCouncilMinRoles()`); omitted/false → the
43
+ * tool is absent so the model never calls a council that cannot convene.
44
+ */
45
+ councilConfigured?: boolean;
46
+ /**
47
+ * When provided, the `ask_user` tool is registered so the agent can ask the
48
+ * human a question mid-turn and receive their answer AS the tool result. The
49
+ * handler blocks until the human responds (or dismisses). Omitted (headless
50
+ * with no answerer) → the tool is absent, so the model never calls a card
51
+ * that can never be answered.
52
+ */
53
+ askUser?: (info: AskUserAskInfo) => Promise<string>;
37
54
  }
38
55
  /**
39
56
  * Phase 4R: session-scoped bash repeat detector state.
@@ -8,6 +8,7 @@
8
8
  import { dynamicTool, jsonSchema } from "ai";
9
9
  import { registerGsdWorkflowTools } from "../gsd/workflow-tools.js";
10
10
  import { requestProactiveCompact } from "../orchestrator/compact-request.js";
11
+ import { requestCouncilConvene } from "../orchestrator/council-request.js";
11
12
  import { canonicalizeBashCommand } from "../orchestrator/tool-args-hash.js";
12
13
  import { analyzeImageFromSource, askVisionProxy, listCachedImages } from "../providers/mcp-vision-bridge.js";
13
14
  import { needsVisionProxy } from "../providers/vision-proxy.js";
@@ -15,7 +16,7 @@ import { loadMcpServers } from "../utils/settings.js";
15
16
  import { getBashRun, sliceBashOutput } from "./bash-output-cache.js";
16
17
  import { editFile, readFile, readFiles, writeFile } from "./file.js";
17
18
  import { FileTracker } from "./file-tracker.js";
18
- import { analyzeGitCommand, checkPushGate, checkSensitiveStaging, commitBlockedMessage, pushBlockedMessage, recordCommandOutcome, } from "./git-safety.js";
19
+ import { analyzeGitCommand, checkDestructiveOp, checkPushGate, checkSensitiveStaging, commitBlockedMessage, pushBlockedMessage, recordCommandOutcome, } from "./git-safety.js";
19
20
  import { executeGrep } from "./grep.js";
20
21
  import { registerNativeMuonroiTools } from "./native-tools.js";
21
22
  import { registerNativeResearchTools } from "./research.js";
@@ -168,6 +169,109 @@ export function createBuiltinTools(bash, mode, opts) {
168
169
  });
169
170
  },
170
171
  });
172
+ // convene_council — agent-initiated multi-model council. The model calls this
173
+ // when THIS request genuinely warrants a cross-provider debate (conflicting
174
+ // design tradeoffs, high-stakes decision). The request is queued here; the
175
+ // tool-engine consumes it from the outer restart loop, runs runCouncilV2 with
176
+ // convenePath:true (which suppresses ALL hardcoded post-debate decision
177
+ // surface — no card, no continuation), splices the synthesis into THIS tool's
178
+ // tool_result, and restarts the step so the model reads the conclusion as the
179
+ // result and continues. Registered only when the council is usable so the
180
+ // model never calls a council that cannot convene.
181
+ if (opts?.councilConfigured) {
182
+ tools.convene_council = dynamicTool({
183
+ description: "Convene the multi-model council to debate THIS request. Use ONLY when the task has genuinely conflicting design tradeoffs, needs a cross-provider/second-opinion review, or is a high-stakes architecture/analysis decision where a single model's view is insufficient. It runs a real multi-role debate across several models and returns a synthesized conclusion AS THIS TOOL'S RESULT. Do NOT use it for routine or low-ambiguity work — it is expensive. After calling, read the returned conclusion and continue; nothing is auto-decided for you.",
184
+ inputSchema: jsonSchema({
185
+ type: "object",
186
+ properties: {
187
+ reason: {
188
+ type: "string",
189
+ description: "Why this task needs a multi-model debate (the specific tradeoff or decision at stake).",
190
+ },
191
+ },
192
+ }),
193
+ execute: async (input, execOpts) => {
194
+ const reason = input && typeof input === "object" && typeof input.reason === "string"
195
+ ? input.reason
196
+ : null;
197
+ requestCouncilConvene(reason, execOpts?.toolCallId ?? null);
198
+ return formatResult({
199
+ success: true,
200
+ output: "Council convening — the multi-model debate runs now and its conclusion replaces this result before your next step. Read the conclusion, then continue.",
201
+ });
202
+ },
203
+ });
204
+ }
205
+ // ask_user — agent-initiated question to the human. The model calls this from
206
+ // its OWN judgment (e.g. after a convene_council conclusion, when it wants a
207
+ // go/no-go before implementing). The CLI does NOT synthesise options or decide
208
+ // the branch: question, options, and default all come from the model's input.
209
+ // execute() BLOCKS until the human answers; the answer becomes the tool result.
210
+ // Registered only when a UI answerer is wired (opts.askUser present).
211
+ if (opts?.askUser) {
212
+ const askUser = opts.askUser;
213
+ tools.ask_user = dynamicTool({
214
+ description: "Ask the human user a question and receive their answer AS THIS TOOL'S RESULT. Use when you need a decision, confirmation (e.g. 'proceed with implementation?'), or missing detail that only the user can provide — typically in an interactive discussion, or after a convene_council conclusion when you want a go/no-go before building. Supply the exact `question` and, optionally, `options` the user picks from (omit for a free-text answer). This BLOCKS until the user responds. Nothing is auto-decided: read their answer and continue from your own judgment. Do NOT use it to narrate or for rhetorical questions — only when you genuinely need input to proceed.",
215
+ inputSchema: jsonSchema({
216
+ type: "object",
217
+ properties: {
218
+ question: {
219
+ type: "string",
220
+ description: "The exact question to put to the user.",
221
+ },
222
+ context: {
223
+ type: "string",
224
+ description: "Optional short context shown under the question.",
225
+ },
226
+ options: {
227
+ type: "array",
228
+ description: "Optional choices the user picks from. Omit for a free-text answer. These are YOUR options — the CLI adds none.",
229
+ items: {
230
+ type: "object",
231
+ properties: {
232
+ label: { type: "string", description: "The choice shown to the user." },
233
+ description: { type: "string", description: "Optional one-line explanation of the choice." },
234
+ value: { type: "string", description: "Optional value returned when picked (defaults to the label)." },
235
+ },
236
+ required: ["label"],
237
+ },
238
+ },
239
+ defaultIndex: {
240
+ type: "number",
241
+ description: "Optional index into options for the pre-selected choice (defaults to 0 = first).",
242
+ },
243
+ },
244
+ required: ["question"],
245
+ }),
246
+ execute: async (input) => {
247
+ const obj = (input && typeof input === "object" ? input : {});
248
+ const question = typeof obj.question === "string" ? obj.question : "";
249
+ if (question.trim().length === 0) {
250
+ return formatResult({
251
+ success: false,
252
+ output: "ask_user requires a non-empty `question`.",
253
+ });
254
+ }
255
+ const rawOptions = Array.isArray(obj.options) ? obj.options : [];
256
+ const options = rawOptions
257
+ .filter((o) => !!o && typeof o === "object")
258
+ .map((o) => ({
259
+ label: typeof o.label === "string" ? o.label : String(o.label ?? ""),
260
+ description: typeof o.description === "string" ? o.description : undefined,
261
+ value: typeof o.value === "string" ? o.value : undefined,
262
+ }))
263
+ .filter((o) => o.label.length > 0);
264
+ const info = {
265
+ question,
266
+ context: typeof obj.context === "string" ? obj.context : undefined,
267
+ options: options.length > 0 ? options : undefined,
268
+ defaultIndex: typeof obj.defaultIndex === "number" ? obj.defaultIndex : undefined,
269
+ };
270
+ const answer = await askUser(info);
271
+ return formatResult({ success: true, output: answer });
272
+ },
273
+ });
274
+ }
171
275
  // bash — every foreground call goes through here. We track the LAST
172
276
  // canonical command + runId in SESSION-SCOPED state so we can inject a
173
277
  // reminder when the model issues another bash call that canonicalizes to
@@ -265,6 +369,17 @@ export function createBuiltinTools(bash, mode, opts) {
265
369
  // motivation (session 18285908637a). gitSafetyKey is STABLE per process
266
370
  // (or the real sessionId) — unlike repeatKey, whose anon fallback changes
267
371
  // on every registry rebuild and would silently drop the gate across turns.
372
+ // Destructive-op guard (pre-execution, any permission mode): block a
373
+ // command that would irreversibly discard uncommitted work (git
374
+ // checkout --/restore/reset --hard/clean, git stash drop/clear, or rm of
375
+ // a tracked file) when there is actually work at risk. Routed as
376
+ // `destructive-revert` so the tool-engine shows the safety askcard
377
+ // interactively and hard-blocks headless (autonomous sub-agents) — the
378
+ // fix for the /ideal plan-adherence agent silently reverting user files.
379
+ const destructive = checkDestructiveOp(cmd, bash.getCwd());
380
+ if (destructive.blocked) {
381
+ return _prefixBlock("destructive-revert", destructive.message);
382
+ }
268
383
  const gitShape = analyzeGitCommand(cmd);
269
384
  // Hard-block broad staging when sensitive files are present.
270
385
  // This runs PRE-EXECUTION (before bash.execute) regardless of permission mode.
@@ -194,6 +194,13 @@ export interface ToolGroup {
194
194
  items: ToolGroupItem[];
195
195
  startedAt: number;
196
196
  finishedAt?: number;
197
+ /**
198
+ * True when this group was rebuilt from a persisted transcript rather than
199
+ * observed live (see ui/utils/group-tool-entries.ts). Its timestamps come
200
+ * from entry write-time, so the elapsed span is NOT a real measurement and
201
+ * must not be displayed as one.
202
+ */
203
+ restored?: boolean;
197
204
  inputTokens?: number;
198
205
  outputTokens?: number;
199
206
  }
@@ -225,7 +232,7 @@ export interface StructuredResponse {
225
232
  taskType: string;
226
233
  data: Record<string, unknown>;
227
234
  }
228
- export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap" | "safety-override";
235
+ export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap" | "safety-override" | "ask-user";
229
236
  export type CouncilOptionKind = "choice" | "freetext" | "chat";
230
237
  export interface CouncilQuestionOption {
231
238
  label: string;
@@ -262,6 +269,17 @@ export interface CouncilStatusData {
262
269
  startedAt?: number;
263
270
  tokensIn?: number;
264
271
  tokensOut?: number;
272
+ /**
273
+ * Push-based stream liveness (additive, optional). `elapsedMs` alone cannot
274
+ * prove a council call is alive: the tick generator only advances when the
275
+ * CONSUMER pulls, so a round awaiting its pairs via Promise.all freezes it.
276
+ * These two come from the token stream itself — `streamedChars` counts text +
277
+ * reasoning deltas since the phase began, and `lastDeltaAgeMs` is the age of
278
+ * the most recent delta. Growing chars / small age = SLOW BUT ALIVE; static
279
+ * chars + growing age = genuinely STUCK.
280
+ */
281
+ streamedChars?: number;
282
+ lastDeltaAgeMs?: number;
265
283
  errorMessage?: string;
266
284
  }
267
285
  /** High-level lifecycle event for a council phase. Drives the phase timeline UI. */
@@ -433,6 +451,16 @@ export interface CouncilMessage {
433
451
  attempts?: number;
434
452
  failureReason?: string;
435
453
  runId?: string;
454
+ /**
455
+ * Wall-clock of THIS speaker's LLM call, including retries.
456
+ *
457
+ * Without it the only timed council rows are `debate_complete` (the whole
458
+ * debate, measured at 87s..1128s) and `council_summary` — aggregates that
459
+ * cannot say WHICH model burned the time, so every latency diagnosis is
460
+ * guesswork. Persisted to `interaction_logs.duration_ms` alongside the
461
+ * `speakerModel` this row already carries, making the bottleneck queryable.
462
+ */
463
+ durationMs?: number;
436
464
  }
437
465
  export interface StreamChunk {
438
466
  type: "content" | "tool_calls" | "tool_result" | "tool_approval_request" | "council_question" | "council_preflight" | "council_status" | "council_phase" | "council_message" | "council_info_card" | "council_meta" | "council_round" | "done" | "error" | "reasoning" | "structured_response" | "product_status_card" | "experience_warning" | "experience_injected" | "push_notification" | "halt" | "toast" | "task_list_update";