@wrongstack/cli 0.1.4 → 0.1.8

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,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, 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, DefaultSessionReader, atomicWrite, 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, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, atomicWrite, DefaultSessionReader, makeAgentSubagentRunner, Director, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
3
+ import { WebSocketServer, WebSocket } from 'ws';
3
4
  import * as fs6 from 'fs/promises';
4
5
  import { writeFileSync } from 'fs';
5
6
  import { createRequire } from 'module';
@@ -34,6 +35,506 @@ var init_plugin_api_factory = __esm({
34
35
  "src/plugin-api-factory.ts"() {
35
36
  }
36
37
  });
38
+
39
+ // src/webui-server.ts
40
+ var webui_server_exports = {};
41
+ __export(webui_server_exports, {
42
+ runWebUI: () => runWebUI
43
+ });
44
+ async function runWebUI(opts) {
45
+ const port = opts.port ?? 3457;
46
+ const clients = /* @__PURE__ */ new Map();
47
+ let abortController = null;
48
+ const wss = new WebSocketServer({ port });
49
+ console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
50
+ const eventUnsubscribers = [];
51
+ function setupEvents() {
52
+ for (const unsub of eventUnsubscribers) unsub();
53
+ eventUnsubscribers.length = 0;
54
+ eventUnsubscribers.push(
55
+ opts.events.on("iteration.started", (e) => {
56
+ broadcast({
57
+ type: "iteration.started",
58
+ payload: { index: e.index }
59
+ });
60
+ })
61
+ );
62
+ eventUnsubscribers.push(
63
+ opts.events.on("provider.text_delta", (e) => {
64
+ broadcast({
65
+ type: "provider.text_delta",
66
+ payload: { text: e.text, messageId: "current" }
67
+ });
68
+ })
69
+ );
70
+ eventUnsubscribers.push(
71
+ opts.events.on("tool.started", (e) => {
72
+ broadcast({
73
+ type: "tool.started",
74
+ payload: {
75
+ id: e.id,
76
+ name: e.name,
77
+ input: e.input,
78
+ messageId: `tool_${e.id}`
79
+ }
80
+ });
81
+ })
82
+ );
83
+ eventUnsubscribers.push(
84
+ opts.events.on("tool.progress", (e) => {
85
+ broadcast({
86
+ type: "tool.progress",
87
+ payload: {
88
+ name: e.name,
89
+ id: e.id,
90
+ event: e.event
91
+ }
92
+ });
93
+ })
94
+ );
95
+ eventUnsubscribers.push(
96
+ opts.events.on("tool.executed", (e) => {
97
+ broadcast({
98
+ type: "tool.executed",
99
+ payload: {
100
+ // Forward the tool_use id so the WebUI can correlate this with
101
+ // the matching tool.started bubble for parallel tool calls.
102
+ id: e.id,
103
+ name: e.name,
104
+ durationMs: e.durationMs,
105
+ ok: e.ok,
106
+ input: e.input,
107
+ output: e.output
108
+ }
109
+ });
110
+ })
111
+ );
112
+ eventUnsubscribers.push(
113
+ opts.events.on("provider.response", (e) => {
114
+ broadcast({
115
+ type: "provider.response",
116
+ payload: {
117
+ usage: e.usage,
118
+ stopReason: e.stopReason,
119
+ messageId: "current"
120
+ }
121
+ });
122
+ })
123
+ );
124
+ eventUnsubscribers.push(
125
+ opts.events.on("error", (e) => {
126
+ broadcast({
127
+ type: "error",
128
+ payload: {
129
+ phase: e.phase,
130
+ message: e.err instanceof Error ? e.err.message : String(e.err)
131
+ }
132
+ });
133
+ })
134
+ );
135
+ }
136
+ return new Promise((resolve3) => {
137
+ wss.on("listening", () => {
138
+ console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
139
+ setupEvents();
140
+ });
141
+ wss.on("connection", (ws) => {
142
+ const client = { ws, sessionId: opts.session.id };
143
+ clients.set(ws, client);
144
+ console.log("[WebUI] Client connected");
145
+ ws.on("message", async (data) => {
146
+ try {
147
+ const msg = JSON.parse(data.toString());
148
+ await handleMessage(ws, client, msg);
149
+ } catch (err) {
150
+ console.error("[WebUI] Failed to parse message", err);
151
+ }
152
+ });
153
+ ws.on("close", () => {
154
+ console.log("[WebUI] Client disconnected");
155
+ clients.delete(ws);
156
+ });
157
+ send(ws, {
158
+ type: "session.start",
159
+ payload: {
160
+ sessionId: opts.session.id,
161
+ model: opts.agent.ctx.model,
162
+ provider: opts.agent.ctx.provider.id
163
+ }
164
+ });
165
+ });
166
+ wss.on("error", (err) => {
167
+ console.error("[WebUI] Server error:", err);
168
+ });
169
+ function shutdown() {
170
+ console.log("[WebUI] Shutting down...");
171
+ for (const unsub of eventUnsubscribers) unsub();
172
+ for (const [ws] of clients) {
173
+ ws.close();
174
+ }
175
+ clients.clear();
176
+ wss.close(() => {
177
+ console.log("[WebUI] Server stopped");
178
+ resolve3();
179
+ });
180
+ }
181
+ process.on("SIGINT", shutdown);
182
+ process.on("SIGTERM", shutdown);
183
+ });
184
+ async function handleMessage(ws, client, msg) {
185
+ switch (msg.type) {
186
+ case "user_message":
187
+ await handleUserMessage(ws, client, msg.payload.content);
188
+ break;
189
+ case "abort":
190
+ abortController?.abort();
191
+ broadcast({
192
+ type: "error",
193
+ payload: { phase: "abort", message: "User aborted" }
194
+ });
195
+ break;
196
+ case "ping":
197
+ send(ws, { type: "pong", payload: {} });
198
+ break;
199
+ case "providers.list":
200
+ await handleProvidersList(ws);
201
+ break;
202
+ case "provider.models":
203
+ await handleProviderModels(ws, msg.payload.providerId);
204
+ break;
205
+ case "providers.saved":
206
+ await handleProvidersSaved(ws);
207
+ break;
208
+ case "key.add":
209
+ case "key.update": {
210
+ const m = msg;
211
+ await handleKeyUpsert(ws, m.payload.providerId, m.payload.label, m.payload.apiKey);
212
+ break;
213
+ }
214
+ case "key.delete": {
215
+ const m = msg;
216
+ await handleKeyDelete(ws, m.payload.providerId, m.payload.label);
217
+ break;
218
+ }
219
+ case "key.set_active": {
220
+ const m = msg;
221
+ await handleKeySetActive(ws, m.payload.providerId, m.payload.label);
222
+ break;
223
+ }
224
+ case "provider.add": {
225
+ const m = msg;
226
+ await handleProviderAdd(ws, m.payload);
227
+ break;
228
+ }
229
+ case "provider.remove": {
230
+ const m = msg;
231
+ await handleProviderRemove(ws, m.payload.providerId);
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ async function handleUserMessage(ws, client, content) {
237
+ abortController?.abort();
238
+ abortController = new AbortController();
239
+ try {
240
+ const result = await opts.agent.run(content, {
241
+ signal: abortController.signal
242
+ });
243
+ send(ws, {
244
+ type: "run.result",
245
+ payload: {
246
+ status: result.status,
247
+ iterations: result.iterations,
248
+ finalText: result.finalText,
249
+ error: result.error ? {
250
+ code: result.error.code,
251
+ message: result.error.message,
252
+ recoverable: result.error.recoverable
253
+ } : void 0
254
+ }
255
+ });
256
+ } catch (err) {
257
+ send(ws, {
258
+ type: "error",
259
+ payload: {
260
+ phase: "agent.run",
261
+ message: err instanceof Error ? err.message : String(err)
262
+ }
263
+ });
264
+ } finally {
265
+ abortController = null;
266
+ }
267
+ }
268
+ function send(ws, msg) {
269
+ if (ws.readyState === WebSocket.OPEN) {
270
+ ws.send(JSON.stringify(msg));
271
+ }
272
+ }
273
+ function broadcast(msg) {
274
+ const data = JSON.stringify(msg);
275
+ for (const [ws] of clients) {
276
+ if (ws.readyState === WebSocket.OPEN) {
277
+ ws.send(data);
278
+ }
279
+ }
280
+ }
281
+ async function handleProvidersList(ws) {
282
+ if (!opts.modelsRegistry) {
283
+ sendResult(ws, false, "Models registry not available");
284
+ return;
285
+ }
286
+ try {
287
+ const providers = await opts.modelsRegistry.listProviders();
288
+ const savedProviders = await loadSavedProviders();
289
+ const savedIds = new Set(Object.keys(savedProviders));
290
+ send(ws, {
291
+ type: "provider.catalog",
292
+ payload: {
293
+ providers: providers.map((p) => ({
294
+ id: p.id,
295
+ name: p.name,
296
+ family: p.family,
297
+ apiBase: p.apiBase,
298
+ envVars: p.envVars,
299
+ modelCount: p.models.length,
300
+ hasApiKey: savedIds.has(p.id) || p.envVars.some((v) => !!process.env[v])
301
+ }))
302
+ }
303
+ });
304
+ } catch (err) {
305
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
306
+ }
307
+ }
308
+ async function handleProviderModels(ws, providerId) {
309
+ if (!opts.modelsRegistry) {
310
+ sendResult(ws, false, "Models registry not available");
311
+ return;
312
+ }
313
+ try {
314
+ const provider = await opts.modelsRegistry.getProvider(providerId);
315
+ if (!provider) {
316
+ sendResult(ws, false, `Provider "${providerId}" not found in catalog`);
317
+ return;
318
+ }
319
+ send(ws, {
320
+ type: "provider.models",
321
+ payload: {
322
+ provider: providerId,
323
+ models: provider.models.map((m) => ({
324
+ id: m.id,
325
+ name: m.name,
326
+ releaseDate: m.release_date,
327
+ contextWindow: m.limit?.context,
328
+ inputCost: m.cost?.input,
329
+ outputCost: m.cost?.output,
330
+ capabilities: [
331
+ ...m.tool_call ? ["tools"] : [],
332
+ ...m.reasoning ? ["reasoning"] : [],
333
+ ...m.modalities?.input?.includes("image") ? ["vision"] : [],
334
+ ...m.open_weights ? ["open_weights"] : []
335
+ ]
336
+ }))
337
+ }
338
+ });
339
+ } catch (err) {
340
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
341
+ }
342
+ }
343
+ async function handleProvidersSaved(ws) {
344
+ try {
345
+ const providers = await loadSavedProviders();
346
+ send(ws, {
347
+ type: "providers.saved",
348
+ payload: {
349
+ providers: Object.entries(providers).map(([id, cfg]) => ({
350
+ id,
351
+ family: cfg.family,
352
+ baseUrl: cfg.baseUrl,
353
+ apiKeys: normalizeKeys2(cfg).map((k) => ({
354
+ label: k.label,
355
+ maskedKey: maskedKey2(k.apiKey),
356
+ isActive: k.label === cfg.activeKey,
357
+ createdAt: k.createdAt
358
+ }))
359
+ }))
360
+ }
361
+ });
362
+ } catch (err) {
363
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
364
+ }
365
+ }
366
+ async function handleKeyUpsert(ws, providerId, label, apiKey) {
367
+ try {
368
+ const providers = await loadSavedProviders();
369
+ const existing = providers[providerId] ?? { type: providerId };
370
+ const keys = normalizeKeys2(existing);
371
+ const existingIdx = keys.findIndex((k) => k.label === label);
372
+ if (existingIdx >= 0) {
373
+ keys[existingIdx] = { ...keys[existingIdx], apiKey, createdAt: nowIso2() };
374
+ } else {
375
+ keys.push({ label, apiKey, createdAt: nowIso2() });
376
+ }
377
+ writeKeysBack2(existing, keys);
378
+ if (!existing.activeKey) existing.activeKey = label;
379
+ providers[providerId] = existing;
380
+ await saveProviders(providers);
381
+ sendResult(ws, true, `Key "${label}" saved for ${providerId}`);
382
+ } catch (err) {
383
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
384
+ }
385
+ }
386
+ async function handleKeyDelete(ws, providerId, label) {
387
+ try {
388
+ const providers = await loadSavedProviders();
389
+ const existing = providers[providerId];
390
+ if (!existing) {
391
+ sendResult(ws, false, `Provider "${providerId}" not found`);
392
+ return;
393
+ }
394
+ const keys = normalizeKeys2(existing).filter((k) => k.label !== label);
395
+ if (keys.length === 0) {
396
+ delete providers[providerId];
397
+ } else {
398
+ writeKeysBack2(existing, keys);
399
+ if (existing.activeKey === label) {
400
+ existing.activeKey = keys[0].label;
401
+ }
402
+ providers[providerId] = existing;
403
+ }
404
+ await saveProviders(providers);
405
+ sendResult(ws, true, `Key "${label}" deleted from ${providerId}`);
406
+ } catch (err) {
407
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
408
+ }
409
+ }
410
+ async function handleKeySetActive(ws, providerId, label) {
411
+ try {
412
+ const providers = await loadSavedProviders();
413
+ const existing = providers[providerId];
414
+ if (!existing) {
415
+ sendResult(ws, false, `Provider "${providerId}" not found`);
416
+ return;
417
+ }
418
+ existing.activeKey = label;
419
+ writeKeysBack2(existing, normalizeKeys2(existing));
420
+ providers[providerId] = existing;
421
+ await saveProviders(providers);
422
+ sendResult(ws, true, `Active key for ${providerId} set to "${label}"`);
423
+ } catch (err) {
424
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
425
+ }
426
+ }
427
+ async function handleProviderAdd(ws, payload) {
428
+ try {
429
+ const providers = await loadSavedProviders();
430
+ if (providers[payload.id]) {
431
+ sendResult(ws, false, `Provider "${payload.id}" already exists. Use key.add to add a key.`);
432
+ return;
433
+ }
434
+ const newProv = {
435
+ type: payload.id,
436
+ family: payload.family,
437
+ baseUrl: payload.baseUrl
438
+ };
439
+ if (payload.apiKey) {
440
+ newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: nowIso2() }];
441
+ newProv.activeKey = "default";
442
+ }
443
+ providers[payload.id] = newProv;
444
+ await saveProviders(providers);
445
+ sendResult(ws, true, `Provider "${payload.id}" added`);
446
+ } catch (err) {
447
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
448
+ }
449
+ }
450
+ async function handleProviderRemove(ws, providerId) {
451
+ try {
452
+ const providers = await loadSavedProviders();
453
+ if (!providers[providerId]) {
454
+ sendResult(ws, false, `Provider "${providerId}" not found`);
455
+ return;
456
+ }
457
+ delete providers[providerId];
458
+ await saveProviders(providers);
459
+ sendResult(ws, true, `Provider "${providerId}" removed`);
460
+ } catch (err) {
461
+ sendResult(ws, false, err instanceof Error ? err.message : String(err));
462
+ }
463
+ }
464
+ async function loadSavedProviders() {
465
+ if (!opts.globalConfigPath) return {};
466
+ let raw;
467
+ try {
468
+ raw = await fs6.readFile(opts.globalConfigPath, "utf8");
469
+ } catch {
470
+ return {};
471
+ }
472
+ let parsed = {};
473
+ try {
474
+ parsed = JSON.parse(raw);
475
+ } catch {
476
+ return {};
477
+ }
478
+ return parsed.providers ?? {};
479
+ }
480
+ async function saveProviders(providers) {
481
+ if (!opts.globalConfigPath) return;
482
+ let raw;
483
+ try {
484
+ raw = await fs6.readFile(opts.globalConfigPath, "utf8");
485
+ } catch {
486
+ raw = "{}";
487
+ }
488
+ let parsed;
489
+ try {
490
+ parsed = JSON.parse(raw);
491
+ } catch {
492
+ parsed = {};
493
+ }
494
+ parsed.providers = providers;
495
+ await atomicWrite(opts.globalConfigPath, JSON.stringify(parsed, null, 2), { mode: 384 });
496
+ }
497
+ function normalizeKeys2(cfg) {
498
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
499
+ return cfg.apiKeys.map((k) => ({ ...k }));
500
+ }
501
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
502
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
503
+ }
504
+ return [];
505
+ }
506
+ function writeKeysBack2(cfg, keys) {
507
+ if (keys.length === 0) {
508
+ delete cfg.apiKeys;
509
+ delete cfg.apiKey;
510
+ delete cfg.activeKey;
511
+ return;
512
+ }
513
+ cfg.apiKeys = keys;
514
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
515
+ cfg.apiKey = active.apiKey;
516
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
517
+ cfg.activeKey = active.label;
518
+ }
519
+ }
520
+ function maskedKey2(key) {
521
+ if (!key) return "\u2014";
522
+ if (key.length <= 8) return "\u2022".repeat(key.length);
523
+ const head = key.slice(0, 4);
524
+ const tail = key.slice(-4);
525
+ return `${head}\u2026${tail}`;
526
+ }
527
+ function nowIso2() {
528
+ return (/* @__PURE__ */ new Date()).toISOString();
529
+ }
530
+ function sendResult(ws, success, message) {
531
+ send(ws, { type: "key.operation_result", payload: { success, message } });
532
+ }
533
+ }
534
+ var init_webui_server = __esm({
535
+ "src/webui-server.ts"() {
536
+ }
537
+ });
37
538
  var ReadlineInputReader = class {
38
539
  rl;
39
540
  historyFile;
@@ -516,17 +1017,17 @@ async function resolveModelSelection(answer, models, provider, _registry, render
516
1017
  var theme2 = { primary: color.amber };
517
1018
  async function saveToGlobalConfig(configPath, provider, model) {
518
1019
  try {
519
- const { atomicWrite: atomicWrite3 } = await import('@wrongstack/core');
520
- const fs8 = await import('fs/promises');
1020
+ const { atomicWrite: atomicWrite4 } = await import('@wrongstack/core');
1021
+ const fs9 = await import('fs/promises');
521
1022
  let existing = {};
522
1023
  try {
523
- const raw = await fs8.readFile(configPath, "utf8");
1024
+ const raw = await fs9.readFile(configPath, "utf8");
524
1025
  existing = JSON.parse(raw);
525
1026
  } catch {
526
1027
  }
527
1028
  existing.provider = provider;
528
1029
  existing.model = model;
529
- await atomicWrite3(configPath, JSON.stringify(existing, null, 2));
1030
+ await atomicWrite4(configPath, JSON.stringify(existing, null, 2));
530
1031
  return true;
531
1032
  } catch {
532
1033
  return false;
@@ -545,9 +1046,11 @@ function buildBuiltinSlashCommands(opts) {
545
1046
  statsCommand(opts),
546
1047
  spawnCommand(opts),
547
1048
  agentsCommand(opts),
1049
+ fleetCommand(opts),
548
1050
  metricsCommand(opts),
549
1051
  healthCommand(opts),
550
1052
  memoryCommand(opts),
1053
+ todosCommand(opts),
551
1054
  saveCommand(opts),
552
1055
  loadCommand(opts),
553
1056
  exitCommand(opts)
@@ -597,6 +1100,68 @@ function memoryCommand(opts) {
597
1100
  }
598
1101
  };
599
1102
  }
1103
+ function todosCommand(opts) {
1104
+ return {
1105
+ name: "todos",
1106
+ description: "Inspect or edit the live todo list: /todos [show|clear|add <text>|done <id|index>]",
1107
+ async run(args) {
1108
+ const ctx = opts.context;
1109
+ if (!ctx) return { message: "No active context." };
1110
+ const [verb, ...rest] = args.trim().split(/\s+/);
1111
+ const restJoined = rest.join(" ").trim();
1112
+ switch (verb) {
1113
+ case "":
1114
+ case "show":
1115
+ case "list": {
1116
+ const todos = ctx.todos;
1117
+ if (todos.length === 0) {
1118
+ return { message: "No todos. The agent will add some when it plans work." };
1119
+ }
1120
+ const lines = [];
1121
+ const done = todos.filter((t) => t.status === "completed").length;
1122
+ lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
1123
+ todos.forEach((t, i) => {
1124
+ const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
1125
+ const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
1126
+ const label = t.status === "completed" ? color.dim(text) : text;
1127
+ lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
1128
+ });
1129
+ return { message: lines.join("\n") };
1130
+ }
1131
+ case "clear": {
1132
+ const n = ctx.todos.length;
1133
+ ctx.todos.length = 0;
1134
+ return { message: n === 0 ? "Todos were already empty." : `Cleared ${n} todo${n === 1 ? "" : "s"}.` };
1135
+ }
1136
+ case "add": {
1137
+ if (!restJoined) return { message: "Usage: /todos add <text>" };
1138
+ ctx.todos.push({
1139
+ id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
1140
+ content: restJoined,
1141
+ status: "pending"
1142
+ });
1143
+ return { message: `Added: ${restJoined}` };
1144
+ }
1145
+ case "done":
1146
+ case "complete": {
1147
+ if (!restJoined) return { message: "Usage: /todos done <id|index>" };
1148
+ const asIndex = Number.parseInt(restJoined, 10);
1149
+ let target = !Number.isNaN(asIndex) ? ctx.todos[asIndex - 1] : ctx.todos.find((t) => t.id === restJoined);
1150
+ if (!target) {
1151
+ target = ctx.todos.find((t) => t.content.toLowerCase().includes(restJoined.toLowerCase()));
1152
+ }
1153
+ if (!target) return { message: `No todo matched "${restJoined}".` };
1154
+ target.status = "completed";
1155
+ return { message: `Marked done: ${target.content}` };
1156
+ }
1157
+ default:
1158
+ return {
1159
+ message: `Unknown subcommand "${verb}". Try: show | clear | add <text> | done <id|index>`
1160
+ };
1161
+ }
1162
+ }
1163
+ };
1164
+ }
600
1165
  function initCommand(opts) {
601
1166
  return {
602
1167
  name: "init",
@@ -810,11 +1375,11 @@ function clearCommand(opts) {
810
1375
  ].join("\n"),
811
1376
  async run(_args, ctx) {
812
1377
  if (ctx) {
813
- ctx.messages = [];
814
- ctx.todos = [];
1378
+ ctx.state.replaceMessages([]);
1379
+ ctx.state.replaceTodos([]);
815
1380
  ctx.readFiles.clear();
816
1381
  ctx.fileMtimes.clear();
817
- ctx.meta = {};
1382
+ for (const key of Object.keys(ctx.meta)) ctx.state.deleteMeta(key);
818
1383
  }
819
1384
  await opts.memoryStore?.clear();
820
1385
  opts.onClear?.();
@@ -960,21 +1525,25 @@ ${lines.join("\n")}
960
1525
  function skillCommand(opts) {
961
1526
  return {
962
1527
  name: "skill",
963
- description: "Show a skill manifest or list skills.",
1528
+ description: "Show skill details or list available skills.",
964
1529
  async run(args) {
965
1530
  if (!opts.skillLoader) {
966
1531
  const msg = "No skill loader configured.";
967
1532
  return { message: msg };
968
1533
  }
969
1534
  if (!args.trim()) {
970
- const list = await opts.skillLoader.list();
971
- if (list.length === 0) {
1535
+ const entries = await opts.skillLoader.listEntries();
1536
+ if (entries.length === 0) {
972
1537
  const msg2 = "No skills found.";
973
1538
  return { message: msg2 };
974
1539
  }
975
- const lines = list.map((s) => ` ${s.name.padEnd(24)} ${color.dim(`[${s.source}]`)} ${s.description.split("\n")[0]}`);
976
- const msg = `Skills:
977
- ${lines.join("\n")}
1540
+ const lines = entries.map((e) => {
1541
+ const scopeTag = e.scope.length > 0 ? ` ${color.dim(`(${e.scope.slice(0, 3).join(", ")})`)}` : "";
1542
+ return ` ${color.bold(e.name)}${scopeTag}
1543
+ Use when: ${e.trigger}`;
1544
+ });
1545
+ const msg = `Available skills:
1546
+ ${lines.join("\n\n")}
978
1547
  `;
979
1548
  return { message: msg };
980
1549
  }
@@ -1105,18 +1674,46 @@ function statusIcon(status) {
1105
1674
  if (status === "degraded") return color.yellow("\u25CF");
1106
1675
  return color.red("\u25CF");
1107
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
+ }
1108
1701
  function spawnCommand(opts) {
1109
1702
  return {
1110
1703
  name: "spawn",
1111
- 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>",
1112
1705
  async run(args) {
1113
- const description = args.trim();
1114
- 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
+ }
1115
1712
  if (!opts.onSpawn) {
1116
1713
  return { message: "Multi-agent is not enabled in this session." };
1117
1714
  }
1118
1715
  try {
1119
- const summary = await opts.onSpawn(description);
1716
+ const summary = Object.keys(parsed).length > 0 ? await opts.onSpawn(description, parsed) : await opts.onSpawn(description);
1120
1717
  return { message: summary };
1121
1718
  } catch (err) {
1122
1719
  return {
@@ -1138,6 +1735,63 @@ function agentsCommand(opts) {
1138
1735
  }
1139
1736
  };
1140
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
+ }
1141
1795
 
1142
1796
  // src/pre-launch.ts
1143
1797
  var MANIFESTS = [
@@ -1498,7 +2152,7 @@ function patchConfig(base, patch) {
1498
2152
 
1499
2153
  // src/repl.ts
1500
2154
  async function runRepl(opts) {
1501
- if (opts.banner !== false) printBanner(opts.renderer);
2155
+ if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
1502
2156
  let activeCtrl;
1503
2157
  let interrupts = 0;
1504
2158
  const onSigint = () => {
@@ -1516,78 +2170,82 @@ async function runRepl(opts) {
1516
2170
  };
1517
2171
  process.on("SIGINT", onSigint);
1518
2172
  const builder = new InputBuilder({ store: opts.attachments });
1519
- for (; ; ) {
1520
- let raw;
1521
- try {
1522
- raw = await readPossiblyMultiline(opts);
1523
- } catch {
1524
- break;
1525
- }
1526
- const trimmed = raw.trim();
1527
- if (!trimmed) {
1528
- interrupts = 0;
1529
- continue;
1530
- }
1531
- interrupts = 0;
1532
- if (trimmed.startsWith("/")) {
2173
+ try {
2174
+ for (; ; ) {
2175
+ let raw;
1533
2176
  try {
1534
- const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
1535
- if (res?.message) opts.renderer.write(`${res.message}
2177
+ raw = await readPossiblyMultiline(opts);
2178
+ } catch {
2179
+ break;
2180
+ }
2181
+ const trimmed = raw.trim();
2182
+ if (!trimmed) {
2183
+ interrupts = 0;
2184
+ continue;
2185
+ }
2186
+ interrupts = 0;
2187
+ if (trimmed.startsWith("/")) {
2188
+ try {
2189
+ const res = await opts.slashRegistry.dispatch(trimmed, opts.agent.ctx);
2190
+ if (res?.message) opts.renderer.write(`${res.message}
1536
2191
  `);
1537
- if (res?.exit) break;
1538
- } catch (err) {
1539
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2192
+ if (res?.exit) break;
2193
+ } catch (err) {
2194
+ opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2195
+ }
2196
+ continue;
1540
2197
  }
1541
- continue;
1542
- }
1543
- const ph = await builder.appendPaste(raw);
1544
- if (ph) {
1545
- const lineCount = raw.split("\n").length;
1546
- opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
2198
+ const ph = await builder.appendPaste(raw);
2199
+ if (ph) {
2200
+ const lineCount = raw.split("\n").length;
2201
+ opts.renderer.write(color.dim(` \u21B3 ${ph} (${lineCount} lines)
1547
2202
  `));
1548
- }
1549
- const blocks = await builder.submit();
1550
- const runCtrl = new AbortController();
1551
- activeCtrl = runCtrl;
1552
- try {
1553
- const startedAt = Date.now();
1554
- const before = opts.tokenCounter?.total();
1555
- const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
1556
- const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
1557
- if (result.status === "aborted") {
1558
- opts.renderer.writeWarning("Aborted.");
1559
- } else if (result.status === "failed") {
1560
- const err = result.error;
1561
- if (err) {
1562
- const tag = err.recoverable ? " (recoverable)" : "";
1563
- opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
1564
- } else {
1565
- opts.renderer.writeError("Failed.");
1566
- }
1567
- } else if (result.status === "max_iterations") {
1568
- opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
1569
2203
  }
1570
- if (opts.tokenCounter && before) {
1571
- const after = opts.tokenCounter.total();
1572
- const costAfter = opts.tokenCounter.estimateCost().total;
1573
- const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
1574
- opts.renderer.write(
1575
- `
2204
+ const blocks = await builder.submit();
2205
+ const runCtrl = new AbortController();
2206
+ activeCtrl = runCtrl;
2207
+ try {
2208
+ const startedAt = Date.now();
2209
+ const before = opts.tokenCounter?.total();
2210
+ const costBefore = opts.tokenCounter?.estimateCost().total ?? 0;
2211
+ const result = await opts.agent.run(blocks, { signal: runCtrl.signal });
2212
+ if (result.status === "aborted") {
2213
+ opts.renderer.writeWarning("Aborted.");
2214
+ } else if (result.status === "failed") {
2215
+ const err = result.error;
2216
+ if (err) {
2217
+ const tag = err.recoverable ? " (recoverable)" : "";
2218
+ opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
2219
+ } else {
2220
+ opts.renderer.writeError("Failed.");
2221
+ }
2222
+ } else if (result.status === "max_iterations") {
2223
+ opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
2224
+ }
2225
+ if (opts.tokenCounter && before) {
2226
+ const after = opts.tokenCounter.total();
2227
+ const costAfter = opts.tokenCounter.estimateCost().total;
2228
+ const ctxChip = opts.effectiveMaxContext && opts.effectiveMaxContext > 0 ? ` ctx: ${renderContextChip(after.input, opts.effectiveMaxContext)}` : "";
2229
+ opts.renderer.write(
2230
+ `
1576
2231
  ${color.dim(
1577
- `[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
1578
- )}
2232
+ `[in: ${fmtTok(after.input - before.input)} out: ${fmtTok(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]${ctxChip}`
2233
+ )}
1579
2234
  `
1580
- );
2235
+ );
2236
+ }
2237
+ } catch (err) {
2238
+ opts.renderer.writeError(err instanceof Error ? err.message : String(err));
2239
+ } finally {
2240
+ activeCtrl = void 0;
1581
2241
  }
1582
- } catch (err) {
1583
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
1584
- } finally {
1585
- activeCtrl = void 0;
1586
2242
  }
2243
+ return 0;
2244
+ } finally {
2245
+ process.off("SIGINT", onSigint);
2246
+ await opts.reader.close().catch(() => {
2247
+ });
1587
2248
  }
1588
- process.off("SIGINT", onSigint);
1589
- await opts.reader.close();
1590
- return 0;
1591
2249
  }
1592
2250
  async function readPossiblyMultiline(opts) {
1593
2251
  const firstPrompt = theme.primary("\u203A ");
@@ -1624,13 +2282,15 @@ function renderProgress(ratio, width) {
1624
2282
  const capped = Math.min(width, filled);
1625
2283
  return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1626
2284
  }
1627
- function printBanner(renderer) {
2285
+ function printBanner(renderer, projectName) {
1628
2286
  const lines = [
1629
2287
  theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
1630
- color.dim("Built on the wrong stack. Shipped anyway."),
1631
- color.dim("Type /help for commands, /exit to quit."),
1632
- ""
2288
+ color.dim("Built on the wrong stack. Shipped anyway.")
1633
2289
  ];
2290
+ if (projectName && projectName.length > 0) {
2291
+ lines.push(color.dim("Project: ") + theme.bold(projectName));
2292
+ }
2293
+ lines.push(color.dim("Type /help for commands, /exit to quit."), "");
1634
2294
  renderer.write(`${lines.join("\n")}
1635
2295
  `);
1636
2296
  }
@@ -1900,25 +2560,31 @@ async function ensureProjectMeta(paths, projectRoot) {
1900
2560
  }
1901
2561
  }
1902
2562
  var MultiAgentHost = class {
1903
- constructor(deps) {
2563
+ constructor(deps, opts = {}) {
1904
2564
  this.deps = deps;
2565
+ this.opts = opts;
1905
2566
  }
1906
2567
  deps;
1907
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;
1908
2573
  pending = /* @__PURE__ */ new Map();
1909
2574
  results = [];
2575
+ opts;
1910
2576
  async ensureCoordinator() {
1911
2577
  if (this.coordinator) return this.coordinator;
1912
2578
  const config = this.deps.configStore.get();
1913
2579
  const factory = async (subCfg) => {
1914
2580
  const events = new EventBus();
1915
- const provider = await this.buildSubagentProvider(config);
2581
+ const provider = await this.buildSubagentProvider(config, subCfg.provider);
1916
2582
  const baseSystem = await this.deps.systemPromptBuilder.build({
1917
2583
  cwd: this.deps.cwd,
1918
2584
  projectRoot: this.deps.projectRoot,
1919
2585
  tools: this.filterTools(subCfg.tools),
1920
2586
  model: subCfg.model ?? config.model,
1921
- provider: config.provider
2587
+ provider: subCfg.provider ?? config.provider
1922
2588
  });
1923
2589
  const parentSession = this.deps.session;
1924
2590
  const subSession = {
@@ -1929,6 +2595,11 @@ var MultiAgentHost = class {
1929
2595
  systemPrompt: baseSystem,
1930
2596
  provider,
1931
2597
  session: subSession,
2598
+ // Placeholder — Agent.run() overwrites ctx.signal with the live
2599
+ // per-run signal (see core/agent.ts run()). Tools/middleware that
2600
+ // read ctx.signal after construction will see the runtime signal,
2601
+ // not this one. Kept as `new AbortController().signal` so the
2602
+ // initial value is non-null/non-aborted.
1932
2603
  signal: new AbortController().signal,
1933
2604
  tokenCounter: this.deps.tokenCounter,
1934
2605
  cwd: this.deps.cwd,
@@ -1947,15 +2618,26 @@ var MultiAgentHost = class {
1947
2618
  return { agent, events };
1948
2619
  };
1949
2620
  const runner = makeAgentSubagentRunner({ factory });
1950
- this.coordinator = new DefaultMultiAgentCoordinator(
1951
- {
1952
- coordinatorId: randomUUID(),
1953
- doneCondition: { type: "all_tasks_done" },
1954
- maxConcurrent: 2,
1955
- defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
1956
- },
1957
- { runner }
1958
- );
2621
+ const coordinatorConfig = {
2622
+ coordinatorId: randomUUID(),
2623
+ doneCondition: { type: "all_tasks_done" },
2624
+ maxConcurrent: 2,
2625
+ defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
2626
+ };
2627
+ if (this.opts.directorMode) {
2628
+ this.director = new Director({
2629
+ config: coordinatorConfig,
2630
+ runner,
2631
+ manifestPath: this.opts.manifestPath
2632
+ });
2633
+ this.director.on("task.completed", ({ task, result }) => {
2634
+ this.results.push(result);
2635
+ this.pending.delete(task.id);
2636
+ });
2637
+ this.coordinator = this.director.coordinator;
2638
+ return this.coordinator;
2639
+ }
2640
+ this.coordinator = new DefaultMultiAgentCoordinator(coordinatorConfig, { runner });
1959
2641
  this.coordinator.on(
1960
2642
  "task.completed",
1961
2643
  ({ task, result }) => {
@@ -1965,15 +2647,24 @@ var MultiAgentHost = class {
1965
2647
  );
1966
2648
  return this.coordinator;
1967
2649
  }
1968
- async buildSubagentProvider(config) {
1969
- const newCfg = config.providers?.[config.provider] ?? {
1970
- type: config.provider,
2650
+ /**
2651
+ * Build a Provider for a subagent. When `overrideId` is supplied (from
2652
+ * `SubagentConfig.provider`), looks that provider up in
2653
+ * `config.providers` and constructs it with its own apiKey/baseUrl.
2654
+ * Falls back to the leader's provider when `overrideId` is absent or
2655
+ * not configured (so a typo doesn't crash the whole run — we just
2656
+ * use the leader and the calling code can decide to error later).
2657
+ */
2658
+ async buildSubagentProvider(config, overrideId) {
2659
+ const providerId = overrideId && config.providers?.[overrideId] ? overrideId : config.provider;
2660
+ const newCfg = config.providers?.[providerId] ?? {
2661
+ type: providerId,
1971
2662
  apiKey: config.apiKey,
1972
2663
  baseUrl: config.baseUrl
1973
2664
  };
1974
- return makeProviderFromConfig(config.provider, {
2665
+ return makeProviderFromConfig(providerId, {
1975
2666
  ...newCfg,
1976
- type: config.provider
2667
+ type: providerId
1977
2668
  });
1978
2669
  }
1979
2670
  /** Returns a tool slice for the subagent — full set unless restricted. */
@@ -1990,15 +2681,40 @@ var MultiAgentHost = class {
1990
2681
  for (const t of this.filterTools(allow)) sub.register(t);
1991
2682
  return sub;
1992
2683
  }
1993
- /** Spawn a fresh subagent and assign a single task. Returns task id. */
1994
- async spawn(description) {
1995
- const coord = await this.ensureCoordinator();
1996
- const spawned = await coord.spawn({
1997
- name: "adhoc",
2684
+ /**
2685
+ * Spawn a fresh subagent and assign a single task. Returns task id.
2686
+ *
2687
+ * Optional `opts` lets the caller (a `/spawn` slash command or the
2688
+ * future director surface) override the subagent's provider, model,
2689
+ * and tool slice on a per-spawn basis. Without options, the legacy
2690
+ * behavior holds: the subagent uses the leader's provider/model and
2691
+ * the full tool registry.
2692
+ */
2693
+ async spawn(description, opts) {
2694
+ await this.ensureCoordinator();
2695
+ const subagentConfig = {
2696
+ name: opts?.name ?? "adhoc",
1998
2697
  role: "general",
1999
2698
  maxToolCalls: 20,
2000
- maxIterations: 20
2001
- });
2699
+ maxIterations: 20,
2700
+ provider: opts?.provider,
2701
+ model: opts?.model,
2702
+ tools: opts?.tools
2703
+ };
2704
+ if (this.director) {
2705
+ const subagentId = await this.director.spawn(subagentConfig);
2706
+ const taskId2 = randomUUID();
2707
+ this.pending.set(taskId2, { description, subagentId });
2708
+ await this.director.assign({
2709
+ id: taskId2,
2710
+ description,
2711
+ subagentId,
2712
+ maxToolCalls: 20
2713
+ });
2714
+ return { subagentId, taskId: taskId2 };
2715
+ }
2716
+ const coord = this.coordinator;
2717
+ const spawned = await coord.spawn(subagentConfig);
2002
2718
  const taskId = randomUUID();
2003
2719
  this.pending.set(taskId, { description, subagentId: spawned.subagentId });
2004
2720
  await coord.assign({
@@ -2018,6 +2734,79 @@ var MultiAgentHost = class {
2018
2734
  const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
2019
2735
  return { pending, completed: this.results, summary };
2020
2736
  }
2737
+ /**
2738
+ * Roll up per-subagent runtime cost from completed TaskResults. We don't
2739
+ * yet have FleetUsageAggregator wired into the simple MultiAgentHost
2740
+ * path (that lives on `Director`), so this aggregates iterations / tool
2741
+ * calls / duration which we *do* have — enough to spot a thrashing
2742
+ * worker without paying for a heavier orchestrator on every /spawn.
2743
+ *
2744
+ * Returns rows sorted by total duration descending (slowest first) so
2745
+ * the table renders the most interesting subagent at the top.
2746
+ */
2747
+ usage() {
2748
+ const bySubagent = /* @__PURE__ */ new Map();
2749
+ for (const r of this.results) {
2750
+ const cur = bySubagent.get(r.subagentId) ?? { tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0, lastStatus: "unknown" };
2751
+ cur.tasks += 1;
2752
+ cur.iterations += r.iterations;
2753
+ cur.toolCalls += r.toolCalls;
2754
+ cur.durationMs += r.durationMs;
2755
+ cur.lastStatus = r.status;
2756
+ bySubagent.set(r.subagentId, cur);
2757
+ }
2758
+ const rows = Array.from(bySubagent.entries()).map(([subagentId, v]) => ({
2759
+ subagentId,
2760
+ tasks: v.tasks,
2761
+ iterations: v.iterations,
2762
+ toolCalls: v.toolCalls,
2763
+ durationMs: v.durationMs,
2764
+ status: v.lastStatus
2765
+ })).sort((a, b) => b.durationMs - a.durationMs);
2766
+ const totals = rows.reduce(
2767
+ (acc, r) => ({
2768
+ tasks: acc.tasks + r.tasks,
2769
+ iterations: acc.iterations + r.iterations,
2770
+ toolCalls: acc.toolCalls + r.toolCalls,
2771
+ durationMs: acc.durationMs + r.durationMs
2772
+ }),
2773
+ { tasks: 0, iterations: 0, toolCalls: 0, durationMs: 0 }
2774
+ );
2775
+ return { rows, totals };
2776
+ }
2777
+ /**
2778
+ * Force the director to write its manifest to disk and return the path,
2779
+ * or `null` when director mode is off (the simple coordinator path has
2780
+ * no manifest). Callers should fall back to a friendly user message
2781
+ * when `null` is returned — e.g. `/fleet manifest` does this already.
2782
+ *
2783
+ * The returned string is the absolute path of the manifest file. The
2784
+ * file contents are JSON; readers can `JSON.parse(fs.readFileSync(...))`
2785
+ * to consume.
2786
+ */
2787
+ async manifest() {
2788
+ if (!this.director) return null;
2789
+ return this.director.writeManifest();
2790
+ }
2791
+ /**
2792
+ * True when this host is running in director mode. Surfaces the mode
2793
+ * to slash commands and tests without exposing the underlying Director
2794
+ * (which would let callers bypass the host's coordination layer).
2795
+ */
2796
+ isDirectorMode() {
2797
+ return !!this.director;
2798
+ }
2799
+ /**
2800
+ * Terminate a single subagent. Returns true when the subagent existed
2801
+ * (regardless of whether stop() succeeded or it was already idle),
2802
+ * false when no coordinator has been created yet — meaning the user
2803
+ * called /fleet kill before any /spawn, and there's nothing to do.
2804
+ */
2805
+ async kill(subagentId) {
2806
+ if (!this.coordinator) return false;
2807
+ await this.coordinator.stop(subagentId);
2808
+ return true;
2809
+ }
2021
2810
  async stopAll() {
2022
2811
  if (this.coordinator) {
2023
2812
  await this.coordinator.stopAll();
@@ -3354,7 +4143,8 @@ async function helpCmd(_args, deps) {
3354
4143
  " wstack version Print version",
3355
4144
  "",
3356
4145
  "Global flags:",
3357
- " --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config"
4146
+ " --provider, --model, --cwd, --log-level, --yolo, --verbose, --trace, --config",
4147
+ " --director Run with Director-backed orchestration (writes fleet manifest)"
3358
4148
  ];
3359
4149
  deps.renderer.write(lines.join("\n") + "\n");
3360
4150
  return 0;
@@ -3418,7 +4208,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
3418
4208
  "alt-screen",
3419
4209
  "output-json",
3420
4210
  "prompt",
3421
- "metrics"
4211
+ "metrics",
4212
+ "webui"
3422
4213
  ]);
3423
4214
  function parseArgs(argv) {
3424
4215
  const flags = {};
@@ -3618,6 +4409,8 @@ async function main(argv) {
3618
4409
  TOKENS.TokenCounter,
3619
4410
  () => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
3620
4411
  );
4412
+ const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
4413
+ container.bind(TOKENS.ModeStore, () => modeStore);
3621
4414
  container.bind(
3622
4415
  TOKENS.SessionStore,
3623
4416
  () => new DefaultSessionStore({ dir: wpaths.projectSessions })
@@ -3629,11 +4422,25 @@ async function main(argv) {
3629
4422
  bundledDir: config.features.skills ? resolveBundledSkillsDir() : void 0
3630
4423
  });
3631
4424
  container.bind(TOKENS.SkillLoader, () => skillLoader);
4425
+ const activeMode = await modeStore.getActiveMode();
4426
+ const modeId = activeMode?.id ?? "default";
4427
+ const modePrompt = activeMode?.prompt ?? "";
4428
+ const resolvedModel = await modelsRegistry.getModel(config.provider, config.model);
4429
+ const modelCapabilities = resolvedModel?.capabilities ? {
4430
+ maxContextTokens: resolvedModel.capabilities.maxContext,
4431
+ supportsTools: resolvedModel.capabilities.tools,
4432
+ supportsVision: resolvedModel.capabilities.vision,
4433
+ supportsReasoning: resolvedModel.capabilities.reasoning
4434
+ } : void 0;
3632
4435
  container.bind(
3633
4436
  TOKENS.SystemPromptBuilder,
3634
4437
  () => new DefaultSystemPromptBuilder({
3635
4438
  memoryStore,
3636
- skillLoader: config.features.skills ? skillLoader : void 0
4439
+ skillLoader: config.features.skills ? skillLoader : void 0,
4440
+ modeStore,
4441
+ modeId,
4442
+ modePrompt,
4443
+ modelCapabilities
3637
4444
  })
3638
4445
  );
3639
4446
  container.bind(TOKENS.Renderer, () => renderer);
@@ -3897,7 +4704,7 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
3897
4704
  model: config.model
3898
4705
  });
3899
4706
  if (restoredMessages.length > 0) {
3900
- context.messages.push(...restoredMessages);
4707
+ context.state.replaceMessages(restoredMessages);
3901
4708
  }
3902
4709
  const pipelines = createDefaultPipelines();
3903
4710
  const installBoundary = (p) => {
@@ -3957,7 +4764,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
3957
4764
  soft: config.context.softThreshold,
3958
4765
  hard: config.context.hardThreshold
3959
4766
  },
3960
- "soft"
4767
+ {
4768
+ aggressiveOn: "soft",
4769
+ failureMode: "throw_on_hard",
4770
+ events
4771
+ }
3961
4772
  );
3962
4773
  pipelines.contextWindow.use({
3963
4774
  name: "AutoCompaction",
@@ -4077,6 +4888,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4077
4888
  return err instanceof Error ? err.message : String(err);
4078
4889
  }
4079
4890
  };
4891
+ const directorMode = flags["director"] === true;
4892
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path5.join(wpaths.projectSessions, session.id, "fleet.json") : void 0;
4080
4893
  const multiAgentHost = new MultiAgentHost({
4081
4894
  container,
4082
4895
  toolRegistry,
@@ -4088,7 +4901,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4088
4901
  tokenCounter,
4089
4902
  projectRoot,
4090
4903
  cwd
4091
- });
4904
+ }, { directorMode, manifestPath });
4905
+ if (directorMode) {
4906
+ renderer.writeInfo(`Director mode enabled. Fleet manifest \u2192 ${manifestPath}`);
4907
+ }
4092
4908
  const slashCmds = buildBuiltinSlashCommands({
4093
4909
  registry: slashRegistry,
4094
4910
  toolRegistry,
@@ -4101,9 +4917,14 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4101
4917
  context,
4102
4918
  metricsSink,
4103
4919
  healthRegistry,
4104
- onSpawn: async (description) => {
4105
- const { subagentId, taskId } = await multiAgentHost.spawn(description);
4106
- return `Spawned subagent ${subagentId} for task ${taskId}. Use /agents to track progress.`;
4920
+ onSpawn: async (description, spawnOpts) => {
4921
+ const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
4922
+ const tags = [];
4923
+ if (spawnOpts?.provider) tags.push(spawnOpts.provider);
4924
+ if (spawnOpts?.model) tags.push(spawnOpts.model);
4925
+ if (spawnOpts?.name) tags.push(`"${spawnOpts.name}"`);
4926
+ const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
4927
+ return `Spawned subagent ${subagentId}${tag} for task ${taskId}. Use /agents to track progress.`;
4107
4928
  },
4108
4929
  onAgents: () => {
4109
4930
  const s = multiAgentHost.status();
@@ -4118,6 +4939,60 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4118
4939
  }
4119
4940
  return lines.join("\n");
4120
4941
  },
4942
+ onFleet: async (action, target) => {
4943
+ if (action === "status") {
4944
+ const s = multiAgentHost.status();
4945
+ const lines = [color.bold("Fleet status"), ` ${s.summary}`];
4946
+ if (s.pending.length > 0) {
4947
+ lines.push("", color.dim(" Pending"));
4948
+ for (const p of s.pending) {
4949
+ lines.push(` ${p.taskId.slice(0, 8)} \u2192 ${p.subagentId.slice(0, 8)} \xB7 ${p.description.slice(0, 60)}`);
4950
+ }
4951
+ }
4952
+ if (s.completed.length > 0) {
4953
+ lines.push("", color.dim(" Completed"));
4954
+ for (const r of s.completed) {
4955
+ const mark = r.status === "success" ? color.green("\u2713") : color.red("\u2717");
4956
+ lines.push(` ${mark} ${r.taskId.slice(0, 8)} \u2192 ${r.subagentId.slice(0, 8)} \xB7 ${r.iterations}it ${r.toolCalls}tc ${r.durationMs}ms`);
4957
+ }
4958
+ }
4959
+ return lines.join("\n");
4960
+ }
4961
+ if (action === "usage") {
4962
+ const u = multiAgentHost.usage();
4963
+ if (u.rows.length === 0) return "No completed subagent tasks yet.";
4964
+ const lines = [
4965
+ color.bold("Fleet usage"),
4966
+ color.dim(" subagent tasks iter tools ms status")
4967
+ ];
4968
+ for (const r of u.rows) {
4969
+ lines.push(
4970
+ ` ${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}`
4971
+ );
4972
+ }
4973
+ lines.push(
4974
+ color.dim(" \u2500".repeat(28)),
4975
+ ` ${"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)}`
4976
+ );
4977
+ return lines.join("\n");
4978
+ }
4979
+ if (action === "kill") {
4980
+ if (!target) return "Usage: /fleet kill <subagent-id>";
4981
+ const ok = await multiAgentHost.kill(target);
4982
+ return ok ? `Sent stop signal to ${target}.` : "No coordinator is running yet \u2014 nothing to kill.";
4983
+ }
4984
+ if (action === "manifest") {
4985
+ if (!multiAgentHost.isDirectorMode()) {
4986
+ return "Manifest is only available when the run was started with --director.";
4987
+ }
4988
+ const p = await multiAgentHost.manifest();
4989
+ if (!p) {
4990
+ return "Director is active but no subagents have been spawned \u2014 nothing to record yet.";
4991
+ }
4992
+ return `Manifest written \u2192 ${p}`;
4993
+ }
4994
+ return `Unknown fleet action: ${action}`;
4995
+ },
4121
4996
  onExit: () => {
4122
4997
  void mcpRegistry.stopAll();
4123
4998
  },
@@ -4255,11 +5130,36 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4255
5130
  process.stdout.write(
4256
5131
  color.dim(`Session saved: ${session.id} \u2014 resume with `) + color.cyan(`wstack resume ${session.id}`) + "\n"
4257
5132
  );
5133
+ },
5134
+ onClearHistory: (dispatch) => {
5135
+ dispatch({ type: "clearHistory" });
5136
+ dispatch({ type: "resetContextChip" });
4258
5137
  }
4259
5138
  });
4260
5139
  } finally {
4261
5140
  renderer.setSilent(false);
4262
5141
  }
5142
+ } else if (flags.webui) {
5143
+ const { runWebUI: runWebUI2 } = await Promise.resolve().then(() => (init_webui_server(), webui_server_exports));
5144
+ const webuiPromise = runWebUI2({
5145
+ agent,
5146
+ events,
5147
+ session,
5148
+ port: Number.parseInt(String(flags.port ?? "3457"), 10),
5149
+ modelsRegistry,
5150
+ globalConfigPath: wpaths.globalConfig
5151
+ });
5152
+ code = await runRepl({
5153
+ agent,
5154
+ renderer,
5155
+ reader,
5156
+ slashRegistry,
5157
+ tokenCounter,
5158
+ attachments,
5159
+ effectiveMaxContext,
5160
+ projectName: path5.basename(projectRoot) || void 0
5161
+ });
5162
+ await webuiPromise;
4263
5163
  } else {
4264
5164
  code = await runRepl({
4265
5165
  agent,
@@ -4268,7 +5168,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
4268
5168
  slashRegistry,
4269
5169
  tokenCounter,
4270
5170
  attachments,
4271
- effectiveMaxContext
5171
+ effectiveMaxContext,
5172
+ projectName: path5.basename(projectRoot) || void 0
4272
5173
  });
4273
5174
  }
4274
5175
  } finally {