@wrongstack/cli 0.1.10 → 0.2.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/README.md +19 -3
- package/dist/index.js +580 -63
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { color, DefaultPathResolver, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultModeStore, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, makeDirectorSessionFactory,
|
|
2
|
+
import { color, DefaultPathResolver, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultModeStore, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, InputBuilder, decryptConfigSecrets, encryptConfigSecrets, DefaultPluginAPI } from '@wrongstack/core';
|
|
3
3
|
import * as fs3 from 'fs/promises';
|
|
4
4
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
@@ -1470,7 +1470,7 @@ function buildStatsCommand(opts) {
|
|
|
1470
1470
|
function buildFleetCommand(opts) {
|
|
1471
1471
|
return {
|
|
1472
1472
|
name: "fleet",
|
|
1473
|
-
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|help]",
|
|
1473
|
+
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|retry [taskId]|log <id>|stream on|off|help]",
|
|
1474
1474
|
help: [
|
|
1475
1475
|
"Usage:",
|
|
1476
1476
|
" /fleet Show fleet status (alias for /fleet status).",
|
|
@@ -1478,6 +1478,13 @@ function buildFleetCommand(opts) {
|
|
|
1478
1478
|
" /fleet usage Per-subagent runtime cost.",
|
|
1479
1479
|
" /fleet kill <id> Terminate a running subagent.",
|
|
1480
1480
|
" /fleet manifest Print the director manifest.",
|
|
1481
|
+
" /fleet retry List interrupted tasks from the last run.",
|
|
1482
|
+
" /fleet retry <taskId> Re-spawn the matching subagent and re-assign the task.",
|
|
1483
|
+
" /fleet retry all Re-assign every interrupted task at once.",
|
|
1484
|
+
" /fleet log List subagent transcripts available on disk.",
|
|
1485
|
+
" /fleet log <id> Print a compact summary of a subagent transcript.",
|
|
1486
|
+
" /fleet log <id> raw Dump the full per-subagent JSONL.",
|
|
1487
|
+
" /fleet stream on|off Show/hide subagent activity in the main history.",
|
|
1481
1488
|
" /fleet help Show this help."
|
|
1482
1489
|
].join("\n"),
|
|
1483
1490
|
async run(args) {
|
|
@@ -1496,6 +1503,38 @@ function buildFleetCommand(opts) {
|
|
|
1496
1503
|
if (!target) return { message: "Usage: /fleet kill <subagent-id>" };
|
|
1497
1504
|
return { message: await opts.onFleet("kill", target) };
|
|
1498
1505
|
}
|
|
1506
|
+
case "retry": {
|
|
1507
|
+
if (!opts.onFleetRetry) {
|
|
1508
|
+
return { message: "Retry is only available when director mode is active." };
|
|
1509
|
+
}
|
|
1510
|
+
const msg = await opts.onFleetRetry(target);
|
|
1511
|
+
return { message: msg };
|
|
1512
|
+
}
|
|
1513
|
+
case "log": {
|
|
1514
|
+
if (!opts.onFleetLog) {
|
|
1515
|
+
return { message: "Log inspection is only available when a fleet root is configured." };
|
|
1516
|
+
}
|
|
1517
|
+
const [id, ...modeRest] = rest;
|
|
1518
|
+
const mode = modeRest.join(" ").trim() === "raw" ? "raw" : "summary";
|
|
1519
|
+
return { message: await opts.onFleetLog(id, mode) };
|
|
1520
|
+
}
|
|
1521
|
+
case "stream": {
|
|
1522
|
+
const ctrl = opts.fleetStreamController;
|
|
1523
|
+
if (!ctrl) {
|
|
1524
|
+
return { message: "Stream toggle is only available in the TUI." };
|
|
1525
|
+
}
|
|
1526
|
+
const arg = (target ?? "").toLowerCase();
|
|
1527
|
+
if (arg === "" || arg === "status") {
|
|
1528
|
+
return { message: `Fleet streaming is ${ctrl.enabled ? "on" : "off"}.` };
|
|
1529
|
+
}
|
|
1530
|
+
if (arg !== "on" && arg !== "off") {
|
|
1531
|
+
return { message: "Usage: /fleet stream on|off" };
|
|
1532
|
+
}
|
|
1533
|
+
const enabled = arg === "on";
|
|
1534
|
+
ctrl.setEnabled(enabled);
|
|
1535
|
+
ctrl.enabled = enabled;
|
|
1536
|
+
return { message: `Fleet streaming ${enabled ? "enabled" : "disabled"}.` };
|
|
1537
|
+
}
|
|
1499
1538
|
case "help":
|
|
1500
1539
|
case "?":
|
|
1501
1540
|
return {
|
|
@@ -1713,6 +1752,66 @@ function buildMetricsCommand(opts) {
|
|
|
1713
1752
|
}
|
|
1714
1753
|
};
|
|
1715
1754
|
}
|
|
1755
|
+
function buildPlanCommand(opts) {
|
|
1756
|
+
return {
|
|
1757
|
+
name: "plan",
|
|
1758
|
+
description: "Strategic plan board: /plan [show|add <title>|done <id|#>|remove <id|#>|clear]",
|
|
1759
|
+
async run(args) {
|
|
1760
|
+
const planPath = opts.planPath;
|
|
1761
|
+
if (!planPath) return { message: "Plan storage is not configured for this session." };
|
|
1762
|
+
const ctx = opts.context;
|
|
1763
|
+
const sessionId = ctx?.session.id ?? "unknown";
|
|
1764
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
1765
|
+
const restJoined = rest.join(" ").trim();
|
|
1766
|
+
const plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
1767
|
+
switch (verb) {
|
|
1768
|
+
case "":
|
|
1769
|
+
case "show":
|
|
1770
|
+
case "list": {
|
|
1771
|
+
return { message: formatPlan(plan) };
|
|
1772
|
+
}
|
|
1773
|
+
case "add": {
|
|
1774
|
+
if (!restJoined) return { message: "Usage: /plan add <title>" };
|
|
1775
|
+
const { plan: updated, item } = addPlanItem(plan, restJoined);
|
|
1776
|
+
await savePlan(planPath, updated);
|
|
1777
|
+
return { message: `Added: ${item.title}
|
|
1778
|
+
${formatPlan(updated)}` };
|
|
1779
|
+
}
|
|
1780
|
+
case "start":
|
|
1781
|
+
case "progress": {
|
|
1782
|
+
if (!restJoined) return { message: "Usage: /plan start <id|index>" };
|
|
1783
|
+
const updated = setPlanItemStatus(plan, restJoined, "in_progress");
|
|
1784
|
+
await savePlan(planPath, updated);
|
|
1785
|
+
return { message: formatPlan(updated) };
|
|
1786
|
+
}
|
|
1787
|
+
case "done":
|
|
1788
|
+
case "complete": {
|
|
1789
|
+
if (!restJoined) return { message: "Usage: /plan done <id|index>" };
|
|
1790
|
+
const updated = setPlanItemStatus(plan, restJoined, "done");
|
|
1791
|
+
await savePlan(planPath, updated);
|
|
1792
|
+
return { message: formatPlan(updated) };
|
|
1793
|
+
}
|
|
1794
|
+
case "remove":
|
|
1795
|
+
case "delete":
|
|
1796
|
+
case "rm": {
|
|
1797
|
+
if (!restJoined) return { message: "Usage: /plan remove <id|index>" };
|
|
1798
|
+
const updated = removePlanItem(plan, restJoined);
|
|
1799
|
+
await savePlan(planPath, updated);
|
|
1800
|
+
return { message: formatPlan(updated) };
|
|
1801
|
+
}
|
|
1802
|
+
case "clear": {
|
|
1803
|
+
const updated = clearPlan(plan);
|
|
1804
|
+
await savePlan(planPath, updated);
|
|
1805
|
+
return { message: "Plan cleared." };
|
|
1806
|
+
}
|
|
1807
|
+
default:
|
|
1808
|
+
return {
|
|
1809
|
+
message: `Unknown subcommand "${verb}". Try: show | add <title> | start <id|#> | done <id|#> | remove <id|#> | clear`
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1716
1815
|
function buildSaveCommand(opts) {
|
|
1717
1816
|
return {
|
|
1718
1817
|
name: "save",
|
|
@@ -1922,6 +2021,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
1922
2021
|
buildHealthCommand(opts),
|
|
1923
2022
|
buildMemoryCommand(opts),
|
|
1924
2023
|
buildTodosCommand(opts),
|
|
2024
|
+
buildPlanCommand(opts),
|
|
1925
2025
|
buildSaveCommand(opts),
|
|
1926
2026
|
buildLoadCommand(opts),
|
|
1927
2027
|
buildExitCommand(opts)
|
|
@@ -3704,6 +3804,35 @@ function fmtTok(n) {
|
|
|
3704
3804
|
function patchConfig(base, patch) {
|
|
3705
3805
|
return Object.freeze({ ...base, ...patch });
|
|
3706
3806
|
}
|
|
3807
|
+
function fmtDuration(ms) {
|
|
3808
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3809
|
+
const s = ms / 1e3;
|
|
3810
|
+
if (s < 60) return `${s.toFixed(s < 10 ? 1 : 0)}s`;
|
|
3811
|
+
const m = Math.floor(s / 60);
|
|
3812
|
+
const remSec = Math.round(s - m * 60);
|
|
3813
|
+
if (m < 60) return remSec === 0 ? `${m}m` : `${m}m${remSec}s`;
|
|
3814
|
+
const h = Math.floor(m / 60);
|
|
3815
|
+
const remMin = m - h * 60;
|
|
3816
|
+
return `${h}h${remMin}m`;
|
|
3817
|
+
}
|
|
3818
|
+
function fmtTaskResultLine(r, color28) {
|
|
3819
|
+
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
3820
|
+
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
3821
|
+
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
3822
|
+
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
3823
|
+
const errKindChip = errKind ? color28.dim(` [${errKind}]`) : "";
|
|
3824
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color28.dim(errTail)}` : "";
|
|
3825
|
+
switch (r.status) {
|
|
3826
|
+
case "success":
|
|
3827
|
+
return { mark: color28.green("\u2713"), stats, tail: "" };
|
|
3828
|
+
case "timeout":
|
|
3829
|
+
return { mark: color28.yellow("\u23F1"), stats: `${color28.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
3830
|
+
case "stopped":
|
|
3831
|
+
return { mark: color28.dim("\u2298"), stats: `${color28.dim("stopped")} ${stats}`, tail: errSnip };
|
|
3832
|
+
case "failed":
|
|
3833
|
+
return { mark: color28.red("\u2717"), stats: `${color28.red("failed")} ${stats}`, tail: errSnip };
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3707
3836
|
|
|
3708
3837
|
// src/boot.ts
|
|
3709
3838
|
function resolveBundledSkillsDir() {
|
|
@@ -4013,7 +4142,10 @@ async function execute(deps) {
|
|
|
4013
4142
|
savedProviderCfg,
|
|
4014
4143
|
resolvedProvider,
|
|
4015
4144
|
getPickableProviders,
|
|
4016
|
-
switchProviderAndModel
|
|
4145
|
+
switchProviderAndModel,
|
|
4146
|
+
director,
|
|
4147
|
+
fleetRoster,
|
|
4148
|
+
fleetStreamController
|
|
4017
4149
|
} = deps;
|
|
4018
4150
|
let code = 0;
|
|
4019
4151
|
try {
|
|
@@ -4021,6 +4153,11 @@ async function execute(deps) {
|
|
|
4021
4153
|
if (promptFlag) {
|
|
4022
4154
|
positional.unshift(promptFlag);
|
|
4023
4155
|
}
|
|
4156
|
+
const goalFlag = typeof flags["goal"] === "string" ? flags["goal"] : void 0;
|
|
4157
|
+
const askFlag = typeof flags["ask"] === "string" ? flags["ask"] : void 0;
|
|
4158
|
+
if ((goalFlag || askFlag) && positional.length === 0 && !promptFlag) {
|
|
4159
|
+
flags.tui = true;
|
|
4160
|
+
}
|
|
4024
4161
|
if (positional.length > 0 || promptFlag) {
|
|
4025
4162
|
const query = positional.join(" ");
|
|
4026
4163
|
const ctrl = new AbortController();
|
|
@@ -4107,7 +4244,14 @@ async function execute(deps) {
|
|
|
4107
4244
|
getPickableProviders,
|
|
4108
4245
|
switchProviderAndModel,
|
|
4109
4246
|
effectiveMaxContext,
|
|
4110
|
-
|
|
4247
|
+
// Default OFF so the terminal's native scrollback works for chat
|
|
4248
|
+
// history out of the box (mouse wheel / Shift+PgUp). Users who hit
|
|
4249
|
+
// resize/overlay-leak artifacts can opt back into alt-screen with
|
|
4250
|
+
// `--alt-screen` or `/altscreen on`. `--no-alt-screen` still wins
|
|
4251
|
+
// when both are passed.
|
|
4252
|
+
altScreen: flags["alt-screen"] === true && flags["no-alt-screen"] !== true,
|
|
4253
|
+
director,
|
|
4254
|
+
fleetRoster,
|
|
4111
4255
|
onAfterExit: () => {
|
|
4112
4256
|
process.stdout.write(
|
|
4113
4257
|
color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
|
|
@@ -4116,7 +4260,10 @@ async function execute(deps) {
|
|
|
4116
4260
|
onClearHistory: (dispatch) => {
|
|
4117
4261
|
dispatch({ type: "clearHistory" });
|
|
4118
4262
|
dispatch({ type: "resetContextChip" });
|
|
4119
|
-
}
|
|
4263
|
+
},
|
|
4264
|
+
fleetStreamController,
|
|
4265
|
+
initialGoal: goalFlag,
|
|
4266
|
+
initialAsk: askFlag
|
|
4120
4267
|
});
|
|
4121
4268
|
} finally {
|
|
4122
4269
|
renderer.setSilent(false);
|
|
@@ -4185,6 +4332,13 @@ var MultiAgentHost = class {
|
|
|
4185
4332
|
pending = /* @__PURE__ */ new Map();
|
|
4186
4333
|
results = [];
|
|
4187
4334
|
opts;
|
|
4335
|
+
/**
|
|
4336
|
+
* Populated by `promoteToDirector` when it refuses to promote (typically
|
|
4337
|
+
* because a non-director coordinator is already running). The delegate
|
|
4338
|
+
* tool reads this through `getPromotionBlockReason` to render an
|
|
4339
|
+
* actionable error instead of a generic "could not activate director".
|
|
4340
|
+
*/
|
|
4341
|
+
promotionBlockReason = null;
|
|
4188
4342
|
/**
|
|
4189
4343
|
* Force the lazy build path to run *now* and return the live Director,
|
|
4190
4344
|
* or null when director mode is off. Used by the CLI to register the
|
|
@@ -4208,8 +4362,64 @@ var MultiAgentHost = class {
|
|
|
4208
4362
|
directorRunId: this.opts.directorRunId
|
|
4209
4363
|
});
|
|
4210
4364
|
}
|
|
4365
|
+
const coordinatorConfig = {
|
|
4366
|
+
coordinatorId: randomUUID(),
|
|
4367
|
+
doneCondition: { type: "all_tasks_done" },
|
|
4368
|
+
maxConcurrent: 8
|
|
4369
|
+
// No defaultBudget. Caps land on a subagent ONLY when the
|
|
4370
|
+
// orchestrator (delegate-tool / spawn_subagent) or the user
|
|
4371
|
+
// (CLI flag) sets them explicitly. The prior defaults
|
|
4372
|
+
// (1000 tools / 200 iter / 4h) silently killed long autonomous
|
|
4373
|
+
// runs; for a "work until done" director we want no implicit
|
|
4374
|
+
// ceilings. The orchestrator can still cap a single subagent
|
|
4375
|
+
// by passing maxToolCalls/maxIterations through the spawn tool.
|
|
4376
|
+
};
|
|
4377
|
+
if (this.opts.directorMode) {
|
|
4378
|
+
this.director = new Director({
|
|
4379
|
+
config: coordinatorConfig,
|
|
4380
|
+
manifestPath: this.opts.manifestPath,
|
|
4381
|
+
sharedScratchpadPath: this.opts.sharedScratchpadPath,
|
|
4382
|
+
stateCheckpointPath: this.opts.stateCheckpointPath,
|
|
4383
|
+
sessionWriter: this.opts.sessionWriter,
|
|
4384
|
+
// Autonomy: allow nested directors a few levels deep. Default
|
|
4385
|
+
// is 2 (root + one tier of workers), which trips the moment a
|
|
4386
|
+
// worker tries to recurse into "let me delegate the parser
|
|
4387
|
+
// analysis to a tighter specialist". 5 lets the director
|
|
4388
|
+
// structure work as deeply as the task requires without us
|
|
4389
|
+
// having to pass a flag every time.
|
|
4390
|
+
maxSpawnDepth: 5
|
|
4391
|
+
});
|
|
4392
|
+
this.director.on("task.completed", ({ task, result }) => {
|
|
4393
|
+
this.results.push(result);
|
|
4394
|
+
this.pending.delete(task.id);
|
|
4395
|
+
this.emitLifecycleCompleted(task.id, result);
|
|
4396
|
+
});
|
|
4397
|
+
this.coordinator = this.director.coordinator;
|
|
4398
|
+
} else {
|
|
4399
|
+
this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, {});
|
|
4400
|
+
this.coordinator.on(
|
|
4401
|
+
"task.completed",
|
|
4402
|
+
({ task, result }) => {
|
|
4403
|
+
this.results.push(result);
|
|
4404
|
+
this.pending.delete(task.id);
|
|
4405
|
+
this.emitLifecycleCompleted(task.id, result);
|
|
4406
|
+
}
|
|
4407
|
+
);
|
|
4408
|
+
}
|
|
4409
|
+
this.coordinator.on(
|
|
4410
|
+
"task.assigned",
|
|
4411
|
+
({ task, subagentId }) => {
|
|
4412
|
+
this.deps.events.emit("subagent.task_started", {
|
|
4413
|
+
subagentId,
|
|
4414
|
+
taskId: task.id,
|
|
4415
|
+
description: task.description
|
|
4416
|
+
});
|
|
4417
|
+
}
|
|
4418
|
+
);
|
|
4211
4419
|
const runner = this.buildSubagentRunner(config);
|
|
4212
|
-
|
|
4420
|
+
const innerCoord = this.opts.directorMode ? this.director.coordinator : this.coordinator;
|
|
4421
|
+
innerCoord.setRunner(runner);
|
|
4422
|
+
return this.coordinator;
|
|
4213
4423
|
}
|
|
4214
4424
|
/**
|
|
4215
4425
|
* Build the per-subagent runner: agent factory → runner. Extracted so
|
|
@@ -4224,7 +4434,12 @@ var MultiAgentHost = class {
|
|
|
4224
4434
|
projectRoot: this.deps.projectRoot,
|
|
4225
4435
|
tools: this.filterTools(subCfg.tools),
|
|
4226
4436
|
model: subCfg.model ?? config.model,
|
|
4227
|
-
provider: subCfg.provider ?? config.provider
|
|
4437
|
+
provider: subCfg.provider ?? config.provider,
|
|
4438
|
+
// Tell the builder this is a subagent build — skips the host's
|
|
4439
|
+
// plan injection so each subagent gets a clean, task-scoped
|
|
4440
|
+
// prompt instead of inheriting strategic context that's
|
|
4441
|
+
// meaningless to a single delegated subtask.
|
|
4442
|
+
subagent: true
|
|
4228
4443
|
});
|
|
4229
4444
|
let subSession;
|
|
4230
4445
|
if (this.sessionFactory) {
|
|
@@ -4259,46 +4474,34 @@ var MultiAgentHost = class {
|
|
|
4259
4474
|
providers: this.deps.providerRegistry,
|
|
4260
4475
|
events,
|
|
4261
4476
|
pipelines: createDefaultPipelines(),
|
|
4262
|
-
context: ctx
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
/**
|
|
4269
|
-
* Build the coordinator (or Director wrapper) with task-completion
|
|
4270
|
-
* drain wired into the host's result buffer.
|
|
4271
|
-
*/
|
|
4272
|
-
buildCoordinator(runner) {
|
|
4273
|
-
const coordinatorConfig = {
|
|
4274
|
-
coordinatorId: randomUUID(),
|
|
4275
|
-
doneCondition: { type: "all_tasks_done" },
|
|
4276
|
-
maxConcurrent: 2,
|
|
4277
|
-
defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
|
|
4278
|
-
};
|
|
4279
|
-
if (this.opts.directorMode) {
|
|
4280
|
-
this.director = new Director({
|
|
4281
|
-
config: coordinatorConfig,
|
|
4282
|
-
runner,
|
|
4283
|
-
manifestPath: this.opts.manifestPath,
|
|
4284
|
-
sharedScratchpadPath: this.opts.sharedScratchpadPath
|
|
4477
|
+
context: ctx,
|
|
4478
|
+
// Subagents cannot answer interactive permission prompts — they
|
|
4479
|
+
// run under a director, not the user. Auto-approve everything
|
|
4480
|
+
// (except tool-level hard denies); the user already authorized
|
|
4481
|
+
// the work when they invoked the leader.
|
|
4482
|
+
permissionPolicy: new AutoApprovePermissionPolicy()
|
|
4285
4483
|
});
|
|
4286
|
-
this.
|
|
4287
|
-
|
|
4288
|
-
|
|
4484
|
+
const hostEvents = this.deps.events;
|
|
4485
|
+
const offToolBridge = events.on("tool.executed", (e) => {
|
|
4486
|
+
hostEvents.emit("subagent.tool_executed", {
|
|
4487
|
+
subagentId: subCfg.id ?? subCfg.name ?? "subagent",
|
|
4488
|
+
name: e.name,
|
|
4489
|
+
durationMs: e.durationMs,
|
|
4490
|
+
ok: e.ok,
|
|
4491
|
+
input: e.input,
|
|
4492
|
+
outputBytes: e.outputBytes
|
|
4493
|
+
});
|
|
4289
4494
|
});
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
);
|
|
4301
|
-
return this.coordinator;
|
|
4495
|
+
const dispose = async () => {
|
|
4496
|
+
offToolBridge();
|
|
4497
|
+
try {
|
|
4498
|
+
await subSession.close?.();
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
return { agent, events, dispose };
|
|
4503
|
+
};
|
|
4504
|
+
return makeAgentSubagentRunner({ factory, fleetBus: this.director?.fleet });
|
|
4302
4505
|
}
|
|
4303
4506
|
/**
|
|
4304
4507
|
* Build a Provider for a subagent. When `overrideId` is supplied (from
|
|
@@ -4348,21 +4551,30 @@ var MultiAgentHost = class {
|
|
|
4348
4551
|
const subagentConfig = {
|
|
4349
4552
|
name: opts?.name ?? "adhoc",
|
|
4350
4553
|
role: "general",
|
|
4351
|
-
maxToolCalls: 20,
|
|
4352
|
-
maxIterations: 20,
|
|
4353
4554
|
provider: opts?.provider,
|
|
4354
4555
|
model: opts?.model,
|
|
4355
4556
|
tools: opts?.tools
|
|
4356
4557
|
};
|
|
4558
|
+
const transcriptPath = this.sessionFactory ? path13.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
4357
4559
|
if (this.director) {
|
|
4358
4560
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
4359
4561
|
const taskId2 = randomUUID();
|
|
4360
4562
|
this.pending.set(taskId2, { description, subagentId });
|
|
4563
|
+
this.deps.events.emit("subagent.spawned", {
|
|
4564
|
+
subagentId,
|
|
4565
|
+
taskId: taskId2,
|
|
4566
|
+
name: subagentConfig.name,
|
|
4567
|
+
provider: opts?.provider,
|
|
4568
|
+
model: opts?.model,
|
|
4569
|
+
description,
|
|
4570
|
+
transcriptPath
|
|
4571
|
+
});
|
|
4361
4572
|
await this.director.assign({
|
|
4362
4573
|
id: taskId2,
|
|
4363
4574
|
description,
|
|
4364
|
-
subagentId
|
|
4365
|
-
maxToolCalls
|
|
4575
|
+
subagentId
|
|
4576
|
+
// No maxToolCalls — same reasoning as the spawn config above.
|
|
4577
|
+
// The director / orchestrator owns the budget decision.
|
|
4366
4578
|
});
|
|
4367
4579
|
return { subagentId, taskId: taskId2 };
|
|
4368
4580
|
}
|
|
@@ -4370,14 +4582,42 @@ var MultiAgentHost = class {
|
|
|
4370
4582
|
const spawned = await coord.spawn(subagentConfig);
|
|
4371
4583
|
const taskId = randomUUID();
|
|
4372
4584
|
this.pending.set(taskId, { description, subagentId: spawned.subagentId });
|
|
4585
|
+
this.deps.events.emit("subagent.spawned", {
|
|
4586
|
+
subagentId: spawned.subagentId,
|
|
4587
|
+
taskId,
|
|
4588
|
+
name: subagentConfig.name,
|
|
4589
|
+
provider: opts?.provider,
|
|
4590
|
+
model: opts?.model,
|
|
4591
|
+
description,
|
|
4592
|
+
transcriptPath
|
|
4593
|
+
});
|
|
4373
4594
|
await coord.assign({
|
|
4374
4595
|
id: taskId,
|
|
4375
4596
|
description,
|
|
4376
|
-
subagentId: spawned.subagentId
|
|
4377
|
-
maxToolCalls
|
|
4597
|
+
subagentId: spawned.subagentId
|
|
4598
|
+
// No maxToolCalls — see comment on the director branch above.
|
|
4378
4599
|
});
|
|
4379
4600
|
return { subagentId: spawned.subagentId, taskId };
|
|
4380
4601
|
}
|
|
4602
|
+
/**
|
|
4603
|
+
* Relay a `task.completed` notification (from either the Director or
|
|
4604
|
+
* the raw coordinator) to the EventBus so non-director TUIs and any
|
|
4605
|
+
* other observer can react. We forward the full result shape rather
|
|
4606
|
+
* than mutating the existing `task.completed` schema — coordination
|
|
4607
|
+
* code already binds to that event, and adding subscribers there
|
|
4608
|
+
* would change ordering semantics for those listeners.
|
|
4609
|
+
*/
|
|
4610
|
+
emitLifecycleCompleted(taskId, result) {
|
|
4611
|
+
this.deps.events.emit("subagent.task_completed", {
|
|
4612
|
+
subagentId: result.subagentId,
|
|
4613
|
+
taskId,
|
|
4614
|
+
status: result.status,
|
|
4615
|
+
iterations: result.iterations,
|
|
4616
|
+
toolCalls: result.toolCalls,
|
|
4617
|
+
durationMs: result.durationMs,
|
|
4618
|
+
error: result.error
|
|
4619
|
+
});
|
|
4620
|
+
}
|
|
4381
4621
|
status() {
|
|
4382
4622
|
const pending = Array.from(this.pending.entries()).map(([taskId, v]) => ({
|
|
4383
4623
|
taskId,
|
|
@@ -4459,6 +4699,10 @@ var MultiAgentHost = class {
|
|
|
4459
4699
|
async promoteToDirector() {
|
|
4460
4700
|
if (this.director) return this.director;
|
|
4461
4701
|
if (this.coordinator) {
|
|
4702
|
+
const status = this.coordinator.getStatus();
|
|
4703
|
+
const running = status.subagents.filter((s) => s.status === "running").length;
|
|
4704
|
+
const idle = status.subagents.filter((s) => s.status === "idle").length;
|
|
4705
|
+
this.promotionBlockReason = `Cannot promote to director: a non-director coordinator is already in use (${running} running, ${idle} idle, ${status.pendingTasks} pending tasks). Stop the existing subagents with /fleet kill <id> or wait for them to finish, then retry \u2014 or restart wstack with --director to start in director mode.`;
|
|
4462
4706
|
return null;
|
|
4463
4707
|
}
|
|
4464
4708
|
this.opts.directorMode = true;
|
|
@@ -4471,6 +4715,9 @@ var MultiAgentHost = class {
|
|
|
4471
4715
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
4472
4716
|
this.opts.sessionsRoot = path13.join(this.opts.fleetRoot, "subagents");
|
|
4473
4717
|
}
|
|
4718
|
+
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
4719
|
+
this.opts.stateCheckpointPath = path13.join(this.opts.fleetRoot, "director-state.json");
|
|
4720
|
+
}
|
|
4474
4721
|
await this.ensureDirector();
|
|
4475
4722
|
return this.director ?? null;
|
|
4476
4723
|
}
|
|
@@ -4482,6 +4729,16 @@ var MultiAgentHost = class {
|
|
|
4482
4729
|
isDirectorMode() {
|
|
4483
4730
|
return !!this.director;
|
|
4484
4731
|
}
|
|
4732
|
+
/**
|
|
4733
|
+
* Why the most recent `promoteToDirector` call returned null. Cleared
|
|
4734
|
+
* implicitly on the next successful promotion. The delegate tool reads
|
|
4735
|
+
* this so the LLM sees the actual blocker (e.g. "3 running subagents,
|
|
4736
|
+
* wait or /fleet kill") instead of a generic "Director could not be
|
|
4737
|
+
* activated" message that gives no path forward.
|
|
4738
|
+
*/
|
|
4739
|
+
getPromotionBlockReason() {
|
|
4740
|
+
return this.promotionBlockReason;
|
|
4741
|
+
}
|
|
4485
4742
|
/**
|
|
4486
4743
|
* Terminate a single subagent. Returns true when the subagent existed
|
|
4487
4744
|
* (regardless of whether stop() succeeded or it was already idle),
|
|
@@ -4844,6 +5101,7 @@ async function main(argv) {
|
|
|
4844
5101
|
supportsVision: resolvedModel.capabilities.vision,
|
|
4845
5102
|
supportsReasoning: resolvedModel.capabilities.reasoning
|
|
4846
5103
|
} : void 0;
|
|
5104
|
+
const sessionRef = {};
|
|
4847
5105
|
container.bind(
|
|
4848
5106
|
TOKENS.SystemPromptBuilder,
|
|
4849
5107
|
() => new DefaultSystemPromptBuilder({
|
|
@@ -4852,7 +5110,10 @@ async function main(argv) {
|
|
|
4852
5110
|
modeStore,
|
|
4853
5111
|
modeId,
|
|
4854
5112
|
modePrompt,
|
|
4855
|
-
modelCapabilities
|
|
5113
|
+
modelCapabilities,
|
|
5114
|
+
// Reads the ref each time — returns undefined until the session
|
|
5115
|
+
// is created, then resolves to the per-session plan JSON path.
|
|
5116
|
+
planPath: () => sessionRef.current ? path13.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
4856
5117
|
})
|
|
4857
5118
|
);
|
|
4858
5119
|
container.bind(TOKENS.Renderer, () => renderer);
|
|
@@ -5091,6 +5352,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5091
5352
|
provider: config.provider
|
|
5092
5353
|
});
|
|
5093
5354
|
}
|
|
5355
|
+
sessionRef.current = session;
|
|
5094
5356
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
5095
5357
|
const attachments = new DefaultAttachmentStore({
|
|
5096
5358
|
spoolDir: path13.join(wpaths.projectSessions, session.id, "attachments")
|
|
@@ -5122,6 +5384,54 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5122
5384
|
if (restoredMessages.length > 0) {
|
|
5123
5385
|
context.state.replaceMessages(restoredMessages);
|
|
5124
5386
|
}
|
|
5387
|
+
const todosCheckpointPath = path13.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
5388
|
+
if (resumeId) {
|
|
5389
|
+
try {
|
|
5390
|
+
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
5391
|
+
if (restoredTodos && restoredTodos.length > 0) {
|
|
5392
|
+
context.state.replaceTodos(restoredTodos);
|
|
5393
|
+
renderer.writeInfo(
|
|
5394
|
+
`Restored ${restoredTodos.length} todo${restoredTodos.length === 1 ? "" : "s"} from previous run.`
|
|
5395
|
+
);
|
|
5396
|
+
}
|
|
5397
|
+
} catch {
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
attachTodosCheckpoint(
|
|
5401
|
+
context.state,
|
|
5402
|
+
todosCheckpointPath,
|
|
5403
|
+
session.id
|
|
5404
|
+
);
|
|
5405
|
+
const planPath = path13.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
5406
|
+
context.state.setMeta("plan.path", planPath);
|
|
5407
|
+
if (resumeId) {
|
|
5408
|
+
try {
|
|
5409
|
+
const fleetRoot2 = path13.join(wpaths.projectSessions, session.id);
|
|
5410
|
+
const dirState = await loadDirectorState(path13.join(fleetRoot2, "director-state.json"));
|
|
5411
|
+
if (dirState) {
|
|
5412
|
+
const tCounts = {};
|
|
5413
|
+
for (const t of dirState.tasks) {
|
|
5414
|
+
tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
5415
|
+
}
|
|
5416
|
+
const summary = Object.entries(tCounts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
5417
|
+
renderer.writeInfo(
|
|
5418
|
+
`Prior fleet state: ${dirState.subagents.length} subagent${dirState.subagents.length === 1 ? "" : "s"}, tasks ${summary || "(none)"}.`
|
|
5419
|
+
);
|
|
5420
|
+
}
|
|
5421
|
+
} catch {
|
|
5422
|
+
}
|
|
5423
|
+
try {
|
|
5424
|
+
const plan = await loadPlan(planPath);
|
|
5425
|
+
if (plan && plan.items.length > 0) {
|
|
5426
|
+
const open = plan.items.filter((p) => p.status !== "done").length;
|
|
5427
|
+
const done = plan.items.length - open;
|
|
5428
|
+
renderer.writeInfo(
|
|
5429
|
+
`Plan: ${plan.items.length} item${plan.items.length === 1 ? "" : "s"} (${open} open, ${done} done). Use /plan to review.`
|
|
5430
|
+
);
|
|
5431
|
+
}
|
|
5432
|
+
} catch {
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5125
5435
|
const pipelines = createDefaultPipelines();
|
|
5126
5436
|
const installBoundary = (p) => {
|
|
5127
5437
|
p.setErrorHandler((ev) => {
|
|
@@ -5268,10 +5578,12 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5268
5578
|
}
|
|
5269
5579
|
};
|
|
5270
5580
|
const directorMode = flags["director"] === true;
|
|
5581
|
+
let director = null;
|
|
5271
5582
|
const fleetRoot = directorMode ? path13.join(wpaths.projectSessions, session.id) : void 0;
|
|
5272
5583
|
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path13.join(fleetRoot, "fleet.json") : void 0;
|
|
5273
5584
|
const sharedScratchpadPath = directorMode ? path13.join(fleetRoot, "shared") : void 0;
|
|
5274
5585
|
const subagentSessionsRoot = directorMode ? path13.join(fleetRoot, "subagents") : void 0;
|
|
5586
|
+
const stateCheckpointPath = directorMode ? path13.join(fleetRoot, "director-state.json") : void 0;
|
|
5275
5587
|
const fleetRootForPromotion = path13.join(wpaths.projectSessions, session.id);
|
|
5276
5588
|
const multiAgentHost = new MultiAgentHost(
|
|
5277
5589
|
{
|
|
@@ -5292,11 +5604,25 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5292
5604
|
sharedScratchpadPath,
|
|
5293
5605
|
sessionsRoot: subagentSessionsRoot,
|
|
5294
5606
|
directorRunId: session.id,
|
|
5295
|
-
fleetRoot: fleetRootForPromotion
|
|
5607
|
+
fleetRoot: fleetRootForPromotion,
|
|
5608
|
+
stateCheckpointPath,
|
|
5609
|
+
sessionWriter: session
|
|
5296
5610
|
}
|
|
5297
5611
|
);
|
|
5612
|
+
toolRegistry.register(
|
|
5613
|
+
createDelegateTool({
|
|
5614
|
+
host: multiAgentHost,
|
|
5615
|
+
roster: FLEET_ROSTER,
|
|
5616
|
+
// Wire the per-subagent transcript location so the tool can
|
|
5617
|
+
// extract partial output on timeout / budget exhaustion. Without
|
|
5618
|
+
// this, a subagent that hit its iteration cap returns an empty
|
|
5619
|
+
// result and the host LLM has no idea what work was done.
|
|
5620
|
+
sessionsRoot: subagentSessionsRoot,
|
|
5621
|
+
directorRunId: session.id
|
|
5622
|
+
})
|
|
5623
|
+
);
|
|
5298
5624
|
if (directorMode) {
|
|
5299
|
-
|
|
5625
|
+
director = await multiAgentHost.ensureDirector();
|
|
5300
5626
|
if (director) {
|
|
5301
5627
|
for (const tool of director.tools(FLEET_ROSTER)) {
|
|
5302
5628
|
toolRegistry.register(tool);
|
|
@@ -5310,6 +5636,12 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5310
5636
|
renderer.writeInfo(`Director mode enabled. Fleet manifest \u2192 ${manifestPath}`);
|
|
5311
5637
|
}
|
|
5312
5638
|
}
|
|
5639
|
+
const fleetStreamController = {
|
|
5640
|
+
enabled: true,
|
|
5641
|
+
setEnabled(enabled) {
|
|
5642
|
+
this.enabled = enabled;
|
|
5643
|
+
}
|
|
5644
|
+
};
|
|
5313
5645
|
const slashCmds = buildBuiltinSlashCommands({
|
|
5314
5646
|
registry: slashRegistry,
|
|
5315
5647
|
toolRegistry,
|
|
@@ -5322,6 +5654,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5322
5654
|
context,
|
|
5323
5655
|
metricsSink,
|
|
5324
5656
|
healthRegistry,
|
|
5657
|
+
planPath,
|
|
5658
|
+
fleetStreamController,
|
|
5325
5659
|
onSpawn: async (description, spawnOpts) => {
|
|
5326
5660
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
5327
5661
|
const tags = [];
|
|
@@ -5338,9 +5672,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5338
5672
|
lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
|
|
5339
5673
|
}
|
|
5340
5674
|
for (const r of s.completed) {
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
);
|
|
5675
|
+
const fmt = fmtTaskResultLine(r, color);
|
|
5676
|
+
lines.push(` ${fmt.mark} ${r.taskId.slice(0, 8)} ${fmt.stats}${fmt.tail}`);
|
|
5344
5677
|
}
|
|
5345
5678
|
return lines.join("\n");
|
|
5346
5679
|
},
|
|
@@ -5359,9 +5692,9 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5359
5692
|
if (s.completed.length > 0) {
|
|
5360
5693
|
lines.push("", color.dim(" Completed"));
|
|
5361
5694
|
for (const r of s.completed) {
|
|
5362
|
-
const
|
|
5695
|
+
const fmt = fmtTaskResultLine(r, color);
|
|
5363
5696
|
lines.push(
|
|
5364
|
-
` ${mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${
|
|
5697
|
+
` ${fmt.mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${fmt.stats}${fmt.tail}`
|
|
5365
5698
|
);
|
|
5366
5699
|
}
|
|
5367
5700
|
}
|
|
@@ -5402,10 +5735,191 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5402
5735
|
}
|
|
5403
5736
|
return `Unknown fleet action: ${action}`;
|
|
5404
5737
|
},
|
|
5738
|
+
onFleetLog: async (subagentId, mode) => {
|
|
5739
|
+
const subagentsRoot = path13.join(fleetRootForPromotion, "subagents");
|
|
5740
|
+
let runDirs;
|
|
5741
|
+
try {
|
|
5742
|
+
runDirs = await fs3.readdir(subagentsRoot);
|
|
5743
|
+
} catch {
|
|
5744
|
+
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
5745
|
+
}
|
|
5746
|
+
const found = [];
|
|
5747
|
+
for (const runId of runDirs) {
|
|
5748
|
+
const runDir = path13.join(subagentsRoot, runId);
|
|
5749
|
+
let files;
|
|
5750
|
+
try {
|
|
5751
|
+
files = await fs3.readdir(runDir);
|
|
5752
|
+
} catch {
|
|
5753
|
+
continue;
|
|
5754
|
+
}
|
|
5755
|
+
for (const f of files) {
|
|
5756
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
5757
|
+
const full = path13.join(runDir, f);
|
|
5758
|
+
try {
|
|
5759
|
+
const stat2 = await fs3.stat(full);
|
|
5760
|
+
found.push({
|
|
5761
|
+
runId,
|
|
5762
|
+
subagentId: f.replace(/\.jsonl$/, ""),
|
|
5763
|
+
file: full,
|
|
5764
|
+
size: stat2.size
|
|
5765
|
+
});
|
|
5766
|
+
} catch {
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
if (found.length === 0) {
|
|
5771
|
+
return "No subagent transcripts found on disk.";
|
|
5772
|
+
}
|
|
5773
|
+
if (!subagentId) {
|
|
5774
|
+
const lines2 = [
|
|
5775
|
+
`${found.length} subagent transcript${found.length === 1 ? "" : "s"} on disk:`
|
|
5776
|
+
];
|
|
5777
|
+
for (const t2 of found) {
|
|
5778
|
+
lines2.push(
|
|
5779
|
+
` ${color.cyan(t2.subagentId.padEnd(18))} ${color.dim(t2.runId.slice(0, 18))} ${color.dim(`${(t2.size / 1024).toFixed(1)} KB`)}`
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5782
|
+
lines2.push("Use `/fleet log <subagentId>` for a summary, or append `raw` for the full JSONL.");
|
|
5783
|
+
return lines2.join("\n");
|
|
5784
|
+
}
|
|
5785
|
+
const matches = found.filter(
|
|
5786
|
+
(t2) => t2.subagentId === subagentId || t2.subagentId.startsWith(subagentId)
|
|
5787
|
+
);
|
|
5788
|
+
if (matches.length === 0) {
|
|
5789
|
+
return `No transcript matched "${subagentId}". Run \`/fleet log\` to list available ids.`;
|
|
5790
|
+
}
|
|
5791
|
+
if (matches.length > 1) {
|
|
5792
|
+
return [
|
|
5793
|
+
`Ambiguous id "${subagentId}" \u2014 ${matches.length} matches:`,
|
|
5794
|
+
...matches.map((m) => ` ${m.subagentId} (${m.runId})`)
|
|
5795
|
+
].join("\n");
|
|
5796
|
+
}
|
|
5797
|
+
const t = matches[0];
|
|
5798
|
+
const raw = await fs3.readFile(t.file, "utf8");
|
|
5799
|
+
if (mode === "raw") return raw;
|
|
5800
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
5801
|
+
const counts = {};
|
|
5802
|
+
let firstUser = null;
|
|
5803
|
+
let lastResponse = null;
|
|
5804
|
+
let totalIterations = 0;
|
|
5805
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
5806
|
+
for (const line of lines) {
|
|
5807
|
+
try {
|
|
5808
|
+
const ev = JSON.parse(line);
|
|
5809
|
+
counts[ev.type] = (counts[ev.type] ?? 0) + 1;
|
|
5810
|
+
if (ev.type === "user_input" && !firstUser) {
|
|
5811
|
+
const txt = typeof ev.content === "string" ? ev.content : Array.isArray(ev.content) ? ev.content.filter((b) => b.type === "text").map((b) => b.text).join(" ") : "";
|
|
5812
|
+
firstUser = txt.slice(0, 120);
|
|
5813
|
+
}
|
|
5814
|
+
if (ev.type === "llm_response") {
|
|
5815
|
+
if (Array.isArray(ev.content)) {
|
|
5816
|
+
const txt = ev.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ");
|
|
5817
|
+
if (txt) lastResponse = txt.slice(0, 240);
|
|
5818
|
+
}
|
|
5819
|
+
totalIterations += 1;
|
|
5820
|
+
}
|
|
5821
|
+
if (ev.type === "tool_use" && typeof ev.name === "string") {
|
|
5822
|
+
toolNames.set(ev.name, (toolNames.get(ev.name) ?? 0) + 1);
|
|
5823
|
+
}
|
|
5824
|
+
} catch {
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
const toolBreakdown = toolNames.size > 0 ? Array.from(toolNames.entries()).sort((a, b) => b[1] - a[1]).map(([n, c]) => `${n}\xD7${c}`).join(", ") : "(none)";
|
|
5828
|
+
const out = [
|
|
5829
|
+
color.bold(`Subagent ${t.subagentId}`) + color.dim(` (run ${t.runId})`),
|
|
5830
|
+
` ${lines.length} events \xB7 ${totalIterations} llm iterations \xB7 ${(t.size / 1024).toFixed(1)} KB`,
|
|
5831
|
+
` tools: ${toolBreakdown}`
|
|
5832
|
+
];
|
|
5833
|
+
if (firstUser) out.push("", color.dim(" task:"), ` ${firstUser}`);
|
|
5834
|
+
if (lastResponse) out.push("", color.dim(" last response:"), ` ${lastResponse}`);
|
|
5835
|
+
out.push("", color.dim(" event mix:"));
|
|
5836
|
+
for (const [type, count] of Object.entries(counts).sort((a, b) => b[1] - a[1])) {
|
|
5837
|
+
out.push(` ${type.padEnd(20)} ${count}`);
|
|
5838
|
+
}
|
|
5839
|
+
out.push("", color.dim("Use `/fleet log <id> raw` for the full JSONL."));
|
|
5840
|
+
return out.join("\n");
|
|
5841
|
+
},
|
|
5842
|
+
onFleetRetry: async (taskId) => {
|
|
5843
|
+
if (!multiAgentHost.isDirectorMode()) {
|
|
5844
|
+
const promoted = await multiAgentHost.promoteToDirector();
|
|
5845
|
+
if (!promoted) {
|
|
5846
|
+
return "Cannot retry: a coordinator already exists in non-director mode.";
|
|
5847
|
+
}
|
|
5848
|
+
for (const tool of promoted.tools(FLEET_ROSTER)) {
|
|
5849
|
+
toolRegistry.register(tool);
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
const dir = await multiAgentHost.ensureDirector();
|
|
5853
|
+
if (!dir) return "Director is not available.";
|
|
5854
|
+
const dirStatePath = path13.join(fleetRootForPromotion, "director-state.json");
|
|
5855
|
+
const prior = await loadDirectorState(dirStatePath);
|
|
5856
|
+
if (!prior) {
|
|
5857
|
+
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
5858
|
+
}
|
|
5859
|
+
const interrupted = prior.tasks.filter(
|
|
5860
|
+
(t) => t.status === "running" || t.status === "pending"
|
|
5861
|
+
);
|
|
5862
|
+
if (interrupted.length === 0) {
|
|
5863
|
+
return "No interrupted tasks: every prior task reached a terminal state.";
|
|
5864
|
+
}
|
|
5865
|
+
if (!taskId) {
|
|
5866
|
+
const lines = [
|
|
5867
|
+
`${interrupted.length} interrupted task${interrupted.length === 1 ? "" : "s"} from prior run:`
|
|
5868
|
+
];
|
|
5869
|
+
for (const t of interrupted) {
|
|
5870
|
+
const owner = t.subagentId ? prior.subagents.find((s) => s.id === t.subagentId) : void 0;
|
|
5871
|
+
const tag = owner ? `${owner.name ?? owner.id} (${owner.role ?? "no-role"})` : "no-owner";
|
|
5872
|
+
lines.push(
|
|
5873
|
+
` ${t.taskId.slice(0, 12)} ${t.status.padEnd(8)} ${tag} ${(t.description ?? "").slice(0, 60)}`
|
|
5874
|
+
);
|
|
5875
|
+
}
|
|
5876
|
+
lines.push("Run `/fleet retry <taskId>` or `/fleet retry all` to re-assign.");
|
|
5877
|
+
return lines.join("\n");
|
|
5878
|
+
}
|
|
5879
|
+
const targets = taskId === "all" ? interrupted : interrupted.filter(
|
|
5880
|
+
(t) => t.taskId === taskId || t.taskId.startsWith(taskId)
|
|
5881
|
+
);
|
|
5882
|
+
if (targets.length === 0) {
|
|
5883
|
+
return `No interrupted task matched "${taskId}".`;
|
|
5884
|
+
}
|
|
5885
|
+
const results = [];
|
|
5886
|
+
for (const t of targets) {
|
|
5887
|
+
const owner = t.subagentId ? prior.subagents.find((s) => s.id === t.subagentId) : void 0;
|
|
5888
|
+
if (!owner) {
|
|
5889
|
+
results.push(` - ${t.taskId.slice(0, 12)}: no owner record, skipped.`);
|
|
5890
|
+
continue;
|
|
5891
|
+
}
|
|
5892
|
+
const rosterCfg = owner.role ? FLEET_ROSTER[owner.role] : void 0;
|
|
5893
|
+
const cfg = rosterCfg ? { ...rosterCfg } : {
|
|
5894
|
+
name: owner.name ?? owner.id,
|
|
5895
|
+
role: owner.role,
|
|
5896
|
+
provider: owner.provider,
|
|
5897
|
+
model: owner.model
|
|
5898
|
+
};
|
|
5899
|
+
try {
|
|
5900
|
+
const newSubId = await dir.spawn(cfg);
|
|
5901
|
+
const newTaskId = await dir.assign({
|
|
5902
|
+
id: "",
|
|
5903
|
+
description: t.description ?? "(no description)",
|
|
5904
|
+
subagentId: newSubId
|
|
5905
|
+
});
|
|
5906
|
+
results.push(
|
|
5907
|
+
` ${color.green("\u2713")} ${t.taskId.slice(0, 12)} \u2192 re-spawned ${newSubId.slice(0, 12)} (task ${newTaskId.slice(0, 12)})`
|
|
5908
|
+
);
|
|
5909
|
+
} catch (err) {
|
|
5910
|
+
results.push(
|
|
5911
|
+
` ${color.red("\u2717")} ${t.taskId.slice(0, 12)} \u2192 ${err instanceof Error ? err.message : String(err)}`
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
return [`Retried ${targets.length} task${targets.length === 1 ? "" : "s"}:`, ...results].join(
|
|
5916
|
+
"\n"
|
|
5917
|
+
);
|
|
5918
|
+
},
|
|
5405
5919
|
onDirector: async () => {
|
|
5406
|
-
const
|
|
5407
|
-
if (!
|
|
5408
|
-
for (const tool of
|
|
5920
|
+
const director2 = await multiAgentHost.promoteToDirector();
|
|
5921
|
+
if (!director2) return null;
|
|
5922
|
+
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
5409
5923
|
toolRegistry.register(tool);
|
|
5410
5924
|
}
|
|
5411
5925
|
const mp = path13.join(fleetRootForPromotion, "fleet.json");
|
|
@@ -5477,7 +5991,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
|
|
|
5477
5991
|
savedProviderCfg,
|
|
5478
5992
|
resolvedProvider: resolvedProvider ?? void 0,
|
|
5479
5993
|
getPickableProviders: () => buildPickableProviders(modelsRegistry, config),
|
|
5480
|
-
switchProviderAndModel
|
|
5994
|
+
switchProviderAndModel,
|
|
5995
|
+
director: director ?? null,
|
|
5996
|
+
fleetRoster: FLEET_ROSTER,
|
|
5997
|
+
fleetStreamController
|
|
5481
5998
|
});
|
|
5482
5999
|
}
|
|
5483
6000
|
async function promptRecovery(reader, renderer, abandoned, autoRecover) {
|