@wrongstack/cli 0.6.7 → 0.7.2

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 CHANGED
@@ -3,11 +3,11 @@ import * as path23 from 'path';
3
3
  import { join } from 'path';
4
4
  import * as fsp2 from 'fs/promises';
5
5
  import { readdir, readFile } from 'fs/promises';
6
- 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, makeAgentSubagentRunner, NULL_FLEET_BUS, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
6
+ 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, makeLLMClassifier, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderProgress, renderTaskGraph, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, InputBuilder, FsError, ERROR_CODES, projectHash, WrongStackError, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, SpecVersioning, ParallelEternalEngine, allServers as allServers$1 } from '@wrongstack/core';
7
7
  import { createRequire } from 'module';
8
8
  import * as os6 from 'os';
9
9
  import os6__default from 'os';
10
- import * as crypto from 'crypto';
10
+ import * as crypto2 from 'crypto';
11
11
  import { randomUUID } from 'crypto';
12
12
  import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
13
13
  import { WebSocketServer, WebSocket } from 'ws';
@@ -18,6 +18,9 @@ import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
18
18
  import * as readline from 'readline';
19
19
  import { spawn } from 'child_process';
20
20
  import { SkillInstaller } from '@wrongstack/core/skills';
21
+ import { WrongStackACPServer } from '@wrongstack/acp/agent';
22
+ import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
23
+ import { ACP_AGENTS, SubagentBudget } from '@wrongstack/core/coordination';
21
24
  import { allServers } from '@wrongstack/core/infrastructure';
22
25
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
23
26
  import { ToolExecutor } from '@wrongstack/core/execution';
@@ -1244,12 +1247,12 @@ ${lines.join("\n")}`
1244
1247
  if (analysis.bottlenecks.length > 0) {
1245
1248
  lines.push("");
1246
1249
  lines.push(` \u{1F6AB} Bottlenecks (blocking most downstream):`);
1247
- analysis.bottlenecks.forEach((bt) => {
1250
+ for (const bt of analysis.bottlenecks) {
1248
1251
  const node = graph.nodes.get(bt.taskId);
1249
1252
  if (node) {
1250
1253
  lines.push(` \u2022 ${node.title} (blocks ${bt.blockedCount} task(s))`);
1251
1254
  }
1252
- });
1255
+ }
1253
1256
  }
1254
1257
  if (analysis.parallelGroups.length > 0) {
1255
1258
  lines.push("");
@@ -1262,12 +1265,12 @@ ${lines.join("\n")}`
1262
1265
  if (analysis.readyTasks.length > 0) {
1263
1266
  lines.push("");
1264
1267
  lines.push(` \u2705 Ready to start now:`);
1265
- analysis.readyTasks.forEach((taskId) => {
1268
+ for (const taskId of analysis.readyTasks) {
1266
1269
  const node = graph.nodes.get(taskId);
1267
1270
  if (node) {
1268
1271
  lines.push(` \u2022 ${node.title}`);
1269
1272
  }
1270
- });
1273
+ }
1271
1274
  }
1272
1275
  lines.push(`\u2570${"\u2500".repeat(55)}\u256F`);
1273
1276
  return { message: lines.join("\n") };
@@ -1468,7 +1471,10 @@ var init_sdd = __esm({
1468
1471
  return Date.now() - this.phaseStartTime;
1469
1472
  }
1470
1473
  getVersioning() {
1471
- return this.versioning ?? (this.versioning = new SpecVersioning());
1474
+ if (this.versioning === null) {
1475
+ this.versioning = new SpecVersioning();
1476
+ }
1477
+ return this.versioning;
1472
1478
  }
1473
1479
  clearTaskState() {
1474
1480
  this.taskStore = null;
@@ -1554,7 +1560,7 @@ function currentVersion() {
1554
1560
  return "dev";
1555
1561
  }
1556
1562
  function isNewer(a, b) {
1557
- const parse = (v) => v.replace(/^v/i, "").split(".").map((p) => parseInt(p, 10) || 0);
1563
+ const parse = (v) => v.replace(/^v/i, "").split(".").map((p) => Number.parseInt(p, 10) || 0);
1558
1564
  const [ap, bp] = [parse(a), parse(b)];
1559
1565
  for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
1560
1566
  const ai = ap[i] ?? 0;
@@ -1663,7 +1669,7 @@ async function runWebUI(opts) {
1663
1669
  const port = opts.port ?? 3457;
1664
1670
  const clients = /* @__PURE__ */ new Map();
1665
1671
  let abortController = null;
1666
- const authToken = crypto.randomBytes(16).toString("hex");
1672
+ const authToken = crypto2.randomBytes(16).toString("hex");
1667
1673
  const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
1668
1674
  console.log(`[WebUI] WebSocket server starting on ws://127.0.0.1:${port}`);
1669
1675
  const eventUnsubscribers = [];
@@ -2244,6 +2250,35 @@ ${diff}`;
2244
2250
  }
2245
2251
  return "chore: update";
2246
2252
  }
2253
+ function makeProviderClassifier(provider, model) {
2254
+ return makeLLMClassifier(async (prompt) => {
2255
+ const ctrl = new AbortController();
2256
+ const timeout = setTimeout(() => ctrl.abort(), 15e3);
2257
+ try {
2258
+ const resp = await provider.complete(
2259
+ {
2260
+ model,
2261
+ system: [
2262
+ {
2263
+ type: "text",
2264
+ text: 'You are an agent router. Choose the single best agent for the task. Reply with ONLY a compact JSON object {"role":"...","reason":"..."}.'
2265
+ }
2266
+ ],
2267
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
2268
+ maxTokens: 120,
2269
+ temperature: 0
2270
+ },
2271
+ { signal: ctrl.signal }
2272
+ );
2273
+ const content = resp.content;
2274
+ return Array.isArray(content) ? content[0]?.text ?? "" : "";
2275
+ } catch {
2276
+ return "";
2277
+ } finally {
2278
+ clearTimeout(timeout);
2279
+ }
2280
+ });
2281
+ }
2247
2282
 
2248
2283
  // src/arg-parser.ts
2249
2284
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
@@ -2753,7 +2788,16 @@ function entryId(ts) {
2753
2788
  return ts.replace(/[:.]/g, "-").slice(0, 19);
2754
2789
  }
2755
2790
  async function ensureHistoryDir(homeFn = defaultHomeDir) {
2756
- await fsp2.mkdir(historyDir(homeFn), { recursive: true });
2791
+ try {
2792
+ await fsp2.mkdir(historyDir(homeFn), { recursive: true });
2793
+ } catch (err) {
2794
+ throw new FsError({
2795
+ message: err instanceof Error ? err.message : String(err),
2796
+ code: ERROR_CODES.FS_MKDIR_FAILED,
2797
+ path: historyDir(homeFn),
2798
+ cause: err
2799
+ });
2800
+ }
2757
2801
  }
2758
2802
  async function readIndex(homeFn = defaultHomeDir) {
2759
2803
  try {
@@ -2765,7 +2809,16 @@ async function readIndex(homeFn = defaultHomeDir) {
2765
2809
  }
2766
2810
  async function writeIndex(idx, homeFn = defaultHomeDir) {
2767
2811
  await ensureHistoryDir(homeFn);
2768
- await atomicWrite(historyIndexPath(homeFn), JSON.stringify(idx, null, 2));
2812
+ try {
2813
+ await atomicWrite(historyIndexPath(homeFn), JSON.stringify(idx, null, 2));
2814
+ } catch (err) {
2815
+ throw new FsError({
2816
+ message: err instanceof Error ? err.message : String(err),
2817
+ code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
2818
+ path: historyIndexPath(homeFn),
2819
+ cause: err
2820
+ });
2821
+ }
2769
2822
  }
2770
2823
  async function backupCurrent(homeFn = defaultHomeDir) {
2771
2824
  const cfg = configPath(homeFn);
@@ -2810,11 +2863,20 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
2810
2863
  snapshotMasked: maskConfigSecrets(newCfg),
2811
2864
  diffSummary: diffSummary(oldCfg, newCfg)
2812
2865
  };
2813
- await fsp2.writeFile(
2814
- path23.join(historyDir(homeFn), `${id}.json`),
2815
- JSON.stringify(entry, null, 2),
2816
- "utf8"
2817
- );
2866
+ try {
2867
+ await fsp2.writeFile(
2868
+ path23.join(historyDir(homeFn), `${id}.json`),
2869
+ JSON.stringify(entry, null, 2),
2870
+ "utf8"
2871
+ );
2872
+ } catch (err) {
2873
+ throw new FsError({
2874
+ message: err instanceof Error ? err.message : String(err),
2875
+ code: ERROR_CODES.FS_WRITE_FAILED,
2876
+ path: path23.join(historyDir(homeFn), `${id}.json`),
2877
+ cause: err
2878
+ });
2879
+ }
2818
2880
  const idx = await readIndex(homeFn);
2819
2881
  idx.entries.unshift({ id, timestamp, description });
2820
2882
  await writeIndex(idx, homeFn);
@@ -3476,17 +3538,41 @@ function buildClearCommand(opts) {
3476
3538
  };
3477
3539
  }
3478
3540
  async function runGit(args, cwd) {
3479
- return new Promise((resolve4) => {
3480
- const child = spawn("git", args, {
3481
- cwd,
3482
- stdio: ["ignore", "pipe", "pipe"]
3541
+ try {
3542
+ return await new Promise((resolve4, reject) => {
3543
+ const child = spawn("git", args, {
3544
+ cwd,
3545
+ stdio: ["ignore", "pipe", "pipe"]
3546
+ });
3547
+ let stdout = "";
3548
+ let stderr = "";
3549
+ child.stdout?.on("data", (d) => {
3550
+ stdout += d;
3551
+ });
3552
+ child.stderr?.on("data", (d) => {
3553
+ stderr += d;
3554
+ });
3555
+ child.on("error", (err) => {
3556
+ reject(new WrongStackError({
3557
+ message: `Failed to run git: ${err.message}`,
3558
+ code: ERROR_CODES.TOOL_EXECUTION_FAILED,
3559
+ subsystem: "tool",
3560
+ context: { command: "git", args, cwd },
3561
+ cause: err
3562
+ }));
3563
+ });
3564
+ child.on("close", (code) => resolve4({ stdout, stderr, code: code ?? 0 }));
3483
3565
  });
3484
- let stdout = "";
3485
- let stderr = "";
3486
- child.stdout?.on("data", (d) => stdout += d);
3487
- child.stderr?.on("data", (d) => stderr += d);
3488
- child.on("close", (code) => resolve4({ stdout, stderr, code: code ?? 0 }));
3489
- });
3566
+ } catch (err) {
3567
+ if (err instanceof WrongStackError) throw err;
3568
+ throw new WrongStackError({
3569
+ message: err instanceof Error ? err.message : String(err),
3570
+ code: ERROR_CODES.TOOL_EXECUTION_FAILED,
3571
+ subsystem: "tool",
3572
+ context: { command: "git", args, cwd },
3573
+ cause: err
3574
+ });
3575
+ }
3490
3576
  }
3491
3577
  function detectCommitType(stats) {
3492
3578
  const lines = stats.split("\n");
@@ -3799,6 +3885,17 @@ function buildStatsCommand(opts) {
3799
3885
  }
3800
3886
  };
3801
3887
  }
3888
+ var PHASE_ORDER = [
3889
+ { phase: "discovery", label: "1 \xB7 Discovery" },
3890
+ { phase: "planning", label: "2 \xB7 Planning" },
3891
+ { phase: "build", label: "3 \xB7 Build" },
3892
+ { phase: "verify", label: "4 \xB7 Verify" },
3893
+ { phase: "review", label: "5 \xB7 Review" },
3894
+ { phase: "domain", label: "6 \xB7 Domain" },
3895
+ { phase: "knowledge", label: "7 \xB7 Knowledge" },
3896
+ { phase: "delivery", label: "8 \xB7 Delivery & Ops" },
3897
+ { phase: "meta", label: "9 \xB7 Meta" }
3898
+ ];
3802
3899
  function buildFleetCommand(opts) {
3803
3900
  return {
3804
3901
  name: "fleet",
@@ -3807,12 +3904,15 @@ function buildFleetCommand(opts) {
3807
3904
  "Usage:",
3808
3905
  " /fleet Show fleet status (default)",
3809
3906
  " /fleet status Same as /fleet (verbose status)",
3907
+ " /fleet list List the agent roster grouped by phase",
3908
+ " /fleet dispatch <task> Route a task to the best agent and spawn it",
3810
3909
  " /fleet spawn <role> [count] Spawn N subagents of a role (default 1)",
3811
3910
  " /fleet terminate <subagentId> Stop a specific subagent by id",
3812
3911
  " /fleet kill Stop all running subagents",
3813
3912
  " /fleet usage Token and cost breakdown across the fleet",
3814
3913
  " /fleet journal Show recent journal entries from /goal journal",
3815
3914
  "",
3915
+ "In the TUI, press Ctrl+F to open the graphical fleet monitor.",
3816
3916
  "Works during /autonomy parallel mode and standalone director sessions."
3817
3917
  ].join("\n"),
3818
3918
  async run(args) {
@@ -3846,8 +3946,9 @@ function buildFleetCommand(opts) {
3846
3946
  const id = sa.id?.padEnd(36) ?? "".padEnd(36);
3847
3947
  const name = (sa.name ?? "worker").padEnd(16);
3848
3948
  const statusColor = sa.status === "running" ? color.green(sa.status.padEnd(10)) : sa.status === "idle" ? color.dim(sa.status.padEnd(10)) : color.dim(sa.status.padEnd(10));
3949
+ const ext = sa.extensions && sa.extensions > 0 ? `${color.yellow(`\u26A1\xD7${sa.extensions}`)} ` : "";
3849
3950
  const task = sa.currentTask ?? color.dim("\u2014");
3850
- lines.push(` ${id} ${name} ${statusColor} ${task}`);
3951
+ lines.push(` ${id} ${name} ${statusColor} ${ext}${task}`);
3851
3952
  }
3852
3953
  }
3853
3954
  const msg3 = lines.join("\n");
@@ -3953,29 +4054,28 @@ function buildFleetCommand(opts) {
3953
4054
  if (cmd === "terminate" || cmd === "stop") {
3954
4055
  const targetId = subargs[0];
3955
4056
  if (!targetId) {
3956
- const msg2 = `${color.amber("\u26A0 /fleet terminate requires a subagentId.")} Use /fleet to see active ids.`;
3957
- opts.renderer.writeWarning(msg2);
3958
- return { message: msg2 };
4057
+ const msg3 = `${color.amber("\u26A0 /fleet terminate requires a subagentId.")} Use /fleet to see active ids.`;
4058
+ opts.renderer.writeWarning(msg3);
4059
+ return { message: msg3 };
3959
4060
  }
3960
4061
  if (!opts.onFleetTerminate) {
3961
- const msg2 = `${color.amber("\u26A0 /fleet terminate is not wired in this session.")}`;
3962
- opts.renderer.writeWarning(msg2);
3963
- return { message: msg2 };
4062
+ const msg3 = `${color.amber("\u26A0 /fleet terminate is not wired in this session.")}`;
4063
+ opts.renderer.writeWarning(msg3);
4064
+ return { message: msg3 };
3964
4065
  }
3965
4066
  const ok = opts.onFleetTerminate(targetId);
3966
4067
  if (ok) {
3967
- const msg2 = `${color.green("\u2713 Terminated")} subagent ${color.bold(targetId)}.`;
3968
- opts.renderer.write(msg2);
3969
- return { message: msg2 };
3970
- } else {
3971
- const msg2 = `${color.red("\u2717 Failed")} to terminate ${color.bold(targetId)}. Subagent may already be stopped.`;
3972
- opts.renderer.writeWarning(msg2);
3973
- return { message: msg2 };
4068
+ const msg3 = `${color.green("\u2713 Terminated")} subagent ${color.bold(targetId)}.`;
4069
+ opts.renderer.write(msg3);
4070
+ return { message: msg3 };
3974
4071
  }
4072
+ const msg2 = `${color.red("\u2717 Failed")} to terminate ${color.bold(targetId)}. Subagent may already be stopped.`;
4073
+ opts.renderer.writeWarning(msg2);
4074
+ return { message: msg2 };
3975
4075
  }
3976
4076
  if (cmd === "spawn" || cmd === "add") {
3977
4077
  const role = subargs[0] ?? "worker";
3978
- const count = Math.min(16, Math.max(1, parseInt(subargs[1] ?? "1", 10) || 1));
4078
+ const count = Math.min(16, Math.max(1, Number.parseInt(subargs[1] ?? "1", 10) || 1));
3979
4079
  if (!opts.onFleetSpawn) {
3980
4080
  const msg3 = `${color.amber("\u26A0 /fleet spawn is not wired in this session.")}`;
3981
4081
  opts.renderer.writeWarning(msg3);
@@ -4001,11 +4101,64 @@ function buildFleetCommand(opts) {
4001
4101
  }
4002
4102
  return { message: msg2 };
4003
4103
  }
4104
+ if (cmd === "list" || cmd === "roster" || cmd === "agents") {
4105
+ const lines = [`${color.bold("Agent Roster")} ${color.dim("(spawn with /fleet spawn <role>)")}`];
4106
+ for (const { phase, label } of PHASE_ORDER) {
4107
+ const defs = AGENTS_BY_PHASE[phase];
4108
+ if (!defs || defs.length === 0) continue;
4109
+ lines.push("");
4110
+ lines.push(color.cyan(` Phase ${label}`));
4111
+ for (const def of defs) {
4112
+ const role = (def.config.role ?? "").padEnd(18);
4113
+ lines.push(` ${color.bold(role)} ${color.dim(def.capability.summary)}`);
4114
+ }
4115
+ }
4116
+ const msg2 = lines.join("\n");
4117
+ opts.renderer.write(msg2);
4118
+ return { message: msg2 };
4119
+ }
4120
+ if (cmd === "dispatch" || cmd === "route") {
4121
+ const task = subargs.join(" ").trim();
4122
+ if (!task) {
4123
+ const msg3 = `Usage: /fleet dispatch <task description> \u2014 routes the task to the best agent.`;
4124
+ opts.renderer.writeWarning(msg3);
4125
+ return { message: msg3 };
4126
+ }
4127
+ const decision = await dispatchAgent(task, { classifier: opts.onDispatchClassify });
4128
+ const pct2 = Math.round(decision.confidence * 100);
4129
+ const lines = [];
4130
+ lines.push(
4131
+ `${color.bold("\u2192 " + decision.role)} ${color.dim(`(${decision.method}, ${pct2}% confidence)`)}`
4132
+ );
4133
+ lines.push(` ${color.dim(decision.definition.capability.summary)}`);
4134
+ lines.push(` ${color.dim("why:")} ${decision.reason}`);
4135
+ if (decision.alternatives.length > 0) {
4136
+ const alts = decision.alternatives.slice(0, 3).map((a) => a.role).join(", ");
4137
+ lines.push(` ${color.dim("alternatives:")} ${alts}`);
4138
+ }
4139
+ if (opts.onFleetSpawn) {
4140
+ try {
4141
+ const id = await opts.onFleetSpawn(decision.role);
4142
+ lines.push(` ${color.green("\u2713 spawned")} ${color.bold(decision.role)} as ${color.dim(id)}`);
4143
+ } catch (err) {
4144
+ lines.push(
4145
+ ` ${color.amber("\u26A0 spawn failed:")} ${err instanceof Error ? err.message : String(err)}`
4146
+ );
4147
+ }
4148
+ } else {
4149
+ lines.push(` ${color.dim("(no fleet active \u2014 run /autonomy parallel or --director to spawn)")}`);
4150
+ }
4151
+ const msg2 = lines.join("\n");
4152
+ opts.renderer.write(msg2);
4153
+ return { message: msg2 };
4154
+ }
4004
4155
  if (cmd === "help" || cmd === "?") {
4005
4156
  const msg2 = [
4006
4157
  `${color.bold("Fleet Commands")}`,
4007
4158
  ` ${color.dim("/fleet")} Show fleet status (default)`,
4008
4159
  ` ${color.dim("/fleet status")} Same as /fleet (verbose status)`,
4160
+ ` ${color.dim("/fleet list")} List the agent roster grouped by phase`,
4161
+ ` ${color.dim("/fleet dispatch <task>")} Route a task to the best agent and spawn it`,
4009
4162
  ` ${color.dim("/fleet spawn <role> [count]")} Spawn N subagents of a role (default 1)`,
4010
4163
  ` ${color.dim("/fleet terminate <subagentId>")} Stop a specific subagent by id`,
4011
4164
  ` ${color.dim("/fleet kill")} Stop all running subagents`,
@@ -4015,7 +4168,7 @@ function buildFleetCommand(opts) {
4015
4168
  opts.renderer.write(msg2);
4016
4169
  return { message: msg2 };
4017
4170
  }
4018
- const valid = ["status", "usage", "spawn", "terminate", "kill", "retry", "journal"];
4171
+ const valid = ["status", "list", "dispatch", "usage", "spawn", "terminate", "kill", "retry", "journal"];
4019
4172
  const msg = `Unknown subcommand "${cmd}". Valid subcommands: ${valid.join(", ")}. Run /fleet with no args to see status, or /fleet help for usage.`;
4020
4173
  opts.renderer.writeWarning(msg);
4021
4174
  return { message: msg };
@@ -5424,8 +5577,8 @@ ${list}
5424
5577
 
5425
5578
  Use \`/security report <number>\` to view a specific report.` };
5426
5579
  }
5427
- const index = parseInt(reportId, 10) - 1;
5428
- if (!isNaN(index) && reports[index]) {
5580
+ const index = Number.parseInt(reportId, 10) - 1;
5581
+ if (!Number.isNaN(index) && reports[index]) {
5429
5582
  const content = await readFile(join(reportsDir, reports[index]), "utf-8");
5430
5583
  return { message: `# Security Report
5431
5584
 
@@ -5505,8 +5658,17 @@ async function loadStatuslineConfig() {
5505
5658
  }
5506
5659
  async function saveStatuslineConfig(cfg) {
5507
5660
  const p = resolveConfigPath();
5508
- await fsp2.mkdir(path23.dirname(p), { recursive: true });
5509
- await atomicWrite(p, JSON.stringify(cfg, null, 2));
5661
+ try {
5662
+ await fsp2.mkdir(path23.dirname(p), { recursive: true });
5663
+ await atomicWrite(p, JSON.stringify(cfg, null, 2));
5664
+ } catch (err) {
5665
+ throw new FsError({
5666
+ message: err instanceof Error ? err.message : String(err),
5667
+ code: err instanceof Error && err.message.includes("mkdir") ? ERROR_CODES.FS_MKDIR_FAILED : ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
5668
+ path: p,
5669
+ cause: err
5670
+ });
5671
+ }
5510
5672
  }
5511
5673
  function buildStatuslineCommand(deps) {
5512
5674
  return {
@@ -6708,6 +6870,7 @@ async function runProjectCheck(opts) {
6708
6870
  const { spawn: spawn3 } = await import('child_process');
6709
6871
  await new Promise((resolve4, reject) => {
6710
6872
  const child = spawn3("git", ["init"], { cwd: projectRoot });
6873
+ child.on("error", reject);
6711
6874
  child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(`git init failed with ${code}`)));
6712
6875
  });
6713
6876
  renderer.write(` ${color.green("\u2713")} Git repository initialized
@@ -7090,6 +7253,162 @@ function summarize(value, name) {
7090
7253
  }
7091
7254
  return "";
7092
7255
  }
7256
+ var acpCmd = async (args, deps) => {
7257
+ const sub = args[0];
7258
+ if (!sub || sub === "server" || sub === "serve") {
7259
+ return runACPServer(deps);
7260
+ }
7261
+ if (sub === "help") {
7262
+ deps.renderer.write(`wstack acp \u2014 ACP (Agent Client Protocol) integration
7263
+
7264
+ Usage:
7265
+ wstack acp Start WrongStack as an ACP server (blocks)
7266
+ wstack acp server Same as above
7267
+ wstack acp list List available ACP agents
7268
+ wstack acp spawn <id> <task>
7269
+ Spawn an ACP agent as a subagent and wait for result
7270
+ wstack acp help Show this help
7271
+
7272
+ ACP Mode:
7273
+ When run as \`wstack acp\`. WrongStack acts as an ACP-compatible agent.
7274
+ ACP clients (Zed, JetBrains, VS Code) spawn it as a subprocess and
7275
+ communicate over stdio JSON-RPC.
7276
+ Press Ctrl+C to stop.
7277
+
7278
+ spawn:
7279
+ Spawns a named ACP agent (cline, gemini-cli, copilot, openhands, goose)
7280
+ with the given task and waits for its result.
7281
+ Example: wstack acp spawn cline "fix the login bug"
7282
+ `);
7283
+ return 0;
7284
+ }
7285
+ if (sub === "list") {
7286
+ return listACPAgents(deps);
7287
+ }
7288
+ if (sub === "spawn") {
7289
+ return spawnACPAgent(args.slice(1), deps);
7290
+ }
7291
+ deps.renderer.writeError(`Unknown acp subcommand: ${sub}
7292
+ `);
7293
+ deps.renderer.write("Run `wstack acp help` for usage.\n");
7294
+ return 1;
7295
+ };
7296
+ async function runACPServer(deps) {
7297
+ const toolRegistry = deps.toolRegistry;
7298
+ const tools = toolRegistry?.list() ?? [];
7299
+ deps.renderer.writeInfo("Starting WrongStack ACP server...\n");
7300
+ deps.renderer.writeInfo(`Exposing ${tools.length} tool(s) via ACP protocol.
7301
+ `);
7302
+ deps.renderer.writeInfo("Waiting for ACP client connection on stdin/stdout...\n");
7303
+ deps.renderer.writeInfo("Press Ctrl+C to stop.\n");
7304
+ const server = new WrongStackACPServer({ tools });
7305
+ const shutdown = () => {
7306
+ deps.renderer.writeWarning("\nShutting down ACP server...");
7307
+ server.stop();
7308
+ process.exit(0);
7309
+ };
7310
+ process.on("SIGINT", shutdown);
7311
+ process.on("SIGTERM", shutdown);
7312
+ try {
7313
+ await server.start();
7314
+ } catch (err) {
7315
+ deps.renderer.writeError(`ACP server error: ${err instanceof Error ? err.message : String(err)}
7316
+ `);
7317
+ return 1;
7318
+ }
7319
+ return 0;
7320
+ }
7321
+ function listACPAgents(deps) {
7322
+ deps.renderer.write("Available ACP agents:\n\n");
7323
+ for (const a of ACP_AGENTS) {
7324
+ const id = a.id ?? a.role;
7325
+ const name = a.name ?? a.role ?? "";
7326
+ const desc = a.prompt?.slice(0, 50) ?? "";
7327
+ deps.renderer.write(` ${id.padEnd(16)} ${name.padEnd(20)} ${desc}\u2026
7328
+ `);
7329
+ }
7330
+ deps.renderer.write("\nUse `wstack acp spawn <agent> <task>` to delegate a task.\n");
7331
+ return 0;
7332
+ }
7333
+ async function spawnACPAgent(args, deps) {
7334
+ const [subagentId, ...taskParts] = args;
7335
+ if (!subagentId) {
7336
+ deps.renderer.writeError("Usage: wstack acp spawn <agent-id> <task>\n");
7337
+ deps.renderer.write("Run `wstack acp list` to see available agents.\n");
7338
+ return 1;
7339
+ }
7340
+ const task = taskParts.join(" ");
7341
+ if (!task) {
7342
+ deps.renderer.writeError("Usage: wstack acp spawn <agent-id> <task>\n");
7343
+ deps.renderer.write("Task description is required.\n");
7344
+ return 1;
7345
+ }
7346
+ const cmd = ACP_AGENT_COMMANDS[subagentId];
7347
+ if (!cmd) {
7348
+ deps.renderer.writeError(`Unknown ACP agent: ${subagentId}
7349
+ `);
7350
+ deps.renderer.write("Run `wstack acp list` to see available agents.\n");
7351
+ return 1;
7352
+ }
7353
+ deps.renderer.writeInfo(`Spawning ACP agent '${subagentId}'\u2026
7354
+ `);
7355
+ const cleanup = () => {
7356
+ if (stop) {
7357
+ try {
7358
+ stop();
7359
+ } catch {
7360
+ }
7361
+ }
7362
+ };
7363
+ process.on("SIGINT", cleanup);
7364
+ process.on("SIGTERM", cleanup);
7365
+ let stop = null;
7366
+ try {
7367
+ const { runner, stop: runStop } = await makeACPSubagentRunnerWithStop(cmd);
7368
+ stop = runStop;
7369
+ const taskId = `acp-${crypto.randomUUID()}`;
7370
+ const budget = new SubagentBudget({
7371
+ timeoutMs: 5 * 60 * 1e3,
7372
+ maxIterations: 2e3,
7373
+ maxToolCalls: 5e3
7374
+ });
7375
+ const ctx = {
7376
+ subagentId,
7377
+ config: {
7378
+ id: subagentId,
7379
+ name: cmd.role ?? subagentId,
7380
+ role: subagentId,
7381
+ provider: "acp",
7382
+ prompt: ""
7383
+ },
7384
+ budget,
7385
+ signal: new AbortController().signal,
7386
+ bridge: null
7387
+ };
7388
+ budget.start();
7389
+ deps.renderer.writeInfo("Running task\u2026\n");
7390
+ const result = await runner(
7391
+ { id: taskId, description: task },
7392
+ ctx
7393
+ );
7394
+ deps.renderer.write("\n--- Result ---\n");
7395
+ deps.renderer.write(String(result.result ?? "no result"));
7396
+ deps.renderer.write("\n---------------\n");
7397
+ deps.renderer.writeInfo(
7398
+ `Done. iterations=${result.iterations} toolCalls=${result.toolCalls}
7399
+ `
7400
+ );
7401
+ return 0;
7402
+ } catch (err) {
7403
+ deps.renderer.writeError(`ACP agent error: ${err instanceof Error ? err.message : String(err)}
7404
+ `);
7405
+ return 1;
7406
+ } finally {
7407
+ cleanup();
7408
+ process.off("SIGINT", cleanup);
7409
+ process.off("SIGTERM", cleanup);
7410
+ }
7411
+ }
7093
7412
 
7094
7413
  // src/auth-menu.ts
7095
7414
  init_provider_config_utils();
@@ -7786,7 +8105,7 @@ var updateCmd = async (args, deps) => {
7786
8105
  deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
7787
8106
  `);
7788
8107
  try {
7789
- const result = await new Promise((resolve4) => {
8108
+ const result = await new Promise((resolve4, reject) => {
7790
8109
  const child = spawn("npm", ["install", "-g", "wrongstack@latest"], {
7791
8110
  cwd,
7792
8111
  stdio: "pipe"
@@ -7795,6 +8114,7 @@ var updateCmd = async (args, deps) => {
7795
8114
  child.stderr?.on("data", (d) => {
7796
8115
  stderr += d;
7797
8116
  });
8117
+ child.on("error", reject);
7798
8118
  child.on("close", (code) => resolve4({ code: code ?? 0 }));
7799
8119
  });
7800
8120
  if (result.code === 0) {
@@ -8957,8 +9277,8 @@ var rewindCmd = async (args, deps) => {
8957
9277
  deps.renderer.write("Rewinding to session start...\n");
8958
9278
  result = await rewind.rewindToStart(sessionId);
8959
9279
  } else if (flags.last) {
8960
- const n = parseInt(flags.last, 10);
8961
- if (isNaN(n) || n < 1) {
9280
+ const n = Number.parseInt(flags.last, 10);
9281
+ if (Number.isNaN(n) || n < 1) {
8962
9282
  deps.renderer.writeError("--last requires a positive number");
8963
9283
  return 1;
8964
9284
  }
@@ -8966,8 +9286,8 @@ var rewindCmd = async (args, deps) => {
8966
9286
  `);
8967
9287
  result = await rewind.rewindLastN(sessionId, n);
8968
9288
  } else if (flags.to) {
8969
- const idx = parseInt(flags.to, 10);
8970
- if (isNaN(idx) || idx < 0) {
9289
+ const idx = Number.parseInt(flags.to, 10);
9290
+ if (Number.isNaN(idx) || idx < 0) {
8971
9291
  deps.renderer.writeError("--to requires a non-negative number");
8972
9292
  return 1;
8973
9293
  }
@@ -9088,6 +9408,7 @@ var helpCmd = async (_args, deps) => {
9088
9408
 
9089
9409
  // src/subcommands/index.ts
9090
9410
  var subcommands = {
9411
+ acp: acpCmd,
9091
9412
  init: initCmd,
9092
9413
  auth: authCmd,
9093
9414
  update: updateCmd,
@@ -9130,22 +9451,22 @@ function fmtDuration(ms) {
9130
9451
  const remMin = m - h * 60;
9131
9452
  return `${h}h${remMin}m`;
9132
9453
  }
9133
- function fmtTaskResultLine(r, color38) {
9454
+ function fmtTaskResultLine(r, color39) {
9134
9455
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
9135
9456
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
9136
9457
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
9137
9458
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
9138
- const errKindChip = errKind ? color38.dim(` [${errKind}]`) : "";
9139
- const errSnip = errMsg || errKind ? `${errKindChip}${color38.dim(errTail)}` : "";
9459
+ const errKindChip = errKind ? color39.dim(` [${errKind}]`) : "";
9460
+ const errSnip = errMsg || errKind ? `${errKindChip}${color39.dim(errTail)}` : "";
9140
9461
  switch (r.status) {
9141
9462
  case "success":
9142
- return { mark: color38.green("\u2713"), stats, tail: "" };
9463
+ return { mark: color39.green("\u2713"), stats, tail: "" };
9143
9464
  case "timeout":
9144
- return { mark: color38.yellow("\u23F1"), stats: `${color38.yellow("timeout")} ${stats}`, tail: errSnip };
9465
+ return { mark: color39.yellow("\u23F1"), stats: `${color39.yellow("timeout")} ${stats}`, tail: errSnip };
9145
9466
  case "stopped":
9146
- return { mark: color38.dim("\u2298"), stats: `${color38.dim("stopped")} ${stats}`, tail: errSnip };
9467
+ return { mark: color39.dim("\u2298"), stats: `${color39.dim("stopped")} ${stats}`, tail: errSnip };
9147
9468
  case "failed":
9148
- return { mark: color38.red("\u2717"), stats: `${color38.red("failed")} ${stats}`, tail: errSnip };
9469
+ return { mark: color39.red("\u2717"), stats: `${color39.red("failed")} ${stats}`, tail: errSnip };
9149
9470
  }
9150
9471
  }
9151
9472
 
@@ -9304,6 +9625,214 @@ async function boot(argv) {
9304
9625
  updateInfo
9305
9626
  };
9306
9627
  }
9628
+ function fmtElapsed(ms) {
9629
+ const s = Math.floor(ms / 1e3);
9630
+ if (s < 60) return `${s}s`;
9631
+ const m = Math.floor(s / 60);
9632
+ const rem = s % 60;
9633
+ if (m < 60) return `${m}m${rem.toString().padStart(2, "0")}s`;
9634
+ const h = Math.floor(m / 60);
9635
+ return `${h}h${(m % 60).toString().padStart(2, "0")}m`;
9636
+ }
9637
+ function renderFleetLine(states, now, columns, version) {
9638
+ const all = [...states.values()];
9639
+ if (all.length === 0) return "";
9640
+ const running = all.filter((a) => a.status === "running");
9641
+ const done = all.filter((a) => a.status === "done").length;
9642
+ const failed = all.filter((a) => a.status === "failed").length;
9643
+ const versionChip = version ? `${color.bold("WS")}${color.dim(` v${version}`)} ${color.dim("\u2502")} ` : "";
9644
+ const counts = versionChip + `${color.cyan("\u27F3 fleet")} ${color.yellow(`\u25B6${running.length}`)} ${color.green(`\u2713${done}`)}` + (failed > 0 ? ` ${color.red(`\u2717${failed}`)}` : "");
9645
+ const shown = running.sort((a, b) => a.startedAt - b.startedAt).slice(0, 4).map((a) => {
9646
+ const elapsed = fmtElapsed(Math.max(0, now - a.startedAt));
9647
+ const tool = a.lastTool ? ` ${color.dim(a.lastTool)}` : "";
9648
+ const ext = a.extensions && a.extensions > 0 ? ` ${color.yellow(`\u26A1${a.extensions}`)}` : "";
9649
+ return `${color.bold(a.name)} ${color.yellow("\u25B6")} ${color.dim(elapsed)} ${color.dim(`L${a.iterations}`)} ${color.dim(`${a.toolCalls}t`)}${tool}${ext}`;
9650
+ });
9651
+ let line = shown.length > 0 ? `${counts} ${color.dim("\u2502")} ${shown.join(color.dim(" \xB7 "))}` : counts;
9652
+ const max = Math.max(20, columns - 1);
9653
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
9654
+ if (visible.length > max) {
9655
+ let count = 0;
9656
+ let out = "";
9657
+ let i = 0;
9658
+ while (i < line.length && count < max - 1) {
9659
+ if (line[i] === "\x1B") {
9660
+ const end = line.indexOf("m", i);
9661
+ if (end !== -1) {
9662
+ out += line.slice(i, end + 1);
9663
+ i = end + 1;
9664
+ continue;
9665
+ }
9666
+ }
9667
+ out += line[i];
9668
+ count++;
9669
+ i++;
9670
+ }
9671
+ line = out + "\u2026";
9672
+ }
9673
+ return line;
9674
+ }
9675
+ var FleetStatusLine = class {
9676
+ events;
9677
+ out;
9678
+ throttleMs;
9679
+ now;
9680
+ version;
9681
+ states = /* @__PURE__ */ new Map();
9682
+ unsubs = [];
9683
+ active = false;
9684
+ rows = 0;
9685
+ repaintTimer = null;
9686
+ tickTimer = null;
9687
+ lastPaint = 0;
9688
+ onResize = () => {
9689
+ if (this.active) {
9690
+ this.rows = this.out.rows ?? 24;
9691
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
9692
+ this.paint(true);
9693
+ }
9694
+ };
9695
+ constructor(opts) {
9696
+ this.events = opts.events;
9697
+ this.out = opts.out ?? process.stdout;
9698
+ this.throttleMs = opts.throttleMs ?? 150;
9699
+ this.now = opts.now ?? Date.now;
9700
+ this.version = opts.version;
9701
+ }
9702
+ /** Subscribe to host fleet events. No terminal output until a subagent spawns. */
9703
+ start() {
9704
+ const ensure = (id, name) => {
9705
+ let s = this.states.get(id);
9706
+ if (!s) {
9707
+ s = {
9708
+ id,
9709
+ name: name ?? id,
9710
+ status: "running",
9711
+ iterations: 0,
9712
+ toolCalls: 0,
9713
+ startedAt: this.now()
9714
+ };
9715
+ this.states.set(id, s);
9716
+ } else if (name && s.name === s.id) {
9717
+ s.name = name;
9718
+ }
9719
+ return s;
9720
+ };
9721
+ this.unsubs.push(
9722
+ this.events.on("subagent.spawned", (e) => {
9723
+ ensure(e.subagentId, e.name);
9724
+ this.activate();
9725
+ this.schedulePaint();
9726
+ }),
9727
+ this.events.on("subagent.task_started", (e) => {
9728
+ const s = ensure(e.subagentId);
9729
+ s.status = "running";
9730
+ this.activate();
9731
+ this.schedulePaint();
9732
+ }),
9733
+ this.events.on("subagent.tool_executed", (e) => {
9734
+ const s = ensure(e.subagentId);
9735
+ s.toolCalls++;
9736
+ s.lastTool = e.name;
9737
+ this.activate();
9738
+ this.schedulePaint();
9739
+ }),
9740
+ this.events.on("subagent.iteration_summary", (e) => {
9741
+ const s = ensure(e.subagentId);
9742
+ s.iterations = e.iteration;
9743
+ if (typeof e.toolCalls === "number") s.toolCalls = e.toolCalls;
9744
+ if (e.currentTool) s.lastTool = e.currentTool;
9745
+ this.schedulePaint();
9746
+ }),
9747
+ this.events.on("subagent.budget_extended", (e) => {
9748
+ const s = ensure(e.subagentId);
9749
+ s.extensions = e.totalExtensions;
9750
+ this.activate();
9751
+ this.schedulePaint();
9752
+ }),
9753
+ this.events.on("subagent.task_completed", (e) => {
9754
+ const s = ensure(e.subagentId);
9755
+ s.status = e.status === "success" ? "done" : "failed";
9756
+ s.iterations = e.iterations;
9757
+ s.toolCalls = e.toolCalls;
9758
+ s.endedAt = this.now();
9759
+ this.schedulePaint();
9760
+ if (![...this.states.values()].some((a) => a.status === "running")) {
9761
+ setTimeout(() => {
9762
+ if (![...this.states.values()].some((a) => a.status === "running")) {
9763
+ this.deactivate();
9764
+ }
9765
+ }, 800);
9766
+ }
9767
+ })
9768
+ );
9769
+ }
9770
+ /** Unsubscribe and restore the terminal. Idempotent. */
9771
+ stop() {
9772
+ for (const u of this.unsubs.splice(0)) u();
9773
+ this.deactivate();
9774
+ }
9775
+ isTty() {
9776
+ return !!this.out.isTTY;
9777
+ }
9778
+ activate() {
9779
+ if (this.active || !this.isTty()) return;
9780
+ this.active = true;
9781
+ this.rows = this.out.rows ?? 24;
9782
+ this.out.write("\n");
9783
+ this.out.write(`\x1B[1;${this.rows - 1}r`);
9784
+ this.out.write(`\x1B[${this.rows - 1};1H`);
9785
+ this.out.on("resize", this.onResize);
9786
+ this.tickTimer = setInterval(() => this.paint(true), 1e3);
9787
+ if (this.tickTimer.unref) this.tickTimer.unref();
9788
+ this.paint(true);
9789
+ }
9790
+ deactivate() {
9791
+ if (!this.active) return;
9792
+ this.active = false;
9793
+ if (this.tickTimer) {
9794
+ clearInterval(this.tickTimer);
9795
+ this.tickTimer = null;
9796
+ }
9797
+ if (this.repaintTimer) {
9798
+ clearTimeout(this.repaintTimer);
9799
+ this.repaintTimer = null;
9800
+ }
9801
+ this.out.off("resize", this.onResize);
9802
+ if (this.isTty()) {
9803
+ this.out.write("\x1B7");
9804
+ this.out.write(`\x1B[${this.rows};1H`);
9805
+ this.out.write("\x1B[2K");
9806
+ this.out.write("\x1B8");
9807
+ this.out.write("\x1B[r");
9808
+ }
9809
+ }
9810
+ schedulePaint() {
9811
+ if (!this.active) return;
9812
+ const since = this.now() - this.lastPaint;
9813
+ if (since >= this.throttleMs) {
9814
+ this.paint(false);
9815
+ return;
9816
+ }
9817
+ if (this.repaintTimer) return;
9818
+ this.repaintTimer = setTimeout(() => {
9819
+ this.repaintTimer = null;
9820
+ this.paint(false);
9821
+ }, this.throttleMs - since);
9822
+ if (this.repaintTimer.unref) this.repaintTimer.unref();
9823
+ }
9824
+ paint(force) {
9825
+ if (!this.active || !this.isTty()) return;
9826
+ if (!force) this.lastPaint = this.now();
9827
+ else this.lastPaint = this.now();
9828
+ const line = renderFleetLine(this.states, this.now(), this.out.columns ?? 80, this.version);
9829
+ this.out.write("\x1B7");
9830
+ this.out.write(`\x1B[${this.rows};1H`);
9831
+ this.out.write("\x1B[2K");
9832
+ this.out.write(line);
9833
+ this.out.write("\x1B8");
9834
+ }
9835
+ };
9307
9836
 
9308
9837
  // src/repl.ts
9309
9838
  init_sdd();
@@ -9382,9 +9911,15 @@ async function runRepl(opts) {
9382
9911
  } else {
9383
9912
  const beforeGoal = await loadGoalSafe(opts);
9384
9913
  const beforeIter = beforeGoal?.iterations ?? 0;
9914
+ const coord = engine.getCoordinator();
9915
+ if (coord) {
9916
+ const stats = coord.getStats();
9917
+ opts.renderer.write(
9918
+ color.dim(` \u250C\u2500 Fleet: ${stats.running} running, ${stats.idle} idle, ${stats.pending} pending, ${stats.completed} done`) + "\n"
9919
+ );
9920
+ }
9385
9921
  opts.renderer.write(
9386
- color.magenta(`
9387
- \u21B3 [parallel #${beforeIter + 1}] launching fan-out\u2026
9922
+ color.magenta(` \u21B3 [parallel #${beforeIter + 1}] launching fan-out\u2026
9388
9923
  `)
9389
9924
  );
9390
9925
  interrupts = 0;
@@ -9392,6 +9927,13 @@ async function runRepl(opts) {
9392
9927
  const ok = await engine.runOneIteration();
9393
9928
  const afterGoal = await loadGoalSafe(opts);
9394
9929
  const last = afterGoal?.journal[afterGoal.journal.length - 1];
9930
+ if (coord) {
9931
+ const stats = coord.getStats();
9932
+ opts.renderer.write(
9933
+ color.dim(` \u2514\u2500 Fleet: ${stats.running} running, ${stats.idle} idle, ${stats.completed} done
9934
+ `)
9935
+ );
9936
+ }
9395
9937
  if (last) {
9396
9938
  const mark = last.status === "success" ? color.green("\u2713") : last.status === "failure" ? color.red("\u2717") : color.amber("\u2298");
9397
9939
  const tail = last.note ? color.dim(` \u2014 ${last.note.slice(0, 80)}`) : "";
@@ -9743,9 +10285,17 @@ async function renderGoalBanner(opts) {
9743
10285
  const goal = await loadGoalSafe(opts);
9744
10286
  if (!goal) return;
9745
10287
  const summary = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
10288
+ const stateColor = goal.goalState === "active" ? color.green : goal.goalState === "paused" ? color.amber : goal.goalState === "completed" ? color.green : goal.goalState === "abandoned" ? color.dim : color.dim;
9746
10289
  opts.renderer.write(
9747
- color.dim("Goal: ") + color.bold(summary) + color.dim(` (iter ${goal.iterations})`) + "\n"
10290
+ color.dim("Goal: ") + stateColor(summary) + color.dim(` [${goal.goalState}] (iter ${goal.iterations})`) + "\n"
9748
10291
  );
10292
+ if (goal.journal.length > 0) {
10293
+ const lastEntry = goal.journal[goal.journal.length - 1];
10294
+ const statusIcon2 = lastEntry.status === "success" ? "\u2713" : lastEntry.status === "failure" ? "\u2717" : lastEntry.status === "aborted" ? "\u2298" : lastEntry.status === "skipped" ? "\u229D" : "\xB7";
10295
+ opts.renderer.write(
10296
+ color.dim(` Last: ${statusIcon2} ${lastEntry.task} (${lastEntry.status})`) + "\n"
10297
+ );
10298
+ }
9749
10299
  if (goal.engineState === "running") {
9750
10300
  opts.renderer.write(
9751
10301
  color.amber(" \u21BA Eternal engine was running when last session ended.") + "\n"
@@ -9770,6 +10320,18 @@ async function renderGoalBanner(opts) {
9770
10320
  color.dim(" Use `/autonomy eternal` to resume.") + "\n"
9771
10321
  );
9772
10322
  }
10323
+ } else if (goal.goalState === "paused") {
10324
+ opts.renderer.write(
10325
+ color.amber(" \u23F8 Goal is paused. Use `/goal resume` to continue.") + "\n"
10326
+ );
10327
+ } else if (goal.goalState === "completed") {
10328
+ opts.renderer.write(
10329
+ color.green(" \u2713 Goal completed! Use `/goal clear` to set a new goal.") + "\n"
10330
+ );
10331
+ } else if (goal.goalState === "abandoned") {
10332
+ opts.renderer.write(
10333
+ color.dim(" Use `/goal clear` to set a new goal.") + "\n"
10334
+ );
9773
10335
  }
9774
10336
  opts.renderer.write("\n");
9775
10337
  }
@@ -9868,6 +10430,7 @@ async function execute(deps) {
9868
10430
  skillLoader
9869
10431
  } = deps;
9870
10432
  let code = 0;
10433
+ let fleetStatusLine = null;
9871
10434
  try {
9872
10435
  const visionAdapters = () => createToolVisionAdapters(agent.tools);
9873
10436
  const supportsVision = async () => {
@@ -9887,6 +10450,11 @@ async function execute(deps) {
9887
10450
  if ((goalFlag || askFlag) && positional.length === 0 && !promptFlag) {
9888
10451
  flags.tui = true;
9889
10452
  }
10453
+ const enteringTui = !(positional.length > 0 || promptFlag) && !!flags.tui && flags["no-tui"] !== true;
10454
+ if (!enteringTui) {
10455
+ fleetStatusLine = new FleetStatusLine({ events, version: CLI_VERSION });
10456
+ fleetStatusLine.start();
10457
+ }
9890
10458
  if (positional.length > 0 || promptFlag) {
9891
10459
  const query = positional.join(" ");
9892
10460
  const ctrl = new AbortController();
@@ -10011,6 +10579,7 @@ async function execute(deps) {
10011
10579
  setStatuslineHiddenItems,
10012
10580
  initialGoal: goalFlag,
10013
10581
  initialAsk: askFlag,
10582
+ projectRoot,
10014
10583
  getSDDContext: () => {
10015
10584
  const { getActiveSDDContext: getActiveSDDContext2 } = (init_sdd(), __toCommonJS(sdd_exports));
10016
10585
  return getActiveSDDContext2();
@@ -10097,6 +10666,7 @@ async function execute(deps) {
10097
10666
  });
10098
10667
  }
10099
10668
  } finally {
10669
+ fleetStatusLine?.stop();
10100
10670
  try {
10101
10671
  stats.render(renderer);
10102
10672
  } catch (err) {
@@ -10156,6 +10726,13 @@ var MultiAgentHost = class {
10156
10726
  getCoordinator() {
10157
10727
  return this.director.coordinator;
10158
10728
  }
10729
+ /** Public accessor for the Director — used by buildRoutingRunner. */
10730
+ getDirector() {
10731
+ return this.director;
10732
+ }
10733
+ async ensureCoordinator(config) {
10734
+ await this.buildDirector();
10735
+ }
10159
10736
  async buildDirector() {
10160
10737
  if (this.director) return;
10161
10738
  const config = this.deps.configStore.get();
@@ -10211,22 +10788,41 @@ var MultiAgentHost = class {
10211
10788
  limit: payload.limit
10212
10789
  });
10213
10790
  });
10214
- this.getCoordinator().on("task.assigned", ({ task, subagentId }) => {
10215
- this.deps.events.emit("subagent.task_started", {
10216
- subagentId,
10217
- taskId: task.id,
10218
- description: task.description
10791
+ this.director.fleet.filter("budget.extended", (e) => {
10792
+ const payload = e.payload;
10793
+ this.deps.events.emit("subagent.budget_extended", {
10794
+ subagentId: e.subagentId,
10795
+ kind: payload.kind,
10796
+ newLimit: payload.newLimit,
10797
+ totalExtensions: payload.totalExtensions
10219
10798
  });
10220
10799
  });
10221
- const runner = this.buildSubagentRunner(config);
10800
+ this.getCoordinator().on(
10801
+ "task.assigned",
10802
+ ({
10803
+ task,
10804
+ subagentId
10805
+ }) => {
10806
+ this.deps.events.emit("subagent.task_started", {
10807
+ subagentId,
10808
+ taskId: task.id,
10809
+ description: task.description
10810
+ });
10811
+ }
10812
+ );
10813
+ const runner = await this.buildSubagentRunner(config);
10222
10814
  this.getCoordinator().setRunner(runner);
10223
10815
  }
10224
10816
  /**
10225
- * Build the per-subagent runner: agent factory runner. Extracted so
10226
- * ensureCoordinator stays focused on orchestration setup.
10817
+ * Build a per-role subagent factory: given a SubagentConfig, construct a
10818
+ * fresh, isolated Agent with the role's filtered tools and (when the config
10819
+ * carries one) the role's persona as an appended system-prompt block. Public
10820
+ * so the autonomy-parallel engine can reuse the exact same agent-construction
10821
+ * path the director/spawn flow uses — each parallel slot then runs as a real,
10822
+ * specialized, concurrency-safe agent instead of sharing the leader's Context.
10227
10823
  */
10228
- buildSubagentRunner(config) {
10229
- const factory = async (subCfg) => {
10824
+ makeSubagentFactory(config) {
10825
+ return async (subCfg) => {
10230
10826
  const events = new EventBus();
10231
10827
  const provider = await this.buildSubagentProvider(config, subCfg.provider);
10232
10828
  const baseSystem = await this.deps.systemPromptBuilder.build({
@@ -10241,6 +10837,9 @@ var MultiAgentHost = class {
10241
10837
  // meaningless to a single delegated subtask.
10242
10838
  subagent: true
10243
10839
  });
10840
+ if (subCfg.systemPromptOverride) {
10841
+ baseSystem.push({ type: "text", text: subCfg.systemPromptOverride });
10842
+ }
10244
10843
  let subSession;
10245
10844
  if (this.sessionFactory) {
10246
10845
  const subagentName = subCfg.id ?? subCfg.name ?? `sub_${randomUUID().slice(0, 8)}`;
@@ -10319,7 +10918,22 @@ var MultiAgentHost = class {
10319
10918
  };
10320
10919
  return { agent, events, dispose };
10321
10920
  };
10322
- return makeAgentSubagentRunner({ factory, fleetBus: this.director?.fleet ?? NULL_FLEET_BUS });
10921
+ }
10922
+ /**
10923
+ * Build the per-subagent runner.
10924
+ *
10925
+ * ACP agents (provider: 'acp') get their own runner via
10926
+ * makeACPSubagentRunner — they run external processes and don't go
10927
+ * through the Agent factory. Regular agents use the standard
10928
+ * makeAgentSubagentRunner path.
10929
+ */
10930
+ async buildSubagentRunner(config) {
10931
+ return buildRoutingRunner(config, this);
10932
+ }
10933
+ async buildACPRunner(subagentId) {
10934
+ const cmd = ACP_AGENT_COMMANDS[subagentId];
10935
+ if (!cmd) throw new Error(`Unknown ACP agent: ${subagentId}`);
10936
+ return makeACPSubagentRunner(cmd);
10323
10937
  }
10324
10938
  /**
10325
10939
  * Build a Provider for a subagent. When `overrideId` is supplied (from
@@ -10341,6 +10955,24 @@ var MultiAgentHost = class {
10341
10955
  type: providerId
10342
10956
  });
10343
10957
  }
10958
+ async spawnACP(subagentId, task, config) {
10959
+ const taskId = randomUUID();
10960
+ await this.ensureCoordinator(config);
10961
+ const coordinator = this.getCoordinator();
10962
+ const acpRunner = await this.buildACPRunner(subagentId);
10963
+ coordinator.setRunner(acpRunner);
10964
+ await coordinator.spawn({
10965
+ id: subagentId,
10966
+ name: subagentId,
10967
+ role: subagentId,
10968
+ provider: "acp"
10969
+ });
10970
+ await coordinator.assign({
10971
+ id: taskId,
10972
+ description: task
10973
+ });
10974
+ return taskId;
10975
+ }
10344
10976
  /** Returns a tool slice for the subagent — full set unless restricted. */
10345
10977
  filterTools(allow) {
10346
10978
  const all = this.deps.toolRegistry.list();
@@ -10350,7 +10982,7 @@ var MultiAgentHost = class {
10350
10982
  }
10351
10983
  subagentToolRegistry(allow) {
10352
10984
  if (!allow || allow.length === 0) return this.deps.toolRegistry;
10353
- const sub = this.deps.toolRegistry.clone();
10985
+ const sub = new ToolRegistry();
10354
10986
  for (const t of this.filterTools(allow)) sub.register(t);
10355
10987
  return sub;
10356
10988
  }
@@ -10595,6 +11227,20 @@ var MultiAgentHost = class {
10595
11227
  }
10596
11228
  }
10597
11229
  };
11230
+ function buildRoutingRunner(config, host) {
11231
+ const standardRunner = makeAgentSubagentRunner({
11232
+ factory: host.makeSubagentFactory(config),
11233
+ fleetBus: host.getDirector()?.fleet ?? NULL_FLEET_BUS
11234
+ });
11235
+ return async (task, ctx) => {
11236
+ const subCfg = ctx.config;
11237
+ if (subCfg.provider === "acp") {
11238
+ const cacheKey = subCfg.role ?? subCfg.name ?? subCfg.id;
11239
+ return host.buildACPRunner(cacheKey).then((r) => r(task, ctx));
11240
+ }
11241
+ return standardRunner(task, ctx);
11242
+ };
11243
+ }
10598
11244
  function makePromptDelegate(reader) {
10599
11245
  return async (tool, input, suggestedPattern) => {
10600
11246
  process.stdout.write("\x07");
@@ -12068,7 +12714,11 @@ Restart WrongStack to load or unload plugin code in this session.`;
12068
12714
  projectRoot,
12069
12715
  compactor: container.resolve(TOKENS.Compactor),
12070
12716
  maxContextTokens: effectiveMaxContext > 0 ? effectiveMaxContext : void 0,
12071
- onIteration: broadcastEternalIteration
12717
+ onIteration: broadcastEternalIteration,
12718
+ // Real per-role factory: each dispatched slot runs as a fresh,
12719
+ // isolated agent with the role's filtered tools + persona prompt
12720
+ // (instead of sharing the leader agent's Context).
12721
+ subagentFactory: multiAgentHost.makeSubagentFactory(config)
12072
12722
  });
12073
12723
  }
12074
12724
  void parallelEngine.prime?.();
@@ -12095,10 +12745,13 @@ Restart WrongStack to load or unload plugin code in this session.`;
12095
12745
  onBeforeExit: async () => {
12096
12746
  const { spawn: spawn3 } = await import('child_process');
12097
12747
  const cwd2 = projectRoot;
12098
- const statusResult = await new Promise((resolve4) => {
12748
+ const statusResult = await new Promise((resolve4, reject) => {
12099
12749
  const child = spawn3("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
12100
12750
  let stdout = "";
12101
- child.stdout?.on("data", (d) => stdout += d);
12751
+ child.stdout?.on("data", (d) => {
12752
+ stdout += d;
12753
+ });
12754
+ child.on("error", reject);
12102
12755
  child.on("close", (code) => resolve4({ stdout, code: code ?? 0 }));
12103
12756
  });
12104
12757
  if (statusResult.stdout.trim().length > 0) {
@@ -12143,7 +12796,11 @@ Restart WrongStack to load or unload plugin code in this session.`;
12143
12796
  provider: context.provider,
12144
12797
  model: context.model
12145
12798
  });
12146
- }
12799
+ },
12800
+ onDispatchClassify: makeProviderClassifier(
12801
+ context.provider,
12802
+ context.model
12803
+ )
12147
12804
  });
12148
12805
  for (const cmd of slashCmds) slashRegistry.register(cmd);
12149
12806
  const eternalFlag = typeof flags["eternal"] === "string" ? flags["eternal"].trim() : "";
@@ -12262,12 +12919,12 @@ if (isMain) {
12262
12919
  main(process.argv.slice(2)).then(
12263
12920
  (c) => {
12264
12921
  process.exitCode = c;
12265
- setTimeout(() => process.exit(c), 200).unref();
12922
+ setTimeout(() => process.exit(c), 500).unref();
12266
12923
  },
12267
12924
  (err) => {
12268
12925
  process.stderr.write((err instanceof Error ? err.stack : String(err)) + "\n");
12269
12926
  process.exitCode = 1;
12270
- setTimeout(() => process.exit(1), 200).unref();
12927
+ setTimeout(() => process.exit(1), 500).unref();
12271
12928
  }
12272
12929
  );
12273
12930
  }