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
@@ -0,0 +1,102 @@
1
+ /**
2
+ * src/utils/loop-profiler.ts
3
+ *
4
+ * Rolling V8 CPU profiler — the instrument that names the culprit when the
5
+ * event loop blocks (see `event-loop-monitor.ts` for the incident this exists
6
+ * for).
7
+ *
8
+ * Why a CPU profile and not a stack dump: while the JS thread is blocked, NO
9
+ * JS can run — you cannot ask the process what it is doing, because asking is
10
+ * itself JS. V8's sampling profiler runs on its OWN thread and samples the JS
11
+ * thread regardless of whether that thread is stuck, so the profile taken
12
+ * across a block contains the blocking frames. Verified under Bun 1.3.13: a
13
+ * deliberate 400ms synchronous block yielded 54 samples.
14
+ *
15
+ * Rolling segments keep the retained profile small without losing the block:
16
+ * rotation is loop-driven, so it CANNOT run during a block — whichever segment
17
+ * is in flight when the loop freezes stays in flight for the whole freeze, and
18
+ * is still open when the monitor detects the block right after. So `capture()`
19
+ * always writes a profile containing the entire block, however long it ran.
20
+ *
21
+ * Off by default (`MUONROI_LOOP_PROFILE=1` to arm) — continuous profiling has a
22
+ * real, if small, cost and this is a diagnostic, not a feature.
23
+ */
24
+ /** Minimal shape of the V8 CPU profile we consume (`Profiler.stop` result). */
25
+ interface CpuProfileNode {
26
+ id: number;
27
+ callFrame: {
28
+ functionName?: string;
29
+ url?: string;
30
+ lineNumber?: number;
31
+ };
32
+ children?: number[];
33
+ }
34
+ interface CpuProfile {
35
+ nodes: CpuProfileNode[];
36
+ samples?: number[];
37
+ }
38
+ /**
39
+ * Reduce a CPU profile to the single hottest stack, leaf-first.
40
+ *
41
+ * Why not just the top frame: samples land on the LEAF, which for a blocking
42
+ * loop is whatever primitive it calls (`Date.now`, a regex step, a syscall) —
43
+ * true but useless. Verified on a synthetic 3s block: the leaf was `now` with
44
+ * 188 samples while the actual culprit `theGuiltyFunction` had 4. The CALLERS
45
+ * are the answer, so we walk up from the hottest leaf and return the chain.
46
+ *
47
+ * This is what makes the profile readable from debug.log alone, without
48
+ * requiring anyone to open Chrome DevTools.
49
+ *
50
+ * Pure — unit-testable with a hand-built profile.
51
+ *
52
+ * @param maxFrames how many frames of the chain to return, leaf-first
53
+ * @returns frames like `theGuiltyFunction (file.ts:12)`, or [] if unusable
54
+ */
55
+ export declare function summarizeHotStack(profile: CpuProfile, maxFrames?: number): string[];
56
+ /** What a successful {@link LoopProfiler.capture} produced. */
57
+ export interface CaptureResult {
58
+ /** Path of the written `.cpuprofile` (open in Chrome DevTools for detail). */
59
+ file: string;
60
+ /** Hottest stack, leaf-first — readable without DevTools. See {@link summarizeHotStack}. */
61
+ hotStack: string[];
62
+ }
63
+ /** A rolling CPU profiler. All methods are best-effort and never throw. */
64
+ export interface LoopProfiler {
65
+ /**
66
+ * Discard the current segment and start a fresh one. Call periodically from
67
+ * the event loop to bound profile size — it is a no-op during a block, which
68
+ * is exactly what preserves the evidence.
69
+ */
70
+ rotate(): void;
71
+ /**
72
+ * Stop the current segment, write it to disk, and start a fresh one.
73
+ *
74
+ * Async because the inspector delivers `Profiler.stop` on a later tick
75
+ * (measured under Bun 1.3.13 — the callback does NOT run synchronously). The
76
+ * loop is already free by the time this is called, so awaiting is safe.
77
+ *
78
+ * @returns the profile path plus the hottest stack, or null on failure.
79
+ */
80
+ capture(reason: string): Promise<CaptureResult | null>;
81
+ /** Stop profiling and disconnect. Idempotent. */
82
+ dispose(): void;
83
+ }
84
+ /** Options for {@link createLoopProfiler}. */
85
+ export interface LoopProfilerOpts {
86
+ /**
87
+ * V8 sampling interval in microseconds. 10_000 (10ms) is ~100 samples/sec —
88
+ * plenty to identify a multi-second blocking frame, at a fraction of the cost
89
+ * of the 1ms default.
90
+ */
91
+ samplingIntervalUs?: number;
92
+ /** Directory for `.cpuprofile` files. Defaults to `~/.muonroi-cli/profiles`. */
93
+ dir?: string;
94
+ }
95
+ /**
96
+ * Create and arm a rolling CPU profiler.
97
+ *
98
+ * Returns null when the inspector is unavailable (some runtimes/builds) — the
99
+ * caller keeps the block detector, just without stacks. Never throws.
100
+ */
101
+ export declare function createLoopProfiler(opts?: LoopProfilerOpts): Promise<LoopProfiler | null>;
102
+ export {};
@@ -0,0 +1,202 @@
1
+ /**
2
+ * src/utils/loop-profiler.ts
3
+ *
4
+ * Rolling V8 CPU profiler — the instrument that names the culprit when the
5
+ * event loop blocks (see `event-loop-monitor.ts` for the incident this exists
6
+ * for).
7
+ *
8
+ * Why a CPU profile and not a stack dump: while the JS thread is blocked, NO
9
+ * JS can run — you cannot ask the process what it is doing, because asking is
10
+ * itself JS. V8's sampling profiler runs on its OWN thread and samples the JS
11
+ * thread regardless of whether that thread is stuck, so the profile taken
12
+ * across a block contains the blocking frames. Verified under Bun 1.3.13: a
13
+ * deliberate 400ms synchronous block yielded 54 samples.
14
+ *
15
+ * Rolling segments keep the retained profile small without losing the block:
16
+ * rotation is loop-driven, so it CANNOT run during a block — whichever segment
17
+ * is in flight when the loop freezes stays in flight for the whole freeze, and
18
+ * is still open when the monitor detects the block right after. So `capture()`
19
+ * always writes a profile containing the entire block, however long it ran.
20
+ *
21
+ * Off by default (`MUONROI_LOOP_PROFILE=1` to arm) — continuous profiling has a
22
+ * real, if small, cost and this is a diagnostic, not a feature.
23
+ */
24
+ import fs from "node:fs";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+ import { logger } from "./logger.js";
28
+ /** Frames V8 synthesises; naming one as the culprit tells nobody anything. */
29
+ const SYNTHETIC_FRAMES = new Set(["(root)", "(program)", "(idle)", "(garbage collector)", "(anonymous)", ""]);
30
+ /**
31
+ * Reduce a CPU profile to the single hottest stack, leaf-first.
32
+ *
33
+ * Why not just the top frame: samples land on the LEAF, which for a blocking
34
+ * loop is whatever primitive it calls (`Date.now`, a regex step, a syscall) —
35
+ * true but useless. Verified on a synthetic 3s block: the leaf was `now` with
36
+ * 188 samples while the actual culprit `theGuiltyFunction` had 4. The CALLERS
37
+ * are the answer, so we walk up from the hottest leaf and return the chain.
38
+ *
39
+ * This is what makes the profile readable from debug.log alone, without
40
+ * requiring anyone to open Chrome DevTools.
41
+ *
42
+ * Pure — unit-testable with a hand-built profile.
43
+ *
44
+ * @param maxFrames how many frames of the chain to return, leaf-first
45
+ * @returns frames like `theGuiltyFunction (file.ts:12)`, or [] if unusable
46
+ */
47
+ export function summarizeHotStack(profile, maxFrames = 8) {
48
+ const samples = profile?.samples;
49
+ const nodes = profile?.nodes;
50
+ if (!Array.isArray(samples) || !Array.isArray(nodes) || samples.length === 0)
51
+ return [];
52
+ const byId = new Map();
53
+ const parentOf = new Map();
54
+ for (const n of nodes) {
55
+ byId.set(n.id, n);
56
+ for (const c of n.children ?? [])
57
+ parentOf.set(c, n.id);
58
+ }
59
+ const selfCounts = new Map();
60
+ for (const s of samples)
61
+ selfCounts.set(s, (selfCounts.get(s) ?? 0) + 1);
62
+ let hottest = -1;
63
+ let best = -1;
64
+ for (const [id, count] of selfCounts) {
65
+ if (count > best) {
66
+ best = count;
67
+ hottest = id;
68
+ }
69
+ }
70
+ if (hottest < 0)
71
+ return [];
72
+ const frames = [];
73
+ const seen = new Set();
74
+ let cur = hottest;
75
+ while (cur !== undefined && !seen.has(cur) && frames.length < maxFrames) {
76
+ seen.add(cur);
77
+ const node = byId.get(cur);
78
+ if (!node)
79
+ break;
80
+ const name = node.callFrame?.functionName ?? "";
81
+ if (!SYNTHETIC_FRAMES.has(name)) {
82
+ const url = node.callFrame?.url ?? "";
83
+ const short = url ? `${url.split(/[\\/]/).pop()}:${(node.callFrame?.lineNumber ?? 0) + 1}` : "native";
84
+ frames.push(`${name} (${short})`);
85
+ }
86
+ cur = parentOf.get(cur);
87
+ }
88
+ return frames;
89
+ }
90
+ /**
91
+ * Create and arm a rolling CPU profiler.
92
+ *
93
+ * Returns null when the inspector is unavailable (some runtimes/builds) — the
94
+ * caller keeps the block detector, just without stacks. Never throws.
95
+ */
96
+ export async function createLoopProfiler(opts = {}) {
97
+ const samplingIntervalUs = opts.samplingIntervalUs ?? 10_000;
98
+ const dir = opts.dir ?? path.join(os.homedir(), ".muonroi-cli", "profiles");
99
+ let session;
100
+ try {
101
+ const inspector = await import("node:inspector");
102
+ session = new inspector.Session();
103
+ session.connect();
104
+ }
105
+ catch (err) {
106
+ logger.warn("cli", `[loop-profiler] inspector unavailable — block reports will have no stacks: ${err?.message}`, { error: err });
107
+ return null;
108
+ }
109
+ /** Promise-free post: the profiler is driven from timer callbacks. */
110
+ const post = (method, params) => {
111
+ try {
112
+ session.post(method, params, (err) => {
113
+ if (err) {
114
+ logger.warn("cli", `[loop-profiler] ${method} failed: ${err.message}`, { error: err });
115
+ }
116
+ });
117
+ }
118
+ catch (err) {
119
+ logger.warn("cli", `[loop-profiler] ${method} threw: ${err?.message}`, { error: err });
120
+ }
121
+ };
122
+ let disposed = false;
123
+ const start = () => {
124
+ post("Profiler.setSamplingInterval", { interval: samplingIntervalUs });
125
+ post("Profiler.start");
126
+ };
127
+ try {
128
+ post("Profiler.enable");
129
+ start();
130
+ }
131
+ catch (err) {
132
+ logger.warn("cli", `[loop-profiler] failed to arm: ${err?.message}`, { error: err });
133
+ return null;
134
+ }
135
+ /**
136
+ * Stop the current segment and resolve with its profile (null on failure).
137
+ * Restarts a fresh segment unless disposing.
138
+ */
139
+ const stopSegment = (restart) => new Promise((resolve) => {
140
+ try {
141
+ session.post("Profiler.stop", (err, res) => {
142
+ if (err) {
143
+ logger.warn("cli", `[loop-profiler] Profiler.stop failed: ${err.message}`, { error: err });
144
+ resolve(null);
145
+ }
146
+ else {
147
+ resolve(res?.profile ?? null);
148
+ }
149
+ if (restart && !disposed)
150
+ start();
151
+ });
152
+ }
153
+ catch (err) {
154
+ logger.warn("cli", `[loop-profiler] Profiler.stop threw: ${err?.message}`, { error: err });
155
+ resolve(null);
156
+ }
157
+ });
158
+ return {
159
+ rotate() {
160
+ if (disposed)
161
+ return;
162
+ // Drop the segment on the floor — it contained no block worth keeping.
163
+ void stopSegment(true);
164
+ },
165
+ async capture(reason) {
166
+ if (disposed)
167
+ return null;
168
+ const profile = await stopSegment(true);
169
+ if (!profile)
170
+ return null;
171
+ try {
172
+ fs.mkdirSync(dir, { recursive: true });
173
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
174
+ const safeReason = reason.replace(/[^a-z0-9_-]/gi, "_").slice(0, 40);
175
+ const file = path.join(dir, `loop-block-${stamp}-${safeReason}.cpuprofile`);
176
+ fs.writeFileSync(file, JSON.stringify(profile), "utf8");
177
+ return { file, hotStack: summarizeHotStack(profile) };
178
+ }
179
+ catch (err) {
180
+ logger.error("cli", `[loop-profiler] failed to write profile: ${err?.message}`, {
181
+ error: err,
182
+ dir,
183
+ });
184
+ return null;
185
+ }
186
+ },
187
+ dispose() {
188
+ if (disposed)
189
+ return;
190
+ disposed = true;
191
+ void stopSegment(false);
192
+ try {
193
+ post("Profiler.disable");
194
+ session.disconnect();
195
+ }
196
+ catch (err) {
197
+ logger.warn("cli", `[loop-profiler] disconnect failed: ${err?.message}`, { error: err });
198
+ }
199
+ },
200
+ };
201
+ }
202
+ //# sourceMappingURL=loop-profiler.js.map
@@ -78,6 +78,12 @@ export interface ProviderKeyConfig {
78
78
  }
79
79
  export interface UserSettings {
80
80
  apiKey?: string;
81
+ /**
82
+ * One-time guard: true once legacy keychain/settings API keys have been
83
+ * migrated into the env-store (`~/.muonroi-cli/.env`). See
84
+ * `migrateLegacyKeysToEnv` in src/providers/keychain.ts.
85
+ */
86
+ keysMigratedToEnv?: boolean;
81
87
  defaultModel?: string;
82
88
  /**
83
89
  * Preferred provider. When set, the splash/config UI hides the model
@@ -384,6 +390,27 @@ export declare function getProviderStallTimeoutMs(): number;
384
390
  * override: MUONROI_PROVIDER_PROGRESS_TIMEOUT_MS.
385
391
  */
386
392
  export declare function getProviderProgressTimeoutMs(): number;
393
+ /**
394
+ * Event-loop block reporting threshold (ms). The monitor
395
+ * (`src/utils/event-loop-monitor.ts`) reports a block when its own tick runs at
396
+ * least this late — evidence that the JS thread was stuck and NO timer could
397
+ * fire, which is why the provider stall watchdogs above cannot see this class
398
+ * of freeze (measured live 2026-07-16: a 304.5s TUI freeze that the 120s stall
399
+ * watchdog only rescued 6.7s AFTER the loop recovered).
400
+ *
401
+ * 2s is well clear of ordinary GC pauses and heavy renders while still catching
402
+ * anything a user would call a freeze. Range 250–60_000; 0 disables.
403
+ * Default 2_000. Env override: MUONROI_LOOP_BLOCK_THRESHOLD_MS.
404
+ */
405
+ export declare function getLoopBlockThresholdMs(): number;
406
+ /**
407
+ * Whether to run the rolling CPU profiler (`src/utils/loop-profiler.ts`) so a
408
+ * reported event-loop block comes with the culprit's stack rather than just a
409
+ * duration. OFF by default: continuous V8 sampling costs a little on every
410
+ * session, and this is a diagnostic for a sporadic bug, not a feature. Arm it
411
+ * when hunting a freeze. Env: MUONROI_LOOP_PROFILE=1 (0/unset disables).
412
+ */
413
+ export declare function isLoopProfileEnabled(): boolean;
387
414
  /**
388
415
  * Number of times to AUTOMATICALLY re-issue a streaming model call after the
389
416
  * stall watchdog fires WITHOUT any chunk having arrived (a time-to-first-byte
@@ -275,10 +275,12 @@ export function saveProjectSettings(partial) {
275
275
  }
276
276
  export function getApiKey() {
277
277
  // Test escape hatch (api-key harness spec): suppress all key sources so the
278
- // boot flow reaches the API-key modal. See src/index.ts resolveKeyForModel.
278
+ // boot flow reaches the provider picker. See src/index.ts resolveKeyForModel.
279
279
  if (process.env.MUONROI_TEST_NO_KEYCHAIN === "1")
280
280
  return undefined;
281
- return process.env.MUONROI_API_KEY || loadUserSettings().apiKey;
281
+ // Env-only: the legacy plaintext `settings.apiKey` is migrated to the
282
+ // env-store on first run (migrateLegacyKeysToEnv) and no longer read here.
283
+ return process.env.MUONROI_API_KEY || undefined;
282
284
  }
283
285
  export function getBaseURL(provider) {
284
286
  if (process.env.MUONROI_BASE_URL)
@@ -662,6 +664,40 @@ export function getProviderProgressTimeoutMs() {
662
664
  }
663
665
  return 300_000;
664
666
  }
667
+ /**
668
+ * Event-loop block reporting threshold (ms). The monitor
669
+ * (`src/utils/event-loop-monitor.ts`) reports a block when its own tick runs at
670
+ * least this late — evidence that the JS thread was stuck and NO timer could
671
+ * fire, which is why the provider stall watchdogs above cannot see this class
672
+ * of freeze (measured live 2026-07-16: a 304.5s TUI freeze that the 120s stall
673
+ * watchdog only rescued 6.7s AFTER the loop recovered).
674
+ *
675
+ * 2s is well clear of ordinary GC pauses and heavy renders while still catching
676
+ * anything a user would call a freeze. Range 250–60_000; 0 disables.
677
+ * Default 2_000. Env override: MUONROI_LOOP_BLOCK_THRESHOLD_MS.
678
+ */
679
+ export function getLoopBlockThresholdMs() {
680
+ const envRaw = process.env.MUONROI_LOOP_BLOCK_THRESHOLD_MS;
681
+ if (envRaw !== undefined && envRaw !== "") {
682
+ const n = Number(envRaw);
683
+ if (Number.isFinite(n) && n === 0)
684
+ return 0; // explicit disable
685
+ if (Number.isFinite(n) && n >= 250 && n <= 60_000)
686
+ return Math.floor(n);
687
+ }
688
+ return 2_000;
689
+ }
690
+ /**
691
+ * Whether to run the rolling CPU profiler (`src/utils/loop-profiler.ts`) so a
692
+ * reported event-loop block comes with the culprit's stack rather than just a
693
+ * duration. OFF by default: continuous V8 sampling costs a little on every
694
+ * session, and this is a diagnostic for a sporadic bug, not a feature. Arm it
695
+ * when hunting a freeze. Env: MUONROI_LOOP_PROFILE=1 (0/unset disables).
696
+ */
697
+ export function isLoopProfileEnabled() {
698
+ const raw = process.env.MUONROI_LOOP_PROFILE?.trim().toLowerCase();
699
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
700
+ }
665
701
  /**
666
702
  * Number of times to AUTOMATICALLY re-issue a streaming model call after the
667
703
  * stall watchdog fires WITHOUT any chunk having arrived (a time-to-first-byte
@@ -1,4 +1,3 @@
1
- import type { ProviderFactory as LegacyProvider } from "../providers/runtime.js";
2
1
  export interface SideQuestionResult {
3
2
  response: string;
4
3
  usage?: {
@@ -7,4 +6,4 @@ export interface SideQuestionResult {
7
6
  outputTokens?: number;
8
7
  };
9
8
  }
10
- export declare function runSideQuestion(question: string, provider: LegacyProvider, modelId: string, conversationContext: string, signal?: AbortSignal): Promise<SideQuestionResult>;
9
+ export declare function runSideQuestion(question: string, modelId: string, conversationContext: string, signal?: AbortSignal): Promise<SideQuestionResult>;
@@ -3,8 +3,8 @@ import { resolveModelRuntime } from "../providers/runtime.js";
3
3
  const SIDE_QUESTION_SYSTEM = `You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.
4
4
 
5
5
  If conversation context is provided below, use it to give a more relevant answer.`;
6
- export async function runSideQuestion(question, provider, modelId, conversationContext, signal) {
7
- const runtime = resolveModelRuntime(provider, modelId);
6
+ export async function runSideQuestion(question, modelId, conversationContext, signal) {
7
+ const runtime = resolveModelRuntime(modelId);
8
8
  const system = conversationContext
9
9
  ? `${SIDE_QUESTION_SYSTEM}\n\n<conversation_context>\n${conversationContext}\n</conversation_context>`
10
10
  : SIDE_QUESTION_SYSTEM;
@@ -1,3 +1,4 @@
1
+ import { VERIFY_FAIL_MARKER, VERIFY_PASS_MARKER } from "../product-loop/verify-result.js";
1
2
  import { ensureVerifyCheckpoint } from "./checkpoint.js";
2
3
  import { loadVerifyEnvironment } from "./environment.js";
3
4
  import { buildBrowserGuidance, buildEvidenceGuidance, buildReadinessGuidance } from "./evidence.js";
@@ -69,6 +70,51 @@ export function buildVerifyTaskPrompt(cwd, settings, recipeOverride, sandboxMode
69
70
  "Phase 1 — Setup:",
70
71
  "- Probe the host for runtimes. If missing, attempt to install them or report as blockers.",
71
72
  ];
73
+ // Does this project actually expose a long-running app to start + smoke-test in
74
+ // a browser? Only then are Phase 3 (start app) / Phase 4 (browser QA) applicable.
75
+ // A CLI/library/script project (pytest-only Python, a Go/Rust binary, a plain
76
+ // script) has smokeKind "cli"/"none" and no startCommand — forcing "Start the
77
+ // app (REQUIRED)" + "browser QA (REQUIRED)" there is contradictory: the sub-agent
78
+ // cannot satisfy them, so it emits NEITHER verdict marker → parseVerifyResult →
79
+ // UNKNOWN → engineering-floor fails → the sprint retries forever on a passing
80
+ // build. Gate the runtime/browser phases and the verdict rule on this flag.
81
+ const hasRuntimeSmoke = profile.recipe.smokeKind === "http" && Boolean(profile.recipe.startCommand);
82
+ const runtimePhases = hasRuntimeSmoke
83
+ ? [
84
+ "Phase 3 — Start the app (REQUIRED, do not skip):",
85
+ "- Start the app using startCommand from the recipe, running it in the background.",
86
+ "- Wait for the app to be ready: use a curl readiness loop or `agent-browser wait --load networkidle`.",
87
+ "- If the app fails to start, report the error but still attempt to capture evidence (logs, screenshots).",
88
+ "",
89
+ "Phase 4 — Browser QA testing (REQUIRED, do not skip):",
90
+ "- You are a QA tester. Open the app in the browser and test it like a human would.",
91
+ `- agent-browser commands run on the HOST${sandboxMode === "shuru" ? ", not the sandbox" : ""}. They WILL work. Do not skip them.`,
92
+ "- Record a video of the entire browser session.",
93
+ "- Navigate the app: click links, buttons, menus. Verify pages load correctly.",
94
+ "- Check for JavaScript console errors.",
95
+ "- Spend 3-5 interactions testing the critical path. Take screenshots after each.",
96
+ "- This is the most important phase. Build/lint passing means nothing if the app doesn't actually work.",
97
+ "",
98
+ "Phase 5 — Teardown:",
99
+ "- Stop recording, close browser, THEN stop the dev server.",
100
+ ]
101
+ : [
102
+ "Phase 3 — Runtime smoke (CLI / library / script — there is NO long-running app to start):",
103
+ '- This project exposes no HTTP server or startable app (smokeKind is not "http"), so there is NOTHING to start in the background and NO browser QA to run.',
104
+ "- Do the equivalent CLI smoke instead: run the built binary / script (or the recipe's smoke command) once and confirm it produces the expected output and a zero exit code.",
105
+ "- Capture the exact command and its output as evidence.",
106
+ "- Browser and app-start phases are NOT APPLICABLE here — skipping them is CORRECT and is NOT a failure. Do NOT treat their absence as a blocker.",
107
+ ];
108
+ const verdictRule = hasRuntimeSmoke
109
+ ? [
110
+ `- After the report, emit the verdict on its own final line: exactly \`${VERIFY_PASS_MARKER}\` if install/build/test and the smoke/QA phases all succeeded, otherwise exactly \`${VERIFY_FAIL_MARKER}\`.`,
111
+ `- Emit \`${VERIFY_FAIL_MARKER}\` on ANY failed or skipped required phase (build error, failing test, app did not start, blocking console error). Do NOT emit \`${VERIFY_PASS_MARKER}\` if you could not actually run the recipe.`,
112
+ ]
113
+ : [
114
+ `- After the report, emit the verdict on its own final line: exactly \`${VERIFY_PASS_MARKER}\` if install/build/test (and the CLI smoke, if any) all succeeded, otherwise exactly \`${VERIFY_FAIL_MARKER}\`.`,
115
+ `- This is a CLI/library/script project with no app to start: the browser and app-start phases are N/A, NOT failures. Do NOT withhold \`${VERIFY_PASS_MARKER}\` merely because they were not run.`,
116
+ `- Emit \`${VERIFY_FAIL_MARKER}\` ONLY on a real failure: an install/build error, a failing test, or a CLI smoke that produced the wrong result. Do NOT emit \`${VERIFY_PASS_MARKER}\` if you could not actually run the recipe's tests.`,
117
+ ];
72
118
  return [
73
119
  "Run a local verification pass for the current workspace.",
74
120
  "",
@@ -90,22 +136,7 @@ export function buildVerifyTaskPrompt(cwd, settings, recipeOverride, sandboxMode
90
136
  "Phase 2 — Build and test:",
91
137
  "- Run installCommands, buildCommands, and testCommands from the recipe.",
92
138
  "",
93
- "Phase 3 — Start the app (REQUIRED, do not skip):",
94
- "- Start the app using startCommand from the recipe, running it in the background.",
95
- "- Wait for the app to be ready: use a curl readiness loop or `agent-browser wait --load networkidle`.",
96
- "- If the app fails to start, report the error but still attempt to capture evidence (logs, screenshots).",
97
- "",
98
- "Phase 4 — Browser QA testing (REQUIRED, do not skip):",
99
- "- You are a QA tester. Open the app in the browser and test it like a human would.",
100
- `- agent-browser commands run on the HOST${sandboxMode === "shuru" ? ", not the sandbox" : ""}. They WILL work. Do not skip them.`,
101
- "- Record a video of the entire browser session.",
102
- "- Navigate the app: click links, buttons, menus. Verify pages load correctly.",
103
- "- Check for JavaScript console errors.",
104
- "- Spend 3-5 interactions testing the critical path. Take screenshots after each.",
105
- "- This is the most important phase. Build/lint passing means nothing if the app doesn't actually work.",
106
- "",
107
- "Phase 5 — Teardown:",
108
- "- Stop recording, close browser, THEN stop the dev server.",
139
+ ...runtimePhases,
109
140
  ...buildReadinessGuidance(profile),
110
141
  ...buildBrowserGuidance(profile),
111
142
  ...buildRetryGuidance(profile),
@@ -122,6 +153,10 @@ export function buildVerifyTaskPrompt(cwd, settings, recipeOverride, sandboxMode
122
153
  "- The Summary must say what recipe/source of truth you used and whether you changed the inferred/default one.",
123
154
  "- Evidence is mandatory even on failure. If you captured screenshots, video, or logs, include their exact workspace-relative file paths in the Evidence section.",
124
155
  "- Use markdown links for artifact paths when practical, otherwise include the plain relative paths.",
156
+ "",
157
+ "Verdict marker (MANDATORY — the loop parses this, an absent marker scores the sprint 0.00):",
158
+ ...verdictRule,
159
+ "- The marker is the last line, nothing after it.",
125
160
  ].join("\n");
126
161
  }
127
162
  export function createVerifyTaskRequest(cwd, settings, recipeOverride, sandboxMode = "shuru") {
@@ -1,5 +1,5 @@
1
1
  import type { TaskRequest, ToolResult, VerifyRecipe } from "../types/index.js";
2
- import type { SandboxSettings } from "../utils/settings.js";
2
+ import { type SandboxSettings } from "../utils/settings.js";
3
3
  import { type PreparedVerifyCheckpoint } from "./checkpoint.js";
4
4
  import { type VerifyProjectProfile } from "./recipes.js";
5
5
  export interface VerifyAgentLike {
@@ -1,3 +1,4 @@
1
+ import { getCurrentSandboxMode } from "../utils/settings.js";
1
2
  import { ensureVerifyCheckpoint } from "./checkpoint.js";
2
3
  import { buildVerifyTaskPrompt } from "./entrypoint.js";
3
4
  import { loadVerifyEnvironment, saveVerifyEnvironment } from "./environment.js";
@@ -39,8 +40,21 @@ export async function prepareVerifyRun(agent, options = {}) {
39
40
  }
40
41
  }
41
42
  const sandboxSettings = buildRuntimeSandboxSettings(profile);
42
- options.onProgress?.("Preparing verify checkpoint");
43
- const checkpoint = await ensureVerifyCheckpoint(cwd, profile, sandboxSettings);
43
+ // Sandbox "off" run the recipe directly on the host; do NOT bootstrap a
44
+ // `shuru` checkpoint. ensureVerifyCheckpoint spawns `shuru checkpoint …`
45
+ // whenever the recipe has installCommands, regardless of mode — on a host
46
+ // without shuru installed that throws "Executable not found in $PATH: shuru",
47
+ // which parseVerifyResult maps to ERROR (never PASS), pinning the sprint score
48
+ // at 0.00. Skip it when the sole source of truth (getCurrentSandboxMode) is off.
49
+ const checkpoint = getCurrentSandboxMode() === "off"
50
+ ? { created: false }
51
+ : await (async () => {
52
+ options.onProgress?.("Preparing verify checkpoint");
53
+ return ensureVerifyCheckpoint(cwd, profile, sandboxSettings);
54
+ })();
55
+ if (getCurrentSandboxMode() === "off") {
56
+ options.onProgress?.("Sandbox off — running verify on host (no shuru checkpoint)");
57
+ }
44
58
  if (checkpoint.checkpointName) {
45
59
  sandboxSettings.from = checkpoint.checkpointName;
46
60
  if (checkpoint.guestWorkdir) {
@@ -57,7 +71,10 @@ export async function prepareVerifyRun(agent, options = {}) {
57
71
  const taskRequest = {
58
72
  agent: "verify",
59
73
  description: "Run local verification",
60
- prompt: buildVerifyTaskPrompt(cwd, sandboxSettings, profile.recipe),
74
+ // Thread the resolved mode so the prompt tells the sub-agent to run "on the
75
+ // host" instead of "inside the active Shuru sandbox" when sandbox is off
76
+ // (the param defaults to "shuru", which was wrong for an off host).
77
+ prompt: buildVerifyTaskPrompt(cwd, sandboxSettings, profile.recipe, getCurrentSandboxMode()),
61
78
  };
62
79
  return {
63
80
  profile,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.8.5",
6
+ "version": "1.9.0",
7
7
  "description": "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.",
8
8
  "repository": {
9
9
  "type": "git",
@@ -98,7 +98,6 @@
98
98
  "diff": "^8.0.3",
99
99
  "fast-xml-parser": "^4.4.1",
100
100
  "gpt-tokenizer": "^3.4.0",
101
- "keytar": "7.9.0",
102
101
  "ollama-ai-provider-v2": "1.5.5",
103
102
  "proper-lockfile": "^4.1.2",
104
103
  "react": "19.2.5",
@@ -1,55 +0,0 @@
1
- import { type SpawnSyncReturns } from "node:child_process";
2
- /**
3
- * Thin wrapper around the Bitwarden CLI for writing secrets into the user's
4
- * vault. Mirrors the assumptions of `runKeysImportBw`: the secret lives in the
5
- * `notes` field of a Secure Note item named `<prefix><id>`.
6
- *
7
- * Auth contract: caller must have `bw` in PATH and `BW_SESSION` exported.
8
- * On any error the returned object has `ok: false` so callers can degrade
9
- * gracefully (e.g. fall back to writing only the OS keychain).
10
- */
11
- export interface BwWriteResult {
12
- ok: boolean;
13
- action?: "created" | "updated";
14
- error?: string;
15
- }
16
- type Runner = (cmd: string, args: string[], input?: string) => SpawnSyncReturns<string>;
17
- /**
18
- * Write `notes` into a vault item named `name`. Creates a Secure Note if
19
- * absent; updates the notes field if present. Calls `bw sync` after a write
20
- * so subsequent `bw get` calls see the new value.
21
- */
22
- export declare function writeBwSecureNote(name: string, notes: string, options?: {
23
- runner?: Runner;
24
- }): Promise<BwWriteResult>;
25
- export interface BwUnlockResult {
26
- ok: boolean;
27
- session?: string;
28
- error?: string;
29
- }
30
- /**
31
- * Unlock the BW vault with a master password and return the session token
32
- * in-memory only. Caller passes the returned session to subsequent calls;
33
- * we never write to BW_SESSION env or persist it to disk.
34
- */
35
- export declare function unlockWithPassword(password: string, options?: {
36
- runner?: Runner;
37
- }): Promise<BwUnlockResult>;
38
- export interface BwListedNote {
39
- name: string;
40
- notes: string;
41
- }
42
- /**
43
- * List Secure Notes whose name starts with `prefix`. Used by the in-TUI
44
- * sync flow to discover which providers have keys stored in the vault.
45
- */
46
- export declare function listSecureNotesByPrefix(session: string, prefix: string, options?: {
47
- runner?: Runner;
48
- }): Promise<{
49
- ok: true;
50
- items: BwListedNote[];
51
- } | {
52
- ok: false;
53
- error: string;
54
- }>;
55
- export {};