@wrongstack/cli 0.1.7 → 0.1.9

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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { color, DefaultLogger, DefaultModelsRegistry, 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, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, atomicWrite, DefaultSessionReader, makeAgentSubagentRunner, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
2
+ import { color, DefaultLogger, DefaultModelsRegistry, 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, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, makeDirectorSessionFactory, makeAgentSubagentRunner, Director, DefaultMultiAgentCoordinator, InputBuilder, DefaultPluginAPI, atomicWrite, DefaultSessionReader, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
3
3
  import { WebSocketServer, WebSocket } from 'ws';
4
4
  import * as fs6 from 'fs/promises';
5
5
  import { writeFileSync } from 'fs';
6
6
  import { createRequire } from 'module';
7
- import * as path5 from 'path';
7
+ import * as path6 from 'path';
8
8
  import { MCPRegistry } from '@wrongstack/mcp';
9
9
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
10
10
  import { rememberTool, forgetTool } from '@wrongstack/tools';
@@ -541,7 +541,7 @@ var ReadlineInputReader = class {
541
541
  history = [];
542
542
  pending = false;
543
543
  constructor(opts = {}) {
544
- this.historyFile = opts.historyFile ?? path5.join(os3.homedir(), ".wrongstack", "history");
544
+ this.historyFile = opts.historyFile ?? path6.join(os3.homedir(), ".wrongstack", "history");
545
545
  }
546
546
  async loadHistory() {
547
547
  try {
@@ -553,7 +553,7 @@ var ReadlineInputReader = class {
553
553
  }
554
554
  async saveHistory() {
555
555
  try {
556
- await fs6.mkdir(path5.dirname(this.historyFile), { recursive: true });
556
+ await fs6.mkdir(path6.dirname(this.historyFile), { recursive: true });
557
557
  await fs6.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
558
558
  } catch {
559
559
  }
@@ -1046,6 +1046,7 @@ function buildBuiltinSlashCommands(opts) {
1046
1046
  statsCommand(opts),
1047
1047
  spawnCommand(opts),
1048
1048
  agentsCommand(opts),
1049
+ fleetCommand(opts),
1049
1050
  metricsCommand(opts),
1050
1051
  healthCommand(opts),
1051
1052
  memoryCommand(opts),
@@ -1167,8 +1168,8 @@ function initCommand(opts) {
1167
1168
  description: "Scaffold .wrongstack/AGENTS.md in the current project.",
1168
1169
  async run(args, ctx) {
1169
1170
  const force = args.trim() === "--force";
1170
- const dir = path5.join(ctx.projectRoot, ".wrongstack");
1171
- const file = path5.join(dir, "AGENTS.md");
1171
+ const dir = path6.join(ctx.projectRoot, ".wrongstack");
1172
+ const file = path6.join(dir, "AGENTS.md");
1172
1173
  try {
1173
1174
  await fs6.access(file);
1174
1175
  if (!force) {
@@ -1199,7 +1200,7 @@ No project type auto-detected. Edit the file to add build/test commands and conv
1199
1200
  async function detectProjectFacts(root) {
1200
1201
  const facts = { hints: [] };
1201
1202
  try {
1202
- const pkg = JSON.parse(await fs6.readFile(path5.join(root, "package.json"), "utf8"));
1203
+ const pkg = JSON.parse(await fs6.readFile(path6.join(root, "package.json"), "utf8"));
1203
1204
  const scripts = pkg.scripts ?? {};
1204
1205
  const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
1205
1206
  if (scripts["build"]) facts.build = `${pm} run build`;
@@ -1210,28 +1211,28 @@ async function detectProjectFacts(root) {
1210
1211
  } catch {
1211
1212
  }
1212
1213
  try {
1213
- await fs6.access(path5.join(root, "pyproject.toml"));
1214
+ await fs6.access(path6.join(root, "pyproject.toml"));
1214
1215
  facts.test ??= "pytest";
1215
1216
  facts.lint ??= "ruff check .";
1216
1217
  facts.hints.push("pyproject.toml");
1217
1218
  } catch {
1218
1219
  }
1219
1220
  try {
1220
- await fs6.access(path5.join(root, "go.mod"));
1221
+ await fs6.access(path6.join(root, "go.mod"));
1221
1222
  facts.build ??= "go build ./...";
1222
1223
  facts.test ??= "go test ./...";
1223
1224
  facts.hints.push("go.mod");
1224
1225
  } catch {
1225
1226
  }
1226
1227
  try {
1227
- await fs6.access(path5.join(root, "Cargo.toml"));
1228
+ await fs6.access(path6.join(root, "Cargo.toml"));
1228
1229
  facts.build ??= "cargo build";
1229
1230
  facts.test ??= "cargo test";
1230
1231
  facts.hints.push("Cargo.toml");
1231
1232
  } catch {
1232
1233
  }
1233
1234
  try {
1234
- await fs6.access(path5.join(root, "Makefile"));
1235
+ await fs6.access(path6.join(root, "Makefile"));
1235
1236
  facts.build ??= "make";
1236
1237
  facts.test ??= "make test";
1237
1238
  facts.hints.push("Makefile");
@@ -1673,18 +1674,46 @@ function statusIcon(status) {
1673
1674
  if (status === "degraded") return color.yellow("\u25CF");
1674
1675
  return color.red("\u25CF");
1675
1676
  }
1677
+ function parseSpawnFlags(input) {
1678
+ const opts = {};
1679
+ let rest = input;
1680
+ const consume = (re) => {
1681
+ const m = rest.match(re);
1682
+ if (m) {
1683
+ rest = rest.slice(m[0].length).replace(/^\s+/, "");
1684
+ return m;
1685
+ }
1686
+ return null;
1687
+ };
1688
+ while (rest.length > 0) {
1689
+ let m;
1690
+ if (m = consume(/^--provider=(\S+)\s*/)) opts.provider = m[1];
1691
+ else if (m = consume(/^--model=(\S+)\s*/)) opts.model = m[1];
1692
+ else if (m = consume(/^--name=("([^"]+)"|(\S+))\s*/)) opts.name = m[2] ?? m[3];
1693
+ else if (m = consume(/^--tools=(\S+)\s*/)) opts.tools = m[1].split(",").map((t) => t.trim()).filter(Boolean);
1694
+ else if (m = consume(/^-p\s+(\S+)\s*/)) opts.provider = m[1];
1695
+ else if (m = consume(/^-m\s+(\S+)\s*/)) opts.model = m[1];
1696
+ else if (m = consume(/^-n\s+("([^"]+)"|(\S+))\s*/)) opts.name = m[2] ?? m[3];
1697
+ else break;
1698
+ }
1699
+ return { description: rest.trim(), opts };
1700
+ }
1676
1701
  function spawnCommand(opts) {
1677
1702
  return {
1678
1703
  name: "spawn",
1679
- description: "Spawn an isolated subagent to handle a task. Usage: /spawn <task description>",
1704
+ description: "Spawn an isolated subagent to handle a task. Usage: /spawn [--provider=<id>] [--model=<id>] [--name=<label>] [--tools=a,b,c] <task description>",
1680
1705
  async run(args) {
1681
- const description = args.trim();
1682
- if (!description) return { message: "Usage: /spawn <task description>" };
1706
+ const { description, opts: parsed } = parseSpawnFlags(args.trim());
1707
+ if (!description) {
1708
+ return {
1709
+ message: "Usage: /spawn [--provider=<id>] [--model=<id>] [--name=<label>] [--tools=a,b,c] <task description>"
1710
+ };
1711
+ }
1683
1712
  if (!opts.onSpawn) {
1684
1713
  return { message: "Multi-agent is not enabled in this session." };
1685
1714
  }
1686
1715
  try {
1687
- const summary = await opts.onSpawn(description);
1716
+ const summary = Object.keys(parsed).length > 0 ? await opts.onSpawn(description, parsed) : await opts.onSpawn(description);
1688
1717
  return { message: summary };
1689
1718
  } catch (err) {
1690
1719
  return {
@@ -1706,6 +1735,63 @@ function agentsCommand(opts) {
1706
1735
  }
1707
1736
  };
1708
1737
  }
1738
+ function fleetCommand(opts) {
1739
+ return {
1740
+ name: "fleet",
1741
+ description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|help]",
1742
+ help: [
1743
+ "Usage:",
1744
+ " /fleet Show fleet status (alias for /fleet status).",
1745
+ " /fleet status Pending + completed subagent task table.",
1746
+ " /fleet usage Per-subagent runtime cost \u2014 iterations, tool calls, duration.",
1747
+ " /fleet kill <id> Terminate a running subagent by id (or prefix).",
1748
+ " /fleet manifest Print the director manifest (only with --director).",
1749
+ " /fleet help Show this help.",
1750
+ "",
1751
+ "Subagent ids are returned by /spawn and listed in /fleet status."
1752
+ ].join("\n"),
1753
+ async run(args) {
1754
+ if (!opts.onFleet) {
1755
+ return { message: "Multi-agent is not enabled in this session." };
1756
+ }
1757
+ const trimmed = args.trim();
1758
+ const [verb, ...rest] = trimmed.length === 0 ? ["status"] : trimmed.split(/\s+/);
1759
+ const target = rest.join(" ").trim() || void 0;
1760
+ switch (verb) {
1761
+ case "status":
1762
+ case "usage":
1763
+ case "manifest": {
1764
+ const out = await opts.onFleet(verb, void 0);
1765
+ return { message: out };
1766
+ }
1767
+ case "kill": {
1768
+ if (!target) {
1769
+ return { message: "Usage: /fleet kill <subagent-id>" };
1770
+ }
1771
+ const out = await opts.onFleet("kill", target);
1772
+ return { message: out };
1773
+ }
1774
+ case "help":
1775
+ case "?":
1776
+ return {
1777
+ message: [
1778
+ "/fleet \u2014 inspect or control the subagent fleet",
1779
+ "",
1780
+ " /fleet \u2192 status (default)",
1781
+ " /fleet status pending + completed tasks per subagent",
1782
+ " /fleet usage iterations, tool calls, duration roll-up",
1783
+ " /fleet kill <id> terminate a subagent",
1784
+ " /fleet manifest director manifest (requires --director)"
1785
+ ].join("\n")
1786
+ };
1787
+ default:
1788
+ return {
1789
+ message: `Unknown subcommand "${verb}". Try: status | usage | kill <id> | manifest | help`
1790
+ };
1791
+ }
1792
+ }
1793
+ };
1794
+ }
1709
1795
 
1710
1796
  // src/pre-launch.ts
1711
1797
  var MANIFESTS = [
@@ -1722,13 +1808,13 @@ var MANIFESTS = [
1722
1808
  ];
1723
1809
  async function detectProjectKind(projectRoot) {
1724
1810
  try {
1725
- await fs6.access(path5.join(projectRoot, ".wrongstack", "AGENTS.md"));
1811
+ await fs6.access(path6.join(projectRoot, ".wrongstack", "AGENTS.md"));
1726
1812
  return "initialized";
1727
1813
  } catch {
1728
1814
  }
1729
1815
  for (const m of MANIFESTS) {
1730
1816
  try {
1731
- await fs6.access(path5.join(projectRoot, m));
1817
+ await fs6.access(path6.join(projectRoot, m));
1732
1818
  return "project";
1733
1819
  } catch {
1734
1820
  }
@@ -1736,8 +1822,8 @@ async function detectProjectKind(projectRoot) {
1736
1822
  return "empty";
1737
1823
  }
1738
1824
  async function scaffoldAgentsMd(projectRoot) {
1739
- const dir = path5.join(projectRoot, ".wrongstack");
1740
- const file = path5.join(dir, "AGENTS.md");
1825
+ const dir = path6.join(projectRoot, ".wrongstack");
1826
+ const file = path6.join(dir, "AGENTS.md");
1741
1827
  const facts = await detectProjectFacts(projectRoot);
1742
1828
  const body = renderAgentsTemplate(facts);
1743
1829
  await fs6.mkdir(dir, { recursive: true });
@@ -1750,7 +1836,7 @@ async function runProjectCheck(opts) {
1750
1836
  if (kind === "initialized") {
1751
1837
  renderer.write(
1752
1838
  `
1753
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path5.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1839
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path6.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1754
1840
  `
1755
1841
  );
1756
1842
  return true;
@@ -2412,7 +2498,7 @@ function renderProgress2(ratio, width) {
2412
2498
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
2413
2499
  }
2414
2500
  async function bootConfig(flags) {
2415
- const cwd = typeof flags["cwd"] === "string" ? path5.resolve(flags["cwd"]) : process.cwd();
2501
+ const cwd = typeof flags["cwd"] === "string" ? path6.resolve(flags["cwd"]) : process.cwd();
2416
2502
  const pathResolver = new DefaultPathResolver(cwd);
2417
2503
  const projectRoot = pathResolver.projectRoot;
2418
2504
  const userHome = os3.homedir();
@@ -2474,31 +2560,71 @@ async function ensureProjectMeta(paths, projectRoot) {
2474
2560
  }
2475
2561
  }
2476
2562
  var MultiAgentHost = class {
2477
- constructor(deps) {
2563
+ constructor(deps, opts = {}) {
2478
2564
  this.deps = deps;
2565
+ this.opts = opts;
2479
2566
  }
2480
2567
  deps;
2481
2568
  coordinator;
2569
+ /** Lazily built when `opts.directorMode` is set. Owns its own internal
2570
+ * coordinator; the host's `coordinator` field still points at it so
2571
+ * the rest of the methods don't need to branch. */
2572
+ director;
2573
+ /** Lazily built alongside the director — produces per-subagent JSONL
2574
+ * writers under `<sessionsRoot>/<runId>/`. Null in non-director mode. */
2575
+ sessionFactory;
2482
2576
  pending = /* @__PURE__ */ new Map();
2483
2577
  results = [];
2578
+ opts;
2579
+ /**
2580
+ * Force the lazy build path to run *now* and return the live Director,
2581
+ * or null when director mode is off. Used by the CLI to register the
2582
+ * fleet's LLM-callable orchestration tools (spawn_subagent,
2583
+ * assign_task, await_tasks, ask_subagent, roll_up, terminate_subagent,
2584
+ * fleet_status, fleet_usage) into the leader's ToolRegistry before the
2585
+ * agent starts — without this, the leader literally cannot see the
2586
+ * orchestration tools and `--director` becomes a no-op.
2587
+ */
2588
+ async ensureDirector() {
2589
+ if (!this.opts.directorMode) return null;
2590
+ await this.ensureCoordinator();
2591
+ return this.director ?? null;
2592
+ }
2484
2593
  async ensureCoordinator() {
2485
2594
  if (this.coordinator) return this.coordinator;
2486
2595
  const config = this.deps.configStore.get();
2596
+ if (this.opts.directorMode && this.opts.sessionsRoot && !this.sessionFactory) {
2597
+ this.sessionFactory = makeDirectorSessionFactory({
2598
+ sessionsRoot: this.opts.sessionsRoot,
2599
+ directorRunId: this.opts.directorRunId
2600
+ });
2601
+ }
2487
2602
  const factory = async (subCfg) => {
2488
2603
  const events = new EventBus();
2489
- const provider = await this.buildSubagentProvider(config);
2604
+ const provider = await this.buildSubagentProvider(config, subCfg.provider);
2490
2605
  const baseSystem = await this.deps.systemPromptBuilder.build({
2491
2606
  cwd: this.deps.cwd,
2492
2607
  projectRoot: this.deps.projectRoot,
2493
2608
  tools: this.filterTools(subCfg.tools),
2494
2609
  model: subCfg.model ?? config.model,
2495
- provider: config.provider
2610
+ provider: subCfg.provider ?? config.provider
2496
2611
  });
2497
- const parentSession = this.deps.session;
2498
- const subSession = {
2499
- id: parentSession.id,
2500
- append: (ev) => parentSession.append({ ...ev })
2501
- };
2612
+ let subSession;
2613
+ if (this.sessionFactory) {
2614
+ const subagentName = subCfg.name ?? subCfg.id ?? `sub_${randomUUID().slice(0, 8)}`;
2615
+ subSession = await this.sessionFactory.createSubagentSession({
2616
+ subagentId: subagentName,
2617
+ provider: subCfg.provider ?? config.provider,
2618
+ model: subCfg.model ?? config.model,
2619
+ title: `subagent: ${subagentName}`
2620
+ });
2621
+ } else {
2622
+ const parentSession = this.deps.session;
2623
+ subSession = {
2624
+ id: parentSession.id,
2625
+ append: (ev) => parentSession.append({ ...ev })
2626
+ };
2627
+ }
2502
2628
  const ctx = new Context({
2503
2629
  systemPrompt: baseSystem,
2504
2630
  provider,
@@ -2526,15 +2652,27 @@ var MultiAgentHost = class {
2526
2652
  return { agent, events };
2527
2653
  };
2528
2654
  const runner = makeAgentSubagentRunner({ factory });
2529
- this.coordinator = new DefaultMultiAgentCoordinator(
2530
- {
2531
- coordinatorId: randomUUID(),
2532
- doneCondition: { type: "all_tasks_done" },
2533
- maxConcurrent: 2,
2534
- defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
2535
- },
2536
- { runner }
2537
- );
2655
+ const coordinatorConfig = {
2656
+ coordinatorId: randomUUID(),
2657
+ doneCondition: { type: "all_tasks_done" },
2658
+ maxConcurrent: 2,
2659
+ defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
2660
+ };
2661
+ if (this.opts.directorMode) {
2662
+ this.director = new Director({
2663
+ config: coordinatorConfig,
2664
+ runner,
2665
+ manifestPath: this.opts.manifestPath,
2666
+ sharedScratchpadPath: this.opts.sharedScratchpadPath
2667
+ });
2668
+ this.director.on("task.completed", ({ task, result }) => {
2669
+ this.results.push(result);
2670
+ this.pending.delete(task.id);
2671
+ });
2672
+ this.coordinator = this.director.coordinator;
2673
+ return this.coordinator;
2674
+ }
2675
+ this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, { runner });
2538
2676
  this.coordinator.on(
2539
2677
  "task.completed",
2540
2678
  ({ task, result }) => {
@@ -2544,15 +2682,24 @@ var MultiAgentHost = class {
2544
2682
  );
2545
2683
  return this.coordinator;
2546
2684
  }
2547
- async buildSubagentProvider(config) {
2548
- const newCfg = config.providers?.[config.provider] ?? {
2549
- type: config.provider,
2685
+ /**
2686
+ * Build a Provider for a subagent. When `overrideId` is supplied (from
2687
+ * `SubagentConfig.provider`), looks that provider up in
2688
+ * `config.providers` and constructs it with its own apiKey/baseUrl.
2689
+ * Falls back to the leader's provider when `overrideId` is absent or
2690
+ * not configured (so a typo doesn't crash the whole run — we just
2691
+ * use the leader and the calling code can decide to error later).
2692
+ */
2693
+ async buildSubagentProvider(config, overrideId) {
2694
+ const providerId = overrideId && config.providers?.[overrideId] ? overrideId : config.provider;
2695
+ const newCfg = config.providers?.[providerId] ?? {
2696
+ type: providerId,
2550
2697
  apiKey: config.apiKey,
2551
2698
  baseUrl: config.baseUrl
2552
2699
  };
2553
- return makeProviderFromConfig(config.provider, {
2700
+ return makeProviderFromConfig(providerId, {
2554
2701
  ...newCfg,
2555
- type: config.provider
2702
+ type: providerId
2556
2703
  });
2557
2704
  }
2558
2705
  /** Returns a tool slice for the subagent — full set unless restricted. */
@@ -2569,15 +2716,40 @@ var MultiAgentHost = class {
2569
2716
  for (const t of this.filterTools(allow)) sub.register(t);
2570
2717
  return sub;
2571
2718
  }
2572
- /** Spawn a fresh subagent and assign a single task. Returns task id. */
2573
- async spawn(description) {
2574
- const coord = await this.ensureCoordinator();
2575
- const spawned = await coord.spawn({
2576
- name: "adhoc",
2719
+ /**
2720
+ * Spawn a fresh subagent and assign a single task. Returns task id.
2721
+ *
2722
+ * Optional `opts` lets the caller (a `/spawn` slash command or the
2723
+ * future director surface) override the subagent's provider, model,
2724
+ * and tool slice on a per-spawn basis. Without options, the legacy
2725
+ * behavior holds: the subagent uses the leader's provider/model and
2726
+ * the full tool registry.
2727
+ */
2728
+ async spawn(description, opts) {
2729
+ await this.ensureCoordinator();
2730
+ const subagentConfig = {
2731
+ name: opts?.name ?? "adhoc",
2577
2732
  role: "general",
2578
2733
  maxToolCalls: 20,
2579
- maxIterations: 20
2580
- });
2734
+ maxIterations: 20,
2735
+ provider: opts?.provider,
2736
+ model: opts?.model,
2737
+ tools: opts?.tools
2738
+ };
2739
+ if (this.director) {
2740
+ const subagentId = await this.director.spawn(subagentConfig);
2741
+ const taskId2 = randomUUID();
2742
+ this.pending.set(taskId2, { description, subagentId });
2743
+ await this.director.assign({
2744
+ id: taskId2,
2745
+ description,
2746
+ subagentId,
2747
+ maxToolCalls: 20
2748
+ });
2749
+ return { subagentId, taskId: taskId2 };
2750
+ }
2751
+ const coord = this.coordinator;
2752
+ const spawned = await coord.spawn(subagentConfig);
2581
2753
  const taskId = randomUUID();
2582
2754
  this.pending.set(taskId, { description, subagentId: spawned.subagentId });
2583
2755
  await coord.assign({
@@ -2597,6 +2769,79 @@ var MultiAgentHost = class {
2597
2769
  const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
2598
2770
  return { pending, completed: this.results, summary };
2599
2771
  }
2772
+ /**
2773
+ * Roll up per-subagent runtime cost from completed TaskResults. We don't
2774
+ * yet have FleetUsageAggregator wired into the simple MultiAgentHost
2775
+ * path (that lives on `Director`), so this aggregates iterations / tool
2776
+ * calls / duration which we *do* have — enough to spot a thrashing
2777
+ * worker without paying for a heavier orchestrator on every /spawn.
2778
+ *
2779
+ * Returns rows sorted by total duration descending (slowest first) so
2780
+ * the table renders the most interesting subagent at the top.
2781
+ */
2782
+ usage() {
2783
+ const bySubagent = /* @__PURE__ */ new Map();
2784
+ for (const r of this.results) {
2785
+ const cur = bySubagent.get(r.subagentId) ?? { tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0, lastStatus: "unknown" };
2786
+ cur.tasks += 1;
2787
+ cur.iterations += r.iterations;
2788
+ cur.toolCalls += r.toolCalls;
2789
+ cur.durationMs += r.durationMs;
2790
+ cur.lastStatus = r.status;
2791
+ bySubagent.set(r.subagentId, cur);
2792
+ }
2793
+ const rows = Array.from(bySubagent.entries()).map(([subagentId, v]) => ({
2794
+ subagentId,
2795
+ tasks: v.tasks,
2796
+ iterations: v.iterations,
2797
+ toolCalls: v.toolCalls,
2798
+ durationMs: v.durationMs,
2799
+ status: v.lastStatus
2800
+ })).sort((a, b) => b.durationMs - a.durationMs);
2801
+ const totals = rows.reduce(
2802
+ (acc, r) => ({
2803
+ tasks: acc.tasks + r.tasks,
2804
+ iterations: acc.iterations + r.iterations,
2805
+ toolCalls: acc.toolCalls + r.toolCalls,
2806
+ durationMs: acc.durationMs + r.durationMs
2807
+ }),
2808
+ { tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0 }
2809
+ );
2810
+ return { rows, totals };
2811
+ }
2812
+ /**
2813
+ * Force the director to write its manifest to disk and return the path,
2814
+ * or `null` when director mode is off (the simple coordinator path has
2815
+ * no manifest). Callers should fall back to a friendly user message
2816
+ * when `null` is returned — e.g. `/fleet manifest` does this already.
2817
+ *
2818
+ * The returned string is the absolute path of the manifest file. The
2819
+ * file contents are JSON; readers can `JSON.parse(fs.readFileSync(...))`
2820
+ * to consume.
2821
+ */
2822
+ async manifest() {
2823
+ if (!this.director) return null;
2824
+ return this.director.writeManifest();
2825
+ }
2826
+ /**
2827
+ * True when this host is running in director mode. Surfaces the mode
2828
+ * to slash commands and tests without exposing the underlying Director
2829
+ * (which would let callers bypass the host's coordination layer).
2830
+ */
2831
+ isDirectorMode() {
2832
+ return !!this.director;
2833
+ }
2834
+ /**
2835
+ * Terminate a single subagent. Returns true when the subagent existed
2836
+ * (regardless of whether stop() succeeded or it was already idle),
2837
+ * false when no coordinator has been created yet — meaning the user
2838
+ * called /fleet kill before any /spawn, and there's nothing to do.
2839
+ */
2840
+ async kill(subagentId) {
2841
+ if (!this.coordinator) return false;
2842
+ await this.coordinator.stop(subagentId);
2843
+ return true;
2844
+ }
2600
2845
  async stopAll() {
2601
2846
  if (this.coordinator) {
2602
2847
  await this.coordinator.stopAll();
@@ -3367,8 +3612,8 @@ async function initCmd(_args, deps) {
3367
3612
  };
3368
3613
  if (apiKey) config.apiKey = apiKey;
3369
3614
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
3370
- await fs6.mkdir(path5.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3371
- const agentsFile = path5.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
3615
+ await fs6.mkdir(path6.join(deps.projectRoot, ".wrongstack"), { recursive: true });
3616
+ const agentsFile = path6.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
3372
3617
  try {
3373
3618
  await fs6.access(agentsFile);
3374
3619
  } catch {
@@ -3776,7 +4021,7 @@ async function doctorCmd(_args, deps) {
3776
4021
  }
3777
4022
  try {
3778
4023
  await fs6.mkdir(deps.paths.projectSessions, { recursive: true });
3779
- const probe = path5.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
4024
+ const probe = path6.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
3780
4025
  await fs6.writeFile(probe, "");
3781
4026
  await fs6.unlink(probe);
3782
4027
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
@@ -3879,8 +4124,8 @@ async function exportCmd(args, deps) {
3879
4124
  return 1;
3880
4125
  }
3881
4126
  if (output) {
3882
- await fs6.mkdir(path5.dirname(path5.resolve(deps.cwd, output)), { recursive: true });
3883
- await fs6.writeFile(path5.resolve(deps.cwd, output), rendered, "utf8");
4127
+ await fs6.mkdir(path6.dirname(path6.resolve(deps.cwd, output)), { recursive: true });
4128
+ await fs6.writeFile(path6.resolve(deps.cwd, output), rendered, "utf8");
3884
4129
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
3885
4130
  `);
3886
4131
  } else {
@@ -3933,13 +4178,14 @@ async function helpCmd(_args, deps) {
3933
4178
  " wstack version Print version",
3934
4179
  "",
3935
4180
  "Global flags:",
3936
- " --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config"
4181
+ " --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config",
4182
+ " --director Run with Director-backed orchestration (writes fleet manifest)"
3937
4183
  ];
3938
4184
  deps.renderer.write(lines.join("\n") + "\n");
3939
4185
  return 0;
3940
4186
  }
3941
4187
  async function projectsCmd(_args, deps) {
3942
- const projectsRoot = path5.join(deps.paths.globalRoot, "projects");
4188
+ const projectsRoot = path6.join(deps.paths.globalRoot, "projects");
3943
4189
  try {
3944
4190
  const entries = await fs6.readdir(projectsRoot);
3945
4191
  if (entries.length === 0) {
@@ -3949,7 +4195,7 @@ async function projectsCmd(_args, deps) {
3949
4195
  for (const hash of entries) {
3950
4196
  try {
3951
4197
  const meta = JSON.parse(
3952
- await fs6.readFile(path5.join(projectsRoot, hash, "meta.json"), "utf8")
4198
+ await fs6.readFile(path6.join(projectsRoot, hash, "meta.json"), "utf8")
3953
4199
  );
3954
4200
  deps.renderer.write(
3955
4201
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -4040,7 +4286,7 @@ function resolveBundledSkillsDir() {
4040
4286
  try {
4041
4287
  const req2 = createRequire(import.meta.url);
4042
4288
  const corePkg = req2.resolve("@wrongstack/core/package.json");
4043
- return path5.join(path5.dirname(corePkg), "skills");
4289
+ return path6.join(path6.dirname(corePkg), "skills");
4044
4290
  } catch {
4045
4291
  return void 0;
4046
4292
  }
@@ -4309,7 +4555,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4309
4555
  const dumpMetrics = () => {
4310
4556
  if (!metricsSink) return;
4311
4557
  try {
4312
- const out = path5.join(wpaths.projectSessions, "metrics.json");
4558
+ const out = path6.join(wpaths.projectSessions, "metrics.json");
4313
4559
  const snap = metricsSink.snapshot();
4314
4560
  writeFileSync(out, JSON.stringify(snap, null, 2));
4315
4561
  } catch {
@@ -4466,10 +4712,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4466
4712
  }
4467
4713
  await recoveryLock.write(session.id).catch(() => void 0);
4468
4714
  const attachments = new DefaultAttachmentStore({
4469
- spoolDir: path5.join(wpaths.projectSessions, session.id, "attachments")
4715
+ spoolDir: path6.join(wpaths.projectSessions, session.id, "attachments")
4470
4716
  });
4471
4717
  const queueStore = new QueueStore({
4472
- dir: path5.join(wpaths.projectSessions, session.id)
4718
+ dir: path6.join(wpaths.projectSessions, session.id)
4473
4719
  });
4474
4720
  const tokenCounter = container.resolve(TOKENS.TokenCounter);
4475
4721
  const stats = new SessionStats(events, tokenCounter);
@@ -4677,6 +4923,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4677
4923
  return err instanceof Error ? err.message : String(err);
4678
4924
  }
4679
4925
  };
4926
+ const directorMode = flags["director"] === true;
4927
+ const fleetRoot = directorMode ? path6.join(wpaths.projectSessions, session.id) : void 0;
4928
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path6.join(fleetRoot, "fleet.json") : void 0;
4929
+ const sharedScratchpadPath = directorMode ? path6.join(fleetRoot, "shared") : void 0;
4930
+ const subagentSessionsRoot = directorMode ? path6.join(fleetRoot, "subagents") : void 0;
4680
4931
  const multiAgentHost = new MultiAgentHost({
4681
4932
  container,
4682
4933
  toolRegistry,
@@ -4688,7 +4939,28 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4688
4939
  tokenCounter,
4689
4940
  projectRoot,
4690
4941
  cwd
4942
+ }, {
4943
+ directorMode,
4944
+ manifestPath,
4945
+ sharedScratchpadPath,
4946
+ sessionsRoot: subagentSessionsRoot,
4947
+ directorRunId: session.id
4691
4948
  });
4949
+ if (directorMode) {
4950
+ const director = await multiAgentHost.ensureDirector();
4951
+ if (director) {
4952
+ for (const tool of director.tools(FLEET_ROSTER)) {
4953
+ toolRegistry.register(tool);
4954
+ }
4955
+ renderer.writeInfo(`Director mode enabled. Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`);
4956
+ renderer.writeInfo(` fleet root \u2192 ${fleetRoot}`);
4957
+ renderer.writeInfo(` manifest \u2192 ${manifestPath}`);
4958
+ renderer.writeInfo(` scratchpad \u2192 ${sharedScratchpadPath}`);
4959
+ renderer.writeInfo(` subagents \u2192 ${subagentSessionsRoot}`);
4960
+ } else {
4961
+ renderer.writeInfo(`Director mode enabled. Fleet manifest \u2192 ${manifestPath}`);
4962
+ }
4963
+ }
4692
4964
  const slashCmds = buildBuiltinSlashCommands({
4693
4965
  registry: slashRegistry,
4694
4966
  toolRegistry,
@@ -4701,9 +4973,14 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4701
4973
  context,
4702
4974
  metricsSink,
4703
4975
  healthRegistry,
4704
- onSpawn: async (description) => {
4705
- const { subagentId, taskId } = await multiAgentHost.spawn(description);
4706
- return `Spawned subagent ${subagentId} for task ${taskId}. Use /agents to track progress.`;
4976
+ onSpawn: async (description, spawnOpts) => {
4977
+ const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
4978
+ const tags = [];
4979
+ if (spawnOpts?.provider) tags.push(spawnOpts.provider);
4980
+ if (spawnOpts?.model) tags.push(spawnOpts.model);
4981
+ if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
4982
+ const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
4983
+ return `Spawned subagent ${subagentId}${tag} for task ${taskId}. Use /agents to track progress.`;
4707
4984
  },
4708
4985
  onAgents: () => {
4709
4986
  const s = multiAgentHost.status();
@@ -4718,6 +4995,60 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4718
4995
  }
4719
4996
  return lines.join("\n");
4720
4997
  },
4998
+ onFleet: async (action, target) => {
4999
+ if (action === "status") {
5000
+ const s = multiAgentHost.status();
5001
+ const lines = [color.bold("Fleet status"), ` ${s.summary}`];
5002
+ if (s.pending.length > 0) {
5003
+ lines.push("", color.dim(" Pending"));
5004
+ for (const p of s.pending) {
5005
+ lines.push(` ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`);
5006
+ }
5007
+ }
5008
+ if (s.completed.length > 0) {
5009
+ lines.push("", color.dim(" Completed"));
5010
+ for (const r of s.completed) {
5011
+ const mark = r.status === "success" ? color.green("\u2713") : color.red("\u2717");
5012
+ lines.push(` ${mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${r.iterations}it ${r.toolCalls}tc ${r.durationMs}ms`);
5013
+ }
5014
+ }
5015
+ return lines.join("\n");
5016
+ }
5017
+ if (action === "usage") {
5018
+ const u = multiAgentHost.usage();
5019
+ if (u.rows.length === 0) return "No completed subagent tasks yet.";
5020
+ const lines = [
5021
+ color.bold("Fleet usage"),
5022
+ color.dim(" subagent tasks iter tools ms status")
5023
+ ];
5024
+ for (const r of u.rows) {
5025
+ lines.push(
5026
+ ` ${r.subagentId.slice(0, 14).padEnd(14)} ${String(r.tasks).padStart(5)} ${String(r.iterations).padStart(4)} ${String(r.toolCalls).padStart(5)} ${String(r.durationMs).padStart(5)} ${r.status}`
5027
+ );
5028
+ }
5029
+ lines.push(
5030
+ color.dim(" \u2500".repeat(28)),
5031
+ ` ${"TOTAL".padEnd(14)} ${String(u.totals.tasks).padStart(5)} ${String(u.totals.iterations).padStart(4)} ${String(u.totals.toolCalls).padStart(5)} ${String(u.totals.durationMs).padStart(5)}`
5032
+ );
5033
+ return lines.join("\n");
5034
+ }
5035
+ if (action === "kill") {
5036
+ if (!target) return "Usage: /fleet kill <subagent-id>";
5037
+ const ok = await multiAgentHost.kill(target);
5038
+ return ok ? `Sent stop signal to ${target}.` : "No coordinator is running yet \u2014 nothing to kill.";
5039
+ }
5040
+ if (action === "manifest") {
5041
+ if (!multiAgentHost.isDirectorMode()) {
5042
+ return "Manifest is only available when the run was started with --director.";
5043
+ }
5044
+ const p = await multiAgentHost.manifest();
5045
+ if (!p) {
5046
+ return "Director is active but no subagents have been spawned \u2014 nothing to record yet.";
5047
+ }
5048
+ return `Manifest written \u2192 ${p}`;
5049
+ }
5050
+ return `Unknown fleet action: ${action}`;
5051
+ },
4721
5052
  onExit: () => {
4722
5053
  void mcpRegistry.stopAll();
4723
5054
  },
@@ -4882,7 +5213,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4882
5213
  tokenCounter,
4883
5214
  attachments,
4884
5215
  effectiveMaxContext,
4885
- projectName: path5.basename(projectRoot) || void 0
5216
+ projectName: path6.basename(projectRoot) || void 0
4886
5217
  });
4887
5218
  await webuiPromise;
4888
5219
  } else {
@@ -4894,7 +5225,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4894
5225
  tokenCounter,
4895
5226
  attachments,
4896
5227
  effectiveMaxContext,
4897
- projectName: path5.basename(projectRoot) || void 0
5228
+ projectName: path6.basename(projectRoot) || void 0
4898
5229
  });
4899
5230
  }
4900
5231
  } finally {