@wrongstack/cli 0.109.1 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, EventBus, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, loadTasks, emptyTaskFile, saveTasks, formatTaskProgress, formatTaskList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, InputBuilder, FsError } from '@wrongstack/core';
2
+ import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, EventBus, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, loadTasks, emptyTaskFile, saveTasks, formatTaskProgress, formatTaskList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, InputBuilder, FsError, estimateMessageTokens } from '@wrongstack/core';
3
3
  import * as fsp4 from 'fs/promises';
4
4
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
5
- import * as path8 from 'path';
5
+ import * as path9 from 'path';
6
6
  import { join } from 'path';
7
7
  import { createRequire } from 'module';
8
8
  import * as os2 from 'os';
@@ -18,7 +18,7 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
18
18
  import { builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes } from '@wrongstack/tools';
19
19
  import { fileURLToPath } from 'url';
20
20
  import * as readline from 'readline';
21
- import * as fs13 from 'fs';
21
+ import * as fs14 from 'fs';
22
22
  import { writeFileSync, existsSync, readFileSync } from 'fs';
23
23
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
24
24
  import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
@@ -386,7 +386,7 @@ async function findSpec(store, idOrTitle) {
386
386
  async function gatherProjectContext2(projectRoot) {
387
387
  const parts = [];
388
388
  try {
389
- const pkgPath = path8.join(projectRoot, "package.json");
389
+ const pkgPath = path9.join(projectRoot, "package.json");
390
390
  const pkgRaw = await fsp4.readFile(pkgPath, "utf8");
391
391
  const pkg = JSON.parse(pkgRaw);
392
392
  parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
@@ -402,13 +402,13 @@ async function gatherProjectContext2(projectRoot) {
402
402
  } catch {
403
403
  }
404
404
  try {
405
- const tsconfigPath = path8.join(projectRoot, "tsconfig.json");
405
+ const tsconfigPath = path9.join(projectRoot, "tsconfig.json");
406
406
  await fsp4.access(tsconfigPath);
407
407
  parts.push("Language: TypeScript");
408
408
  } catch {
409
409
  }
410
410
  try {
411
- const srcDir = path8.join(projectRoot, "src");
411
+ const srcDir = path9.join(projectRoot, "src");
412
412
  const entries = await fsp4.readdir(srcDir, { withFileTypes: true });
413
413
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
414
414
  if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
@@ -1602,7 +1602,7 @@ __export(update_check_exports, {
1602
1602
  getUpdateNotification: () => getUpdateNotification
1603
1603
  });
1604
1604
  function cachePath(homeFn = defaultHomeDir2) {
1605
- return path8.join(homeFn(), ".wrongstack", "update-cache.json");
1605
+ return path9.join(homeFn(), ".wrongstack", "update-cache.json");
1606
1606
  }
1607
1607
  function currentVersion() {
1608
1608
  const req2 = createRequire(import.meta.url);
@@ -1639,7 +1639,7 @@ async function readCache(homeFn = defaultHomeDir2) {
1639
1639
  }
1640
1640
  async function writeCache(entry, homeFn = defaultHomeDir2) {
1641
1641
  try {
1642
- const dir = path8.dirname(cachePath(homeFn));
1642
+ const dir = path9.dirname(cachePath(homeFn));
1643
1643
  await fsp4.mkdir(dir, { recursive: true });
1644
1644
  await fsp4.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
1645
1645
  } catch {
@@ -1746,7 +1746,7 @@ async function runWebUI(opts) {
1746
1746
  try {
1747
1747
  const requireFromHere = createRequire(import.meta.url);
1748
1748
  const serverEntry = requireFromHere.resolve("@wrongstack/webui/server");
1749
- const distDir = path8.resolve(path8.dirname(serverEntry), "..");
1749
+ const distDir = path9.resolve(path9.dirname(serverEntry), "..");
1750
1750
  httpServer = createHttpServer({ host, distDir, wsPort });
1751
1751
  const openUrl = `http://${host}:${httpPort}`;
1752
1752
  httpServer?.listen(httpPort, host, () => {
@@ -1763,7 +1763,7 @@ async function runWebUI(opts) {
1763
1763
  `[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}.`
1764
1764
  );
1765
1765
  }
1766
- const registryBaseDir = opts.globalConfigPath ? path8.dirname(opts.globalConfigPath) : void 0;
1766
+ const registryBaseDir = opts.globalConfigPath ? path9.dirname(opts.globalConfigPath) : void 0;
1767
1767
  if (opts.projectRoot) {
1768
1768
  void registerInstance(
1769
1769
  {
@@ -1772,7 +1772,7 @@ async function runWebUI(opts) {
1772
1772
  wsPort,
1773
1773
  host,
1774
1774
  projectRoot: opts.projectRoot,
1775
- projectName: path8.basename(opts.projectRoot) || opts.projectRoot,
1775
+ projectName: path9.basename(opts.projectRoot) || opts.projectRoot,
1776
1776
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1777
1777
  url: `http://${host}:${httpPort}`
1778
1778
  },
@@ -2083,8 +2083,7 @@ async function runWebUI(opts) {
2083
2083
  ws.close();
2084
2084
  }
2085
2085
  clients.clear();
2086
- void unregisterInstance(process.pid, registryBaseDir).catch(() => {
2087
- });
2086
+ void unregisterInstance(process.pid, registryBaseDir).catch((err) => console.debug(`[webui-server] unregister failed: ${err}`));
2088
2087
  httpServer?.close();
2089
2088
  wss.close(() => {
2090
2089
  console.log("[WebUI] Server stopped");
@@ -2160,6 +2159,15 @@ async function runWebUI(opts) {
2160
2159
  await handleProviderRemove(ws, m.payload.providerId);
2161
2160
  break;
2162
2161
  }
2162
+ default:
2163
+ send(ws, {
2164
+ type: "error",
2165
+ payload: {
2166
+ phase: "ws.dispatch",
2167
+ message: `Unknown message type: ${String(msg.type)}`
2168
+ }
2169
+ });
2170
+ break;
2163
2171
  }
2164
2172
  }
2165
2173
  async function handleUserMessage(ws, _client, content) {
@@ -2400,7 +2408,7 @@ async function runWebUI(opts) {
2400
2408
  }
2401
2409
  }
2402
2410
  function getVault() {
2403
- const keyFile = path8.join(path8.dirname(opts.globalConfigPath ?? ""), ".key");
2411
+ const keyFile = path9.join(path9.dirname(opts.globalConfigPath ?? ""), ".key");
2404
2412
  return new DefaultSecretVault({ keyFile });
2405
2413
  }
2406
2414
  async function loadSavedProviders() {
@@ -2701,10 +2709,10 @@ async function detectPackageManager(root, declared) {
2701
2709
  const name = declared.split("@")[0];
2702
2710
  if (name) return name;
2703
2711
  }
2704
- if (await pathExists(path8.join(root, "pnpm-lock.yaml"))) return "pnpm";
2705
- if (await pathExists(path8.join(root, "bun.lockb"))) return "bun";
2706
- if (await pathExists(path8.join(root, "bun.lock"))) return "bun";
2707
- if (await pathExists(path8.join(root, "yarn.lock"))) return "yarn";
2712
+ if (await pathExists(path9.join(root, "pnpm-lock.yaml"))) return "pnpm";
2713
+ if (await pathExists(path9.join(root, "bun.lockb"))) return "bun";
2714
+ if (await pathExists(path9.join(root, "bun.lock"))) return "bun";
2715
+ if (await pathExists(path9.join(root, "yarn.lock"))) return "yarn";
2708
2716
  return "npm";
2709
2717
  }
2710
2718
  function hasUsableScript(scripts, name) {
@@ -2725,7 +2733,7 @@ function parseMakeTargets(makefile) {
2725
2733
  async function detectProjectFacts(root) {
2726
2734
  const facts = { hints: [] };
2727
2735
  try {
2728
- const pkg = JSON.parse(await fsp4.readFile(path8.join(root, "package.json"), "utf8"));
2736
+ const pkg = JSON.parse(await fsp4.readFile(path9.join(root, "package.json"), "utf8"));
2729
2737
  const scripts = pkg.scripts ?? {};
2730
2738
  const pm = await detectPackageManager(root, pkg.packageManager);
2731
2739
  if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
@@ -2739,14 +2747,14 @@ async function detectProjectFacts(root) {
2739
2747
  } catch {
2740
2748
  }
2741
2749
  try {
2742
- if (!await pathExists(path8.join(root, "pyproject.toml"))) throw new Error("not python");
2750
+ if (!await pathExists(path9.join(root, "pyproject.toml"))) throw new Error("not python");
2743
2751
  facts.test ??= "pytest";
2744
2752
  facts.lint ??= "ruff check .";
2745
2753
  facts.hints.push("pyproject.toml");
2746
2754
  } catch {
2747
2755
  }
2748
2756
  try {
2749
- if (!await pathExists(path8.join(root, "go.mod"))) throw new Error("not go");
2757
+ if (!await pathExists(path9.join(root, "go.mod"))) throw new Error("not go");
2750
2758
  facts.build ??= "go build ./...";
2751
2759
  facts.test ??= "go test ./...";
2752
2760
  facts.run ??= "go run .";
@@ -2754,7 +2762,7 @@ async function detectProjectFacts(root) {
2754
2762
  } catch {
2755
2763
  }
2756
2764
  try {
2757
- if (!await pathExists(path8.join(root, "Cargo.toml"))) throw new Error("not rust");
2765
+ if (!await pathExists(path9.join(root, "Cargo.toml"))) throw new Error("not rust");
2758
2766
  facts.build ??= "cargo build";
2759
2767
  facts.test ??= "cargo test";
2760
2768
  facts.lint ??= "cargo clippy";
@@ -2763,7 +2771,7 @@ async function detectProjectFacts(root) {
2763
2771
  } catch {
2764
2772
  }
2765
2773
  try {
2766
- const makefile = await fsp4.readFile(path8.join(root, "Makefile"), "utf8");
2774
+ const makefile = await fsp4.readFile(path9.join(root, "Makefile"), "utf8");
2767
2775
  const targets = parseMakeTargets(makefile);
2768
2776
  facts.build ??= targets.has("build") ? "make build" : "make";
2769
2777
  if (targets.has("test")) facts.test ??= "make test";
@@ -2873,20 +2881,7 @@ function countToolResults(messages) {
2873
2881
  return count;
2874
2882
  }
2875
2883
  function estimateTokens(messages) {
2876
- let total = 0;
2877
- for (const m of messages) {
2878
- const content = m.content;
2879
- if (typeof content === "string") {
2880
- total += Math.ceil(content.length / 4);
2881
- } else if (Array.isArray(content)) {
2882
- for (const b of content) {
2883
- if (b.type === "text") total += Math.ceil(b.text.length / 4);
2884
- else if (b.type === "tool_use" || b.type === "tool_result")
2885
- total += Math.ceil(JSON.stringify(b).length / 4);
2886
- }
2887
- }
2888
- }
2889
- return total;
2884
+ return estimateMessageTokens(messages);
2890
2885
  }
2891
2886
 
2892
2887
  // src/slash-commands/auth.ts
@@ -3071,7 +3066,7 @@ function formatPhaseList(graph) {
3071
3066
  }
3072
3067
  async function gatherProjectContext(projectRoot) {
3073
3068
  try {
3074
- const raw = await fsp4.readFile(path8.join(projectRoot, "package.json"), "utf8");
3069
+ const raw = await fsp4.readFile(path9.join(projectRoot, "package.json"), "utf8");
3075
3070
  const pkg = JSON.parse(raw);
3076
3071
  const parts = [
3077
3072
  `Project: ${String(pkg.name ?? "unknown")}`,
@@ -3187,6 +3182,7 @@ function buildAutoPhaseCommand(opts) {
3187
3182
  };
3188
3183
  }
3189
3184
  }
3185
+ return { message: `Unknown subcommand "${sub}". Run \`/autophase\` for usage.` };
3190
3186
  }
3191
3187
  };
3192
3188
  }
@@ -3874,7 +3870,7 @@ ${formatContextWindowModeList(active)}`;
3874
3870
  const lines = [
3875
3871
  `${color.bold("Context Window")}`,
3876
3872
  ` messages: ${messages.length} total (${countTurnPairs(messages)} user+assistant pairs)`,
3877
- ` tokens (est): ${estimateTokens(messages).toLocaleString()} (chars / 4 estimate)`,
3873
+ ` tokens (est): ${estimateTokens(messages).toLocaleString()} (\u2248 chars/3.5)`,
3878
3874
  ` mode: ${policy ? `${policy.id} (${policy.name})` : "balanced"}`,
3879
3875
  ` limit: ${formatLimit(readEffectiveLimit(ctx, opts))}`,
3880
3876
  ` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
@@ -3993,14 +3989,59 @@ function buildStatsCommand(opts) {
3993
3989
  }
3994
3990
  };
3995
3991
  }
3992
+ function resolvePersistPath(deps) {
3993
+ const scope = deps.configStore.get().configScope;
3994
+ if (scope === "project" && deps.inProjectConfigPath) {
3995
+ return deps.inProjectConfigPath;
3996
+ }
3997
+ return deps.globalConfigPath;
3998
+ }
3999
+ async function ensureProjectDir(filePath) {
4000
+ const dir = path9.dirname(filePath);
4001
+ try {
4002
+ await fsp4.mkdir(dir, { recursive: true });
4003
+ } catch {
4004
+ }
4005
+ }
4006
+ var PROJECT_SAFE_FIELDS = /* @__PURE__ */ new Set([
4007
+ "provider",
4008
+ "model",
4009
+ "fallbackModels",
4010
+ "modelMatrix",
4011
+ "maxConcurrent",
4012
+ "autonomy",
4013
+ "hints",
4014
+ "nextPrediction",
4015
+ "debugStream",
4016
+ "configScope",
4017
+ "yolo",
4018
+ "features",
4019
+ "context",
4020
+ "log",
4021
+ "session",
4022
+ "indexing",
4023
+ "tools",
4024
+ "launch"
4025
+ ]);
4026
+ function filterSafeForProject(cfg) {
4027
+ const out = {};
4028
+ for (const [key, value] of Object.entries(cfg)) {
4029
+ if (PROJECT_SAFE_FIELDS.has(key)) {
4030
+ out[key] = value;
4031
+ }
4032
+ }
4033
+ return out;
4034
+ }
3996
4035
  async function persistAutonomySetting(deps, mutator) {
4036
+ const targetPath = resolvePersistPath(deps);
4037
+ await ensureProjectDir(targetPath);
3997
4038
  let raw;
3998
4039
  let fileExists = true;
3999
4040
  try {
4000
- raw = await fsp4.readFile(deps.globalConfigPath, "utf8");
4041
+ raw = await fsp4.readFile(targetPath, "utf8");
4001
4042
  } catch (err) {
4002
4043
  if (err.code !== "ENOENT") {
4003
- throw new Error(`Could not read ${deps.globalConfigPath}: ${err.message}`);
4044
+ throw new Error(`Could not read ${targetPath}: ${err.message}`);
4004
4045
  }
4005
4046
  fileExists = false;
4006
4047
  raw = "{}";
@@ -4010,7 +4051,7 @@ async function persistAutonomySetting(deps, mutator) {
4010
4051
  parsed = JSON.parse(raw);
4011
4052
  } catch (err) {
4012
4053
  if (fileExists) {
4013
- throw new Error(`Config at ${deps.globalConfigPath} is not valid JSON: ${err.message}`);
4054
+ throw new Error(`Config at ${targetPath} is not valid JSON: ${err.message}`);
4014
4055
  }
4015
4056
  parsed = {};
4016
4057
  }
@@ -4018,18 +4059,26 @@ async function persistAutonomySetting(deps, mutator) {
4018
4059
  const autonomy = decrypted.autonomy ?? {};
4019
4060
  mutator(autonomy);
4020
4061
  decrypted.autonomy = autonomy;
4021
- const encrypted = encryptConfigSecrets$1(decrypted, deps.vault);
4022
- await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4062
+ const newScope = decrypted.configScope;
4063
+ const actualTarget = newScope === "project" && deps.inProjectConfigPath ? deps.inProjectConfigPath : newScope === "global" ? deps.globalConfigPath : targetPath;
4064
+ if (actualTarget !== targetPath) {
4065
+ await ensureProjectDir(actualTarget);
4066
+ }
4067
+ const toWrite = actualTarget === deps.globalConfigPath ? decrypted : filterSafeForProject(decrypted);
4068
+ const encrypted = encryptConfigSecrets$1(toWrite, deps.vault);
4069
+ await atomicWrite(actualTarget, JSON.stringify(encrypted, null, 2), { mode: 384 });
4023
4070
  deps.configStore.update({ autonomy: decrypted.autonomy });
4024
4071
  }
4025
4072
  async function persistConfigSetting(deps, mutator) {
4073
+ const targetPath = resolvePersistPath(deps);
4074
+ await ensureProjectDir(targetPath);
4026
4075
  let raw;
4027
4076
  let fileExists = true;
4028
4077
  try {
4029
- raw = await fsp4.readFile(deps.globalConfigPath, "utf8");
4078
+ raw = await fsp4.readFile(targetPath, "utf8");
4030
4079
  } catch (err) {
4031
4080
  if (err.code !== "ENOENT") {
4032
- throw new Error(`Could not read ${deps.globalConfigPath}: ${err.message}`);
4081
+ throw new Error(`Could not read ${targetPath}: ${err.message}`);
4033
4082
  }
4034
4083
  fileExists = false;
4035
4084
  raw = "{}";
@@ -4039,14 +4088,20 @@ async function persistConfigSetting(deps, mutator) {
4039
4088
  parsed = JSON.parse(raw);
4040
4089
  } catch (err) {
4041
4090
  if (fileExists) {
4042
- throw new Error(`Config at ${deps.globalConfigPath} is not valid JSON: ${err.message}`);
4091
+ throw new Error(`Config at ${targetPath} is not valid JSON: ${err.message}`);
4043
4092
  }
4044
4093
  parsed = {};
4045
4094
  }
4046
4095
  const decrypted = decryptConfigSecrets$1(parsed, deps.vault);
4047
4096
  mutator(decrypted);
4048
- const encrypted = encryptConfigSecrets$1(decrypted, deps.vault);
4049
- await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4097
+ const newScope = decrypted.configScope;
4098
+ const actualTarget = newScope === "project" && deps.inProjectConfigPath ? deps.inProjectConfigPath : newScope === "global" ? deps.globalConfigPath : targetPath;
4099
+ if (actualTarget !== targetPath) {
4100
+ await ensureProjectDir(actualTarget);
4101
+ }
4102
+ const toWrite = actualTarget === deps.globalConfigPath ? decrypted : filterSafeForProject(decrypted);
4103
+ const encrypted = encryptConfigSecrets$1(toWrite, deps.vault);
4104
+ await atomicWrite(actualTarget, JSON.stringify(encrypted, null, 2), { mode: 384 });
4050
4105
  deps.configStore.update(decrypted);
4051
4106
  }
4052
4107
  async function persistTelegramConfig(deps, mutator) {
@@ -4078,7 +4133,7 @@ async function persistTelegramConfig(deps, mutator) {
4078
4133
  decrypted.extensions = extensions;
4079
4134
  const encrypted = encryptConfigSecrets$1(decrypted, deps.vault);
4080
4135
  await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4081
- deps.configStore.update({ extensions: decrypted.extensions });
4136
+ deps.configStore.update({ extensions });
4082
4137
  }
4083
4138
 
4084
4139
  // src/slash-commands/enhance.ts
@@ -5546,6 +5601,7 @@ ${formatGoal(updated)}` };
5546
5601
  } catch {
5547
5602
  }
5548
5603
  if (opts.onEternalStop) opts.onEternalStop();
5604
+ if (opts.onAutonomy) opts.onAutonomy("off");
5549
5605
  const msg = `${color.amber("Goal cleared.")} Previous goal marked abandoned; eternal mode will stop.`;
5550
5606
  opts.renderer.write(msg);
5551
5607
  return { message: msg };
@@ -5690,8 +5746,8 @@ function buildInitCommand(opts) {
5690
5746
  category: "Config",
5691
5747
  description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
5692
5748
  async run(_args, ctx) {
5693
- const dir = path8.join(ctx.projectRoot, ".wrongstack");
5694
- const file = path8.join(dir, "AGENTS.md");
5749
+ const dir = path9.join(ctx.projectRoot, ".wrongstack");
5750
+ const file = path9.join(dir, "AGENTS.md");
5695
5751
  const detected = await detectProjectFacts(ctx.projectRoot);
5696
5752
  const body = renderAgentsTemplate(detected);
5697
5753
  await fsp4.mkdir(dir, { recursive: true });
@@ -5906,9 +5962,9 @@ function stateBadge(state) {
5906
5962
  return color.dim(state);
5907
5963
  }
5908
5964
  }
5909
- async function readConfig(path26) {
5965
+ async function readConfig(path27) {
5910
5966
  try {
5911
- return JSON.parse(await fsp4.readFile(path26, "utf8"));
5967
+ return JSON.parse(await fsp4.readFile(path27, "utf8"));
5912
5968
  } catch {
5913
5969
  return {};
5914
5970
  }
@@ -5916,11 +5972,11 @@ async function readConfig(path26) {
5916
5972
  function isMcpServerRecord(value) {
5917
5973
  return !!value && typeof value === "object" && !Array.isArray(value);
5918
5974
  }
5919
- async function writeConfig(path26, cfg) {
5975
+ async function writeConfig(path27, cfg) {
5920
5976
  const raw = JSON.stringify(cfg, null, 2);
5921
- const tmp = path26 + ".tmp";
5977
+ const tmp = path27 + ".tmp";
5922
5978
  await fsp4.writeFile(tmp, raw, "utf8");
5923
- await fsp4.rename(tmp, path26);
5979
+ await fsp4.rename(tmp, path27);
5924
5980
  }
5925
5981
 
5926
5982
  // src/slash-commands/mcp.ts
@@ -7134,7 +7190,7 @@ function buildSetModelCommand(opts) {
7134
7190
  for (const phase of MATRIX_PHASE_KEYS) {
7135
7191
  const agents = AGENTS_BY_PHASE[phase];
7136
7192
  if (agents && agents.length > 0) {
7137
- picks.push(agents[0].config.role);
7193
+ picks.push(agents[0]?.config.role);
7138
7194
  }
7139
7195
  }
7140
7196
  picks.push("security-scanner", "bug-hunter");
@@ -7379,6 +7435,130 @@ function buildSetModelCommand(opts) {
7379
7435
  }
7380
7436
  };
7381
7437
  }
7438
+ function fmtTokens(n) {
7439
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
7440
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
7441
+ return String(n);
7442
+ }
7443
+ function fmtPrice(pricePer1k) {
7444
+ if (pricePer1k === void 0 || pricePer1k <= 0) return color.dim("\u2014");
7445
+ return `$${pricePer1k.toFixed(2)}/M tok`;
7446
+ }
7447
+ function contextBar(maxContext) {
7448
+ const emoji = maxContext > 2e5 ? "\u{1F7E2}" : maxContext > 128e3 ? "\u{1F7E1}" : "\u{1F534}";
7449
+ return `${emoji} ${fmtTokens(maxContext)}`;
7450
+ }
7451
+ function buildModelCapsCommand(opts) {
7452
+ return {
7453
+ name: "modelcaps",
7454
+ category: "Config",
7455
+ description: "List available models with capacities (context window, max output, pricing).",
7456
+ help: [
7457
+ "Usage:",
7458
+ " /modelcaps List all available models grouped by provider",
7459
+ " /modelcaps <provider> Show models for one provider only",
7460
+ " /modelcaps <fragment> Filter models by id fragment (case-insensitive)",
7461
+ " /modelcaps summary Show agent-type \u2192 model mapping matrix",
7462
+ "",
7463
+ "Capacities shown: context window, max output tokens, input/output pricing.",
7464
+ "\u25CF = API key present \xB7 \u25CB = no key (model listed but not usable)."
7465
+ ].join("\n"),
7466
+ async run(args) {
7467
+ const trimmed = args.trim().toLowerCase();
7468
+ if (trimmed === "summary") {
7469
+ return {
7470
+ message: [
7471
+ `${color.bold("Agent-Type \u2192 Model Mapping")} ${color.dim("\u2014 use /setmodel")}`,
7472
+ "",
7473
+ `${color.dim("Run /setmodel to see the current model matrix and resolution chain.")}`,
7474
+ `${color.dim("Each agent role resolves its model via: role \u2192 phase \u2192 * \u2192 leader.")}`,
7475
+ "",
7476
+ `${color.dim("/setmodel \u2014 show leader + matrix + resolution summary")}`,
7477
+ `${color.dim("/setmodel resolve <role> \u2014 walk the resolution chain for one role")}`
7478
+ ].join("\n")
7479
+ };
7480
+ }
7481
+ const cachePath2 = opts.paths?.modelsCache;
7482
+ if (!cachePath2) {
7483
+ return { message: `${color.red("Models cache path not available")}.` };
7484
+ }
7485
+ let providers;
7486
+ try {
7487
+ const raw = await fsp4.readFile(cachePath2, "utf8");
7488
+ const parsed = JSON.parse(raw);
7489
+ const payload = parsed.payload ?? parsed;
7490
+ providers = Object.entries(payload).map(([id, p]) => ({
7491
+ id: p.id ?? id,
7492
+ name: p.name ?? id,
7493
+ family: p.npm ?? id,
7494
+ models: Object.values(p.models ?? {}).map((m) => ({
7495
+ id: m.id,
7496
+ name: m.name,
7497
+ capabilities: {
7498
+ contextWindow: m.limit?.context,
7499
+ maxOutputTokens: m.limit?.output
7500
+ },
7501
+ pricing: m.cost
7502
+ }))
7503
+ }));
7504
+ } catch {
7505
+ return {
7506
+ message: [
7507
+ `${color.amber("Models cache not available")}.`,
7508
+ `${color.dim(`Expected at: ${cachePath2}`)}`,
7509
+ "",
7510
+ `${color.dim("Run wstack sync-models or wait for the next auto-sync.")}`
7511
+ ].join("\n")
7512
+ };
7513
+ }
7514
+ const config = opts.configStore.get();
7515
+ const configProviders = config?.providers ?? {};
7516
+ function hasKey(providerId) {
7517
+ const pc = configProviders[providerId];
7518
+ if (!pc) return false;
7519
+ if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
7520
+ if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
7521
+ return false;
7522
+ }
7523
+ const lines = [
7524
+ `${color.bold("Available Models")} ${color.dim("\u2014 capacities + pricing")}`,
7525
+ ""
7526
+ ];
7527
+ let shown = 0;
7528
+ for (const prov of providers) {
7529
+ if (trimmed && !trimmed.includes("/") && !prov.id.toLowerCase().includes(trimmed) && !prov.name.toLowerCase().includes(trimmed)) {
7530
+ continue;
7531
+ }
7532
+ const keyed = hasKey(prov.id);
7533
+ const marker = keyed ? color.green("\u25CF") : color.dim("\u25CB");
7534
+ lines.push(` ${marker} ${color.bold(prov.id.padEnd(16))} ${color.dim(`(${prov.name})`)}`);
7535
+ const models = prov.models ?? [];
7536
+ if (models.length === 0) {
7537
+ lines.push(` ${color.dim("no models listed \u2014 any model id accepted")}`);
7538
+ }
7539
+ for (const m of models) {
7540
+ if (trimmed?.includes("/")) {
7541
+ const frag = trimmed.split("/").pop() ?? "";
7542
+ if (frag && !m.id.toLowerCase().includes(frag)) continue;
7543
+ }
7544
+ const cap = m.capabilities;
7545
+ const ctx = cap?.contextWindow ?? 0;
7546
+ const maxOut = cap?.maxOutputTokens ?? 0;
7547
+ lines.push(
7548
+ ` ${color.cyan(m.id)} ${contextBar(ctx)}` + (maxOut > 0 ? ` ${color.dim("out")} ${fmtTokens(maxOut)}` : "") + ` ${color.dim("in")} ${fmtPrice(m.pricing?.input)} ${color.dim("out")} ${fmtPrice(m.pricing?.output)}`
7549
+ );
7550
+ shown++;
7551
+ }
7552
+ lines.push("");
7553
+ }
7554
+ if (shown === 0) {
7555
+ lines.push(` ${color.dim("No models matched. Try /modelcaps without a filter.")}`);
7556
+ }
7557
+ lines.push(color.dim(`${shown} model(s). \u25CF = key present \xB7 \u25CB = no key. Use /modelcaps summary for agent-type mapping.`));
7558
+ return { message: lines.join("\n") };
7559
+ }
7560
+ };
7561
+ }
7382
7562
  var noOpVault4 = {
7383
7563
  encrypt: (v) => v,
7384
7564
  decrypt: (v) => v,
@@ -7396,6 +7576,8 @@ function buildSettingsCommand(opts) {
7396
7576
  " /settings delay <seconds> Auto-proceed delay in auto mode (0 disables)",
7397
7577
  " /settings mode <off|suggest|auto> Default autonomy mode at startup",
7398
7578
  " /settings hints on|off Show or suppress rotating launch hints",
7579
+ " /settings debug-stream on|off Raw SSE hex-dump to stderr for debugging",
7580
+ " /settings config-scope global|project Save settings globally or per-project",
7399
7581
  " /settings defaults Show built-in default values",
7400
7582
  "",
7401
7583
  "Settings are persisted to ~/.wrongstack/config.json."
@@ -7405,12 +7587,16 @@ function buildSettingsCommand(opts) {
7405
7587
  const delay = autonomy?.autoProceedDelayMs ?? 45e3;
7406
7588
  const mode = autonomy?.defaultMode ?? "off";
7407
7589
  const hints = opts.configStore.get().hints !== false;
7590
+ const debugStream = opts.configStore.get().debugStream === true;
7591
+ const configScope = opts.configStore.get().configScope ?? "global";
7408
7592
  return [
7409
7593
  `${color.bold("WrongStack")} ${color.dim("\u2014 Settings")}`,
7410
7594
  "",
7411
7595
  ` auto-proceed delay: ${color.cyan(formatDelay(delay))} ${color.dim("change: /settings delay <seconds>")}`,
7412
7596
  ` default autonomy mode: ${color.cyan(mode)} ${color.dim("change: /settings mode off|suggest|auto")}`,
7413
7597
  ` launch hints: ${hints ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hints on|off")}`,
7598
+ ` debug stream: ${debugStream ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings debug-stream on|off")}`,
7599
+ ` config scope: ${color.cyan(configScope)} ${color.dim("change: /settings config-scope global|project")}`,
7414
7600
  "",
7415
7601
  color.dim(" Persisted to ~/.wrongstack/config.json \xB7 /settings help for more")
7416
7602
  ].join("\n");
@@ -7449,6 +7635,7 @@ function buildSettingsCommand(opts) {
7449
7635
  const persistDeps = {
7450
7636
  configStore: opts.configStore,
7451
7637
  globalConfigPath: opts.paths.globalConfig,
7638
+ inProjectConfigPath: opts.paths.inProjectConfig,
7452
7639
  vault: noOpVault4
7453
7640
  };
7454
7641
  try {
@@ -7493,8 +7680,32 @@ function buildSettingsCommand(opts) {
7493
7680
  });
7494
7681
  return { message: `${color.green("\u2713")} launch hints \u2192 ${on ? color.cyan("on") : color.dim("off")}` };
7495
7682
  }
7683
+ if (sub === "debug-stream") {
7684
+ const raw = (parts[1] ?? "").toLowerCase();
7685
+ if (!["on", "off"].includes(raw)) {
7686
+ return { message: `${color.amber("Usage:")} /settings debug-stream on|off` };
7687
+ }
7688
+ const on = raw === "on";
7689
+ const { setDebugStreamEnabled } = await import('@wrongstack/providers');
7690
+ setDebugStreamEnabled(on);
7691
+ await persistConfigSetting(persistDeps, (cfg) => {
7692
+ cfg.debugStream = on;
7693
+ });
7694
+ return { message: `${color.green("\u2713")} debug stream \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim("raw SSE hex-dump to stderr")}` };
7695
+ }
7696
+ if (sub === "config-scope") {
7697
+ const raw = (parts[1] ?? "").toLowerCase();
7698
+ if (!["global", "project"].includes(raw)) {
7699
+ return { message: `${color.amber("Usage:")} /settings config-scope global|project` };
7700
+ }
7701
+ await persistConfigSetting(persistDeps, (cfg) => {
7702
+ cfg.configScope = raw;
7703
+ });
7704
+ const label = raw === "project" ? `${color.cyan("project")} \u2014 settings saved to <project>/.wrongstack/config.json` : `${color.cyan("global")} \u2014 settings saved to ~/.wrongstack/config.json`;
7705
+ return { message: `${color.green("\u2713")} config scope \u2192 ${label}` };
7706
+ }
7496
7707
  return {
7497
- message: `${color.red("Unknown setting")} "${sub}". Try ${color.dim("/settings")}, ${color.dim("/settings delay <s>")}, ${color.dim("/settings mode <m>")}, or ${color.dim("/settings hints on|off")}.`
7708
+ message: `${color.red("Unknown setting")} "${sub}". Try ${color.dim("/settings")}, ${color.dim("/settings delay <s>")}, ${color.dim("/settings mode <m>")}, ${color.dim("/settings hints on|off")}, ${color.dim("/settings debug-stream on|off")}, or ${color.dim("/settings config-scope global|project")}.`
7498
7709
  };
7499
7710
  } catch (err) {
7500
7711
  return {
@@ -7745,7 +7956,7 @@ var DEFAULTS = {
7745
7956
  cost: true
7746
7957
  };
7747
7958
  function resolveConfigPath() {
7748
- return process.env[CONFIG_ENV] ?? path8.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
7959
+ return process.env[CONFIG_ENV] ?? path9.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
7749
7960
  }
7750
7961
  async function loadStatuslineConfig() {
7751
7962
  const p = resolveConfigPath();
@@ -7759,7 +7970,7 @@ async function loadStatuslineConfig() {
7759
7970
  async function saveStatuslineConfig(cfg) {
7760
7971
  const p = resolveConfigPath();
7761
7972
  try {
7762
- await fsp4.mkdir(path8.dirname(p), { recursive: true });
7973
+ await fsp4.mkdir(path9.dirname(p), { recursive: true });
7763
7974
  await atomicWrite(p, JSON.stringify(cfg, null, 2));
7764
7975
  } catch (err) {
7765
7976
  throw new FsError({
@@ -8276,6 +8487,7 @@ function buildBuiltinSlashCommands(opts) {
8276
8487
  buildSettingsCommand(opts),
8277
8488
  buildTelegramSetupCommand(opts),
8278
8489
  buildSetModelCommand(opts),
8490
+ buildModelCapsCommand(opts),
8279
8491
  buildModelsCommand(opts),
8280
8492
  buildCollabCommand(opts),
8281
8493
  buildStatuslineCommand({
@@ -8305,13 +8517,13 @@ var MANIFESTS = [
8305
8517
  ];
8306
8518
  async function detectProjectKind(projectRoot) {
8307
8519
  try {
8308
- await fsp4.access(path8.join(projectRoot, ".wrongstack", "AGENTS.md"));
8520
+ await fsp4.access(path9.join(projectRoot, ".wrongstack", "AGENTS.md"));
8309
8521
  return "initialized";
8310
8522
  } catch {
8311
8523
  }
8312
8524
  for (const m of MANIFESTS) {
8313
8525
  try {
8314
- await fsp4.access(path8.join(projectRoot, m));
8526
+ await fsp4.access(path9.join(projectRoot, m));
8315
8527
  return "project";
8316
8528
  } catch {
8317
8529
  }
@@ -8319,8 +8531,8 @@ async function detectProjectKind(projectRoot) {
8319
8531
  return "empty";
8320
8532
  }
8321
8533
  async function scaffoldAgentsMd(projectRoot) {
8322
- const dir = path8.join(projectRoot, ".wrongstack");
8323
- const file = path8.join(dir, "AGENTS.md");
8534
+ const dir = path9.join(projectRoot, ".wrongstack");
8535
+ const file = path9.join(dir, "AGENTS.md");
8324
8536
  const facts = await detectProjectFacts(projectRoot);
8325
8537
  const body = renderAgentsTemplate(facts);
8326
8538
  await fsp4.mkdir(dir, { recursive: true });
@@ -8333,7 +8545,7 @@ async function runProjectCheck(opts) {
8333
8545
  if (kind === "initialized") {
8334
8546
  renderer.write(
8335
8547
  `
8336
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path8.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
8548
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path9.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
8337
8549
  `
8338
8550
  );
8339
8551
  return true;
@@ -8364,7 +8576,7 @@ async function runProjectCheck(opts) {
8364
8576
  }
8365
8577
  return true;
8366
8578
  }
8367
- const gitDir = path8.join(projectRoot, ".git");
8579
+ const gitDir = path9.join(projectRoot, ".git");
8368
8580
  let hasGit = false;
8369
8581
  try {
8370
8582
  await fsp4.access(gitDir);
@@ -8558,7 +8770,7 @@ var ReadlineInputReader = class {
8558
8770
  history = [];
8559
8771
  pending = false;
8560
8772
  constructor(opts = {}) {
8561
- this.historyFile = opts.historyFile ?? path8.join(os2.homedir(), ".wrongstack", "history");
8773
+ this.historyFile = opts.historyFile ?? path9.join(os2.homedir(), ".wrongstack", "history");
8562
8774
  }
8563
8775
  async loadHistory() {
8564
8776
  try {
@@ -8570,7 +8782,7 @@ var ReadlineInputReader = class {
8570
8782
  }
8571
8783
  async saveHistory() {
8572
8784
  try {
8573
- await fsp4.mkdir(path8.dirname(this.historyFile), { recursive: true });
8785
+ await fsp4.mkdir(path9.dirname(this.historyFile), { recursive: true });
8574
8786
  await fsp4.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
8575
8787
  } catch {
8576
8788
  }
@@ -8608,7 +8820,10 @@ var ReadlineInputReader = class {
8608
8820
  const fresh = this.ensure();
8609
8821
  this.installPromptGuard(fresh);
8610
8822
  return new Promise((resolve5) => {
8823
+ let settled = false;
8611
8824
  const settle = (line) => {
8825
+ if (settled) return;
8826
+ settled = true;
8612
8827
  setOutputLineGuard(null);
8613
8828
  resolve5(line);
8614
8829
  };
@@ -8620,6 +8835,7 @@ var ReadlineInputReader = class {
8620
8835
  settle(line);
8621
8836
  });
8622
8837
  fresh.once("close", () => settle(""));
8838
+ fresh.on && fresh.on("error", (_e) => settle(""));
8623
8839
  }).then((result) => {
8624
8840
  this.rl?.close();
8625
8841
  return result;
@@ -8859,20 +9075,20 @@ function assertSafeToDelete(filename, parentDir) {
8859
9075
  if (PROTECTED_BASENAMES.has(filename)) {
8860
9076
  throw new Error(`Refusing to delete protected file: ${filename}`);
8861
9077
  }
8862
- if (filename !== path8.basename(filename)) {
9078
+ if (filename !== path9.basename(filename)) {
8863
9079
  throw new Error(`Refusing to delete path with traversal: ${filename}`);
8864
9080
  }
8865
9081
  if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
8866
9082
  throw new Error(`Refusing to delete unknown file: ${filename}`);
8867
9083
  }
8868
- const resolvedParent = path8.resolve(parentDir);
9084
+ const resolvedParent = path9.resolve(parentDir);
8869
9085
  if (!resolvedParent.endsWith(".wrongstack")) {
8870
9086
  throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
8871
9087
  }
8872
9088
  }
8873
9089
  async function safeDelete(filePath) {
8874
- const dir = path8.dirname(filePath);
8875
- const filename = path8.basename(filePath);
9090
+ const dir = path9.dirname(filePath);
9091
+ const filename = path9.basename(filePath);
8876
9092
  try {
8877
9093
  assertSafeToDelete(filename, dir);
8878
9094
  await fsp4.unlink(filePath);
@@ -8917,16 +9133,16 @@ function diffSummary(oldCfg, newCfg) {
8917
9133
  }
8918
9134
  var defaultHomeDir = () => os2__default.homedir();
8919
9135
  function historyDir(homeFn = defaultHomeDir) {
8920
- return path8.join(homeFn(), ".wrongstack", "config.history", "entries");
9136
+ return path9.join(homeFn(), ".wrongstack", "config.history", "entries");
8921
9137
  }
8922
9138
  function historyIndexPath(homeFn = defaultHomeDir) {
8923
- return path8.join(homeFn(), ".wrongstack", "config.history", "index.json");
9139
+ return path9.join(homeFn(), ".wrongstack", "config.history", "index.json");
8924
9140
  }
8925
9141
  function configPath(homeFn = defaultHomeDir) {
8926
- return path8.join(homeFn(), ".wrongstack", "config.json");
9142
+ return path9.join(homeFn(), ".wrongstack", "config.json");
8927
9143
  }
8928
9144
  function backupLastPath(homeFn = defaultHomeDir) {
8929
- return path8.join(homeFn(), ".wrongstack", "config.json.last");
9145
+ return path9.join(homeFn(), ".wrongstack", "config.json.last");
8930
9146
  }
8931
9147
  function entryId(ts) {
8932
9148
  return ts.replace(/[:.]/g, "-").slice(0, 19);
@@ -8981,17 +9197,17 @@ async function backupCurrent(homeFn = defaultHomeDir) {
8981
9197
  }
8982
9198
  if (content !== void 0) {
8983
9199
  try {
8984
- const bakPath = path8.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
9200
+ const bakPath = path9.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
8985
9201
  await atomicWrite(bakPath, content);
8986
9202
  } catch {
8987
9203
  }
8988
9204
  }
8989
9205
  try {
8990
- const dir = path8.join(homeFn(), ".wrongstack");
9206
+ const dir = path9.join(homeFn(), ".wrongstack");
8991
9207
  const files = await fsp4.readdir(dir);
8992
9208
  const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
8993
9209
  for (const f of baks.slice(10)) {
8994
- await safeDelete(path8.join(dir, f));
9210
+ await safeDelete(path9.join(dir, f));
8995
9211
  }
8996
9212
  } catch {
8997
9213
  }
@@ -9009,7 +9225,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
9009
9225
  };
9010
9226
  try {
9011
9227
  await fsp4.writeFile(
9012
- path8.join(historyDir(homeFn), `${id}.json`),
9228
+ path9.join(historyDir(homeFn), `${id}.json`),
9013
9229
  JSON.stringify(entry, null, 2),
9014
9230
  "utf8"
9015
9231
  );
@@ -9017,7 +9233,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
9017
9233
  throw new FsError({
9018
9234
  message: err instanceof Error ? err.message : String(err),
9019
9235
  code: ERROR_CODES.FS_WRITE_FAILED,
9020
- path: path8.join(historyDir(homeFn), `${id}.json`),
9236
+ path: path9.join(historyDir(homeFn), `${id}.json`),
9021
9237
  cause: err
9022
9238
  });
9023
9239
  }
@@ -9032,7 +9248,7 @@ async function listHistory(homeFn = defaultHomeDir) {
9032
9248
  }
9033
9249
  async function getHistoryEntry(id, homeFn = defaultHomeDir) {
9034
9250
  try {
9035
- const raw = await fsp4.readFile(path8.join(historyDir(homeFn), `${id}.json`), "utf8");
9251
+ const raw = await fsp4.readFile(path9.join(historyDir(homeFn), `${id}.json`), "utf8");
9036
9252
  return JSON.parse(raw);
9037
9253
  } catch {
9038
9254
  return null;
@@ -9098,10 +9314,10 @@ var theme = { primary: color.amber };
9098
9314
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os2__default.homedir()) {
9099
9315
  try {
9100
9316
  const { atomicWrite: atomicWrite14 } = await import('@wrongstack/core');
9101
- const fs27 = await import('fs/promises');
9317
+ const fs29 = await import('fs/promises');
9102
9318
  let existing = {};
9103
9319
  try {
9104
- const raw = await fs27.readFile(configPath2, "utf8");
9320
+ const raw = await fs29.readFile(configPath2, "utf8");
9105
9321
  existing = JSON.parse(raw);
9106
9322
  } catch {
9107
9323
  }
@@ -9437,12 +9653,12 @@ function pickGroupIndex(opts) {
9437
9653
  try {
9438
9654
  let current = 0;
9439
9655
  try {
9440
- const parsed = Number.parseInt(fs13.readFileSync(opts.cursorFile, "utf8").trim(), 10);
9656
+ const parsed = Number.parseInt(fs14.readFileSync(opts.cursorFile, "utf8").trim(), 10);
9441
9657
  if (Number.isFinite(parsed)) current = wrap(parsed);
9442
9658
  } catch {
9443
9659
  }
9444
- fs13.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
9445
- fs13.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
9660
+ fs14.mkdirSync(path9.dirname(opts.cursorFile), { recursive: true });
9661
+ fs14.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
9446
9662
  return current;
9447
9663
  } catch {
9448
9664
  }
@@ -9778,14 +9994,14 @@ function summarize(value, name) {
9778
9994
  if (typeof v === "object" && v !== null) {
9779
9995
  const o = v;
9780
9996
  if (name === "edit") {
9781
- const path26 = typeof o["path"] === "string" ? o["path"] : "";
9997
+ const path27 = typeof o["path"] === "string" ? o["path"] : "";
9782
9998
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
9783
- return `${path26} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
9999
+ return `${path27} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
9784
10000
  }
9785
10001
  if (name === "write") {
9786
- const path26 = typeof o["path"] === "string" ? o["path"] : "";
10002
+ const path27 = typeof o["path"] === "string" ? o["path"] : "";
9787
10003
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
9788
- return bytes !== void 0 ? `${path26} ${bytes}B` : path26;
10004
+ return bytes !== void 0 ? `${path27} ${bytes}B` : path27;
9789
10005
  }
9790
10006
  if (typeof o["count"] === "number") {
9791
10007
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -10009,7 +10225,8 @@ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.am
10009
10225
  renderer.write(color.dim(" (no keys saved)\n"));
10010
10226
  } else {
10011
10227
  for (let i = 0; i < keys.length; i++) {
10012
- renderKeyLine(renderer, keys[i], i + 1, active);
10228
+ const key = keys[i];
10229
+ if (key) renderKeyLine(renderer, key, i + 1, active);
10013
10230
  }
10014
10231
  }
10015
10232
  }
@@ -10101,7 +10318,7 @@ async function confirm(deps, question) {
10101
10318
  return answer === "y" || answer === "yes";
10102
10319
  }
10103
10320
  function suggestLabel(usedLabels) {
10104
- let candidate = "default";
10321
+ const candidate = "default";
10105
10322
  if (!usedLabels.has(candidate)) return candidate;
10106
10323
  let n = 2;
10107
10324
  while (usedLabels.has(`key${n}`)) n++;
@@ -10420,7 +10637,7 @@ ${color.amber("?")} ${providerId} > `
10420
10637
  if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") {
10421
10638
  return;
10422
10639
  }
10423
- const [verb, argRaw] = raw.split(/\s+/, 2);
10640
+ const [verb = "", argRaw = ""] = raw.split(/\s+/, 2);
10424
10641
  const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
10425
10642
  const handled = await dispatchAction(verb, arg, providerId, keys, cfg, deps);
10426
10643
  if (handled === "exit") return;
@@ -11002,7 +11219,7 @@ var doctorCmd = async (_args, deps) => {
11002
11219
  }
11003
11220
  try {
11004
11221
  await fsp4.mkdir(deps.paths.projectSessions, { recursive: true });
11005
- const probe = path8.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
11222
+ const probe = path9.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
11006
11223
  await fsp4.writeFile(probe, "");
11007
11224
  await fsp4.unlink(probe);
11008
11225
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
@@ -11105,8 +11322,8 @@ var exportCmd = async (args, deps) => {
11105
11322
  return 1;
11106
11323
  }
11107
11324
  if (output) {
11108
- await fsp4.mkdir(path8.dirname(path8.resolve(deps.cwd, output)), { recursive: true });
11109
- await fsp4.writeFile(path8.resolve(deps.cwd, output), rendered, "utf8");
11325
+ await fsp4.mkdir(path9.dirname(path9.resolve(deps.cwd, output)), { recursive: true });
11326
+ await fsp4.writeFile(path9.resolve(deps.cwd, output), rendered, "utf8");
11110
11327
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
11111
11328
  `);
11112
11329
  } else {
@@ -11179,8 +11396,8 @@ var initCmd = async (_args, deps) => {
11179
11396
  const vault = new DefaultSecretVault({ keyFile: deps.paths.secretsKey });
11180
11397
  const encrypted = encryptConfigSecrets(config, vault);
11181
11398
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2), { mode: 384 });
11182
- await fsp4.mkdir(path8.join(deps.projectRoot, ".wrongstack"), { recursive: true });
11183
- const agentsFile = path8.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
11399
+ await fsp4.mkdir(path9.join(deps.projectRoot, ".wrongstack"), { recursive: true });
11400
+ const agentsFile = path9.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
11184
11401
  const projectFacts = await detectProjectFacts(deps.projectRoot);
11185
11402
  await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
11186
11403
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
@@ -11637,7 +11854,7 @@ var usageCmd = async (_args, deps) => {
11637
11854
  return 0;
11638
11855
  };
11639
11856
  var projectsCmd = async (_args, deps) => {
11640
- const projectsRoot = path8.join(deps.paths.globalRoot, "projects");
11857
+ const projectsRoot = path9.join(deps.paths.globalRoot, "projects");
11641
11858
  try {
11642
11859
  const entries = await fsp4.readdir(projectsRoot);
11643
11860
  if (entries.length === 0) {
@@ -11647,7 +11864,7 @@ var projectsCmd = async (_args, deps) => {
11647
11864
  for (const hash of entries) {
11648
11865
  try {
11649
11866
  const meta = JSON.parse(
11650
- await fsp4.readFile(path8.join(projectsRoot, hash, "meta.json"), "utf8")
11867
+ await fsp4.readFile(path9.join(projectsRoot, hash, "meta.json"), "utf8")
11651
11868
  );
11652
11869
  deps.renderer.write(
11653
11870
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -12026,7 +12243,7 @@ async function listFleetRuns(deps) {
12026
12243
  }
12027
12244
  const runs = [];
12028
12245
  for (const id of entries) {
12029
- const runDir = path8.join(deps.paths.projectSessions, id);
12246
+ const runDir = path9.join(deps.paths.projectSessions, id);
12030
12247
  let stat4;
12031
12248
  try {
12032
12249
  stat4 = await fsp4.stat(runDir);
@@ -12039,17 +12256,17 @@ async function listFleetRuns(deps) {
12039
12256
  let subagentCount = 0;
12040
12257
  let subagentsDir;
12041
12258
  try {
12042
- await fsp4.access(path8.join(runDir, "fleet.json"));
12259
+ await fsp4.access(path9.join(runDir, "fleet.json"));
12043
12260
  manifest = true;
12044
12261
  } catch {
12045
12262
  }
12046
12263
  try {
12047
- await fsp4.access(path8.join(runDir, "checkpoint.json"));
12264
+ await fsp4.access(path9.join(runDir, "checkpoint.json"));
12048
12265
  checkpoint = true;
12049
12266
  } catch {
12050
12267
  }
12051
12268
  try {
12052
- subagentsDir = path8.join(runDir, "subagents");
12269
+ subagentsDir = path9.join(runDir, "subagents");
12053
12270
  const files = await fsp4.readdir(subagentsDir);
12054
12271
  subagentCount = files.filter((f) => f.endsWith(".jsonl")).length;
12055
12272
  } catch {
@@ -12078,7 +12295,7 @@ async function listFleetRuns(deps) {
12078
12295
  return 0;
12079
12296
  }
12080
12297
  async function showFleetRun(runId, deps) {
12081
- const runDir = path8.join(deps.paths.projectSessions, runId);
12298
+ const runDir = path9.join(deps.paths.projectSessions, runId);
12082
12299
  let stat4;
12083
12300
  try {
12084
12301
  stat4 = await fsp4.stat(runDir);
@@ -12095,7 +12312,7 @@ async function showFleetRun(runId, deps) {
12095
12312
  deps.renderer.write(color.bold(`
12096
12313
  Fleet Run: ${runId}
12097
12314
  `) + "\n");
12098
- const manifestPath = path8.join(runDir, "fleet.json");
12315
+ const manifestPath = path9.join(runDir, "fleet.json");
12099
12316
  let manifestData = null;
12100
12317
  try {
12101
12318
  manifestData = await fsp4.readFile(manifestPath, "utf8");
@@ -12111,7 +12328,7 @@ Fleet Run: ${runId}
12111
12328
  deps.renderer.write(` ${color.dim("\u25CB")} fleet.json \u2014 not found
12112
12329
  `);
12113
12330
  }
12114
- const checkpointPath = path8.join(runDir, "checkpoint.json");
12331
+ const checkpointPath = path9.join(runDir, "checkpoint.json");
12115
12332
  let checkpointData = null;
12116
12333
  try {
12117
12334
  checkpointData = await fsp4.readFile(checkpointPath, "utf8");
@@ -12158,7 +12375,7 @@ Fleet Run: ${runId}
12158
12375
  } catch {
12159
12376
  }
12160
12377
  }
12161
- const subagentsDir = path8.join(runDir, "subagents");
12378
+ const subagentsDir = path9.join(runDir, "subagents");
12162
12379
  let subagentFiles = [];
12163
12380
  try {
12164
12381
  subagentFiles = await fsp4.readdir(subagentsDir);
@@ -12170,7 +12387,7 @@ Fleet Run: ${runId}
12170
12387
  Subagent transcripts (${subagentFiles.length}):
12171
12388
  `);
12172
12389
  for (const f of subagentFiles.sort()) {
12173
- const filePath = path8.join(subagentsDir, f);
12390
+ const filePath = path9.join(subagentsDir, f);
12174
12391
  let size;
12175
12392
  try {
12176
12393
  const s = await fsp4.stat(filePath);
@@ -12187,7 +12404,7 @@ Fleet Run: ${runId}
12187
12404
  ${color.dim("\u25CB")} No subagent transcripts
12188
12405
  `);
12189
12406
  }
12190
- const sharedDir = path8.join(runDir, "shared");
12407
+ const sharedDir = path9.join(runDir, "shared");
12191
12408
  try {
12192
12409
  const files = await fsp4.readdir(sharedDir);
12193
12410
  deps.renderer.write(`
@@ -12354,7 +12571,7 @@ function findSessionId(args) {
12354
12571
  var rewindCmd = async (args, deps) => {
12355
12572
  const flags = parseRewindFlags(args);
12356
12573
  const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
12357
- const sessionsDir = path8.join(wpaths.globalRoot, "sessions");
12574
+ const sessionsDir = path9.join(wpaths.globalRoot, "sessions");
12358
12575
  const rewind = new DefaultSessionRewinder(sessionsDir, deps.projectRoot);
12359
12576
  let sessionId = findSessionId(args);
12360
12577
  if (!sessionId) {
@@ -12594,10 +12811,10 @@ var auditCmd = async (args, deps) => {
12594
12811
  return verify.ok ? 0 : 1;
12595
12812
  };
12596
12813
  async function listAudits(log, dir, deps) {
12597
- const fs27 = await import('fs/promises');
12814
+ const fs29 = await import('fs/promises');
12598
12815
  let entries;
12599
12816
  try {
12600
- entries = await fs27.readdir(dir);
12817
+ entries = await fs29.readdir(dir);
12601
12818
  } catch {
12602
12819
  deps.renderer.write(
12603
12820
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -12650,6 +12867,750 @@ var skillsCmd = async (_args, deps) => {
12650
12867
  );
12651
12868
  return 0;
12652
12869
  };
12870
+ var MODEL_PROFILES = [
12871
+ { provider: "anthropic", pattern: /claude-opus/i, family: "Claude Opus", strengths: ["reasoning", "planning"], bestFor: ["planning", "security", "debugging"], costTier: "premium", speedTier: "slow" },
12872
+ { provider: "anthropic", pattern: /claude-sonnet/i, family: "Claude Sonnet", strengths: ["coding", "balanced"], bestFor: ["coding", "general"], costTier: "standard", speedTier: "fast" },
12873
+ { provider: "anthropic", pattern: /claude-haiku/i, family: "Claude Haiku", strengths: ["speed"], bestFor: ["lightweight", "docs"], avoidFor: ["planning"], costTier: "budget", speedTier: "fast" },
12874
+ { provider: "openai", pattern: /gpt-5|o3|o4/i, family: "GPT-5/o3/o4", strengths: ["reasoning", "coding"], bestFor: ["planning", "coding", "debugging"], costTier: "premium", speedTier: "normal" },
12875
+ { provider: "openai", pattern: /gpt-4/i, family: "GPT-4", strengths: ["coding"], bestFor: ["coding", "docs"], costTier: "standard", speedTier: "fast" },
12876
+ { provider: "openai", pattern: /gpt-4o-mini/i, family: "GPT-4o Mini", strengths: ["speed"], bestFor: ["lightweight", "docs"], avoidFor: ["planning"], costTier: "budget", speedTier: "fast" },
12877
+ { provider: "google", pattern: /gemini-(?:2\.5|3)/i, family: "Gemini 2.5/3", strengths: ["context", "coding"], bestFor: ["coding", "data"], costTier: "standard", speedTier: "normal" },
12878
+ { provider: "google", pattern: /gemini.*flash/i, family: "Gemini Flash", strengths: ["speed"], bestFor: ["lightweight", "docs"], avoidFor: ["planning"], costTier: "budget", speedTier: "fast" },
12879
+ { provider: "deepseek", pattern: /deepseek/i, family: "DeepSeek", strengths: ["coding", "cost-effective"], bestFor: ["coding", "general"], costTier: "standard", speedTier: "normal" }
12880
+ ];
12881
+ function fmtTokens2(n) {
12882
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
12883
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
12884
+ return String(n);
12885
+ }
12886
+ function fmtMs(ms) {
12887
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(1)}s`;
12888
+ return `${ms}ms`;
12889
+ }
12890
+ function fmtPrice2(usdPer1M) {
12891
+ if (usdPer1M === void 0) return color.dim("?");
12892
+ if (usdPer1M >= 10) return `$${usdPer1M.toFixed(1)}`;
12893
+ return `$${usdPer1M.toFixed(2)}`;
12894
+ }
12895
+ function checkMark(ok) {
12896
+ return ok ? color.green("\u2713") : color.red("\u2717");
12897
+ }
12898
+ function costLabel(tier) {
12899
+ switch (tier) {
12900
+ case "premium":
12901
+ return color.red("$$$");
12902
+ case "standard":
12903
+ return color.amber("$$");
12904
+ case "budget":
12905
+ return color.green("$");
12906
+ default:
12907
+ return color.dim("?");
12908
+ }
12909
+ }
12910
+ function speedLabel(tier) {
12911
+ switch (tier) {
12912
+ case "fast":
12913
+ return color.green("\u26A1");
12914
+ case "normal":
12915
+ return color.amber("\u2192");
12916
+ case "slow":
12917
+ return color.red("\u{1F422}");
12918
+ default:
12919
+ return color.dim("?");
12920
+ }
12921
+ }
12922
+ function scoreBar(score, max) {
12923
+ const pct2 = Math.min(1, Math.max(0, score / max));
12924
+ const filled = Math.round(pct2 * 10);
12925
+ const bar = color.green("\u2588".repeat(filled)) + color.dim("\u2591".repeat(10 - filled));
12926
+ return `${bar} ${score}/${max}`;
12927
+ }
12928
+ var ROLE_CATEGORY = {
12929
+ "security-scanner": "security",
12930
+ "security-reviewer": "security",
12931
+ "bug-hunter": "debugging",
12932
+ debugger: "debugging",
12933
+ tracer: "debugging",
12934
+ planner: "planning",
12935
+ architect: "planning",
12936
+ "refactor-planner": "planning",
12937
+ test: "testing",
12938
+ e2e: "testing",
12939
+ document: "docs",
12940
+ simplifier: "docs",
12941
+ "code-reviewer": "review",
12942
+ critic: "review",
12943
+ executor: "coding",
12944
+ refactor: "refactoring",
12945
+ migration: "coding",
12946
+ frontend: "frontend",
12947
+ backend: "backend",
12948
+ api: "backend",
12949
+ auth: "backend",
12950
+ designer: "frontend",
12951
+ analyst: "data",
12952
+ data: "data",
12953
+ database: "data",
12954
+ explore: "planning",
12955
+ search: "planning",
12956
+ researcher: "planning"
12957
+ };
12958
+ function findProfile(pid, mid) {
12959
+ for (const p of MODEL_PROFILES) {
12960
+ if (p.provider === pid && p.pattern.test(mid)) return p;
12961
+ }
12962
+ return void 0;
12963
+ }
12964
+ function scoreModel(pid, mid, category, ctxWindow) {
12965
+ const profile = findProfile(pid, mid);
12966
+ let score = 50;
12967
+ if (profile) {
12968
+ if (profile.bestFor.includes(category)) score += 35;
12969
+ if (profile.avoidFor?.includes(category)) score -= 50;
12970
+ if (category === "planning" && profile.costTier === "premium") score += 15;
12971
+ if (profile.speedTier === "slow" && category === "planning") score += 10;
12972
+ if (profile.costTier === "budget" && category !== "planning" && category !== "security") score += 10;
12973
+ }
12974
+ if (ctxWindow > 2e5) score += 10;
12975
+ else if (ctxWindow > 1e5) score += 5;
12976
+ else if (ctxWindow > 32e3) score += 2;
12977
+ return { score, profile };
12978
+ }
12979
+ function rankModels(providers, hasKey, category, limit) {
12980
+ const candidates = [];
12981
+ for (const prov of providers) {
12982
+ if (!hasKey(prov.id)) continue;
12983
+ for (const m of prov.models ?? []) {
12984
+ const ctxWindow = m.capabilities?.contextWindow ?? 0;
12985
+ const { score, profile } = scoreModel(prov.id, m.id, category, ctxWindow);
12986
+ if (score > 0) {
12987
+ candidates.push({
12988
+ provider: prov.id,
12989
+ model: m.id,
12990
+ profile,
12991
+ score,
12992
+ ctxWindow,
12993
+ maxOutput: m.capabilities?.maxOutputTokens ?? 0,
12994
+ inputPrice: m.pricing?.input,
12995
+ outputPrice: m.pricing?.output
12996
+ });
12997
+ }
12998
+ }
12999
+ }
13000
+ candidates.sort((a, b) => b.score - a.score);
13001
+ return candidates.slice(0, limit);
13002
+ }
13003
+ var EVAL_TASKS = {
13004
+ coding: {
13005
+ label: "Code Generation",
13006
+ prompt: "Write a TypeScript function parseCSV(input: string): { headers: string[]; rows: string[][] } that handles quoted fields, escaped quotes, and empty lines. Return an error string on malformed input. Keep under 40 lines."
13007
+ },
13008
+ planning: {
13009
+ label: "Architecture Planning",
13010
+ prompt: "Design the folder structure and key interfaces for a monorepo CLI tool with slash commands, model routing, subagent spawning, and config persistence. List packages, their responsibilities, and the 5 most important TypeScript interfaces."
13011
+ },
13012
+ security: {
13013
+ label: "Vulnerability Detection",
13014
+ prompt: 'Review this code for security issues:\n```ts\napp.get("/api/user", (req, res) => {\n const id = req.query.id;\n const user = db.query("SELECT * FROM users WHERE id = " + id);\n res.json(user);\n});\n\napp.post("/api/run", (req, res) => {\n const { cmd } = req.body;\n exec("echo " + cmd, (err, stdout) => res.send(stdout));\n});\n```\nList every vulnerability, its severity (critical/high/medium), and the exact fix.'
13015
+ },
13016
+ debugging: {
13017
+ label: "Bug Diagnosis",
13018
+ prompt: 'This async function has 2 bugs. Find and fix both:\n```ts\nasync function processBatch(items: string[]) {\n const results = [];\n for (const item of items) {\n const result = await fetch("https://api.example.com/" + item);\n results.push(result);\n }\n return results.map(r => r.json());\n}\n```\nExplain what each bug is, why it fails, and write the corrected version.'
13019
+ },
13020
+ testing: {
13021
+ label: "Test Authoring",
13022
+ prompt: 'Write vitest test cases for this deepMerge function:\n```ts\nfunction deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n const merged = { ...base };\n for (const [key, val] of Object.entries(overrides)) {\n if (val === null) { delete merged[key]; continue; }\n if (typeof val === "object" && !Array.isArray(val) && typeof merged[key] === "object" && !Array.isArray(merged[key])) {\n merged[key] = deepMerge(merged[key] as Record<string, unknown>, val as Record<string, unknown>);\n } else { merged[key] = val; }\n }\n return merged;\n}\n```\nCover: happy path, edge cases, and error conditions.'
13023
+ },
13024
+ docs: {
13025
+ label: "Documentation",
13026
+ prompt: "Write TSDoc comments for this RateLimiter interface. Include @param, @returns, @throws, and @example for each method:\n```ts\ninterface RateLimiter {\n tryAcquire(key: string, maxPerWindow: number, windowMs: number): Promise<boolean>;\n getRemaining(key: string): Promise<number>;\n reset(key: string): Promise<void>;\n}\n```"
13027
+ },
13028
+ review: {
13029
+ label: "Code Review",
13030
+ prompt: 'Review this PR change:\n```diff\n async function loadConfig(path: string) {\n- const raw = await fs.readFile(path, "utf8");\n- return JSON.parse(raw);\n+ const raw = await fs.readFile(path);\n+ const config = JSON.parse(raw);\n+ process.env.API_KEY = config.apiKey;\n+ return config;\n }\n```\nList issues by severity (blocking / should-fix / nit) and explain your reasoning.'
13031
+ },
13032
+ refactoring: {
13033
+ label: "Refactoring",
13034
+ prompt: 'Refactor this nested condition into a cleaner pattern:\n```ts\nfunction getDiscount(user: { type: string; years: number; coupon?: string }): number {\n if (user.type === "premium") {\n if (user.years > 5) {\n if (user.coupon === "BLACKFRIDAY") return 0.5;\n return 0.3;\n }\n return 0.2;\n }\n if (user.type === "standard") {\n if (user.years > 3) return 0.15;\n return 0.1;\n }\n return 0;\n}\n```\nShow your refactored code and explain why your approach is cleaner.'
13035
+ }
13036
+ };
13037
+ var EVAL_CATEGORIES = Object.keys(EVAL_TASKS);
13038
+ function roleCat(role) {
13039
+ return ROLE_CATEGORY[role] ?? "general";
13040
+ }
13041
+ function createProviderForId(providerId, cfg) {
13042
+ const savedCfg = cfg.providers?.[providerId];
13043
+ const resolvedProviderId = savedCfg?.type ?? providerId;
13044
+ const cfgWithType = Object.assign(
13045
+ { type: resolvedProviderId },
13046
+ savedCfg ?? { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl }
13047
+ );
13048
+ try {
13049
+ return makeProviderFromConfig(resolvedProviderId, cfgWithType);
13050
+ } catch {
13051
+ return void 0;
13052
+ }
13053
+ }
13054
+ async function rankResponses(provider, leaderModel, taskPrompt, responses) {
13055
+ const labelToIdx = /* @__PURE__ */ new Map();
13056
+ const responseBlock = responses.map((r, i) => {
13057
+ const label = String.fromCharCode(65 + i);
13058
+ labelToIdx.set(label, i);
13059
+ return `=== Response ${label} ===
13060
+ ${r.text.slice(0, 800)}`;
13061
+ }).join("\n\n");
13062
+ const rankingPrompt = `Rank these responses from BEST (1) to WORST.
13063
+
13064
+ TASK:
13065
+ ${taskPrompt.slice(0, 600)}
13066
+
13067
+ RESPONSES:
13068
+ ${responseBlock}
13069
+
13070
+ Output ONLY a ranked list, one per line:
13071
+ 1. Response X \u2014 brief reason
13072
+ 2. Response Y \u2014 brief reason`;
13073
+ try {
13074
+ const resp = await provider.complete(
13075
+ {
13076
+ model: leaderModel,
13077
+ system: [{ type: "text", text: "You are an expert evaluator. Rank responses concisely. Output ONLY the ranked list." }],
13078
+ messages: [{ role: "user", content: [{ type: "text", text: rankingPrompt }] }],
13079
+ maxTokens: 400
13080
+ },
13081
+ { signal: AbortSignal.timeout(3e4) }
13082
+ );
13083
+ const text = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
13084
+ const rankings = [];
13085
+ for (const line of text.split("\n")) {
13086
+ const m = line.match(/^\s*(\d+)[\.\)]\s*Response\s+([A-Z])/i);
13087
+ if (m) {
13088
+ const label = m[2].toUpperCase();
13089
+ const idx = labelToIdx.get(label);
13090
+ if (idx !== void 0 && !rankings.includes(idx)) {
13091
+ rankings.push(idx);
13092
+ }
13093
+ }
13094
+ }
13095
+ return rankings.length > 0 ? rankings : responses.map((_, i) => i);
13096
+ } catch {
13097
+ return responses.map((_, i) => i);
13098
+ }
13099
+ }
13100
+ async function readProviders(cachePath2) {
13101
+ if (!cachePath2) {
13102
+ return `${color.red("Models cache not available")}.`;
13103
+ }
13104
+ try {
13105
+ const raw = await fsp4.readFile(cachePath2, "utf8");
13106
+ const parsed = JSON.parse(raw);
13107
+ const payload = parsed.payload ?? parsed;
13108
+ return Object.entries(payload).map(([id, p]) => ({
13109
+ id: p.id ?? id,
13110
+ name: p.name ?? id,
13111
+ family: p.npm ?? id,
13112
+ models: Object.values(p.models ?? {}).map((m) => ({
13113
+ id: m.id,
13114
+ name: m.name,
13115
+ capabilities: {
13116
+ contextWindow: m.limit?.context,
13117
+ maxOutputTokens: m.limit?.output
13118
+ },
13119
+ pricing: m.cost
13120
+ }))
13121
+ }));
13122
+ } catch {
13123
+ return `${color.amber("Models cache not available")}. Run wstack sync-models.`;
13124
+ }
13125
+ }
13126
+ function checkHasKey(pid, config) {
13127
+ if (pid === config.provider && config.provider) return true;
13128
+ const pc = config.providers?.[pid];
13129
+ if (!pc) return false;
13130
+ if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
13131
+ if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
13132
+ return false;
13133
+ }
13134
+ var modeldiagCmd = async (args, deps) => {
13135
+ const sub = args[0]?.toLowerCase() || "full";
13136
+ const cacheResult = await readProviders(deps.paths.modelsCache);
13137
+ if (typeof cacheResult === "string") {
13138
+ deps.renderer.write(`${cacheResult}
13139
+ `);
13140
+ return cacheResult.includes(color.red("")) ? 1 : 0;
13141
+ }
13142
+ const providers = cacheResult;
13143
+ const config = deps.config;
13144
+ const modelMatrix = config.modelMatrix ?? {};
13145
+ function hasKey(pid) {
13146
+ return checkHasKey(pid, config);
13147
+ }
13148
+ function writeLine(line = "") {
13149
+ deps.renderer.write(`${line}
13150
+ `);
13151
+ }
13152
+ if (sub === "keys") {
13153
+ writeLine(`${color.bold("API Key Status")}`);
13154
+ writeLine();
13155
+ for (const prov of providers) {
13156
+ const k = hasKey(prov.id);
13157
+ writeLine(` ${checkMark(k)} ${color.bold(prov.id.padEnd(18))} ${color.dim(prov.name)}`);
13158
+ }
13159
+ writeLine();
13160
+ writeLine(`${color.dim(`Leader: ${config.provider}/${config.model}`)}`);
13161
+ return 0;
13162
+ }
13163
+ if (sub === "caps") {
13164
+ writeLine(`${color.bold("Model Capabilities")} ${color.dim("\u2014 matched to known profiles")}`);
13165
+ writeLine();
13166
+ for (const prov of providers) {
13167
+ if (!hasKey(prov.id)) continue;
13168
+ writeLine(` ${color.bold(prov.id)} ${color.dim(`(${prov.name})`)}`);
13169
+ const tiers = { premium: [], standard: [], budget: [], unknown: [] };
13170
+ for (const m of prov.models ?? []) {
13171
+ const profile = findProfile(prov.id, m.id);
13172
+ tiers[profile?.costTier ?? "unknown"].push(m);
13173
+ }
13174
+ for (const tier of ["premium", "standard", "budget", "unknown"]) {
13175
+ const tierModels = tiers[tier];
13176
+ if (tierModels.length === 0) continue;
13177
+ const label = tier === "unknown" ? color.dim("unmatched") : `${costLabel(tier)} ${tier}`;
13178
+ writeLine(` ${label}`);
13179
+ for (const m of tierModels) {
13180
+ const cap = m.capabilities;
13181
+ const ctx = cap?.contextWindow ?? 0;
13182
+ const maxOut = cap?.maxOutputTokens ?? 0;
13183
+ const profile = findProfile(prov.id, m.id);
13184
+ const family = profile ? `${speedLabel(profile.speedTier)} ${color.green(profile.family)}` : color.dim("no profile match");
13185
+ const pricing = m.pricing ? `${color.dim("in")}${fmtPrice2(m.pricing.input)} ${color.dim("out")}${fmtPrice2(m.pricing.output)}` : color.dim("pricing ?");
13186
+ writeLine(
13187
+ ` ${color.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens2(ctx).padEnd(6)}` : color.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens2(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
13188
+ );
13189
+ }
13190
+ }
13191
+ writeLine();
13192
+ }
13193
+ writeLine(color.dim("Prices in USD per 1M tokens (input/output). ctx = context window, out = max output."));
13194
+ return 0;
13195
+ }
13196
+ async function renderSuggest() {
13197
+ writeLine();
13198
+ writeLine(`${color.bold("Agent \u2192 Model Suggestions")} ${color.amber("(heuristic \u2014 untested)")}`);
13199
+ writeLine(color.dim('These are profile-based best guesses. Test them with wstack modeldiag bench <role> "<prompt>".'));
13200
+ writeLine();
13201
+ const keyedProviders = providers.filter((p) => hasKey(p.id));
13202
+ if (keyedProviders.length === 0) {
13203
+ writeLine(` ${color.amber("No providers have API keys configured. Add keys with wstack auth.")}`);
13204
+ } else {
13205
+ const roles = [
13206
+ "security-scanner",
13207
+ "bug-hunter",
13208
+ "planner",
13209
+ "architect",
13210
+ "refactor-planner",
13211
+ "test",
13212
+ "document",
13213
+ "code-reviewer",
13214
+ "executor",
13215
+ "debugger"
13216
+ ];
13217
+ for (const role of roles) {
13218
+ if (modelMatrix[role]) {
13219
+ const entry = modelMatrix[role];
13220
+ const p = entry.provider ?? config.provider;
13221
+ writeLine(` ${color.dim(role.padEnd(20))} \u2192 ${color.cyan(`${p}/${entry.model}`)} ${color.dim("(user-configured)")}`);
13222
+ continue;
13223
+ }
13224
+ const cat = roleCat(role);
13225
+ const ranked = rankModels(providers, hasKey, cat, 3);
13226
+ if (ranked.length === 0) {
13227
+ writeLine(` ${color.dim(role.padEnd(20))} \u2192 ${color.dim("no candidates")}`);
13228
+ continue;
13229
+ }
13230
+ const best = ranked[0];
13231
+ const family = best.profile ? ` ${color.dim(`(${best.profile.family})`)}` : "";
13232
+ const bar = scoreBar(best.score, 110);
13233
+ writeLine(
13234
+ ` ${color.amber(role.padEnd(20))} \u2192 ${color.cyan(`${best.provider}/${best.model}`)}${family}`
13235
+ );
13236
+ writeLine(` ${" ".repeat(22)} ${bar} ${color.dim(cat)}`);
13237
+ if (ranked.length > 1 && ranked[1].score >= best.score - 15) {
13238
+ for (const alt of ranked.slice(1)) {
13239
+ const af = alt.profile ? ` (${alt.profile.family})` : "";
13240
+ writeLine(` ${" ".repeat(22)} ${color.dim(`${alt.provider}/${alt.model}${af} score ${alt.score}`)}`);
13241
+ }
13242
+ }
13243
+ }
13244
+ writeLine();
13245
+ writeLine(` ${color.bold("leader".padEnd(20))} \u2192 ${color.cyan(`${config.provider}/${config.model}`)}`);
13246
+ }
13247
+ }
13248
+ if (sub === "suggest") {
13249
+ await renderSuggest();
13250
+ writeLine();
13251
+ writeLine(color.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
13252
+ writeLine(color.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
13253
+ return 0;
13254
+ }
13255
+ if (sub === "test") {
13256
+ writeLine(`${color.bold("Connectivity Test")}`);
13257
+ writeLine();
13258
+ const keyed = providers.filter((p) => hasKey(p.id));
13259
+ if (keyed.length === 0) {
13260
+ writeLine(` ${color.amber("No providers have API keys. Add keys with wstack auth.")}`);
13261
+ return 0;
13262
+ }
13263
+ for (const prov of keyed) {
13264
+ writeLine(` ${color.cyan("\u27F3")} ${prov.id}... ${color.dim("(capability scan, no API call)")}`);
13265
+ const profile = findProfile(prov.id, config.model ?? "");
13266
+ const firstModel = prov.models?.[0]?.id ?? config.model ?? "?";
13267
+ const cap = prov.models?.[0]?.capabilities;
13268
+ const ctx = cap?.contextWindow ?? 0;
13269
+ writeLine(` ${checkMark(true)} provider: ${prov.id}`);
13270
+ writeLine(` ${checkMark(ctx > 0)} context: ${ctx > 0 ? fmtTokens2(ctx) : "unknown"}`);
13271
+ writeLine(` ${checkMark(!!profile)} profile: ${profile?.family ?? "no match"}`);
13272
+ writeLine(` model: ${color.cyan(firstModel)}`);
13273
+ writeLine();
13274
+ }
13275
+ writeLine(color.dim("Full API connectivity test requires an active session (costs tokens)."));
13276
+ writeLine(color.dim('Use wstack modeldiag bench <role> "<prompt>" to test models with real API calls.'));
13277
+ return 0;
13278
+ }
13279
+ if (sub === "bench") {
13280
+ const benchArgs = args.slice(1);
13281
+ if (benchArgs.length < 2) {
13282
+ writeLine(`${color.amber("Usage:")} wstack modeldiag bench <role> "<test prompt>" [--providers=p1,p2]`);
13283
+ writeLine();
13284
+ writeLine(color.dim('Example: wstack modeldiag bench verify "Write a function that checks if a string is a palindrome"'));
13285
+ writeLine(color.dim("Tests the top 5 candidate models for the role with your prompt and reports results."));
13286
+ writeLine(color.dim("Add --providers=anthropic,google to test across multiple providers."));
13287
+ return 0;
13288
+ }
13289
+ const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
13290
+ let providerFilter;
13291
+ if (providersEqIdx >= 0) {
13292
+ const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
13293
+ if (rawFilter) {
13294
+ providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
13295
+ benchArgs.splice(providersEqIdx, 1);
13296
+ }
13297
+ }
13298
+ const benchRole = benchArgs[0];
13299
+ if (!benchRole) {
13300
+ writeLine(`${color.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`);
13301
+ return 1;
13302
+ }
13303
+ const benchPrompt = benchArgs.slice(1).join(" ");
13304
+ const cat = roleCat(benchRole);
13305
+ const candidates = rankModels(providers, hasKey, cat, 5);
13306
+ if (candidates.length === 0) {
13307
+ writeLine(`${color.amber("No candidate models found")} for role "${benchRole}" (category: ${cat}).`);
13308
+ return 0;
13309
+ }
13310
+ let targetCandidates;
13311
+ if (providerFilter && providerFilter.length > 0) {
13312
+ targetCandidates = candidates.filter((c) => providerFilter.includes(c.provider));
13313
+ if (targetCandidates.length === 0) {
13314
+ writeLine(`${color.amber("No candidates match the specified providers")}: ${providerFilter.join(", ")}`);
13315
+ writeLine(`Candidate providers: ${[...new Set(candidates.map((c) => c.provider))].join(", ")}`);
13316
+ return 0;
13317
+ }
13318
+ } else {
13319
+ targetCandidates = candidates;
13320
+ }
13321
+ writeLine(`${color.bold("Model Benchmark")} \u2014 ${color.amber(benchRole)} ${color.dim(`(category: ${cat})`)}`);
13322
+ writeLine(`${color.dim("Prompt:")} "${benchPrompt.slice(0, 120)}${benchPrompt.length > 120 ? "\u2026" : ""}"`);
13323
+ writeLine();
13324
+ writeLine(
13325
+ ` ${color.dim("# model".padEnd(52))} ${color.dim("score".padEnd(12))} ${color.dim("latency".padEnd(10))} ${color.dim("tokens".padEnd(14))} ${color.dim("first line")}`
13326
+ );
13327
+ writeLine(` ${color.dim("\u2500".repeat(108))}`);
13328
+ const providerInstances = /* @__PURE__ */ new Map();
13329
+ for (const pid of [...new Set(targetCandidates.map((c) => c.provider))]) {
13330
+ const prov = createProviderForId(pid, config);
13331
+ if (prov) providerInstances.set(pid, prov);
13332
+ }
13333
+ let idx = 0;
13334
+ for (const c of targetCandidates.slice(0, 20)) {
13335
+ idx++;
13336
+ const label = `${idx}`.padStart(2);
13337
+ const modelKey = `${c.provider}/${c.model}`;
13338
+ const prov = providerInstances.get(c.provider);
13339
+ if (!prov) {
13340
+ writeLine(
13341
+ ` ${label} ${color.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color.red("NO PROVIDER")}`
13342
+ );
13343
+ continue;
13344
+ }
13345
+ try {
13346
+ const start = Date.now();
13347
+ const resp = await prov.complete(
13348
+ {
13349
+ model: c.model,
13350
+ messages: [{ role: "user", content: [{ type: "text", text: benchPrompt }] }],
13351
+ maxTokens: 256
13352
+ },
13353
+ { signal: AbortSignal.timeout(3e4) }
13354
+ );
13355
+ const latency = Date.now() - start;
13356
+ const firstText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
13357
+ const firstLineClean = firstText.replace(/\n/g, " ").slice(0, 80) || color.dim("(empty)");
13358
+ const provColor = c.provider === config.provider ? color.green : color.cyan;
13359
+ const usage = resp.usage;
13360
+ writeLine(
13361
+ ` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color.amber(fmtMs(latency).padEnd(8))} ${color.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
13362
+ );
13363
+ } catch (err) {
13364
+ const errMsg = err instanceof Error ? err.message : String(err);
13365
+ writeLine(
13366
+ ` ${label} ${color.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color.red("FAILED")} ${color.dim(errMsg.slice(0, 40))}`
13367
+ );
13368
+ }
13369
+ }
13370
+ const testedProviders = [...new Set(targetCandidates.map((c) => c.provider))];
13371
+ writeLine();
13372
+ writeLine(
13373
+ color.dim(`Tested ${idx} model(s) across ${testedProviders.length} provider(s): ${testedProviders.join(", ")}.`)
13374
+ );
13375
+ writeLine(color.dim("Pin the best: wstack setmodel set <role> <provider>/<model>"));
13376
+ return 0;
13377
+ }
13378
+ if (sub === "eval" || sub === "evall") {
13379
+ const evalArgs = args.slice(1);
13380
+ const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
13381
+ const providerFilter = providersEq ? providersEq.replace("--providers=", "").split(",").map((s) => s.trim()).filter(Boolean) : void 0;
13382
+ const maxEq = evalArgs.find((a) => a.startsWith("--max="));
13383
+ const maxModels = maxEq ? Math.max(1, parseInt(maxEq.replace("--max=", ""), 10) || 2) : 2;
13384
+ const quick = evalArgs.includes("--quick");
13385
+ const modelsPerCat = quick ? 1 : maxModels;
13386
+ const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
13387
+ const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
13388
+ if (targetCategories.length === 0 && roleFilter) {
13389
+ writeLine(`${color.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`);
13390
+ return 1;
13391
+ }
13392
+ const keyedProviderIds2 = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
13393
+ let targetProviderIds;
13394
+ if (providerFilter && providerFilter.length > 0) {
13395
+ const unknown = providerFilter.filter((pid) => !keyedProviderIds2.includes(pid));
13396
+ targetProviderIds = providerFilter.filter((pid) => keyedProviderIds2.includes(pid));
13397
+ if (targetProviderIds.length === 0) {
13398
+ const noKeyMsg = unknown.length > 0 ? `None of the specified providers (${unknown.join(", ")}) have API keys. Add keys with wstack auth.` : "None of the specified providers have API keys configured.";
13399
+ writeLine(`${color.amber(noKeyMsg)}`);
13400
+ return 0;
13401
+ }
13402
+ } else if (keyedProviderIds2.length === 0) {
13403
+ writeLine(`${color.amber("No providers have API keys. Add keys with wstack auth.")}`);
13404
+ return 0;
13405
+ } else if (keyedProviderIds2.length === 1) {
13406
+ targetProviderIds = keyedProviderIds2;
13407
+ } else {
13408
+ const providerList = keyedProviderIds2.map((pid, i) => {
13409
+ const info = providers.find((p) => p.id === pid);
13410
+ return ` ${color.cyan(String(i + 1))}) ${color.bold(pid.padEnd(16))} ${color.dim(info?.name ?? "")}`;
13411
+ }).join("\n");
13412
+ deps.renderer.write(`
13413
+ ${color.bold("Select providers to evaluate")}
13414
+
13415
+ ${providerList}
13416
+
13417
+ ${color.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
13418
+ `);
13419
+ const input = await deps.reader.readLine(" > ");
13420
+ const selected = input.trim().toLowerCase();
13421
+ if (selected === "" || selected === "all") {
13422
+ targetProviderIds = keyedProviderIds2;
13423
+ } else {
13424
+ targetProviderIds = [];
13425
+ for (const part of selected.split(",").map((s) => s.trim())) {
13426
+ const idx = parseInt(part, 10);
13427
+ if (idx >= 1 && idx <= keyedProviderIds2.length) {
13428
+ const pid = keyedProviderIds2[idx - 1];
13429
+ if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
13430
+ } else if (keyedProviderIds2.includes(part)) {
13431
+ if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
13432
+ }
13433
+ }
13434
+ }
13435
+ if (targetProviderIds.length === 0) {
13436
+ writeLine(color.dim("No providers selected."));
13437
+ return 0;
13438
+ }
13439
+ }
13440
+ const leaderModel = config.model ?? "unknown";
13441
+ const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds2.includes(pid)) : [];
13442
+ const warningLine = unknownProviders.length > 0 ? ` ${color.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
13443
+ ` : "";
13444
+ writeLine(`${color.bold("Model Competency Evaluation")}`);
13445
+ writeLine(color.dim(`Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`));
13446
+ writeLine(warningLine);
13447
+ writeLine(color.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
13448
+ writeLine();
13449
+ const collected = /* @__PURE__ */ new Map();
13450
+ let total = 0;
13451
+ let ok = 0;
13452
+ for (const pid of targetProviderIds) {
13453
+ const prov = createProviderForId(pid, config);
13454
+ if (!prov) {
13455
+ writeLine(color.dim(` \u2298 ${pid}: provider unavailable, skipping`));
13456
+ continue;
13457
+ }
13458
+ for (const cat of targetCategories) {
13459
+ const task = EVAL_TASKS[cat];
13460
+ if (!task) continue;
13461
+ const candidates = rankModels(providers, hasKey, cat, modelsPerCat).filter((c) => c.provider === pid);
13462
+ if (candidates.length === 0) continue;
13463
+ if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
13464
+ for (const c of candidates) {
13465
+ total++;
13466
+ const modelKey = `${pid}/${c.model}`;
13467
+ try {
13468
+ const start = Date.now();
13469
+ const resp = await prov.complete(
13470
+ {
13471
+ model: c.model,
13472
+ system: [{ type: "text", text: "Be thorough and correct." }],
13473
+ messages: [{ role: "user", content: [{ type: "text", text: task.prompt }] }],
13474
+ maxTokens: 1024
13475
+ },
13476
+ { signal: AbortSignal.timeout(45e3) }
13477
+ );
13478
+ const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
13479
+ const respUsage = resp.usage;
13480
+ collected.get(cat).set(modelKey, {
13481
+ model: modelKey,
13482
+ latency: Date.now() - start,
13483
+ tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
13484
+ text: respText
13485
+ });
13486
+ ok++;
13487
+ } catch {
13488
+ collected.get(cat).set(modelKey, {
13489
+ model: modelKey,
13490
+ latency: -1,
13491
+ tokens: 0,
13492
+ text: ""
13493
+ });
13494
+ }
13495
+ }
13496
+ }
13497
+ }
13498
+ writeLine(`${color.dim(`Phase 1: ${ok}/${total} calls succeeded`)}`);
13499
+ writeLine();
13500
+ if (collected.size === 0) {
13501
+ writeLine(color.amber("No responses collected. Check provider configuration."));
13502
+ return 0;
13503
+ }
13504
+ const leaderProvider = createProviderForId(config.provider, config);
13505
+ if (leaderProvider) {
13506
+ writeLine(`${color.bold("Phase 2")} \u2014 ${color.dim("leader ranks responses")}`);
13507
+ writeLine();
13508
+ }
13509
+ const rankings = /* @__PURE__ */ new Map();
13510
+ for (const [cat, responses] of collected) {
13511
+ const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
13512
+ if (valid.length < 2) {
13513
+ if (valid.length === 1) {
13514
+ const m = valid[0].model;
13515
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
13516
+ rankings.get(m).set(cat, { rank: 1, total: 1 });
13517
+ }
13518
+ continue;
13519
+ }
13520
+ const task = EVAL_TASKS[cat];
13521
+ if (leaderProvider) {
13522
+ const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
13523
+ for (let i = 0; i < valid.length; i++) {
13524
+ const m = valid[ranked[i] ?? i].model;
13525
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
13526
+ rankings.get(m).set(cat, { rank: i + 1, total: valid.length });
13527
+ }
13528
+ } else {
13529
+ for (const r of valid) {
13530
+ if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
13531
+ rankings.get(r.model).set(cat, { rank: 1, total: valid.length });
13532
+ }
13533
+ }
13534
+ }
13535
+ writeLine(`${color.bold("Competency Report")}`);
13536
+ writeLine();
13537
+ const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
13538
+ const catList = [...collected.keys()];
13539
+ const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
13540
+ const cw = 12;
13541
+ writeLine(
13542
+ ` ${color.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
13543
+ );
13544
+ writeLine(` ${color.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
13545
+ for (const model of allModels) {
13546
+ const mr = rankings.get(model);
13547
+ const provFromModel = model.split("/")[0] ?? "";
13548
+ const modelColor = provFromModel === config.provider ? color.cyan : color.green;
13549
+ let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
13550
+ for (const cat of catList) {
13551
+ const e = mr.get(cat);
13552
+ if (e) {
13553
+ const pct2 = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
13554
+ const pc = pct2 >= 80 ? color.green : pct2 >= 50 ? color.amber : color.red;
13555
+ row += `${pc(`#${e.rank} ${pct2}%`.padEnd(cw + 2))}`;
13556
+ } else {
13557
+ row += color.dim("\u2014".padEnd(cw + 2));
13558
+ }
13559
+ }
13560
+ writeLine(row);
13561
+ }
13562
+ writeLine();
13563
+ writeLine(color.dim("#1 100% = best in category. \u2014 = not tested."));
13564
+ writeLine();
13565
+ writeLine(color.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
13566
+ writeLine(color.dim("Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"));
13567
+ writeLine(color.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick"));
13568
+ return 0;
13569
+ }
13570
+ writeLine(`${color.bold("API Key Status")}`);
13571
+ writeLine();
13572
+ for (const prov of providers) {
13573
+ const k = hasKey(prov.id);
13574
+ writeLine(` ${checkMark(k)} ${color.bold(prov.id.padEnd(18))} ${color.dim(prov.name)}`);
13575
+ }
13576
+ writeLine();
13577
+ writeLine(`${color.dim(`Leader: ${config.provider}/${config.model}`)}`);
13578
+ writeLine();
13579
+ writeLine(`${color.bold("Model Capabilities")} ${color.dim("\u2014 matched to known profiles")}`);
13580
+ writeLine();
13581
+ for (const prov of providers) {
13582
+ if (!hasKey(prov.id)) continue;
13583
+ writeLine(` ${color.bold(prov.id)} ${color.dim(`(${prov.name})`)}`);
13584
+ const tiers = { premium: [], standard: [], budget: [], unknown: [] };
13585
+ for (const m of prov.models ?? []) {
13586
+ const profile = findProfile(prov.id, m.id);
13587
+ tiers[profile?.costTier ?? "unknown"].push(m);
13588
+ }
13589
+ for (const tier of ["premium", "standard", "budget", "unknown"]) {
13590
+ const tierModels = tiers[tier];
13591
+ if (tierModels.length === 0) continue;
13592
+ const label = tier === "unknown" ? color.dim("unmatched") : `${costLabel(tier)} ${tier}`;
13593
+ writeLine(` ${label}`);
13594
+ for (const m of tierModels) {
13595
+ const cap = m.capabilities;
13596
+ const ctx = cap?.contextWindow ?? 0;
13597
+ const maxOut = cap?.maxOutputTokens ?? 0;
13598
+ const profile = findProfile(prov.id, m.id);
13599
+ const family = profile ? `${speedLabel(profile.speedTier)} ${color.green(profile.family)}` : color.dim("no profile match");
13600
+ const pricing = m.pricing ? `${color.dim("in")}${fmtPrice2(m.pricing.input)} ${color.dim("out")}${fmtPrice2(m.pricing.output)}` : color.dim("pricing ?");
13601
+ writeLine(
13602
+ ` ${color.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens2(ctx).padEnd(6)}` : color.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens2(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
13603
+ );
13604
+ }
13605
+ }
13606
+ writeLine();
13607
+ }
13608
+ await renderSuggest();
13609
+ writeLine();
13610
+ writeLine(color.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
13611
+ writeLine(color.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
13612
+ return 0;
13613
+ };
12653
13614
  var versionCmd = async (_args, deps) => {
12654
13615
  deps.renderer.write(
12655
13616
  `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os2.platform()})
@@ -12729,7 +13690,8 @@ var subcommands = {
12729
13690
  usage: usageCmd,
12730
13691
  version: versionCmd,
12731
13692
  help: helpCmd,
12732
- projects: projectsCmd
13693
+ projects: projectsCmd,
13694
+ modeldiag: modeldiagCmd
12733
13695
  };
12734
13696
 
12735
13697
  // src/utils.ts
@@ -12752,22 +13714,22 @@ function fmtDuration(ms) {
12752
13714
  const remMin = m - h * 60;
12753
13715
  return `${h}h${remMin}m`;
12754
13716
  }
12755
- function fmtTaskResultLine(r, color56) {
13717
+ function fmtTaskResultLine(r, color58) {
12756
13718
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
12757
13719
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
12758
13720
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
12759
13721
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
12760
- const errKindChip = errKind ? color56.dim(` [${errKind}]`) : "";
12761
- const errSnip = errMsg || errKind ? `${errKindChip}${color56.dim(errTail)}` : "";
13722
+ const errKindChip = errKind ? color58.dim(` [${errKind}]`) : "";
13723
+ const errSnip = errMsg || errKind ? `${errKindChip}${color58.dim(errTail)}` : "";
12762
13724
  switch (r.status) {
12763
13725
  case "success":
12764
- return { mark: color56.green("\u2713"), stats, tail: "" };
13726
+ return { mark: color58.green("\u2713"), stats, tail: "" };
12765
13727
  case "timeout":
12766
- return { mark: color56.yellow("\u23F1"), stats: `${color56.yellow("timeout")} ${stats}`, tail: errSnip };
13728
+ return { mark: color58.yellow("\u23F1"), stats: `${color58.yellow("timeout")} ${stats}`, tail: errSnip };
12767
13729
  case "stopped":
12768
- return { mark: color56.dim("\u2298"), stats: `${color56.dim("stopped")} ${stats}`, tail: errSnip };
13730
+ return { mark: color58.dim("\u2298"), stats: `${color58.dim("stopped")} ${stats}`, tail: errSnip };
12769
13731
  case "failed":
12770
- return { mark: color56.red("\u2717"), stats: `${color56.red("failed")} ${stats}`, tail: errSnip };
13732
+ return { mark: color58.red("\u2717"), stats: `${color58.red("failed")} ${stats}`, tail: errSnip };
12771
13733
  }
12772
13734
  }
12773
13735
 
@@ -12785,7 +13747,7 @@ function resolveBundledSkillsDir() {
12785
13747
  try {
12786
13748
  const req2 = createRequire(import.meta.url);
12787
13749
  const corePkg = req2.resolve("@wrongstack/core/package.json");
12788
- return path8.join(path8.dirname(corePkg), "skills");
13750
+ return path9.join(path9.dirname(corePkg), "skills");
12789
13751
  } catch {
12790
13752
  return void 0;
12791
13753
  }
@@ -13035,7 +13997,7 @@ async function boot(argv) {
13035
13997
  } catch {
13036
13998
  }
13037
13999
  printLaunchHints(renderer, flags, {
13038
- cursorFile: path8.join(wpaths.cacheDir, "hint-cursor")
14000
+ cursorFile: path9.join(wpaths.cacheDir, "hint-cursor")
13039
14001
  });
13040
14002
  }
13041
14003
  return {
@@ -13056,7 +14018,7 @@ async function boot(argv) {
13056
14018
  }
13057
14019
  async function checkGitInCwd(opts) {
13058
14020
  const { cwd, renderer, reader } = opts;
13059
- const cwdGit = path8.join(cwd, ".git");
14021
+ const cwdGit = path9.join(cwd, ".git");
13060
14022
  let hasCwdGit = false;
13061
14023
  try {
13062
14024
  await fsp4.access(cwdGit);
@@ -13097,10 +14059,10 @@ async function checkGitInCwd(opts) {
13097
14059
  }
13098
14060
  }
13099
14061
  }
13100
- const parentDir = path8.dirname(cwd);
14062
+ const parentDir = path9.dirname(cwd);
13101
14063
  if (parentDir !== cwd) {
13102
14064
  try {
13103
- await fsp4.access(path8.join(parentDir, ".git"));
14065
+ await fsp4.access(path9.join(parentDir, ".git"));
13104
14066
  renderer.write(
13105
14067
  ` ${color.dim("\u2139")} A ${color.bold(".git")} repo exists in the parent directory: ${color.dim(parentDir)}
13106
14068
  `
@@ -13477,6 +14439,28 @@ async function runRepl(opts) {
13477
14439
  `
13478
14440
  );
13479
14441
  }
14442
+ if (engine.currentState === "stopped") {
14443
+ const goal = await loadGoalSafe(opts);
14444
+ if (goal?.goalState === "completed") {
14445
+ const u = goal.journal.length > 0 ? summarizeUsage(goal) : null;
14446
+ const costLine = u && u.iterationsWithUsage > 0 ? color.dim(` \u2014 ${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out \xB7 ${goal.iterations} iterations`) : goal.iterations > 0 ? color.dim(` \u2014 ${goal.iterations} iterations`) : "";
14447
+ opts.renderer.write(
14448
+ color.green(`
14449
+ \u{1F3AF} Goal completed!${costLine}
14450
+
14451
+ `) + color.dim(" Goal cleared. Use /goal set <mission> to create a new goal.\n")
14452
+ );
14453
+ if (opts.projectRoot) {
14454
+ try {
14455
+ const { unlink: unlink4 } = await import('fs/promises');
14456
+ await unlink4(goalFilePath(opts.projectRoot));
14457
+ } catch {
14458
+ }
14459
+ }
14460
+ }
14461
+ opts.onAutonomy?.("off");
14462
+ continue;
14463
+ }
13480
14464
  } catch (err) {
13481
14465
  opts.renderer.writeError(
13482
14466
  `[eternal] ${err instanceof Error ? err.message : String(err)}`
@@ -13523,6 +14507,28 @@ async function runRepl(opts) {
13523
14507
  `
13524
14508
  );
13525
14509
  }
14510
+ if (engine.currentState === "stopped") {
14511
+ const goal = await loadGoalSafe(opts);
14512
+ if (goal?.goalState === "completed") {
14513
+ const u = goal.journal.length > 0 ? summarizeUsage(goal) : null;
14514
+ const costLine = u && u.iterationsWithUsage > 0 ? color.dim(` \u2014 ${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out \xB7 ${goal.iterations} iterations`) : goal.iterations > 0 ? color.dim(` \u2014 ${goal.iterations} iterations`) : "";
14515
+ opts.renderer.write(
14516
+ color.green(`
14517
+ \u{1F3AF} Goal completed!${costLine}
14518
+
14519
+ `) + color.dim(" Goal cleared. Use /goal set <mission> to create a new goal.\n")
14520
+ );
14521
+ if (opts.projectRoot) {
14522
+ try {
14523
+ const { unlink: unlink4 } = await import('fs/promises');
14524
+ await unlink4(goalFilePath(opts.projectRoot));
14525
+ } catch {
14526
+ }
14527
+ }
14528
+ }
14529
+ opts.onAutonomy?.("off");
14530
+ continue;
14531
+ }
13526
14532
  } catch (err) {
13527
14533
  opts.renderer.writeError(
13528
14534
  `[parallel] ${err instanceof Error ? err.message : String(err)}`
@@ -14296,7 +15302,10 @@ async function execute(deps) {
14296
15302
  logLevel: cfg.log?.level ?? "info",
14297
15303
  auditLevel: cfg.session?.auditLevel ?? "standard",
14298
15304
  indexOnStart: cfg.indexing?.onSessionStart !== false,
14299
- maxIterations: cfg.tools?.maxIterations ?? 500
15305
+ maxIterations: cfg.tools?.maxIterations ?? 500,
15306
+ debugStream: cfg.debugStream ?? false,
15307
+ configScope: cfg.configScope ?? "global",
15308
+ enhanceDelayMs: cfg.autonomy?.enhanceDelayMs ?? 6e4
14300
15309
  };
14301
15310
  },
14302
15311
  async saveSettings(s) {
@@ -14305,6 +15314,7 @@ async function execute(deps) {
14305
15314
  {
14306
15315
  configStore,
14307
15316
  globalConfigPath: wpaths.globalConfig,
15317
+ inProjectConfigPath: wpaths.inProjectConfig,
14308
15318
  vault: { encrypt: (v) => v, decrypt: (v) => v, isEncrypted: () => false }
14309
15319
  },
14310
15320
  (autonomy) => {
@@ -14318,8 +15328,10 @@ async function execute(deps) {
14318
15328
  a["confirmExit"] = s.confirmExit ?? true;
14319
15329
  }
14320
15330
  );
14321
- if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.nextPrediction !== void 0) {
14322
- const raw = await fsp4.readFile(wpaths.globalConfig, "utf8").catch(() => "{}");
15331
+ if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.nextPrediction !== void 0 || s.debugStream !== void 0 || s.configScope !== void 0 || s.enhanceDelayMs !== void 0) {
15332
+ const configScope = s.configScope ?? (configStore.get().configScope ?? "global");
15333
+ const targetPath = configScope === "project" && wpaths.inProjectConfig ? wpaths.inProjectConfig : wpaths.globalConfig;
15334
+ const raw = await fsp4.readFile(targetPath, "utf8").catch(() => "{}");
14323
15335
  const parsed = JSON.parse(raw);
14324
15336
  const vault = { encrypt: (v) => v, decrypt: (v) => v, isEncrypted: () => false };
14325
15337
  const decrypted = decryptConfigSecrets$1(parsed, vault);
@@ -14361,8 +15373,25 @@ async function execute(deps) {
14361
15373
  tools.maxIterations = s.maxIterations;
14362
15374
  decrypted.tools = tools;
14363
15375
  }
14364
- const encrypted = encryptConfigSecrets$1(decrypted, vault);
14365
- await atomicWrite(wpaths.globalConfig, JSON.stringify(encrypted, null, 2), { mode: 384 });
15376
+ if (s.debugStream !== void 0) {
15377
+ decrypted.debugStream = s.debugStream;
15378
+ const { setDebugStreamEnabled } = await import('@wrongstack/providers');
15379
+ setDebugStreamEnabled(s.debugStream);
15380
+ }
15381
+ if (s.configScope !== void 0) {
15382
+ decrypted.configScope = s.configScope;
15383
+ }
15384
+ if (s.enhanceDelayMs !== void 0) {
15385
+ const autonomy = decrypted.autonomy ?? {};
15386
+ autonomy.enhanceDelayMs = s.enhanceDelayMs;
15387
+ decrypted.autonomy = autonomy;
15388
+ }
15389
+ const toWrite = targetPath === wpaths.globalConfig ? decrypted : filterSafeForProject(decrypted);
15390
+ const encrypted = encryptConfigSecrets$1(toWrite, vault);
15391
+ if (targetPath !== wpaths.globalConfig) {
15392
+ await fsp4.mkdir(path9.dirname(targetPath), { recursive: true }).catch((err) => console.debug(`[execution] mkdir failed: ${err}`));
15393
+ }
15394
+ await atomicWrite(targetPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
14366
15395
  configStore.update({
14367
15396
  ...s.nextPrediction !== void 0 ? { nextPrediction: s.nextPrediction } : {},
14368
15397
  ...s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 ? { features: decrypted.features } : {},
@@ -14370,7 +15399,10 @@ async function execute(deps) {
14370
15399
  ...s.logLevel !== void 0 ? { log: decrypted.log } : {},
14371
15400
  ...s.auditLevel !== void 0 ? { session: decrypted.session } : {},
14372
15401
  ...s.indexOnStart !== void 0 ? { indexing: decrypted.indexing } : {},
14373
- ...s.maxIterations !== void 0 ? { tools: decrypted.tools } : {}
15402
+ ...s.maxIterations !== void 0 ? { tools: decrypted.tools } : {},
15403
+ ...s.debugStream !== void 0 ? { debugStream: s.debugStream } : {},
15404
+ ...s.configScope !== void 0 ? { configScope: s.configScope } : {},
15405
+ ...s.enhanceDelayMs !== void 0 ? { autonomy: { ...configStore.get().autonomy ?? {}, enhanceDelayMs: s.enhanceDelayMs } } : {}
14374
15406
  });
14375
15407
  }
14376
15408
  if (s.streamFleet !== void 0) {
@@ -14445,6 +15477,18 @@ async function execute(deps) {
14445
15477
  getModeLabel: () => {
14446
15478
  const metaMode = context.meta?.["mode"];
14447
15479
  return typeof metaMode === "string" ? metaMode : modeId ?? "default";
15480
+ },
15481
+ registerDebugStreamCallback: (cb) => {
15482
+ void import('@wrongstack/providers').then(({ setDebugStreamCallback }) => setDebugStreamCallback(cb)).catch(
15483
+ (err) => console.error("[execution] failed to register debug stream callback:", err)
15484
+ );
15485
+ },
15486
+ restoreDebugStreamCallback: () => {
15487
+ void import('@wrongstack/providers').then(
15488
+ ({ setDebugStreamCallback, defaultDebugStreamCallback }) => setDebugStreamCallback(defaultDebugStreamCallback)
15489
+ ).catch(
15490
+ (err) => console.error("[execution] failed to restore debug stream callback:", err)
15491
+ );
14448
15492
  }
14449
15493
  });
14450
15494
  } finally {
@@ -14475,7 +15519,7 @@ async function execute(deps) {
14475
15519
  supportsVision,
14476
15520
  attachments,
14477
15521
  effectiveMaxContext,
14478
- projectName: path8.basename(projectRoot) || void 0,
15522
+ projectName: path9.basename(projectRoot) || void 0,
14479
15523
  projectRoot,
14480
15524
  getAutonomy,
14481
15525
  onAutonomy,
@@ -14490,7 +15534,7 @@ async function execute(deps) {
14490
15534
  onAgentIterationComplete: director ? (tokens) => director.setLeaderContextPressure(tokens) : void 0
14491
15535
  });
14492
15536
  } finally {
14493
- await webuiPromise.catch(() => void 0);
15537
+ await webuiPromise.catch((err) => console.debug(`[execution] webui shutdown failed: ${err}`));
14494
15538
  }
14495
15539
  } else {
14496
15540
  code = await runRepl({
@@ -14503,7 +15547,7 @@ async function execute(deps) {
14503
15547
  supportsVision,
14504
15548
  attachments,
14505
15549
  effectiveMaxContext,
14506
- projectName: path8.basename(projectRoot) || void 0,
15550
+ projectName: path9.basename(projectRoot) || void 0,
14507
15551
  getAutonomy,
14508
15552
  onAutonomy,
14509
15553
  getNextPredict,
@@ -14635,7 +15679,7 @@ var MultiAgentHost = class {
14635
15679
  doneCondition: { type: "all_tasks_done" },
14636
15680
  maxConcurrent: this.opts.maxConcurrent ?? 4
14637
15681
  };
14638
- const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path8.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
15682
+ const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path9.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
14639
15683
  this.director = new Director({
14640
15684
  config: coordinatorConfig,
14641
15685
  manifestPath: this.opts.manifestPath,
@@ -15104,16 +16148,16 @@ var MultiAgentHost = class {
15104
16148
  if (this.director) return this.director;
15105
16149
  this.opts.directorMode = true;
15106
16150
  if (this.opts.fleetRoot && !this.opts.manifestPath) {
15107
- this.opts.manifestPath = path8.join(this.opts.fleetRoot, "fleet.json");
16151
+ this.opts.manifestPath = path9.join(this.opts.fleetRoot, "fleet.json");
15108
16152
  }
15109
16153
  if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
15110
- this.opts.sharedScratchpadPath = path8.join(this.opts.fleetRoot, "shared");
16154
+ this.opts.sharedScratchpadPath = path9.join(this.opts.fleetRoot, "shared");
15111
16155
  }
15112
16156
  if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
15113
- this.opts.sessionsRoot = path8.join(this.opts.fleetRoot, "subagents");
16157
+ this.opts.sessionsRoot = path9.join(this.opts.fleetRoot, "subagents");
15114
16158
  }
15115
16159
  if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
15116
- this.opts.stateCheckpointPath = path8.join(this.opts.fleetRoot, "director-state.json");
16160
+ this.opts.stateCheckpointPath = path9.join(this.opts.fleetRoot, "director-state.json");
15117
16161
  }
15118
16162
  await this.ensureDirector();
15119
16163
  return this.director ?? null;
@@ -15285,11 +16329,11 @@ var SessionStats = class {
15285
16329
  if (e.name === "bash") this.bashCommands++;
15286
16330
  else if (e.name === "fetch") this.fetches++;
15287
16331
  if (!e.ok) return;
15288
- const path26 = typeof input?.path === "string" ? input.path : void 0;
15289
- if (e.name === "read" && path26) this.readPaths.add(path26);
15290
- else if (e.name === "edit" && path26) this.editedPaths.add(path26);
15291
- else if (e.name === "write" && path26) {
15292
- this.writtenPaths.add(path26);
16332
+ const path27 = typeof input?.path === "string" ? input.path : void 0;
16333
+ if (e.name === "read" && path27) this.readPaths.add(path27);
16334
+ else if (e.name === "edit" && path27) this.editedPaths.add(path27);
16335
+ else if (e.name === "write" && path27) {
16336
+ this.writtenPaths.add(path27);
15293
16337
  const content = typeof input?.content === "string" ? input.content : "";
15294
16338
  this.bytesWritten += Buffer.byteLength(content, "utf8");
15295
16339
  }
@@ -15894,7 +16938,12 @@ async function setupCompaction(params) {
15894
16938
  effectiveMaxContext,
15895
16939
  // Calibrated estimator: recordActualUsage() is called after each API
15896
16940
  // response so this converges on real token counts for compaction decisions.
15897
- (ctx) => estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
16941
+ (ctx) => estimateRequestTokensCalibrated(
16942
+ ctx.messages,
16943
+ ctx.systemPrompt,
16944
+ ctx.tools ?? [],
16945
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
16946
+ ).total,
15898
16947
  initialPolicy.thresholds,
15899
16948
  {
15900
16949
  aggressiveOn: initialPolicy.aggressiveOn,
@@ -15956,9 +17005,13 @@ function parseModelRef(ref) {
15956
17005
  }
15957
17006
  return { model: trimmed };
15958
17007
  }
15959
- function overloadStatus(err) {
17008
+ function shouldFallback(err) {
17009
+ if (err instanceof StreamHangError) {
17010
+ return 599;
17011
+ }
15960
17012
  if (!(err instanceof ProviderError)) return null;
15961
17013
  const s = err.status;
17014
+ if (s === 0) return s;
15962
17015
  if (s === 429 || s === 529 || s >= 500) return s;
15963
17016
  return null;
15964
17017
  }
@@ -15990,7 +17043,7 @@ function createFallbackModelExtension(deps) {
15990
17043
  const cfg = deps.getConfig();
15991
17044
  const chain = cfg.fallbackModels ?? [];
15992
17045
  for (const ref of chain) {
15993
- const status = overloadStatus(lastErr);
17046
+ const status = shouldFallback(lastErr);
15994
17047
  if (status === null) break;
15995
17048
  const parsed = parseModelRef(ref);
15996
17049
  if (!parsed.model) continue;
@@ -16107,7 +17160,7 @@ function setupMetrics(params) {
16107
17160
  const dumpMetrics = () => {
16108
17161
  if (!metricsSink) return;
16109
17162
  try {
16110
- const out = path8.join(wpaths.projectSessions, "metrics.json");
17163
+ const out = path9.join(wpaths.projectSessions, "metrics.json");
16111
17164
  const snap = metricsSink.snapshot();
16112
17165
  writeFileSync(out, JSON.stringify(snap, null, 2));
16113
17166
  } catch {
@@ -16182,7 +17235,7 @@ async function setupCodebaseIndexing(deps) {
16182
17235
  if (tool?.mutating && FILE_EDIT_TOOLS.has(tool.name)) {
16183
17236
  const fp = payload.toolUse.input?.file_path;
16184
17237
  if (typeof fp === "string" && fp.length > 0) {
16185
- const abs = path8.resolve(payload.ctx.cwd, fp);
17238
+ const abs = path9.resolve(payload.ctx.cwd, fp);
16186
17239
  if (isIndexableFile(abs)) {
16187
17240
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
16188
17241
  }
@@ -16197,11 +17250,11 @@ async function setupCodebaseIndexing(deps) {
16197
17250
  let watcher;
16198
17251
  if (idx.watchExternal) {
16199
17252
  try {
16200
- watcher = fs13.watch(projectRoot, { recursive: true }, (_event, filename) => {
17253
+ watcher = fs14.watch(projectRoot, { recursive: true }, (_event, filename) => {
16201
17254
  if (!filename) return;
16202
17255
  const rel = filename.toString();
16203
17256
  if (isIgnored(rel)) return;
16204
- const abs = path8.resolve(projectRoot, rel);
17257
+ const abs = path9.resolve(projectRoot, rel);
16205
17258
  if (!isIndexableFile(abs)) return;
16206
17259
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
16207
17260
  });
@@ -16448,7 +17501,7 @@ async function setupSession(params) {
16448
17501
  } = params;
16449
17502
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
16450
17503
  if (count > 0) renderer.writeInfo(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
16451
- }).catch(() => void 0);
17504
+ }).catch((err) => console.debug(`[session] prune failed: ${err}`));
16452
17505
  let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
16453
17506
  const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
16454
17507
  if (!resumeId && !flags["no-recovery"]) {
@@ -16490,9 +17543,9 @@ async function setupSession(params) {
16490
17543
  const sessionRef = { current: session };
16491
17544
  await recoveryLock.write(session?.id).catch(() => void 0);
16492
17545
  const attachments = new DefaultAttachmentStore({
16493
- spoolDir: path8.join(wpaths.projectSessions, session?.id, "attachments")
17546
+ spoolDir: path9.join(wpaths.projectSessions, session?.id, "attachments")
16494
17547
  });
16495
- const queueStore = new QueueStore({ dir: path8.join(wpaths.projectSessions, session?.id) });
17548
+ const queueStore = new QueueStore({ dir: path9.join(wpaths.projectSessions, session?.id) });
16496
17549
  const ctxSignal = new AbortController().signal;
16497
17550
  const context = new Context({
16498
17551
  systemPrompt,
@@ -16505,7 +17558,7 @@ async function setupSession(params) {
16505
17558
  model: config.model
16506
17559
  });
16507
17560
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
16508
- const todosCheckpointPath = path8.join(wpaths.projectSessions, `${session?.id}.todos.json`);
17561
+ const todosCheckpointPath = path9.join(wpaths.projectSessions, `${session?.id}.todos.json`);
16509
17562
  if (resumeId) {
16510
17563
  try {
16511
17564
  const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
@@ -16523,15 +17576,15 @@ async function setupSession(params) {
16523
17576
  todosCheckpointPath,
16524
17577
  session?.id
16525
17578
  );
16526
- const planPath = path8.join(wpaths.projectSessions, `${session?.id}.plan.json`);
17579
+ const planPath = path9.join(wpaths.projectSessions, `${session?.id}.plan.json`);
16527
17580
  context.state.setMeta("plan.path", planPath);
16528
- const taskPath = path8.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
17581
+ const taskPath = path9.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
16529
17582
  context.state.setMeta("task.path", taskPath);
16530
17583
  let dirState;
16531
17584
  if (resumeId) {
16532
17585
  try {
16533
- const fleetRoot = path8.join(wpaths.projectSessions, session?.id);
16534
- dirState = await loadDirectorState(path8.join(fleetRoot, "director-state.json"));
17586
+ const fleetRoot = path9.join(wpaths.projectSessions, session?.id);
17587
+ dirState = await loadDirectorState(path9.join(fleetRoot, "director-state.json"));
16535
17588
  if (dirState) {
16536
17589
  const tCounts = {};
16537
17590
  for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
@@ -16571,7 +17624,7 @@ function resolveBundledSkillsDir2() {
16571
17624
  try {
16572
17625
  const req2 = createRequire(import.meta.url);
16573
17626
  const corePkg = req2.resolve("@wrongstack/core/package.json");
16574
- return path8.join(path8.dirname(corePkg), "skills");
17627
+ return path9.join(path9.dirname(corePkg), "skills");
16575
17628
  } catch {
16576
17629
  return void 0;
16577
17630
  }
@@ -16682,6 +17735,9 @@ async function main(argv) {
16682
17735
  updateInfo
16683
17736
  } = ctx;
16684
17737
  updateInfo = await printUpdateNotice(updateInfo);
17738
+ const { setDebugStreamEnabled, setDebugStreamCallback, defaultDebugStreamCallback } = await import('@wrongstack/providers');
17739
+ if (config.debugStream) setDebugStreamEnabled(true);
17740
+ setDebugStreamCallback(defaultDebugStreamCallback);
16685
17741
  const pathResolver = new DefaultPathResolver(cwd);
16686
17742
  const events = new EventBus();
16687
17743
  events.setLogger(logger);
@@ -16768,7 +17824,7 @@ async function main(argv) {
16768
17824
  modeId,
16769
17825
  modePrompt,
16770
17826
  modelCapabilities,
16771
- planPath: () => sessionRef.current ? path8.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
17827
+ planPath: () => sessionRef.current ? path9.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
16772
17828
  contributors: [
16773
17829
  // Injects the ETERNAL AUTONOMY block when the user has activated
16774
17830
  // a long-running autonomy engine. Without this, the per-iteration
@@ -17158,12 +18214,12 @@ async function main(argv) {
17158
18214
  }
17159
18215
  }
17160
18216
  };
17161
- const fleetRoot = directorMode ? path8.join(wpaths.projectSessions, session.id) : void 0;
17162
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path8.join(expectDefined(fleetRoot), "fleet.json") : void 0;
17163
- const sharedScratchpadPath = directorMode ? path8.join(expectDefined(fleetRoot), "shared") : void 0;
17164
- const subagentSessionsRoot = directorMode ? path8.join(expectDefined(fleetRoot), "subagents") : void 0;
17165
- const stateCheckpointPath = directorMode ? path8.join(expectDefined(fleetRoot), "director-state.json") : void 0;
17166
- const fleetRootForPromotion = path8.join(wpaths.projectSessions, session.id);
18217
+ const fleetRoot = directorMode ? path9.join(wpaths.projectSessions, session.id) : void 0;
18218
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path9.join(expectDefined(fleetRoot), "fleet.json") : void 0;
18219
+ const sharedScratchpadPath = directorMode ? path9.join(expectDefined(fleetRoot), "shared") : void 0;
18220
+ const subagentSessionsRoot = directorMode ? path9.join(expectDefined(fleetRoot), "subagents") : void 0;
18221
+ const stateCheckpointPath = directorMode ? path9.join(expectDefined(fleetRoot), "director-state.json") : void 0;
18222
+ const fleetRootForPromotion = path9.join(wpaths.projectSessions, session.id);
17167
18223
  const brainQueue = new BrainDecisionQueue(events);
17168
18224
  const brain = new ObservableBrainArbiter(
17169
18225
  new HumanEscalatingBrainArbiter(new DefaultBrainArbiter(), brainQueue),
@@ -17301,6 +18357,13 @@ async function main(argv) {
17301
18357
  enhanceController,
17302
18358
  llmProvider: provider,
17303
18359
  llmModel: config.model,
18360
+ createProvider: (pid) => {
18361
+ try {
18362
+ return buildProviderForId(pid);
18363
+ } catch {
18364
+ return void 0;
18365
+ }
18366
+ },
17304
18367
  statuslineConfig: statuslineConfigDeps,
17305
18368
  statuslineHiddenItems: [...currentHiddenItems],
17306
18369
  setStatuslineHiddenItems,
@@ -17521,7 +18584,7 @@ async function main(argv) {
17521
18584
  return director.spawn(cfg);
17522
18585
  },
17523
18586
  onFleetLog: async (subagentId, mode) => {
17524
- const subagentsRoot = path8.join(fleetRootForPromotion, "subagents");
18587
+ const subagentsRoot = path9.join(fleetRootForPromotion, "subagents");
17525
18588
  let runDirs;
17526
18589
  try {
17527
18590
  runDirs = await fsp4.readdir(subagentsRoot);
@@ -17530,7 +18593,7 @@ async function main(argv) {
17530
18593
  }
17531
18594
  const found = [];
17532
18595
  for (const runId of runDirs) {
17533
- const runDir = path8.join(subagentsRoot, runId);
18596
+ const runDir = path9.join(subagentsRoot, runId);
17534
18597
  let files;
17535
18598
  try {
17536
18599
  files = await fsp4.readdir(runDir);
@@ -17539,7 +18602,7 @@ async function main(argv) {
17539
18602
  }
17540
18603
  for (const f of files) {
17541
18604
  if (!f.endsWith(".jsonl")) continue;
17542
- const full = path8.join(runDir, f);
18605
+ const full = path9.join(runDir, f);
17543
18606
  try {
17544
18607
  const stat4 = await fsp4.stat(full);
17545
18608
  found.push({
@@ -17640,7 +18703,7 @@ async function main(argv) {
17640
18703
  }
17641
18704
  const dir = await multiAgentHost.ensureDirector();
17642
18705
  if (!dir) return "Director is not available.";
17643
- const dirStatePath = path8.join(fleetRootForPromotion, "director-state.json");
18706
+ const dirStatePath = path9.join(fleetRootForPromotion, "director-state.json");
17644
18707
  const prior = await loadDirectorState(dirStatePath);
17645
18708
  if (!prior) {
17646
18709
  return "No prior director-state.json found \u2014 nothing to retry.";
@@ -17709,9 +18772,9 @@ async function main(argv) {
17709
18772
  for (const tool of director2.tools(FLEET_ROSTER)) {
17710
18773
  toolRegistry.register(tool);
17711
18774
  }
17712
- const mp = path8.join(fleetRootForPromotion, "fleet.json");
17713
- const sp = path8.join(fleetRootForPromotion, "shared");
17714
- const ss = path8.join(fleetRootForPromotion, "subagents");
18775
+ const mp = path9.join(fleetRootForPromotion, "fleet.json");
18776
+ const sp = path9.join(fleetRootForPromotion, "shared");
18777
+ const ss = path9.join(fleetRootForPromotion, "subagents");
17715
18778
  const lines = [
17716
18779
  `${color.green("\u2713")} Promoted to director mode.`,
17717
18780
  ` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
@@ -17861,6 +18924,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
17861
18924
  message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
17862
18925
  };
17863
18926
  }
18927
+ return void 0;
17864
18928
  },
17865
18929
  onClear: () => {
17866
18930
  if (flags.tui && !flags["no-tui"]) return;