@wrongstack/cli 0.54.1 → 0.63.4

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,7 +2,7 @@
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, 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, 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';
@@ -10,21 +10,21 @@ import * as crypto2 from 'crypto';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
12
12
  import { WebSocketServer, WebSocket } from 'ws';
13
- import { MCPRegistry } from '@wrongstack/mcp';
13
+ import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
14
14
  import { capabilitiesFor, buildProviderFactoriesFromRegistry, makeProviderFromConfig } from '@wrongstack/providers';
15
15
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
16
16
  import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
17
17
  import { fileURLToPath } from 'url';
18
18
  import * as readline from 'readline';
19
- import * as fs11 from 'fs';
19
+ import * as fs12 from 'fs';
20
20
  import { writeFileSync, existsSync, readFileSync } from 'fs';
21
21
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
22
22
  import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
23
23
  import { ACP_AGENTS, SubagentBudget } from '@wrongstack/core/coordination';
24
24
  import { spawn } from 'child_process';
25
25
  import { allServers } from '@wrongstack/core/infrastructure';
26
- import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
27
26
  import { ToolExecutor } from '@wrongstack/core/execution';
27
+ import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
28
28
 
29
29
  var __defProp = Object.defineProperty;
30
30
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -2345,29 +2345,43 @@ async function resolveRuntimeMaxContext(input) {
2345
2345
  const providerConfig = input.runtimeProviderConfig ?? input.config.providers?.[input.providerId];
2346
2346
  const providerOverride = positiveNumber(readConfiguredMaxContext(providerConfig));
2347
2347
  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;
2348
+ const catalogId = providerConfig?.type && providerConfig.type !== input.providerId ? providerConfig.type : input.providerId;
2349
+ if (input.modelsRegistry) {
2350
+ const topLevelBaseUrlApplies = input.providerId === input.config.provider;
2351
+ const configuredBaseUrl = providerConfig?.baseUrl ?? (topLevelBaseUrlApplies ? input.config.baseUrl : void 0);
2352
+ let divergesFromCatalog = false;
2353
+ if (configuredBaseUrl) {
2354
+ const resolved = await safeGetProvider(input.modelsRegistry, catalogId);
2355
+ divergesFromCatalog = normalizeBaseUrl(configuredBaseUrl) !== normalizeBaseUrl(resolved?.apiBase);
2356
+ }
2357
+ if (!divergesFromCatalog) {
2358
+ const mergedModels = mergeCustomModelDefs(providerConfig?.customModels, input.config.models);
2359
+ const caps = await capabilitiesFor(
2360
+ input.modelsRegistry,
2361
+ catalogId,
2362
+ input.modelId,
2363
+ mergedModels
2364
+ ).catch(() => void 0);
2365
+ const catalogMax = positiveNumber(caps?.maxContext);
2366
+ if (catalogMax) return catalogMax;
2367
+ const directModel = await input.modelsRegistry.getModel(catalogId, input.modelId).catch(() => void 0);
2368
+ const directMax = positiveNumber(directModel?.capabilities.maxContext);
2369
+ if (directMax) return directMax;
2370
+ }
2368
2371
  }
2369
2372
  return positiveNumber(input.provider.capabilities.maxContext) ?? 0;
2370
2373
  }
2374
+ async function safeGetProvider(registry, id) {
2375
+ try {
2376
+ return await registry.getProvider(id);
2377
+ } catch {
2378
+ return void 0;
2379
+ }
2380
+ }
2381
+ function normalizeBaseUrl(url) {
2382
+ if (!url) return "";
2383
+ return url.trim().toLowerCase().replace(/\/+$/, "");
2384
+ }
2371
2385
  function readConfiguredMaxContext(providerConfig) {
2372
2386
  if (!providerConfig || typeof providerConfig !== "object") return void 0;
2373
2387
  const capabilities = providerConfig.capabilities;
@@ -2381,6 +2395,7 @@ function positiveNumber(value) {
2381
2395
  // src/arg-parser.ts
2382
2396
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2383
2397
  "yolo",
2398
+ "yolo-destructive",
2384
2399
  "force-all-yolo",
2385
2400
  "verbose",
2386
2401
  "trace",
@@ -2407,7 +2422,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2407
2422
  "autonomy",
2408
2423
  "eternal",
2409
2424
  "no-hints",
2410
- "hints"
2425
+ "hints",
2426
+ "no-hooks"
2411
2427
  ]);
2412
2428
  function parseArgs(argv) {
2413
2429
  const flags = {};
@@ -2735,10 +2751,12 @@ function buildAutonomyCommand(opts) {
2735
2751
  " auto \u2014 After each turn, agent picks the best next step and continues.",
2736
2752
  " Runs indefinitely until you press Esc or Ctrl+C.",
2737
2753
  " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
2738
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2754
+ " Force-enables regular YOLO; destructive-gated calls still use",
2755
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2739
2756
  " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
2740
2757
  " spawns N agents, awaits results, aggregates. Requires /goal.",
2741
- " Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
2758
+ " Force-enables regular YOLO; destructive-gated calls still use",
2759
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2742
2760
  "",
2743
2761
  "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
2744
2762
  "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
@@ -2896,7 +2914,7 @@ function buildAutonomyCommand(opts) {
2896
2914
  opts.onEternalStart(newMode);
2897
2915
  const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
2898
2916
  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.")}`;
2917
+ ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
2900
2918
  opts.renderer.write(msg2);
2901
2919
  return { message: msg2 };
2902
2920
  }
@@ -3373,6 +3391,11 @@ function buildContextCommand(opts) {
3373
3391
  " /context Show counts: messages, est. tokens, tool calls, todos, read files.",
3374
3392
  " /context detail As above, plus model, cwd, projectRoot, and the file list.",
3375
3393
  " /context repair Repair orphan tool_use/tool_result blocks after manual compaction.",
3394
+ " /context limit Show effective context window for this session.",
3395
+ " /context limit <tokens> Set effective context window for this session (e.g. 220k).",
3396
+ " /context limit <tokens> --persist Persist effective context window to config.",
3397
+ " /context thresholds <warn> <soft> <hard> Set compaction thresholds (percent or decimal).",
3398
+ " /context thresholds <warn> <soft> <hard> --persist Persist thresholds to config.",
3376
3399
  " /context mode List context-window modes.",
3377
3400
  " /context mode <id> Switch context-window mode for this session."
3378
3401
  ].join("\n"),
@@ -3400,6 +3423,83 @@ ${formatContextWindowModeList(active)}`;
3400
3423
  ` empty msgs: removed ${repaired.report.removedMessages}`
3401
3424
  ].join("\n") : "Context repair: no orphan tool_use/tool_result blocks found.";
3402
3425
  opts.renderer.write(`${msg2}
3426
+ `);
3427
+ return { message: msg2 };
3428
+ }
3429
+ if (trimmed === "limit") {
3430
+ const limit = readEffectiveLimit(ctx, opts);
3431
+ const msg2 = limit > 0 ? `Effective context window: ${limit.toLocaleString()} tokens` : "Effective context window: unknown (auto-compaction may be disabled).";
3432
+ opts.renderer.write(`${msg2}
3433
+ `);
3434
+ return { message: msg2 };
3435
+ }
3436
+ if (trimmed.startsWith("limit ")) {
3437
+ const persist = hasPersistFlag(trimmed);
3438
+ const raw = stripPersistFlag(trimmed.slice("limit ".length)).trim();
3439
+ const limit = parseTokenCount(raw);
3440
+ if (!limit) {
3441
+ const msg3 = `Invalid context limit "${raw}". Use a positive token count, e.g. 220k or 220000.`;
3442
+ opts.renderer.write(`${color.red(msg3)}
3443
+ `);
3444
+ return { message: msg3 };
3445
+ }
3446
+ ctx.meta["effectiveMaxContext"] = limit;
3447
+ const effective = opts.onContextLimit?.(limit) ?? limit;
3448
+ if (persist) {
3449
+ const error = await persistContextConfig(opts, { effectiveMaxContext: limit });
3450
+ if (error) {
3451
+ opts.renderer.write(`${color.red(error)}
3452
+ `);
3453
+ return { message: error };
3454
+ }
3455
+ }
3456
+ const msg2 = `${color.green("Effective context window set:")} ${effective.toLocaleString()} tokens${persist ? " (persisted)" : ""}`;
3457
+ opts.renderer.write(`${msg2}
3458
+ `);
3459
+ return { message: msg2 };
3460
+ }
3461
+ if (trimmed.startsWith("thresholds ")) {
3462
+ const persist = hasPersistFlag(trimmed);
3463
+ const thresholdArgs = stripPersistFlag(trimmed.slice("thresholds ".length)).trim();
3464
+ const parts = thresholdArgs.split(/\s+/).filter(Boolean);
3465
+ if (parts.length !== 3) {
3466
+ const msg3 = "Usage: /context thresholds <warn> <soft> <hard> (examples: 60% 75% 90% or 0.6 0.75 0.9)";
3467
+ opts.renderer.write(`${color.red(msg3)}
3468
+ `);
3469
+ return { message: msg3 };
3470
+ }
3471
+ const thresholds = parts.map(parseThreshold);
3472
+ if (thresholds.some((v) => v === null)) {
3473
+ const msg3 = "Invalid thresholds. Use percentages (60%) or decimals between 0 and 1.";
3474
+ opts.renderer.write(`${color.red(msg3)}
3475
+ `);
3476
+ return { message: msg3 };
3477
+ }
3478
+ const [warn, soft, hard] = thresholds;
3479
+ if (!(warn < soft && soft < hard)) {
3480
+ const msg3 = "Invalid thresholds: require warn < soft < hard.";
3481
+ opts.renderer.write(`${color.red(msg3)}
3482
+ `);
3483
+ return { message: msg3 };
3484
+ }
3485
+ const base = readPolicy(ctx) ?? resolveContextWindowPolicy({});
3486
+ const policy2 = { ...base, thresholds: { warn, soft, hard } };
3487
+ ctx.meta["contextWindowMode"] = policy2.id;
3488
+ ctx.meta["contextWindowPolicy"] = policy2;
3489
+ if (persist) {
3490
+ const error = await persistContextConfig(opts, {
3491
+ warnThreshold: warn,
3492
+ softThreshold: soft,
3493
+ hardThreshold: hard
3494
+ });
3495
+ if (error) {
3496
+ opts.renderer.write(`${color.red(error)}
3497
+ `);
3498
+ return { message: error };
3499
+ }
3500
+ }
3501
+ const msg2 = `${color.green("Context thresholds set:")} warn ${pct(warn)}, soft ${pct(soft)}, hard ${pct(hard)}${persist ? " (persisted)" : ""}`;
3502
+ opts.renderer.write(`${msg2}
3403
3503
  `);
3404
3504
  return { message: msg2 };
3405
3505
  }
@@ -3433,6 +3533,7 @@ ${formatContextWindowModeList(active)}`;
3433
3533
  ` messages: ${messages.length} total (${countTurnPairs(messages)} user+assistant pairs)`,
3434
3534
  ` tokens (est): ${estimateTokens(messages).toLocaleString()} (chars / 4 estimate)`,
3435
3535
  ` mode: ${policy ? `${policy.id} (${policy.name})` : "balanced"}`,
3536
+ ` limit: ${formatLimit(readEffectiveLimit(ctx, opts))}`,
3436
3537
  ` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
3437
3538
  ` tools: ${countToolUses(messages)} calls made, ${countToolResults(messages)} results in history`,
3438
3539
  ` read files: ${ctx.readFiles.size} files`,
@@ -3459,6 +3560,68 @@ function readPolicy(ctx) {
3459
3560
  const policy = ctx.meta?.["contextWindowPolicy"];
3460
3561
  return policy && typeof policy === "object" ? policy : null;
3461
3562
  }
3563
+ function hasPersistFlag(input) {
3564
+ return /(?:^|\s)--persist(?:\s|$)/.test(input);
3565
+ }
3566
+ function stripPersistFlag(input) {
3567
+ return input.replace(/(?:^|\s)--persist(?:\s|$)/g, " ").trim();
3568
+ }
3569
+ async function persistContextConfig(opts, patch) {
3570
+ if (!opts.configStore || !opts.paths) return "Cannot persist context settings: config store not available.";
3571
+ let raw = "{}";
3572
+ try {
3573
+ raw = await fsp3.readFile(opts.paths.globalConfig, "utf8");
3574
+ } catch (err) {
3575
+ if (err.code !== "ENOENT") {
3576
+ return `Could not read ${opts.paths.globalConfig}: ${err.message}`;
3577
+ }
3578
+ }
3579
+ let parsed;
3580
+ try {
3581
+ parsed = JSON.parse(raw);
3582
+ } catch (err) {
3583
+ return `Config at ${opts.paths.globalConfig} is not valid JSON: ${err.message}`;
3584
+ }
3585
+ const current = opts.configStore.get();
3586
+ const context = {
3587
+ ...current.context,
3588
+ ...parsed.context ?? {},
3589
+ ...patch
3590
+ };
3591
+ parsed.context = context;
3592
+ await atomicWrite(opts.paths.globalConfig, JSON.stringify(parsed, null, 2), { mode: 384 });
3593
+ opts.configStore.update({ context });
3594
+ return null;
3595
+ }
3596
+ function readEffectiveLimit(ctx, opts) {
3597
+ const live = opts.onContextLimit?.();
3598
+ if (typeof live === "number" && Number.isFinite(live) && live > 0) return live;
3599
+ const metaLimit = ctx.meta?.["effectiveMaxContext"];
3600
+ if (typeof metaLimit === "number" && Number.isFinite(metaLimit) && metaLimit > 0) return metaLimit;
3601
+ const providerLimit = ctx.provider?.capabilities?.maxContext;
3602
+ return typeof providerLimit === "number" && Number.isFinite(providerLimit) && providerLimit > 0 ? providerLimit : 0;
3603
+ }
3604
+ function parseTokenCount(raw) {
3605
+ const normalized = raw.trim().toLowerCase().replace(/,/g, "").replace(/_/g, "");
3606
+ const match = /^(\d+(?:\.\d+)?)([km])?$/.exec(normalized);
3607
+ if (!match) return null;
3608
+ const value = Number(match[1]);
3609
+ const unit = match[2];
3610
+ const scaled = unit === "m" ? value * 1e6 : unit === "k" ? value * 1e3 : value;
3611
+ const rounded = Math.floor(scaled);
3612
+ return Number.isFinite(rounded) && rounded > 0 ? rounded : null;
3613
+ }
3614
+ function parseThreshold(raw) {
3615
+ const s = raw.trim();
3616
+ const percent = s.endsWith("%");
3617
+ const n = Number((percent ? s.slice(0, -1) : s).trim());
3618
+ if (!Number.isFinite(n)) return null;
3619
+ const value = percent ? n / 100 : n;
3620
+ return value > 0 && value < 1 ? value : null;
3621
+ }
3622
+ function formatLimit(limit) {
3623
+ return limit > 0 ? `${limit.toLocaleString()} tokens` : "unknown";
3624
+ }
3462
3625
  function pct(n) {
3463
3626
  return `${Math.round(n * 100)}%`;
3464
3627
  }
@@ -4867,12 +5030,20 @@ function parseMcpArgs(args) {
4867
5030
  }
4868
5031
  async function runMcpManagementCommand(parsed, deps) {
4869
5032
  const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
4870
- const configured = config.mcpServers ?? {};
5033
+ const diskConfig = await readConfig(configPath2);
5034
+ const configured = isMcpServerRecord(diskConfig.mcpServers) ? diskConfig.mcpServers : config.mcpServers ?? {};
4871
5035
  switch (parsed.action) {
4872
5036
  case "list":
4873
5037
  return renderList(configured, mcpRegistry, allServerPresets);
4874
5038
  case "add":
4875
- return runAdd(parsed.name, parsed.enable ?? false, configured, configPath2, allServerPresets);
5039
+ return runAdd(
5040
+ parsed.name,
5041
+ parsed.enable ?? false,
5042
+ configured,
5043
+ configPath2,
5044
+ mcpRegistry,
5045
+ allServerPresets
5046
+ );
4876
5047
  case "remove":
4877
5048
  return runRemove(parsed.name, configured, configPath2, mcpRegistry);
4878
5049
  case "enable":
@@ -4916,34 +5087,46 @@ function renderList(configured, mcpRegistry, all) {
4916
5087
  lines.push(color.dim(" /mcp restart <name> (runtime restart)"));
4917
5088
  return lines.join("\n");
4918
5089
  }
4919
- async function runAdd(name, enable, configured, configPath2, all) {
5090
+ async function runAdd(name, enable, configured, configPath2, mcpRegistry, all) {
4920
5091
  const preset = all[name];
4921
5092
  if (!preset) {
4922
5093
  const known = Object.keys(all).join(", ");
4923
5094
  return `Unknown server "${name}". Available: ${known}`;
4924
5095
  }
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
- }
5096
+ const existing = configured[name];
5097
+ const nextCfg = existing ? { ...preset, ...existing, enabled: enable } : { ...preset, enabled: enable };
4934
5098
  const full = await readConfig(configPath2);
4935
- const mcpServers = { ...full.mcpServers ?? {}, [name]: { ...preset, enabled: enable } };
5099
+ const mcpServers = {
5100
+ ...isMcpServerRecord(full.mcpServers) ? full.mcpServers : {},
5101
+ [name]: nextCfg
5102
+ };
4936
5103
  full.mcpServers = mcpServers;
4937
5104
  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}.`;
5105
+ if (!enable) {
5106
+ const verb = existing ? "Updated" : "Added (disabled \u2014 /mcp enable to start)";
5107
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5108
+ }
5109
+ try {
5110
+ if (mcpRegistry.list().some((server) => server.name === name)) {
5111
+ await mcpRegistry.restart(name);
5112
+ } else {
5113
+ await mcpRegistry.start(nextCfg);
5114
+ }
5115
+ const verb = existing ? "Updated and started" : "Enabled and started";
5116
+ return `${color.green(verb)} "${name}" (${nextCfg.transport}). Config written to ${configPath2}.`;
5117
+ } catch (err) {
5118
+ const message = err instanceof Error ? err.message : String(err);
5119
+ return `${color.yellow("Enabled")} "${name}" in config, but failed to start: ${message}`;
5120
+ }
4940
5121
  }
4941
5122
  async function runRemove(name, configured, configPath2, mcpRegistry) {
4942
5123
  if (!configured[name]) return `Server "${name}" is not in config.`;
4943
5124
  await mcpRegistry.stop(name).catch(() => {
4944
5125
  });
4945
5126
  const full = await readConfig(configPath2);
4946
- const mcpServers = { ...full.mcpServers ?? {} };
5127
+ const mcpServers = {
5128
+ ...full.mcpServers ?? {}
5129
+ };
4947
5130
  delete mcpServers[name];
4948
5131
  full.mcpServers = mcpServers;
4949
5132
  await writeConfig(configPath2, full);
@@ -4962,7 +5145,9 @@ async function runEnable(name, configured, configPath2, mcpRegistry) {
4962
5145
  }
4963
5146
  }
4964
5147
  const full = await readConfig(configPath2);
4965
- const mcpServers = { ...full.mcpServers ?? {} };
5148
+ const mcpServers = {
5149
+ ...full.mcpServers ?? {}
5150
+ };
4966
5151
  mcpServers[name] = { ...mcpServers[name], enabled: true };
4967
5152
  full.mcpServers = mcpServers;
4968
5153
  await writeConfig(configPath2, full);
@@ -4979,7 +5164,9 @@ async function runDisable(name, configured, configPath2, mcpRegistry) {
4979
5164
  await mcpRegistry.stop(name).catch(() => {
4980
5165
  });
4981
5166
  const full = await readConfig(configPath2);
4982
- const mcpServers = { ...full.mcpServers ?? {} };
5167
+ const mcpServers = {
5168
+ ...full.mcpServers ?? {}
5169
+ };
4983
5170
  mcpServers[name] = { ...mcpServers[name], enabled: false };
4984
5171
  full.mcpServers = mcpServers;
4985
5172
  await writeConfig(configPath2, full);
@@ -5020,6 +5207,9 @@ async function readConfig(path25) {
5020
5207
  return {};
5021
5208
  }
5022
5209
  }
5210
+ function isMcpServerRecord(value) {
5211
+ return !!value && typeof value === "object" && !Array.isArray(value);
5212
+ }
5023
5213
  async function writeConfig(path25, cfg) {
5024
5214
  const raw = JSON.stringify(cfg, null, 2);
5025
5215
  const tmp = path25 + ".tmp";
@@ -6340,12 +6530,12 @@ function buildYoloCommand(opts) {
6340
6530
  help: [
6341
6531
  "Usage:",
6342
6532
  " /yolo Show current YOLO status",
6343
- " /yolo on Enable YOLO mode (auto-approve every tool call)",
6533
+ " /yolo on Enable YOLO mode (auto-approve normal project work)",
6344
6534
  " /yolo off Disable YOLO mode (restore permission prompts)",
6345
6535
  " /yolo toggle Toggle YOLO mode",
6346
6536
  "",
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."
6537
+ "YOLO mode auto-approves normal in-project tool calls, including simple shell commands.",
6538
+ "Clearly destructive calls may still ask unless --yolo-destructive is enabled."
6349
6539
  ].join("\n"),
6350
6540
  async run(args) {
6351
6541
  const arg = args.trim().toLowerCase();
@@ -6356,7 +6546,7 @@ function buildYoloCommand(opts) {
6356
6546
  }
6357
6547
  if (!arg) {
6358
6548
  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)")}`;
6549
+ const status = current ? `${color.yellow("ON")} ${color.dim("(auto-approving normal project work)")}` : `${color.green("OFF")} ${color.dim("(permission prompts active)")}`;
6360
6550
  const msg2 = `YOLO mode: ${status}`;
6361
6551
  opts.renderer.write(msg2);
6362
6552
  return { message: msg2 };
@@ -6374,7 +6564,7 @@ function buildYoloCommand(opts) {
6374
6564
  return { message: msg2 };
6375
6565
  }
6376
6566
  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`;
6567
+ const label = newState ? `${color.yellow("ENABLED")} \u2014 normal project tool calls will be auto-approved` : `${color.green("DISABLED")} \u2014 permission prompts are active`;
6378
6568
  const msg = `YOLO mode: ${label}`;
6379
6569
  opts.renderer.write(msg);
6380
6570
  return { message: msg };
@@ -6581,7 +6771,7 @@ async function runLaunchPrompts(opts) {
6581
6771
  yolo = yoloPinned;
6582
6772
  } else {
6583
6773
  const answer = (await reader.readLine(
6584
- ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n/q]")} `
6774
+ ` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve normal project work)")} ${color.dim("[Y/n/q]")} `
6585
6775
  )).trim().toLowerCase();
6586
6776
  if (answer === "q") {
6587
6777
  renderer.write(color.dim(" Goodbye!\n"));
@@ -7138,11 +7328,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
7138
7328
  var theme = { primary: color.amber };
7139
7329
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
7140
7330
  try {
7141
- const { atomicWrite: atomicWrite12 } = await import('@wrongstack/core');
7142
- const fs24 = await import('fs/promises');
7331
+ const { atomicWrite: atomicWrite13 } = await import('@wrongstack/core');
7332
+ const fs25 = await import('fs/promises');
7143
7333
  let existing = {};
7144
7334
  try {
7145
- const raw = await fs24.readFile(configPath2, "utf8");
7335
+ const raw = await fs25.readFile(configPath2, "utf8");
7146
7336
  existing = JSON.parse(raw);
7147
7337
  } catch {
7148
7338
  }
@@ -7150,7 +7340,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
7150
7340
  existing.provider = provider;
7151
7341
  existing.model = model;
7152
7342
  await backupCurrent(homeFn);
7153
- await atomicWrite12(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7343
+ await atomicWrite13(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7154
7344
  try {
7155
7345
  await appendHistory(
7156
7346
  oldCfg,
@@ -7440,7 +7630,7 @@ var GROUPS = [
7440
7630
  items: [
7441
7631
  { key: "/mode", blurb: "switch persona: code-reviewer, debugger, architect, tester, devops, \u2026" },
7442
7632
  { 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" },
7633
+ { key: "/yolo on|off|toggle", blurb: "auto-approve normal project work without restart" },
7444
7634
  { key: "/context mode frugal|balanced|deep|archival", blurb: "pick how aggressively history is trimmed" },
7445
7635
  { key: "/compact", blurb: "manually compact the in-flight context window" },
7446
7636
  { key: "/plan show|add|start|done", blurb: "strategic roadmap, survives /resume across sessions" }
@@ -7473,12 +7663,12 @@ function pickGroupIndex(opts) {
7473
7663
  try {
7474
7664
  let current = 0;
7475
7665
  try {
7476
- const parsed = Number.parseInt(fs11.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7666
+ const parsed = Number.parseInt(fs12.readFileSync(opts.cursorFile, "utf8").trim(), 10);
7477
7667
  if (Number.isFinite(parsed)) current = wrap(parsed);
7478
7668
  } catch {
7479
7669
  }
7480
- fs11.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7481
- fs11.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7670
+ fs12.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
7671
+ fs12.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
7482
7672
  return current;
7483
7673
  } catch {
7484
7674
  }
@@ -9016,9 +9206,160 @@ var initCmd = async (_args, deps) => {
9016
9206
  deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
9017
9207
  return 0;
9018
9208
  };
9209
+ var AllowAllPermissionPolicy = class extends AutoApprovePermissionPolicy {
9210
+ async evaluate() {
9211
+ return { permission: "auto", source: "default" };
9212
+ }
9213
+ };
9214
+ function parseToolsFlag(flags) {
9215
+ const raw = flags["tools"];
9216
+ if (typeof raw !== "string") return null;
9217
+ const set = new Set(
9218
+ raw.split(",").map((s) => s.trim()).filter(Boolean)
9219
+ );
9220
+ return set.size > 0 ? set : null;
9221
+ }
9222
+ function makeServeContext(cwd, projectRoot, signal) {
9223
+ const provider = {
9224
+ id: "mcp-serve",
9225
+ capabilities: { maxContext: 0 },
9226
+ complete: async () => {
9227
+ throw new Error("no model provider in `mcp serve` mode");
9228
+ },
9229
+ stream: () => {
9230
+ throw new Error("no model provider in `mcp serve` mode");
9231
+ }
9232
+ };
9233
+ const session = { append: async () => {
9234
+ } };
9235
+ const tokenCounter = {
9236
+ account: () => {
9237
+ },
9238
+ total: () => ({ input: 0, output: 0 }),
9239
+ estimateCost: () => ({ total: 0 })
9240
+ };
9241
+ return new Context({
9242
+ systemPrompt: [],
9243
+ provider,
9244
+ session,
9245
+ signal,
9246
+ tokenCounter,
9247
+ cwd,
9248
+ projectRoot,
9249
+ model: "mcp-serve",
9250
+ tools: []
9251
+ });
9252
+ }
9253
+ async function selectExposedTools(registry, ctx, policy, whitelist) {
9254
+ const allowed = [];
9255
+ for (const tool of registry.list()) {
9256
+ if (whitelist && !whitelist.has(tool.name)) continue;
9257
+ const decision = await policy.evaluate(tool, {}, ctx);
9258
+ if (decision.permission === "auto") allowed.push(tool);
9259
+ }
9260
+ return allowed;
9261
+ }
9262
+ async function serveMcpStdio(deps) {
9263
+ const flags = deps.flags ?? {};
9264
+ const yolo = flags["yolo"] === true || flags["allow-all"] === true;
9265
+ const whitelist = parseToolsFlag(flags);
9266
+ const log = (m) => process.stderr.write(`${m}
9267
+ `);
9268
+ let registry = deps.toolRegistry;
9269
+ if (!registry) {
9270
+ registry = new ToolRegistry();
9271
+ registry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
9272
+ }
9273
+ const controller = new AbortController();
9274
+ const ctx = makeServeContext(deps.cwd, deps.projectRoot, controller.signal);
9275
+ const permissionPolicy = yolo ? new AllowAllPermissionPolicy() : new AutoApprovePermissionPolicy();
9276
+ const executor = new ToolExecutor(registry, {
9277
+ permissionPolicy,
9278
+ secretScrubber: new DefaultSecretScrubber(),
9279
+ perIterationOutputCapBytes: 1e6
9280
+ });
9281
+ const allowed = await selectExposedTools(registry, ctx, permissionPolicy, whitelist);
9282
+ const allowedNames = new Set(allowed.map((t) => t.name));
9283
+ if (allowed.length === 0) {
9284
+ log(
9285
+ "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."
9286
+ );
9287
+ }
9288
+ let counter = 0;
9289
+ const host = {
9290
+ listTools: () => allowed.map((t) => ({
9291
+ name: t.name,
9292
+ description: t.description,
9293
+ inputSchema: t.inputSchema ?? { type: "object" }
9294
+ })),
9295
+ callTool: async (name, callArgs) => {
9296
+ if (!allowedNames.has(name)) {
9297
+ return { content: `Tool "${name}" is not exposed by this server`, isError: true };
9298
+ }
9299
+ const use = {
9300
+ type: "tool_use",
9301
+ id: `srv_${++counter}`,
9302
+ name,
9303
+ input: callArgs
9304
+ };
9305
+ const batch = await executor.executeBatch([use], ctx, "sequential");
9306
+ const result = batch.outputs[0]?.result;
9307
+ if (!result || result.type === "tool_confirm_pending") {
9308
+ return {
9309
+ content: `Tool "${name}" requires interactive confirmation, which is unavailable over MCP`,
9310
+ isError: true
9311
+ };
9312
+ }
9313
+ return { content: result.content, isError: Boolean(result.is_error) };
9314
+ }
9315
+ };
9316
+ const server = new MCPServer({
9317
+ host,
9318
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9319
+ });
9320
+ const mode = yolo ? "yolo: all tools" : "safe: read-only tools";
9321
+ if (flags["http"] === true || typeof flags["http"] === "string" || flags["port"] || flags["host"]) {
9322
+ const port = Number(flags["port"] ?? flags["http"] ?? 0) || 0;
9323
+ const httpHost = typeof flags["host"] === "string" ? flags["host"] : "127.0.0.1";
9324
+ const token = typeof flags["token"] === "string" ? flags["token"] : void 0;
9325
+ let handle2;
9326
+ try {
9327
+ handle2 = await serveHttp(server, {
9328
+ port,
9329
+ host: httpHost,
9330
+ token,
9331
+ logger: { warn: (m) => log(`[mcp-serve] ${m}`) }
9332
+ });
9333
+ } catch (err) {
9334
+ log(`wrongstack MCP server: ${err instanceof Error ? err.message : String(err)}`);
9335
+ return 1;
9336
+ }
9337
+ log(
9338
+ `wrongstack MCP server ready at ${handle2.url} \u2014 exposing ${allowed.length} tool(s) (${mode})${token ? " [token auth]" : ""}.`
9339
+ );
9340
+ await new Promise((resolve3) => {
9341
+ const stop = () => resolve3();
9342
+ process.once("SIGINT", stop);
9343
+ process.once("SIGTERM", stop);
9344
+ });
9345
+ await handle2.close();
9346
+ controller.abort();
9347
+ return 0;
9348
+ }
9349
+ log(`wrongstack MCP server ready on stdio \u2014 exposing ${allowed.length} tool(s) (${mode}).`);
9350
+ const handle = serveStdio(server);
9351
+ await handle.done;
9352
+ controller.abort();
9353
+ return 0;
9354
+ }
9355
+
9356
+ // src/subcommands/handlers/mcp.ts
9019
9357
  var BUILT_IN_MCP = allServers();
9020
9358
  var mcpCmd = async (args, deps) => {
9021
9359
  const sub = args[0];
9360
+ if (sub === "serve") {
9361
+ return serveMcpStdio(deps);
9362
+ }
9022
9363
  if (!sub || sub === "list") {
9023
9364
  const servers = deps.config.mcpServers ?? {};
9024
9365
  if (Object.keys(servers).length === 0) {
@@ -10266,10 +10607,10 @@ var auditCmd = async (args, deps) => {
10266
10607
  return verify.ok ? 0 : 1;
10267
10608
  };
10268
10609
  async function listAudits(log, dir, deps) {
10269
- const fs24 = await import('fs/promises');
10610
+ const fs25 = await import('fs/promises');
10270
10611
  let entries;
10271
10612
  try {
10272
- entries = await fs24.readdir(dir);
10613
+ entries = await fs25.readdir(dir);
10273
10614
  } catch {
10274
10615
  deps.renderer.write(
10275
10616
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -10356,7 +10697,15 @@ var helpCmd = async (_args, deps) => {
10356
10697
  " wstack doctor Health checks",
10357
10698
  " wstack export <id> [opts] Render a session",
10358
10699
  " wstack usage Token + cost summary",
10359
- " wstack version Print version"
10700
+ " wstack version Print version",
10701
+ "",
10702
+ color.bold("Common flags"),
10703
+ " --yolo Auto-approve normal in-project tool calls",
10704
+ " --yolo-destructive Also auto-approve clearly destructive YOLO-gated calls",
10705
+ " --force-all-yolo Deprecated alias for --yolo-destructive",
10706
+ " --tui / --no-tui Force or disable TUI mode",
10707
+ ' --eternal "<mission>" Start an eternal-autonomy loop',
10708
+ " --no-hints Hide launch hints"
10360
10709
  ];
10361
10710
  deps.renderer.write(lines.join("\n") + "\n");
10362
10711
  return 0;
@@ -10529,7 +10878,8 @@ async function boot(argv) {
10529
10878
  vault,
10530
10879
  cwd,
10531
10880
  projectRoot,
10532
- userHome
10881
+ userHome,
10882
+ flags
10533
10883
  });
10534
10884
  await reader.close();
10535
10885
  return code;
@@ -10647,6 +10997,20 @@ async function boot(argv) {
10647
10997
  updateInfo
10648
10998
  };
10649
10999
  }
11000
+ var CONTEXT_OVERFLOW_RE = /context window|exceeds the context|too many tokens|context.*tokens/i;
11001
+ function contextOverflowHint(err) {
11002
+ const structured = err.code === ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW || err.code === ERROR_CODES.AGENT_CONTEXT_OVERFLOW;
11003
+ const textual = CONTEXT_OVERFLOW_RE.test(`${err.message}
11004
+ ${err.describe()}`);
11005
+ if (!structured && !textual) return null;
11006
+ return [
11007
+ "Provider rejected the request as over its effective context window.",
11008
+ "If you use a custom baseUrl/proxy, the real limit may be lower than models.dev reports.",
11009
+ "Try: /context limit 220k",
11010
+ "Then, if needed: /context thresholds 50% 70% 85%",
11011
+ "Persistent config: set context.effectiveMaxContext."
11012
+ ].join("\n");
11013
+ }
10650
11014
  function fmtElapsed(ms) {
10651
11015
  const s = Math.floor(ms / 1e3);
10652
11016
  if (s < 60) return `${s}s`;
@@ -11238,6 +11602,8 @@ ${taskList}`;
11238
11602
  if (err) {
11239
11603
  const tag = err.recoverable ? " (recoverable)" : "";
11240
11604
  opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
11605
+ const hint = contextOverflowHint(err);
11606
+ if (hint) opts.renderer.writeWarning(hint);
11241
11607
  } else {
11242
11608
  opts.renderer.writeError("Failed.");
11243
11609
  }
@@ -11679,6 +12045,8 @@ async function execute(deps) {
11679
12045
  if (err) {
11680
12046
  const tag = err.recoverable ? " (recoverable)" : "";
11681
12047
  renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
12048
+ const hint = contextOverflowHint(err);
12049
+ if (hint) renderer.writeWarning(hint);
11682
12050
  } else {
11683
12051
  renderer.writeError("Failed.");
11684
12052
  }
@@ -13305,6 +13673,10 @@ async function setupCompaction(params) {
13305
13673
  providerId: config.provider ?? provider.id,
13306
13674
  modelId: config.model ?? context.model
13307
13675
  });
13676
+ const initialPolicy = resolveContextWindowPolicy(config.context);
13677
+ context.meta ??= {};
13678
+ context.meta["contextWindowMode"] = initialPolicy.id;
13679
+ context.meta["contextWindowPolicy"] = initialPolicy;
13308
13680
  let autoCompactor;
13309
13681
  if (config.context.autoCompact !== false && effectiveMaxContext > 0) {
13310
13682
  const auditLevel = resolveAuditLevel(fullConfig ?? config);
@@ -13315,15 +13687,15 @@ async function setupCompaction(params) {
13315
13687
  // Calibrated estimator: recordActualUsage() is called after each API
13316
13688
  // response so this converges on real token counts for compaction decisions.
13317
13689
  (ctx) => estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
13690
+ initialPolicy.thresholds,
13318
13691
  {
13319
- warn: config.context.warnThreshold,
13320
- soft: config.context.softThreshold,
13321
- hard: config.context.hardThreshold
13322
- },
13323
- {
13324
- aggressiveOn: "soft",
13692
+ aggressiveOn: initialPolicy.aggressiveOn,
13325
13693
  failureMode: "throw_on_hard",
13326
13694
  events,
13695
+ policyProvider: (ctx) => {
13696
+ const policy = ctx.meta?.["contextWindowPolicy"];
13697
+ return policy && typeof policy === "object" ? policy : null;
13698
+ },
13327
13699
  sessionBridge
13328
13700
  }
13329
13701
  );
@@ -13342,7 +13714,8 @@ function createAgent(params) {
13342
13714
  confirmAwaiter: params.confirmAwaiter,
13343
13715
  iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
13344
13716
  perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
13345
- tracer: params.tracer
13717
+ tracer: params.tracer,
13718
+ hookRunner: params.hookRunner
13346
13719
  });
13347
13720
  return new Agent({
13348
13721
  container: params.container,
@@ -13360,6 +13733,139 @@ function createAgent(params) {
13360
13733
  tracer: params.tracer
13361
13734
  });
13362
13735
  }
13736
+ function parseModelRef(ref) {
13737
+ const trimmed = ref.trim();
13738
+ const slash = trimmed.indexOf("/");
13739
+ if (slash !== -1) {
13740
+ return {
13741
+ provider: trimmed.slice(0, slash) || void 0,
13742
+ model: trimmed.slice(slash + 1).trim()
13743
+ };
13744
+ }
13745
+ const parts = trimmed.split(/\s+/);
13746
+ if (parts.length >= 2) {
13747
+ return { provider: parts[0], model: parts.slice(1).join(" ") };
13748
+ }
13749
+ return { model: trimmed };
13750
+ }
13751
+ function overloadStatus(err) {
13752
+ if (!(err instanceof ProviderError)) return null;
13753
+ const s = err.status;
13754
+ if (s === 429 || s === 529 || s >= 500) return s;
13755
+ return null;
13756
+ }
13757
+ function createFallbackModelExtension(deps) {
13758
+ const initial = deps.getConfig().fallbackModels ?? [];
13759
+ if (initial.length === 0) return null;
13760
+ let dirty = false;
13761
+ return {
13762
+ name: "fallback-model",
13763
+ beforeRun: (ctx) => {
13764
+ if (!dirty) return;
13765
+ const cfg = deps.getConfig();
13766
+ try {
13767
+ ctx.provider = deps.buildProvider(cfg.provider);
13768
+ ctx.model = cfg.model;
13769
+ deps.onModelSwitch?.(cfg.provider, cfg.model);
13770
+ } catch (err) {
13771
+ deps.logger.warn(
13772
+ `fallback-model: could not restore primary "${cfg.provider}/${cfg.model}": ${err instanceof Error ? err.message : String(err)}`
13773
+ );
13774
+ }
13775
+ dirty = false;
13776
+ },
13777
+ wrapProviderRunner: async (ctx, request, inner) => {
13778
+ try {
13779
+ return await inner(ctx, request);
13780
+ } catch (firstErr) {
13781
+ let lastErr = firstErr;
13782
+ const cfg = deps.getConfig();
13783
+ const chain = cfg.fallbackModels ?? [];
13784
+ for (const ref of chain) {
13785
+ const status = overloadStatus(lastErr);
13786
+ if (status === null) break;
13787
+ const parsed = parseModelRef(ref);
13788
+ if (!parsed.model) continue;
13789
+ const targetProviderId = parsed.provider ?? cfg.provider;
13790
+ const from = { providerId: ctx.provider.id, model: ctx.model };
13791
+ let nextProvider;
13792
+ try {
13793
+ nextProvider = deps.buildProvider(targetProviderId);
13794
+ } catch (err) {
13795
+ deps.logger.warn(
13796
+ `fallback-model: skipping "${ref}" \u2014 cannot build provider "${targetProviderId}": ${err instanceof Error ? err.message : String(err)}`
13797
+ );
13798
+ continue;
13799
+ }
13800
+ const providerSwitched = nextProvider.id !== from.providerId;
13801
+ ctx.provider = nextProvider;
13802
+ ctx.model = parsed.model;
13803
+ request.model = parsed.model;
13804
+ dirty = true;
13805
+ deps.onModelSwitch?.(targetProviderId, parsed.model);
13806
+ deps.events.emit("provider.fallback", {
13807
+ from,
13808
+ to: { providerId: nextProvider.id, model: parsed.model },
13809
+ status,
13810
+ providerSwitched
13811
+ });
13812
+ try {
13813
+ return await inner(ctx, request);
13814
+ } catch (err) {
13815
+ lastErr = err;
13816
+ }
13817
+ }
13818
+ throw lastErr;
13819
+ }
13820
+ }
13821
+ };
13822
+ }
13823
+
13824
+ // src/hooks-wiring.ts
13825
+ var HookBlockedError = class extends Error {
13826
+ constructor(reason) {
13827
+ super(`Prompt blocked by hook: ${reason}`);
13828
+ this.name = "HookBlockedError";
13829
+ }
13830
+ };
13831
+ function createUserPromptSubmitMiddleware(hookRunner) {
13832
+ return {
13833
+ name: "UserPromptSubmitHooks",
13834
+ handler: async (payload, next) => {
13835
+ const prompt = payload.text;
13836
+ if (prompt && hookRunner.has("UserPromptSubmit")) {
13837
+ const r = await hookRunner.userPromptSubmit(prompt, payload.ctx);
13838
+ if (r.block) throw new HookBlockedError(r.reason ?? "no reason given");
13839
+ if (r.additionalContext) {
13840
+ const block = { type: "text", text: r.additionalContext };
13841
+ payload.content = [...payload.content, block];
13842
+ payload.text = `${prompt}
13843
+
13844
+ ${r.additionalContext}`;
13845
+ }
13846
+ }
13847
+ return next(payload);
13848
+ }
13849
+ };
13850
+ }
13851
+ function createLifecycleHooksExtension(hookRunner) {
13852
+ let started = false;
13853
+ return {
13854
+ name: "lifecycle-hooks",
13855
+ beforeRun: async (ctx) => {
13856
+ if (started) return;
13857
+ started = true;
13858
+ if (!hookRunner.has("SessionStart")) return;
13859
+ const r = await hookRunner.sessionStart(ctx);
13860
+ if (r.additionalContext) {
13861
+ ctx.systemPrompt.push({ type: "text", text: r.additionalContext });
13862
+ }
13863
+ },
13864
+ afterRun: async (ctx) => {
13865
+ if (hookRunner.has("Stop")) await hookRunner.stop(ctx);
13866
+ }
13867
+ };
13868
+ }
13363
13869
  function setupMetrics(params) {
13364
13870
  const { flags, wpaths, events, logger, config } = params;
13365
13871
  let metricsSink;
@@ -13475,7 +13981,8 @@ async function setupPlugins(params) {
13475
13981
  skillLoader,
13476
13982
  configStore,
13477
13983
  pipelines,
13478
- paths
13984
+ paths,
13985
+ hookRegistry
13479
13986
  } = params;
13480
13987
  const builtinPlugins = [];
13481
13988
  const disabledBuiltins = new Set(
@@ -13540,6 +14047,7 @@ async function setupPlugins(params) {
13540
14047
  config: pluginConfig,
13541
14048
  log,
13542
14049
  extensions: agent.extensions,
14050
+ hookRegistry,
13543
14051
  sessionWriter: {
13544
14052
  transcriptPath: sessionWriter.transcriptPath,
13545
14053
  append: (e) => sessionWriter.append(e)
@@ -13785,10 +14293,8 @@ async function launchEternalFromFlag(deps) {
13785
14293
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
13786
14294
  } : emptyGoal2(eternalFlag);
13787
14295
  await saveGoal2(goalPath, next);
13788
- const policy = deps.container.resolve(
13789
- TOKENS.PermissionPolicy
13790
- );
13791
- policy.setYolo(true);
14296
+ const policy = deps.container.resolve(TOKENS.PermissionPolicy);
14297
+ policy.setYolo?.(true);
13792
14298
  deps.configRef.current = patchConfig(deps.configRef.current, { yolo: true });
13793
14299
  const compactor = deps.container.resolve(TOKENS.Compactor);
13794
14300
  const engine = new EternalAutonomyEngine({
@@ -13851,7 +14357,7 @@ async function main(argv) {
13851
14357
  modelsRegistry,
13852
14358
  permission: {
13853
14359
  yolo: config.yolo,
13854
- forceAllYolo: flags["force-all-yolo"] === true,
14360
+ yoloDestructive: flags["yolo-destructive"] === true || flags["force-all-yolo"] === true,
13855
14361
  promptDelegate: makePromptDelegate(reader)
13856
14362
  },
13857
14363
  compactor: {
@@ -14121,6 +14627,19 @@ async function main(argv) {
14121
14627
  });
14122
14628
  });
14123
14629
  const pipelines = setupPipelines({ events, logger });
14630
+ const hooksEnabled = flags["no-hooks"] !== true;
14631
+ const hookRegistry = new HookRegistry();
14632
+ if (hooksEnabled) hookRegistry.loadShellHooks(config.hooks);
14633
+ container.bind(TOKENS.HookRegistry, () => hookRegistry);
14634
+ const hookRunner = new HookRunner({
14635
+ registry: hookRegistry,
14636
+ logger,
14637
+ allowShell: hooksEnabled,
14638
+ sessionId: () => session.id
14639
+ });
14640
+ if (hooksEnabled) {
14641
+ pipelines.userInput.use(createUserPromptSubmitMiddleware(hookRunner));
14642
+ }
14124
14643
  const compactor = container.resolve(TOKENS.Compactor);
14125
14644
  const compactionSetup = await setupCompaction({
14126
14645
  compactor,
@@ -14167,8 +14686,12 @@ async function main(argv) {
14167
14686
  pipelines,
14168
14687
  context,
14169
14688
  config,
14170
- confirmAwaiter: makeConfirmAwaiter(reader)
14689
+ confirmAwaiter: makeConfirmAwaiter(reader),
14690
+ hookRunner
14171
14691
  });
14692
+ if (hooksEnabled) {
14693
+ agent.extensions.register(createLifecycleHooksExtension(hookRunner));
14694
+ }
14172
14695
  const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
14173
14696
  if (config.features.mcp) {
14174
14697
  for (const cfg of Object.values(config.mcpServers ?? {})) {
@@ -14196,24 +14719,41 @@ async function main(argv) {
14196
14719
  healthRegistry,
14197
14720
  skillLoader: config.features.skills ? skillLoader : void 0,
14198
14721
  configStore,
14199
- paths: wpaths
14722
+ paths: wpaths,
14723
+ hookRegistry
14200
14724
  });
14725
+ const resolveProviderCfg = (providerId) => {
14726
+ const savedCfg = config.providers?.[providerId];
14727
+ const resolvedProviderId = savedCfg?.type ?? providerId;
14728
+ const cfgWithType = {
14729
+ ...savedCfg ?? { type: providerId, apiKey: config.apiKey, baseUrl: config.baseUrl },
14730
+ type: resolvedProviderId
14731
+ };
14732
+ return { resolvedProviderId, cfgWithType };
14733
+ };
14734
+ const buildProviderForId = (providerId) => {
14735
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14736
+ return config.features.modelsRegistry && providerRegistry.has(resolvedProviderId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(resolvedProviderId, cfgWithType);
14737
+ };
14738
+ const refreshMaxContextFor = (providerId, modelId) => {
14739
+ const { resolvedProviderId, cfgWithType } = resolveProviderCfg(providerId);
14740
+ void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14741
+ };
14742
+ const fallbackExtension = createFallbackModelExtension({
14743
+ getConfig: () => config,
14744
+ buildProvider: buildProviderForId,
14745
+ onModelSwitch: refreshMaxContextFor,
14746
+ events,
14747
+ logger
14748
+ });
14749
+ if (fallbackExtension) agent.extensions.register(fallbackExtension);
14201
14750
  const switchProviderAndModel = (providerId, modelId) => {
14202
14751
  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;
14752
+ context.provider = buildProviderForId(providerId);
14213
14753
  context.model = modelId;
14214
14754
  config = patchConfig(config, { provider: providerId, model: modelId });
14215
14755
  configStore.update({ provider: providerId, model: modelId });
14216
- void refreshMaxContext(resolvedProviderId, modelId, cfgWithType);
14756
+ refreshMaxContextFor(providerId, modelId);
14217
14757
  return null;
14218
14758
  } catch (err) {
14219
14759
  return err instanceof Error ? err.message : String(err);
@@ -14820,6 +15360,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
14820
15360
  }
14821
15361
  return result.message;
14822
15362
  },
15363
+ onContextLimit: (tokens) => {
15364
+ if (typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0) {
15365
+ effectiveMaxContext = tokens;
15366
+ context.provider.capabilities.maxContext = tokens;
15367
+ context.meta["effectiveMaxContext"] = tokens;
15368
+ autoCompactor?.setMaxContext(tokens);
15369
+ events.emit("ctx.max_context", {
15370
+ providerId: config.provider,
15371
+ modelId: context.model,
15372
+ maxContext: tokens
15373
+ });
15374
+ updateSpinnerContext();
15375
+ }
15376
+ return effectiveMaxContext;
15377
+ },
14823
15378
  onMcp: async (args) => {
14824
15379
  const parsed = parseMcpArgs(args);
14825
15380
  if (!parsed) {
@@ -14838,11 +15393,11 @@ Restart WrongStack to load or unload plugin code in this session.`;
14838
15393
  onYolo: (setTo) => {
14839
15394
  const policy = container.resolve(TOKENS.PermissionPolicy);
14840
15395
  if (setTo !== void 0) {
14841
- policy.setYolo(setTo);
15396
+ policy.setYolo?.(setTo);
14842
15397
  config = patchConfig(config, { yolo: setTo });
14843
15398
  return setTo;
14844
15399
  }
14845
- return policy.getYolo();
15400
+ return policy.getYolo?.() ?? config.yolo ?? false;
14846
15401
  },
14847
15402
  onNextPredict: (setTo) => {
14848
15403
  if (setTo !== void 0) {
@@ -15076,7 +15631,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
15076
15631
  setStatuslineHiddenItems,
15077
15632
  getYolo: () => {
15078
15633
  const policy = container.resolve(TOKENS.PermissionPolicy);
15079
- return policy.getYolo();
15634
+ return policy.getYolo?.() ?? config.yolo ?? false;
15080
15635
  },
15081
15636
  getAutonomy: () => autonomyMode,
15082
15637
  onAutonomy: (setTo) => {