@wrongstack/cli 0.9.7 → 0.9.20
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/index.js +643 -40
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as path24 from 'path';
|
|
3
3
|
import * as fsp3 from 'fs/promises';
|
|
4
|
-
import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
|
|
4
|
+
import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
6
|
import * as os3 from 'os';
|
|
7
7
|
import os3__default from 'os';
|
|
@@ -380,12 +380,9 @@ function buildSddCommand(opts) {
|
|
|
380
380
|
name: "sdd",
|
|
381
381
|
description: "AI-driven SDD: /sdd [new|approve|execute|cancel|status|list|show|templates]",
|
|
382
382
|
async run(args) {
|
|
383
|
-
opts.context;
|
|
384
383
|
if (!opts.paths) return { message: "SDD not available \u2014 paths not configured." };
|
|
385
384
|
const specsDir = opts.paths.projectSpecs;
|
|
386
|
-
const graphsDir = opts.paths.projectTaskGraphs;
|
|
387
385
|
const specStore = new SpecStore({ baseDir: specsDir });
|
|
388
|
-
new TaskGraphStore({ baseDir: graphsDir });
|
|
389
386
|
const versioning = sddState.getVersioning();
|
|
390
387
|
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
391
388
|
const restJoined = rest.join(" ").trim();
|
|
@@ -1024,8 +1021,8 @@ Start executing the tasks one by one.`
|
|
|
1024
1021
|
return { message: lines2.join("\n") };
|
|
1025
1022
|
}
|
|
1026
1023
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
const stored = await
|
|
1024
|
+
const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
|
|
1025
|
+
const stored = await graphStore.load(graphId);
|
|
1029
1026
|
if (stored) {
|
|
1030
1027
|
return { message: renderTaskGraph(stored, { compact: false }) };
|
|
1031
1028
|
}
|
|
@@ -1245,8 +1242,8 @@ ${lines.join("\n")}`
|
|
|
1245
1242
|
return { message: "No task graph found. Generate tasks first." };
|
|
1246
1243
|
}
|
|
1247
1244
|
try {
|
|
1248
|
-
const
|
|
1249
|
-
const graph = await
|
|
1245
|
+
const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
|
|
1246
|
+
const graph = await graphStore.load(graphId);
|
|
1250
1247
|
if (!graph) {
|
|
1251
1248
|
return { message: "Could not load task graph." };
|
|
1252
1249
|
}
|
|
@@ -1659,7 +1656,7 @@ async function checkForUpdate(signal, homeFn) {
|
|
|
1659
1656
|
outdated: isNewer(latest, current),
|
|
1660
1657
|
checkFailed: false
|
|
1661
1658
|
};
|
|
1662
|
-
} catch (
|
|
1659
|
+
} catch (_err) {
|
|
1663
1660
|
if (aborted()) {
|
|
1664
1661
|
return { current, latest: current, outdated: false, checkFailed: true };
|
|
1665
1662
|
}
|
|
@@ -1980,7 +1977,7 @@ async function runWebUI(opts) {
|
|
|
1980
1977
|
}
|
|
1981
1978
|
}
|
|
1982
1979
|
}
|
|
1983
|
-
async function handleUserMessage(ws,
|
|
1980
|
+
async function handleUserMessage(ws, _client, content) {
|
|
1984
1981
|
if (abortController) {
|
|
1985
1982
|
send(ws, {
|
|
1986
1983
|
type: "error",
|
|
@@ -3553,8 +3550,85 @@ function buildLoadCommand(opts) {
|
|
|
3553
3550
|
return {
|
|
3554
3551
|
name: "resume",
|
|
3555
3552
|
aliases: ["load", "sessions"],
|
|
3556
|
-
description: "List recent sessions.",
|
|
3557
|
-
async run() {
|
|
3553
|
+
description: "List recent sessions, show incomplete ones (--incomplete), or plan a recovery (--recover <id>).",
|
|
3554
|
+
async run(args) {
|
|
3555
|
+
const parts = args.split(/\s+/).filter(Boolean);
|
|
3556
|
+
const showIncomplete = parts.includes("--incomplete") || parts.includes("-i");
|
|
3557
|
+
const recoverIdx = parts.findIndex((p) => p === "--recover");
|
|
3558
|
+
const recoverTarget = recoverIdx >= 0 ? parts[recoverIdx + 1] : void 0;
|
|
3559
|
+
if (recoverTarget) {
|
|
3560
|
+
if (!opts.paths) {
|
|
3561
|
+
return { message: color.yellow("No paths configured \u2014 cannot build a recovery plan.") };
|
|
3562
|
+
}
|
|
3563
|
+
const recovery = new SessionRecovery(opts.paths.projectSessions);
|
|
3564
|
+
const plan = await recovery.recover(recoverTarget);
|
|
3565
|
+
if (!plan) {
|
|
3566
|
+
return {
|
|
3567
|
+
message: color.yellow(
|
|
3568
|
+
`No session log found for ${recoverTarget} (or it is empty).`
|
|
3569
|
+
)
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
const lines2 = [
|
|
3573
|
+
color.bold(`Recovery plan for ${plan.sessionId}`),
|
|
3574
|
+
` Stale: ${plan.stale ? color.yellow("yes") : color.green("no")}`
|
|
3575
|
+
];
|
|
3576
|
+
if (plan.context) {
|
|
3577
|
+
lines2.push(` Last in-flight context: ${color.cyan(plan.context)}`);
|
|
3578
|
+
}
|
|
3579
|
+
if (plan.lastCheckpoint && plan.lastCheckpoint.type === "checkpoint") {
|
|
3580
|
+
const cp = plan.lastCheckpoint;
|
|
3581
|
+
lines2.push(
|
|
3582
|
+
` Last checkpoint: promptIndex=${cp.promptIndex} preview=${color.dim(`"${cp.promptPreview}"`)} at ${color.dim(cp.ts)}`
|
|
3583
|
+
);
|
|
3584
|
+
} else {
|
|
3585
|
+
lines2.push(` Last checkpoint: ${color.dim("(none \u2014 full re-execution)")}`);
|
|
3586
|
+
}
|
|
3587
|
+
lines2.push(
|
|
3588
|
+
` Pending events: ${plan.pendingEvents.length} (the work that would re-run on resume)`
|
|
3589
|
+
);
|
|
3590
|
+
if (plan.pendingEvents.length > 0) {
|
|
3591
|
+
const summary = summarizePending(plan.pendingEvents);
|
|
3592
|
+
lines2.push(...summary);
|
|
3593
|
+
}
|
|
3594
|
+
lines2.push("");
|
|
3595
|
+
lines2.push(
|
|
3596
|
+
color.dim(
|
|
3597
|
+
plan.stale ? " This session crashed mid-iteration. The full re-execution kernel is coming in a follow-up; for now use this plan to decide whether to start fresh." : " This session ended cleanly; the plan above describes the most recent turn(s) for context."
|
|
3598
|
+
)
|
|
3599
|
+
);
|
|
3600
|
+
return { message: lines2.join("\n") };
|
|
3601
|
+
}
|
|
3602
|
+
if (showIncomplete) {
|
|
3603
|
+
if (!opts.paths) {
|
|
3604
|
+
return { message: color.yellow("No paths configured \u2014 cannot scan for incomplete sessions.") };
|
|
3605
|
+
}
|
|
3606
|
+
const recovery = new SessionRecovery(opts.paths.projectSessions);
|
|
3607
|
+
const resumable = await recovery.listResumable();
|
|
3608
|
+
if (resumable.length === 0) {
|
|
3609
|
+
return {
|
|
3610
|
+
message: color.dim("No incomplete sessions. (Every recorded run ended cleanly.)")
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
const lines2 = [
|
|
3614
|
+
color.bold(`${resumable.length} incomplete session(s)`),
|
|
3615
|
+
color.dim(" (process died mid-iteration; the last event was in_flight_start)"),
|
|
3616
|
+
""
|
|
3617
|
+
];
|
|
3618
|
+
for (const s of resumable) {
|
|
3619
|
+
const t = color.dim(s.lastEventTs.slice(0, 19).replace("T", " "));
|
|
3620
|
+
lines2.push(
|
|
3621
|
+
` ${color.cyan(s.sessionId)} ${t} ${color.dim(`${s.eventCount} events`)} ${s.context}`
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
lines2.push("");
|
|
3625
|
+
lines2.push(
|
|
3626
|
+
color.dim(
|
|
3627
|
+
" Resuming re-executes the in-flight work from the last checkpoint. Full resume is coming in a follow-up; for now use this list to identify crashes and decide whether to restart fresh."
|
|
3628
|
+
)
|
|
3629
|
+
);
|
|
3630
|
+
return { message: lines2.join("\n") };
|
|
3631
|
+
}
|
|
3558
3632
|
if (!opts.sessionStore) return { message: "No session store configured." };
|
|
3559
3633
|
const list = await opts.sessionStore.list(10);
|
|
3560
3634
|
if (list.length === 0) return { message: "No saved sessions." };
|
|
@@ -3565,7 +3639,8 @@ function buildLoadCommand(opts) {
|
|
|
3565
3639
|
${lines.join("\n")}
|
|
3566
3640
|
|
|
3567
3641
|
${color.dim(`Resume one with: wstack resume ${list[0]?.id ?? "<id>"}
|
|
3568
|
-
`)}
|
|
3642
|
+
`)}
|
|
3643
|
+
${color.dim("Tip: /resume --incomplete \u2014 list sessions that crashed mid-iteration")}`;
|
|
3569
3644
|
opts.renderer.write(msg);
|
|
3570
3645
|
return { message: msg };
|
|
3571
3646
|
}
|
|
@@ -3589,6 +3664,47 @@ function buildExitCommand(opts) {
|
|
|
3589
3664
|
}
|
|
3590
3665
|
};
|
|
3591
3666
|
}
|
|
3667
|
+
function summarizePending(events) {
|
|
3668
|
+
const lines = [];
|
|
3669
|
+
const cap = 12;
|
|
3670
|
+
for (const ev of events.slice(-cap)) {
|
|
3671
|
+
const t = "ts" in ev ? color.dim(String(ev.ts).slice(11, 19)) : color.dim("--:--:--");
|
|
3672
|
+
const kind = color.cyan(String(ev.type).padEnd(18));
|
|
3673
|
+
lines.push(` ${t} ${kind} ${summariseEvent(ev)}`);
|
|
3674
|
+
}
|
|
3675
|
+
if (events.length > cap) {
|
|
3676
|
+
lines.push(color.dim(` \u2026 and ${events.length - cap} more`));
|
|
3677
|
+
}
|
|
3678
|
+
return lines;
|
|
3679
|
+
}
|
|
3680
|
+
function summariseEvent(ev) {
|
|
3681
|
+
switch (ev.type) {
|
|
3682
|
+
case "user_input": {
|
|
3683
|
+
const text = "text" in ev && typeof ev.text === "string" ? ev.text : Array.isArray(ev.content) ? "\u2026" : "";
|
|
3684
|
+
return color.dim(text.length > 60 ? text.slice(0, 59) + "\u2026" : text);
|
|
3685
|
+
}
|
|
3686
|
+
case "llm_response":
|
|
3687
|
+
return color.dim("(model reply)");
|
|
3688
|
+
case "tool_use":
|
|
3689
|
+
return color.dim(`name=${ev.name ?? "?"}`);
|
|
3690
|
+
case "tool_result":
|
|
3691
|
+
return color.dim(`name=${ev.name ?? "?"}`);
|
|
3692
|
+
case "in_flight_start":
|
|
3693
|
+
return color.dim(`context="${ev.context ?? ""}"`);
|
|
3694
|
+
case "in_flight_end":
|
|
3695
|
+
return color.dim(`reason=${ev.reason ?? "?"}`);
|
|
3696
|
+
case "checkpoint":
|
|
3697
|
+
return color.dim(
|
|
3698
|
+
`promptIndex=${ev.promptIndex ?? "?"}`
|
|
3699
|
+
);
|
|
3700
|
+
case "compaction":
|
|
3701
|
+
return color.dim("(compaction)");
|
|
3702
|
+
case "error":
|
|
3703
|
+
return color.red(String(ev.message ?? ""));
|
|
3704
|
+
default:
|
|
3705
|
+
return color.dim("\u2026");
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3592
3708
|
|
|
3593
3709
|
// src/slash-commands/spawn-agents.ts
|
|
3594
3710
|
function buildSpawnCommand(opts) {
|
|
@@ -4173,8 +4289,48 @@ ${lines.join("\n")}`;
|
|
|
4173
4289
|
}
|
|
4174
4290
|
};
|
|
4175
4291
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
4292
|
+
async function runModePicker(modeStore, reader) {
|
|
4293
|
+
const modes = await modeStore.listModes();
|
|
4294
|
+
const active = await modeStore.getActiveMode();
|
|
4295
|
+
const activeIdx = modes.findIndex((m) => m.id === active?.id);
|
|
4296
|
+
let cursor = activeIdx >= 0 ? activeIdx : 0;
|
|
4297
|
+
const render = (currentCursor) => {
|
|
4298
|
+
const lines = [];
|
|
4299
|
+
lines.push(`
|
|
4300
|
+
${color.bold(color.amber("WrongStack") + color.dim(" \u2014 Mode Selection"))}
|
|
4301
|
+
`);
|
|
4302
|
+
lines.push(color.dim(" \u2191\u2193 navigate Enter select q quit\n"));
|
|
4303
|
+
lines.push("");
|
|
4304
|
+
for (let i = 0; i < modes.length; i++) {
|
|
4305
|
+
const m = modes[i];
|
|
4306
|
+
const mark = m.id === active?.id ? color.green(" [active]") : "";
|
|
4307
|
+
const prefix = i === currentCursor ? color.bold("\u276F ") : " ";
|
|
4308
|
+
const name = i === currentCursor ? color.bold(m.name) : m.name;
|
|
4309
|
+
lines.push(` ${prefix}${name} ${color.dim(m.description)}${mark}`);
|
|
4310
|
+
}
|
|
4311
|
+
lines.push("");
|
|
4312
|
+
return lines.join("\n");
|
|
4313
|
+
};
|
|
4314
|
+
process.stdout.write(render(cursor));
|
|
4315
|
+
const options = [
|
|
4316
|
+
{ key: "\x1B[A", label: "\u2191", value: "up" },
|
|
4317
|
+
{ key: "\x1B[B", label: "\u2193", value: "down" },
|
|
4318
|
+
{ key: "\r", label: "Enter", value: "enter" },
|
|
4319
|
+
{ key: "q", label: "q", value: "quit" }
|
|
4320
|
+
];
|
|
4321
|
+
while (true) {
|
|
4322
|
+
const answer = await reader.readKey("", options);
|
|
4323
|
+
if (answer === "quit") return void 0;
|
|
4324
|
+
if (answer === "enter") return modes[cursor];
|
|
4325
|
+
if (answer === "up") {
|
|
4326
|
+
cursor = cursor > 0 ? cursor - 1 : modes.length - 1;
|
|
4327
|
+
} else if (answer === "down") {
|
|
4328
|
+
cursor = cursor < modes.length - 1 ? cursor + 1 : 0;
|
|
4329
|
+
}
|
|
4330
|
+
process.stdout.write("\x1B[J");
|
|
4331
|
+
process.stdout.write(render(cursor));
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4178
4334
|
function buildModeCommand(opts) {
|
|
4179
4335
|
return {
|
|
4180
4336
|
name: "mode",
|
|
@@ -4194,7 +4350,7 @@ function buildModeCommand(opts) {
|
|
|
4194
4350
|
" /mode brief Switch to brief mode",
|
|
4195
4351
|
" /mode teach Switch to teach mode"
|
|
4196
4352
|
].join("\n"),
|
|
4197
|
-
async run(args) {
|
|
4353
|
+
async run(args, _ctx) {
|
|
4198
4354
|
const modeStore = opts.modeStore;
|
|
4199
4355
|
if (!modeStore) {
|
|
4200
4356
|
return { message: "Mode store not available in this context." };
|
|
@@ -4202,6 +4358,17 @@ function buildModeCommand(opts) {
|
|
|
4202
4358
|
const modes = await modeStore.listModes();
|
|
4203
4359
|
const active = await modeStore.getActiveMode();
|
|
4204
4360
|
if (!args.trim()) {
|
|
4361
|
+
if (opts.inputReader) {
|
|
4362
|
+
const selected = await runModePicker(modeStore, opts.inputReader);
|
|
4363
|
+
if (!selected) {
|
|
4364
|
+
return { message: "Mode selection cancelled." };
|
|
4365
|
+
}
|
|
4366
|
+
await modeStore.setActiveMode(selected.id);
|
|
4367
|
+
return {
|
|
4368
|
+
message: `Switched to "${selected.name}" mode.
|
|
4369
|
+
${selected.description}`
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4205
4372
|
const lines = [`Current mode: ${active?.name ?? "none"}`, "", "Available modes:"];
|
|
4206
4373
|
for (const m of modes) {
|
|
4207
4374
|
const mark = m.id === active?.id ? " [active]" : "";
|
|
@@ -5306,6 +5473,195 @@ function buildAutoPhaseCommand(opts) {
|
|
|
5306
5473
|
}
|
|
5307
5474
|
};
|
|
5308
5475
|
}
|
|
5476
|
+
function buildCollabCommand(opts) {
|
|
5477
|
+
return {
|
|
5478
|
+
name: "collab",
|
|
5479
|
+
description: "Live collaboration helpers (status / invite / history).",
|
|
5480
|
+
async run(args, ctx) {
|
|
5481
|
+
const parts = args.split(/\s+/).filter(Boolean);
|
|
5482
|
+
const sub = (parts[0] ?? "status").toLowerCase();
|
|
5483
|
+
switch (sub) {
|
|
5484
|
+
case "status":
|
|
5485
|
+
return statusCommand(ctx.session?.id);
|
|
5486
|
+
case "invite":
|
|
5487
|
+
return inviteCommand(ctx.session?.id);
|
|
5488
|
+
case "history":
|
|
5489
|
+
return historyCommand(opts, ctx.session?.id, parts.slice(1));
|
|
5490
|
+
case "annotations":
|
|
5491
|
+
case "notes":
|
|
5492
|
+
return annotationsCommand(opts, ctx.session?.id);
|
|
5493
|
+
case "help":
|
|
5494
|
+
case "--help":
|
|
5495
|
+
case "-h":
|
|
5496
|
+
return helpCommand();
|
|
5497
|
+
default:
|
|
5498
|
+
return {
|
|
5499
|
+
message: color.yellow(
|
|
5500
|
+
`Unknown subcommand: ${sub}. Try: /collab status | invite | history | annotations`
|
|
5501
|
+
)
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
};
|
|
5506
|
+
}
|
|
5507
|
+
function statusCommand(sessionId) {
|
|
5508
|
+
const lines = [];
|
|
5509
|
+
lines.push(color.bold("Live collaboration"));
|
|
5510
|
+
lines.push(` Session: ${color.cyan(sessionId ?? "(none)")}`);
|
|
5511
|
+
lines.push(` Observers: ${color.dim("unknown (check webui)")}`);
|
|
5512
|
+
lines.push("");
|
|
5513
|
+
lines.push(
|
|
5514
|
+
color.dim(
|
|
5515
|
+
" Tip: /collab invite \u2014 print a webui join link for a teammate"
|
|
5516
|
+
)
|
|
5517
|
+
);
|
|
5518
|
+
lines.push(
|
|
5519
|
+
color.dim(" /collab history 20 \u2014 show the last 20 events of this session")
|
|
5520
|
+
);
|
|
5521
|
+
return { message: lines.join("\n") };
|
|
5522
|
+
}
|
|
5523
|
+
function inviteCommand(sessionId) {
|
|
5524
|
+
if (!sessionId) {
|
|
5525
|
+
return { message: color.yellow("No active session to invite to.") };
|
|
5526
|
+
}
|
|
5527
|
+
const url = `http://127.0.0.1:3457/?session=${encodeURIComponent(sessionId)}`;
|
|
5528
|
+
const lines = [
|
|
5529
|
+
color.bold("Live-collaboration invite"),
|
|
5530
|
+
` Session ID: ${color.cyan(sessionId)}`,
|
|
5531
|
+
` Share this URL with a teammate (they must be able to reach your machine):`,
|
|
5532
|
+
` ${color.cyan(url)}`,
|
|
5533
|
+
"",
|
|
5534
|
+
color.dim(
|
|
5535
|
+
' The teammate opens the URL in their browser, then clicks "Join as observer"'
|
|
5536
|
+
),
|
|
5537
|
+
color.dim(" on the panel that appears above the chat. They will see a live mirror"),
|
|
5538
|
+
color.dim(" of tool calls, iterations, and subagent spawns \u2014 read-only."),
|
|
5539
|
+
"",
|
|
5540
|
+
color.dim(" Reminder: expose the port with: pnpm --filter @wrongstack/webui dev")
|
|
5541
|
+
];
|
|
5542
|
+
return { message: lines.join("\n") };
|
|
5543
|
+
}
|
|
5544
|
+
async function historyCommand(opts, sessionId, args) {
|
|
5545
|
+
const limit = Math.max(1, Math.min(200, Number.parseInt(args[0] ?? "20", 10) || 20));
|
|
5546
|
+
if (!sessionId) return { message: color.yellow("No active session.") };
|
|
5547
|
+
if (!opts.sessionStore) {
|
|
5548
|
+
return { message: color.yellow("No session store configured.") };
|
|
5549
|
+
}
|
|
5550
|
+
const { DefaultSessionReader: DefaultSessionReader2 } = await import('@wrongstack/core');
|
|
5551
|
+
const reader = new DefaultSessionReader2({ store: opts.sessionStore });
|
|
5552
|
+
const events = [];
|
|
5553
|
+
try {
|
|
5554
|
+
for await (const ev of reader.replay(sessionId)) {
|
|
5555
|
+
events.push(ev);
|
|
5556
|
+
}
|
|
5557
|
+
} catch (err) {
|
|
5558
|
+
return {
|
|
5559
|
+
message: color.yellow(
|
|
5560
|
+
`Failed to read session: ${err instanceof Error ? err.message : String(err)}`
|
|
5561
|
+
)
|
|
5562
|
+
};
|
|
5563
|
+
}
|
|
5564
|
+
const tail = events.slice(-limit);
|
|
5565
|
+
if (tail.length === 0) {
|
|
5566
|
+
return { message: color.dim("No events recorded yet for this session.") };
|
|
5567
|
+
}
|
|
5568
|
+
const lines = [
|
|
5569
|
+
color.bold(`Last ${tail.length} events of ${sessionId}`),
|
|
5570
|
+
""
|
|
5571
|
+
];
|
|
5572
|
+
for (const raw of tail) {
|
|
5573
|
+
const ev = raw;
|
|
5574
|
+
const t = ev.ts ? color.dim(ev.ts.slice(11, 19)) : color.dim("--:--:--");
|
|
5575
|
+
const kind = color.cyan((ev.type ?? "unknown").padEnd(16));
|
|
5576
|
+
const summary = summarise(ev);
|
|
5577
|
+
lines.push(` ${t} ${kind} ${summary}`);
|
|
5578
|
+
}
|
|
5579
|
+
return { message: lines.join("\n") };
|
|
5580
|
+
}
|
|
5581
|
+
async function annotationsCommand(opts, sessionId) {
|
|
5582
|
+
if (!sessionId) return { message: color.yellow("No active session.") };
|
|
5583
|
+
if (!opts.sessionStore) {
|
|
5584
|
+
return { message: color.yellow("No session store configured.") };
|
|
5585
|
+
}
|
|
5586
|
+
const { DefaultSessionReader: DefaultSessionReader2, AnnotationsStore } = await import('@wrongstack/core');
|
|
5587
|
+
new DefaultSessionReader2({ store: opts.sessionStore });
|
|
5588
|
+
const storeDir = opts.sessionStore.dir;
|
|
5589
|
+
if (!storeDir) {
|
|
5590
|
+
return {
|
|
5591
|
+
message: color.yellow(
|
|
5592
|
+
"Annotations view needs the session store dir; not exposed in this build."
|
|
5593
|
+
)
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
const annotations = new AnnotationsStore({ dir: storeDir });
|
|
5597
|
+
const open = await annotations.listOpen(sessionId);
|
|
5598
|
+
if (open.length === 0) {
|
|
5599
|
+
return {
|
|
5600
|
+
message: color.dim("No open annotations for this session.")
|
|
5601
|
+
};
|
|
5602
|
+
}
|
|
5603
|
+
const lines = [
|
|
5604
|
+
color.bold(`${open.length} open annotation(s) for ${sessionId}`),
|
|
5605
|
+
""
|
|
5606
|
+
];
|
|
5607
|
+
for (const a of open) {
|
|
5608
|
+
const t = color.dim(a.createdAt.slice(11, 19));
|
|
5609
|
+
const author = color.cyan(a.authorId.slice(0, 8));
|
|
5610
|
+
const idx = color.dim(`#${a.atEventIndex}`);
|
|
5611
|
+
lines.push(` ${t} ${idx} ${author} ${a.text}`);
|
|
5612
|
+
}
|
|
5613
|
+
return { message: lines.join("\n") };
|
|
5614
|
+
}
|
|
5615
|
+
function summarise(ev) {
|
|
5616
|
+
switch (ev["type"]) {
|
|
5617
|
+
case "user_input": {
|
|
5618
|
+
const text = typeof ev["text"] === "string" ? ev["text"] : "";
|
|
5619
|
+
return color.dim(truncate(text, 80));
|
|
5620
|
+
}
|
|
5621
|
+
case "llm_response": {
|
|
5622
|
+
const text = typeof ev["text"] === "string" ? ev["text"] : "";
|
|
5623
|
+
return color.dim(truncate(text, 80));
|
|
5624
|
+
}
|
|
5625
|
+
case "tool_result": {
|
|
5626
|
+
const name = String(ev["name"] ?? "?");
|
|
5627
|
+
const ok = ev["ok"] === false ? color.red("(failed)") : color.green("(ok)");
|
|
5628
|
+
return `${name} ${ok}`;
|
|
5629
|
+
}
|
|
5630
|
+
case "compaction": {
|
|
5631
|
+
const before = ev["before"];
|
|
5632
|
+
const after = ev["after"];
|
|
5633
|
+
return `tokens ${before}\u2192${after}`;
|
|
5634
|
+
}
|
|
5635
|
+
case "error": {
|
|
5636
|
+
return color.red(String(ev["message"] ?? "(no message)"));
|
|
5637
|
+
}
|
|
5638
|
+
default: {
|
|
5639
|
+
return color.dim(JSON.stringify(ev).slice(0, 80));
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
function truncate(s, n) {
|
|
5644
|
+
if (s.length <= n) return s;
|
|
5645
|
+
return `${s.slice(0, n - 1)}\u2026`;
|
|
5646
|
+
}
|
|
5647
|
+
function helpCommand() {
|
|
5648
|
+
const lines = [
|
|
5649
|
+
color.bold("/collab \u2014 live collaboration (read-only observers)"),
|
|
5650
|
+
"",
|
|
5651
|
+
"Subcommands:",
|
|
5652
|
+
` ${color.cyan("status")} Show current session's collaboration status`,
|
|
5653
|
+
` ${color.cyan("invite")} Print a webui join link for a teammate`,
|
|
5654
|
+
` ${color.cyan("history [N]")} Show the last N events of this session (default 20, max 200)`,
|
|
5655
|
+
` ${color.cyan("annotations")} List open (unresolved) annotations from webui annotators`,
|
|
5656
|
+
` ${color.cyan("help")} Show this help`,
|
|
5657
|
+
"",
|
|
5658
|
+
color.dim(
|
|
5659
|
+
" The observer flow itself lives in the webui: open the URL printed"
|
|
5660
|
+
),
|
|
5661
|
+
color.dim(' by `invite` and click "Join as observer" on the panel above the chat.')
|
|
5662
|
+
];
|
|
5663
|
+
return { message: lines.join("\n") };
|
|
5664
|
+
}
|
|
5309
5665
|
|
|
5310
5666
|
// src/slash-commands/worktree.ts
|
|
5311
5667
|
function buildWorktreeCommand(opts) {
|
|
@@ -5524,6 +5880,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
5524
5880
|
buildAutoPhaseCommand(opts),
|
|
5525
5881
|
buildWorktreeCommand(opts),
|
|
5526
5882
|
buildSettingsCommand(opts),
|
|
5883
|
+
buildCollabCommand(opts),
|
|
5527
5884
|
buildStatuslineCommand({
|
|
5528
5885
|
cwd: opts.cwd,
|
|
5529
5886
|
hiddenItems: opts.statuslineHiddenItems ?? [],
|
|
@@ -5753,7 +6110,11 @@ async function bootConfig(flags) {
|
|
|
5753
6110
|
}
|
|
5754
6111
|
}
|
|
5755
6112
|
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
5756
|
-
|
|
6113
|
+
let config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
|
|
6114
|
+
const syncConfig = await configLoader.loadSyncConfig();
|
|
6115
|
+
if (syncConfig) {
|
|
6116
|
+
config = Object.freeze({ ...config, sync: syncConfig });
|
|
6117
|
+
}
|
|
5757
6118
|
return {
|
|
5758
6119
|
paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
|
|
5759
6120
|
config,
|
|
@@ -5906,7 +6267,7 @@ var ReadlineInputReader = class {
|
|
|
5906
6267
|
* user gets visual confirmation that bytes are arriving (especially on
|
|
5907
6268
|
* paste, which previously felt like nothing happened). Pasted chunks
|
|
5908
6269
|
* are echoed as a run of bullets, Backspace/DEL erases one bullet, and
|
|
5909
|
-
* Ctrl+U / Ctrl+
|
|
6270
|
+
* Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
|
|
5910
6271
|
* nothing to hide when piped, and echoing bullets to a file is noise.
|
|
5911
6272
|
*
|
|
5912
6273
|
* Returns the raw entered string (no trim — caller decides).
|
|
@@ -7844,9 +8205,9 @@ var updateCmd = async (args, deps) => {
|
|
|
7844
8205
|
cwd,
|
|
7845
8206
|
stdio: "pipe"
|
|
7846
8207
|
});
|
|
7847
|
-
let
|
|
8208
|
+
let _stderr = "";
|
|
7848
8209
|
child.stderr?.on("data", (d) => {
|
|
7849
|
-
|
|
8210
|
+
_stderr += d;
|
|
7850
8211
|
});
|
|
7851
8212
|
child.on("error", reject);
|
|
7852
8213
|
child.on("close", (code) => resolve4({ code: code ?? 0 }));
|
|
@@ -9080,6 +9441,167 @@ ${result.errors.length} error(s):
|
|
|
9080
9441
|
return 1;
|
|
9081
9442
|
}
|
|
9082
9443
|
};
|
|
9444
|
+
var replayCmd = async (args, deps) => {
|
|
9445
|
+
const wpaths = resolveWstackPaths({
|
|
9446
|
+
projectRoot: deps.projectRoot,
|
|
9447
|
+
userHome: deps.userHome
|
|
9448
|
+
});
|
|
9449
|
+
const store = new ReplayLogStore({ dir: wpaths.projectSessions });
|
|
9450
|
+
if (args[0] === "--list" || args[0] === "-l") {
|
|
9451
|
+
const all = await store.list();
|
|
9452
|
+
if (all.length === 0) {
|
|
9453
|
+
deps.renderer.write(
|
|
9454
|
+
color.dim("No replay logs recorded yet. Run with --record to start one.") + "\n"
|
|
9455
|
+
);
|
|
9456
|
+
return 0;
|
|
9457
|
+
}
|
|
9458
|
+
const lines2 = [
|
|
9459
|
+
color.bold(`${all.length} replay log(s)`),
|
|
9460
|
+
color.dim(" Sorted by session id. Each entry has at least one recorded request/response."),
|
|
9461
|
+
""
|
|
9462
|
+
];
|
|
9463
|
+
for (const r of all) {
|
|
9464
|
+
lines2.push(
|
|
9465
|
+
` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${color.dim(r.path)}`
|
|
9466
|
+
);
|
|
9467
|
+
}
|
|
9468
|
+
lines2.push("");
|
|
9469
|
+
lines2.push(
|
|
9470
|
+
color.dim(
|
|
9471
|
+
" Inspect: wstack replay <sessionId>"
|
|
9472
|
+
)
|
|
9473
|
+
);
|
|
9474
|
+
lines2.push(
|
|
9475
|
+
color.dim(
|
|
9476
|
+
" Replay: wstack --replay <sessionId>"
|
|
9477
|
+
)
|
|
9478
|
+
);
|
|
9479
|
+
deps.renderer.write(lines2.join("\n") + "\n");
|
|
9480
|
+
return 0;
|
|
9481
|
+
}
|
|
9482
|
+
const sessionId = args[0];
|
|
9483
|
+
if (!sessionId) {
|
|
9484
|
+
deps.renderer.writeError(
|
|
9485
|
+
"Usage: wstack replay <sessionId>\n wstack replay --list\n\nLists recorded provider responses for the session. To actually\nre-run the agent with frozen responses, use:\n wstack --replay " + color.cyan("<sessionId>") + "\n wstack --record # start a fresh recording\n"
|
|
9486
|
+
);
|
|
9487
|
+
return 1;
|
|
9488
|
+
}
|
|
9489
|
+
const entries = await store.load(sessionId);
|
|
9490
|
+
if (entries.length === 0) {
|
|
9491
|
+
deps.renderer.write(
|
|
9492
|
+
color.yellow(
|
|
9493
|
+
`No replay entries recorded for session ${sessionId}.
|
|
9494
|
+
Run a session with --record first, then --replay to re-execute it.`
|
|
9495
|
+
) + "\n"
|
|
9496
|
+
);
|
|
9497
|
+
return 0;
|
|
9498
|
+
}
|
|
9499
|
+
const lines = [
|
|
9500
|
+
color.bold(`Replay log for ${sessionId}`),
|
|
9501
|
+
` ${entries.length} recorded request/response pair(s)`,
|
|
9502
|
+
` Log file: ${color.dim(`${sessionId}.replay.jsonl`)}`,
|
|
9503
|
+
"",
|
|
9504
|
+
"To replay this session deterministically:",
|
|
9505
|
+
` ${color.cyan(`wstack --replay ${sessionId}`)}`,
|
|
9506
|
+
"",
|
|
9507
|
+
"Recorded timestamps (oldest first):"
|
|
9508
|
+
];
|
|
9509
|
+
for (const e of entries.slice(-10)) {
|
|
9510
|
+
lines.push(
|
|
9511
|
+
` ${color.dim(e.ts.slice(11, 19))} ${e.hash.slice(0, 16)}\u2026 ${e.request.model}`
|
|
9512
|
+
);
|
|
9513
|
+
}
|
|
9514
|
+
if (entries.length > 10) {
|
|
9515
|
+
lines.push(color.dim(` \u2026 and ${entries.length - 10} more`));
|
|
9516
|
+
}
|
|
9517
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
9518
|
+
return 0;
|
|
9519
|
+
};
|
|
9520
|
+
var auditCmd = async (args, deps) => {
|
|
9521
|
+
const wpaths = resolveWstackPaths({
|
|
9522
|
+
projectRoot: deps.projectRoot,
|
|
9523
|
+
userHome: deps.userHome
|
|
9524
|
+
});
|
|
9525
|
+
const log = new ToolAuditLog({ dir: wpaths.projectSessions });
|
|
9526
|
+
if (args[0] === "--list" || args[0] === "-l") {
|
|
9527
|
+
return listAudits(log, wpaths.projectSessions, deps);
|
|
9528
|
+
}
|
|
9529
|
+
const sessionId = args[0];
|
|
9530
|
+
if (!sessionId) {
|
|
9531
|
+
deps.renderer.writeError(
|
|
9532
|
+
"Usage: wstack audit <sessionId>\n wstack audit --list\n\nInspects a session's tamper-evident tool audit log. Each entry\nis chained to the previous via SHA-256; any post-hoc modification\nbreaks the chain. Use this to verify a session was not tampered\nwith after recording.\n"
|
|
9533
|
+
);
|
|
9534
|
+
return 1;
|
|
9535
|
+
}
|
|
9536
|
+
const entries = await log.load(sessionId);
|
|
9537
|
+
if (entries.length === 0) {
|
|
9538
|
+
deps.renderer.write(
|
|
9539
|
+
color.yellow(
|
|
9540
|
+
`No audit entries for session ${sessionId}. (The audit log is written when tools are recorded \u2014 check that the session ran with audit enabled.)`
|
|
9541
|
+
) + "\n"
|
|
9542
|
+
);
|
|
9543
|
+
return 0;
|
|
9544
|
+
}
|
|
9545
|
+
const verify = await log.verify(sessionId);
|
|
9546
|
+
const lines = [
|
|
9547
|
+
color.bold(`Audit log for ${sessionId}`),
|
|
9548
|
+
verify.ok ? ` ${color.green("\u2713 Verified")} ${entries.length} entries, chain intact` : ` ${color.red("\u2717 BROKEN")} at entry ${verify.brokenAt}: ${verify.reason}`,
|
|
9549
|
+
"",
|
|
9550
|
+
" Entries (oldest first):"
|
|
9551
|
+
];
|
|
9552
|
+
const cap = 12;
|
|
9553
|
+
for (const e of entries.slice(0, cap)) {
|
|
9554
|
+
const t = color.dim(e.ts.slice(11, 19));
|
|
9555
|
+
const idx = color.dim(`#${e.index}`.padStart(4));
|
|
9556
|
+
const tool = e.toolName;
|
|
9557
|
+
const err = e.isError ? color.red("(error)") : "";
|
|
9558
|
+
lines.push(
|
|
9559
|
+
` ${t} ${idx} ${color.cyan(tool.padEnd(14))} ${err} ${color.dim(e.hash.slice(0, 12) + "\u2026")}`
|
|
9560
|
+
);
|
|
9561
|
+
}
|
|
9562
|
+
if (entries.length > cap) {
|
|
9563
|
+
lines.push(color.dim(` \u2026 and ${entries.length - cap} more`));
|
|
9564
|
+
}
|
|
9565
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
9566
|
+
return verify.ok ? 0 : 1;
|
|
9567
|
+
};
|
|
9568
|
+
async function listAudits(log, dir, deps) {
|
|
9569
|
+
const fs22 = await import('fs/promises');
|
|
9570
|
+
let entries;
|
|
9571
|
+
try {
|
|
9572
|
+
entries = await fs22.readdir(dir);
|
|
9573
|
+
} catch {
|
|
9574
|
+
deps.renderer.write(
|
|
9575
|
+
color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
|
|
9576
|
+
);
|
|
9577
|
+
return 0;
|
|
9578
|
+
}
|
|
9579
|
+
const out = [];
|
|
9580
|
+
for (const name of entries) {
|
|
9581
|
+
if (!name.endsWith(".audit.jsonl")) continue;
|
|
9582
|
+
const sessionId = name.slice(0, -".audit.jsonl".length);
|
|
9583
|
+
const all = await log.load(sessionId);
|
|
9584
|
+
const verify = await log.verify(sessionId);
|
|
9585
|
+
out.push({ sessionId, entryCount: all.length, ok: verify.ok });
|
|
9586
|
+
}
|
|
9587
|
+
out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
9588
|
+
if (out.length === 0) {
|
|
9589
|
+
deps.renderer.write(color.dim("No audit logs recorded yet.") + "\n");
|
|
9590
|
+
return 0;
|
|
9591
|
+
}
|
|
9592
|
+
const lines = [
|
|
9593
|
+
color.bold(`${out.length} audit log(s)`),
|
|
9594
|
+
""
|
|
9595
|
+
];
|
|
9596
|
+
for (const r of out) {
|
|
9597
|
+
const status = r.ok ? color.green("\u2713 intact") : color.red("\u2717 broken");
|
|
9598
|
+
lines.push(
|
|
9599
|
+
` ${color.cyan(r.sessionId)} ${color.dim(`${r.entryCount} entries`)} ${status}`
|
|
9600
|
+
);
|
|
9601
|
+
}
|
|
9602
|
+
deps.renderer.write(lines.join("\n") + "\n");
|
|
9603
|
+
return 0;
|
|
9604
|
+
}
|
|
9083
9605
|
var toolsCmd = async (_args, deps) => {
|
|
9084
9606
|
const reg = deps.toolRegistry;
|
|
9085
9607
|
if (!reg) return 0;
|
|
@@ -9146,6 +9668,8 @@ var subcommands = {
|
|
|
9146
9668
|
sessions: sessionsCmd,
|
|
9147
9669
|
config: configCmd,
|
|
9148
9670
|
rewind: rewindCmd,
|
|
9671
|
+
replay: replayCmd,
|
|
9672
|
+
audit: auditCmd,
|
|
9149
9673
|
tools: toolsCmd,
|
|
9150
9674
|
skills: skillsCmd,
|
|
9151
9675
|
providers: providersCmd,
|
|
@@ -9182,22 +9706,22 @@ function fmtDuration(ms) {
|
|
|
9182
9706
|
const remMin = m - h * 60;
|
|
9183
9707
|
return `${h}h${remMin}m`;
|
|
9184
9708
|
}
|
|
9185
|
-
function fmtTaskResultLine(r,
|
|
9709
|
+
function fmtTaskResultLine(r, color41) {
|
|
9186
9710
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
9187
9711
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
9188
9712
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
9189
9713
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
9190
|
-
const errKindChip = errKind ?
|
|
9191
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
9714
|
+
const errKindChip = errKind ? color41.dim(` [${errKind}]`) : "";
|
|
9715
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color41.dim(errTail)}` : "";
|
|
9192
9716
|
switch (r.status) {
|
|
9193
9717
|
case "success":
|
|
9194
|
-
return { mark:
|
|
9718
|
+
return { mark: color41.green("\u2713"), stats, tail: "" };
|
|
9195
9719
|
case "timeout":
|
|
9196
|
-
return { mark:
|
|
9720
|
+
return { mark: color41.yellow("\u23F1"), stats: `${color41.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
9197
9721
|
case "stopped":
|
|
9198
|
-
return { mark:
|
|
9722
|
+
return { mark: color41.dim("\u2298"), stats: `${color41.dim("stopped")} ${stats}`, tail: errSnip };
|
|
9199
9723
|
case "failed":
|
|
9200
|
-
return { mark:
|
|
9724
|
+
return { mark: color41.red("\u2717"), stats: `${color41.red("failed")} ${stats}`, tail: errSnip };
|
|
9201
9725
|
}
|
|
9202
9726
|
}
|
|
9203
9727
|
|
|
@@ -9687,7 +10211,7 @@ async function runRepl(opts) {
|
|
|
9687
10211
|
);
|
|
9688
10212
|
interrupts = 0;
|
|
9689
10213
|
try {
|
|
9690
|
-
|
|
10214
|
+
await engine.runOneIteration();
|
|
9691
10215
|
const afterGoal = await loadGoalSafe(opts);
|
|
9692
10216
|
const last = afterGoal?.journal[afterGoal.journal.length - 1];
|
|
9693
10217
|
if (coord) {
|
|
@@ -9801,7 +10325,7 @@ ${color.dim(taskList2)}
|
|
|
9801
10325
|
}
|
|
9802
10326
|
}
|
|
9803
10327
|
}
|
|
9804
|
-
} catch (
|
|
10328
|
+
} catch (_runErr) {
|
|
9805
10329
|
opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
|
|
9806
10330
|
}
|
|
9807
10331
|
}
|
|
@@ -10501,7 +11025,7 @@ async function execute(deps) {
|
|
|
10501
11025
|
fleetStatusLine?.stop();
|
|
10502
11026
|
try {
|
|
10503
11027
|
stats.render(renderer);
|
|
10504
|
-
} catch (
|
|
11028
|
+
} catch (_err) {
|
|
10505
11029
|
}
|
|
10506
11030
|
await Promise.resolve(detachTodosCheckpoint?.()).catch(() => void 0);
|
|
10507
11031
|
await mcpRegistry.stopAll();
|
|
@@ -10526,6 +11050,15 @@ var MultiAgentHost = class {
|
|
|
10526
11050
|
/** Own FleetManager — created in buildDirector(), used for pending task
|
|
10527
11051
|
* tracking so status() can show descriptions without host-side state. */
|
|
10528
11052
|
fleetManager;
|
|
11053
|
+
/** Own FleetEmitTool — created in buildDirector() so subagents in director
|
|
11054
|
+
* mode can publish structured events (bug.found, refactor.plan,
|
|
11055
|
+
* critic.evaluation) onto the fleet bus without needing the tool registered
|
|
11056
|
+
* in the host's ToolRegistry. */
|
|
11057
|
+
fleetEmitTool;
|
|
11058
|
+
/** Own FleetStatusTool — created in buildDirector() so subagents in director
|
|
11059
|
+
* mode can read fleet state (subagent statuses, task progress) without the
|
|
11060
|
+
* tool being in the host's ToolRegistry. */
|
|
11061
|
+
fleetStatusTool;
|
|
10529
11062
|
/** Lazily built alongside the director — produces per-subagent JSONL
|
|
10530
11063
|
* writers under `<sessionsRoot>/<runId>/`. Null without sessionsRoot. */
|
|
10531
11064
|
sessionFactory;
|
|
@@ -10562,7 +11095,7 @@ var MultiAgentHost = class {
|
|
|
10562
11095
|
getDirector() {
|
|
10563
11096
|
return this.director;
|
|
10564
11097
|
}
|
|
10565
|
-
async ensureCoordinator(
|
|
11098
|
+
async ensureCoordinator(_config) {
|
|
10566
11099
|
await this.buildDirector();
|
|
10567
11100
|
}
|
|
10568
11101
|
async buildDirector() {
|
|
@@ -10629,6 +11162,15 @@ var MultiAgentHost = class {
|
|
|
10629
11162
|
totalExtensions: payload.totalExtensions
|
|
10630
11163
|
});
|
|
10631
11164
|
});
|
|
11165
|
+
this.director.fleet.filter("ctx.pct", (e) => {
|
|
11166
|
+
const payload = e.payload;
|
|
11167
|
+
this.deps.events.emit("subagent.ctx_pct", {
|
|
11168
|
+
subagentId: e.subagentId,
|
|
11169
|
+
load: payload.load,
|
|
11170
|
+
tokens: payload.tokens,
|
|
11171
|
+
maxContext: payload.maxContext
|
|
11172
|
+
});
|
|
11173
|
+
});
|
|
10632
11174
|
this.getCoordinator().on(
|
|
10633
11175
|
"task.assigned",
|
|
10634
11176
|
({
|
|
@@ -10642,9 +11184,19 @@ var MultiAgentHost = class {
|
|
|
10642
11184
|
});
|
|
10643
11185
|
}
|
|
10644
11186
|
);
|
|
11187
|
+
this.fleetEmitTool = makeFleetEmitTool(this.director);
|
|
11188
|
+
this.fleetStatusTool = makeFleetStatusTool(this.director);
|
|
10645
11189
|
const runner = await this.buildSubagentRunner(config);
|
|
10646
11190
|
this.getCoordinator().setRunner(runner);
|
|
10647
11191
|
}
|
|
11192
|
+
/**
|
|
11193
|
+
* Returns the FleetEmitTool for director-mode subagents, if the director
|
|
11194
|
+
* has been built. Used by makeSubagentFactory to inject the tool into
|
|
11195
|
+
* the filtered tool registry so collab session agents can emit fleet events.
|
|
11196
|
+
*/
|
|
11197
|
+
getFleetEmitTool() {
|
|
11198
|
+
return this.fleetEmitTool;
|
|
11199
|
+
}
|
|
10648
11200
|
/**
|
|
10649
11201
|
* Build a per-role subagent factory: given a SubagentConfig, construct a
|
|
10650
11202
|
* fresh, isolated Agent with the role's filtered tools and (when the config
|
|
@@ -10689,6 +11241,21 @@ var MultiAgentHost = class {
|
|
|
10689
11241
|
append: (ev) => parentSession.append({ ...ev })
|
|
10690
11242
|
};
|
|
10691
11243
|
}
|
|
11244
|
+
const tools = subCfg.tools ? [...subCfg.tools] : void 0;
|
|
11245
|
+
let injectedFleetEmit;
|
|
11246
|
+
let injectedFleetStatus;
|
|
11247
|
+
if (tools) {
|
|
11248
|
+
const emitIdx = tools.indexOf("fleet_emit");
|
|
11249
|
+
if (emitIdx >= 0 && this.fleetEmitTool) {
|
|
11250
|
+
tools.splice(emitIdx, 1);
|
|
11251
|
+
injectedFleetEmit = this.fleetEmitTool;
|
|
11252
|
+
}
|
|
11253
|
+
const statusIdx = tools.indexOf("fleet_status");
|
|
11254
|
+
if (statusIdx >= 0 && this.fleetStatusTool) {
|
|
11255
|
+
tools.splice(statusIdx, 1);
|
|
11256
|
+
injectedFleetStatus = this.fleetStatusTool;
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
10692
11259
|
const ctx = new Context({
|
|
10693
11260
|
systemPrompt: baseSystem,
|
|
10694
11261
|
provider,
|
|
@@ -10698,9 +11265,12 @@ var MultiAgentHost = class {
|
|
|
10698
11265
|
cwd: subCwd,
|
|
10699
11266
|
projectRoot: this.deps.projectRoot,
|
|
10700
11267
|
model: subCfg.model ?? config.model,
|
|
10701
|
-
tools: this.filterTools(
|
|
11268
|
+
tools: this.filterTools(tools)
|
|
10702
11269
|
});
|
|
10703
|
-
const
|
|
11270
|
+
const baseRegistry = this.subagentToolRegistry(tools);
|
|
11271
|
+
if (injectedFleetEmit) baseRegistry.register(injectedFleetEmit);
|
|
11272
|
+
if (injectedFleetStatus) baseRegistry.register(injectedFleetStatus);
|
|
11273
|
+
const toolExecutor = new ToolExecutor(baseRegistry, {
|
|
10704
11274
|
permissionPolicy: new AutoApprovePermissionPolicy(),
|
|
10705
11275
|
secretScrubber: this.deps.secretScrubber,
|
|
10706
11276
|
renderer: this.deps.renderer,
|
|
@@ -10712,7 +11282,7 @@ var MultiAgentHost = class {
|
|
|
10712
11282
|
});
|
|
10713
11283
|
const agent = new Agent({
|
|
10714
11284
|
container: this.deps.container,
|
|
10715
|
-
tools:
|
|
11285
|
+
tools: baseRegistry,
|
|
10716
11286
|
providers: this.deps.providerRegistry,
|
|
10717
11287
|
events,
|
|
10718
11288
|
pipelines: createDefaultPipelines(),
|
|
@@ -11111,9 +11681,9 @@ function makeConfirmAwaiter(reader) {
|
|
|
11111
11681
|
function stringifyInput(input) {
|
|
11112
11682
|
if (!input || typeof input !== "object") return "";
|
|
11113
11683
|
const obj = input;
|
|
11114
|
-
return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${
|
|
11684
|
+
return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate2(JSON.stringify(v), 80)}`).join(" ");
|
|
11115
11685
|
}
|
|
11116
|
-
function
|
|
11686
|
+
function truncate2(s, max) {
|
|
11117
11687
|
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
11118
11688
|
}
|
|
11119
11689
|
function hasDiff(input) {
|
|
@@ -11121,6 +11691,26 @@ function hasDiff(input) {
|
|
|
11121
11691
|
input && typeof input === "object" && "diff" in input
|
|
11122
11692
|
);
|
|
11123
11693
|
}
|
|
11694
|
+
function bindReplayToContainer(opts) {
|
|
11695
|
+
const { container, wpaths, sessionId, mode, logger } = opts;
|
|
11696
|
+
if (!opts.container.has(TOKENS.ProviderRunner)) {
|
|
11697
|
+
container.bind(TOKENS.ProviderRunner, () => ({
|
|
11698
|
+
run: (o) => runProviderWithRetry(o)
|
|
11699
|
+
}));
|
|
11700
|
+
}
|
|
11701
|
+
const inner = container.resolve(TOKENS.ProviderRunner);
|
|
11702
|
+
const log = new ReplayLogStore({ dir: wpaths.projectSessions });
|
|
11703
|
+
const wrapped = new ReplayProviderRunner(inner, {
|
|
11704
|
+
log,
|
|
11705
|
+
sessionId,
|
|
11706
|
+
mode,
|
|
11707
|
+
logger: logger ? {
|
|
11708
|
+
debug: (m) => logger.debug?.(m),
|
|
11709
|
+
warn: (m) => logger.warn?.(m)
|
|
11710
|
+
} : void 0
|
|
11711
|
+
});
|
|
11712
|
+
container.bind(TOKENS.ProviderRunner, () => wrapped);
|
|
11713
|
+
}
|
|
11124
11714
|
var SessionStats = class {
|
|
11125
11715
|
tokenCounter;
|
|
11126
11716
|
startedAt = Date.now();
|
|
@@ -11599,7 +12189,6 @@ async function setupCompaction(params) {
|
|
|
11599
12189
|
function createAgent(params) {
|
|
11600
12190
|
const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
|
|
11601
12191
|
const renderer = params.container.has(TOKENS.Renderer) ? params.container.resolve(TOKENS.Renderer) : void 0;
|
|
11602
|
-
params.container.resolve(TOKENS.Logger);
|
|
11603
12192
|
const toolExecutor = new ToolExecutor(params.tools, {
|
|
11604
12193
|
permissionPolicy: params.permissionPolicy ?? params.container.resolve(TOKENS.PermissionPolicy),
|
|
11605
12194
|
secretScrubber,
|
|
@@ -11983,7 +12572,6 @@ async function main(argv) {
|
|
|
11983
12572
|
wpaths,
|
|
11984
12573
|
cwd,
|
|
11985
12574
|
projectRoot,
|
|
11986
|
-
userHome,
|
|
11987
12575
|
flags,
|
|
11988
12576
|
positional,
|
|
11989
12577
|
modelsRegistry,
|
|
@@ -12025,6 +12613,22 @@ async function main(argv) {
|
|
|
12025
12613
|
compactor: { preserveK: config.context.preserveK, eliseThreshold: config.context.eliseThreshold },
|
|
12026
12614
|
bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0
|
|
12027
12615
|
});
|
|
12616
|
+
const replayFlag = flags["replay"];
|
|
12617
|
+
const recordFlag = flags["record"];
|
|
12618
|
+
if (typeof replayFlag === "string" || recordFlag === true) {
|
|
12619
|
+
const sessionId = typeof replayFlag === "string" ? replayFlag : `record-${Date.now()}`;
|
|
12620
|
+
const mode = recordFlag === true ? "record" : "replay";
|
|
12621
|
+
bindReplayToContainer({
|
|
12622
|
+
container,
|
|
12623
|
+
wpaths,
|
|
12624
|
+
sessionId,
|
|
12625
|
+
mode,
|
|
12626
|
+
logger
|
|
12627
|
+
});
|
|
12628
|
+
logger.info(
|
|
12629
|
+
`replay: ProviderRunner bound in '${mode}' mode for session ${sessionId}`
|
|
12630
|
+
);
|
|
12631
|
+
}
|
|
12028
12632
|
const configStore = container.resolve(TOKENS.ConfigStore);
|
|
12029
12633
|
container.bind(TOKENS.PathResolver, () => pathResolver);
|
|
12030
12634
|
container.bind(TOKENS.Renderer, () => renderer);
|
|
@@ -12092,7 +12696,7 @@ async function main(argv) {
|
|
|
12092
12696
|
}
|
|
12093
12697
|
const events = new EventBus();
|
|
12094
12698
|
events.setLogger(logger);
|
|
12095
|
-
const { metricsSink, healthRegistry
|
|
12699
|
+
const { metricsSink, healthRegistry } = (() => {
|
|
12096
12700
|
const ms = setupMetrics({ flags, wpaths, events, logger, config: { provider: config.provider, model: config.model } });
|
|
12097
12701
|
return ms;
|
|
12098
12702
|
})();
|
|
@@ -12171,7 +12775,6 @@ async function main(argv) {
|
|
|
12171
12775
|
});
|
|
12172
12776
|
const session = sessResult.session;
|
|
12173
12777
|
sessionRef.current = session;
|
|
12174
|
-
sessResult.restoredMessages;
|
|
12175
12778
|
const context = sessResult.context;
|
|
12176
12779
|
const attachments = sessResult.attachments;
|
|
12177
12780
|
const recoveryLock = sessResult.recoveryLock;
|