@wrongstack/cli 0.6.0 → 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
@@ -3,7 +3,7 @@ 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, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, 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, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1 } from '@wrongstack/core';
6
+ import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, 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';
@@ -17,8 +17,8 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
17
17
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
18
18
  import * as readline from 'readline';
19
19
  import { spawn } from 'child_process';
20
- import { buildGoalPreamble } from '@wrongstack/tui';
21
20
  import { SkillInstaller } from '@wrongstack/core/skills';
21
+ import { allServers } from '@wrongstack/core/infrastructure';
22
22
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
23
23
  import { ToolExecutor } from '@wrongstack/core/execution';
24
24
  import { writeFileSync } from 'fs';
@@ -1104,7 +1104,7 @@ async function runWebUI(opts) {
1104
1104
  let abortController = null;
1105
1105
  const authToken = crypto.randomBytes(16).toString("hex");
1106
1106
  const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
1107
- console.log(`[WebUI] WebSocket server starting on ws://localhost:${port}`);
1107
+ console.log(`[WebUI] WebSocket server starting on ws://127.0.0.1:${port}`);
1108
1108
  const eventUnsubscribers = [];
1109
1109
  function setupEvents() {
1110
1110
  for (const unsub of eventUnsubscribers) unsub();
@@ -1220,7 +1220,7 @@ async function runWebUI(opts) {
1220
1220
  }
1221
1221
  return new Promise((resolve4) => {
1222
1222
  wss.on("listening", () => {
1223
- console.log(`[WebUI] WebSocket server running on ws://localhost:${port}`);
1223
+ console.log(`[WebUI] WebSocket server running on ws://127.0.0.1:${port}`);
1224
1224
  setupEvents();
1225
1225
  });
1226
1226
  wss.on("connection", (ws, req2) => {
@@ -2322,10 +2322,10 @@ var theme = { primary: color.amber };
2322
2322
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
2323
2323
  try {
2324
2324
  const { atomicWrite: atomicWrite8 } = await import('@wrongstack/core');
2325
- const fs19 = await import('fs/promises');
2325
+ const fs20 = await import('fs/promises');
2326
2326
  let existing = {};
2327
2327
  try {
2328
- const raw = await fs19.readFile(configPath2, "utf8");
2328
+ const raw = await fs20.readFile(configPath2, "utf8");
2329
2329
  existing = JSON.parse(raw);
2330
2330
  } catch {
2331
2331
  }
@@ -2679,52 +2679,79 @@ async function detectProjectFacts(root) {
2679
2679
  }
2680
2680
  function renderAgentsTemplate(f) {
2681
2681
  const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
2682
+ const hints = f.hints.length > 0 ? `
2683
+
2684
+ > Auto-detected: ${f.hints.join(", ")}` : "";
2682
2685
  return `# AGENTS.md
2683
2686
 
2684
- This file is loaded into WrongStack's system prompt as project context.
2685
- Keep it concise, factual, and durable: write the information future agents
2686
- 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.
2687
2691
 
2688
2692
  ## Project brief
2689
2693
 
2690
- - **Purpose:** _What does this project do, and why does it exist?_
2694
+ - **Purpose:** _What does this project do and why does it exist?_
2691
2695
  - **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
2692
- - **Runtime/deployment:** _Where does it run: CLI, server, browser, worker, library, package?_
2693
- - **Main entry points:** _Which files or commands should an agent inspect first?_
2696
+ - **Runtime / deployment:** _CLI, server, browser, worker, library, package?_${hints}
2694
2697
 
2695
2698
  ## How to work safely
2696
2699
 
2697
2700
  - _Project-specific rules the agent should always follow._
2698
2701
  - _Files, generated artifacts, migrations, or config the agent should not edit without asking._
2699
- - _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._
2700
2704
 
2701
2705
  ## Commands
2702
2706
 
2703
- - **Build:** ${cmd(f.build)}
2704
- - **Test:** ${cmd(f.test)}
2705
- - **Lint:** ${cmd(f.lint)}
2706
- - **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.)_ |
2707
2722
 
2708
2723
  ## Architecture notes
2709
2724
 
2710
2725
  _Summarize the important modules, data flow, boundaries, and ownership rules.
2711
- 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._
2712
2736
 
2713
2737
  ## Domain knowledge
2714
2738
 
2715
2739
  _Business rules, acronyms, invariants, external services, and notes where the
2716
- 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"._
2717
2743
 
2718
2744
  ## Verification checklist
2719
2745
 
2720
2746
  - _What should be run after code changes?_
2721
2747
  - _What manual smoke test proves the common path still works?_
2722
2748
  - _What failure modes deserve extra attention?_
2749
+ - _Any known flaky tests or environment-dependent behavior?_
2723
2750
 
2724
2751
  ## Useful pointers
2725
2752
 
2726
- - _Docs, dashboards, runbooks, issue trackers, design notes, or owner contacts._
2727
- `;
2753
+ - _Docs, dashboards, runbooks, issue trackers, design notes, owner contacts._
2754
+ - _Related projects or repositories._`;
2728
2755
  }
2729
2756
  function countTurnPairs(messages) {
2730
2757
  let count = 0;
@@ -3128,7 +3155,7 @@ function buildStatsCommand(opts) {
3128
3155
  function buildFleetCommand(opts) {
3129
3156
  return {
3130
3157
  name: "fleet",
3131
- 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]",
3132
3159
  help: [
3133
3160
  "Usage:",
3134
3161
  " /fleet Show fleet status (alias for /fleet status).",
@@ -3136,6 +3163,8 @@ function buildFleetCommand(opts) {
3136
3163
  " /fleet usage Per-subagent runtime cost.",
3137
3164
  " /fleet kill <id> Terminate a running subagent.",
3138
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.",
3139
3168
  " /fleet retry List interrupted tasks from the last run.",
3140
3169
  " /fleet retry <taskId> Re-spawn the matching subagent and re-assign the task.",
3141
3170
  " /fleet retry all Re-assign every interrupted task at once.",
@@ -3161,6 +3190,9 @@ function buildFleetCommand(opts) {
3161
3190
  if (!target) return { message: "Usage: /fleet kill <subagent-id>" };
3162
3191
  return { message: await opts.onFleet("kill", target) };
3163
3192
  }
3193
+ case "concurrency": {
3194
+ return { message: await opts.onFleet("concurrency", target) };
3195
+ }
3164
3196
  case "retry": {
3165
3197
  if (!opts.onFleetRetry) {
3166
3198
  return { message: "Retry is only available when director mode is active." };
@@ -3298,20 +3330,10 @@ function buildHelpCommand(opts) {
3298
3330
  function buildInitCommand(opts) {
3299
3331
  return {
3300
3332
  name: "init",
3301
- description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
3302
- async run(args, ctx) {
3303
- const force = args.trim() === "--force";
3333
+ description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
3334
+ async run(_args, ctx) {
3304
3335
  const dir = path23.join(ctx.projectRoot, ".wrongstack");
3305
3336
  const file = path23.join(dir, "AGENTS.md");
3306
- try {
3307
- await fsp2.access(file);
3308
- if (!force) {
3309
- const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
3310
- opts.renderer.writeWarning(msg2);
3311
- return { message: msg2 };
3312
- }
3313
- } catch {
3314
- }
3315
3337
  const detected = await detectProjectFacts(ctx.projectRoot);
3316
3338
  const body = renderAgentsTemplate(detected);
3317
3339
  await fsp2.mkdir(dir, { recursive: true });
@@ -3332,6 +3354,223 @@ No project type auto-detected. Edit the file with project context and instructio
3332
3354
  }
3333
3355
  };
3334
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
+ }
3335
3574
 
3336
3575
  // src/slash-commands/memory.ts
3337
3576
  function buildMemoryCommand(opts) {
@@ -4676,6 +4915,7 @@ function buildBuiltinSlashCommands(opts) {
4676
4915
  buildSkillUpdateCommand(opts),
4677
4916
  buildSkillUninstallCommand(opts),
4678
4917
  buildPluginCommand(opts),
4918
+ buildMcpSlashCommand(opts),
4679
4919
  buildDiagCommand(opts),
4680
4920
  buildStatsCommand(opts),
4681
4921
  buildSpawnCommand(opts),
@@ -6138,12 +6378,8 @@ var initCmd = async (_args, deps) => {
6138
6378
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
6139
6379
  await fsp2.mkdir(path23.join(deps.projectRoot, ".wrongstack"), { recursive: true });
6140
6380
  const agentsFile = path23.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
6141
- try {
6142
- await fsp2.access(agentsFile);
6143
- } catch {
6144
- const detected2 = await detectProjectFacts(deps.projectRoot);
6145
- await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
6146
- }
6381
+ const projectFacts = await detectProjectFacts(deps.projectRoot);
6382
+ await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
6147
6383
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
6148
6384
  deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
6149
6385
  deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
@@ -6179,7 +6415,7 @@ var mcpCmd = async (args, deps) => {
6179
6415
  return removeMcpServer(name, deps);
6180
6416
  }
6181
6417
  if (sub === "restart") {
6182
- 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.");
6183
6419
  return 0;
6184
6420
  }
6185
6421
  deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
@@ -6354,7 +6590,7 @@ function renderConfiguredPlugins(config) {
6354
6590
  return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
6355
6591
  }).join("\n");
6356
6592
  }
6357
- async function readConfig(file) {
6593
+ async function readConfig2(file) {
6358
6594
  try {
6359
6595
  return JSON.parse(await fsp2.readFile(file, "utf8"));
6360
6596
  } catch {
@@ -6373,7 +6609,7 @@ function officialPluginState(config, spec) {
6373
6609
  return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
6374
6610
  }
6375
6611
  async function upsertPlugin(spec, opts, deps, verb) {
6376
- const existing = await readConfig(deps.configPath);
6612
+ const existing = await readConfig2(deps.configPath);
6377
6613
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
6378
6614
  const idx = plugins.findIndex((p) => pluginName(p) === spec);
6379
6615
  const nextEntry = pluginEntry(spec, opts.enabled);
@@ -6396,7 +6632,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
6396
6632
  };
6397
6633
  }
6398
6634
  async function removePlugin(spec, deps) {
6399
- const existing = await readConfig(deps.configPath);
6635
+ const existing = await readConfig2(deps.configPath);
6400
6636
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
6401
6637
  const next = plugins.filter((p) => pluginName(p) !== spec);
6402
6638
  if (next.length === plugins.length) {
@@ -6932,12 +7168,23 @@ function parseRewindFlags(args) {
6932
7168
  }
6933
7169
  return flags;
6934
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
+ }
6935
7182
  var rewindCmd = async (args, deps) => {
6936
7183
  const flags = parseRewindFlags(args);
6937
7184
  const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
6938
7185
  const sessionsDir = path23.join(wpaths.globalRoot, "sessions");
6939
7186
  const rewind = new DefaultSessionRewinder(sessionsDir);
6940
- let sessionId = args.find((a) => !a.startsWith("--"));
7187
+ let sessionId = findSessionId(args);
6941
7188
  if (!sessionId) {
6942
7189
  if (!deps.sessionStore) {
6943
7190
  deps.renderer.writeError("No session store available.");
@@ -7146,22 +7393,22 @@ function fmtDuration(ms) {
7146
7393
  const remMin = m - h * 60;
7147
7394
  return `${h}h${remMin}m`;
7148
7395
  }
7149
- function fmtTaskResultLine(r, color34) {
7396
+ function fmtTaskResultLine(r, color35) {
7150
7397
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
7151
7398
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
7152
7399
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
7153
7400
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
7154
- const errKindChip = errKind ? color34.dim(` [${errKind}]`) : "";
7155
- const errSnip = errMsg || errKind ? `${errKindChip}${color34.dim(errTail)}` : "";
7401
+ const errKindChip = errKind ? color35.dim(` [${errKind}]`) : "";
7402
+ const errSnip = errMsg || errKind ? `${errKindChip}${color35.dim(errTail)}` : "";
7156
7403
  switch (r.status) {
7157
7404
  case "success":
7158
- return { mark: color34.green("\u2713"), stats, tail: "" };
7405
+ return { mark: color35.green("\u2713"), stats, tail: "" };
7159
7406
  case "timeout":
7160
- return { mark: color34.yellow("\u23F1"), stats: `${color34.yellow("timeout")} ${stats}`, tail: errSnip };
7407
+ return { mark: color35.yellow("\u23F1"), stats: `${color35.yellow("timeout")} ${stats}`, tail: errSnip };
7161
7408
  case "stopped":
7162
- return { mark: color34.dim("\u2298"), stats: `${color34.dim("stopped")} ${stats}`, tail: errSnip };
7409
+ return { mark: color35.dim("\u2298"), stats: `${color35.dim("stopped")} ${stats}`, tail: errSnip };
7163
7410
  case "failed":
7164
- return { mark: color34.red("\u2717"), stats: `${color34.red("failed")} ${stats}`, tail: errSnip };
7411
+ return { mark: color35.red("\u2717"), stats: `${color35.red("failed")} ${stats}`, tail: errSnip };
7165
7412
  }
7166
7413
  }
7167
7414
 
@@ -8107,7 +8354,7 @@ var MultiAgentHost = class {
8107
8354
  const coordinatorConfig = {
8108
8355
  coordinatorId: randomUUID(),
8109
8356
  doneCondition: { type: "all_tasks_done" },
8110
- maxConcurrent: 8
8357
+ maxConcurrent: this.opts.maxConcurrent ?? 4
8111
8358
  };
8112
8359
  const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path23.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
8113
8360
  this.director = new Director({
@@ -8484,6 +8731,37 @@ var MultiAgentHost = class {
8484
8731
  await this.getCoordinator().stopAll();
8485
8732
  }
8486
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
+ }
8487
8765
  };
8488
8766
  function makePromptDelegate(reader) {
8489
8767
  return async (tool, input, suggestedPattern) => {
@@ -9186,6 +9464,8 @@ async function main(argv) {
9186
9464
  const memoryStore = container.resolve(TOKENS.MemoryStore);
9187
9465
  const skillLoader = container.resolve(TOKENS.SkillLoader);
9188
9466
  const sessionRef = {};
9467
+ const autonomyModeRef = { current: "off" };
9468
+ const goalPathForPrompt = path23.join(projectRoot, ".wrongstack", "goal.json");
9189
9469
  container.bind(
9190
9470
  TOKENS.SystemPromptBuilder,
9191
9471
  () => new DefaultSystemPromptBuilder({
@@ -9195,7 +9475,17 @@ async function main(argv) {
9195
9475
  modeId,
9196
9476
  modePrompt,
9197
9477
  modelCapabilities,
9198
- planPath: () => sessionRef.current ? path23.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
+ ]
9199
9489
  })
9200
9490
  );
9201
9491
  const toolRegistry = new ToolRegistry();
@@ -9368,6 +9658,9 @@ async function main(argv) {
9368
9658
  }
9369
9659
  };
9370
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;
9371
9664
  let director = null;
9372
9665
  let autonomyMode = "off";
9373
9666
  let eternalEngine = null;
@@ -9408,7 +9701,8 @@ async function main(argv) {
9408
9701
  directorRunId: session.id,
9409
9702
  fleetRoot: fleetRootForPromotion,
9410
9703
  stateCheckpointPath,
9411
- sessionWriter: session
9704
+ sessionWriter: session,
9705
+ maxConcurrent
9412
9706
  }
9413
9707
  );
9414
9708
  toolRegistry.register(
@@ -9423,6 +9717,13 @@ async function main(argv) {
9423
9717
  directorRunId: session.id
9424
9718
  })
9425
9719
  );
9720
+ toolRegistry.register(
9721
+ createMcpControlTool({
9722
+ getConfig: () => configStore.get(),
9723
+ configPath: wpaths.globalConfig,
9724
+ registry: mcpRegistry
9725
+ })
9726
+ );
9426
9727
  if (directorMode) {
9427
9728
  director = await multiAgentHost.ensureDirector();
9428
9729
  if (director) {
@@ -9582,6 +9883,22 @@ async function main(argv) {
9582
9883
  }
9583
9884
  return `Manifest written \u2192 ${p}`;
9584
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
+ }
9585
9902
  return `Unknown fleet action: ${action}`;
9586
9903
  },
9587
9904
  onFleetLog: async (subagentId, mode) => {
@@ -9800,6 +10117,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
9800
10117
  }
9801
10118
  return result.message;
9802
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
+ },
9803
10135
  onYolo: (setTo) => {
9804
10136
  const policy = container.resolve(TOKENS.PermissionPolicy);
9805
10137
  if (setTo !== void 0) {
@@ -9812,6 +10144,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
9812
10144
  onAutonomy: (setTo) => {
9813
10145
  if (setTo !== void 0) {
9814
10146
  autonomyMode = setTo;
10147
+ autonomyModeRef.current = setTo;
9815
10148
  return setTo;
9816
10149
  }
9817
10150
  return autonomyMode;
@@ -9912,6 +10245,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
9912
10245
  });
9913
10246
  await eternalEngine.prime();
9914
10247
  autonomyMode = "eternal";
10248
+ autonomyModeRef.current = "eternal";
9915
10249
  renderer.write(
9916
10250
  color.red("Eternal mode launching from --eternal flag.") + color.dim(` Goal: ${eternalFlag.slice(0, 80)}${eternalFlag.length > 80 ? "\u2026" : ""}`) + "\n"
9917
10251
  );