@wrongstack/cli 0.5.7 → 0.6.1

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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import * as path21 from 'path';
2
+ 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, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, FleetManager, makeDirectorSessionFactory, Director, makeAgentSubagentRunner, NULL_FLEET_BUS, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } 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, 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, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, loadGoal, goalFilePath, summarizeUsage, emptyGoal, saveGoal, buildGoalPreamble, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, 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';
@@ -11,7 +11,6 @@ import * as crypto 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';
14
- import { writeFileSync } from 'fs';
15
14
  import { MCPRegistry } from '@wrongstack/mcp';
16
15
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
17
16
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
@@ -19,7 +18,10 @@ import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
19
18
  import * as readline from 'readline';
20
19
  import { spawn } from 'child_process';
21
20
  import { SkillInstaller } from '@wrongstack/core/skills';
21
+ import { allServers } from '@wrongstack/core/infrastructure';
22
22
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
23
+ import { ToolExecutor } from '@wrongstack/core/execution';
24
+ import { writeFileSync } from 'fs';
23
25
 
24
26
  var __defProp = Object.defineProperty;
25
27
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -261,8 +263,8 @@ function buildSddCommand(opts) {
261
263
  async run(args) {
262
264
  const ctx = opts.context;
263
265
  const projectRoot = ctx?.projectRoot ?? process.cwd();
264
- const specsDir = path21.join(projectRoot, ".wrongstack", "specs");
265
- const graphsDir = path21.join(projectRoot, ".wrongstack", "task-graphs");
266
+ const specsDir = path23.join(projectRoot, ".wrongstack", "specs");
267
+ const graphsDir = path23.join(projectRoot, ".wrongstack", "task-graphs");
266
268
  const specStore = new SpecStore({ baseDir: specsDir });
267
269
  new TaskGraphStore({ baseDir: graphsDir });
268
270
  const versioning = new SpecVersioning();
@@ -278,7 +280,7 @@ function buildSddCommand(opts) {
278
280
  const forceFlag = rest.includes("--force") || rest.includes("-f");
279
281
  const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
280
282
  if (!sddState.getBuilder() && !forceFlag) {
281
- const sessionPath = path21.join(projectRoot, ".wrongstack", "sdd-session.json");
283
+ const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
282
284
  try {
283
285
  await fsp2.access(sessionPath);
284
286
  const projectContext2 = await gatherProjectContext(projectRoot);
@@ -313,7 +315,7 @@ function buildSddCommand(opts) {
313
315
  projectContext,
314
316
  minQuestions: 2,
315
317
  maxQuestions: 10,
316
- sessionPath: path21.join(projectRoot, ".wrongstack", "sdd-session.json")
318
+ sessionPath: path23.join(projectRoot, ".wrongstack", "sdd-session.json")
317
319
  }));
318
320
  const builder = sddState.getBuilder();
319
321
  builder.startSession(title);
@@ -581,7 +583,7 @@ Start executing the tasks one by one.`
581
583
  };
582
584
  }
583
585
  case "cancel": {
584
- const sessionPath = path21.join(projectRoot, ".wrongstack", "sdd-session.json");
586
+ const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
585
587
  let deletedFromDisk = false;
586
588
  try {
587
589
  await fsp2.unlink(sessionPath);
@@ -605,7 +607,7 @@ Start executing the tasks one by one.`
605
607
  if (sddState.getBuilder()) {
606
608
  return { message: "An SDD session is already active. Use /sdd cancel first." };
607
609
  }
608
- const sessionPath = path21.join(projectRoot, ".wrongstack", "sdd-session.json");
610
+ const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
609
611
  const projectContext = await gatherProjectContext(projectRoot);
610
612
  sddState.setBuilder(new AISpecBuilder({
611
613
  store: specStore,
@@ -831,7 +833,7 @@ function sddHelp() {
831
833
  async function gatherProjectContext(projectRoot) {
832
834
  const parts = [];
833
835
  try {
834
- const pkgPath = path21.join(projectRoot, "package.json");
836
+ const pkgPath = path23.join(projectRoot, "package.json");
835
837
  const pkgRaw = await fsp2.readFile(pkgPath, "utf8");
836
838
  const pkg = JSON.parse(pkgRaw);
837
839
  parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
@@ -847,13 +849,13 @@ async function gatherProjectContext(projectRoot) {
847
849
  } catch {
848
850
  }
849
851
  try {
850
- const tsconfigPath = path21.join(projectRoot, "tsconfig.json");
852
+ const tsconfigPath = path23.join(projectRoot, "tsconfig.json");
851
853
  await fsp2.access(tsconfigPath);
852
854
  parts.push("Language: TypeScript");
853
855
  } catch {
854
856
  }
855
857
  try {
856
- const srcDir = path21.join(projectRoot, "src");
858
+ const srcDir = path23.join(projectRoot, "src");
857
859
  const entries = await fsp2.readdir(srcDir, { withFileTypes: true });
858
860
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
859
861
  if (dirs.length > 0) {
@@ -976,7 +978,7 @@ __export(update_check_exports, {
976
978
  getUpdateNotification: () => getUpdateNotification
977
979
  });
978
980
  function cachePath(homeFn = defaultHomeDir2) {
979
- return path21.join(homeFn(), ".wrongstack", "update-cache.json");
981
+ return path23.join(homeFn(), ".wrongstack", "update-cache.json");
980
982
  }
981
983
  function currentVersion() {
982
984
  const req2 = createRequire(import.meta.url);
@@ -1013,7 +1015,7 @@ async function readCache(homeFn = defaultHomeDir2) {
1013
1015
  }
1014
1016
  async function writeCache(entry, homeFn = defaultHomeDir2) {
1015
1017
  try {
1016
- const dir = path21.dirname(cachePath(homeFn));
1018
+ const dir = path23.dirname(cachePath(homeFn));
1017
1019
  await fsp2.mkdir(dir, { recursive: true });
1018
1020
  await fsp2.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
1019
1021
  } catch {
@@ -1102,7 +1104,7 @@ async function runWebUI(opts) {
1102
1104
  let abortController = null;
1103
1105
  const authToken = crypto.randomBytes(16).toString("hex");
1104
1106
  const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
1105
- console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
1107
+ console.log(`[WebUI] WebSocket server starting on ws://127.0.0.1:${port}`);
1106
1108
  const eventUnsubscribers = [];
1107
1109
  function setupEvents() {
1108
1110
  for (const unsub of eventUnsubscribers) unsub();
@@ -1196,10 +1198,29 @@ async function runWebUI(opts) {
1196
1198
  });
1197
1199
  })
1198
1200
  );
1201
+ if (opts.subscribeEternalIteration) {
1202
+ eventUnsubscribers.push(
1203
+ opts.subscribeEternalIteration((entry) => {
1204
+ broadcast({
1205
+ type: "eternal.iteration",
1206
+ payload: {
1207
+ iteration: entry.iteration,
1208
+ at: entry.at,
1209
+ source: entry.source,
1210
+ task: entry.task,
1211
+ status: entry.status,
1212
+ note: entry.note,
1213
+ tokens: entry.tokens,
1214
+ costUsd: entry.costUsd
1215
+ }
1216
+ });
1217
+ })
1218
+ );
1219
+ }
1199
1220
  }
1200
1221
  return new Promise((resolve4) => {
1201
1222
  wss.on("listening", () => {
1202
- console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
1223
+ console.log(`[WebUI] WebSocket server running on ws://127.0.0.1:${port}`);
1203
1224
  setupEvents();
1204
1225
  });
1205
1226
  wss.on("connection", (ws, req2) => {
@@ -1584,26 +1605,40 @@ async function runWebUI(opts) {
1584
1605
  return {};
1585
1606
  }
1586
1607
  if (!parsed.providers) return {};
1587
- const keyFile = path21.join(path21.dirname(opts.globalConfigPath), ".key");
1608
+ const keyFile = path23.join(path23.dirname(opts.globalConfigPath), ".key");
1588
1609
  const vault = new DefaultSecretVault$1({ keyFile });
1589
1610
  return decryptConfigSecrets$1(parsed.providers, vault);
1590
1611
  }
1591
1612
  async function saveProviders(providers) {
1592
1613
  if (!opts.globalConfigPath) return;
1593
1614
  let raw;
1615
+ let fileExists = true;
1594
1616
  try {
1595
1617
  raw = await fsp2.readFile(opts.globalConfigPath, "utf8");
1596
- } catch {
1618
+ } catch (err) {
1619
+ if (err.code !== "ENOENT") {
1620
+ throw new Error(
1621
+ `Refusing to mutate ${opts.globalConfigPath}: ${err.message}`,
1622
+ { cause: err }
1623
+ );
1624
+ }
1625
+ fileExists = false;
1597
1626
  raw = "{}";
1598
1627
  }
1599
1628
  let parsed;
1600
1629
  try {
1601
1630
  parsed = JSON.parse(raw);
1602
- } catch {
1631
+ } catch (err) {
1632
+ if (fileExists) {
1633
+ throw new Error(
1634
+ `Refusing to overwrite corrupt config at ${opts.globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
1635
+ { cause: err }
1636
+ );
1637
+ }
1603
1638
  parsed = {};
1604
1639
  }
1605
1640
  parsed.providers = providers;
1606
- const keyFile = path21.join(path21.dirname(opts.globalConfigPath), ".key");
1641
+ const keyFile = path23.join(path23.dirname(opts.globalConfigPath), ".key");
1607
1642
  const vault = new DefaultSecretVault$1({ keyFile });
1608
1643
  const encrypted = encryptConfigSecrets(parsed, vault);
1609
1644
  await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
@@ -1618,19 +1653,6 @@ var init_webui_server = __esm({
1618
1653
  }
1619
1654
  });
1620
1655
 
1621
- // src/plugin-api-factory.ts
1622
- var plugin_api_factory_exports = {};
1623
- __export(plugin_api_factory_exports, {
1624
- default: () => createApi
1625
- });
1626
- function createApi(ownerName, base) {
1627
- return new DefaultPluginAPI({ ownerName, ...base });
1628
- }
1629
- var init_plugin_api_factory = __esm({
1630
- "src/plugin-api-factory.ts"() {
1631
- }
1632
- });
1633
-
1634
1656
  // src/slash-commands/commit-llm.ts
1635
1657
  async function generateCommitMessageWithLLM(diff, opts) {
1636
1658
  const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
@@ -1788,7 +1810,7 @@ function parseSpawnFlags(input) {
1788
1810
  return { description: rest.trim(), opts };
1789
1811
  }
1790
1812
  async function bootConfig(flags) {
1791
- const cwd = typeof flags["cwd"] === "string" ? path21.resolve(flags["cwd"]) : process.cwd();
1813
+ const cwd = typeof flags["cwd"] === "string" ? path23.resolve(flags["cwd"]) : process.cwd();
1792
1814
  const pathResolver = new DefaultPathResolver(cwd);
1793
1815
  const projectRoot = pathResolver.projectRoot;
1794
1816
  const userHome = os6.homedir();
@@ -1855,7 +1877,7 @@ var ReadlineInputReader = class {
1855
1877
  history = [];
1856
1878
  pending = false;
1857
1879
  constructor(opts = {}) {
1858
- this.historyFile = opts.historyFile ?? path21.join(os6.homedir(), ".wrongstack", "history");
1880
+ this.historyFile = opts.historyFile ?? path23.join(os6.homedir(), ".wrongstack", "history");
1859
1881
  }
1860
1882
  async loadHistory() {
1861
1883
  try {
@@ -1867,7 +1889,7 @@ var ReadlineInputReader = class {
1867
1889
  }
1868
1890
  async saveHistory() {
1869
1891
  try {
1870
- await fsp2.mkdir(path21.dirname(this.historyFile), { recursive: true });
1892
+ await fsp2.mkdir(path23.dirname(this.historyFile), { recursive: true });
1871
1893
  await fsp2.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
1872
1894
  } catch {
1873
1895
  }
@@ -2094,20 +2116,20 @@ function assertSafeToDelete(filename, parentDir) {
2094
2116
  if (PROTECTED_BASENAMES.has(filename)) {
2095
2117
  throw new Error(`Refusing to delete protected file: ${filename}`);
2096
2118
  }
2097
- if (filename !== path21.basename(filename)) {
2119
+ if (filename !== path23.basename(filename)) {
2098
2120
  throw new Error(`Refusing to delete path with traversal: ${filename}`);
2099
2121
  }
2100
2122
  if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
2101
2123
  throw new Error(`Refusing to delete unknown file: ${filename}`);
2102
2124
  }
2103
- const resolvedParent = path21.resolve(parentDir);
2125
+ const resolvedParent = path23.resolve(parentDir);
2104
2126
  if (!resolvedParent.endsWith(".wrongstack")) {
2105
2127
  throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
2106
2128
  }
2107
2129
  }
2108
2130
  async function safeDelete(filePath) {
2109
- const dir = path21.dirname(filePath);
2110
- const filename = path21.basename(filePath);
2131
+ const dir = path23.dirname(filePath);
2132
+ const filename = path23.basename(filePath);
2111
2133
  try {
2112
2134
  assertSafeToDelete(filename, dir);
2113
2135
  await fsp2.unlink(filePath);
@@ -2152,16 +2174,16 @@ function diffSummary(oldCfg, newCfg) {
2152
2174
  }
2153
2175
  var defaultHomeDir = () => os6__default.homedir();
2154
2176
  function historyDir(homeFn = defaultHomeDir) {
2155
- return path21.join(homeFn(), ".wrongstack", "config.history", "entries");
2177
+ return path23.join(homeFn(), ".wrongstack", "config.history", "entries");
2156
2178
  }
2157
2179
  function historyIndexPath(homeFn = defaultHomeDir) {
2158
- return path21.join(homeFn(), ".wrongstack", "config.history", "index.json");
2180
+ return path23.join(homeFn(), ".wrongstack", "config.history", "index.json");
2159
2181
  }
2160
2182
  function configPath(homeFn = defaultHomeDir) {
2161
- return path21.join(homeFn(), ".wrongstack", "config.json");
2183
+ return path23.join(homeFn(), ".wrongstack", "config.json");
2162
2184
  }
2163
2185
  function backupLastPath(homeFn = defaultHomeDir) {
2164
- return path21.join(homeFn(), ".wrongstack", "config.json.last");
2186
+ return path23.join(homeFn(), ".wrongstack", "config.json.last");
2165
2187
  }
2166
2188
  function entryId(ts) {
2167
2189
  return ts.replace(/[:.]/g, "-").slice(0, 19);
@@ -2179,7 +2201,7 @@ async function readIndex(homeFn = defaultHomeDir) {
2179
2201
  }
2180
2202
  async function writeIndex(idx, homeFn = defaultHomeDir) {
2181
2203
  await ensureHistoryDir(homeFn);
2182
- await fsp2.writeFile(historyIndexPath(homeFn), JSON.stringify(idx, null, 2), "utf8");
2204
+ await atomicWrite(historyIndexPath(homeFn), JSON.stringify(idx, null, 2));
2183
2205
  }
2184
2206
  async function backupCurrent(homeFn = defaultHomeDir) {
2185
2207
  const cfg = configPath(homeFn);
@@ -2198,17 +2220,17 @@ async function backupCurrent(homeFn = defaultHomeDir) {
2198
2220
  }
2199
2221
  if (content !== void 0) {
2200
2222
  try {
2201
- const bakPath = path21.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
2223
+ const bakPath = path23.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
2202
2224
  await atomicWrite(bakPath, content);
2203
2225
  } catch {
2204
2226
  }
2205
2227
  }
2206
2228
  try {
2207
- const dir = path21.join(homeFn(), ".wrongstack");
2229
+ const dir = path23.join(homeFn(), ".wrongstack");
2208
2230
  const files = await fsp2.readdir(dir);
2209
2231
  const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
2210
2232
  for (const f of baks.slice(10)) {
2211
- await safeDelete(path21.join(dir, f));
2233
+ await safeDelete(path23.join(dir, f));
2212
2234
  }
2213
2235
  } catch {
2214
2236
  }
@@ -2225,7 +2247,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
2225
2247
  diffSummary: diffSummary(oldCfg, newCfg)
2226
2248
  };
2227
2249
  await fsp2.writeFile(
2228
- path21.join(historyDir(homeFn), `${id}.json`),
2250
+ path23.join(historyDir(homeFn), `${id}.json`),
2229
2251
  JSON.stringify(entry, null, 2),
2230
2252
  "utf8"
2231
2253
  );
@@ -2240,7 +2262,7 @@ async function listHistory(homeFn = defaultHomeDir) {
2240
2262
  }
2241
2263
  async function getHistoryEntry(id, homeFn = defaultHomeDir) {
2242
2264
  try {
2243
- const raw = await fsp2.readFile(path21.join(historyDir(homeFn), `${id}.json`), "utf8");
2265
+ const raw = await fsp2.readFile(path23.join(historyDir(homeFn), `${id}.json`), "utf8");
2244
2266
  return JSON.parse(raw);
2245
2267
  } catch {
2246
2268
  return null;
@@ -2299,11 +2321,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
2299
2321
  var theme = { primary: color.amber };
2300
2322
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
2301
2323
  try {
2302
- const { atomicWrite: atomicWrite7 } = await import('@wrongstack/core');
2303
- const fs17 = await import('fs/promises');
2324
+ const { atomicWrite: atomicWrite8 } = await import('@wrongstack/core');
2325
+ const fs20 = await import('fs/promises');
2304
2326
  let existing = {};
2305
2327
  try {
2306
- const raw = await fs17.readFile(configPath2, "utf8");
2328
+ const raw = await fs20.readFile(configPath2, "utf8");
2307
2329
  existing = JSON.parse(raw);
2308
2330
  } catch {
2309
2331
  }
@@ -2311,7 +2333,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
2311
2333
  existing.provider = provider;
2312
2334
  existing.model = model;
2313
2335
  await backupCurrent(homeFn);
2314
- await atomicWrite7(configPath2, JSON.stringify(existing, null, 2));
2336
+ await atomicWrite8(configPath2, JSON.stringify(existing, null, 2));
2315
2337
  try {
2316
2338
  await appendHistory(
2317
2339
  oldCfg,
@@ -2440,8 +2462,12 @@ ${color.bold(theme.primary("WrongStack") + color.dim(" \u2014 Provider & Model S
2440
2462
  const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color.dim(`[Enter = ${defaultProvider}]`)}` : "";
2441
2463
  const providerAnswer = (await reader.readLine(
2442
2464
  `
2443
- ${color.amber("?")} Select provider (1-${ordered.length})${defaultHint}: `
2465
+ ${color.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color.dim("[q to quit]")}: `
2444
2466
  )).trim();
2467
+ if (providerAnswer.toLowerCase() === "q") {
2468
+ renderer.write(color.dim("Cancelled.\n"));
2469
+ return void 0;
2470
+ }
2445
2471
  if (!providerAnswer) {
2446
2472
  if (defaultIdx !== void 0) {
2447
2473
  const def = ordered[defaultIdx - 1];
@@ -2503,15 +2529,23 @@ async function pickModel(provider, registry, renderer, reader, defaultModel) {
2503
2529
  if (offset < models.length) {
2504
2530
  const more = (await reader.readLine(
2505
2531
  `
2506
- ${color.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number or ${color.dim("Enter")} for more: `
2532
+ ${color.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number, ${color.dim("Enter")} for more, or ${color.dim("q")} to quit: `
2507
2533
  )).trim();
2534
+ if (more.toLowerCase() === "q") {
2535
+ renderer.write(color.dim("Cancelled.\n"));
2536
+ return void 0;
2537
+ }
2508
2538
  if (!more) continue;
2509
2539
  return resolveModelSelection(more, models, provider, registry, renderer);
2510
2540
  }
2511
2541
  }
2512
2542
  const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color.dim(`[Enter = ${defaultModel}]`)}` : "";
2513
2543
  const answer = (await reader.readLine(`
2514
- ${color.amber("?")} Select model (1-${models.length})${defaultHint}: `)).trim();
2544
+ ${color.amber("?")} Select model (1-${models.length})${defaultHint} ${color.dim("[q to quit]")}: `)).trim();
2545
+ if (answer.toLowerCase() === "q") {
2546
+ renderer.write(color.dim("Cancelled.\n"));
2547
+ return void 0;
2548
+ }
2515
2549
  if (!answer) {
2516
2550
  if (defaultIdxInModels >= 0 && defaultModel) {
2517
2551
  renderer.write(
@@ -2569,10 +2603,10 @@ async function detectPackageManager(root, declared) {
2569
2603
  const name = declared.split("@")[0];
2570
2604
  if (name) return name;
2571
2605
  }
2572
- if (await pathExists(path21.join(root, "pnpm-lock.yaml"))) return "pnpm";
2573
- if (await pathExists(path21.join(root, "bun.lockb"))) return "bun";
2574
- if (await pathExists(path21.join(root, "bun.lock"))) return "bun";
2575
- if (await pathExists(path21.join(root, "yarn.lock"))) return "yarn";
2606
+ if (await pathExists(path23.join(root, "pnpm-lock.yaml"))) return "pnpm";
2607
+ if (await pathExists(path23.join(root, "bun.lockb"))) return "bun";
2608
+ if (await pathExists(path23.join(root, "bun.lock"))) return "bun";
2609
+ if (await pathExists(path23.join(root, "yarn.lock"))) return "yarn";
2576
2610
  return "npm";
2577
2611
  }
2578
2612
  function hasUsableScript(scripts, name) {
@@ -2593,7 +2627,7 @@ function parseMakeTargets(makefile) {
2593
2627
  async function detectProjectFacts(root) {
2594
2628
  const facts = { hints: [] };
2595
2629
  try {
2596
- const pkg = JSON.parse(await fsp2.readFile(path21.join(root, "package.json"), "utf8"));
2630
+ const pkg = JSON.parse(await fsp2.readFile(path23.join(root, "package.json"), "utf8"));
2597
2631
  const scripts = pkg.scripts ?? {};
2598
2632
  const pm = await detectPackageManager(root, pkg.packageManager);
2599
2633
  if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
@@ -2607,14 +2641,14 @@ async function detectProjectFacts(root) {
2607
2641
  } catch {
2608
2642
  }
2609
2643
  try {
2610
- if (!await pathExists(path21.join(root, "pyproject.toml"))) throw new Error("not python");
2644
+ if (!await pathExists(path23.join(root, "pyproject.toml"))) throw new Error("not python");
2611
2645
  facts.test ??= "pytest";
2612
2646
  facts.lint ??= "ruff check .";
2613
2647
  facts.hints.push("pyproject.toml");
2614
2648
  } catch {
2615
2649
  }
2616
2650
  try {
2617
- if (!await pathExists(path21.join(root, "go.mod"))) throw new Error("not go");
2651
+ if (!await pathExists(path23.join(root, "go.mod"))) throw new Error("not go");
2618
2652
  facts.build ??= "go build ./...";
2619
2653
  facts.test ??= "go test ./...";
2620
2654
  facts.run ??= "go run .";
@@ -2622,7 +2656,7 @@ async function detectProjectFacts(root) {
2622
2656
  } catch {
2623
2657
  }
2624
2658
  try {
2625
- if (!await pathExists(path21.join(root, "Cargo.toml"))) throw new Error("not rust");
2659
+ if (!await pathExists(path23.join(root, "Cargo.toml"))) throw new Error("not rust");
2626
2660
  facts.build ??= "cargo build";
2627
2661
  facts.test ??= "cargo test";
2628
2662
  facts.lint ??= "cargo clippy";
@@ -2631,7 +2665,7 @@ async function detectProjectFacts(root) {
2631
2665
  } catch {
2632
2666
  }
2633
2667
  try {
2634
- const makefile = await fsp2.readFile(path21.join(root, "Makefile"), "utf8");
2668
+ const makefile = await fsp2.readFile(path23.join(root, "Makefile"), "utf8");
2635
2669
  const targets = parseMakeTargets(makefile);
2636
2670
  facts.build ??= targets.has("build") ? "make build" : "make";
2637
2671
  if (targets.has("test")) facts.test ??= "make test";
@@ -2645,52 +2679,79 @@ async function detectProjectFacts(root) {
2645
2679
  }
2646
2680
  function renderAgentsTemplate(f) {
2647
2681
  const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
2682
+ const hints = f.hints.length > 0 ? `
2683
+
2684
+ > Auto-detected: ${f.hints.join(", ")}` : "";
2648
2685
  return `# AGENTS.md
2649
2686
 
2650
- This file is loaded into WrongStack's system prompt as project context.
2651
- Keep it concise, factual, and durable: write the information future agents
2652
- need before they touch this codebase.
2687
+ > **DO NOT DELETE THIS FILE.** It is loaded into WrongStack's system prompt as
2688
+ > persistent project context. Previous content here may contain decisions,
2689
+ > architecture notes, domain knowledge, or verification history that should be
2690
+ > preserved. Merge additions rather than replacing.
2653
2691
 
2654
2692
  ## Project brief
2655
2693
 
2656
- - **Purpose:** _What does this project do, and why does it exist?_
2694
+ - **Purpose:** _What does this project do and why does it exist?_
2657
2695
  - **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
2658
- - **Runtime/deployment:** _Where does it run: CLI, server, browser, worker, library, package?_
2659
- - **Main entry points:** _Which files or commands should an agent inspect first?_
2696
+ - **Runtime / deployment:** _CLI, server, browser, worker, library, package?_${hints}
2660
2697
 
2661
2698
  ## How to work safely
2662
2699
 
2663
2700
  - _Project-specific rules the agent should always follow._
2664
2701
  - _Files, generated artifacts, migrations, or config the agent should not edit without asking._
2665
- - _Preferred style or architecture choices that are not obvious from the code._
2702
+ - _Preferred style or architecture choices not obvious from the code._
2703
+ - _Known fragile areas or historical bugs that deserve extra caution._
2666
2704
 
2667
2705
  ## Commands
2668
2706
 
2669
- - **Build:** ${cmd(f.build)}
2670
- - **Test:** ${cmd(f.test)}
2671
- - **Lint:** ${cmd(f.lint)}
2672
- - **Run locally:** ${cmd(f.run)}
2707
+ | Command | Script |
2708
+ |---------|--------|
2709
+ | Build | ${cmd(f.build)} |
2710
+ | Test | ${cmd(f.test)} |
2711
+ | Lint | ${cmd(f.lint)} |
2712
+ | Run locally | ${cmd(f.run)} |
2713
+
2714
+ ## Key files and entry points
2715
+
2716
+ | File / directory | Role |
2717
+ |---|---|
2718
+ | _src/_ | _Main source entry point(s)_ |
2719
+ | _tests/_ | _Test root or convention_ |
2720
+ | _docs/_ | _Architecture, runbooks, design notes_ |
2721
+ | _scripts/_ | _Automation scripts (CI, release, install, etc.)_ |
2673
2722
 
2674
2723
  ## Architecture notes
2675
2724
 
2676
2725
  _Summarize the important modules, data flow, boundaries, and ownership rules.
2677
- Mention anything a newcomer might misread._
2726
+ Mention anything a newcomer might misread or that looks unusual but is intentional._
2727
+
2728
+ ### Dependency layers
2729
+
2730
+ _Describe the key dependency direction or layered structure, e.g.: "core has no
2731
+ runtime deps; cli assembles everything above it."_
2732
+
2733
+ ### Extension points
2734
+
2735
+ _Plugin, MCP, extension hooks, custom tools \u2014 what's wired up and how._
2678
2736
 
2679
2737
  ## Domain knowledge
2680
2738
 
2681
2739
  _Business rules, acronyms, invariants, external services, and notes where the
2682
- code looks unusual but is intentional._
2740
+ code looks unusual but is intentional. E.g.: "IDs are ULIDs, not UUIDs", "the
2741
+ \`draft\` flag means uncommitted billing metadata", "MCP servers are restarted
2742
+ on disconnect with exponential backoff, up to 3 attempts"._
2683
2743
 
2684
2744
  ## Verification checklist
2685
2745
 
2686
2746
  - _What should be run after code changes?_
2687
2747
  - _What manual smoke test proves the common path still works?_
2688
2748
  - _What failure modes deserve extra attention?_
2749
+ - _Any known flaky tests or environment-dependent behavior?_
2689
2750
 
2690
2751
  ## Useful pointers
2691
2752
 
2692
- - _Docs, dashboards, runbooks, issue trackers, design notes, or owner contacts._
2693
- `;
2753
+ - _Docs, dashboards, runbooks, issue trackers, design notes, owner contacts._
2754
+ - _Related projects or repositories._`;
2694
2755
  }
2695
2756
  function countTurnPairs(messages) {
2696
2757
  let count = 0;
@@ -3094,7 +3155,7 @@ function buildStatsCommand(opts) {
3094
3155
  function buildFleetCommand(opts) {
3095
3156
  return {
3096
3157
  name: "fleet",
3097
- description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|retry [taskId]|log <id>|stream on|off|help]",
3158
+ description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|concurrency [N]|retry [taskId]|log <id>|stream on|off|help]",
3098
3159
  help: [
3099
3160
  "Usage:",
3100
3161
  " /fleet Show fleet status (alias for /fleet status).",
@@ -3102,6 +3163,8 @@ function buildFleetCommand(opts) {
3102
3163
  " /fleet usage Per-subagent runtime cost.",
3103
3164
  " /fleet kill <id> Terminate a running subagent.",
3104
3165
  " /fleet manifest Print the director manifest.",
3166
+ " /fleet concurrency Show the current concurrent-subagent ceiling.",
3167
+ " /fleet concurrency N Set the ceiling to N (>= 1). Lowering does not preempt running tasks.",
3105
3168
  " /fleet retry List interrupted tasks from the last run.",
3106
3169
  " /fleet retry <taskId> Re-spawn the matching subagent and re-assign the task.",
3107
3170
  " /fleet retry all Re-assign every interrupted task at once.",
@@ -3127,6 +3190,9 @@ function buildFleetCommand(opts) {
3127
3190
  if (!target) return { message: "Usage: /fleet kill <subagent-id>" };
3128
3191
  return { message: await opts.onFleet("kill", target) };
3129
3192
  }
3193
+ case "concurrency": {
3194
+ return { message: await opts.onFleet("concurrency", target) };
3195
+ }
3130
3196
  case "retry": {
3131
3197
  if (!opts.onFleetRetry) {
3132
3198
  return { message: "Retry is only available when director mode is active." };
@@ -3264,20 +3330,10 @@ function buildHelpCommand(opts) {
3264
3330
  function buildInitCommand(opts) {
3265
3331
  return {
3266
3332
  name: "init",
3267
- description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
3268
- async run(args, ctx) {
3269
- const force = args.trim() === "--force";
3270
- const dir = path21.join(ctx.projectRoot, ".wrongstack");
3271
- const file = path21.join(dir, "AGENTS.md");
3272
- try {
3273
- await fsp2.access(file);
3274
- if (!force) {
3275
- const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
3276
- opts.renderer.writeWarning(msg2);
3277
- return { message: msg2 };
3278
- }
3279
- } catch {
3280
- }
3333
+ description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
3334
+ async run(_args, ctx) {
3335
+ const dir = path23.join(ctx.projectRoot, ".wrongstack");
3336
+ const file = path23.join(dir, "AGENTS.md");
3281
3337
  const detected = await detectProjectFacts(ctx.projectRoot);
3282
3338
  const body = renderAgentsTemplate(detected);
3283
3339
  await fsp2.mkdir(dir, { recursive: true });
@@ -3298,6 +3354,223 @@ No project type auto-detected. Edit the file with project context and instructio
3298
3354
  }
3299
3355
  };
3300
3356
  }
3357
+ function parseMcpArgs(args) {
3358
+ const trimmed = args.trim();
3359
+ if (!trimmed || trimmed === "list") return { action: "list", name: "" };
3360
+ const parts = trimmed.split(/\s+/);
3361
+ const action = parts[0];
3362
+ const name = parts[1] ?? "";
3363
+ const enable = parts.includes("--enable") || parts.includes("-e");
3364
+ switch (action) {
3365
+ case "add":
3366
+ return name ? { action: "add", name, enable } : null;
3367
+ case "remove":
3368
+ return name ? { action: "remove", name } : null;
3369
+ case "enable":
3370
+ return name ? { action: "enable", name } : null;
3371
+ case "disable":
3372
+ return name ? { action: "disable", name } : null;
3373
+ case "restart":
3374
+ return name ? { action: "restart", name } : null;
3375
+ default:
3376
+ return null;
3377
+ }
3378
+ }
3379
+ async function runMcpManagementCommand(parsed, deps) {
3380
+ const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
3381
+ const configured = config.mcpServers ?? {};
3382
+ switch (parsed.action) {
3383
+ case "list":
3384
+ return renderList(configured, mcpRegistry, allServerPresets);
3385
+ case "add":
3386
+ return runAdd(parsed.name, parsed.enable ?? false, configured, configPath2, allServerPresets);
3387
+ case "remove":
3388
+ return runRemove(parsed.name, configured, configPath2, mcpRegistry);
3389
+ case "enable":
3390
+ return runEnable(parsed.name, configured, configPath2, mcpRegistry);
3391
+ case "disable":
3392
+ return runDisable(parsed.name, configured, configPath2, mcpRegistry);
3393
+ case "restart":
3394
+ return runRestart(parsed.name, mcpRegistry);
3395
+ }
3396
+ }
3397
+ function renderList(configured, mcpRegistry, all) {
3398
+ const lines = [];
3399
+ const liveStatus = mcpRegistry.list();
3400
+ const liveMap = new Map(liveStatus.map((s) => [s.name, s]));
3401
+ const configuredNames = new Set(Object.keys(configured));
3402
+ if (configuredNames.size > 0) {
3403
+ lines.push(color.bold("Configured servers:"));
3404
+ for (const [name, cfg] of Object.entries(configured)) {
3405
+ const live = liveMap.get(name);
3406
+ const toolCount = live ? color.dim(` (${live.toolCount} tools)`) : "";
3407
+ const enabled = cfg.enabled === false ? `${color.dim("disabled")} ` : `${color.green("\u25CF enabled")} `;
3408
+ const stateStr = live ? stateBadge(live.state) : color.dim("\u25CB not running");
3409
+ lines.push(` ${color.bold(name)} ${enabled}${stateStr}${toolCount}`);
3410
+ if (cfg.description) lines.push(` ${color.dim(cfg.description)}`);
3411
+ }
3412
+ lines.push("");
3413
+ }
3414
+ const unconfigured = Object.entries(all).filter(([n]) => !configuredNames.has(n));
3415
+ lines.push(color.bold("Available presets (run `/mcp add <name> --enable` to enable):"));
3416
+ if (unconfigured.length === 0) {
3417
+ lines.push(` ${color.dim("All presets are already configured.")}`);
3418
+ } else {
3419
+ for (const [name, cfg] of unconfigured) {
3420
+ const warn = cfg.permission === "deny" ? color.red(" \u26A0") : "";
3421
+ lines.push(` ${color.bold(name)} ${cfg.description ?? cfg.transport}${warn}`);
3422
+ }
3423
+ }
3424
+ lines.push("");
3425
+ lines.push(color.dim(" /mcp add <name> [--enable] /mcp remove <name>"));
3426
+ lines.push(color.dim(" /mcp enable <name> /mcp disable <name>"));
3427
+ lines.push(color.dim(" /mcp restart <name> (runtime restart)"));
3428
+ return lines.join("\n");
3429
+ }
3430
+ async function runAdd(name, enable, configured, configPath2, all) {
3431
+ const preset = all[name];
3432
+ if (!preset) {
3433
+ const known = Object.keys(all).join(", ");
3434
+ return `Unknown server "${name}". Available: ${known}`;
3435
+ }
3436
+ if (configured[name]) {
3437
+ const full2 = await readConfig(configPath2);
3438
+ full2.mcpServers = {
3439
+ ...full2.mcpServers ?? {},
3440
+ [name]: { ...preset, ...configured[name], enabled: enable }
3441
+ };
3442
+ await writeConfig(configPath2, full2);
3443
+ return `${color.green("Updated")} "${name}" (${enable ? "enabled" : "disabled"}). Config written.`;
3444
+ }
3445
+ const full = await readConfig(configPath2);
3446
+ const mcpServers = { ...full.mcpServers ?? {}, [name]: { ...preset, enabled: enable } };
3447
+ full.mcpServers = mcpServers;
3448
+ await writeConfig(configPath2, full);
3449
+ const verb = enable ? "Enabled" : "Added (disabled \u2014 /mcp enable to start)";
3450
+ return `${color.green(verb)} "${name}" (${preset.transport}). Config written to ${configPath2}.`;
3451
+ }
3452
+ async function runRemove(name, configured, configPath2, mcpRegistry) {
3453
+ if (!configured[name]) return `Server "${name}" is not in config.`;
3454
+ await mcpRegistry.stop(name).catch(() => {
3455
+ });
3456
+ const full = await readConfig(configPath2);
3457
+ const mcpServers = { ...full.mcpServers ?? {} };
3458
+ delete mcpServers[name];
3459
+ full.mcpServers = mcpServers;
3460
+ await writeConfig(configPath2, full);
3461
+ return `${color.yellow("Removed")} "${name}" from config.`;
3462
+ }
3463
+ async function runEnable(name, configured, configPath2, mcpRegistry) {
3464
+ const cfg = configured[name];
3465
+ if (!cfg) return `Server "${name}" is not in config. Run \`/mcp add ${name} --enable\` first.`;
3466
+ if (cfg.enabled !== false) {
3467
+ try {
3468
+ await mcpRegistry.restart(name);
3469
+ return `${color.green("\u25CF")} "${name}" is already enabled and running.`;
3470
+ } catch {
3471
+ await mcpRegistry.start({ ...cfg, enabled: true });
3472
+ return `${color.green("Enabled")} "${name}" and started.`;
3473
+ }
3474
+ }
3475
+ const full = await readConfig(configPath2);
3476
+ const mcpServers = { ...full.mcpServers ?? {} };
3477
+ mcpServers[name] = { ...mcpServers[name], enabled: true };
3478
+ full.mcpServers = mcpServers;
3479
+ await writeConfig(configPath2, full);
3480
+ try {
3481
+ await mcpRegistry.restart(name);
3482
+ } catch {
3483
+ await mcpRegistry.start({ ...cfg, enabled: true });
3484
+ }
3485
+ return `${color.green("Enabled")} "${name}" and started.`;
3486
+ }
3487
+ async function runDisable(name, configured, configPath2, mcpRegistry) {
3488
+ const cfg = configured[name];
3489
+ if (!cfg) return `Server "${name}" is not in config.`;
3490
+ await mcpRegistry.stop(name).catch(() => {
3491
+ });
3492
+ const full = await readConfig(configPath2);
3493
+ const mcpServers = { ...full.mcpServers ?? {} };
3494
+ mcpServers[name] = { ...mcpServers[name], enabled: false };
3495
+ full.mcpServers = mcpServers;
3496
+ await writeConfig(configPath2, full);
3497
+ return `${color.yellow("Disabled")} "${name}" and stopped.`;
3498
+ }
3499
+ async function runRestart(name, mcpRegistry) {
3500
+ const live = mcpRegistry.list();
3501
+ if (!live.find((s) => s.name === name)) {
3502
+ return `Server "${name}" is not currently running. Add it with \`/mcp add ${name} --enable\`.`;
3503
+ }
3504
+ try {
3505
+ await mcpRegistry.restart(name);
3506
+ return `${color.green("\u2713")} Restarted "${name}".`;
3507
+ } catch (err) {
3508
+ return `${color.red("\u2717")} Failed to restart "${name}": ${err instanceof Error ? err.message : String(err)}`;
3509
+ }
3510
+ }
3511
+ function stateBadge(state) {
3512
+ switch (state) {
3513
+ case "connected":
3514
+ return color.green("\u25CF connected");
3515
+ case "connecting":
3516
+ return color.cyan("\u25D0 connecting");
3517
+ case "reconnecting":
3518
+ return color.cyan("\u25D1 reconnecting");
3519
+ case "disconnected":
3520
+ return color.dim("\u25CB disconnected");
3521
+ case "failed":
3522
+ return color.red("\u2717 failed");
3523
+ default:
3524
+ return color.dim(state);
3525
+ }
3526
+ }
3527
+ async function readConfig(path24) {
3528
+ try {
3529
+ return JSON.parse(await fsp2.readFile(path24, "utf8"));
3530
+ } catch {
3531
+ return {};
3532
+ }
3533
+ }
3534
+ async function writeConfig(path24, cfg) {
3535
+ const raw = JSON.stringify(cfg, null, 2);
3536
+ const tmp = path24 + ".tmp";
3537
+ await fsp2.writeFile(tmp, raw, "utf8");
3538
+ await fsp2.rename(tmp, path24);
3539
+ }
3540
+
3541
+ // src/slash-commands/mcp.ts
3542
+ function buildMcpSlashCommand(opts) {
3543
+ return {
3544
+ name: "mcp",
3545
+ description: "Manage MCP servers: /mcp [list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
3546
+ aliases: ["mcp-servers"],
3547
+ argsHint: "[list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
3548
+ help: [
3549
+ "Usage:",
3550
+ " /mcp List available and configured servers.",
3551
+ " /mcp list Same.",
3552
+ " /mcp add <name> Add server preset to config (disabled).",
3553
+ " /mcp add <name> --enable Add and immediately enable.",
3554
+ " /mcp remove <name> Remove server from config.",
3555
+ " /mcp enable <name> Enable server in config + start it.",
3556
+ " /mcp disable <name> Disable server in config + stop it.",
3557
+ " /mcp restart <name> Stop and restart a running server (REPL only).",
3558
+ "",
3559
+ "Examples:",
3560
+ " /mcp",
3561
+ " /mcp add filesystem --enable",
3562
+ " /mcp enable github",
3563
+ " /mcp restart brave-search"
3564
+ ].join("\n"),
3565
+ async run(args) {
3566
+ if (!opts.onMcp) {
3567
+ return { message: "MCP management is not available in this session." };
3568
+ }
3569
+ const result = await opts.onMcp(args.trim());
3570
+ return { message: result };
3571
+ }
3572
+ };
3573
+ }
3301
3574
 
3302
3575
  // src/slash-commands/memory.ts
3303
3576
  function buildMemoryCommand(opts) {
@@ -3769,21 +4042,24 @@ function buildAutonomyCommand(opts) {
3769
4042
  description: "Toggle or query autonomy mode (self-driving agent).",
3770
4043
  help: [
3771
4044
  "Usage:",
3772
- " /autonomy Show current autonomy status",
3773
- " /autonomy off Disabled \u2014 agent stops after each turn (default)",
3774
- " /autonomy suggest Show next-step suggestions after each turn",
3775
- " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
3776
- " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 off",
4045
+ " /autonomy Show current autonomy status",
4046
+ " /autonomy off Disabled \u2014 agent stops after each turn (default)",
4047
+ " /autonomy suggest Show next-step suggestions after each turn",
4048
+ " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
4049
+ " /autonomy eternal Sittin-sene mode \u2014 runs forever against /goal",
4050
+ " /autonomy stop Stop eternal mode (no-op for other modes)",
4051
+ " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 off",
3777
4052
  "",
3778
4053
  "Modes:",
3779
4054
  " off \u2014 Normal interactive mode. Agent stops and waits.",
3780
4055
  " suggest \u2014 After each turn, agent suggests next steps. You pick.",
3781
4056
  " auto \u2014 After each turn, agent picks the best next step and continues.",
3782
4057
  " Runs indefinitely until you press Esc or Ctrl+C.",
4058
+ " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
4059
+ " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
3783
4060
  "",
3784
- "In auto mode the agent works autonomously. Press Esc to redirect,",
3785
- "Ctrl+C to stop. The agent suggests context-aware next steps based on",
3786
- "the conversation history."
4061
+ "In auto/eternal modes the agent works autonomously. Press Esc to redirect,",
4062
+ "Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
3787
4063
  ].join("\n"),
3788
4064
  async run(args) {
3789
4065
  const arg = args.trim().toLowerCase();
@@ -3792,14 +4068,64 @@ function buildAutonomyCommand(opts) {
3792
4068
  opts.renderer.writeWarning(msg2);
3793
4069
  return { message: msg2 };
3794
4070
  }
3795
- if (!arg) {
4071
+ if (!arg || arg === "status") {
3796
4072
  const current = opts.onAutonomy();
3797
4073
  const labels2 = {
3798
4074
  off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
3799
4075
  suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
3800
- auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`
4076
+ auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
4077
+ eternal: `${color.red("ETERNAL")} ${color.dim("(sittin-sene \u2014 goal-driven, YOLO, until /autonomy stop)")}`
3801
4078
  };
3802
- const msg2 = `Autonomy mode: ${labels2[current]}`;
4079
+ const lines = [`Autonomy mode: ${labels2[current]}`];
4080
+ try {
4081
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
4082
+ if (goal) {
4083
+ const u = summarizeUsage(goal);
4084
+ lines.push(color.dim(` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`));
4085
+ lines.push(color.dim(` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`));
4086
+ if (u.iterationsWithUsage > 0) {
4087
+ lines.push(
4088
+ color.dim(
4089
+ ` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
4090
+ )
4091
+ );
4092
+ }
4093
+ const recent = goal.journal.slice(-10);
4094
+ const failed = recent.filter((e) => e.status === "failure").length;
4095
+ if (failed > 0) {
4096
+ lines.push(color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`));
4097
+ }
4098
+ }
4099
+ } catch {
4100
+ }
4101
+ const msg2 = lines.join("\n");
4102
+ opts.renderer.write(msg2);
4103
+ return { message: msg2 };
4104
+ }
4105
+ if (arg === "stop" || arg === "halt" || arg === "kill") {
4106
+ if (!opts.onEternalStop) {
4107
+ const msg3 = "No eternal-mode controller wired in this session.";
4108
+ opts.renderer.writeWarning(msg3);
4109
+ return { message: msg3 };
4110
+ }
4111
+ opts.onEternalStop();
4112
+ opts.onAutonomy("off");
4113
+ let summaryLine = "";
4114
+ try {
4115
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
4116
+ if (goal) {
4117
+ const u = summarizeUsage(goal);
4118
+ if (u.iterationsWithUsage > 0) {
4119
+ summaryLine = "\n" + color.dim(
4120
+ ` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
4121
+ );
4122
+ } else if (goal.iterations > 0) {
4123
+ summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
4124
+ }
4125
+ }
4126
+ } catch {
4127
+ }
4128
+ const msg2 = `${color.amber("Eternal mode stop requested.")} The current iteration will finish, then the loop exits.${summaryLine}`;
3803
4129
  opts.renderer.write(msg2);
3804
4130
  return { message: msg2 };
3805
4131
  }
@@ -3810,20 +4136,47 @@ function buildAutonomyCommand(opts) {
3810
4136
  newMode = "off";
3811
4137
  } else if (arg === "suggest" || arg === "suggestions") {
3812
4138
  newMode = "suggest";
4139
+ } else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
4140
+ newMode = "eternal";
3813
4141
  } else if (arg === "toggle" || arg === "cycle") {
3814
4142
  const current = opts.onAutonomy() ?? "off";
3815
- const cycle = ["off", "suggest", "auto"];
4143
+ const cycle = ["off", "suggest", "auto", "eternal"];
3816
4144
  newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
3817
4145
  } else {
3818
- const msg2 = `Unknown argument: ${arg}. Use /autonomy on, /autonomy off, /autonomy suggest, or /autonomy toggle.`;
4146
+ const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, stop, or toggle.`;
3819
4147
  opts.renderer.writeWarning(msg2);
3820
4148
  return { message: msg2 };
3821
4149
  }
4150
+ if (newMode === "eternal") {
4151
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
4152
+ if (!goal) {
4153
+ const msg3 = `${color.red("Eternal mode requires a goal.")} Run \`/goal set <mission>\` first.`;
4154
+ opts.renderer.writeWarning(msg3);
4155
+ return { message: msg3 };
4156
+ }
4157
+ if (!opts.onEternalStart) {
4158
+ const msg3 = "Eternal mode controller is not wired in this session.";
4159
+ opts.renderer.writeWarning(msg3);
4160
+ return { message: msg3 };
4161
+ }
4162
+ if (opts.onYolo) opts.onYolo(true);
4163
+ opts.onAutonomy(newMode);
4164
+ opts.onEternalStart();
4165
+ const msg2 = `${color.red("Autonomy mode: ETERNAL")} \u2014 engine launching against goal: ${color.bold(goal.goal)}
4166
+ ${color.dim("YOLO forced ON. Use /autonomy stop to end. Journal at /goal journal.")}`;
4167
+ opts.renderer.write(msg2);
4168
+ return { message: msg2 };
4169
+ }
4170
+ const previous = opts.onAutonomy();
4171
+ if (previous === "eternal" && opts.onEternalStop) {
4172
+ opts.onEternalStop();
4173
+ }
3822
4174
  opts.onAutonomy(newMode);
3823
4175
  const labels = {
3824
4176
  off: `${color.green("OFF")} \u2014 agent stops after each turn`,
3825
4177
  suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
3826
- auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`
4178
+ auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
4179
+ eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`
3827
4180
  };
3828
4181
  const msg = `Autonomy mode: ${labels[newMode]}`;
3829
4182
  opts.renderer.write(msg);
@@ -3831,6 +4184,124 @@ function buildAutonomyCommand(opts) {
3831
4184
  }
3832
4185
  };
3833
4186
  }
4187
+ var KNOWN_VERBS = /* @__PURE__ */ new Set([
4188
+ "",
4189
+ "show",
4190
+ "status",
4191
+ "set",
4192
+ "new",
4193
+ "clear",
4194
+ "reset",
4195
+ "journal",
4196
+ "log"
4197
+ ]);
4198
+ function buildGoalCommand(opts) {
4199
+ return {
4200
+ name: "goal",
4201
+ description: "Set, inspect, or clear the long-running autonomous mission used by /autonomy eternal.",
4202
+ help: [
4203
+ "Usage:",
4204
+ " /goal Show current goal + recent journal",
4205
+ " /goal set <text> Set a new goal (overwrites previous)",
4206
+ " /goal clear Clear the goal (stops eternal mode if running)",
4207
+ " /goal status Same as /goal (alias)",
4208
+ " /goal journal [N] Show last N journal entries (default 25)",
4209
+ "",
4210
+ "Goals live in <projectRoot>/.wrongstack/goal.json and persist across sessions.",
4211
+ "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
4212
+ "every iteration to decide what to do next."
4213
+ ].join("\n"),
4214
+ async run(args) {
4215
+ const trimmed = args.trim();
4216
+ const [verbRaw, ...rest] = trimmed.split(/\s+/);
4217
+ const verb = (verbRaw ?? "").toLowerCase();
4218
+ const restJoined = rest.join(" ").trim();
4219
+ const goalPath = goalFilePath(opts.projectRoot);
4220
+ const verbForDispatch = verb && !KNOWN_VERBS.has(verb) ? "set" : verb;
4221
+ const setText = verbForDispatch === "set" && !KNOWN_VERBS.has(verb) ? trimmed : restJoined;
4222
+ switch (verbForDispatch) {
4223
+ case "":
4224
+ case "show":
4225
+ case "status": {
4226
+ const current = await loadGoal(goalPath);
4227
+ if (!current) {
4228
+ const msg2 = "No goal set. Use `/goal set <mission text>` to create one.";
4229
+ opts.renderer.write(msg2);
4230
+ return { message: msg2 };
4231
+ }
4232
+ const msg = formatGoal(current);
4233
+ opts.renderer.write(msg);
4234
+ return { message: msg };
4235
+ }
4236
+ case "set":
4237
+ case "new": {
4238
+ if (!setText) {
4239
+ const msg2 = "Usage: /goal set <mission text>";
4240
+ opts.renderer.writeWarning(msg2);
4241
+ return { message: msg2 };
4242
+ }
4243
+ const existing = await loadGoal(goalPath);
4244
+ const next = existing ? { ...existing, goal: setText, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal(setText);
4245
+ await saveGoal(goalPath, next);
4246
+ const shortGoal = setText.length > 80 ? `${setText.slice(0, 80)}\u2026` : setText;
4247
+ const msg = `\u{1F3AF} ${color.green("Goal locked:")} ${shortGoal}
4248
+ ${color.dim(`Stored in ${goalPath} \u2014 Esc / /steer to redirect, Ctrl+C to stop.`)}`;
4249
+ opts.renderer.write(msg);
4250
+ return { message: msg, runText: buildGoalPreamble(setText) };
4251
+ }
4252
+ case "clear":
4253
+ case "reset": {
4254
+ const existing = await loadGoal(goalPath);
4255
+ if (!existing) {
4256
+ const msg2 = "No goal to clear.";
4257
+ opts.renderer.write(msg2);
4258
+ return { message: msg2 };
4259
+ }
4260
+ const { unlink: unlink4 } = await import('fs/promises');
4261
+ try {
4262
+ await unlink4(goalPath);
4263
+ } catch {
4264
+ }
4265
+ if (opts.onEternalStop) opts.onEternalStop();
4266
+ const msg = `${color.amber("Goal cleared.")} Eternal mode will stop on next cycle.`;
4267
+ opts.renderer.write(msg);
4268
+ return { message: msg };
4269
+ }
4270
+ case "journal":
4271
+ case "log": {
4272
+ const current = await loadGoal(goalPath);
4273
+ if (!current) {
4274
+ const msg2 = "No goal set.";
4275
+ opts.renderer.write(msg2);
4276
+ return { message: msg2 };
4277
+ }
4278
+ const n = restJoined ? Math.max(1, Number.parseInt(restJoined, 10) || 25) : 25;
4279
+ if (current.journal.length === 0) {
4280
+ const msg2 = "Journal is empty.";
4281
+ opts.renderer.write(msg2);
4282
+ return { message: msg2 };
4283
+ }
4284
+ const tail = current.journal.slice(-n);
4285
+ const lines = tail.map((e) => {
4286
+ const mark = e.status === "success" ? color.green("\u2713") : e.status === "failure" ? color.red("\u2717") : e.status === "aborted" ? color.amber("\u2298") : color.dim("\xB7");
4287
+ const note = e.note ? color.dim(` \u2014 ${e.note}`) : "";
4288
+ return `${color.dim(`#${e.iteration}`)} ${mark} ${color.dim(`[${e.source}]`)} ${e.task}${note}`;
4289
+ });
4290
+ const header = `Journal (last ${tail.length} of ${current.journal.length}):`;
4291
+ const msg = `${header}
4292
+ ${lines.join("\n")}`;
4293
+ opts.renderer.write(msg);
4294
+ return { message: msg };
4295
+ }
4296
+ default: {
4297
+ const msg = `Unknown subcommand "${verb}". Try: show | set <text> | clear | journal [N]`;
4298
+ opts.renderer.writeWarning(msg);
4299
+ return { message: msg };
4300
+ }
4301
+ }
4302
+ }
4303
+ };
4304
+ }
3834
4305
 
3835
4306
  // src/slash-commands/mode.ts
3836
4307
  function buildModeCommand(opts) {
@@ -3940,6 +4411,15 @@ ${lines.join("\n\n")}
3940
4411
  }
3941
4412
  };
3942
4413
  }
4414
+ function getProviderFromContext(ctx, opts) {
4415
+ if (opts.llmProvider && typeof opts.llmProvider.complete === "function") {
4416
+ return { provider: opts.llmProvider, model: opts.llmModel };
4417
+ }
4418
+ if (ctx.provider && typeof ctx.provider.complete === "function") {
4419
+ return { provider: ctx.provider, model: ctx.model };
4420
+ }
4421
+ return null;
4422
+ }
3943
4423
  function buildSecurityCommand(opts) {
3944
4424
  return {
3945
4425
  name: "security",
@@ -3991,11 +4471,11 @@ async function handleScan(args, ctx, opts) {
3991
4471
  const options = parseArgs2(args);
3992
4472
  const projectRoot = ctx.projectRoot || opts.projectRoot;
3993
4473
  try {
3994
- const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
3995
- if (!orchestratorContext.provider) {
4474
+ const providerInfo = getProviderFromContext(ctx, opts);
4475
+ if (!providerInfo) {
3996
4476
  return { message: "\u274C Security scan requires an active LLM provider. No provider configured." };
3997
4477
  }
3998
- const result = await defaultOrchestrator.run(orchestratorContext, {
4478
+ const result = await defaultOrchestrator.run(providerInfo, {
3999
4479
  projectRoot,
4000
4480
  scanOptions: {
4001
4481
  depth: options.depth || "standard",
@@ -4044,11 +4524,11 @@ async function handleScan(args, ctx, opts) {
4044
4524
  async function handleAudit(ctx, opts) {
4045
4525
  const projectRoot = ctx.projectRoot || opts.projectRoot;
4046
4526
  try {
4047
- const orchestratorContext = ctx.provider ? ctx : { provider: opts.llmProvider, model: opts.llmModel };
4048
- if (!orchestratorContext.provider) {
4527
+ const providerInfo = getProviderFromContext(ctx, opts);
4528
+ if (!providerInfo) {
4049
4529
  return { message: "\u274C Security audit requires an active LLM provider. No provider configured." };
4050
4530
  }
4051
- const result = await defaultOrchestrator.run(orchestratorContext, {
4531
+ const result = await defaultOrchestrator.run(providerInfo, {
4052
4532
  projectRoot,
4053
4533
  reportOptions: { format: "markdown" }
4054
4534
  });
@@ -4159,12 +4639,111 @@ function getHelpMessage() {
4159
4639
 
4160
4640
  Run \`/security scan\` to start.`;
4161
4641
  }
4642
+ var CONFIG_ENV = "WRONGSTACK_STATUSLINE_CONFIG";
4643
+ var DEFAULTS = {
4644
+ todos: true,
4645
+ plan: true,
4646
+ fleet: true,
4647
+ git: true,
4648
+ elapsed: true,
4649
+ context: true,
4650
+ cost: true
4651
+ };
4652
+ function resolveConfigPath() {
4653
+ return process.env[CONFIG_ENV] ?? path23.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
4654
+ }
4655
+ async function loadStatuslineConfig() {
4656
+ const p = resolveConfigPath();
4657
+ try {
4658
+ const raw = await fsp2.readFile(p, "utf8");
4659
+ return { ...DEFAULTS, ...JSON.parse(raw) };
4660
+ } catch {
4661
+ return { ...DEFAULTS };
4662
+ }
4663
+ }
4664
+ async function saveStatuslineConfig(cfg) {
4665
+ const p = resolveConfigPath();
4666
+ await fsp2.mkdir(path23.dirname(p), { recursive: true });
4667
+ await atomicWrite(p, JSON.stringify(cfg, null, 2));
4668
+ }
4669
+ function buildStatuslineCommand(deps) {
4670
+ return {
4671
+ name: "statusline",
4672
+ aliases: ["sl"],
4673
+ description: "Customize status bar chips: /statusline [item] [on|off] or /statusline reset",
4674
+ help: [
4675
+ "Usage: /statusline [item] [on|off]",
4676
+ " /statusline \u2014 show current config",
4677
+ " /statusline <item> on \u2014 enable a chip",
4678
+ " /statusline <item> off \u2014 disable a chip",
4679
+ " /statusline reset \u2014 restore defaults",
4680
+ "",
4681
+ "Available items: todos, plan, fleet, git, elapsed, context, cost",
4682
+ "Persistent across sessions (saved to ~/.wrongstack/statusline.json)."
4683
+ ].join("\n"),
4684
+ async run(args) {
4685
+ const cfg = await deps.getConfig();
4686
+ const trimmed = args.trim();
4687
+ const parts = trimmed.split(/\s+/);
4688
+ const [item, action] = parts;
4689
+ if (!item) {
4690
+ const lines = ["StatusBar chips:"];
4691
+ const items = [
4692
+ "todos",
4693
+ "plan",
4694
+ "fleet",
4695
+ "git",
4696
+ "elapsed",
4697
+ "context",
4698
+ "cost"
4699
+ ];
4700
+ for (const k of items) {
4701
+ const val = cfg[k];
4702
+ if (val === void 0) continue;
4703
+ lines.push(` ${val ? "\u25CF" : "\u25CB"} ${k}`);
4704
+ }
4705
+ return { message: lines.join("\n") };
4706
+ }
4707
+ if (item === "reset") {
4708
+ await deps.setConfig({ ...DEFAULTS });
4709
+ deps.setHiddenItems([]);
4710
+ return { message: "StatusBar config reset to defaults." };
4711
+ }
4712
+ const validItems = [
4713
+ "todos",
4714
+ "plan",
4715
+ "fleet",
4716
+ "git",
4717
+ "elapsed",
4718
+ "context",
4719
+ "cost"
4720
+ ];
4721
+ if (!validItems.includes(item)) {
4722
+ return {
4723
+ message: `Unknown item "${item}". Available: ${validItems.join(", ")}. Usage: /statusline <item> on|off`
4724
+ };
4725
+ }
4726
+ const onOff = action?.toLowerCase();
4727
+ if (!onOff || onOff !== "on" && onOff !== "off") {
4728
+ return { message: `Usage: /statusline ${item} on|off` };
4729
+ }
4730
+ const next = { ...cfg, [item]: onOff === "on" };
4731
+ await deps.setConfig(next);
4732
+ if (onOff === "off") {
4733
+ deps.setHiddenItems([...deps.hiddenItems, item]);
4734
+ } else {
4735
+ deps.setHiddenItems(deps.hiddenItems.filter((i) => i !== item));
4736
+ }
4737
+ return { message: `statusline ${item}: ${onOff}` };
4738
+ }
4739
+ };
4740
+ }
4162
4741
  function makeInstaller(opts, projectRoot, global) {
4163
- const globalRoot = path21.join(os6.homedir(), ".wrongstack");
4742
+ const globalRoot = path23.join(os6.homedir(), ".wrongstack");
4164
4743
  return new SkillInstaller({
4165
- manifestPath: path21.join(globalRoot, "installed-skills.json"),
4166
- projectSkillsDir: path21.join(projectRoot, ".wrongstack", "skills"),
4167
- globalSkillsDir: path21.join(globalRoot, "skills"),
4744
+ manifestPath: path23.join(globalRoot, "installed-skills.json"),
4745
+ projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
4746
+ globalSkillsDir: path23.join(globalRoot, "skills"),
4168
4747
  projectHash: projectHash(projectRoot),
4169
4748
  skillLoader: opts.skillLoader
4170
4749
  });
@@ -4336,6 +4915,7 @@ function buildBuiltinSlashCommands(opts) {
4336
4915
  buildSkillUpdateCommand(opts),
4337
4916
  buildSkillUninstallCommand(opts),
4338
4917
  buildPluginCommand(opts),
4918
+ buildMcpSlashCommand(opts),
4339
4919
  buildDiagCommand(opts),
4340
4920
  buildStatsCommand(opts),
4341
4921
  buildSpawnCommand(opts),
@@ -4352,12 +4932,22 @@ function buildBuiltinSlashCommands(opts) {
4352
4932
  buildLoadCommand(opts),
4353
4933
  buildYoloCommand(opts),
4354
4934
  buildAutonomyCommand(opts),
4935
+ buildGoalCommand(opts),
4355
4936
  buildModeCommand(opts),
4356
4937
  buildExitCommand(opts),
4357
4938
  buildCommitCommand(),
4358
4939
  buildGitcheckCommand(),
4359
4940
  buildPushCommand(),
4360
- buildSecurityCommand(opts)
4941
+ buildSecurityCommand(opts),
4942
+ buildStatuslineCommand({
4943
+ cwd: opts.cwd,
4944
+ hiddenItems: opts.statuslineHiddenItems ?? [],
4945
+ setHiddenItems: opts.setStatuslineHiddenItems ?? (() => {
4946
+ }),
4947
+ getConfig: opts.statuslineConfig?.get ?? (async () => ({})),
4948
+ setConfig: opts.statuslineConfig?.set ?? (async () => {
4949
+ })
4950
+ })
4361
4951
  ];
4362
4952
  }
4363
4953
 
@@ -4376,13 +4966,13 @@ var MANIFESTS = [
4376
4966
  ];
4377
4967
  async function detectProjectKind(projectRoot) {
4378
4968
  try {
4379
- await fsp2.access(path21.join(projectRoot, ".wrongstack", "AGENTS.md"));
4969
+ await fsp2.access(path23.join(projectRoot, ".wrongstack", "AGENTS.md"));
4380
4970
  return "initialized";
4381
4971
  } catch {
4382
4972
  }
4383
4973
  for (const m of MANIFESTS) {
4384
4974
  try {
4385
- await fsp2.access(path21.join(projectRoot, m));
4975
+ await fsp2.access(path23.join(projectRoot, m));
4386
4976
  return "project";
4387
4977
  } catch {
4388
4978
  }
@@ -4390,8 +4980,8 @@ async function detectProjectKind(projectRoot) {
4390
4980
  return "empty";
4391
4981
  }
4392
4982
  async function scaffoldAgentsMd(projectRoot) {
4393
- const dir = path21.join(projectRoot, ".wrongstack");
4394
- const file = path21.join(dir, "AGENTS.md");
4983
+ const dir = path23.join(projectRoot, ".wrongstack");
4984
+ const file = path23.join(dir, "AGENTS.md");
4395
4985
  const facts = await detectProjectFacts(projectRoot);
4396
4986
  const body = renderAgentsTemplate(facts);
4397
4987
  await fsp2.mkdir(dir, { recursive: true });
@@ -4404,7 +4994,7 @@ async function runProjectCheck(opts) {
4404
4994
  if (kind === "initialized") {
4405
4995
  renderer.write(
4406
4996
  `
4407
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path21.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
4997
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path23.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
4408
4998
  `
4409
4999
  );
4410
5000
  return true;
@@ -4416,8 +5006,12 @@ async function runProjectCheck(opts) {
4416
5006
  `
4417
5007
  );
4418
5008
  const answer2 = (await reader.readLine(
4419
- ` ${color.amber("?")} Scaffold ${color.bold("AGENTS.md")} now? ${color.dim("[y/N]")} `
5009
+ ` ${color.amber("?")} Scaffold ${color.bold("AGENTS.md")} now? ${color.dim("[y/N/q]")} `
4420
5010
  )).trim().toLowerCase();
5011
+ if (answer2 === "q") {
5012
+ renderer.write(color.dim(" Cancelled.\n"));
5013
+ return false;
5014
+ }
4421
5015
  if (answer2 === "y" || answer2 === "yes") {
4422
5016
  try {
4423
5017
  const file = await scaffoldAgentsMd(projectRoot);
@@ -4431,7 +5025,7 @@ async function runProjectCheck(opts) {
4431
5025
  }
4432
5026
  return true;
4433
5027
  }
4434
- const gitDir = path21.join(projectRoot, ".git");
5028
+ const gitDir = path23.join(projectRoot, ".git");
4435
5029
  let hasGit = false;
4436
5030
  try {
4437
5031
  await fsp2.access(gitDir);
@@ -4445,8 +5039,12 @@ async function runProjectCheck(opts) {
4445
5039
  `
4446
5040
  );
4447
5041
  const answer2 = (await reader.readLine(
4448
- ` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
5042
+ ` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N/q]")} `
4449
5043
  )).trim().toLowerCase();
5044
+ if (answer2 === "q") {
5045
+ renderer.write(color.dim(" Cancelled.\n"));
5046
+ return false;
5047
+ }
4450
5048
  if (answer2 === "y" || answer2 === "yes") {
4451
5049
  try {
4452
5050
  const { spawn: spawn3 } = await import('child_process');
@@ -4468,8 +5066,8 @@ async function runProjectCheck(opts) {
4468
5066
  `
4469
5067
  );
4470
5068
  }
4471
- const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
4472
- if (answer === "n" || answer === "no") {
5069
+ const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n/q]")} `)).trim().toLowerCase();
5070
+ if (answer === "q" || answer === "n" || answer === "no") {
4473
5071
  renderer.write(color.dim(" Cancelled.\n"));
4474
5072
  return false;
4475
5073
  }
@@ -4483,8 +5081,12 @@ async function runLaunchPrompts(opts) {
4483
5081
  } else {
4484
5082
  const answer = (await reader.readLine(
4485
5083
  `
4486
- ${color.amber("?")} Interactive mode: ${color.bold("T")}UI / ${color.bold("R")}EPL ${color.dim("[T/r]")} `
5084
+ ${color.amber("?")} Interactive mode: ${color.bold("T")}UI / ${color.bold("R")}EPL ${color.dim("[T/r/q]")} `
4487
5085
  )).trim().toLowerCase();
5086
+ if (answer === "q") {
5087
+ renderer.write(color.dim(" Goodbye!\n"));
5088
+ process.exit(0);
5089
+ }
4488
5090
  mode = answer === "r" || answer === "repl" ? "repl" : "tui";
4489
5091
  }
4490
5092
  let yolo;
@@ -4492,8 +5094,12 @@ async function runLaunchPrompts(opts) {
4492
5094
  yolo = yoloPinned;
4493
5095
  } else {
4494
5096
  const answer = (await reader.readLine(
4495
- ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n]")} `
5097
+ ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n/q]")} `
4496
5098
  )).trim().toLowerCase();
5099
+ if (answer === "q") {
5100
+ renderer.write(color.dim(" Goodbye!\n"));
5101
+ process.exit(0);
5102
+ }
4497
5103
  yolo = answer !== "n" && answer !== "no";
4498
5104
  }
4499
5105
  renderer.write(
@@ -4732,14 +5338,14 @@ function summarize(value, name) {
4732
5338
  if (typeof v === "object" && v !== null) {
4733
5339
  const o = v;
4734
5340
  if (name === "edit") {
4735
- const path22 = typeof o["path"] === "string" ? o["path"] : "";
5341
+ const path24 = typeof o["path"] === "string" ? o["path"] : "";
4736
5342
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
4737
- return `${path22} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
5343
+ return `${path24} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
4738
5344
  }
4739
5345
  if (name === "write") {
4740
- const path22 = typeof o["path"] === "string" ? o["path"] : "";
5346
+ const path24 = typeof o["path"] === "string" ? o["path"] : "";
4741
5347
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
4742
- return bytes !== void 0 ? `${path22} ${bytes}B` : path22;
5348
+ return bytes !== void 0 ? `${path24} ${bytes}B` : path24;
4743
5349
  }
4744
5350
  if (typeof o["count"] === "number") {
4745
5351
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -4889,10 +5495,12 @@ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.am
4889
5495
  deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
4890
5496
  `);
4891
5497
  deps.renderer.write(` ${color.bold("b")} Back
5498
+ `);
5499
+ deps.renderer.write(` ${color.bold("q")} Quit
4892
5500
  `);
4893
5501
  const raw = (await deps.reader.readLine(`
4894
5502
  ${color.amber("?")} ${providerId} > `)).trim();
4895
- if (!raw || raw === "b" || raw === "back") return;
5503
+ if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") return;
4896
5504
  const [verb, argRaw] = raw.split(/\s+/, 2);
4897
5505
  const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
4898
5506
  if (verb === "a" || verb === "add") {
@@ -4901,8 +5509,9 @@ ${color.amber("?")} ${providerId} > `)).trim();
4901
5509
  }
4902
5510
  if (verb === "x" || verb === "remove") {
4903
5511
  const confirm = (await deps.reader.readLine(
4904
- ` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N]")} `
5512
+ ` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N/q]")} `
4905
5513
  )).trim().toLowerCase();
5514
+ if (confirm === "q") continue;
4906
5515
  if (confirm === "y" || confirm === "yes") {
4907
5516
  await mutateProviders(deps, (all) => {
4908
5517
  delete all[providerId];
@@ -4940,8 +5549,9 @@ ${color.amber("?")} ${providerId} > `)).trim();
4940
5549
  }
4941
5550
  const target = keys[arg - 1];
4942
5551
  const confirm = (await deps.reader.readLine(
4943
- ` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N]")} `
5552
+ ` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N/q]")} `
4944
5553
  )).trim().toLowerCase();
5554
+ if (confirm === "q") continue;
4945
5555
  if (confirm !== "y" && confirm !== "yes") continue;
4946
5556
  await mutateProviders(deps, (all) => {
4947
5557
  const p = all[providerId];
@@ -5038,8 +5648,8 @@ async function addForNewProvider(deps) {
5038
5648
  deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
5039
5649
  }
5040
5650
  if (catalog.length === 0) {
5041
- const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id: `)).trim();
5042
- if (!pid) return;
5651
+ const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
5652
+ if (!pid || pid === "q") return;
5043
5653
  const fam = (await deps.reader.readLine(
5044
5654
  ` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
5045
5655
  )).trim();
@@ -5059,8 +5669,9 @@ async function addForNewProvider(deps) {
5059
5669
  )
5060
5670
  );
5061
5671
  const filterRaw = (await deps.reader.readLine(
5062
- ` ${color.amber("?")} Filter ${color.dim('(substring of id/name, "s" for unsaved-only, empty = all)')}: `
5672
+ ` ${color.amber("?")} Filter ${color.dim('(substring, "s" for unsaved-only, q to quit)')}: `
5063
5673
  )).trim();
5674
+ if (filterRaw === "q") return;
5064
5675
  const filterLc = filterRaw.toLowerCase();
5065
5676
  const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
5066
5677
  const matches = (p) => {
@@ -5114,9 +5725,9 @@ async function addForNewProvider(deps) {
5114
5725
  `);
5115
5726
  const answer = (await deps.reader.readLine(
5116
5727
  `
5117
- ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
5728
+ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
5118
5729
  )).trim();
5119
- if (!answer) return;
5730
+ if (!answer || answer === "q") return;
5120
5731
  let chosen;
5121
5732
  const num = Number.parseInt(answer, 10);
5122
5733
  if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
@@ -5133,7 +5744,8 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
5133
5744
  Defaults from models.dev \u2014 press Enter to keep, or type a new value.
5134
5745
  `)
5135
5746
  );
5136
- const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)}: `)).trim();
5747
+ const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `)).trim();
5748
+ if (famRaw === "q") return;
5137
5749
  let family = chosen.family;
5138
5750
  if (famRaw) {
5139
5751
  if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
@@ -5145,8 +5757,9 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
5145
5757
  family = famRaw;
5146
5758
  }
5147
5759
  const baseRaw = (await deps.reader.readLine(
5148
- ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)}: `
5760
+ ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
5149
5761
  )).trim();
5762
+ if (baseRaw === "q") return;
5150
5763
  const baseUrl = baseRaw || chosen.apiBase;
5151
5764
  const providersNow = await loadProviders(deps);
5152
5765
  let suggestedAlias = chosen.id;
@@ -5191,17 +5804,18 @@ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies
5191
5804
  `
5192
5805
  );
5193
5806
  const type = (await deps.reader.readLine(
5194
- ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy")')}: `
5807
+ ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
5195
5808
  )).trim();
5196
- if (!type) return;
5809
+ if (!type || type === "q") return;
5197
5810
  const existing = (await loadProviders(deps))[type];
5198
5811
  if (existing) {
5199
5812
  deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
5200
5813
  return;
5201
5814
  }
5202
5815
  const familyRaw = (await deps.reader.readLine(
5203
- ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")}: `
5816
+ ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
5204
5817
  )).trim();
5818
+ if (familyRaw === "q") return;
5205
5819
  if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
5206
5820
  deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
5207
5821
  return;
@@ -5336,13 +5950,21 @@ async function loadProviders(deps) {
5336
5950
  let raw;
5337
5951
  try {
5338
5952
  raw = await fsp2.readFile(deps.globalConfigPath, "utf8");
5339
- } catch {
5953
+ } catch (err) {
5954
+ if (err.code !== "ENOENT") {
5955
+ deps.renderer.writeWarning(
5956
+ `Could not read ${deps.globalConfigPath}: ${err.message}. Treating as empty.`
5957
+ );
5958
+ }
5340
5959
  return {};
5341
5960
  }
5342
5961
  let parsed = {};
5343
5962
  try {
5344
5963
  parsed = JSON.parse(raw);
5345
- } catch {
5964
+ } catch (err) {
5965
+ deps.renderer.writeWarning(
5966
+ `Config at ${deps.globalConfigPath} is not valid JSON: ${err.message}`
5967
+ );
5346
5968
  return {};
5347
5969
  }
5348
5970
  const decrypted = decryptConfigSecrets(parsed, deps.vault);
@@ -5350,15 +5972,29 @@ async function loadProviders(deps) {
5350
5972
  }
5351
5973
  async function mutateProviders(deps, mutator) {
5352
5974
  let raw;
5975
+ let fileExists = true;
5353
5976
  try {
5354
5977
  raw = await fsp2.readFile(deps.globalConfigPath, "utf8");
5355
- } catch {
5978
+ } catch (err) {
5979
+ if (err.code !== "ENOENT") {
5980
+ throw new Error(
5981
+ `Refusing to mutate ${deps.globalConfigPath}: ${err.message}`,
5982
+ { cause: err }
5983
+ );
5984
+ }
5985
+ fileExists = false;
5356
5986
  raw = "{}";
5357
5987
  }
5358
5988
  let parsed;
5359
5989
  try {
5360
5990
  parsed = JSON.parse(raw);
5361
- } catch {
5991
+ } catch (err) {
5992
+ if (fileExists) {
5993
+ throw new Error(
5994
+ `Refusing to overwrite corrupt config at ${deps.globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
5995
+ { cause: err }
5996
+ );
5997
+ }
5362
5998
  parsed = {};
5363
5999
  }
5364
6000
  const decrypted = decryptConfigSecrets(parsed, deps.vault);
@@ -5562,7 +6198,7 @@ var doctorCmd = async (_args, deps) => {
5562
6198
  }
5563
6199
  try {
5564
6200
  await fsp2.mkdir(deps.paths.projectSessions, { recursive: true });
5565
- const probe = path21.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
6201
+ const probe = path23.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
5566
6202
  await fsp2.writeFile(probe, "");
5567
6203
  await fsp2.unlink(probe);
5568
6204
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
@@ -5665,8 +6301,8 @@ var exportCmd = async (args, deps) => {
5665
6301
  return 1;
5666
6302
  }
5667
6303
  if (output) {
5668
- await fsp2.mkdir(path21.dirname(path21.resolve(deps.cwd, output)), { recursive: true });
5669
- await fsp2.writeFile(path21.resolve(deps.cwd, output), rendered, "utf8");
6304
+ await fsp2.mkdir(path23.dirname(path23.resolve(deps.cwd, output)), { recursive: true });
6305
+ await fsp2.writeFile(path23.resolve(deps.cwd, output), rendered, "utf8");
5670
6306
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
5671
6307
  `);
5672
6308
  } else {
@@ -5695,7 +6331,12 @@ var initCmd = async (_args, deps) => {
5695
6331
  `
5696
6332
  );
5697
6333
  const defaultId = ranked[0]?.id ?? "anthropic";
5698
- const providerId = (await deps.reader.readLine(`Provider [${defaultId}]: `)).trim() || defaultId;
6334
+ const providerAnswer = (await deps.reader.readLine(`Provider [${defaultId}]: `)).trim();
6335
+ if (providerAnswer === "q") {
6336
+ deps.renderer.write(color.dim("Cancelled.\n"));
6337
+ return 0;
6338
+ }
6339
+ const providerId = providerAnswer || defaultId;
5699
6340
  const provider = await deps.modelsRegistry.getProvider(providerId);
5700
6341
  if (!provider) {
5701
6342
  deps.renderer.writeError(`Provider "${providerId}" not found in models.dev catalog.`);
@@ -5709,7 +6350,12 @@ var initCmd = async (_args, deps) => {
5709
6350
  }
5710
6351
  const suggestedModel = await deps.modelsRegistry.suggestModel(providerId) ?? "";
5711
6352
  const modelHint = suggestedModel ? ` [${suggestedModel}]` : "";
5712
- const modelId = (await deps.reader.readLine(`Model${modelHint}: `)).trim() || suggestedModel;
6353
+ const modelAnswer = (await deps.reader.readLine(`Model${modelHint}: `)).trim();
6354
+ if (modelAnswer === "q") {
6355
+ deps.renderer.write(color.dim("Cancelled.\n"));
6356
+ return 0;
6357
+ }
6358
+ const modelId = modelAnswer || suggestedModel;
5713
6359
  if (!modelId) {
5714
6360
  deps.renderer.writeError("No model selected. Aborting.");
5715
6361
  return 1;
@@ -5726,18 +6372,14 @@ var initCmd = async (_args, deps) => {
5726
6372
  await fsp2.mkdir(deps.paths.globalRoot, { recursive: true });
5727
6373
  const config = { version: 1, provider: providerId, model: modelId };
5728
6374
  if (apiKey) config.apiKey = apiKey;
5729
- const keyFile = path21.join(path21.dirname(deps.paths.globalConfig), ".key");
6375
+ const keyFile = path23.join(path23.dirname(deps.paths.globalConfig), ".key");
5730
6376
  const vault = new DefaultSecretVault$1({ keyFile });
5731
6377
  const encrypted = encryptConfigSecrets(config, vault);
5732
6378
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
5733
- await fsp2.mkdir(path21.join(deps.projectRoot, ".wrongstack"), { recursive: true });
5734
- const agentsFile = path21.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
5735
- try {
5736
- await fsp2.access(agentsFile);
5737
- } catch {
5738
- const detected2 = await detectProjectFacts(deps.projectRoot);
5739
- await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
5740
- }
6379
+ await fsp2.mkdir(path23.join(deps.projectRoot, ".wrongstack"), { recursive: true });
6380
+ const agentsFile = path23.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
6381
+ const projectFacts = await detectProjectFacts(deps.projectRoot);
6382
+ await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
5741
6383
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
5742
6384
  deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
5743
6385
  deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
@@ -5773,7 +6415,7 @@ var mcpCmd = async (args, deps) => {
5773
6415
  return removeMcpServer(name, deps);
5774
6416
  }
5775
6417
  if (sub === "restart") {
5776
- deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
6418
+ deps.renderer.writeWarning("mcp restart is only available in REPL mode. Use /mcp restart instead.");
5777
6419
  return 0;
5778
6420
  }
5779
6421
  deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
@@ -5948,7 +6590,7 @@ function renderConfiguredPlugins(config) {
5948
6590
  return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
5949
6591
  }).join("\n");
5950
6592
  }
5951
- async function readConfig(file) {
6593
+ async function readConfig2(file) {
5952
6594
  try {
5953
6595
  return JSON.parse(await fsp2.readFile(file, "utf8"));
5954
6596
  } catch {
@@ -5967,7 +6609,7 @@ function officialPluginState(config, spec) {
5967
6609
  return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
5968
6610
  }
5969
6611
  async function upsertPlugin(spec, opts, deps, verb) {
5970
- const existing = await readConfig(deps.configPath);
6612
+ const existing = await readConfig2(deps.configPath);
5971
6613
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
5972
6614
  const idx = plugins.findIndex((p) => pluginName(p) === spec);
5973
6615
  const nextEntry = pluginEntry(spec, opts.enabled);
@@ -5990,7 +6632,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
5990
6632
  };
5991
6633
  }
5992
6634
  async function removePlugin(spec, deps) {
5993
- const existing = await readConfig(deps.configPath);
6635
+ const existing = await readConfig2(deps.configPath);
5994
6636
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
5995
6637
  const next = plugins.filter((p) => pluginName(p) !== spec);
5996
6638
  if (next.length === plugins.length) {
@@ -6041,7 +6683,7 @@ var usageCmd = async (_args, deps) => {
6041
6683
  return 0;
6042
6684
  };
6043
6685
  var projectsCmd = async (_args, deps) => {
6044
- const projectsRoot = path21.join(deps.paths.globalRoot, "projects");
6686
+ const projectsRoot = path23.join(deps.paths.globalRoot, "projects");
6045
6687
  try {
6046
6688
  const entries = await fsp2.readdir(projectsRoot);
6047
6689
  if (entries.length === 0) {
@@ -6051,7 +6693,7 @@ var projectsCmd = async (_args, deps) => {
6051
6693
  for (const hash of entries) {
6052
6694
  try {
6053
6695
  const meta = JSON.parse(
6054
- await fsp2.readFile(path21.join(projectsRoot, hash, "meta.json"), "utf8")
6696
+ await fsp2.readFile(path23.join(projectsRoot, hash, "meta.json"), "utf8")
6055
6697
  );
6056
6698
  deps.renderer.write(
6057
6699
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -6212,7 +6854,7 @@ async function listFleetRuns(deps) {
6212
6854
  }
6213
6855
  const runs = [];
6214
6856
  for (const id of entries) {
6215
- const runDir = path21.join(deps.paths.projectSessions, id);
6857
+ const runDir = path23.join(deps.paths.projectSessions, id);
6216
6858
  let stat3;
6217
6859
  try {
6218
6860
  stat3 = await fsp2.stat(runDir);
@@ -6225,17 +6867,17 @@ async function listFleetRuns(deps) {
6225
6867
  let subagentCount = 0;
6226
6868
  let subagentsDir;
6227
6869
  try {
6228
- await fsp2.access(path21.join(runDir, "fleet.json"));
6870
+ await fsp2.access(path23.join(runDir, "fleet.json"));
6229
6871
  manifest = true;
6230
6872
  } catch {
6231
6873
  }
6232
6874
  try {
6233
- await fsp2.access(path21.join(runDir, "checkpoint.json"));
6875
+ await fsp2.access(path23.join(runDir, "checkpoint.json"));
6234
6876
  checkpoint = true;
6235
6877
  } catch {
6236
6878
  }
6237
6879
  try {
6238
- subagentsDir = path21.join(runDir, "subagents");
6880
+ subagentsDir = path23.join(runDir, "subagents");
6239
6881
  const files = await fsp2.readdir(subagentsDir);
6240
6882
  subagentCount = files.filter((f) => f.endsWith(".jsonl")).length;
6241
6883
  } catch {
@@ -6264,7 +6906,7 @@ async function listFleetRuns(deps) {
6264
6906
  return 0;
6265
6907
  }
6266
6908
  async function showFleetRun(runId, deps) {
6267
- const runDir = path21.join(deps.paths.projectSessions, runId);
6909
+ const runDir = path23.join(deps.paths.projectSessions, runId);
6268
6910
  let stat3;
6269
6911
  try {
6270
6912
  stat3 = await fsp2.stat(runDir);
@@ -6281,7 +6923,7 @@ async function showFleetRun(runId, deps) {
6281
6923
  deps.renderer.write(color.bold(`
6282
6924
  Fleet Run: ${runId}
6283
6925
  `) + "\n");
6284
- const manifestPath = path21.join(runDir, "fleet.json");
6926
+ const manifestPath = path23.join(runDir, "fleet.json");
6285
6927
  let manifestData = null;
6286
6928
  try {
6287
6929
  manifestData = await fsp2.readFile(manifestPath, "utf8");
@@ -6297,7 +6939,7 @@ Fleet Run: ${runId}
6297
6939
  deps.renderer.write(` ${color.dim("\u25CB")} fleet.json \u2014 not found
6298
6940
  `);
6299
6941
  }
6300
- const checkpointPath = path21.join(runDir, "checkpoint.json");
6942
+ const checkpointPath = path23.join(runDir, "checkpoint.json");
6301
6943
  let checkpointData = null;
6302
6944
  try {
6303
6945
  checkpointData = await fsp2.readFile(checkpointPath, "utf8");
@@ -6344,7 +6986,7 @@ Fleet Run: ${runId}
6344
6986
  } catch {
6345
6987
  }
6346
6988
  }
6347
- const subagentsDir = path21.join(runDir, "subagents");
6989
+ const subagentsDir = path23.join(runDir, "subagents");
6348
6990
  let subagentFiles = [];
6349
6991
  try {
6350
6992
  subagentFiles = await fsp2.readdir(subagentsDir);
@@ -6356,7 +6998,7 @@ Fleet Run: ${runId}
6356
6998
  Subagent transcripts (${subagentFiles.length}):
6357
6999
  `);
6358
7000
  for (const f of subagentFiles.sort()) {
6359
- const filePath = path21.join(subagentsDir, f);
7001
+ const filePath = path23.join(subagentsDir, f);
6360
7002
  let size;
6361
7003
  try {
6362
7004
  const s = await fsp2.stat(filePath);
@@ -6373,7 +7015,7 @@ Fleet Run: ${runId}
6373
7015
  ${color.dim("\u25CB")} No subagent transcripts
6374
7016
  `);
6375
7017
  }
6376
- const sharedDir = path21.join(runDir, "shared");
7018
+ const sharedDir = path23.join(runDir, "shared");
6377
7019
  try {
6378
7020
  const files = await fsp2.readdir(sharedDir);
6379
7021
  deps.renderer.write(`
@@ -6526,12 +7168,23 @@ function parseRewindFlags(args) {
6526
7168
  }
6527
7169
  return flags;
6528
7170
  }
7171
+ function findSessionId(args) {
7172
+ for (let i = 0; i < args.length; i++) {
7173
+ const a = args[i];
7174
+ if (a === "--last" || a === "--to") {
7175
+ i++;
7176
+ continue;
7177
+ }
7178
+ if (!a.startsWith("--")) return a;
7179
+ }
7180
+ return void 0;
7181
+ }
6529
7182
  var rewindCmd = async (args, deps) => {
6530
7183
  const flags = parseRewindFlags(args);
6531
7184
  const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
6532
- const sessionsDir = path21.join(wpaths.globalRoot, "sessions");
7185
+ const sessionsDir = path23.join(wpaths.globalRoot, "sessions");
6533
7186
  const rewind = new DefaultSessionRewinder(sessionsDir);
6534
- let sessionId = args.find((a) => !a.startsWith("--"));
7187
+ let sessionId = findSessionId(args);
6535
7188
  if (!sessionId) {
6536
7189
  if (!deps.sessionStore) {
6537
7190
  deps.renderer.writeError("No session store available.");
@@ -6672,6 +7325,7 @@ var helpCmd = async (_args, deps) => {
6672
7325
  "",
6673
7326
  " wstack Start REPL",
6674
7327
  ' wstack "<task>" Run task and exit',
7328
+ ' wstack --eternal "<mission>" Launch eternal-autonomy loop against a goal \u2014 Ctrl+C to stop',
6675
7329
  " wstack resume [<id>] Resume a session",
6676
7330
  " wstack sessions List recent sessions",
6677
7331
  " wstack init Pick provider + model from models.dev",
@@ -6739,22 +7393,22 @@ function fmtDuration(ms) {
6739
7393
  const remMin = m - h * 60;
6740
7394
  return `${h}h${remMin}m`;
6741
7395
  }
6742
- function fmtTaskResultLine(r, color33) {
7396
+ function fmtTaskResultLine(r, color35) {
6743
7397
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
6744
7398
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
6745
7399
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
6746
7400
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
6747
- const errKindChip = errKind ? color33.dim(` [${errKind}]`) : "";
6748
- const errSnip = errMsg || errKind ? `${errKindChip}${color33.dim(errTail)}` : "";
7401
+ const errKindChip = errKind ? color35.dim(` [${errKind}]`) : "";
7402
+ const errSnip = errMsg || errKind ? `${errKindChip}${color35.dim(errTail)}` : "";
6749
7403
  switch (r.status) {
6750
7404
  case "success":
6751
- return { mark: color33.green("\u2713"), stats, tail: "" };
7405
+ return { mark: color35.green("\u2713"), stats, tail: "" };
6752
7406
  case "timeout":
6753
- return { mark: color33.yellow("\u23F1"), stats: `${color33.yellow("timeout")} ${stats}`, tail: errSnip };
7407
+ return { mark: color35.yellow("\u23F1"), stats: `${color35.yellow("timeout")} ${stats}`, tail: errSnip };
6754
7408
  case "stopped":
6755
- return { mark: color33.dim("\u2298"), stats: `${color33.dim("stopped")} ${stats}`, tail: errSnip };
7409
+ return { mark: color35.dim("\u2298"), stats: `${color35.dim("stopped")} ${stats}`, tail: errSnip };
6756
7410
  case "failed":
6757
- return { mark: color33.red("\u2717"), stats: `${color33.red("failed")} ${stats}`, tail: errSnip };
7411
+ return { mark: color35.red("\u2717"), stats: `${color35.red("failed")} ${stats}`, tail: errSnip };
6758
7412
  }
6759
7413
  }
6760
7414
 
@@ -6764,7 +7418,7 @@ function resolveBundledSkillsDir() {
6764
7418
  try {
6765
7419
  const req2 = createRequire(import.meta.url);
6766
7420
  const corePkg = req2.resolve("@wrongstack/core/package.json");
6767
- return path21.join(path21.dirname(corePkg), "skills");
7421
+ return path23.join(path23.dirname(corePkg), "skills");
6768
7422
  } catch {
6769
7423
  return void 0;
6770
7424
  }
@@ -6917,6 +7571,7 @@ async function boot(argv) {
6917
7571
  init_sdd();
6918
7572
  async function runRepl(opts) {
6919
7573
  if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
7574
+ await renderGoalBanner(opts);
6920
7575
  let activeCtrl;
6921
7576
  let interrupts = 0;
6922
7577
  const onSigint = () => {
@@ -6925,6 +7580,12 @@ async function runRepl(opts) {
6925
7580
  opts.renderer.writeWarning("Exiting.");
6926
7581
  process.exit(130);
6927
7582
  }
7583
+ const engine = opts.getEternalEngine?.();
7584
+ if (engine && opts.getAutonomy?.() === "eternal") {
7585
+ engine.stop();
7586
+ opts.renderer.writeWarning("Eternal mode stop requested. Press Ctrl+C again to exit.");
7587
+ return;
7588
+ }
6928
7589
  if (activeCtrl) {
6929
7590
  activeCtrl.abort();
6930
7591
  opts.renderer.writeWarning("Iteration cancelled. Press Ctrl+C again to exit.");
@@ -6936,6 +7597,42 @@ async function runRepl(opts) {
6936
7597
  const builder = new InputBuilder({ store: opts.attachments });
6937
7598
  try {
6938
7599
  for (; ; ) {
7600
+ if (opts.getAutonomy?.() === "eternal") {
7601
+ const engine = opts.getEternalEngine?.();
7602
+ if (!engine) {
7603
+ opts.renderer.writeWarning("Eternal mode set but no engine wired \u2014 falling back to off.");
7604
+ } else {
7605
+ const beforeGoal = await loadGoalSafe(opts);
7606
+ const beforeIter = beforeGoal?.iterations ?? 0;
7607
+ opts.renderer.write(
7608
+ color.dim(`
7609
+ \u21B3 [eternal #${beforeIter + 1}] running iteration\u2026
7610
+ `)
7611
+ );
7612
+ interrupts = 0;
7613
+ try {
7614
+ const ok = await engine.runOneIteration();
7615
+ const afterGoal = await loadGoalSafe(opts);
7616
+ const last = afterGoal?.journal[afterGoal.journal.length - 1];
7617
+ if (!ok && !last) {
7618
+ opts.renderer.write(color.dim(" \u21B3 [eternal] iteration produced no progress.\n"));
7619
+ } else if (last) {
7620
+ const mark = last.status === "success" ? color.green("\u2713") : last.status === "failure" ? color.red("\u2717") : color.amber("\u2298");
7621
+ const tail = last.note ? color.dim(` \u2014 ${last.note.slice(0, 80)}`) : "";
7622
+ opts.renderer.write(
7623
+ ` ${mark} ${color.dim(`#${last.iteration}`)} ${color.dim(`[${last.source}]`)} ${last.task}${tail}
7624
+ `
7625
+ );
7626
+ }
7627
+ } catch (err) {
7628
+ opts.renderer.writeError(
7629
+ `[eternal] ${err instanceof Error ? err.message : String(err)}`
7630
+ );
7631
+ }
7632
+ await new Promise((resolve4) => setTimeout(resolve4, 250));
7633
+ continue;
7634
+ }
7635
+ }
6939
7636
  let raw;
6940
7637
  try {
6941
7638
  raw = await readPossiblyMultiline(opts);
@@ -6948,6 +7645,10 @@ async function runRepl(opts) {
6948
7645
  continue;
6949
7646
  }
6950
7647
  interrupts = 0;
7648
+ if (trimmed === "q") {
7649
+ opts.renderer.write(color.dim(" Goodbye!\n"));
7650
+ break;
7651
+ }
6951
7652
  if (trimmed === "/image" || trimmed === "/paste-image" || raw === "\x1Bv") {
6952
7653
  await pasteClipboardImage(builder, opts);
6953
7654
  continue;
@@ -7219,6 +7920,48 @@ async function pasteClipboardImage(builder, opts) {
7219
7920
  );
7220
7921
  }
7221
7922
  }
7923
+ async function loadGoalSafe(opts) {
7924
+ if (!opts.projectRoot) return null;
7925
+ try {
7926
+ return await loadGoal(goalFilePath(opts.projectRoot));
7927
+ } catch {
7928
+ return null;
7929
+ }
7930
+ }
7931
+ async function renderGoalBanner(opts) {
7932
+ const goal = await loadGoalSafe(opts);
7933
+ if (!goal) return;
7934
+ const summary = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
7935
+ opts.renderer.write(
7936
+ color.dim("Goal: ") + color.bold(summary) + color.dim(` (iter ${goal.iterations})`) + "\n"
7937
+ );
7938
+ if (goal.engineState === "running") {
7939
+ opts.renderer.write(
7940
+ color.amber(" \u21BA Eternal engine was running when last session ended.") + "\n"
7941
+ );
7942
+ try {
7943
+ const answer = (await opts.reader.readLine(color.dim(" Resume eternal mode? [y/N] "))).trim().toLowerCase();
7944
+ if (answer === "y" || answer === "yes") {
7945
+ try {
7946
+ await opts.slashRegistry.dispatch("/autonomy eternal", opts.agent.ctx);
7947
+ } catch (err) {
7948
+ opts.renderer.writeError(
7949
+ `Auto-resume failed: ${err instanceof Error ? err.message : String(err)}`
7950
+ );
7951
+ }
7952
+ } else {
7953
+ opts.renderer.write(
7954
+ color.dim(" Not resuming. Use `/autonomy eternal` later to continue.") + "\n"
7955
+ );
7956
+ }
7957
+ } catch {
7958
+ opts.renderer.write(
7959
+ color.dim(" Use `/autonomy eternal` to resume.") + "\n"
7960
+ );
7961
+ }
7962
+ }
7963
+ opts.renderer.write("\n");
7964
+ }
7222
7965
  async function readPossiblyMultiline(opts) {
7223
7966
  const firstPrompt = theme2.primary("\u203A ");
7224
7967
  const contPrompt = color.dim("\xB7 ");
@@ -7266,7 +8009,7 @@ function printBanner(renderer, projectName) {
7266
8009
  if (projectName && projectName.length > 0) {
7267
8010
  lines.push(color.dim("Project: ") + theme2.bold(projectName));
7268
8011
  }
7269
- lines.push(color.dim("Type /help for commands, /exit to quit."), "");
8012
+ lines.push(color.dim("Type /help for commands, /exit or q to quit."), "");
7270
8013
  renderer.write(`${lines.join("\n")}
7271
8014
  `);
7272
8015
  }
@@ -7302,8 +8045,12 @@ async function execute(deps) {
7302
8045
  director,
7303
8046
  fleetRoster,
7304
8047
  fleetStreamController,
8048
+ statuslineHiddenItems,
8049
+ setStatuslineHiddenItems,
7305
8050
  getYolo,
7306
8051
  getAutonomy,
8052
+ getEternalEngine,
8053
+ subscribeEternalIteration,
7307
8054
  skillLoader
7308
8055
  } = deps;
7309
8056
  let code = 0;
@@ -7411,6 +8158,9 @@ async function execute(deps) {
7411
8158
  queueStore,
7412
8159
  yolo: !!config.yolo,
7413
8160
  getYolo,
8161
+ getAutonomy,
8162
+ getEternalEngine,
8163
+ subscribeEternalIteration,
7414
8164
  appVersion: CLI_VERSION,
7415
8165
  provider: config.provider,
7416
8166
  family: banneredFamily,
@@ -7436,6 +8186,8 @@ async function execute(deps) {
7436
8186
  dispatch({ type: "resetContextChip" });
7437
8187
  },
7438
8188
  fleetStreamController,
8189
+ statuslineHiddenItems,
8190
+ setStatuslineHiddenItems,
7439
8191
  initialGoal: goalFlag,
7440
8192
  initialAsk: askFlag,
7441
8193
  getSDDContext: () => {
@@ -7479,7 +8231,8 @@ async function execute(deps) {
7479
8231
  session,
7480
8232
  port: Number.parseInt(String(flags.port ?? "3457"), 10),
7481
8233
  modelsRegistry,
7482
- globalConfigPath: wpaths.globalConfig
8234
+ globalConfigPath: wpaths.globalConfig,
8235
+ subscribeEternalIteration
7483
8236
  });
7484
8237
  try {
7485
8238
  code = await runRepl({
@@ -7492,8 +8245,10 @@ async function execute(deps) {
7492
8245
  supportsVision,
7493
8246
  attachments,
7494
8247
  effectiveMaxContext,
7495
- projectName: path21.basename(projectRoot) || void 0,
8248
+ projectName: path23.basename(projectRoot) || void 0,
8249
+ projectRoot,
7496
8250
  getAutonomy,
8251
+ getEternalEngine,
7497
8252
  skillLoader
7498
8253
  });
7499
8254
  } finally {
@@ -7510,7 +8265,7 @@ async function execute(deps) {
7510
8265
  supportsVision,
7511
8266
  attachments,
7512
8267
  effectiveMaxContext,
7513
- projectName: path21.basename(projectRoot) || void 0,
8268
+ projectName: path23.basename(projectRoot) || void 0,
7514
8269
  getAutonomy,
7515
8270
  skillLoader
7516
8271
  });
@@ -7599,9 +8354,9 @@ var MultiAgentHost = class {
7599
8354
  const coordinatorConfig = {
7600
8355
  coordinatorId: randomUUID(),
7601
8356
  doneCondition: { type: "all_tasks_done" },
7602
- maxConcurrent: 8
8357
+ maxConcurrent: this.opts.maxConcurrent ?? 4
7603
8358
  };
7604
- const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path21.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
8359
+ const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path23.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
7605
8360
  this.director = new Director({
7606
8361
  config: coordinatorConfig,
7607
8362
  manifestPath: this.opts.manifestPath,
@@ -7687,6 +8442,16 @@ var MultiAgentHost = class {
7687
8442
  model: subCfg.model ?? config.model,
7688
8443
  tools: this.filterTools(subCfg.tools)
7689
8444
  });
8445
+ const toolExecutor = new ToolExecutor(this.subagentToolRegistry(subCfg.tools), {
8446
+ permissionPolicy: new AutoApprovePermissionPolicy(),
8447
+ secretScrubber: this.deps.secretScrubber,
8448
+ renderer: this.deps.renderer,
8449
+ events,
8450
+ confirmAwaiter: void 0,
8451
+ iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? 12e4,
8452
+ perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? 1e5,
8453
+ tracer: void 0
8454
+ });
7690
8455
  const agent = new Agent({
7691
8456
  container: this.deps.container,
7692
8457
  tools: this.subagentToolRegistry(subCfg.tools),
@@ -7698,7 +8463,8 @@ var MultiAgentHost = class {
7698
8463
  // run under a director, not the user. Auto-approve everything
7699
8464
  // (except tool-level hard denies); the user already authorized
7700
8465
  // the work when they invoked the leader.
7701
- permissionPolicy: new AutoApprovePermissionPolicy()
8466
+ permissionPolicy: new AutoApprovePermissionPolicy(),
8467
+ toolExecutor
7702
8468
  });
7703
8469
  const hostEvents = this.deps.events;
7704
8470
  const offToolBridge = events.on("tool.executed", (e) => {
@@ -7774,7 +8540,7 @@ var MultiAgentHost = class {
7774
8540
  model: opts?.model,
7775
8541
  tools: opts?.tools
7776
8542
  };
7777
- const transcriptPath = this.sessionFactory ? path21.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
8543
+ const transcriptPath = this.sessionFactory ? path23.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
7778
8544
  const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig);
7779
8545
  this.fleetManager?.addPendingTask(taskId, subagentId, description);
7780
8546
  this.deps.events.emit("subagent.spawned", {
@@ -7917,16 +8683,16 @@ var MultiAgentHost = class {
7917
8683
  if (this.director) return this.director;
7918
8684
  this.opts.directorMode = true;
7919
8685
  if (this.opts.fleetRoot && !this.opts.manifestPath) {
7920
- this.opts.manifestPath = path21.join(this.opts.fleetRoot, "fleet.json");
8686
+ this.opts.manifestPath = path23.join(this.opts.fleetRoot, "fleet.json");
7921
8687
  }
7922
8688
  if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
7923
- this.opts.sharedScratchpadPath = path21.join(this.opts.fleetRoot, "shared");
8689
+ this.opts.sharedScratchpadPath = path23.join(this.opts.fleetRoot, "shared");
7924
8690
  }
7925
8691
  if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
7926
- this.opts.sessionsRoot = path21.join(this.opts.fleetRoot, "subagents");
8692
+ this.opts.sessionsRoot = path23.join(this.opts.fleetRoot, "subagents");
7927
8693
  }
7928
8694
  if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
7929
- this.opts.stateCheckpointPath = path21.join(this.opts.fleetRoot, "director-state.json");
8695
+ this.opts.stateCheckpointPath = path23.join(this.opts.fleetRoot, "director-state.json");
7930
8696
  }
7931
8697
  await this.ensureDirector();
7932
8698
  return this.director ?? null;
@@ -7965,6 +8731,37 @@ var MultiAgentHost = class {
7965
8731
  await this.getCoordinator().stopAll();
7966
8732
  }
7967
8733
  }
8734
+ /**
8735
+ * Current effective concurrent-subagent ceiling. Reads the live
8736
+ * coordinator config when the director is built; otherwise falls back
8737
+ * to the constructor option (or the default of 4 that buildDirector
8738
+ * will apply on first /spawn).
8739
+ */
8740
+ getMaxConcurrent() {
8741
+ if (this.director) {
8742
+ return this.getCoordinator().config.maxConcurrent ?? 4;
8743
+ }
8744
+ return this.opts.maxConcurrent ?? 4;
8745
+ }
8746
+ /**
8747
+ * Change the concurrent-subagent ceiling at runtime. Updates the
8748
+ * constructor option (so lazy-built director picks it up) and, if the
8749
+ * coordinator already exists, mutates its live config + triggers a
8750
+ * dispatch pass so newly-allowed slots fill immediately.
8751
+ *
8752
+ * Throws on non-positive values; the caller is expected to validate
8753
+ * user input first.
8754
+ */
8755
+ setMaxConcurrent(n) {
8756
+ if (!Number.isFinite(n) || n < 1) {
8757
+ throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
8758
+ }
8759
+ const v = Math.floor(n);
8760
+ this.opts.maxConcurrent = v;
8761
+ if (this.director) {
8762
+ this.getCoordinator().setMaxConcurrent(v);
8763
+ }
8764
+ }
7968
8765
  };
7969
8766
  function makePromptDelegate(reader) {
7970
8767
  return async (tool, input, suggestedPattern) => {
@@ -8047,11 +8844,11 @@ var SessionStats = class {
8047
8844
  if (e.name === "bash") this.bashCommands++;
8048
8845
  else if (e.name === "fetch") this.fetches++;
8049
8846
  if (!e.ok) return;
8050
- const path22 = typeof input?.path === "string" ? input.path : void 0;
8051
- if (e.name === "read" && path22) this.readPaths.add(path22);
8052
- else if (e.name === "edit" && path22) this.editedPaths.add(path22);
8053
- else if (e.name === "write" && path22) {
8054
- this.writtenPaths.add(path22);
8847
+ const path24 = typeof input?.path === "string" ? input.path : void 0;
8848
+ if (e.name === "read" && path24) this.readPaths.add(path24);
8849
+ else if (e.name === "edit" && path24) this.editedPaths.add(path24);
8850
+ else if (e.name === "write" && path24) {
8851
+ this.writtenPaths.add(path24);
8055
8852
  const content = typeof input?.content === "string" ? input.content : "";
8056
8853
  this.bytesWritten += Buffer.byteLength(content, "utf8");
8057
8854
  }
@@ -8277,6 +9074,19 @@ async function setupCompaction(params) {
8277
9074
  return { effectiveMaxContext, autoCompactor };
8278
9075
  }
8279
9076
  function createAgent(params) {
9077
+ const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
9078
+ const renderer = params.container.has(TOKENS.Renderer) ? params.container.resolve(TOKENS.Renderer) : void 0;
9079
+ params.container.resolve(TOKENS.Logger);
9080
+ const toolExecutor = new ToolExecutor(params.tools, {
9081
+ permissionPolicy: params.permissionPolicy ?? params.container.resolve(TOKENS.PermissionPolicy),
9082
+ secretScrubber,
9083
+ renderer,
9084
+ events: params.events,
9085
+ confirmAwaiter: params.confirmAwaiter,
9086
+ iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
9087
+ perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
9088
+ tracer: params.tracer
9089
+ });
8280
9090
  return new Agent({
8281
9091
  container: params.container,
8282
9092
  tools: params.tools,
@@ -8288,9 +9098,144 @@ function createAgent(params) {
8288
9098
  iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
8289
9099
  executionStrategy: params.config.tools.defaultExecutionStrategy,
8290
9100
  perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
8291
- confirmAwaiter: params.confirmAwaiter
9101
+ confirmAwaiter: params.confirmAwaiter,
9102
+ toolExecutor,
9103
+ tracer: params.tracer
8292
9104
  });
8293
9105
  }
9106
+ function setupMetrics(params) {
9107
+ const { flags, wpaths, events, logger, config } = params;
9108
+ let metricsSink;
9109
+ let healthRegistry;
9110
+ let metricsServerHandle;
9111
+ const metricsPortFlag = flags["metrics-port"];
9112
+ const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
9113
+ if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
9114
+ if (!flags.metrics) return { metricsSink, healthRegistry, metricsServerHandle };
9115
+ metricsSink = new InMemoryMetricsSink();
9116
+ wireMetricsToEvents(events, metricsSink);
9117
+ healthRegistry = new DefaultHealthRegistry();
9118
+ healthRegistry.register({
9119
+ name: "session-store",
9120
+ check: async () => {
9121
+ try {
9122
+ await fsp2.access(wpaths.projectSessions);
9123
+ return { status: "healthy" };
9124
+ } catch (e) {
9125
+ return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
9126
+ }
9127
+ }
9128
+ });
9129
+ healthRegistry.register({
9130
+ name: "provider",
9131
+ check: async () => ({
9132
+ status: "healthy",
9133
+ data: { id: config.provider, model: config.model }
9134
+ })
9135
+ });
9136
+ const dumpMetrics = () => {
9137
+ if (!metricsSink) return;
9138
+ try {
9139
+ const out = path23.join(wpaths.projectSessions, "metrics.json");
9140
+ const snap = metricsSink.snapshot();
9141
+ writeFileSync(out, JSON.stringify(snap, null, 2));
9142
+ } catch {
9143
+ }
9144
+ };
9145
+ process.on("exit", dumpMetrics);
9146
+ if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
9147
+ try {
9148
+ metricsServerHandle = startMetricsServer({
9149
+ port: metricsPort,
9150
+ host: process.env["METRICS_HOST"] ?? "127.0.0.1",
9151
+ sink: metricsSink,
9152
+ healthRegistry
9153
+ });
9154
+ logger.info(
9155
+ `metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`
9156
+ );
9157
+ process.on("exit", () => {
9158
+ void metricsServerHandle?.close().catch(() => {
9159
+ });
9160
+ });
9161
+ } catch (err) {
9162
+ logger.warn(
9163
+ `metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`
9164
+ );
9165
+ }
9166
+ }
9167
+ return { metricsSink, healthRegistry, metricsServerHandle };
9168
+ }
9169
+ function createApi(ownerName, base) {
9170
+ return new DefaultPluginAPI({ ownerName, ...base });
9171
+ }
9172
+
9173
+ // src/wiring/plugins.ts
9174
+ async function setupPlugins(params) {
9175
+ const {
9176
+ config,
9177
+ container,
9178
+ events,
9179
+ toolRegistry,
9180
+ providerRegistry,
9181
+ slashCommandRegistry,
9182
+ mcpRegistry,
9183
+ log,
9184
+ agent,
9185
+ sessionWriter,
9186
+ metricsSink,
9187
+ configStore,
9188
+ pipelines
9189
+ } = params;
9190
+ if (!config.features.plugins || !config.plugins || config.plugins.length === 0) return;
9191
+ const resolvedPlugins = [];
9192
+ for (const p of config.plugins) {
9193
+ if (typeof p === "object" && p.enabled === false) continue;
9194
+ const spec = typeof p === "string" ? p : p.name;
9195
+ try {
9196
+ const mod = await import(spec);
9197
+ if (mod.default) resolvedPlugins.push(mod.default);
9198
+ } catch (err) {
9199
+ log.warn(`Plugin "${spec}" failed to load`, err);
9200
+ }
9201
+ }
9202
+ if (resolvedPlugins.length === 0) return;
9203
+ const pluginOptions = buildPluginOptions(config);
9204
+ const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
9205
+ await loadPlugins(resolvedPlugins, {
9206
+ log,
9207
+ pluginOptions,
9208
+ apiFactory: (plugin) => createApi(plugin.name, {
9209
+ container,
9210
+ events,
9211
+ pipelines,
9212
+ toolRegistry,
9213
+ providerRegistry,
9214
+ slashCommandRegistry,
9215
+ mcpRegistry,
9216
+ config: pluginConfig,
9217
+ log,
9218
+ extensions: agent.extensions,
9219
+ sessionWriter: {
9220
+ transcriptPath: sessionWriter.transcriptPath,
9221
+ append: (e) => sessionWriter.append(e)
9222
+ },
9223
+ metricsSink,
9224
+ configStore
9225
+ })
9226
+ });
9227
+ }
9228
+ function buildPluginOptions(config) {
9229
+ const options = {};
9230
+ for (const entry of config.plugins ?? []) {
9231
+ if (typeof entry !== "object") continue;
9232
+ if (entry.options) options[entry.name] = { ...entry.options };
9233
+ }
9234
+ for (const [name, value] of Object.entries(config.extensions ?? {})) {
9235
+ options[name] = { ...options[name] ?? {}, ...value };
9236
+ }
9237
+ return options;
9238
+ }
8294
9239
  async function setupProvider(params) {
8295
9240
  const { config, modelsRegistry, logger } = params;
8296
9241
  const savedProviderCfg = config.providers?.[config.provider];
@@ -8382,12 +9327,12 @@ async function setupSession(params) {
8382
9327
  }
8383
9328
  const sessionRef = { current: session };
8384
9329
  await recoveryLock.write(session.id).catch(() => void 0);
8385
- const attachments = new DefaultAttachmentStore({ spoolDir: path21.join(wpaths.projectSessions, session.id, "attachments") });
8386
- const queueStore = new QueueStore({ dir: path21.join(wpaths.projectSessions, session.id) });
9330
+ const attachments = new DefaultAttachmentStore({ spoolDir: path23.join(wpaths.projectSessions, session.id, "attachments") });
9331
+ const queueStore = new QueueStore({ dir: path23.join(wpaths.projectSessions, session.id) });
8387
9332
  const ctxSignal = new AbortController().signal;
8388
9333
  const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
8389
9334
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
8390
- const todosCheckpointPath = path21.join(wpaths.projectSessions, `${session.id}.todos.json`);
9335
+ const todosCheckpointPath = path23.join(wpaths.projectSessions, `${session.id}.todos.json`);
8391
9336
  if (resumeId) {
8392
9337
  try {
8393
9338
  const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
@@ -8399,13 +9344,13 @@ async function setupSession(params) {
8399
9344
  }
8400
9345
  }
8401
9346
  const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
8402
- const planPath = path21.join(wpaths.projectSessions, `${session.id}.plan.json`);
9347
+ const planPath = path23.join(wpaths.projectSessions, `${session.id}.plan.json`);
8403
9348
  context.state.setMeta("plan.path", planPath);
8404
9349
  let dirState;
8405
9350
  if (resumeId) {
8406
9351
  try {
8407
- const fleetRoot = path21.join(wpaths.projectSessions, session.id);
8408
- dirState = await loadDirectorState(path21.join(fleetRoot, "director-state.json"));
9352
+ const fleetRoot = path23.join(wpaths.projectSessions, session.id);
9353
+ dirState = await loadDirectorState(path23.join(fleetRoot, "director-state.json"));
8409
9354
  if (dirState) {
8410
9355
  const tCounts = {};
8411
9356
  for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
@@ -8432,22 +9377,11 @@ function resolveBundledSkillsDir2() {
8432
9377
  try {
8433
9378
  const req2 = createRequire(import.meta.url);
8434
9379
  const corePkg = req2.resolve("@wrongstack/core/package.json");
8435
- return path21.join(path21.dirname(corePkg), "skills");
9380
+ return path23.join(path23.dirname(corePkg), "skills");
8436
9381
  } catch {
8437
9382
  return void 0;
8438
9383
  }
8439
9384
  }
8440
- function buildPluginOptions(config) {
8441
- const options = {};
8442
- for (const entry of config.plugins ?? []) {
8443
- if (typeof entry !== "object") continue;
8444
- if (entry.options) options[entry.name] = { ...entry.options };
8445
- }
8446
- for (const [name, value] of Object.entries(config.extensions ?? {})) {
8447
- options[name] = { ...options[name] ?? {}, ...value };
8448
- }
8449
- return options;
8450
- }
8451
9385
  async function main(argv) {
8452
9386
  const ctx = await boot(argv);
8453
9387
  if (typeof ctx === "number") return ctx;
@@ -8530,6 +9464,8 @@ async function main(argv) {
8530
9464
  const memoryStore = container.resolve(TOKENS.MemoryStore);
8531
9465
  const skillLoader = container.resolve(TOKENS.SkillLoader);
8532
9466
  const sessionRef = {};
9467
+ const autonomyModeRef = { current: "off" };
9468
+ const goalPathForPrompt = path23.join(projectRoot, ".wrongstack", "goal.json");
8533
9469
  container.bind(
8534
9470
  TOKENS.SystemPromptBuilder,
8535
9471
  () => new DefaultSystemPromptBuilder({
@@ -8539,7 +9475,17 @@ async function main(argv) {
8539
9475
  modeId,
8540
9476
  modePrompt,
8541
9477
  modelCapabilities,
8542
- planPath: () => sessionRef.current ? path21.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
9478
+ planPath: () => sessionRef.current ? path23.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
9479
+ contributors: [
9480
+ // Injects the ETERNAL AUTONOMY block when the user has activated
9481
+ // `/autonomy eternal`. Without this, the per-iteration directive
9482
+ // is the only place the model sees the rules — compaction can
9483
+ // drop it and the model forgets it's in autonomy mode.
9484
+ makeAutonomyPromptContributor({
9485
+ goalPath: goalPathForPrompt,
9486
+ enabled: () => autonomyModeRef.current === "eternal"
9487
+ })
9488
+ ]
8543
9489
  })
8544
9490
  );
8545
9491
  const toolRegistry = new ToolRegistry();
@@ -8553,72 +9499,10 @@ async function main(argv) {
8553
9499
  }
8554
9500
  const events = new EventBus();
8555
9501
  events.setLogger(logger);
8556
- let metricsSink;
8557
- let healthRegistry;
8558
- let metricsServerHandle;
8559
- const metricsPortFlag = flags["metrics-port"];
8560
- const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
8561
- if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
8562
- if (flags.metrics) {
8563
- metricsSink = new InMemoryMetricsSink();
8564
- wireMetricsToEvents(events, metricsSink);
8565
- healthRegistry = new DefaultHealthRegistry();
8566
- healthRegistry.register({
8567
- name: "session-store",
8568
- check: async () => {
8569
- try {
8570
- await fsp2.access(wpaths.projectSessions);
8571
- return { status: "healthy" };
8572
- } catch (e) {
8573
- return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
8574
- }
8575
- }
8576
- });
8577
- healthRegistry.register({
8578
- name: "provider",
8579
- check: async () => ({
8580
- status: "healthy",
8581
- data: { id: config.provider, model: config.model }
8582
- })
8583
- });
8584
- const dumpMetrics = () => {
8585
- if (!metricsSink) return;
8586
- try {
8587
- const out = path21.join(wpaths.projectSessions, "metrics.json");
8588
- const snap = metricsSink.snapshot();
8589
- writeFileSync(out, JSON.stringify(snap, null, 2));
8590
- } catch {
8591
- }
8592
- };
8593
- process.on("exit", dumpMetrics);
8594
- process.on("SIGINT", () => {
8595
- dumpMetrics();
8596
- process.exit(130);
8597
- });
8598
- if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
8599
- try {
8600
- metricsServerHandle = await startMetricsServer({
8601
- port: metricsPort,
8602
- host: process.env.METRICS_HOST ?? "127.0.0.1",
8603
- sink: metricsSink,
8604
- // V2-C: mount /healthz on the same listener so k8s probes can
8605
- // hit one endpoint per pod for both observability and liveness.
8606
- healthRegistry
8607
- });
8608
- logger.info(
8609
- `metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`
8610
- );
8611
- process.on("exit", () => {
8612
- void metricsServerHandle?.close().catch(() => {
8613
- });
8614
- });
8615
- } catch (err) {
8616
- logger.warn(
8617
- `metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`
8618
- );
8619
- }
8620
- }
8621
- }
9502
+ const { metricsSink, healthRegistry, metricsServerHandle } = (() => {
9503
+ const ms = setupMetrics({ flags, wpaths, events, logger, config: { provider: config.provider, model: config.model } });
9504
+ return ms;
9505
+ })();
8622
9506
  const spinner = new Spinner();
8623
9507
  let lastInputTokens = 0;
8624
9508
  events.on("provider.response", (e) => {
@@ -8737,49 +9621,21 @@ async function main(argv) {
8737
9621
  }
8738
9622
  }
8739
9623
  const slashRegistry = new SlashCommandRegistry();
8740
- if (config.features.plugins && config.plugins && config.plugins.length > 0) {
8741
- const resolvedPlugins = [];
8742
- for (const p of config.plugins) {
8743
- if (typeof p === "object" && p.enabled === false) continue;
8744
- const spec = typeof p === "string" ? p : p.name;
8745
- try {
8746
- const mod = await import(spec);
8747
- if (mod.default) resolvedPlugins.push(mod.default);
8748
- } catch (err) {
8749
- logger.warn(`Plugin "${spec}" failed to load`, err);
8750
- }
8751
- }
8752
- if (resolvedPlugins.length > 0) {
8753
- const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
8754
- const pluginOptions = buildPluginOptions(config);
8755
- const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
8756
- await loadPlugins(resolvedPlugins, {
8757
- log: logger,
8758
- // Each plugin's `configSchema` is validated against merged
8759
- // options from `plugins[].options` and `extensions[name]`.
8760
- // The merged view is also exposed as `api.config.extensions`.
8761
- pluginOptions,
8762
- apiFactory: (plugin) => createApi2(plugin.name, {
8763
- container,
8764
- events,
8765
- pipelines,
8766
- toolRegistry,
8767
- providerRegistry,
8768
- slashCommandRegistry: slashRegistry,
8769
- mcpRegistry,
8770
- config: pluginConfig,
8771
- log: logger,
8772
- extensions: agent.extensions,
8773
- sessionWriter: {
8774
- transcriptPath: context.session.transcriptPath,
8775
- append: (e) => context.session.append(e)
8776
- },
8777
- metricsSink,
8778
- configStore
8779
- })
8780
- });
8781
- }
8782
- }
9624
+ await setupPlugins({
9625
+ config,
9626
+ container,
9627
+ events,
9628
+ pipelines,
9629
+ toolRegistry,
9630
+ providerRegistry,
9631
+ slashCommandRegistry: slashRegistry,
9632
+ mcpRegistry,
9633
+ log: logger,
9634
+ agent,
9635
+ sessionWriter: context.session,
9636
+ metricsSink,
9637
+ configStore
9638
+ });
8783
9639
  const switchProviderAndModel = (providerId, modelId) => {
8784
9640
  try {
8785
9641
  const savedCfg = config.providers?.[providerId];
@@ -8802,14 +9658,27 @@ async function main(argv) {
8802
9658
  }
8803
9659
  };
8804
9660
  const directorMode = flags["director"] === true || typeof flags["resume"] === "string";
9661
+ const maxConcurrentFromFlag = typeof flags["max-concurrent"] === "string" ? Number.parseInt(flags["max-concurrent"], 10) : void 0;
9662
+ const maxConcurrentFromEnv = typeof process.env["WRONGSTACK_MAX_CONCURRENT"] === "string" ? Number.parseInt(process.env["WRONGSTACK_MAX_CONCURRENT"], 10) : void 0;
9663
+ const maxConcurrent = Number.isFinite(maxConcurrentFromFlag) && maxConcurrentFromFlag > 0 ? maxConcurrentFromFlag : Number.isFinite(maxConcurrentFromEnv) && maxConcurrentFromEnv > 0 ? maxConcurrentFromEnv : void 0;
8805
9664
  let director = null;
8806
9665
  let autonomyMode = "off";
8807
- const fleetRoot = directorMode ? path21.join(wpaths.projectSessions, session.id) : void 0;
8808
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path21.join(fleetRoot, "fleet.json") : void 0;
8809
- const sharedScratchpadPath = directorMode ? path21.join(fleetRoot, "shared") : void 0;
8810
- const subagentSessionsRoot = directorMode ? path21.join(fleetRoot, "subagents") : void 0;
8811
- const stateCheckpointPath = directorMode ? path21.join(fleetRoot, "director-state.json") : void 0;
8812
- const fleetRootForPromotion = path21.join(wpaths.projectSessions, session.id);
9666
+ let eternalEngine = null;
9667
+ const eternalListeners = /* @__PURE__ */ new Set();
9668
+ const broadcastEternalIteration = (entry) => {
9669
+ for (const fn of eternalListeners) {
9670
+ try {
9671
+ fn(entry);
9672
+ } catch {
9673
+ }
9674
+ }
9675
+ };
9676
+ const fleetRoot = directorMode ? path23.join(wpaths.projectSessions, session.id) : void 0;
9677
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path23.join(fleetRoot, "fleet.json") : void 0;
9678
+ const sharedScratchpadPath = directorMode ? path23.join(fleetRoot, "shared") : void 0;
9679
+ const subagentSessionsRoot = directorMode ? path23.join(fleetRoot, "subagents") : void 0;
9680
+ const stateCheckpointPath = directorMode ? path23.join(fleetRoot, "director-state.json") : void 0;
9681
+ const fleetRootForPromotion = path23.join(wpaths.projectSessions, session.id);
8813
9682
  const multiAgentHost = new MultiAgentHost(
8814
9683
  {
8815
9684
  container,
@@ -8821,7 +9690,8 @@ async function main(argv) {
8821
9690
  session,
8822
9691
  tokenCounter,
8823
9692
  projectRoot,
8824
- cwd
9693
+ cwd,
9694
+ secretScrubber: container.resolve(TOKENS.SecretScrubber)
8825
9695
  },
8826
9696
  {
8827
9697
  directorMode,
@@ -8831,7 +9701,8 @@ async function main(argv) {
8831
9701
  directorRunId: session.id,
8832
9702
  fleetRoot: fleetRootForPromotion,
8833
9703
  stateCheckpointPath,
8834
- sessionWriter: session
9704
+ sessionWriter: session,
9705
+ maxConcurrent
8835
9706
  }
8836
9707
  );
8837
9708
  toolRegistry.register(
@@ -8846,6 +9717,13 @@ async function main(argv) {
8846
9717
  directorRunId: session.id
8847
9718
  })
8848
9719
  );
9720
+ toolRegistry.register(
9721
+ createMcpControlTool({
9722
+ getConfig: () => configStore.get(),
9723
+ configPath: wpaths.globalConfig,
9724
+ registry: mcpRegistry
9725
+ })
9726
+ );
8849
9727
  if (directorMode) {
8850
9728
  director = await multiAgentHost.ensureDirector();
8851
9729
  if (director) {
@@ -8868,6 +9746,20 @@ async function main(argv) {
8868
9746
  this.enabled = enabled;
8869
9747
  }
8870
9748
  };
9749
+ const statuslineConfigDeps = {
9750
+ get: () => loadStatuslineConfig(),
9751
+ set: (cfg) => saveStatuslineConfig(cfg)
9752
+ };
9753
+ const hiddenItemsFromConfig = await loadStatuslineConfig();
9754
+ const hiddenItemsList = [];
9755
+ const ALL_ITEMS = ["todos", "plan", "fleet", "git", "elapsed", "context", "cost"];
9756
+ for (const k of ALL_ITEMS) {
9757
+ if (!hiddenItemsFromConfig[k]) hiddenItemsList.push(k);
9758
+ }
9759
+ const statuslineHiddenItems = hiddenItemsList;
9760
+ [...statuslineHiddenItems];
9761
+ const setStatuslineHiddenItems = (items) => {
9762
+ };
8871
9763
  const slashCmds = buildBuiltinSlashCommands({
8872
9764
  registry: slashRegistry,
8873
9765
  toolRegistry,
@@ -8887,6 +9779,7 @@ async function main(argv) {
8887
9779
  fleetStreamController,
8888
9780
  llmProvider: provider,
8889
9781
  llmModel: config.model,
9782
+ statuslineConfig: statuslineConfigDeps,
8890
9783
  onSpawn: async (description, spawnOpts) => {
8891
9784
  const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
8892
9785
  const tags = [];
@@ -8990,10 +9883,26 @@ async function main(argv) {
8990
9883
  }
8991
9884
  return `Manifest written \u2192 ${p}`;
8992
9885
  }
9886
+ if (action === "concurrency") {
9887
+ const current = multiAgentHost.getMaxConcurrent();
9888
+ if (!target) {
9889
+ return `Concurrent-subagent ceiling: ${current}`;
9890
+ }
9891
+ const n = Number.parseInt(target, 10);
9892
+ if (!Number.isFinite(n) || n < 1) {
9893
+ return `Invalid value "${target}". Concurrency must be an integer >= 1.`;
9894
+ }
9895
+ try {
9896
+ multiAgentHost.setMaxConcurrent(n);
9897
+ } catch (err) {
9898
+ return err instanceof Error ? err.message : String(err);
9899
+ }
9900
+ return `Concurrent-subagent ceiling: ${current} \u2192 ${n}`;
9901
+ }
8993
9902
  return `Unknown fleet action: ${action}`;
8994
9903
  },
8995
9904
  onFleetLog: async (subagentId, mode) => {
8996
- const subagentsRoot = path21.join(fleetRootForPromotion, "subagents");
9905
+ const subagentsRoot = path23.join(fleetRootForPromotion, "subagents");
8997
9906
  let runDirs;
8998
9907
  try {
8999
9908
  runDirs = await fsp2.readdir(subagentsRoot);
@@ -9002,7 +9911,7 @@ async function main(argv) {
9002
9911
  }
9003
9912
  const found = [];
9004
9913
  for (const runId of runDirs) {
9005
- const runDir = path21.join(subagentsRoot, runId);
9914
+ const runDir = path23.join(subagentsRoot, runId);
9006
9915
  let files;
9007
9916
  try {
9008
9917
  files = await fsp2.readdir(runDir);
@@ -9011,7 +9920,7 @@ async function main(argv) {
9011
9920
  }
9012
9921
  for (const f of files) {
9013
9922
  if (!f.endsWith(".jsonl")) continue;
9014
- const full = path21.join(runDir, f);
9923
+ const full = path23.join(runDir, f);
9015
9924
  try {
9016
9925
  const stat3 = await fsp2.stat(full);
9017
9926
  found.push({
@@ -9108,7 +10017,7 @@ async function main(argv) {
9108
10017
  }
9109
10018
  const dir = await multiAgentHost.ensureDirector();
9110
10019
  if (!dir) return "Director is not available.";
9111
- const dirStatePath = path21.join(fleetRootForPromotion, "director-state.json");
10020
+ const dirStatePath = path23.join(fleetRootForPromotion, "director-state.json");
9112
10021
  const prior = await loadDirectorState(dirStatePath);
9113
10022
  if (!prior) {
9114
10023
  return "No prior director-state.json found \u2014 nothing to retry.";
@@ -9179,9 +10088,9 @@ async function main(argv) {
9179
10088
  for (const tool of director2.tools(FLEET_ROSTER)) {
9180
10089
  toolRegistry.register(tool);
9181
10090
  }
9182
- const mp = path21.join(fleetRootForPromotion, "fleet.json");
9183
- const sp = path21.join(fleetRootForPromotion, "shared");
9184
- const ss = path21.join(fleetRootForPromotion, "subagents");
10091
+ const mp = path23.join(fleetRootForPromotion, "fleet.json");
10092
+ const sp = path23.join(fleetRootForPromotion, "shared");
10093
+ const ss = path23.join(fleetRootForPromotion, "subagents");
9185
10094
  const lines = [
9186
10095
  `${color.green("\u2713")} Promoted to director mode.`,
9187
10096
  ` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
@@ -9208,6 +10117,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
9208
10117
  }
9209
10118
  return result.message;
9210
10119
  },
10120
+ onMcp: async (args) => {
10121
+ const parsed = parseMcpArgs(args);
10122
+ if (!parsed) {
10123
+ return [
10124
+ "Usage: /mcp [list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
10125
+ "Run `/mcp` without args to see available servers."
10126
+ ].join("\n");
10127
+ }
10128
+ return runMcpManagementCommand(parsed, {
10129
+ config,
10130
+ configPath: wpaths.globalConfig,
10131
+ mcpRegistry,
10132
+ allServerPresets: allServers$1()
10133
+ });
10134
+ },
9211
10135
  onYolo: (setTo) => {
9212
10136
  const policy = container.resolve(TOKENS.PermissionPolicy);
9213
10137
  if (setTo !== void 0) {
@@ -9220,10 +10144,31 @@ Restart WrongStack to load or unload plugin code in this session.`;
9220
10144
  onAutonomy: (setTo) => {
9221
10145
  if (setTo !== void 0) {
9222
10146
  autonomyMode = setTo;
10147
+ autonomyModeRef.current = setTo;
9223
10148
  return setTo;
9224
10149
  }
9225
10150
  return autonomyMode;
9226
10151
  },
10152
+ onEternalStart: () => {
10153
+ if (!eternalEngine) {
10154
+ eternalEngine = new EternalAutonomyEngine({
10155
+ agent,
10156
+ projectRoot,
10157
+ // Wire the same compactor the manual /compact command uses so
10158
+ // multi-day eternal loops don't overflow the provider's context.
10159
+ // effectiveMaxContext is set up earlier with a model-specific
10160
+ // value; pass it through so aggressive-mode compact triggers
10161
+ // before the next iteration would actually overflow.
10162
+ compactor: container.resolve(TOKENS.Compactor),
10163
+ maxContextTokens: effectiveMaxContext > 0 ? effectiveMaxContext : void 0,
10164
+ onIteration: broadcastEternalIteration
10165
+ });
10166
+ }
10167
+ void eternalEngine.prime();
10168
+ },
10169
+ onEternalStop: () => {
10170
+ eternalEngine?.stop();
10171
+ },
9227
10172
  onExit: () => {
9228
10173
  void mcpRegistry.stopAll();
9229
10174
  },
@@ -9281,6 +10226,30 @@ Restart WrongStack to load or unload plugin code in this session.`;
9281
10226
  }
9282
10227
  });
9283
10228
  for (const cmd of slashCmds) slashRegistry.register(cmd);
10229
+ const eternalFlag = typeof flags["eternal"] === "string" ? flags["eternal"].trim() : "";
10230
+ if (eternalFlag.length > 0) {
10231
+ const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath4, loadGoal: loadGoal4 } = await import('@wrongstack/core');
10232
+ const goalPath = goalFilePath4(projectRoot);
10233
+ const prior = await loadGoal4(goalPath);
10234
+ const next = prior ? { ...prior, goal: eternalFlag, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal2(eternalFlag);
10235
+ await saveGoal2(goalPath, next);
10236
+ const policy = container.resolve(TOKENS.PermissionPolicy);
10237
+ policy.setYolo(true);
10238
+ config = patchConfig(config, { yolo: true });
10239
+ eternalEngine = new EternalAutonomyEngine({
10240
+ agent,
10241
+ projectRoot,
10242
+ compactor: container.resolve(TOKENS.Compactor),
10243
+ maxContextTokens: effectiveMaxContext > 0 ? effectiveMaxContext : void 0,
10244
+ onIteration: broadcastEternalIteration
10245
+ });
10246
+ await eternalEngine.prime();
10247
+ autonomyMode = "eternal";
10248
+ autonomyModeRef.current = "eternal";
10249
+ renderer.write(
10250
+ color.red("Eternal mode launching from --eternal flag.") + color.dim(` Goal: ${eternalFlag.slice(0, 80)}${eternalFlag.length > 80 ? "\u2026" : ""}`) + "\n"
10251
+ );
10252
+ }
9284
10253
  const savedProviderCfg = config.providers?.[config.provider];
9285
10254
  return execute({
9286
10255
  agent,
@@ -9311,11 +10280,18 @@ Restart WrongStack to load or unload plugin code in this session.`;
9311
10280
  director: director ?? null,
9312
10281
  fleetRoster: FLEET_ROSTER,
9313
10282
  fleetStreamController,
10283
+ statuslineHiddenItems,
10284
+ setStatuslineHiddenItems,
9314
10285
  getYolo: () => {
9315
10286
  const policy = container.resolve(TOKENS.PermissionPolicy);
9316
10287
  return policy.getYolo();
9317
10288
  },
9318
10289
  getAutonomy: () => autonomyMode,
10290
+ getEternalEngine: () => eternalEngine,
10291
+ subscribeEternalIteration: (fn) => {
10292
+ eternalListeners.add(fn);
10293
+ return () => eternalListeners.delete(fn);
10294
+ },
9319
10295
  skillLoader: config.features.skills ? skillLoader : void 0
9320
10296
  });
9321
10297
  }