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.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- 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";
|