@wrongstack/cli 0.54.1 → 0.66.13

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
@@ -2,29 +2,30 @@
2
2
  import * as path8 from 'path';
3
3
  import { join } from 'path';
4
4
  import * as fsp3 from 'fs/promises';
5
- import { color, writeErr, DefaultTaskStore, TaskTracker, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, renderTaskGraph, SpecVersioning, DefaultSecretScrubber, atomicWrite, DefaultPathResolver, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, bootConfig as bootConfig$1, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, onResize, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError, ERROR_CODES } from '@wrongstack/core';
5
+ import { color, writeErr, DefaultTaskStore, TaskTracker, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, renderTaskGraph, SpecVersioning, DefaultSecretScrubber, atomicWrite, DefaultPathResolver, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, bootConfig as bootConfig$1, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, onResize, ERROR_CODES, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, InputBuilder, FsError } from '@wrongstack/core';
6
6
  import { createRequire } from 'module';
7
7
  import * as os2 from 'os';
8
8
  import os2__default from 'os';
9
9
  import * as crypto2 from 'crypto';
10
10
  import { randomUUID } from 'crypto';
11
+ import { findFreePort, createHttpServer, openBrowser, registerInstance, unregisterInstance } from '@wrongstack/webui/server';
11
12
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
12
13
  import { WebSocketServer, WebSocket } from 'ws';
13
- import { MCPRegistry } from '@wrongstack/mcp';
14
+ import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
14
15
  import { capabilitiesFor, buildProviderFactoriesFromRegistry, makeProviderFromConfig } from '@wrongstack/providers';
15
16
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
16
17
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
17
18
  import { fileURLToPath } from 'url';
18
19
  import * as readline from 'readline';
19
- import * as fs11 from 'fs';
20
+ import * as fs12 from 'fs';
20
21
  import { writeFileSync, existsSync, readFileSync } from 'fs';
21
22
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
22
23
  import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
23
24
  import { ACP_AGENTS, SubagentBudget } from '@wrongstack/core/coordination';
24
25
  import { spawn } from 'child_process';
25
26
  import { allServers } from '@wrongstack/core/infrastructure';
26
- import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
27
27
  import { ToolExecutor } from '@wrongstack/core/execution';
28
+ import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
28
29
 
29
30
  var __defProp = Object.defineProperty;
30
31
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1694,13 +1695,63 @@ __export(webui_server_exports, {
1694
1695
  runWebUI: () => runWebUI
1695
1696
  });
1696
1697
  async function runWebUI(opts) {
1697
- const port = opts.port ?? 3457;
1698
+ const host = "127.0.0.1";
1699
+ const requestedWsPort = opts.port ?? 3457;
1700
+ const requestedHttpPort = opts.httpPort ?? 3456;
1701
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
1702
+ let httpPort = requestedHttpPort;
1703
+ let wsPort = requestedWsPort;
1704
+ if (!strictPort) {
1705
+ httpPort = await findFreePort(host, requestedHttpPort);
1706
+ wsPort = await findFreePort(host, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
1707
+ }
1708
+ const port = wsPort;
1709
+ const rateLimitMax = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1698
1710
  const clients = /* @__PURE__ */ new Map();
1711
+ const pendingConfirms = /* @__PURE__ */ new Map();
1699
1712
  const secretScrubber = new DefaultSecretScrubber();
1700
1713
  let abortController = null;
1701
1714
  const authToken = crypto2.randomBytes(16).toString("hex");
1702
- const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
1703
- console.log(`[WebUI] WebSocket server starting on ws://127.0.0.1:${port}`);
1715
+ const wss = new WebSocketServer({ port, host, maxPayload: 1 * 1024 * 1024 });
1716
+ console.log(`[WebUI] WebSocket server starting on ws://${host}:${port}`);
1717
+ let httpServer = null;
1718
+ try {
1719
+ const requireFromHere = createRequire(import.meta.url);
1720
+ const serverEntry = requireFromHere.resolve("@wrongstack/webui/server");
1721
+ const distDir = path8.resolve(path8.dirname(serverEntry), "..");
1722
+ httpServer = createHttpServer({ host, distDir, wsPort });
1723
+ const openUrl = `http://${host}:${httpPort}`;
1724
+ httpServer.listen(httpPort, host, () => {
1725
+ console.log(
1726
+ `
1727
+ \u25B8 WebUI ready \u2014 open \x1B[1m${openUrl}\x1B[0m in your browser
1728
+ (same agent as this terminal \xB7 ws:${wsPort})
1729
+ `
1730
+ );
1731
+ if (opts.open) openBrowser(openUrl);
1732
+ });
1733
+ } catch (err) {
1734
+ console.warn(
1735
+ `[WebUI] Frontend not served (run \`pnpm --filter @wrongstack/webui build\`): ${err instanceof Error ? err.message : String(err)}. WS bridge still active on ws://${host}:${wsPort}.`
1736
+ );
1737
+ }
1738
+ const registryBaseDir = opts.globalConfigPath ? path8.dirname(opts.globalConfigPath) : void 0;
1739
+ if (opts.projectRoot) {
1740
+ void registerInstance(
1741
+ {
1742
+ pid: process.pid,
1743
+ httpPort,
1744
+ wsPort,
1745
+ host,
1746
+ projectRoot: opts.projectRoot,
1747
+ projectName: path8.basename(opts.projectRoot) || opts.projectRoot,
1748
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1749
+ url: `http://${host}:${httpPort}`
1750
+ },
1751
+ registryBaseDir
1752
+ ).catch(() => {
1753
+ });
1754
+ }
1704
1755
  const eventUnsubscribers = [];
1705
1756
  function setupEvents() {
1706
1757
  for (const unsub of eventUnsubscribers) unsub();
@@ -1794,6 +1845,88 @@ async function runWebUI(opts) {
1794
1845
  });
1795
1846
  })
1796
1847
  );
1848
+ eventUnsubscribers.push(
1849
+ opts.events.on("tool.confirm_needed", (e) => {
1850
+ const id = e.toolUseId ?? `confirm_${Date.now()}`;
1851
+ pendingConfirms.set(id, e.resolve);
1852
+ broadcast({
1853
+ type: "tool.confirm_needed",
1854
+ payload: {
1855
+ id,
1856
+ toolName: e.tool?.name ?? "unknown",
1857
+ input: secretScrubber.scrubObject(e.input),
1858
+ suggestedPattern: e.suggestedPattern
1859
+ }
1860
+ });
1861
+ })
1862
+ );
1863
+ const forwardSubagent = (kind, payload) => broadcast({ type: "subagent.event", payload: { kind, ...payload } });
1864
+ eventUnsubscribers.push(
1865
+ opts.events.on(
1866
+ "subagent.spawned",
1867
+ (e) => forwardSubagent("spawned", {
1868
+ subagentId: e.subagentId,
1869
+ taskId: e.taskId,
1870
+ name: e.name,
1871
+ provider: e.provider,
1872
+ model: e.model,
1873
+ description: e.description
1874
+ })
1875
+ ),
1876
+ opts.events.on(
1877
+ "subagent.task_started",
1878
+ (e) => forwardSubagent("task_started", {
1879
+ subagentId: e.subagentId,
1880
+ taskId: e.taskId,
1881
+ description: e.description
1882
+ })
1883
+ ),
1884
+ opts.events.on(
1885
+ "subagent.tool_executed",
1886
+ (e) => forwardSubagent("tool_executed", {
1887
+ subagentId: e.subagentId,
1888
+ toolName: e.name,
1889
+ durationMs: e.durationMs,
1890
+ ok: e.ok
1891
+ })
1892
+ ),
1893
+ opts.events.on(
1894
+ "subagent.iteration_summary",
1895
+ (e) => forwardSubagent("iteration_summary", {
1896
+ subagentId: e.subagentId,
1897
+ iteration: e.iteration,
1898
+ toolCalls: e.toolCalls,
1899
+ costUsd: e.costUsd,
1900
+ currentTool: e.currentTool
1901
+ })
1902
+ ),
1903
+ opts.events.on(
1904
+ "subagent.budget_extended",
1905
+ (e) => forwardSubagent("budget_extended", {
1906
+ subagentId: e.subagentId,
1907
+ totalExtensions: e.totalExtensions
1908
+ })
1909
+ ),
1910
+ opts.events.on(
1911
+ "subagent.ctx_pct",
1912
+ (e) => forwardSubagent("ctx_pct", {
1913
+ subagentId: e.subagentId,
1914
+ load: e.load,
1915
+ tokens: e.tokens,
1916
+ maxContext: e.maxContext
1917
+ })
1918
+ ),
1919
+ opts.events.on(
1920
+ "subagent.task_completed",
1921
+ (e) => forwardSubagent("task_completed", {
1922
+ subagentId: e.subagentId,
1923
+ status: e.status,
1924
+ iterations: e.iterations,
1925
+ toolCalls: e.toolCalls,
1926
+ error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
1927
+ })
1928
+ )
1929
+ );
1797
1930
  if (opts.subscribeEternalIteration) {
1798
1931
  eventUnsubscribers.push(
1799
1932
  opts.subscribeEternalIteration((entry) => {
@@ -1814,10 +1947,11 @@ async function runWebUI(opts) {
1814
1947
  );
1815
1948
  }
1816
1949
  }
1817
- return new Promise((resolve3) => {
1950
+ return new Promise((resolve4) => {
1818
1951
  wss.on("listening", () => {
1819
- console.log(`[WebUI] WebSocket server running on ws://127.0.0.1:${port}`);
1952
+ console.log(`[WebUI] WebSocket server running on ws://${host}:${port}`);
1820
1953
  setupEvents();
1954
+ opts.onListening?.({ httpPort, wsPort, host });
1821
1955
  });
1822
1956
  wss.on("connection", (ws, req2) => {
1823
1957
  const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
@@ -1870,17 +2004,19 @@ async function runWebUI(opts) {
1870
2004
  let msgCount = 0;
1871
2005
  let windowResetAt = Date.now() + 6e4;
1872
2006
  ws.on("message", async (data) => {
1873
- const now = Date.now();
1874
- if (now > windowResetAt) {
1875
- msgCount = 0;
1876
- windowResetAt = now + 6e4;
1877
- }
1878
- if (++msgCount > 60) {
1879
- send(ws, {
1880
- type: "error",
1881
- payload: { phase: "rate_limit", message: "Too many messages. Please wait." }
1882
- });
1883
- return;
2007
+ if (rateLimitMax > 0) {
2008
+ const now = Date.now();
2009
+ if (now > windowResetAt) {
2010
+ msgCount = 0;
2011
+ windowResetAt = now + 6e4;
2012
+ }
2013
+ if (++msgCount > rateLimitMax) {
2014
+ send(ws, {
2015
+ type: "error",
2016
+ payload: { phase: "rate_limit", message: "Too many messages. Please wait." }
2017
+ });
2018
+ return;
2019
+ }
1884
2020
  }
1885
2021
  try {
1886
2022
  const msg = JSON.parse(data.toString());
@@ -1892,6 +2028,12 @@ async function runWebUI(opts) {
1892
2028
  ws.on("close", () => {
1893
2029
  console.log("[WebUI] Client disconnected");
1894
2030
  clients.delete(ws);
2031
+ if (clients.size === 0 && pendingConfirms.size > 0) {
2032
+ for (const [id, resolve5] of pendingConfirms) {
2033
+ resolve5("no");
2034
+ pendingConfirms.delete(id);
2035
+ }
2036
+ }
1895
2037
  });
1896
2038
  send(ws, {
1897
2039
  type: "session.start",
@@ -1913,9 +2055,12 @@ async function runWebUI(opts) {
1913
2055
  ws.close();
1914
2056
  }
1915
2057
  clients.clear();
2058
+ void unregisterInstance(process.pid, registryBaseDir).catch(() => {
2059
+ });
2060
+ httpServer?.close();
1916
2061
  wss.close(() => {
1917
2062
  console.log("[WebUI] Server stopped");
1918
- resolve3();
2063
+ resolve4();
1919
2064
  });
1920
2065
  }
1921
2066
  process.on("SIGINT", shutdown);
@@ -1940,6 +2085,15 @@ async function runWebUI(opts) {
1940
2085
  case "ping":
1941
2086
  send(ws, { type: "pong", payload: {} });
1942
2087
  break;
2088
+ case "tool.confirm_result": {
2089
+ const { id, decision } = msg.payload;
2090
+ const resolve4 = pendingConfirms.get(id);
2091
+ if (resolve4) {
2092
+ pendingConfirms.delete(id);
2093
+ resolve4(decision);
2094
+ }
2095
+ break;
2096
+ }
1943
2097
  case "providers.list":
1944
2098
  await handleProvidersList(ws);
1945
2099
  break;
@@ -2345,29 +2499,43 @@ async function resolveRuntimeMaxContext(input) {
2345
2499
  const providerConfig = input.runtimeProviderConfig ?? input.config.providers?.[input.providerId];
2346
2500
  const providerOverride = positiveNumber(readConfiguredMaxContext(providerConfig));
2347
2501
  if (providerOverride) return providerOverride;
2348
- const topLevelBaseUrlApplies = input.providerId === input.config.provider;
2349
- const hasCustomBaseUrl = Boolean(
2350
- providerConfig?.baseUrl || topLevelBaseUrlApplies && input.config.baseUrl
2351
- );
2352
- if (input.modelsRegistry && !hasCustomBaseUrl) {
2353
- const mergedModels = mergeCustomModelDefs(
2354
- providerConfig?.customModels,
2355
- input.config.models
2356
- );
2357
- const caps = await capabilitiesFor(
2358
- input.modelsRegistry,
2359
- input.providerId,
2360
- input.modelId,
2361
- mergedModels
2362
- ).catch(() => void 0);
2363
- const catalogMax = positiveNumber(caps?.maxContext);
2364
- if (catalogMax) return catalogMax;
2365
- const directModel = await input.modelsRegistry.getModel(input.providerId, input.modelId).catch(() => void 0);
2366
- const directMax = positiveNumber(directModel?.capabilities.maxContext);
2367
- if (directMax) return directMax;
2502
+ const catalogId = providerConfig?.type && providerConfig.type !== input.providerId ? providerConfig.type : input.providerId;
2503
+ if (input.modelsRegistry) {
2504
+ const topLevelBaseUrlApplies = input.providerId === input.config.provider;
2505
+ const configuredBaseUrl = providerConfig?.baseUrl ?? (topLevelBaseUrlApplies ? input.config.baseUrl : void 0);
2506
+ let divergesFromCatalog = false;
2507
+ if (configuredBaseUrl) {
2508
+ const resolved = await safeGetProvider(input.modelsRegistry, catalogId);
2509
+ divergesFromCatalog = normalizeBaseUrl(configuredBaseUrl) !== normalizeBaseUrl(resolved?.apiBase);
2510
+ }
2511
+ if (!divergesFromCatalog) {
2512
+ const mergedModels = mergeCustomModelDefs(providerConfig?.customModels, input.config.models);
2513
+ const caps = await capabilitiesFor(
2514
+ input.modelsRegistry,
2515
+ catalogId,
2516
+ input.modelId,
2517
+ mergedModels
2518
+ ).catch(() => void 0);
2519
+ const catalogMax = positiveNumber(caps?.maxContext);
2520
+ if (catalogMax) return catalogMax;
2521
+ const directModel = await input.modelsRegistry.getModel(catalogId, input.modelId).catch(() => void 0);
2522
+ const directMax = positiveNumber(directModel?.capabilities.maxContext);
2523
+ if (directMax) return directMax;
2524
+ }
2368
2525
  }
2369
2526
  return positiveNumber(input.provider.capabilities.maxContext) ?? 0;
2370
2527
  }
2528
+ async function safeGetProvider(registry, id) {
2529
+ try {
2530
+ return await registry.getProvider(id);
2531
+ } catch {
2532
+ return void 0;
2533
+ }
2534
+ }
2535
+ function normalizeBaseUrl(url) {
2536
+ if (!url) return "";
2537
+ return url.trim().toLowerCase().replace(/\/+$/, "");
2538
+ }
2371
2539
  function readConfiguredMaxContext(providerConfig) {
2372
2540
  if (!providerConfig || typeof providerConfig !== "object") return void 0;
2373
2541
  const capabilities = providerConfig.capabilities;
@@ -2381,6 +2549,8 @@ function positiveNumber(value) {
2381
2549
  // src/arg-parser.ts
2382
2550
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2383
2551
  "yolo",
2552
+ "yolo-destructive",
2553
+ "confirm-destructive",
2384
2554
  "force-all-yolo",
2385
2555
  "verbose",
2386
2556
  "trace",
@@ -2399,6 +2569,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2399
2569
  "prompt",
2400
2570
  "metrics",
2401
2571
  "webui",
2572
+ "open",
2402
2573
  "no-check",
2403
2574
  "no-models-refresh",
2404
2575
  "director",
@@ -2407,7 +2578,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2407
2578
  "autonomy",
2408
2579
  "eternal",
2409
2580
  "no-hints",
2410
- "hints"
2581
+ "hints",
2582
+ "no-hooks"
2411
2583
  ]);
2412
2584
  function parseArgs(argv) {
2413
2585
  const flags = {};
@@ -2735,10 +2907,12 @@ function buildAutonomyCommand(opts) {
2735
2907
  " auto \u2014 After each turn, agent picks the best next step and continues.",
2736
2908
  " Runs indefinitely until you press Esc or Ctrl+C.",
2737
2909
  " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
2738
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2910
+ " Force-enables regular YOLO; destructive-gated calls still use",
2911
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2739
2912
  " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
2740
2913
  " spawns N agents, awaits results, aggregates. Requires /goal.",
2741
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2914
+ " Force-enables regular YOLO; destructive-gated calls still use",
2915
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2742
2916
  "",
2743
2917
  "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
2744
2918
  "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
@@ -2896,7 +3070,7 @@ function buildAutonomyCommand(opts) {
2896
3070
  opts.onEternalStart(newMode);
2897
3071
  const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
2898
3072
  const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
2899
- ${color.dim("YOLO forced ON. Use /autonomy stop to end. Journal at /goal journal.")}`;
3073
+ ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
2900
3074
  opts.renderer.write(msg2);
2901
3075
  return { message: msg2 };
2902
3076
  }
@@ -3373,6 +3547,11 @@ function buildContextCommand(opts) {
3373
3547
  " /context Show counts: messages, est. tokens, tool calls, todos, read files.",
3374
3548
  " /context detail As above, plus model, cwd, projectRoot, and the file list.",
3375
3549
  " /context repair Repair orphan tool_use/tool_result blocks after manual compaction.",
3550
+ " /context limit Show effective context window for this session.",
3551
+ " /context limit <tokens> Set effective context window for this session (e.g. 220k).",
3552
+ " /context limit <tokens> --persist Persist effective context window to config.",
3553
+ " /context thresholds <warn> <soft> <hard> Set compaction thresholds (percent or decimal).",
3554
+ " /context thresholds <warn> <soft> <hard> --persist Persist thresholds to config.",
3376
3555
  " /context mode List context-window modes.",
3377
3556
  " /context mode <id> Switch context-window mode for this session."
3378
3557
  ].join("\n"),
@@ -3400,6 +3579,83 @@ ${formatContextWindowModeList(active)}`;
3400
3579
  ` empty msgs: removed ${repaired.report.removedMessages}`
3401
3580
  ].join("\n") : "Context repair: no orphan tool_use/tool_result blocks found.";
3402
3581
  opts.renderer.write(`${msg2}
3582
+ `);
3583
+ return { message: msg2 };
3584
+ }
3585
+ if (trimmed === "limit") {
3586
+ const limit = readEffectiveLimit(ctx, opts);
3587
+ const msg2 = limit > 0 ? `Effective context window: ${limit.toLocaleString()} tokens` : "Effective context window: unknown (auto-compaction may be disabled).";
3588
+ opts.renderer.write(`${msg2}
3589
+ `);
3590
+ return { message: msg2 };
3591
+ }
3592
+ if (trimmed.startsWith("limit ")) {
3593
+ const persist = hasPersistFlag(trimmed);
3594
+ const raw = stripPersistFlag(trimmed.slice("limit ".length)).trim();
3595
+ const limit = parseTokenCount(raw);
3596
+ if (!limit) {
3597
+ const msg3 = `Invalid context limit "${raw}". Use a positive token count, e.g. 220k or 220000.`;
3598
+ opts.renderer.write(`${color.red(msg3)}
3599
+ `);
3600
+ return { message: msg3 };
3601
+ }
3602
+ ctx.meta["effectiveMaxContext"] = limit;
3603
+ const effective = opts.onContextLimit?.(limit) ?? limit;
3604
+ if (persist) {
3605
+ const error = await persistContextConfig(opts, { effectiveMaxContext: limit });
3606
+ if (error) {
3607
+ opts.renderer.write(`${color.red(error)}
3608
+ `);
3609
+ return { message: error };
3610
+ }
3611
+ }
3612
+ const msg2 = `${color.green("Effective context window set:")} ${effective.toLocaleString()} tokens${persist ? " (persisted)" : ""}`;
3613
+ opts.renderer.write(`${msg2}
3614
+ `);
3615
+ return { message: msg2 };
3616
+ }
3617
+ if (trimmed.startsWith("thresholds ")) {
3618
+ const persist = hasPersistFlag(trimmed);
3619
+ const thresholdArgs = stripPersistFlag(trimmed.slice("thresholds ".length)).trim();
3620
+ const parts = thresholdArgs.split(/\s+/).filter(Boolean);
3621
+ if (parts.length !== 3) {
3622
+ const msg3 = "Usage: /context thresholds <warn> <soft> <hard> (examples: 60% 75% 90% or 0.6 0.75 0.9)";
3623
+ opts.renderer.write(`${color.red(msg3)}
3624
+ `);
3625
+ return { message: msg3 };
3626
+ }
3627
+ const thresholds = parts.map(parseThreshold);
3628
+ if (thresholds.some((v) => v === null)) {
3629
+ const msg3 = "Invalid thresholds. Use percentages (60%) or decimals between 0 and 1.";
3630
+ opts.renderer.write(`${color.red(msg3)}
3631
+ `);
3632
+ return { message: msg3 };
3633
+ }
3634
+ const [warn, soft, hard] = thresholds;
3635
+ if (!(warn < soft && soft < hard)) {
3636
+ const msg3 = "Invalid thresholds: require warn < soft < hard.";
3637
+ opts.renderer.write(`${color.red(msg3)}
3638
+ `);
3639
+ return { message: msg3 };
3640
+ }
3641
+ const base = readPolicy(ctx) ?? resolveContextWindowPolicy({});
3642
+ const policy2 = { ...base, thresholds: { warn, soft, hard } };
3643
+ ctx.meta["contextWindowMode"] = policy2.id;
3644
+ ctx.meta["contextWindowPolicy"] = policy2;
3645
+ if (persist) {
3646
+ const error = await persistContextConfig(opts, {
3647
+ warnThreshold: warn,
3648
+ softThreshold: soft,
3649
+ hardThreshold: hard
3650
+ });
3651
+ if (error) {
3652
+ opts.renderer.write(`${color.red(error)}
3653
+ `);
3654
+ return { message: error };
3655
+ }
3656
+ }
3657
+ const msg2 = `${color.green("Context thresholds set:")} warn ${pct(warn)}, soft ${pct(soft)}, hard ${pct(hard)}${persist ? " (persisted)" : ""}`;
3658
+ opts.renderer.write(`${msg2}
3403
3659
  `);
3404
3660
  return { message: msg2 };
3405
3661
  }
@@ -3433,6 +3689,7 @@ ${formatContextWindowModeList(active)}`;
3433
3689
  ` messages: ${messages.length} total (${countTurnPairs(messages)} user+assistant pairs)`,
3434
3690
  ` tokens (est): ${estimateTokens(messages).toLocaleString()} (chars / 4 estimate)`,
3435
3691
  ` mode: ${policy ? `${policy.id} (${policy.name})` : "balanced"}`,
3692
+ ` limit: ${formatLimit(readEffectiveLimit(ctx, opts))}`,
3436
3693
  ` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
3437
3694
  ` tools: ${countToolUses(messages)} calls made, ${countToolResults(messages)} results in history`,
3438
3695
  ` read files: ${ctx.readFiles.size} files`,
@@ -3459,6 +3716,68 @@ function readPolicy(ctx) {
3459
3716
  const policy = ctx.meta?.["contextWindowPolicy"];
3460
3717
  return policy && typeof policy === "object" ? policy : null;
3461
3718
  }
3719
+ function hasPersistFlag(input) {
3720
+ return /(?:^|\s)--persist(?:\s|$)/.test(input);
3721
+ }
3722
+ function stripPersistFlag(input) {
3723
+ return input.replace(/(?:^|\s)--persist(?:\s|$)/g, " ").trim();
3724
+ }
3725
+ async function persistContextConfig(opts, patch) {
3726
+ if (!opts.configStore || !opts.paths) return "Cannot persist context settings: config store not available.";
3727
+ let raw = "{}";
3728
+ try {
3729
+ raw = await fsp3.readFile(opts.paths.globalConfig, "utf8");
3730
+ } catch (err) {
3731
+ if (err.code !== "ENOENT") {
3732
+ return `Could not read ${opts.paths.globalConfig}: ${err.message}`;
3733
+ }
3734
+ }
3735
+ let parsed;
3736
+ try {
3737
+ parsed = JSON.parse(raw);
3738
+ } catch (err) {
3739
+ return `Config at ${opts.paths.globalConfig} is not valid JSON: ${err.message}`;
3740
+ }
3741
+ const current = opts.configStore.get();
3742
+ const context = {
3743
+ ...current.context,
3744
+ ...parsed.context ?? {},
3745
+ ...patch
3746
+ };
3747
+ parsed.context = context;
3748
+ await atomicWrite(opts.paths.globalConfig, JSON.stringify(parsed, null, 2), { mode: 384 });
3749
+ opts.configStore.update({ context });
3750
+ return null;
3751
+ }
3752
+ function readEffectiveLimit(ctx, opts) {
3753
+ const live = opts.onContextLimit?.();
3754
+ if (typeof live === "number" && Number.isFinite(live) && live > 0) return live;
3755
+ const metaLimit = ctx.meta?.["effectiveMaxContext"];
3756
+ if (typeof metaLimit === "number" && Number.isFinite(metaLimit) && metaLimit > 0) return metaLimit;
3757
+ const providerLimit = ctx.provider?.capabilities?.maxContext;
3758
+ return typeof providerLimit === "number" && Number.isFinite(providerLimit) && providerLimit > 0 ? providerLimit : 0;
3759
+ }
3760
+ function parseTokenCount(raw) {
3761
+ const normalized = raw.trim().toLowerCase().replace(/,/g, "").replace(/_/g, "");
3762
+ const match = /^(\d+(?:\.\d+)?)([km])?$/.exec(normalized);
3763
+ if (!match) return null;
3764
+ const value = Number(match[1]);
3765
+ const unit = match[2];
3766
+ const scaled = unit === "m" ? value * 1e6 : unit === "k" ? value * 1e3 : value;
3767
+ const rounded = Math.floor(scaled);
3768
+ return Number.isFinite(rounded) && rounded > 0 ? rounded : null;
3769
+ }
3770
+ function parseThreshold(raw) {
3771
+ const s = raw.trim();
3772
+ const percent = s.endsWith("%");
3773
+ const n = Number((percent ? s.slice(0, -1) : s).trim());
3774
+ if (!Number.isFinite(n)) return null;
3775
+ const value = percent ? n / 100 : n;
3776
+ return value > 0 && value < 1 ? value : null;
3777
+ }
3778
+ function formatLimit(limit) {
3779
+ return limit > 0 ? `${limit.toLocaleString()} tokens` : "unknown";
3780
+ }
3462
3781
  function pct(n) {
3463
3782
  return `${Math.round(n * 100)}%`;
3464
3783
  }
@@ -4867,12 +5186,20 @@ function parseMcpArgs(args) {
4867
5186
  }
4868
5187
  async function runMcpManagementCommand(parsed, deps) {
4869
5188
  const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
4870
- const configured = config.mcpServers ?? {};
5189
+ const diskConfig = await readConfig(configPath2);
5190
+ const configured = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : config.mcpServers ?? {};
4871
5191
  switch (parsed.action) {
4872
5192
  case "list":
4873
5193
  return renderList(configured, mcpRegistry, allServerPresets);
4874
5194
  case "add":
4875
- return runAdd(parsed.name, parsed.enable ?? false, configured, configPath2, allServerPresets);
5195
+ return runAdd(
5196
+ parsed.name,
5197
+ parsed.enable ?? false,
5198
+ configured,
5199
+ configPath2,
5200
+ mcpRegistry,
5201
+ allServerPresets
5202
+ );
4876
5203
  case "remove":
4877
5204
  return runRemove(parsed.name, configured, configPath2, mcpRegistry);
4878
5205
  case "enable":
@@ -4916,34 +5243,46 @@ function renderList(configured, mcpRegistry, all) {
4916
5243
  lines.push(color.dim(" /mcp restart <name> (runtime restart)"));
4917
5244
  return lines.join("\n");
4918
5245
  }
4919
- async function runAdd(name, enable, configured, configPath2, all) {
5246
+ async function runAdd(name, enable, configured, configPath2, mcpRegistry, all) {
4920
5247
  const preset = all[name];
4921
5248
  if (!preset) {
4922
5249
  const known = Object.keys(all).join(", ");
4923
5250
  return `Unknown server "${name}". Available: ${known}`;
4924
5251
  }
4925
- if (configured[name]) {
4926
- const full2 = await readConfig(configPath2);
4927
- full2.mcpServers = {
4928
- ...full2.mcpServers ?? {},
4929
- [name]: { ...preset, ...configured[name], enabled: enable }
4930
- };
4931
- await writeConfig(configPath2, full2);
4932
- return `${color.green("Updated")} "${name}" (${enable ? "enabled" : "disabled"}). Config written.`;
4933
- }
5252
+ const existing = configured[name];
5253
+ const nextCfg = existing ? { ...preset, ...existing, enabled: enable } : { ...preset, enabled: enable };
4934
5254
  const full = await readConfig(configPath2);
4935
- const mcpServers = { ...full.mcpServers ?? {}, [name]: { ...preset, enabled: enable } };
5255
+ const mcpServers = {
5256
+ ...isMcpServerRecord(full.mcpServers) ? full.mcpServers : {},
5257
+ [name]: nextCfg
5258
+ };
4936
5259
  full.mcpServers = mcpServers;
4937
5260
  await writeConfig(configPath2, full);
4938
- const verb = enable ? "Enabled" : "Added (disabled \u2014 /mcp enable to start)";
4939
- return `${color.green(verb)} "${name}" (${preset.transport}). Config written to ${configPath2}.`;
5261
+ if (!enable) {
5262
+ const verb = existing ? "Updated" : "Added (disabled \u2014 /mcp enable to start)";
5263
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5264
+ }
5265
+ try {
5266
+ if (mcpRegistry.list().some((server) => server.name === name)) {
5267
+ await mcpRegistry.restart(name);
5268
+ } else {
5269
+ await mcpRegistry.start(nextCfg);
5270
+ }
5271
+ const verb = existing ? "Updated and started" : "Enabled and started";
5272
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5273
+ } catch (err) {
5274
+ const message = err instanceof Error ? err.message : String(err);
5275
+ return `${color.yellow("Enabled")} "${name}" in config, but failed to start: ${message}`;
5276
+ }
4940
5277
  }
4941
5278
  async function runRemove(name, configured, configPath2, mcpRegistry) {
4942
5279
  if (!configured[name]) return `Server "${name}" is not in config.`;
4943
5280
  await mcpRegistry.stop(name).catch(() => {
4944
5281
  });
4945
5282
  const full = await readConfig(configPath2);
4946
- const mcpServers = { ...full.mcpServers ?? {} };
5283
+ const mcpServers = {
5284
+ ...full.mcpServers ?? {}
5285
+ };
4947
5286
  delete mcpServers[name];
4948
5287
  full.mcpServers = mcpServers;
4949
5288
  await writeConfig(configPath2, full);
@@ -4962,7 +5301,9 @@ async function runEnable(name, configured, configPath2, mcpRegistry) {
4962
5301
  }
4963
5302
  }
4964
5303
  const full = await readConfig(configPath2);
4965
- const mcpServers = { ...full.mcpServers ?? {} };
5304
+ const mcpServers = {
5305
+ ...full.mcpServers ?? {}
5306
+ };
4966
5307
  mcpServers[name] = { ...mcpServers[name], enabled: true };
4967
5308
  full.mcpServers = mcpServers;
4968
5309
  await writeConfig(configPath2, full);
@@ -4979,7 +5320,9 @@ async function runDisable(name, configured, configPath2, mcpRegistry) {
4979
5320
  await mcpRegistry.stop(name).catch(() => {
4980
5321
  });
4981
5322
  const full = await readConfig(configPath2);
4982
- const mcpServers = { ...full.mcpServers ?? {} };
5323
+ const mcpServers = {
5324
+ ...full.mcpServers ?? {}
5325
+ };
4983
5326
  mcpServers[name] = { ...mcpServers[name], enabled: false };
4984
5327
  full.mcpServers = mcpServers;
4985
5328
  await writeConfig(configPath2, full);
@@ -5020,6 +5363,9 @@ async function readConfig(path25) {
5020
5363
  return {};
5021
5364
  }
5022
5365
  }
5366
+ function isMcpServerRecord(value) {
5367
+ return !!value && typeof value === "object" && !Array.isArray(value);
5368
+ }
5023
5369
  async function writeConfig(path25, cfg) {
5024
5370
  const raw = JSON.stringify(cfg, null, 2);
5025
5371
  const tmp = path25 + ".tmp";
@@ -6339,13 +6685,13 @@ function buildYoloCommand(opts) {
6339
6685
  description: "Toggle or query YOLO (auto-approve) mode.",
6340
6686
  help: [
6341
6687
  "Usage:",
6342
- " /yolo Show current YOLO status",
6343
- " /yolo on Enable YOLO mode (auto-approve every tool call)",
6344
- " /yolo off Disable YOLO mode (restore permission prompts)",
6345
- " /yolo toggle Toggle YOLO mode",
6688
+ " /yolo Show current YOLO status",
6689
+ " /yolo on Enable YOLO mode (auto-approve all tool calls)",
6690
+ " /yolo off Disable YOLO mode (restore permission prompts)",
6691
+ " /yolo destructive Toggle destructive confirmation gate (YOLO mode only)",
6346
6692
  "",
6347
- "YOLO mode skips all permission prompts and auto-approves every tool call.",
6348
- "Use with caution \u2014 the agent can execute any tool without asking."
6693
+ "YOLO mode auto-approves everything, including destructive calls.",
6694
+ "Use /yolo destructive to re-enable confirmation for risky operations."
6349
6695
  ].join("\n"),
6350
6696
  async run(args) {
6351
6697
  const arg = args.trim().toLowerCase();
@@ -6356,7 +6702,7 @@ function buildYoloCommand(opts) {
6356
6702
  }
6357
6703
  if (!arg) {
6358
6704
  const current = opts.onYolo();
6359
- const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving all tool calls)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
6705
+ const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving normal project work)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
6360
6706
  const msg2 = `YOLO mode: ${status}`;
6361
6707
  opts.renderer.write(msg2);
6362
6708
  return { message: msg2 };
@@ -6374,7 +6720,7 @@ function buildYoloCommand(opts) {
6374
6720
  return { message: msg2 };
6375
6721
  }
6376
6722
  opts.onYolo(newState);
6377
- const label = newState ? `${color.yellow("ENABLED")} \u2014 all tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
6723
+ const label = newState ? `${color.yellow("ENABLED")} \u2014 normal project tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
6378
6724
  const msg = `YOLO mode: ${label}`;
6379
6725
  opts.renderer.write(msg);
6380
6726
  return { message: msg };
@@ -6527,10 +6873,10 @@ async function runProjectCheck(opts) {
6527
6873
  if (answer2 === "y" || answer2 === "yes") {
6528
6874
  try {
6529
6875
  const { spawn: spawn3 } = await import('child_process');
6530
- await new Promise((resolve3, reject) => {
6876
+ await new Promise((resolve4, reject) => {
6531
6877
  const child = spawn3("git", ["init"], { cwd: projectRoot });
6532
6878
  child.on("error", reject);
6533
- child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`)));
6879
+ child.on("close", (code) => code === 0 ? resolve4() : reject(new Error(`git init failed with ${code}`)));
6534
6880
  });
6535
6881
  renderer.write(` ${color.green("\u2713")} Git repository initialized
6536
6882
  `);
@@ -6581,7 +6927,7 @@ async function runLaunchPrompts(opts) {
6581
6927
  yolo = yoloPinned;
6582
6928
  } else {
6583
6929
  const answer = (await reader.readLine(
6584
- ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n/q]")} `
6930
+ ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve normal project work)")} ${color.dim("[Y/n/q]")} `
6585
6931
  )).trim().toLowerCase();
6586
6932
  if (answer === "q") {
6587
6933
  renderer.write(color.dim(" Goodbye!\n"));
@@ -6674,7 +7020,7 @@ var ReadlineInputReader = class {
6674
7020
  async readLine(prompt) {
6675
7021
  if (this.history.length === 0) await this.loadHistory();
6676
7022
  while (this.pending) {
6677
- await new Promise((resolve3) => setTimeout(resolve3, 50));
7023
+ await new Promise((resolve4) => setTimeout(resolve4, 50));
6678
7024
  }
6679
7025
  this.pending = true;
6680
7026
  try {
@@ -6684,15 +7030,15 @@ var ReadlineInputReader = class {
6684
7030
  this.rl = void 0;
6685
7031
  }
6686
7032
  const fresh = this.ensure();
6687
- return new Promise((resolve3) => {
7033
+ return new Promise((resolve4) => {
6688
7034
  fresh.question(prompt ?? "> ", (line) => {
6689
7035
  if (line.trim()) {
6690
7036
  this.history.push(line);
6691
7037
  void this.saveHistory();
6692
7038
  }
6693
- resolve3(line);
7039
+ resolve4(line);
6694
7040
  });
6695
- fresh.once("close", () => resolve3(""));
7041
+ fresh.once("close", () => resolve4(""));
6696
7042
  }).then((result) => {
6697
7043
  this.rl?.close();
6698
7044
  this.rl = void 0;
@@ -6704,7 +7050,7 @@ var ReadlineInputReader = class {
6704
7050
  }
6705
7051
  async readKey(prompt, options) {
6706
7052
  writeOut(prompt);
6707
- return new Promise((resolve3) => {
7053
+ return new Promise((resolve4) => {
6708
7054
  const stdin = process.stdin;
6709
7055
  const wasRaw = stdin.isRaw;
6710
7056
  const wasPaused = stdin.isPaused();
@@ -6715,7 +7061,7 @@ var ReadlineInputReader = class {
6715
7061
  if (key === "") {
6716
7062
  cleanup();
6717
7063
  writeOut("\n");
6718
- resolve3("");
7064
+ resolve4("");
6719
7065
  return;
6720
7066
  }
6721
7067
  const opt = options.find(
@@ -6725,12 +7071,12 @@ var ReadlineInputReader = class {
6725
7071
  cleanup();
6726
7072
  writeOut(`${opt.key}
6727
7073
  `);
6728
- resolve3(opt.value);
7074
+ resolve4(opt.value);
6729
7075
  }
6730
7076
  };
6731
7077
  const onClose = () => {
6732
7078
  cleanup();
6733
- resolve3("");
7079
+ resolve4("");
6734
7080
  };
6735
7081
  const cleanup = () => {
6736
7082
  stdin.off("data", onData);
@@ -6758,7 +7104,7 @@ var ReadlineInputReader = class {
6758
7104
  this.rl?.close();
6759
7105
  this.rl = void 0;
6760
7106
  writeOut(prompt);
6761
- return new Promise((resolve3) => {
7107
+ return new Promise((resolve4) => {
6762
7108
  let buf = "";
6763
7109
  const wasRaw = stdin.isRaw;
6764
7110
  setRawMode(stdin, true);
@@ -6776,7 +7122,7 @@ var ReadlineInputReader = class {
6776
7122
  cleanup();
6777
7123
  writeOut(` ${dim(`[${buf.length} chars]`)}
6778
7124
  `);
6779
- resolve3(buf);
7125
+ resolve4(buf);
6780
7126
  return;
6781
7127
  }
6782
7128
  if (ch === "") {
@@ -7138,11 +7484,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
7138
7484
  var theme = { primary: color.amber };
7139
7485
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
7140
7486
  try {
7141
- const { atomicWrite: atomicWrite12 } = await import('@wrongstack/core');
7142
- const fs24 = await import('fs/promises');
7487
+ const { atomicWrite: atomicWrite13 } = await import('@wrongstack/core');
7488
+ const fs25 = await import('fs/promises');
7143
7489
  let existing = {};
7144
7490
  try {
7145
- const raw = await fs24.readFile(configPath2, "utf8");
7491
+ const raw = await fs25.readFile(configPath2, "utf8");
7146
7492
  existing = JSON.parse(raw);
7147
7493
  } catch {
7148
7494
  }
@@ -7150,7 +7496,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
7150
7496
  existing.provider = provider;
7151
7497
  existing.model = model;
7152
7498
  await backupCurrent(homeFn);
7153
- await atomicWrite12(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7499
+ await atomicWrite13(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7154
7500
  try {
7155
7501
  await appendHistory(
7156
7502
  oldCfg,
@@ -7440,7 +7786,7 @@ var GROUPS = [
7440
7786
  items: [
7441
7787
  { key: "/mode", blurb: "switch persona: code-reviewer, debugger, architect, tester, devops, \u2026" },
7442
7788
  { key: "/model", blurb: "two-step provider \u2192 model picker, hot-swap at runtime" },
7443
- { key: "/yolo on|off|toggle", blurb: "auto-approve every tool call without restart" },
7789
+ { key: "/yolo on|off|toggle", blurb: "auto-approve normal project work without restart" },
7444
7790
  { key: "/context mode frugal|balanced|deep|archival", blurb: "pick how aggressively history is trimmed" },
7445
7791
  { key: "/compact", blurb: "manually compact the in-flight context window" },
7446
7792
  { key: "/plan show|add|start|done", blurb: "strategic roadmap, survives /resume across sessions" }
@@ -7473,12 +7819,12 @@ function pickGroupIndex(opts) {
7473
7819
  try {
7474
7820
  let current = 0;
7475
7821
  try {
7476
- const parsed = Number.parseInt(fs11.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7822
+ const parsed = Number.parseInt(fs12.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7477
7823
  if (Number.isFinite(parsed)) current = wrap(parsed);
7478
7824
  } catch {
7479
7825
  }
7480
- fs11.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7481
- fs11.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7826
+ fs12.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7827
+ fs12.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7482
7828
  return current;
7483
7829
  } catch {
7484
7830
  }
@@ -8680,8 +9026,9 @@ var updateCmd = async (args, deps) => {
8680
9026
  deps.renderer.write(`Updating wrongstack from v${info.current} to v${info.latest}...
8681
9027
  `);
8682
9028
  try {
8683
- const result = await new Promise((resolve3, reject) => {
8684
- const child = spawn("npm", ["install", "-g", "wrongstack@latest"], {
9029
+ const result = await new Promise((resolve4, reject) => {
9030
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
9031
+ const child = spawn(npmCommand, ["install", "-g", "wrongstack@latest"], {
8685
9032
  cwd,
8686
9033
  stdio: "pipe"
8687
9034
  });
@@ -8690,7 +9037,7 @@ var updateCmd = async (args, deps) => {
8690
9037
  _stderr += d;
8691
9038
  });
8692
9039
  child.on("error", reject);
8693
- child.on("close", (code) => resolve3({ code: code ?? 0 }));
9040
+ child.on("close", (code) => resolve4({ code: code ?? 0 }));
8694
9041
  });
8695
9042
  if (result.code === 0) {
8696
9043
  deps.renderer.write(`
@@ -9016,9 +9363,160 @@ var initCmd = async (_args, deps) => {
9016
9363
  deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
9017
9364
  return 0;
9018
9365
  };
9366
+ var AllowAllPermissionPolicy = class extends AutoApprovePermissionPolicy {
9367
+ async evaluate() {
9368
+ return { permission: "auto", source: "default" };
9369
+ }
9370
+ };
9371
+ function parseToolsFlag(flags) {
9372
+ const raw = flags["tools"];
9373
+ if (typeof raw !== "string") return null;
9374
+ const set = new Set(
9375
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
9376
+ );
9377
+ return set.size > 0 ? set : null;
9378
+ }
9379
+ function makeServeContext(cwd, projectRoot, signal) {
9380
+ const provider = {
9381
+ id: "mcp-serve",
9382
+ capabilities: { maxContext: 0 },
9383
+ complete: async () => {
9384
+ throw new Error("no model provider in `mcp serve` mode");
9385
+ },
9386
+ stream: () => {
9387
+ throw new Error("no model provider in `mcp serve` mode");
9388
+ }
9389
+ };
9390
+ const session = { append: async () => {
9391
+ } };
9392
+ const tokenCounter = {
9393
+ account: () => {
9394
+ },
9395
+ total: () => ({ input: 0, output: 0 }),
9396
+ estimateCost: () => ({ total: 0 })
9397
+ };
9398
+ return new Context({
9399
+ systemPrompt: [],
9400
+ provider,
9401
+ session,
9402
+ signal,
9403
+ tokenCounter,
9404
+ cwd,
9405
+ projectRoot,
9406
+ model: "mcp-serve",
9407
+ tools: []
9408
+ });
9409
+ }
9410
+ async function selectExposedTools(registry, ctx, policy, whitelist) {
9411
+ const allowed = [];
9412
+ for (const tool of registry.list()) {
9413
+ if (whitelist && !whitelist.has(tool.name)) continue;
9414
+ const decision = await policy.evaluate(tool, {}, ctx);
9415
+ if (decision.permission === "auto") allowed.push(tool);
9416
+ }
9417
+ return allowed;
9418
+ }
9419
+ async function serveMcpStdio(deps) {
9420
+ const flags = deps.flags ?? {};
9421
+ const yolo = flags["yolo"] === true || flags["allow-all"] === true;
9422
+ const whitelist = parseToolsFlag(flags);
9423
+ const log = (m) => process.stderr.write(`${m}
9424
+ `);
9425
+ let registry = deps.toolRegistry;
9426
+ if (!registry) {
9427
+ registry = new ToolRegistry();
9428
+ registry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
9429
+ }
9430
+ const controller = new AbortController();
9431
+ const ctx = makeServeContext(deps.cwd, deps.projectRoot, controller.signal);
9432
+ const permissionPolicy = yolo ? new AllowAllPermissionPolicy() : new AutoApprovePermissionPolicy();
9433
+ const executor = new ToolExecutor(registry, {
9434
+ permissionPolicy,
9435
+ secretScrubber: new DefaultSecretScrubber(),
9436
+ perIterationOutputCapBytes: 1e6
9437
+ });
9438
+ const allowed = await selectExposedTools(registry, ctx, permissionPolicy, whitelist);
9439
+ const allowedNames = new Set(allowed.map((t) => t.name));
9440
+ if (allowed.length === 0) {
9441
+ log(
9442
+ "wrongstack MCP server: no tools to expose (all withheld by policy or filtered out). Pass --yolo to expose write/exec tools, or --tools <names> to whitelist."
9443
+ );
9444
+ }
9445
+ let counter = 0;
9446
+ const host = {
9447
+ listTools: () => allowed.map((t) => ({
9448
+ name: t.name,
9449
+ description: t.description,
9450
+ inputSchema: t.inputSchema ?? { type: "object" }
9451
+ })),
9452
+ callTool: async (name, callArgs) => {
9453
+ if (!allowedNames.has(name)) {
9454
+ return { content: `Tool "${name}" is not exposed by this server`, isError: true };
9455
+ }
9456
+ const use = {
9457
+ type: "tool_use",
9458
+ id: `srv_${++counter}`,
9459
+ name,
9460
+ input: callArgs
9461
+ };
9462
+ const batch = await executor.executeBatch([use], ctx, "sequential");
9463
+ const result = batch.outputs[0]?.result;
9464
+ if (!result || result.type === "tool_confirm_pending") {
9465
+ return {
9466
+ content: `Tool "${name}" requires interactive confirmation, which is unavailable over MCP`,
9467
+ isError: true
9468
+ };
9469
+ }
9470
+ return { content: result.content, isError: Boolean(result.is_error) };
9471
+ }
9472
+ };
9473
+ const server = new MCPServer({
9474
+ host,
9475
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9476
+ });
9477
+ const mode = yolo ? "yolo: all tools" : "safe: read-only tools";
9478
+ if (flags["http"] === true || typeof flags["http"] === "string" || flags["port"] || flags["host"]) {
9479
+ const port = Number(flags["port"] ?? flags["http"] ?? 0) || 0;
9480
+ const httpHost = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
9481
+ const token = typeof flags["token"] === "string" ? flags["token"] : void 0;
9482
+ let handle2;
9483
+ try {
9484
+ handle2 = await serveHttp(server, {
9485
+ port,
9486
+ host: httpHost,
9487
+ token,
9488
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9489
+ });
9490
+ } catch (err) {
9491
+ log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
9492
+ return 1;
9493
+ }
9494
+ log(
9495
+ `wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s) (${mode})${token ? " [token auth]" : ""}.`
9496
+ );
9497
+ await new Promise((resolve4) => {
9498
+ const stop = () => resolve4();
9499
+ process.once("SIGINT", stop);
9500
+ process.once("SIGTERM", stop);
9501
+ });
9502
+ await handle2.close();
9503
+ controller.abort();
9504
+ return 0;
9505
+ }
9506
+ log(`wrongstack MCP server ready on stdio \u2014 exposing ${allowed.length} tool(s) (${mode}).`);
9507
+ const handle = serveStdio(server);
9508
+ await handle.done;
9509
+ controller.abort();
9510
+ return 0;
9511
+ }
9512
+
9513
+ // src/subcommands/handlers/mcp.ts
9019
9514
  var BUILT_IN_MCP = allServers();
9020
9515
  var mcpCmd = async (args, deps) => {
9021
9516
  const sub = args[0];
9517
+ if (sub === "serve") {
9518
+ return serveMcpStdio(deps);
9519
+ }
9022
9520
  if (!sub || sub === "list") {
9023
9521
  const servers = deps.config.mcpServers ?? {};
9024
9522
  if (Object.keys(servers).length === 0) {
@@ -10266,10 +10764,10 @@ var auditCmd = async (args, deps) => {
10266
10764
  return verify.ok ? 0 : 1;
10267
10765
  };
10268
10766
  async function listAudits(log, dir, deps) {
10269
- const fs24 = await import('fs/promises');
10767
+ const fs25 = await import('fs/promises');
10270
10768
  let entries;
10271
10769
  try {
10272
- entries = await fs24.readdir(dir);
10770
+ entries = await fs25.readdir(dir);
10273
10771
  } catch {
10274
10772
  deps.renderer.write(
10275
10773
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -10356,7 +10854,18 @@ var helpCmd = async (_args, deps) => {
10356
10854
  " wstack doctor Health checks",
10357
10855
  " wstack export <id> [opts] Render a session",
10358
10856
  " wstack usage Token + cost summary",
10359
- " wstack version Print version"
10857
+ " wstack version Print version",
10858
+ "",
10859
+ color.bold("Common flags"),
10860
+ " --yolo Auto-approve all tool calls (including destructive)",
10861
+ " --confirm-destructive In YOLO mode, still prompt for destructive operations",
10862
+ " --yolo-destructive Deprecated \u2014 YOLO now auto-approves everything by default",
10863
+ " --tui / --no-tui Force or disable TUI mode",
10864
+ " --webui [--port <n>] [--open] Serve the browser UI + WS bridge (prints a URL,",
10865
+ " --open pops the browser; shares this terminal's",
10866
+ " agent; auto-picks free ports)",
10867
+ ' --eternal "<mission>" Start an eternal-autonomy loop',
10868
+ " --no-hints Hide launch hints"
10360
10869
  ];
10361
10870
  deps.renderer.write(lines.join("\n") + "\n");
10362
10871
  return 0;
@@ -10529,7 +11038,8 @@ async function boot(argv) {
10529
11038
  vault,
10530
11039
  cwd,
10531
11040
  projectRoot,
10532
- userHome
11041
+ userHome,
11042
+ flags
10533
11043
  });
10534
11044
  await reader.close();
10535
11045
  return code;
@@ -10582,6 +11092,10 @@ async function boot(argv) {
10582
11092
  return 2;
10583
11093
  }
10584
11094
  }
11095
+ if (flags["webui"]) {
11096
+ flags["tui"] = false;
11097
+ flags["no-tui"] = true;
11098
+ }
10585
11099
  if (isInteractiveTTY) {
10586
11100
  let modePinned;
10587
11101
  if (flags["no-tui"]) modePinned = "repl";
@@ -10647,6 +11161,20 @@ async function boot(argv) {
10647
11161
  updateInfo
10648
11162
  };
10649
11163
  }
11164
+ var CONTEXT_OVERFLOW_RE = /context window|exceeds the context|too many tokens|context.*tokens/i;
11165
+ function contextOverflowHint(err) {
11166
+ const structured = err.code === ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW || err.code === ERROR_CODES.AGENT_CONTEXT_OVERFLOW;
11167
+ const textual = CONTEXT_OVERFLOW_RE.test(`${err.message}
11168
+ ${err.describe()}`);
11169
+ if (!structured && !textual) return null;
11170
+ return [
11171
+ "Provider rejected the request as over its effective context window.",
11172
+ "If you use a custom baseUrl/proxy, the real limit may be lower than models.dev reports.",
11173
+ "Try: /context limit 220k",
11174
+ "Then, if needed: /context thresholds 50% 70% 85%",
11175
+ "Persistent config: set context.effectiveMaxContext."
11176
+ ].join("\n");
11177
+ }
10650
11178
  function fmtElapsed(ms) {
10651
11179
  const s = Math.floor(ms / 1e3);
10652
11180
  if (s < 60) return `${s}s`;
@@ -11006,7 +11534,7 @@ async function runRepl(opts) {
11006
11534
  `[eternal] ${err instanceof Error ? err.message : String(err)}`
11007
11535
  );
11008
11536
  }
11009
- await new Promise((resolve3) => setTimeout(resolve3, 250));
11537
+ await new Promise((resolve4) => setTimeout(resolve4, 250));
11010
11538
  continue;
11011
11539
  }
11012
11540
  } else if (opts.getAutonomy?.() === "eternal-parallel") {
@@ -11052,7 +11580,7 @@ async function runRepl(opts) {
11052
11580
  `[parallel] ${err instanceof Error ? err.message : String(err)}`
11053
11581
  );
11054
11582
  }
11055
- await new Promise((resolve3) => setTimeout(resolve3, 250));
11583
+ await new Promise((resolve4) => setTimeout(resolve4, 250));
11056
11584
  continue;
11057
11585
  }
11058
11586
  }
@@ -11238,6 +11766,8 @@ ${taskList}`;
11238
11766
  if (err) {
11239
11767
  const tag = err.recoverable ? " (recoverable)" : "";
11240
11768
  opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
11769
+ const hint = contextOverflowHint(err);
11770
+ if (hint) opts.renderer.writeWarning(hint);
11241
11771
  } else {
11242
11772
  opts.renderer.writeError("Failed.");
11243
11773
  }
@@ -11679,6 +12209,8 @@ async function execute(deps) {
11679
12209
  if (err) {
11680
12210
  const tag = err.recoverable ? " (recoverable)" : "";
11681
12211
  renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
12212
+ const hint = contextOverflowHint(err);
12213
+ if (hint) renderer.writeWarning(hint);
11682
12214
  } else {
11683
12215
  renderer.writeError("Failed.");
11684
12216
  }
@@ -11698,7 +12230,7 @@ async function execute(deps) {
11698
12230
  ) + "\n"
11699
12231
  );
11700
12232
  }
11701
- } else if (flags.tui && !flags["no-tui"]) {
12233
+ } else if (flags.tui && !flags["no-tui"] && !flags.webui) {
11702
12234
  agent.disableInteractiveConfirmation();
11703
12235
  const { runTui } = await import('@wrongstack/tui');
11704
12236
  renderer.setSilent(true);
@@ -11887,12 +12419,15 @@ async function execute(deps) {
11887
12419
  renderer.setSilent(false);
11888
12420
  }
11889
12421
  } else if (flags.webui) {
12422
+ agent.disableInteractiveConfirmation();
11890
12423
  const { runWebUI: runWebUI2 } = await Promise.resolve().then(() => (init_webui_server(), webui_server_exports));
11891
12424
  const webuiPromise = runWebUI2({
11892
12425
  agent,
11893
12426
  events,
11894
12427
  session,
11895
12428
  port: Number.parseInt(String(flags.port ?? "3457"), 10),
12429
+ projectRoot,
12430
+ open: !!flags.open,
11896
12431
  modelsRegistry,
11897
12432
  globalConfigPath: wpaths.globalConfig,
11898
12433
  subscribeEternalIteration
@@ -12823,7 +13358,7 @@ function samplePaths(set) {
12823
13358
  }
12824
13359
  var WORKTREE_PHASE_CONCURRENCY = 4;
12825
13360
  function gitText(args, cwd) {
12826
- return new Promise((resolve3) => {
13361
+ return new Promise((resolve4) => {
12827
13362
  let out = "";
12828
13363
  const child = spawn("git", args, {
12829
13364
  cwd,
@@ -12836,8 +13371,8 @@ function gitText(args, cwd) {
12836
13371
  child.stderr?.on("data", (c) => {
12837
13372
  out += c.toString();
12838
13373
  });
12839
- child.on("error", () => resolve3({ code: 1, out }));
12840
- child.on("close", (code) => resolve3({ code: code ?? 1, out: out.trim() }));
13374
+ child.on("error", () => resolve4({ code: 1, out }));
13375
+ child.on("close", (code) => resolve4({ code: code ?? 1, out: out.trim() }));
12841
13376
  });
12842
13377
  }
12843
13378
  async function isGitRepo(cwd) {
@@ -12845,7 +13380,7 @@ async function isGitRepo(cwd) {
12845
13380
  return code === 0 && out.trim() === "true";
12846
13381
  }
12847
13382
  function runCmd(cmd, args, cwd, shell = false) {
12848
- return new Promise((resolve3) => {
13383
+ return new Promise((resolve4) => {
12849
13384
  let out = "";
12850
13385
  const child = spawn(cmd, args, {
12851
13386
  cwd,
@@ -12859,8 +13394,8 @@ function runCmd(cmd, args, cwd, shell = false) {
12859
13394
  child.stderr?.on("data", (c) => {
12860
13395
  out += c.toString();
12861
13396
  });
12862
- child.on("error", (e) => resolve3({ code: 1, out: `${out}${String(e)}` }));
12863
- child.on("close", (code) => resolve3({ code: code ?? 1, out: out.trim() }));
13397
+ child.on("error", (e) => resolve4({ code: 1, out: `${out}${String(e)}` }));
13398
+ child.on("close", (code) => resolve4({ code: code ?? 1, out: out.trim() }));
12864
13399
  });
12865
13400
  }
12866
13401
  function detectPackageManager2(root) {
@@ -13253,13 +13788,6 @@ function renderProgress3(ratio, width) {
13253
13788
  const capped = Math.min(width, filled);
13254
13789
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
13255
13790
  }
13256
- var createSessionEventBridge = (_writer, level) => ({
13257
- append: async (_e) => {
13258
- },
13259
- level: level ?? "standard",
13260
- allows: () => true
13261
- });
13262
- var resolveAuditLevel = (cfg) => cfg?.session?.auditLevel ?? "standard";
13263
13791
  function setupPipelines(params) {
13264
13792
  const { events, logger } = params;
13265
13793
  const pipelines = createDefaultPipelines();
@@ -13305,6 +13833,10 @@ async function setupCompaction(params) {
13305
13833
  providerId: config.provider ?? provider.id,
13306
13834
  modelId: config.model ?? context.model
13307
13835
  });
13836
+ const initialPolicy = resolveContextWindowPolicy(config.context);
13837
+ context.meta ??= {};
13838
+ context.meta["contextWindowMode"] = initialPolicy.id;
13839
+ context.meta["contextWindowPolicy"] = initialPolicy;
13308
13840
  let autoCompactor;
13309
13841
  if (config.context.autoCompact !== false && effectiveMaxContext > 0) {
13310
13842
  const auditLevel = resolveAuditLevel(fullConfig ?? config);
@@ -13315,15 +13847,15 @@ async function setupCompaction(params) {
13315
13847
  // Calibrated estimator: recordActualUsage() is called after each API
13316
13848
  // response so this converges on real token counts for compaction decisions.
13317
13849
  (ctx) => estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
13850
+ initialPolicy.thresholds,
13318
13851
  {
13319
- warn: config.context.warnThreshold,
13320
- soft: config.context.softThreshold,
13321
- hard: config.context.hardThreshold
13322
- },
13323
- {
13324
- aggressiveOn: "soft",
13852
+ aggressiveOn: initialPolicy.aggressiveOn,
13325
13853
  failureMode: "throw_on_hard",
13326
13854
  events,
13855
+ policyProvider: (ctx) => {
13856
+ const policy = ctx.meta?.["contextWindowPolicy"];
13857
+ return policy && typeof policy === "object" ? policy : null;
13858
+ },
13327
13859
  sessionBridge
13328
13860
  }
13329
13861
  );
@@ -13342,7 +13874,8 @@ function createAgent(params) {
13342
13874
  confirmAwaiter: params.confirmAwaiter,
13343
13875
  iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
13344
13876
  perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
13345
- tracer: params.tracer
13877
+ tracer: params.tracer,
13878
+ hookRunner: params.hookRunner
13346
13879
  });
13347
13880
  return new Agent({
13348
13881
  container: params.container,
@@ -13360,6 +13893,139 @@ function createAgent(params) {
13360
13893
  tracer: params.tracer
13361
13894
  });
13362
13895
  }
13896
+ function parseModelRef(ref) {
13897
+ const trimmed = ref.trim();
13898
+ const slash = trimmed.indexOf("/");
13899
+ if (slash !== -1) {
13900
+ return {
13901
+ provider: trimmed.slice(0, slash) || void 0,
13902
+ model: trimmed.slice(slash + 1).trim()
13903
+ };
13904
+ }
13905
+ const parts = trimmed.split(/\s+/);
13906
+ if (parts.length >= 2) {
13907
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
13908
+ }
13909
+ return { model: trimmed };
13910
+ }
13911
+ function overloadStatus(err) {
13912
+ if (!(err instanceof ProviderError)) return null;
13913
+ const s = err.status;
13914
+ if (s === 429 || s === 529 || s >= 500) return s;
13915
+ return null;
13916
+ }
13917
+ function createFallbackModelExtension(deps) {
13918
+ const initial = deps.getConfig().fallbackModels ?? [];
13919
+ if (initial.length === 0) return null;
13920
+ let dirty = false;
13921
+ return {
13922
+ name: "fallback-model",
13923
+ beforeRun: (ctx) => {
13924
+ if (!dirty) return;
13925
+ const cfg = deps.getConfig();
13926
+ try {
13927
+ ctx.provider = deps.buildProvider(cfg.provider);
13928
+ ctx.model = cfg.model;
13929
+ deps.onModelSwitch?.(cfg.provider, cfg.model);
13930
+ } catch (err) {
13931
+ deps.logger.warn(
13932
+ `fallback-model: could not restore primary "${cfg.provider}/${cfg.model}": ${err instanceof Error ? err.message : String(err)}`
13933
+ );
13934
+ }
13935
+ dirty = false;
13936
+ },
13937
+ wrapProviderRunner: async (ctx, request, inner) => {
13938
+ try {
13939
+ return await inner(ctx, request);
13940
+ } catch (firstErr) {
13941
+ let lastErr = firstErr;
13942
+ const cfg = deps.getConfig();
13943
+ const chain = cfg.fallbackModels ?? [];
13944
+ for (const ref of chain) {
13945
+ const status = overloadStatus(lastErr);
13946
+ if (status === null) break;
13947
+ const parsed = parseModelRef(ref);
13948
+ if (!parsed.model) continue;
13949
+ const targetProviderId = parsed.provider ?? cfg.provider;
13950
+ const from = { providerId: ctx.provider.id, model: ctx.model };
13951
+ let nextProvider;
13952
+ try {
13953
+ nextProvider = deps.buildProvider(targetProviderId);
13954
+ } catch (err) {
13955
+ deps.logger.warn(
13956
+ `fallback-model: skipping "${ref}" \u2014 cannot build provider "${targetProviderId}": ${err instanceof Error ? err.message : String(err)}`
13957
+ );
13958
+ continue;
13959
+ }
13960
+ const providerSwitched = nextProvider.id !== from.providerId;
13961
+ ctx.provider = nextProvider;
13962
+ ctx.model = parsed.model;
13963
+ request.model = parsed.model;
13964
+ dirty = true;
13965
+ deps.onModelSwitch?.(targetProviderId, parsed.model);
13966
+ deps.events.emit("provider.fallback", {
13967
+ from,
13968
+ to: { providerId: nextProvider.id, model: parsed.model },
13969
+ status,
13970
+ providerSwitched
13971
+ });
13972
+ try {
13973
+ return await inner(ctx, request);
13974
+ } catch (err) {
13975
+ lastErr = err;
13976
+ }
13977
+ }
13978
+ throw lastErr;
13979
+ }
13980
+ }
13981
+ };
13982
+ }
13983
+
13984
+ // src/hooks-wiring.ts
13985
+ var HookBlockedError = class extends Error {
13986
+ constructor(reason) {
13987
+ super(`Prompt blocked by hook: ${reason}`);
13988
+ this.name = "HookBlockedError";
13989
+ }
13990
+ };
13991
+ function createUserPromptSubmitMiddleware(hookRunner) {
13992
+ return {
13993
+ name: "UserPromptSubmitHooks",
13994
+ handler: async (payload, next) => {
13995
+ const prompt = payload.text;
13996
+ if (prompt && hookRunner.has("UserPromptSubmit")) {
13997
+ const r = await hookRunner.userPromptSubmit(prompt, payload.ctx);
13998
+ if (r.block) throw new HookBlockedError(r.reason ?? "no reason given");
13999
+ if (r.additionalContext) {
14000
+ const block = { type: "text", text: r.additionalContext };
14001
+ payload.content = [...payload.content, block];
14002
+ payload.text = `${prompt}
14003
+
14004
+ ${r.additionalContext}`;
14005
+ }
14006
+ }
14007
+ return next(payload);
14008
+ }
14009
+ };
14010
+ }
14011
+ function createLifecycleHooksExtension(hookRunner) {
14012
+ let started = false;
14013
+ return {
14014
+ name: "lifecycle-hooks",
14015
+ beforeRun: async (ctx) => {
14016
+ if (started) return;
14017
+ started = true;
14018
+ if (!hookRunner.has("SessionStart")) return;
14019
+ const r = await hookRunner.sessionStart(ctx);
14020
+ if (r.additionalContext) {
14021
+ ctx.systemPrompt.push({ type: "text", text: r.additionalContext });
14022
+ }
14023
+ },
14024
+ afterRun: async (ctx) => {
14025
+ if (hookRunner.has("Stop")) await hookRunner.stop(ctx);
14026
+ }
14027
+ };
14028
+ }
13363
14029
  function setupMetrics(params) {
13364
14030
  const { flags, wpaths, events, logger, config } = params;
13365
14031
  let metricsSink;
@@ -13475,7 +14141,8 @@ async function setupPlugins(params) {
13475
14141
  skillLoader,
13476
14142
  configStore,
13477
14143
  pipelines,
13478
- paths
14144
+ paths,
14145
+ hookRegistry
13479
14146
  } = params;
13480
14147
  const builtinPlugins = [];
13481
14148
  const disabledBuiltins = new Set(
@@ -13540,6 +14207,7 @@ async function setupPlugins(params) {
13540
14207
  config: pluginConfig,
13541
14208
  log,
13542
14209
  extensions: agent.extensions,
14210
+ hookRegistry,
13543
14211
  sessionWriter: {
13544
14212
  transcriptPath: sessionWriter.transcriptPath,
13545
14213
  append: (e) => sessionWriter.append(e)
@@ -13785,10 +14453,8 @@ async function launchEternalFromFlag(deps) {
13785
14453
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
13786
14454
  } : emptyGoal2(eternalFlag);
13787
14455
  await saveGoal2(goalPath, next);
13788
- const policy = deps.container.resolve(
13789
- TOKENS.PermissionPolicy
13790
- );
13791
- policy.setYolo(true);
14456
+ const policy = deps.container.resolve(TOKENS.PermissionPolicy);
14457
+ policy.setYolo?.(true);
13792
14458
  deps.configRef.current = patchConfig(deps.configRef.current, { yolo: true });
13793
14459
  const compactor = deps.container.resolve(TOKENS.Compactor);
13794
14460
  const engine = new EternalAutonomyEngine({
@@ -13810,21 +14476,6 @@ async function launchEternalFromFlag(deps) {
13810
14476
  }
13811
14477
 
13812
14478
  // src/index.ts
13813
- var createSessionEventBridge2 = (_writer, level, _opts) => ({
13814
- append: async (_e) => {
13815
- },
13816
- level: level ?? "standard",
13817
- allows: () => true
13818
- });
13819
- var resolveAuditLevel2 = (cfg) => cfg?.session?.auditLevel ?? "standard";
13820
- var resolveSessionLoggingConfig = (cfg) => ({
13821
- auditLevel: resolveAuditLevel2(cfg),
13822
- sampling: {
13823
- toolProgress: {
13824
- sampleRate: cfg?.session?.sampling?.toolProgress?.sampleRate ?? 8
13825
- }
13826
- }
13827
- });
13828
14479
  async function main(argv) {
13829
14480
  const ctx = await boot(argv);
13830
14481
  if (typeof ctx === "number") return ctx;
@@ -13851,7 +14502,8 @@ async function main(argv) {
13851
14502
  modelsRegistry,
13852
14503
  permission: {
13853
14504
  yolo: config.yolo,
13854
- forceAllYolo: flags["force-all-yolo"] === true,
14505
+ yoloDestructive: flags["yolo-destructive"] === true || flags["force-all-yolo"] === true,
14506
+ confirmDestructive: flags["confirm-destructive"] === true,
13855
14507
  promptDelegate: makePromptDelegate(reader)
13856
14508
  },
13857
14509
  compactor: {
@@ -14042,9 +14694,13 @@ async function main(argv) {
14042
14694
  const detachTodosCheckpoint = sessResult.detachTodosCheckpoint;
14043
14695
  const priorFleetState = sessResult.priorFleetState;
14044
14696
  const sessionConfig = resolveSessionLoggingConfig(config);
14045
- const sessionBridge = createSessionEventBridge2(
14697
+ const sessionBridge = createSessionEventBridge(
14046
14698
  session,
14047
- sessionConfig.auditLevel);
14699
+ sessionConfig.auditLevel,
14700
+ {
14701
+ sampling: sessionConfig.sampling
14702
+ }
14703
+ );
14048
14704
  const stats = new SessionStats(events, tokenCounter);
14049
14705
  const errorRing = [];
14050
14706
  events.on("error", (e) => {
@@ -14121,6 +14777,19 @@ async function main(argv) {
14121
14777
  });
14122
14778
  });
14123
14779
  const pipelines = setupPipelines({ events, logger });
14780
+ const hooksEnabled = flags["no-hooks"] !== true;
14781
+ const hookRegistry = new HookRegistry();
14782
+ if (hooksEnabled) hookRegistry.loadShellHooks(config.hooks);
14783
+ container.bind(TOKENS.HookRegistry, () => hookRegistry);
14784
+ const hookRunner = new HookRunner({
14785
+ registry: hookRegistry,
14786
+ logger,
14787
+ allowShell: hooksEnabled,
14788
+ sessionId: () => session.id
14789
+ });
14790
+ if (hooksEnabled) {
14791
+ pipelines.userInput.use(createUserPromptSubmitMiddleware(hookRunner));
14792
+ }
14124
14793
  const compactor = container.resolve(TOKENS.Compactor);
14125
14794
  const compactionSetup = await setupCompaction({
14126
14795
  compactor,
@@ -14167,8 +14836,12 @@ async function main(argv) {
14167
14836
  pipelines,
14168
14837
  context,
14169
14838
  config,
14170
- confirmAwaiter: makeConfirmAwaiter(reader)
14839
+ confirmAwaiter: makeConfirmAwaiter(reader),
14840
+ hookRunner
14171
14841
  });
14842
+ if (hooksEnabled) {
14843
+ agent.extensions.register(createLifecycleHooksExtension(hookRunner));
14844
+ }
14172
14845
  const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
14173
14846
  if (config.features.mcp) {
14174
14847
  for (const cfg of Object.values(config.mcpServers ?? {})) {
@@ -14196,24 +14869,41 @@ async function main(argv) {
14196
14869
  healthRegistry,
14197
14870
  skillLoader: config.features.skills ? skillLoader : void 0,
14198
14871
  configStore,
14199
- paths: wpaths
14872
+ paths: wpaths,
14873
+ hookRegistry
14874
+ });
14875
+ const resolveProviderCfg = (providerId) => {
14876
+ const savedCfg = config.providers?.[providerId];
14877
+ const resolvedProviderId = savedCfg?.type ?? providerId;
14878
+ const cfgWithType = {
14879
+ ...savedCfg ?? { type: providerId, apiKey: config.apiKey, baseUrl: config.baseUrl },
14880
+ type: resolvedProviderId
14881
+ };
14882
+ return { resolvedProviderId, cfgWithType };
14883
+ };
14884
+ const buildProviderForId = (providerId) => {
14885
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14886
+ return config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
14887
+ };
14888
+ const refreshMaxContextFor = (providerId, modelId) => {
14889
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14890
+ void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14891
+ };
14892
+ const fallbackExtension = createFallbackModelExtension({
14893
+ getConfig: () => config,
14894
+ buildProvider: buildProviderForId,
14895
+ onModelSwitch: refreshMaxContextFor,
14896
+ events,
14897
+ logger
14200
14898
  });
14899
+ if (fallbackExtension) agent.extensions.register(fallbackExtension);
14201
14900
  const switchProviderAndModel = (providerId, modelId) => {
14202
14901
  try {
14203
- const savedCfg = config.providers?.[providerId];
14204
- const resolvedProviderId = savedCfg?.type ?? providerId;
14205
- const newCfg = savedCfg ?? {
14206
- type: providerId,
14207
- apiKey: config.apiKey,
14208
- baseUrl: config.baseUrl
14209
- };
14210
- const cfgWithType = { ...newCfg, type: resolvedProviderId };
14211
- const newProvider = config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
14212
- context.provider = newProvider;
14902
+ context.provider = buildProviderForId(providerId);
14213
14903
  context.model = modelId;
14214
14904
  config = patchConfig(config, { provider: providerId, model: modelId });
14215
14905
  configStore.update({ provider: providerId, model: modelId });
14216
- void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14906
+ refreshMaxContextFor(providerId, modelId);
14217
14907
  return null;
14218
14908
  } catch (err) {
14219
14909
  return err instanceof Error ? err.message : String(err);
@@ -14820,6 +15510,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
14820
15510
  }
14821
15511
  return result.message;
14822
15512
  },
15513
+ onContextLimit: (tokens) => {
15514
+ if (typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0) {
15515
+ effectiveMaxContext = tokens;
15516
+ context.provider.capabilities.maxContext = tokens;
15517
+ context.meta["effectiveMaxContext"] = tokens;
15518
+ autoCompactor?.setMaxContext(tokens);
15519
+ events.emit("ctx.max_context", {
15520
+ providerId: config.provider,
15521
+ modelId: context.model,
15522
+ maxContext: tokens
15523
+ });
15524
+ updateSpinnerContext();
15525
+ }
15526
+ return effectiveMaxContext;
15527
+ },
14823
15528
  onMcp: async (args) => {
14824
15529
  const parsed = parseMcpArgs(args);
14825
15530
  if (!parsed) {
@@ -14838,11 +15543,11 @@ Restart WrongStack to load or unload plugin code in this session.`;
14838
15543
  onYolo: (setTo) => {
14839
15544
  const policy = container.resolve(TOKENS.PermissionPolicy);
14840
15545
  if (setTo !== void 0) {
14841
- policy.setYolo(setTo);
15546
+ policy.setYolo?.(setTo);
14842
15547
  config = patchConfig(config, { yolo: setTo });
14843
15548
  return setTo;
14844
15549
  }
14845
- return policy.getYolo();
15550
+ return policy.getYolo?.() ?? config.yolo ?? false;
14846
15551
  },
14847
15552
  onNextPredict: (setTo) => {
14848
15553
  if (setTo !== void 0) {
@@ -14905,7 +15610,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
14905
15610
  const { spawn: spawn3 } = await import('child_process');
14906
15611
  const cwd2 = projectRoot;
14907
15612
  const statusResult = await new Promise(
14908
- (resolve3, reject) => {
15613
+ (resolve4, reject) => {
14909
15614
  const child = spawn3("git", ["status", "--porcelain"], {
14910
15615
  cwd: cwd2,
14911
15616
  stdio: ["ignore", "pipe", "pipe"]
@@ -14915,7 +15620,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
14915
15620
  stdout += d;
14916
15621
  });
14917
15622
  child.on("error", reject);
14918
- child.on("close", (code) => resolve3({ stdout, code: code ?? 0 }));
15623
+ child.on("close", (code) => resolve4({ stdout, code: code ?? 0 }));
14919
15624
  }
14920
15625
  );
14921
15626
  if (statusResult.stdout.trim().length > 0) {
@@ -15076,7 +15781,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
15076
15781
  setStatuslineHiddenItems,
15077
15782
  getYolo: () => {
15078
15783
  const policy = container.resolve(TOKENS.PermissionPolicy);
15079
- return policy.getYolo();
15784
+ return policy.getYolo?.() ?? config.yolo ?? false;
15080
15785
  },
15081
15786
  getAutonomy: () => autonomyMode,
15082
15787
  onAutonomy: (setTo) => {