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,39 @@
1
+ /**
2
+ * src/ui/utils/agent-activities.ts
3
+ *
4
+ * Collect every sub-agent / background job the main agent has spawned this
5
+ * session, for the rail's Agents block.
6
+ *
7
+ * Derived entirely from state the UI already holds — the transcript entries and
8
+ * the in-flight tool calls. Spawning goes through tools (`task`, `delegate`,
9
+ * `bash background:true`), so the tool stream IS the record; there is no need
10
+ * for a parallel registry in core, and nothing here can drift out of sync with
11
+ * what the transcript shows.
12
+ *
13
+ * Running entries come from activeToolCalls (no result yet), finished ones from
14
+ * tool_result / tool_group items. A call that is both (still streaming while an
15
+ * earlier identical call finished) is keyed by tool-call id, so it appears once.
16
+ */
17
+ import type { ChatEntry, ToolCall } from "../../types/index.js";
18
+ export type ActivityKind = "subagent" | "delegate" | "background";
19
+ export type ActivityStatus = "running" | "done" | "failed";
20
+ export interface AgentActivity {
21
+ /** Tool-call id — stable across the running → finished transition. */
22
+ id: string;
23
+ kind: ActivityKind;
24
+ /** Short line for the rail row, e.g. an explore agent's description. */
25
+ label: string;
26
+ /** Sub-agent type ("explore", "general", …). Empty for background shells. */
27
+ agent: string;
28
+ status: ActivityStatus;
29
+ /** Full text shown when the row is opened: the prompt/command, then output. */
30
+ detail: string;
31
+ }
32
+ /** The activity kind a tool call represents, or null if it spawns nothing. */
33
+ export declare function activityKindFor(tc: ToolCall): ActivityKind | null;
34
+ /**
35
+ * Every activity in the session, oldest first. Later state for the same
36
+ * tool-call id wins, so a call that finishes replaces its own running row
37
+ * instead of adding a second one.
38
+ */
39
+ export declare function collectAgentActivities(messages: ChatEntry[], activeToolCalls?: ToolCall[]): AgentActivity[];
@@ -0,0 +1,96 @@
1
+ /**
2
+ * src/ui/utils/agent-activities.ts
3
+ *
4
+ * Collect every sub-agent / background job the main agent has spawned this
5
+ * session, for the rail's Agents block.
6
+ *
7
+ * Derived entirely from state the UI already holds — the transcript entries and
8
+ * the in-flight tool calls. Spawning goes through tools (`task`, `delegate`,
9
+ * `bash background:true`), so the tool stream IS the record; there is no need
10
+ * for a parallel registry in core, and nothing here can drift out of sync with
11
+ * what the transcript shows.
12
+ *
13
+ * Running entries come from activeToolCalls (no result yet), finished ones from
14
+ * tool_result / tool_group items. A call that is both (still streaming while an
15
+ * earlier identical call finished) is keyed by tool-call id, so it appears once.
16
+ */
17
+ const KIND_BY_TOOL = {
18
+ task: "subagent",
19
+ delegate: "delegate",
20
+ };
21
+ function parseArgs(tc) {
22
+ try {
23
+ const parsed = JSON.parse(tc.function.arguments || "{}");
24
+ return parsed && typeof parsed === "object" ? parsed : {};
25
+ }
26
+ catch (err) {
27
+ // Arguments stream in as partial JSON while a call assembles — an
28
+ // unparseable value here is expected mid-flight, not an error.
29
+ return {};
30
+ }
31
+ }
32
+ function str(value) {
33
+ return typeof value === "string" ? value : "";
34
+ }
35
+ /** The activity kind a tool call represents, or null if it spawns nothing. */
36
+ export function activityKindFor(tc) {
37
+ const name = tc.function.name;
38
+ const known = KIND_BY_TOOL[name];
39
+ if (known)
40
+ return known;
41
+ // A backgrounded shell is a spawned job too — but only when it actually
42
+ // backgrounds. A normal bash call is just a tool, not an activity.
43
+ if (name === "bash" && parseArgs(tc).background === true)
44
+ return "background";
45
+ return null;
46
+ }
47
+ function toActivity(tc, result) {
48
+ const kind = activityKindFor(tc);
49
+ if (!kind)
50
+ return null;
51
+ const args = parseArgs(tc);
52
+ const agent = kind === "background" ? "" : str(args.agent);
53
+ const command = str(args.command);
54
+ const label = (kind === "background" ? command : str(args.description) || str(args.prompt)) || tc.function.name;
55
+ const request = kind === "background" ? command : str(args.prompt);
56
+ let status = "running";
57
+ if (result)
58
+ status = result.success ? "done" : "failed";
59
+ const output = result ? (result.success ? result.output || "" : result.error || "") : "";
60
+ const detail = [request, output].filter((s) => s.trim().length > 0).join("\n\n");
61
+ return { id: tc.id, kind, label: label.replace(/\s+/g, " ").trim(), agent, status, detail };
62
+ }
63
+ /**
64
+ * Every activity in the session, oldest first. Later state for the same
65
+ * tool-call id wins, so a call that finishes replaces its own running row
66
+ * instead of adding a second one.
67
+ */
68
+ export function collectAgentActivities(messages, activeToolCalls = []) {
69
+ const byId = new Map();
70
+ const add = (tc, result) => {
71
+ if (!tc)
72
+ return;
73
+ const activity = toActivity(tc, result);
74
+ if (activity)
75
+ byId.set(activity.id, activity);
76
+ };
77
+ for (const entry of messages) {
78
+ if (entry.type === "tool_result") {
79
+ add(entry.toolCall, entry.toolResult);
80
+ continue;
81
+ }
82
+ if (entry.type === "tool_group") {
83
+ for (const item of entry.toolGroup?.items ?? [])
84
+ add(item.toolCall, item.result);
85
+ }
86
+ }
87
+ // In-flight calls fill in only what the transcript does not already have.
88
+ // activeToolCalls is not always cleared the instant a result lands, so adding
89
+ // one unconditionally would flip a finished row back to "running".
90
+ for (const tc of activeToolCalls) {
91
+ if (!byId.has(tc.id))
92
+ add(tc, undefined);
93
+ }
94
+ return [...byId.values()];
95
+ }
96
+ //# sourceMappingURL=agent-activities.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * src/ui/utils/group-tool-entries.ts
3
+ *
4
+ * Rebuild the tool_group panels when a flat transcript is (re)loaded.
5
+ *
6
+ * WHY: the live turn renders tool calls inside a `tool_group` ChatEntry, but the
7
+ * persisted transcript stores one `tool_result` entry per call. Anything that
8
+ * replaces the message list with `agent.getChatEntries()` — the end-of-turn
9
+ * resync in finalizeActiveTurn, session load, resume — therefore DESTROYED the
10
+ * group and dropped the transcript back to one flat "→ <tool>" line per call.
11
+ * That is what a user sees after the answer lands, so the nice
12
+ * "Read 2 files, ran 1 shell command" recap only ever existed mid-turn.
13
+ *
14
+ * This folds consecutive tool_result entries back into a done/failed group.
15
+ */
16
+ import type { ChatEntry } from "../../types/index.js";
17
+ /**
18
+ * Fold runs of consecutive groupable tool_result entries into one tool_group
19
+ * entry each. Non-tool entries and rich results pass through untouched, so the
20
+ * transcript keeps its order. A run of one is still grouped: the recap line
21
+ * ("Read 1 file") is what stays visible once collapsed.
22
+ *
23
+ * `tool_call` entries are dropped inside a run — they are the pending twin of a
24
+ * result that is already represented by the item.
25
+ */
26
+ export declare function groupToolEntries(entries: ChatEntry[]): ChatEntry[];
@@ -0,0 +1,111 @@
1
+ /**
2
+ * src/ui/utils/group-tool-entries.ts
3
+ *
4
+ * Rebuild the tool_group panels when a flat transcript is (re)loaded.
5
+ *
6
+ * WHY: the live turn renders tool calls inside a `tool_group` ChatEntry, but the
7
+ * persisted transcript stores one `tool_result` entry per call. Anything that
8
+ * replaces the message list with `agent.getChatEntries()` — the end-of-turn
9
+ * resync in finalizeActiveTurn, session load, resume — therefore DESTROYED the
10
+ * group and dropped the transcript back to one flat "→ <tool>" line per call.
11
+ * That is what a user sees after the answer lands, so the nice
12
+ * "Read 2 files, ran 1 shell command" recap only ever existed mid-turn.
13
+ *
14
+ * This folds consecutive tool_result entries back into a done/failed group.
15
+ */
16
+ /**
17
+ * Tool results whose own renderer shows something a tool-group item line cannot
18
+ * (a plan, a sub-agent run, an image, streaming process logs). These stay as
19
+ * standalone entries and also break a run, so ordering is preserved.
20
+ */
21
+ function rendersRichResult(entry) {
22
+ const name = entry.toolCall?.function.name ?? "";
23
+ const r = entry.toolResult;
24
+ if (!r)
25
+ return true;
26
+ if (r.plan || r.task || r.delegation || r.backgroundProcess || (r.media?.length ?? 0) > 0)
27
+ return true;
28
+ return (name === "generate_plan" ||
29
+ name === "task" ||
30
+ name === "delegate" ||
31
+ name === "delegation_list" ||
32
+ name === "delegation_read" ||
33
+ name === "lsp" ||
34
+ name === "process_logs" ||
35
+ name === "process_stop" ||
36
+ name === "process_list");
37
+ }
38
+ function groupable(entry) {
39
+ return entry.type === "tool_result" && !!entry.toolCall && !rendersRichResult(entry);
40
+ }
41
+ function toItem(entry) {
42
+ const at = entry.timestamp instanceof Date ? entry.timestamp.getTime() : Date.now();
43
+ const failed = entry.toolResult?.success === false;
44
+ return {
45
+ toolCall: entry.toolCall,
46
+ result: entry.toolResult,
47
+ startedAt: at,
48
+ finishedAt: at,
49
+ ...(failed ? { failed: true } : {}),
50
+ };
51
+ }
52
+ /**
53
+ * Fold runs of consecutive groupable tool_result entries into one tool_group
54
+ * entry each. Non-tool entries and rich results pass through untouched, so the
55
+ * transcript keeps its order. A run of one is still grouped: the recap line
56
+ * ("Read 1 file") is what stays visible once collapsed.
57
+ *
58
+ * `tool_call` entries are dropped inside a run — they are the pending twin of a
59
+ * result that is already represented by the item.
60
+ */
61
+ export function groupToolEntries(entries) {
62
+ const out = [];
63
+ let run = [];
64
+ const flush = () => {
65
+ if (run.length === 0)
66
+ return;
67
+ const items = run.map(toItem);
68
+ const first = run[0];
69
+ const last = run[run.length - 1];
70
+ const startedAt = items[0].startedAt;
71
+ const finishedAt = items[items.length - 1].finishedAt ?? startedAt;
72
+ out.push({
73
+ type: "tool_group",
74
+ content: "",
75
+ timestamp: first.timestamp,
76
+ ...(first.modeColor ? { modeColor: first.modeColor } : {}),
77
+ ...(first.remoteKey ? { remoteKey: first.remoteKey } : {}),
78
+ ...(first.sourceLabel ? { sourceLabel: first.sourceLabel } : {}),
79
+ toolGroup: {
80
+ // Deterministic id: the same transcript must rebuild to the same ids, or
81
+ // React remounts every group on each resync and expansion state is lost.
82
+ id: `tg-restored-${startedAt}-${items.length}-${last.toolCall?.id ?? "x"}`,
83
+ state: items.some((i) => i.failed) ? "failed" : "done",
84
+ items,
85
+ startedAt,
86
+ finishedAt,
87
+ // The persisted entries carry a write-time timestamp, not the tool's real
88
+ // wall-clock span, so any duration derived here is fiction (a 10s turn
89
+ // rebuilt as "23ms"). Flag it and let the header omit the number rather
90
+ // than print a confident lie.
91
+ restored: true,
92
+ },
93
+ });
94
+ run = [];
95
+ };
96
+ for (const entry of entries) {
97
+ if (groupable(entry)) {
98
+ run.push(entry);
99
+ continue;
100
+ }
101
+ // A pending tool_call adjacent to a run is already covered by the run's
102
+ // items; dropping it prevents a duplicate line above the group.
103
+ if (entry.type === "tool_call" && run.length > 0)
104
+ continue;
105
+ flush();
106
+ out.push(entry);
107
+ }
108
+ flush();
109
+ return out;
110
+ }
111
+ //# sourceMappingURL=group-tool-entries.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * src/ui/utils/tool-summary.ts
3
+ *
4
+ * Phrasing for the tool-group panel. Pure string helpers, no React, so the
5
+ * wording is unit-testable and the component stays presentational.
6
+ *
7
+ * Active: "Reading 2 files…" (header, present participle)
8
+ * Done: "Read 2 files, ran 1 shell command" (past-tense recap)
9
+ *
10
+ * Counting is per CATEGORY, not per tool name: read_file and
11
+ * mcp__filesystem__read_text_file are both "files" to a reader.
12
+ */
13
+ export declare function categoryForTool(name: string): string;
14
+ /**
15
+ * Header while the group runs: "Reading 2 files…".
16
+ * With a mixed batch the dominant category wins and the rest are summed into a
17
+ * trailing "+N more" so the header never claims work it isn't doing.
18
+ */
19
+ export declare function activeToolGroupHeader(toolNames: readonly string[]): string;
20
+ /** Past-tense recap once the group closes: "Read 2 files, ran 1 shell command". */
21
+ export declare function doneToolGroupSummary(toolNames: readonly string[]): string;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * src/ui/utils/tool-summary.ts
3
+ *
4
+ * Phrasing for the tool-group panel. Pure string helpers, no React, so the
5
+ * wording is unit-testable and the component stays presentational.
6
+ *
7
+ * Active: "Reading 2 files…" (header, present participle)
8
+ * Done: "Read 2 files, ran 1 shell command" (past-tense recap)
9
+ *
10
+ * Counting is per CATEGORY, not per tool name: read_file and
11
+ * mcp__filesystem__read_text_file are both "files" to a reader.
12
+ */
13
+ import { verbForTool } from "./tools.js";
14
+ const CATEGORIES = {
15
+ read: { gerund: "Reading", past: "read", noun: "file", nounPlural: "files" },
16
+ write: { gerund: "Writing", past: "wrote", noun: "file", nounPlural: "files" },
17
+ edit: { gerund: "Editing", past: "edited", noun: "file", nounPlural: "files" },
18
+ bash: { gerund: "Running", past: "ran", noun: "shell command", nounPlural: "shell commands" },
19
+ search: { gerund: "Searching", past: "ran", noun: "search", nounPlural: "searches" },
20
+ agent: { gerund: "Exploring", past: "ran", noun: "sub-agent", nounPlural: "sub-agents" },
21
+ other: { gerund: "Working", past: "ran", noun: "tool", nounPlural: "tools" },
22
+ };
23
+ /** Stable phrase order so the recap doesn't reshuffle as items land. */
24
+ const ORDER = ["read", "write", "edit", "bash", "search", "agent", "other"];
25
+ export function categoryForTool(name) {
26
+ if (name === "write_file" || /^mcp_+filesystem__write_file$/.test(name))
27
+ return "write";
28
+ if (name === "edit_file" || /^mcp_+filesystem__edit_file$/.test(name))
29
+ return "edit";
30
+ if (name === "bash")
31
+ return "bash";
32
+ if (name === "task" || name === "delegate")
33
+ return "agent";
34
+ // Fall back to the existing verb vocabulary so a new tool inherits sane
35
+ // phrasing without being enumerated here twice.
36
+ const verb = verbForTool(name);
37
+ if (verb === "Reading")
38
+ return "read";
39
+ if (verb === "Searching")
40
+ return "search";
41
+ return "other";
42
+ }
43
+ function countByCategory(toolNames) {
44
+ const counts = new Map();
45
+ for (const n of toolNames) {
46
+ const c = categoryForTool(n);
47
+ counts.set(c, (counts.get(c) ?? 0) + 1);
48
+ }
49
+ return counts;
50
+ }
51
+ function phrase(cat, n, past) {
52
+ const noun = n === 1 ? cat.noun : cat.nounPlural;
53
+ return past ? `${cat.past} ${n} ${noun}` : `${cat.gerund} ${n} ${noun}`;
54
+ }
55
+ /**
56
+ * Header while the group runs: "Reading 2 files…".
57
+ * With a mixed batch the dominant category wins and the rest are summed into a
58
+ * trailing "+N more" so the header never claims work it isn't doing.
59
+ */
60
+ export function activeToolGroupHeader(toolNames) {
61
+ if (toolNames.length === 0)
62
+ return "Working…";
63
+ const counts = countByCategory(toolNames);
64
+ let bestKey = "other";
65
+ let best = 0;
66
+ for (const key of ORDER) {
67
+ const c = counts.get(key) ?? 0;
68
+ if (c > best) {
69
+ best = c;
70
+ bestKey = key;
71
+ }
72
+ }
73
+ const rest = toolNames.length - best;
74
+ const head = phrase(CATEGORIES[bestKey], best, false);
75
+ return rest > 0 ? `${head} +${rest} more…` : `${head}…`;
76
+ }
77
+ /** Past-tense recap once the group closes: "Read 2 files, ran 1 shell command". */
78
+ export function doneToolGroupSummary(toolNames) {
79
+ if (toolNames.length === 0)
80
+ return "Done";
81
+ const counts = countByCategory(toolNames);
82
+ const parts = [];
83
+ for (const key of ORDER) {
84
+ const n = counts.get(key) ?? 0;
85
+ if (n > 0)
86
+ parts.push(phrase(CATEGORIES[key], n, true));
87
+ }
88
+ const joined = parts.join(", ");
89
+ return joined.charAt(0).toUpperCase() + joined.slice(1);
90
+ }
91
+ //# sourceMappingURL=tool-summary.js.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * src/utils/event-loop-monitor.ts
3
+ *
4
+ * Event-loop block detector.
5
+ *
6
+ * Root cause it addresses (observed live, 2026-07-16, session 90c3ff533826):
7
+ * the TUI froze for 304.5s mid-turn — keystrokes and mouse clicks produced
8
+ * nothing during the freeze, then ALL replayed at once when it ended (the
9
+ * signature of a blocked event loop, not a stalled render). Crucially the
10
+ * 120s provider stall watchdog (tool-engine.ts) did NOT rescue the turn at its
11
+ * deadline: `stall_rescue` landed at 09:05:47, 6.7s AFTER the loop recovered at
12
+ * 09:05:41. A setTimeout cannot fire while the loop is blocked, so every
13
+ * existing guard in this codebase is blind to this failure mode BY
14
+ * CONSTRUCTION — they are all loop-driven. Same pattern measured across 4
15
+ * models / 4 providers (grok-composer 304.5s, deepseek-v4-flash 320.2s,
16
+ * kimi-k2.7-code 329.7s + 314.8s), so it is not a provider quirk.
17
+ *
18
+ * This module is the missing instrument: a timer that measures its OWN lateness.
19
+ * It cannot prevent a block (it is loop-driven too), but the drift it observes
20
+ * on the first tick AFTER the loop frees up measures the block that just
21
+ * happened. Pair it with the CPU profiler in `loop-profiler.ts`, whose V8
22
+ * sampling thread runs INDEPENDENTLY of the JS thread and therefore captures
23
+ * the culprit stack DURING the block.
24
+ *
25
+ * Deliberately dependency-free and cheap: one unref'd interval, no allocation
26
+ * per tick. `detectBlock` is pure so the drift maths is unit-testable without
27
+ * timers.
28
+ */
29
+ /** A detected event-loop block, reported to `onBlock`. */
30
+ export interface EventLoopBlock {
31
+ /**
32
+ * How long the loop was blocked, in ms. Measured as the monitor tick's own
33
+ * lateness, so it UNDER-reports by up to one tick interval (a block shorter
34
+ * than the tick can hide entirely between two ticks). Treat it as a lower
35
+ * bound.
36
+ */
37
+ blockedMs: number;
38
+ /** What the process last said it was doing — see {@link setLoopBreadcrumb}. */
39
+ breadcrumb: string | null;
40
+ /** ISO timestamp of the tick that observed the block (i.e. after it ended). */
41
+ detectedAt: string;
42
+ }
43
+ /**
44
+ * Decide whether an interval tick's lateness constitutes a block.
45
+ *
46
+ * A healthy tick fires ~`tickMs` after the previous one; the loop being busy
47
+ * pushes it later. Lateness beyond `thresholdMs` is reported as a block of that
48
+ * duration. Pure so the drift maths is testable without real timers.
49
+ *
50
+ * @param expectedAt when this tick was due (monotonic ms)
51
+ * @param actualAt when it actually ran (monotonic ms)
52
+ * @param thresholdMs report only blocks at least this long; <= 0 disables
53
+ * @returns block duration in ms, or null when the tick was on time
54
+ */
55
+ export declare function detectBlock(expectedAt: number, actualAt: number, thresholdMs: number): number | null;
56
+ /**
57
+ * Record what the process is about to do, so a block report can name a suspect
58
+ * instead of just a duration. Call it around anything that could plausibly run
59
+ * long and synchronously (tool execution, a stream step). Cheap — a single
60
+ * assignment; safe to call on a hot path.
61
+ *
62
+ * Pass null to clear. Not a stack: last writer wins, by design — the goal is a
63
+ * hint for the profile, and the profile is the real evidence.
64
+ */
65
+ export declare function setLoopBreadcrumb(label: string | null): void;
66
+ /** The current breadcrumb, or null. */
67
+ export declare function getLoopBreadcrumb(): string | null;
68
+ /** Options for {@link startEventLoopMonitor}. */
69
+ export interface EventLoopMonitorOpts {
70
+ /** Report blocks at least this long. <= 0 disables the monitor entirely. */
71
+ thresholdMs: number;
72
+ /**
73
+ * How often to check. Bounds both the detection granularity and the amount a
74
+ * block can be under-reported by. 500ms costs ~2 wakeups/sec.
75
+ */
76
+ tickMs?: number;
77
+ /** Called once per detected block. Must not throw (it is guarded anyway). */
78
+ onBlock: (block: EventLoopBlock) => void;
79
+ }
80
+ /**
81
+ * Start watching for event-loop blocks. Returns a stop function (idempotent).
82
+ *
83
+ * The interval is unref'd so it never keeps the process alive on its own.
84
+ */
85
+ export declare function startEventLoopMonitor(opts: EventLoopMonitorOpts): () => void;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * src/utils/event-loop-monitor.ts
3
+ *
4
+ * Event-loop block detector.
5
+ *
6
+ * Root cause it addresses (observed live, 2026-07-16, session 90c3ff533826):
7
+ * the TUI froze for 304.5s mid-turn — keystrokes and mouse clicks produced
8
+ * nothing during the freeze, then ALL replayed at once when it ended (the
9
+ * signature of a blocked event loop, not a stalled render). Crucially the
10
+ * 120s provider stall watchdog (tool-engine.ts) did NOT rescue the turn at its
11
+ * deadline: `stall_rescue` landed at 09:05:47, 6.7s AFTER the loop recovered at
12
+ * 09:05:41. A setTimeout cannot fire while the loop is blocked, so every
13
+ * existing guard in this codebase is blind to this failure mode BY
14
+ * CONSTRUCTION — they are all loop-driven. Same pattern measured across 4
15
+ * models / 4 providers (grok-composer 304.5s, deepseek-v4-flash 320.2s,
16
+ * kimi-k2.7-code 329.7s + 314.8s), so it is not a provider quirk.
17
+ *
18
+ * This module is the missing instrument: a timer that measures its OWN lateness.
19
+ * It cannot prevent a block (it is loop-driven too), but the drift it observes
20
+ * on the first tick AFTER the loop frees up measures the block that just
21
+ * happened. Pair it with the CPU profiler in `loop-profiler.ts`, whose V8
22
+ * sampling thread runs INDEPENDENTLY of the JS thread and therefore captures
23
+ * the culprit stack DURING the block.
24
+ *
25
+ * Deliberately dependency-free and cheap: one unref'd interval, no allocation
26
+ * per tick. `detectBlock` is pure so the drift maths is unit-testable without
27
+ * timers.
28
+ */
29
+ import { logger } from "./logger.js";
30
+ /**
31
+ * Decide whether an interval tick's lateness constitutes a block.
32
+ *
33
+ * A healthy tick fires ~`tickMs` after the previous one; the loop being busy
34
+ * pushes it later. Lateness beyond `thresholdMs` is reported as a block of that
35
+ * duration. Pure so the drift maths is testable without real timers.
36
+ *
37
+ * @param expectedAt when this tick was due (monotonic ms)
38
+ * @param actualAt when it actually ran (monotonic ms)
39
+ * @param thresholdMs report only blocks at least this long; <= 0 disables
40
+ * @returns block duration in ms, or null when the tick was on time
41
+ */
42
+ export function detectBlock(expectedAt, actualAt, thresholdMs) {
43
+ if (!(thresholdMs > 0))
44
+ return null;
45
+ const lateBy = actualAt - expectedAt;
46
+ if (!Number.isFinite(lateBy) || lateBy < thresholdMs)
47
+ return null;
48
+ return Math.round(lateBy);
49
+ }
50
+ let _breadcrumb = null;
51
+ /**
52
+ * Record what the process is about to do, so a block report can name a suspect
53
+ * instead of just a duration. Call it around anything that could plausibly run
54
+ * long and synchronously (tool execution, a stream step). Cheap — a single
55
+ * assignment; safe to call on a hot path.
56
+ *
57
+ * Pass null to clear. Not a stack: last writer wins, by design — the goal is a
58
+ * hint for the profile, and the profile is the real evidence.
59
+ */
60
+ export function setLoopBreadcrumb(label) {
61
+ _breadcrumb = label;
62
+ }
63
+ /** The current breadcrumb, or null. */
64
+ export function getLoopBreadcrumb() {
65
+ return _breadcrumb;
66
+ }
67
+ /**
68
+ * Start watching for event-loop blocks. Returns a stop function (idempotent).
69
+ *
70
+ * The interval is unref'd so it never keeps the process alive on its own.
71
+ */
72
+ export function startEventLoopMonitor(opts) {
73
+ const { thresholdMs, onBlock } = opts;
74
+ const tickMs = opts.tickMs && opts.tickMs > 0 ? opts.tickMs : 500;
75
+ if (!(thresholdMs > 0))
76
+ return () => { };
77
+ let expectedAt = performance.now() + tickMs;
78
+ const timer = setInterval(() => {
79
+ const actualAt = performance.now();
80
+ const blockedMs = detectBlock(expectedAt, actualAt, thresholdMs);
81
+ expectedAt = actualAt + tickMs;
82
+ if (blockedMs === null)
83
+ return;
84
+ try {
85
+ onBlock({
86
+ blockedMs,
87
+ breadcrumb: getLoopBreadcrumb(),
88
+ detectedAt: new Date().toISOString(),
89
+ });
90
+ }
91
+ catch (err) {
92
+ logger.error("cli", `[event-loop-monitor] onBlock handler threw: ${err?.message}`, {
93
+ error: err,
94
+ blockedMs,
95
+ });
96
+ }
97
+ }, tickMs);
98
+ timer.unref?.();
99
+ let stopped = false;
100
+ return () => {
101
+ if (stopped)
102
+ return;
103
+ stopped = true;
104
+ clearInterval(timer);
105
+ };
106
+ }
107
+ //# sourceMappingURL=event-loop-monitor.js.map
@@ -50,3 +50,17 @@ export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: nu
50
50
  * The short grace lets the normal fetch-level abort win first when it's quick.
51
51
  */
52
52
  abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
53
+ /**
54
+ * Wall-clock backstop (ms) for an isolated sub-agent task (`runIsolatedTask`).
55
+ *
56
+ * The council `generate()` chokepoint and the sprint impl/verify stages already
57
+ * have deadlines, but the *other* `runIsolatedTask` await points — plan-adherence
58
+ * review + fix, council research, grounding-verify — were bare `await`s. A
59
+ * provider that hangs on the JS side after the sub-agent's stream finishes wedges
60
+ * the whole pipeline with no error (observed live: run mrhc43f0fb9b impl, and the
61
+ * scoping stall). Wrap those calls in `withDeadlineRace(fn, getIsolatedTaskDeadlineMs(), ...)`
62
+ * so a hang surfaces as a rejection the caller already handles. Generous by
63
+ * default so a legitimately long tool-using sub-agent is not cut short; override
64
+ * with MUONROI_IDEAL_ISOLATED_TASK_MS. Clamped to [60s, 30min].
65
+ */
66
+ export declare function getIsolatedTaskDeadlineMs(): number;
@@ -106,4 +106,23 @@ abortSignal, abortGraceMs = 1500) {
106
106
  abortSignal.removeEventListener("abort", abortListener);
107
107
  }
108
108
  }
109
+ /**
110
+ * Wall-clock backstop (ms) for an isolated sub-agent task (`runIsolatedTask`).
111
+ *
112
+ * The council `generate()` chokepoint and the sprint impl/verify stages already
113
+ * have deadlines, but the *other* `runIsolatedTask` await points — plan-adherence
114
+ * review + fix, council research, grounding-verify — were bare `await`s. A
115
+ * provider that hangs on the JS side after the sub-agent's stream finishes wedges
116
+ * the whole pipeline with no error (observed live: run mrhc43f0fb9b impl, and the
117
+ * scoping stall). Wrap those calls in `withDeadlineRace(fn, getIsolatedTaskDeadlineMs(), ...)`
118
+ * so a hang surfaces as a rejection the caller already handles. Generous by
119
+ * default so a legitimately long tool-using sub-agent is not cut short; override
120
+ * with MUONROI_IDEAL_ISOLATED_TASK_MS. Clamped to [60s, 30min].
121
+ */
122
+ export function getIsolatedTaskDeadlineMs() {
123
+ const raw = Number.parseInt(process.env.MUONROI_IDEAL_ISOLATED_TASK_MS ?? "", 10);
124
+ if (Number.isFinite(raw) && raw >= 60_000 && raw <= 1_800_000)
125
+ return raw;
126
+ return 900_000; // 15 min backstop
127
+ }
109
128
  //# sourceMappingURL=llm-deadline.js.map