@wrongstack/cli 0.119.1 → 0.148.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, 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 } 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 path9 from 'path';
5
+ import * as path10 from 'path';
6
6
  import { join } from 'path';
7
7
  import { createRequire } from 'module';
8
8
  import * as os2 from 'os';
@@ -11,14 +11,14 @@ import * as crypto2 from 'crypto';
11
11
  import { randomUUID } from 'crypto';
12
12
  import { findFreePort, createHttpServer, openBrowser, registerInstance, unregisterInstance } from '@wrongstack/webui/server';
13
13
  import { WebSocketServer, WebSocket } from 'ws';
14
- import { spawn } from 'child_process';
14
+ import { spawn, exec, execFileSync } from 'child_process';
15
15
  import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
16
16
  import { capabilitiesFor, buildProviderFactoriesFromRegistry, makeProviderFromConfig } from '@wrongstack/providers';
17
17
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
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 fs14 from 'fs';
21
+ import * as fs15 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';
@@ -96,7 +96,7 @@ async function loadConfigProviders(configPath2, vault, opts) {
96
96
  }
97
97
  async function mutateConfigProviders(configPath2, vault, mutator) {
98
98
  let raw;
99
- let fileExists = true;
99
+ let fileExists2 = true;
100
100
  try {
101
101
  raw = await fsp4.readFile(configPath2, "utf8");
102
102
  } catch (err) {
@@ -106,14 +106,14 @@ async function mutateConfigProviders(configPath2, vault, mutator) {
106
106
  { cause: err }
107
107
  );
108
108
  }
109
- fileExists = false;
109
+ fileExists2 = false;
110
110
  raw = "{}";
111
111
  }
112
112
  let parsed;
113
113
  try {
114
114
  parsed = JSON.parse(raw);
115
115
  } catch (err) {
116
- if (fileExists) {
116
+ if (fileExists2) {
117
117
  throw new Error(
118
118
  `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
119
119
  { cause: err }
@@ -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 = path9.join(projectRoot, "package.json");
389
+ const pkgPath = path10.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 = path9.join(projectRoot, "tsconfig.json");
405
+ const tsconfigPath = path10.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 = path9.join(projectRoot, "src");
411
+ const srcDir = path10.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 path9.join(homeFn(), ".wrongstack", "update-cache.json");
1605
+ return path10.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 = path9.dirname(cachePath(homeFn));
1642
+ const dir = path10.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 = path9.resolve(path9.dirname(serverEntry), "..");
1749
+ const distDir = path10.resolve(path10.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 ? path9.dirname(opts.globalConfigPath) : void 0;
1766
+ const registryBaseDir = opts.globalConfigPath ? path10.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: path9.basename(opts.projectRoot) || opts.projectRoot,
1775
+ projectName: path10.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,12 @@ async function runWebUI(opts) {
2160
2159
  await handleProviderRemove(ws, m.payload.providerId);
2161
2160
  break;
2162
2161
  }
2162
+ default: {
2163
+ console.debug(
2164
+ `[WebUI] Unhandled message type: ${String(msg.type)}`
2165
+ );
2166
+ break;
2167
+ }
2163
2168
  }
2164
2169
  }
2165
2170
  async function handleUserMessage(ws, _client, content) {
@@ -2400,7 +2405,7 @@ async function runWebUI(opts) {
2400
2405
  }
2401
2406
  }
2402
2407
  function getVault() {
2403
- const keyFile = path9.join(path9.dirname(opts.globalConfigPath ?? ""), ".key");
2408
+ const keyFile = path10.join(path10.dirname(opts.globalConfigPath ?? ""), ".key");
2404
2409
  return new DefaultSecretVault({ keyFile });
2405
2410
  }
2406
2411
  async function loadSavedProviders() {
@@ -2701,10 +2706,10 @@ async function detectPackageManager(root, declared) {
2701
2706
  const name = declared.split("@")[0];
2702
2707
  if (name) return name;
2703
2708
  }
2704
- if (await pathExists(path9.join(root, "pnpm-lock.yaml"))) return "pnpm";
2705
- if (await pathExists(path9.join(root, "bun.lockb"))) return "bun";
2706
- if (await pathExists(path9.join(root, "bun.lock"))) return "bun";
2707
- if (await pathExists(path9.join(root, "yarn.lock"))) return "yarn";
2709
+ if (await pathExists(path10.join(root, "pnpm-lock.yaml"))) return "pnpm";
2710
+ if (await pathExists(path10.join(root, "bun.lockb"))) return "bun";
2711
+ if (await pathExists(path10.join(root, "bun.lock"))) return "bun";
2712
+ if (await pathExists(path10.join(root, "yarn.lock"))) return "yarn";
2708
2713
  return "npm";
2709
2714
  }
2710
2715
  function hasUsableScript(scripts, name) {
@@ -2725,7 +2730,7 @@ function parseMakeTargets(makefile) {
2725
2730
  async function detectProjectFacts(root) {
2726
2731
  const facts = { hints: [] };
2727
2732
  try {
2728
- const pkg = JSON.parse(await fsp4.readFile(path9.join(root, "package.json"), "utf8"));
2733
+ const pkg = JSON.parse(await fsp4.readFile(path10.join(root, "package.json"), "utf8"));
2729
2734
  const scripts = pkg.scripts ?? {};
2730
2735
  const pm = await detectPackageManager(root, pkg.packageManager);
2731
2736
  if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
@@ -2739,14 +2744,14 @@ async function detectProjectFacts(root) {
2739
2744
  } catch {
2740
2745
  }
2741
2746
  try {
2742
- if (!await pathExists(path9.join(root, "pyproject.toml"))) throw new Error("not python");
2747
+ if (!await pathExists(path10.join(root, "pyproject.toml"))) throw new Error("not python");
2743
2748
  facts.test ??= "pytest";
2744
2749
  facts.lint ??= "ruff check .";
2745
2750
  facts.hints.push("pyproject.toml");
2746
2751
  } catch {
2747
2752
  }
2748
2753
  try {
2749
- if (!await pathExists(path9.join(root, "go.mod"))) throw new Error("not go");
2754
+ if (!await pathExists(path10.join(root, "go.mod"))) throw new Error("not go");
2750
2755
  facts.build ??= "go build ./...";
2751
2756
  facts.test ??= "go test ./...";
2752
2757
  facts.run ??= "go run .";
@@ -2754,7 +2759,7 @@ async function detectProjectFacts(root) {
2754
2759
  } catch {
2755
2760
  }
2756
2761
  try {
2757
- if (!await pathExists(path9.join(root, "Cargo.toml"))) throw new Error("not rust");
2762
+ if (!await pathExists(path10.join(root, "Cargo.toml"))) throw new Error("not rust");
2758
2763
  facts.build ??= "cargo build";
2759
2764
  facts.test ??= "cargo test";
2760
2765
  facts.lint ??= "cargo clippy";
@@ -2763,7 +2768,7 @@ async function detectProjectFacts(root) {
2763
2768
  } catch {
2764
2769
  }
2765
2770
  try {
2766
- const makefile = await fsp4.readFile(path9.join(root, "Makefile"), "utf8");
2771
+ const makefile = await fsp4.readFile(path10.join(root, "Makefile"), "utf8");
2767
2772
  const targets = parseMakeTargets(makefile);
2768
2773
  facts.build ??= targets.has("build") ? "make build" : "make";
2769
2774
  if (targets.has("test")) facts.test ??= "make test";
@@ -2873,20 +2878,7 @@ function countToolResults(messages) {
2873
2878
  return count;
2874
2879
  }
2875
2880
  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;
2881
+ return estimateMessageTokens(messages);
2890
2882
  }
2891
2883
 
2892
2884
  // src/slash-commands/auth.ts
@@ -3071,7 +3063,7 @@ function formatPhaseList(graph) {
3071
3063
  }
3072
3064
  async function gatherProjectContext(projectRoot) {
3073
3065
  try {
3074
- const raw = await fsp4.readFile(path9.join(projectRoot, "package.json"), "utf8");
3066
+ const raw = await fsp4.readFile(path10.join(projectRoot, "package.json"), "utf8");
3075
3067
  const pkg = JSON.parse(raw);
3076
3068
  const parts = [
3077
3069
  `Project: ${String(pkg.name ?? "unknown")}`,
@@ -3187,6 +3179,7 @@ function buildAutoPhaseCommand(opts) {
3187
3179
  };
3188
3180
  }
3189
3181
  }
3182
+ return { message: `Unknown subcommand "${sub}". Run \`/autophase\` for usage.` };
3190
3183
  }
3191
3184
  };
3192
3185
  }
@@ -3874,7 +3867,7 @@ ${formatContextWindowModeList(active)}`;
3874
3867
  const lines = [
3875
3868
  `${color.bold("Context Window")}`,
3876
3869
  ` messages: ${messages.length} total (${countTurnPairs(messages)} user+assistant pairs)`,
3877
- ` tokens (est): ${estimateTokens(messages).toLocaleString()} (chars / 4 estimate)`,
3870
+ ` tokens (est): ${estimateTokens(messages).toLocaleString()} (\u2248 chars/3.5)`,
3878
3871
  ` mode: ${policy ? `${policy.id} (${policy.name})` : "balanced"}`,
3879
3872
  ` limit: ${formatLimit(readEffectiveLimit(ctx, opts))}`,
3880
3873
  ` system prompt: ${ctx.systemPrompt.length} block${ctx.systemPrompt.length !== 1 ? "s" : ""}`,
@@ -3968,6 +3961,94 @@ function formatLimit(limit) {
3968
3961
  function pct(n) {
3969
3962
  return `${Math.round(n * 100)}%`;
3970
3963
  }
3964
+ var DEFAULT_TIMEOUT_MS = 6e4;
3965
+ var MAX_OUTPUT_LINES = 500;
3966
+ function runCommand(cmd, cwd, timeout) {
3967
+ return new Promise((resolve5) => {
3968
+ exec(
3969
+ cmd,
3970
+ {
3971
+ cwd,
3972
+ timeout,
3973
+ maxBuffer: 2 * 1024 * 1024,
3974
+ // 2 MB
3975
+ windowsHide: true
3976
+ },
3977
+ (error, stdout, stderr) => {
3978
+ resolve5({
3979
+ stdout,
3980
+ stderr,
3981
+ exitCode: error?.code ?? 0,
3982
+ killed: error?.killed ?? false
3983
+ });
3984
+ }
3985
+ );
3986
+ });
3987
+ }
3988
+ function formatOutput(cmd, result, elapsed) {
3989
+ const lines = [];
3990
+ const exitLabel = result.killed ? color.red("TIMEOUT") : result.exitCode === 0 ? color.green("OK") : color.red(`EXIT ${result.exitCode}`);
3991
+ lines.push(`${color.cyan("$")} ${color.bold(cmd)} ${exitLabel} ${color.dim(`${elapsed}ms`)}`);
3992
+ const combined = (result.stdout + result.stderr).trimEnd();
3993
+ if (combined) {
3994
+ const outputLines = combined.split("\n");
3995
+ const truncated = outputLines.length > MAX_OUTPUT_LINES;
3996
+ const shown = truncated ? outputLines.slice(0, MAX_OUTPUT_LINES) : outputLines;
3997
+ lines.push("");
3998
+ lines.push(color.dim("\u2500\u2500"));
3999
+ for (const line of shown) {
4000
+ lines.push(line);
4001
+ }
4002
+ if (truncated) {
4003
+ lines.push(color.dim(`\u2026 (truncated, showing first ${MAX_OUTPUT_LINES} of ${outputLines.length} lines)`));
4004
+ }
4005
+ lines.push(color.dim("\u2500\u2500"));
4006
+ } else {
4007
+ lines.push(color.dim("(no output)"));
4008
+ }
4009
+ return lines.join("\n");
4010
+ }
4011
+ function buildDevCommand(opts) {
4012
+ return {
4013
+ name: "dev",
4014
+ category: "Run",
4015
+ description: "Run a shell command and see the output (LLM does not see it).",
4016
+ argsHint: "<shell command>",
4017
+ help: [
4018
+ "Usage:",
4019
+ " /dev <shell command> Run a command from the chat input.",
4020
+ "",
4021
+ "Examples:",
4022
+ " /dev pnpm release:check",
4023
+ " /dev git diff --stat",
4024
+ " /dev ls -la src/",
4025
+ "",
4026
+ "The command runs in the current working directory. Output is displayed",
4027
+ "in the chat history but is NOT fed to the LLM \u2014 use this for your own",
4028
+ "eyes only. Timeout: 60s. Max output: 500 lines.",
4029
+ "",
4030
+ "This is a convenience shortcut \u2014 equivalent to switching to a terminal",
4031
+ "tab. For commands the LLM should see, use the `exec` tool instead."
4032
+ ].join("\n"),
4033
+ async run(args, _ctx) {
4034
+ const cmd = args.trim();
4035
+ if (!cmd) {
4036
+ return { message: `${color.yellow("Usage:")} /dev <shell command>
4037
+
4038
+ Examples:
4039
+ /dev pnpm release:check
4040
+ /dev git diff --stat` };
4041
+ }
4042
+ const cwd = opts.cwd;
4043
+ const startedAt = Date.now();
4044
+ opts.renderer.write(color.dim(`$ ${cmd}`));
4045
+ const result = await runCommand(cmd, cwd, DEFAULT_TIMEOUT_MS);
4046
+ const elapsed = Date.now() - startedAt;
4047
+ const display = formatOutput(cmd, result, elapsed);
4048
+ return { message: display };
4049
+ }
4050
+ };
4051
+ }
3971
4052
 
3972
4053
  // src/slash-commands/diag-stats.ts
3973
4054
  function buildDiagCommand(opts) {
@@ -4001,7 +4082,7 @@ function resolvePersistPath(deps) {
4001
4082
  return deps.globalConfigPath;
4002
4083
  }
4003
4084
  async function ensureProjectDir(filePath) {
4004
- const dir = path9.dirname(filePath);
4085
+ const dir = path10.dirname(filePath);
4005
4086
  try {
4006
4087
  await fsp4.mkdir(dir, { recursive: true });
4007
4088
  } catch {
@@ -4040,21 +4121,21 @@ async function persistAutonomySetting(deps, mutator) {
4040
4121
  const targetPath = resolvePersistPath(deps);
4041
4122
  await ensureProjectDir(targetPath);
4042
4123
  let raw;
4043
- let fileExists = true;
4124
+ let fileExists2 = true;
4044
4125
  try {
4045
4126
  raw = await fsp4.readFile(targetPath, "utf8");
4046
4127
  } catch (err) {
4047
4128
  if (err.code !== "ENOENT") {
4048
4129
  throw new Error(`Could not read ${targetPath}: ${err.message}`);
4049
4130
  }
4050
- fileExists = false;
4131
+ fileExists2 = false;
4051
4132
  raw = "{}";
4052
4133
  }
4053
4134
  let parsed;
4054
4135
  try {
4055
4136
  parsed = JSON.parse(raw);
4056
4137
  } catch (err) {
4057
- if (fileExists) {
4138
+ if (fileExists2) {
4058
4139
  throw new Error(`Config at ${targetPath} is not valid JSON: ${err.message}`);
4059
4140
  }
4060
4141
  parsed = {};
@@ -4077,21 +4158,21 @@ async function persistConfigSetting(deps, mutator) {
4077
4158
  const targetPath = resolvePersistPath(deps);
4078
4159
  await ensureProjectDir(targetPath);
4079
4160
  let raw;
4080
- let fileExists = true;
4161
+ let fileExists2 = true;
4081
4162
  try {
4082
4163
  raw = await fsp4.readFile(targetPath, "utf8");
4083
4164
  } catch (err) {
4084
4165
  if (err.code !== "ENOENT") {
4085
4166
  throw new Error(`Could not read ${targetPath}: ${err.message}`);
4086
4167
  }
4087
- fileExists = false;
4168
+ fileExists2 = false;
4088
4169
  raw = "{}";
4089
4170
  }
4090
4171
  let parsed;
4091
4172
  try {
4092
4173
  parsed = JSON.parse(raw);
4093
4174
  } catch (err) {
4094
- if (fileExists) {
4175
+ if (fileExists2) {
4095
4176
  throw new Error(`Config at ${targetPath} is not valid JSON: ${err.message}`);
4096
4177
  }
4097
4178
  parsed = {};
@@ -4110,21 +4191,21 @@ async function persistConfigSetting(deps, mutator) {
4110
4191
  }
4111
4192
  async function persistTelegramConfig(deps, mutator) {
4112
4193
  let raw;
4113
- let fileExists = true;
4194
+ let fileExists2 = true;
4114
4195
  try {
4115
4196
  raw = await fsp4.readFile(deps.globalConfigPath, "utf8");
4116
4197
  } catch (err) {
4117
4198
  if (err.code !== "ENOENT") {
4118
4199
  throw new Error(`Could not read ${deps.globalConfigPath}: ${err.message}`);
4119
4200
  }
4120
- fileExists = false;
4201
+ fileExists2 = false;
4121
4202
  raw = "{}";
4122
4203
  }
4123
4204
  let parsed;
4124
4205
  try {
4125
4206
  parsed = JSON.parse(raw);
4126
4207
  } catch (err) {
4127
- if (fileExists) {
4208
+ if (fileExists2) {
4128
4209
  throw new Error(`Config at ${deps.globalConfigPath} is not valid JSON: ${err.message}`);
4129
4210
  }
4130
4211
  parsed = {};
@@ -4137,7 +4218,7 @@ async function persistTelegramConfig(deps, mutator) {
4137
4218
  decrypted.extensions = extensions;
4138
4219
  const encrypted = encryptConfigSecrets$1(decrypted, deps.vault);
4139
4220
  await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
4140
- deps.configStore.update({ extensions: decrypted.extensions });
4221
+ deps.configStore.update({ extensions });
4141
4222
  }
4142
4223
 
4143
4224
  // src/slash-commands/enhance.ts
@@ -4218,6 +4299,7 @@ function buildEnhanceCommand(opts) {
4218
4299
 
4219
4300
  // src/slash-commands/fix-classifier.ts
4220
4301
  var TS = ["typescript-strict"];
4302
+ var TC = ["tech-stack"];
4221
4303
  var BH = ["bug-hunter"];
4222
4304
  var SS = ["security-scanner"];
4223
4305
  var NM = ["node-modern"];
@@ -4592,6 +4674,90 @@ var P = [
4592
4674
  detail: "JavaScript runtime error",
4593
4675
  conf: 0.95
4594
4676
  },
4677
+ // ── Tech stack validation — BEFORE generic dep patterns ─────────────
4678
+ // Technology choice questions: "should I use X?", "is Y deprecated?", etc.
4679
+ {
4680
+ pat: /\b(should I (use|install|add|pick|choose)|is .+ (still (good|maintained|supported|relevant)|deprecated|dead|obsolete|outdated)|what replaces|alternative to|instead of)\b/i,
4681
+ cat: "tech",
4682
+ sub: "tech-choice",
4683
+ lang: void 0,
4684
+ hints: TC,
4685
+ detail: "Technology choice validation",
4686
+ conf: 0.85
4687
+ },
4688
+ {
4689
+ pat: /\bwhat (is the latest|are the latest|version of|versions of)|what version|upgrade to latest|downgrade to|which version of\b/i,
4690
+ cat: "tech",
4691
+ sub: "version-check",
4692
+ lang: void 0,
4693
+ hints: TC,
4694
+ detail: "Version verification needed",
4695
+ conf: 0.9
4696
+ },
4697
+ {
4698
+ pat: /\b(adding|installing|using|switching to|migrating to) (a |an |the )?(package|dependency|library|module|framework|gem|crate)\b/i,
4699
+ cat: "tech",
4700
+ sub: "tech-choice",
4701
+ lang: void 0,
4702
+ hints: TC,
4703
+ detail: "Technology choice validation",
4704
+ conf: 0.8
4705
+ },
4706
+ // Commands that imply tech choices: "pip install X", "cargo add X", etc.
4707
+ {
4708
+ pat: /\b(pip install|pip3 install|pipenv install|poetry add|uv add)\s+[a-zA-Z0-9_-]+/i,
4709
+ cat: "tech",
4710
+ sub: "python-pkg",
4711
+ lang: "python",
4712
+ hints: TC,
4713
+ detail: "Python package choice \u2014 validate before installing",
4714
+ conf: 0.85
4715
+ },
4716
+ {
4717
+ pat: /\b(cargo add|cargo install)\s+[a-zA-Z0-9_-]+/i,
4718
+ cat: "tech",
4719
+ sub: "rust-crate",
4720
+ lang: "rust",
4721
+ hints: TC,
4722
+ detail: "Rust crate choice \u2014 validate before installing",
4723
+ conf: 0.85
4724
+ },
4725
+ {
4726
+ pat: /\b(go get|go install)\s+[a-zA-Z0-9_./-]+/i,
4727
+ cat: "tech",
4728
+ sub: "go-module",
4729
+ lang: "go",
4730
+ hints: TC,
4731
+ detail: "Go module choice \u2014 validate before installing",
4732
+ conf: 0.85
4733
+ },
4734
+ {
4735
+ pat: /\b(gem install|bundle add)\s+[a-zA-Z0-9_-]+/i,
4736
+ cat: "tech",
4737
+ sub: "ruby-gem",
4738
+ lang: "ruby",
4739
+ hints: TC,
4740
+ detail: "Ruby gem choice \u2014 validate before installing",
4741
+ conf: 0.85
4742
+ },
4743
+ {
4744
+ pat: /\b(npm install|pnpm add|yarn add)\s+[a-zA-Z0-9@/_-]+/i,
4745
+ cat: "tech",
4746
+ sub: "js-pkg",
4747
+ lang: "javascript",
4748
+ hints: TC,
4749
+ detail: "JS package choice \u2014 validate before installing",
4750
+ conf: 0.85
4751
+ },
4752
+ {
4753
+ pat: /\b(composer require|nuget install|dotnet add package)\s+[a-zA-Z0-9./_-]+/i,
4754
+ cat: "tech",
4755
+ sub: "pkg-choice",
4756
+ lang: void 0,
4757
+ hints: TC,
4758
+ detail: "Package choice \u2014 validate before installing",
4759
+ conf: 0.85
4760
+ },
4595
4761
  // ── Dependency / Import ───────────────────────────────────────────────
4596
4762
  {
4597
4763
  pat: /\bcannot find module|modulenotfounderror|no such module|missing module/i,
@@ -4728,7 +4894,7 @@ function needsSubagent(c) {
4728
4894
  return c.confidence < 0.85;
4729
4895
  }
4730
4896
  function isSimpleFix(c) {
4731
- return c.category === "ts" && c.confidence >= 0.9 || c.category === "runtime" && c.subcategory === "null-undefined-access" && c.confidence >= 0.85;
4897
+ return c.category === "ts" && c.confidence >= 0.9 || c.category === "runtime" && c.subcategory === "null-undefined-access" && c.confidence >= 0.85 || c.category === "tech" && c.confidence >= 0.85;
4732
4898
  }
4733
4899
 
4734
4900
  // src/slash-commands/fix.ts
@@ -4824,6 +4990,22 @@ function buildDirective(cli, errorText) {
4824
4990
  "3. Fix the config",
4825
4991
  "4. Verify"
4826
4992
  ].join("\n");
4993
+ case "tech":
4994
+ return [
4995
+ `## Tech Stack Validation${lang}`,
4996
+ "",
4997
+ "```",
4998
+ `${errorText}`,
4999
+ "```",
5000
+ "",
5001
+ "Your task:",
5002
+ "1. Detect the ecosystem from project files or context",
5003
+ "2. Verify the package/version against the correct registry",
5004
+ "3. Check if the package is dead, deprecated, or superseded (prehistoric)",
5005
+ "4. Check if the language standard library already covers this need",
5006
+ "5. Report APPROVED (with install command) or REJECTED (with replacement + migration)",
5007
+ `6. Use "This isn't code, this is X-year-old technology" when rejecting on age grounds`
5008
+ ].join("\n");
4827
5009
  case "perf":
4828
5010
  return [
4829
5011
  `## Fix: Performance / Memory Issue${lang}`,
@@ -4863,6 +5045,8 @@ function delegateRoleFor(cli) {
4863
5045
  return "security-scanner";
4864
5046
  case "perf":
4865
5047
  return "refactor-planner";
5048
+ case "tech":
5049
+ return "tech-stack";
4866
5050
  default:
4867
5051
  return "bug-hunter";
4868
5052
  }
@@ -4909,7 +5093,8 @@ Docker, Git, CI/CD, and more.
4909
5093
  | Security / secrets | Any | \`security-scanner\` |
4910
5094
  | Compiler error | Rust, Go, C/C++, Python | \`bug-hunter\` |
4911
5095
  | Dependency / import | Any | \`bug-hunter\` |
4912
- | Performance / leak | Any | \`bug-hunter\` + \`refactor-planner\` |
5096
+ | Tech choice / pkg | Any | \`tech-stack\` |
5097
+ | Performance / leak | Any | \`bug-hunter\` + \`refactor-planner\` |
4913
5098
  | Infrastructure | Config, Docker, Git, CI | \`bug-hunter\` |
4914
5099
  | React / Next.js | JavaScript | \`react-modern\` |
4915
5100
  | Node.js | JavaScript | \`node-modern\` |
@@ -4931,6 +5116,10 @@ When the error confidence is low (< 0.85) or the problem spans multiple files,
4931
5116
  /fix react-dom.development.js:172 Error: Invalid hook call
4932
5117
  /fix Security: hardcoded API key in config.ts
4933
5118
  /fix ERRO1014: SQL injection vulnerability in query builder
5119
+ /fix Should I use axios for API calls?
5120
+ /fix is moment.js still maintained?
5121
+ /fix pip install urllib2
5122
+ /fix what replaces lodash?
4934
5123
  \`\`\`
4935
5124
  `,
4936
5125
  async run(args, _ctx) {
@@ -4952,6 +5141,7 @@ When the error confidence is low (< 0.85) or the problem spans multiple files,
4952
5141
  ` /fix ${color.dim("AttributeError: 'NoneType' object has no attribute 'encode'")}`,
4953
5142
  ` /fix ${color.dim("react-dom.development.js:172 Error: Invalid hook call")}`,
4954
5143
  ` /fix ${color.dim("Security: hardcoded API key in config.ts")}`,
5144
+ ` /fix ${color.dim("Should I use axios for API calls?")}`,
4955
5145
  "",
4956
5146
  "Run `/help fix` for full documentation."
4957
5147
  ].join("\n")
@@ -5605,6 +5795,7 @@ ${formatGoal(updated)}` };
5605
5795
  } catch {
5606
5796
  }
5607
5797
  if (opts.onEternalStop) opts.onEternalStop();
5798
+ if (opts.onAutonomy) opts.onAutonomy("off");
5608
5799
  const msg = `${color.amber("Goal cleared.")} Previous goal marked abandoned; eternal mode will stop.`;
5609
5800
  opts.renderer.write(msg);
5610
5801
  return { message: msg };
@@ -5749,28 +5940,52 @@ function buildInitCommand(opts) {
5749
5940
  category: "Config",
5750
5941
  description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
5751
5942
  async run(_args, ctx) {
5752
- const dir = path9.join(ctx.projectRoot, ".wrongstack");
5753
- const file = path9.join(dir, "AGENTS.md");
5943
+ const dir = path10.join(ctx.projectRoot, ".wrongstack");
5944
+ const file = path10.join(dir, "AGENTS.md");
5945
+ const isFirstInit = !await fileExists(file);
5754
5946
  const detected = await detectProjectFacts(ctx.projectRoot);
5755
5947
  const body = renderAgentsTemplate(detected);
5756
5948
  await fsp4.mkdir(dir, { recursive: true });
5757
5949
  await fsp4.writeFile(file, body, "utf8");
5950
+ let nodePkg = false;
5951
+ try {
5952
+ await fsp4.access(path10.join(ctx.projectRoot, "package.json"));
5953
+ nodePkg = true;
5954
+ } catch {
5955
+ }
5956
+ const lines = [];
5957
+ lines.push(`Wrote ${file}`);
5758
5958
  if (detected.hints.length > 0) {
5759
- const msg2 = `Wrote ${file}
5760
- Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
5761
5959
  opts.renderer.writeInfo(`Wrote ${file}`);
5762
- opts.renderer.writeInfo(
5763
- `Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`
5764
- );
5765
- return { message: msg2 };
5960
+ const hintLine = `Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
5961
+ opts.renderer.writeInfo(hintLine);
5962
+ lines.push(hintLine);
5963
+ } else {
5964
+ opts.renderer.writeInfo(`Wrote ${file}`);
5965
+ lines.push("No project type auto-detected. Edit the file with project context and instructions the system prompt should carry.");
5766
5966
  }
5767
- const msg = `Wrote ${file}
5768
- No project type auto-detected. Edit the file with project context and instructions the system prompt should carry.`;
5769
- opts.renderer.writeInfo(`Wrote ${file}`);
5770
- return { message: msg };
5967
+ if (nodePkg && isFirstInit) {
5968
+ const techHint = [
5969
+ "",
5970
+ `${color.cyan("\u{1F4A1}")} ${color.bold("Tech Stack Audit")} \u2014 This is a Node.js project with a fresh init.`,
5971
+ color.dim(" The LLM may have suggested stale version numbers. Run"),
5972
+ ` ${color.cyan("/techstack --init")} to scan dependencies and verify versions.`
5973
+ ].join("\n");
5974
+ opts.renderer.write(techHint);
5975
+ lines.push(techHint);
5976
+ }
5977
+ return { message: lines.join("\n") };
5771
5978
  }
5772
5979
  };
5773
5980
  }
5981
+ async function fileExists(filePath) {
5982
+ try {
5983
+ await fsp4.access(filePath);
5984
+ return true;
5985
+ } catch {
5986
+ return false;
5987
+ }
5988
+ }
5774
5989
  function parseMcpArgs(args) {
5775
5990
  const trimmed = args.trim();
5776
5991
  if (!trimmed || trimmed === "list") return { action: "list", name: "" };
@@ -5965,9 +6180,9 @@ function stateBadge(state) {
5965
6180
  return color.dim(state);
5966
6181
  }
5967
6182
  }
5968
- async function readConfig(path27) {
6183
+ async function readConfig(path28) {
5969
6184
  try {
5970
- return JSON.parse(await fsp4.readFile(path27, "utf8"));
6185
+ return JSON.parse(await fsp4.readFile(path28, "utf8"));
5971
6186
  } catch {
5972
6187
  return {};
5973
6188
  }
@@ -5975,11 +6190,11 @@ async function readConfig(path27) {
5975
6190
  function isMcpServerRecord(value) {
5976
6191
  return !!value && typeof value === "object" && !Array.isArray(value);
5977
6192
  }
5978
- async function writeConfig(path27, cfg) {
6193
+ async function writeConfig(path28, cfg) {
5979
6194
  const raw = JSON.stringify(cfg, null, 2);
5980
- const tmp = path27 + ".tmp";
6195
+ const tmp = path28 + ".tmp";
5981
6196
  await fsp4.writeFile(tmp, raw, "utf8");
5982
- await fsp4.rename(tmp, path27);
6197
+ await fsp4.rename(tmp, path28);
5983
6198
  }
5984
6199
 
5985
6200
  // src/slash-commands/mcp.ts
@@ -6509,18 +6724,18 @@ var noOpVault2 = {
6509
6724
  };
6510
6725
  async function patchGlobalConfig(globalConfigPath, mutate) {
6511
6726
  let raw = "{}";
6512
- let fileExists = true;
6727
+ let fileExists2 = true;
6513
6728
  try {
6514
6729
  raw = await fsp4.readFile(globalConfigPath, "utf8");
6515
6730
  } catch (err) {
6516
6731
  if (err.code !== "ENOENT") throw err;
6517
- fileExists = false;
6732
+ fileExists2 = false;
6518
6733
  }
6519
6734
  let parsed;
6520
6735
  try {
6521
6736
  parsed = JSON.parse(raw);
6522
6737
  } catch (err) {
6523
- if (fileExists) {
6738
+ if (fileExists2) {
6524
6739
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
6525
6740
  }
6526
6741
  parsed = {};
@@ -6727,31 +6942,51 @@ function buildModelsCommand(opts) {
6727
6942
  function buildNextCommand(opts) {
6728
6943
  return {
6729
6944
  name: "next",
6730
- category: "Config",
6731
- description: "Toggle next-task prediction \u2014 show likely next steps after each turn.",
6732
- argsHint: "[on|off|toggle]",
6945
+ category: "Agent",
6946
+ description: "Show or select next-step suggestions. /next 1 to execute, /next list to view.",
6947
+ argsHint: "[on|off|toggle|list|clear|1 2 3...]",
6733
6948
  help: [
6734
6949
  "Usage:",
6735
- " /next Show whether next-task prediction is on or off",
6736
- " /next on Enable \u2014 after each turn, show 1-3 predicted next steps",
6737
- " /next off Disable (default)",
6738
- " /next toggle Flip the current state",
6950
+ " /next Show whether next-task prediction is on or off",
6951
+ " /next on Enable \u2014 after each turn, show 1-3 predicted next steps",
6952
+ " /next off Disable (default)",
6953
+ " /next toggle Flip the current state",
6954
+ " /next list Show the current suggestion list",
6955
+ " /next clear Clear the suggestion list",
6956
+ " /next 1 Execute suggestion #1 as the next agent turn",
6957
+ " /next 1 2 3 Execute suggestions 1, 2, and 3 in sequence",
6958
+ " /next 1,2,3 Same \u2014 comma separators work too",
6739
6959
  "",
6740
- "Predictions are informational only. They come from a cheap single-shot",
6741
- "model call (no tools, no context replay) and are never run automatically \u2014",
6742
- "copy or retype one to act on it. The setting persists across sessions."
6960
+ "Suggestions are generated automatically after each turn (when prediction is",
6961
+ `on) or manually via ${color.cyan("/suggest")}. Selected suggestions execute`,
6962
+ "immediately \u2014 no refinement or classification step."
6743
6963
  ].join("\n"),
6744
6964
  async run(args) {
6965
+ const trimmed = args.trim();
6966
+ if (/^\d[\d,\s]*$/.test(trimmed) && /\d/.test(trimmed)) {
6967
+ return handleSelection(trimmed, opts);
6968
+ }
6969
+ const arg = trimmed.toLowerCase();
6970
+ if (arg === "list" || arg === "ls" || arg === "show") {
6971
+ return handleList(opts);
6972
+ }
6973
+ if (arg === "clear" || arg === "reset") {
6974
+ opts.onSuggestions?.([]);
6975
+ return { message: color.dim("Suggestion list cleared.") };
6976
+ }
6745
6977
  if (!opts.onNextPredict) {
6746
6978
  const msg2 = "Next-task prediction is not available in this session.";
6747
6979
  opts.renderer.writeWarning(msg2);
6748
6980
  return { message: msg2 };
6749
6981
  }
6750
- const arg = args.trim().toLowerCase();
6751
6982
  const current = opts.onNextPredict();
6752
6983
  const label = (on) => on ? `${color.cyan("ON")} ${color.dim("(predicted next steps shown after each turn)")}` : `${color.green("OFF")} ${color.dim("(no predictions)")}`;
6753
6984
  if (!arg || arg === "status") {
6754
- const msg2 = `Next-task prediction: ${label(current)}`;
6985
+ const suggestions = opts.onSuggestions?.() ?? [];
6986
+ const msg2 = [
6987
+ `Next-task prediction: ${label(current)}`,
6988
+ suggestions.length > 0 ? color.dim(` ${suggestions.length} suggestion(s) available \u2014 use /next list to view, /next 1 to execute`) : color.dim(" No suggestions stored \u2014 use /suggest to generate")
6989
+ ].join("\n");
6755
6990
  opts.renderer.write(msg2);
6756
6991
  return { message: msg2 };
6757
6992
  }
@@ -6763,7 +6998,7 @@ function buildNextCommand(opts) {
6763
6998
  } else if (arg === "toggle" || arg === "cycle") {
6764
6999
  target = !current;
6765
7000
  } else {
6766
- const msg2 = `Unknown argument: ${arg}. Use /next on, off, or toggle.`;
7001
+ const msg2 = `Unknown argument: ${arg}. Use /next on, off, toggle, list, clear, or a number (e.g. /next 1).`;
6767
7002
  opts.renderer.writeWarning(msg2);
6768
7003
  return { message: msg2 };
6769
7004
  }
@@ -6774,6 +7009,67 @@ function buildNextCommand(opts) {
6774
7009
  }
6775
7010
  };
6776
7011
  }
7012
+ function handleSelection(input, opts) {
7013
+ const parts = input.split(/[\s,]+/).filter(Boolean);
7014
+ const indices = parts.map((p) => Number.parseInt(p, 10)).filter((n) => !Number.isNaN(n) && n > 0);
7015
+ if (indices.length === 0) {
7016
+ return { message: color.amber("No valid suggestion numbers found. Use /next 1, /next 1 2 3, etc.") };
7017
+ }
7018
+ const suggestions = opts.onSuggestions?.() ?? [];
7019
+ if (suggestions.length === 0) {
7020
+ return {
7021
+ message: color.amber("No suggestions available. Run /suggest first, or enable prediction with /next on.")
7022
+ };
7023
+ }
7024
+ const invalid = indices.filter((i) => i > suggestions.length);
7025
+ if (invalid.length > 0) {
7026
+ const max = suggestions.length;
7027
+ return {
7028
+ message: color.amber(`Invalid suggestion number(s): ${invalid.join(", ")}. Valid range: 1\u2013${max}.`)
7029
+ };
7030
+ }
7031
+ const selected = indices.map((i) => suggestions[i - 1]).filter((s) => s !== void 0);
7032
+ if (selected.length === 1) {
7033
+ const text = selected[0] ?? "";
7034
+ return {
7035
+ message: `${color.green("\u25B6")} Executing suggestion #${indices[0]}: ${color.dim(text)}`,
7036
+ runText: text
7037
+ };
7038
+ }
7039
+ const tasks = selected.map((s, i) => `${i + 1}. ${s}`).join("\n");
7040
+ const runText = [
7041
+ `## Execute the following tasks in order`,
7042
+ "",
7043
+ tasks,
7044
+ "",
7045
+ "Complete each task before moving to the next. Report results as you go."
7046
+ ].join("\n");
7047
+ return {
7048
+ message: `${color.green("\u25B6")} Executing ${selected.length} tasks: ${indices.join(", ")}`,
7049
+ runText
7050
+ };
7051
+ }
7052
+ function handleList(opts) {
7053
+ const suggestions = opts.onSuggestions?.() ?? [];
7054
+ if (suggestions.length === 0) {
7055
+ return {
7056
+ message: [
7057
+ color.dim("No suggestions available."),
7058
+ "",
7059
+ `Generate suggestions with ${color.cyan("/suggest")} or enable`,
7060
+ `prediction with ${color.cyan("/next on")}.`
7061
+ ].join("\n")
7062
+ };
7063
+ }
7064
+ const lines = [
7065
+ ` ${color.cyan("\u{1F4A1} Suggestions")} ${color.dim(`(use /next 1, /next 1 2 3 to execute)`)}`,
7066
+ ""
7067
+ ];
7068
+ for (let i = 0; i < suggestions.length; i++) {
7069
+ lines.push(` ${color.bold(`${i + 1}.`)} ${suggestions[i]}`);
7070
+ }
7071
+ return { message: lines.join("\n") };
7072
+ }
6777
7073
 
6778
7074
  // src/slash-commands/plugin.ts
6779
7075
  function buildPluginCommand(opts) {
@@ -7111,18 +7407,18 @@ function fmtEntry(e) {
7111
7407
  }
7112
7408
  async function patchGlobalConfig2(globalConfigPath, mutate) {
7113
7409
  let raw = "{}";
7114
- let fileExists = true;
7410
+ let fileExists2 = true;
7115
7411
  try {
7116
7412
  raw = await fsp4.readFile(globalConfigPath, "utf8");
7117
7413
  } catch (err) {
7118
7414
  if (err.code !== "ENOENT") throw err;
7119
- fileExists = false;
7415
+ fileExists2 = false;
7120
7416
  }
7121
7417
  let parsed;
7122
7418
  try {
7123
7419
  parsed = JSON.parse(raw);
7124
7420
  } catch (err) {
7125
- if (fileExists)
7421
+ if (fileExists2)
7126
7422
  throw new Error(`Config at ${globalConfigPath} is not valid JSON: ${err.message}`);
7127
7423
  parsed = {};
7128
7424
  }
@@ -7562,6 +7858,161 @@ function buildModelCapsCommand(opts) {
7562
7858
  }
7563
7859
  };
7564
7860
  }
7861
+ function collectContext(opts) {
7862
+ const parts = [];
7863
+ try {
7864
+ const gitStatus = execFileSync("git", ["status", "--short", "--branch"], {
7865
+ cwd: opts.projectRoot,
7866
+ encoding: "utf8",
7867
+ timeout: 5e3,
7868
+ windowsHide: true
7869
+ }).trim();
7870
+ if (gitStatus) {
7871
+ parts.push("### Git Status", "```", gitStatus, "```");
7872
+ }
7873
+ } catch {
7874
+ }
7875
+ parts.push(`Working directory: ${opts.cwd}`);
7876
+ parts.push(`Project root: ${opts.projectRoot}`);
7877
+ return parts.join("\n");
7878
+ }
7879
+ function buildSuggestPrompt(contextText) {
7880
+ return [
7881
+ "## Suggest Next Steps",
7882
+ "",
7883
+ "Based on the current project state below, generate 3-5 actionable next-step",
7884
+ "suggestions. Each suggestion must be a single imperative sentence that can be",
7885
+ "executed immediately. Be specific \u2014 mention file names, tool names, or commands",
7886
+ "when relevant. Do NOT include preamble, explanation, or wrap in code blocks.",
7887
+ "",
7888
+ "Rules:",
7889
+ '- One suggestion per line, prefixed with the number (e.g. "1. Run tests...")',
7890
+ "- Order by priority: most impactful first",
7891
+ "- Suggestions should be independent \u2014 user can pick any subset",
7892
+ '- If nothing is needed, say "No pending actions \u2014 everything is up to date."',
7893
+ "",
7894
+ contextText || "(No project context available \u2014 suggest generic next steps.)",
7895
+ "",
7896
+ "Output format (strict \u2014 no other text):",
7897
+ "1. First suggestion here",
7898
+ "2. Second suggestion here",
7899
+ "3. Third suggestion here"
7900
+ ].join("\n");
7901
+ }
7902
+ function buildSuggestCommand(opts) {
7903
+ return {
7904
+ name: "suggest",
7905
+ aliases: ["next-steps", "what-next"],
7906
+ category: "Agent",
7907
+ description: "Generate context-aware next-step suggestions for the current session.",
7908
+ argsHint: "[--fast]",
7909
+ help: [
7910
+ "Usage:",
7911
+ " /suggest Generate suggestions using a lightweight subagent",
7912
+ " /suggest --fast Heuristic-only suggestions (no subagent, instant)",
7913
+ "",
7914
+ "Analyzes the current session state (git status, working directory, recent",
7915
+ "activity) and generates 3-5 actionable next-step suggestions. Suggestions",
7916
+ "are stored and can be selected with `/next 1`, `/next 1 2 3`, etc.",
7917
+ "",
7918
+ "Use `/next list` to see the current suggestions at any time."
7919
+ ].join("\n"),
7920
+ async run(args, _ctx) {
7921
+ const trimmed = args.trim().toLowerCase();
7922
+ const fast = /\b(--fast|-f)\b/.test(trimmed);
7923
+ if (fast) {
7924
+ const suggestions = generateHeuristicSuggestions(opts);
7925
+ opts.onSuggestions?.(suggestions);
7926
+ const display = formatSuggestions(suggestions);
7927
+ return { message: display };
7928
+ }
7929
+ if (!opts.onSpawnAndWait) {
7930
+ const suggestions = generateHeuristicSuggestions(opts);
7931
+ opts.onSuggestions?.(suggestions);
7932
+ const display = formatSuggestions(suggestions) + "\n" + color.dim("(Heuristic fallback \u2014 multi-agent not enabled)");
7933
+ return { message: display };
7934
+ }
7935
+ const contextText = collectContext({
7936
+ cwd: opts.cwd,
7937
+ projectRoot: opts.projectRoot
7938
+ });
7939
+ const task = buildSuggestPrompt(contextText);
7940
+ opts.renderer.write(color.dim("Generating suggestions..."));
7941
+ try {
7942
+ const raw = await opts.onSpawnAndWait(task, {
7943
+ name: "suggest"
7944
+ });
7945
+ const suggestions = parseSuggestions(raw);
7946
+ if (suggestions.length === 0) {
7947
+ const fallback = ["No pending actions \u2014 everything is up to date."];
7948
+ opts.onSuggestions?.(fallback);
7949
+ return { message: formatSuggestions(fallback) };
7950
+ }
7951
+ opts.onSuggestions?.(suggestions);
7952
+ return { message: formatSuggestions(suggestions) };
7953
+ } catch (err) {
7954
+ const msg = `Suggestion generation failed: ${err instanceof Error ? err.message : String(err)}`;
7955
+ opts.renderer.writeWarning(msg);
7956
+ return { message: msg };
7957
+ }
7958
+ }
7959
+ };
7960
+ }
7961
+ function parseSuggestions(raw) {
7962
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
7963
+ const numbered = lines.filter((l) => /^\d+[.)]\s/.test(l)).map((l) => l.replace(/^\d+[.)]\s*/, "").trim());
7964
+ if (numbered.length > 0) return numbered.slice(0, 5);
7965
+ const bullets = lines.filter((l) => /^[-*•]\s/.test(l)).map((l) => l.replace(/^[-*•]\s*/, "").trim());
7966
+ if (bullets.length > 0) return bullets.slice(0, 5);
7967
+ return lines.filter((l) => l.length > 10 && !l.startsWith("#") && !l.startsWith("```")).slice(0, 5);
7968
+ }
7969
+ function generateHeuristicSuggestions(opts) {
7970
+ const suggestions = [];
7971
+ try {
7972
+ const gitStatus = execFileSync("git", ["status", "--short"], {
7973
+ cwd: opts.projectRoot,
7974
+ encoding: "utf8",
7975
+ timeout: 5e3,
7976
+ windowsHide: true
7977
+ }).trim();
7978
+ if (gitStatus) {
7979
+ const staged = gitStatus.split("\n").filter((l) => /^[MADRC]/.test(l)).length;
7980
+ const unstaged = gitStatus.split("\n").filter((l) => /^.[MADRC]/.test(l)).length;
7981
+ const untracked = gitStatus.split("\n").filter((l) => l.startsWith("??")).length;
7982
+ if (staged > 0) {
7983
+ suggestions.push(`Commit ${staged} staged file(s) with a descriptive message`);
7984
+ }
7985
+ if (unstaged > 0) {
7986
+ suggestions.push(`Stage and review ${unstaged} modified file(s)`);
7987
+ }
7988
+ if (untracked > 0) {
7989
+ suggestions.push(`Review ${untracked} untracked file(s) \u2014 add to git or .gitignore`);
7990
+ }
7991
+ }
7992
+ } catch {
7993
+ }
7994
+ if (suggestions.length === 0) {
7995
+ suggestions.push("Review recent changes with a diff");
7996
+ suggestions.push("Run the test suite to verify everything passes");
7997
+ suggestions.push("Check for lint or type errors");
7998
+ }
7999
+ return suggestions.slice(0, 5);
8000
+ }
8001
+ function formatSuggestions(suggestions) {
8002
+ if (suggestions.length === 0) {
8003
+ return color.dim("No suggestions available.");
8004
+ }
8005
+ const lines = [
8006
+ ` ${color.cyan("\u{1F4A1} Next steps")} ${color.dim("(use /next 1, /next 2, or /next 1 2 3)")}`,
8007
+ ""
8008
+ ];
8009
+ for (let i = 0; i < suggestions.length; i++) {
8010
+ const num = color.bold(`${i + 1}.`);
8011
+ const text = suggestions[i] ?? "";
8012
+ lines.push(` ${num} ${text}`);
8013
+ }
8014
+ return lines.join("\n");
8015
+ }
7565
8016
  var noOpVault4 = {
7566
8017
  encrypt: (v) => v,
7567
8018
  decrypt: (v) => v,
@@ -7959,7 +8410,7 @@ var DEFAULTS = {
7959
8410
  cost: true
7960
8411
  };
7961
8412
  function resolveConfigPath() {
7962
- return process.env[CONFIG_ENV] ?? path9.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
8413
+ return process.env[CONFIG_ENV] ?? path10.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
7963
8414
  }
7964
8415
  async function loadStatuslineConfig() {
7965
8416
  const p = resolveConfigPath();
@@ -7973,7 +8424,7 @@ async function loadStatuslineConfig() {
7973
8424
  async function saveStatuslineConfig(cfg) {
7974
8425
  const p = resolveConfigPath();
7975
8426
  try {
7976
- await fsp4.mkdir(path9.dirname(p), { recursive: true });
8427
+ await fsp4.mkdir(path10.dirname(p), { recursive: true });
7977
8428
  await atomicWrite(p, JSON.stringify(cfg, null, 2));
7978
8429
  } catch (err) {
7979
8430
  throw new FsError({
@@ -8400,6 +8851,199 @@ function buildWorktreeCommand(opts) {
8400
8851
  }
8401
8852
  };
8402
8853
  }
8854
+ async function discoverPackageFiles(projectRoot) {
8855
+ const files = [];
8856
+ const rootPkg = path10.join(projectRoot, "package.json");
8857
+ try {
8858
+ await fsp4.access(rootPkg);
8859
+ files.push(rootPkg);
8860
+ } catch {
8861
+ }
8862
+ const workspaceFile = path10.join(projectRoot, "pnpm-workspace.yaml");
8863
+ try {
8864
+ await fsp4.access(workspaceFile);
8865
+ const content = await fsp4.readFile(workspaceFile, "utf8");
8866
+ const globMatch = /packages?:\s*\[([^\]]+)\]/s.exec(content);
8867
+ const rawGlobs = globMatch?.[1];
8868
+ if (!rawGlobs) return files;
8869
+ const globs = rawGlobs.split(/[\s,]+/).filter(Boolean).map((g) => g.replace(/['"]/g, ""));
8870
+ for (const g of globs) {
8871
+ const dirPrefix = g.replace(/\/?\*$/, "").replace(/\/\*$/, "");
8872
+ const dir = path10.join(projectRoot, dirPrefix);
8873
+ try {
8874
+ const entries = await fsp4.readdir(dir, { withFileTypes: true });
8875
+ for (const e of entries) {
8876
+ if (!e.isDirectory()) continue;
8877
+ const subPkg = path10.join(dir, e.name, "package.json");
8878
+ try {
8879
+ await fsp4.access(subPkg);
8880
+ files.push(subPkg);
8881
+ } catch {
8882
+ }
8883
+ }
8884
+ } catch {
8885
+ }
8886
+ }
8887
+ } catch {
8888
+ }
8889
+ return files;
8890
+ }
8891
+ function buildTechStackTask(opts) {
8892
+ const pkgList = opts.packageFiles.map((f) => ` - ${path10.relative(opts.projectRoot, f)}`).join("\n");
8893
+ const header = opts.isInit ? [
8894
+ "## Tech Stack Audit \u2014 First-Time Project Init",
8895
+ "",
8896
+ "This project is being initialized for the first time. Scan its dependencies,",
8897
+ "check every package and framework version against the npm registry, and produce",
8898
+ "a report that warns about outdated choices. The LLM that scaffolded this project",
8899
+ "may have suggested stale version numbers \u2014 verify every single one.",
8900
+ ""
8901
+ ].join("\n") : [
8902
+ "## Tech Stack Audit \u2014 Full Project Scan",
8903
+ "",
8904
+ "Scan all project dependencies, verify every package version against the npm",
8905
+ "registry, and produce a report that flags outdated, dead, or obsolete packages.",
8906
+ ""
8907
+ ].join("\n");
8908
+ const outputPath = opts.outputFormat === "json" ? "techstack.json" : "techstack.md";
8909
+ return [
8910
+ header,
8911
+ "",
8912
+ "### Project package files to scan",
8913
+ `${pkgList || " - package.json (root only)"}`,
8914
+ "",
8915
+ "### Instructions",
8916
+ "",
8917
+ "1. **Read** each package.json and extract ALL dependencies (dependencies +",
8918
+ " devDependencies + peerDependencies). Include the workspace root.",
8919
+ "",
8920
+ "2. **For every dependency**, look up its latest version from the npm registry:",
8921
+ ' - `fetch("https://registry.npmjs.org/<package>/latest")`',
8922
+ " - Extract the `version` field from the JSON response",
8923
+ " - Also check `description`, `license`, and `time` fields for age/dead checks",
8924
+ "",
8925
+ "3. **For each package, determine status:**",
8926
+ " - \u{1F7E2} CURRENT: installed version is within 1 minor of latest",
8927
+ " - \u{1F7E1} OUTDATED: installed version is behind latest (major or >1 minor gap)",
8928
+ " - \u{1F534} CRITICAL: package has known CVEs, is deprecated, or >2 years without release",
8929
+ " - \u2620\uFE0F DEAD: package is deprecated, archived, or superseded \u22655 years ago",
8930
+ "",
8931
+ "4. **Apply the tech-stack skill rules:**",
8932
+ ' - Reject packages that are "prehistoric" (superseded \u22655 years ago)',
8933
+ " - Flag dead packages (no release >2 years + critical issues)",
8934
+ " - Prefer Node.js built-ins over third-party packages",
8935
+ "",
8936
+ `5. **Write the report** to \`${outputPath}\` in the project root:`,
8937
+ " - Markdown format: grouped by category, with version tables and warnings",
8938
+ " - JSON format: structured array with name, current, latest, status, notes",
8939
+ "",
8940
+ "6. **Report a summary to chat**: total packages scanned, counts per status,",
8941
+ " top 5 most urgent issues, and total cost estimate (optional).",
8942
+ "",
8943
+ "### Output format (markdown)",
8944
+ "",
8945
+ "```markdown",
8946
+ "# Tech Stack Report",
8947
+ "",
8948
+ "Generated: <date> \xB7 Scanned: <total> packages across <N> files",
8949
+ "",
8950
+ "## \u{1F7E2} Up to Date (<count>)",
8951
+ "| Package | Current | Latest | Age | Notes |",
8952
+ "|---------|---------|--------|-----|-------|",
8953
+ "",
8954
+ "## \u{1F7E1} Outdated (<count>)",
8955
+ "...",
8956
+ "",
8957
+ "## \u{1F534} Critical Issues (<count>)",
8958
+ "...",
8959
+ "",
8960
+ "## \u2620\uFE0F Dead / Obsolete (<count>)",
8961
+ "...",
8962
+ "",
8963
+ "## Recommendations",
8964
+ "- Top 3-5 actionable fixes",
8965
+ "```",
8966
+ "",
8967
+ "### Guardrails",
8968
+ "",
8969
+ "- Use `fetch()` with `AbortSignal.timeout(10000)` on every npm registry call.",
8970
+ "- Skip packages that return 404 (private/internal packages).",
8971
+ "- Deduplicate: same package in multiple package.json files = one row.",
8972
+ "- Do NOT modify any files except writing the report.",
8973
+ "- Run in 2-3 iterations max. Parallel fetch where possible.",
8974
+ "- **IMPORTANT**: Output the chat summary FIRST, then write the file. I need to see results."
8975
+ ].join("\n");
8976
+ }
8977
+ function buildTechStackCommand(opts) {
8978
+ return {
8979
+ name: "techstack",
8980
+ category: "Inspect",
8981
+ aliases: ["tech", "deps"],
8982
+ description: "Scan all project dependencies, verify versions against npm, and produce a techstack report.",
8983
+ argsHint: "[--json] [--init]",
8984
+ help: [
8985
+ "Usage:",
8986
+ " /techstack Scan dependencies + write techstack.md report",
8987
+ " /techstack --json Write techstack.json instead of markdown",
8988
+ " /techstack --init Init-mode scan (compares scaffolded vs latest)",
8989
+ "",
8990
+ "Spawns a subagent that:",
8991
+ " 1. Reads every package.json in the project",
8992
+ " 2. Looks up latest versions on the npm registry",
8993
+ " 3. Flags outdated, dead, or obsolete packages",
8994
+ ` 4. Writes a ${color.cyan("techstack.md")} (or .json) report to the project root`,
8995
+ "",
8996
+ "Uses the `tech-stack` skill for version verification rules.",
8997
+ `Hooked into ${color.cyan("/init")} \u2014 runs automatically on first project setup.`
8998
+ ].join("\n"),
8999
+ async run(args, _ctx) {
9000
+ const trimmed = args.trim().toLowerCase();
9001
+ const outputFormat = /\b(--json|-j)\b/.test(trimmed) ? "json" : "md";
9002
+ const isInit = /\b(--init|-i)\b/.test(trimmed);
9003
+ let packageFiles = [];
9004
+ let discoveryNote = "";
9005
+ try {
9006
+ packageFiles = await discoverPackageFiles(opts.projectRoot);
9007
+ if (packageFiles.length === 0) {
9008
+ discoveryNote = color.amber(
9009
+ "\u26A0 No package.json files found. This does not look like a Node.js project."
9010
+ );
9011
+ }
9012
+ } catch (err) {
9013
+ discoveryNote = color.red(
9014
+ `Could not scan for package files: ${err instanceof Error ? err.message : String(err)}`
9015
+ );
9016
+ }
9017
+ const task = buildTechStackTask({
9018
+ projectRoot: opts.projectRoot,
9019
+ packageFiles,
9020
+ outputFormat,
9021
+ isInit
9022
+ });
9023
+ if (!opts.onSpawnAndWait) {
9024
+ const msg = "Multi-agent is not enabled in this session. Cannot spawn techstack subagent.";
9025
+ opts.renderer.writeWarning(msg);
9026
+ return { message: msg };
9027
+ }
9028
+ const header = isInit ? "Tech Stack Init Audit" : "Tech Stack Audit";
9029
+ const label = `${color.cyan("\u{1F50D}")} ${color.bold(header)} ${color.dim(`(${packageFiles.length} package files)`)}`;
9030
+ opts.renderer.write(label);
9031
+ if (discoveryNote) opts.renderer.write(discoveryNote);
9032
+ opts.renderer.write(
9033
+ color.dim(`Spawning tech-stack subagent \u2192 writes ${outputFormat === "json" ? "techstack.json" : "techstack.md"} when done.`)
9034
+ );
9035
+ try {
9036
+ const name = isInit ? "techstack-init" : "techstack-audit";
9037
+ const summary = await opts.onSpawnAndWait(task, { name });
9038
+ return { message: summary };
9039
+ } catch (err) {
9040
+ const msg = `Tech stack scan failed: ${err instanceof Error ? err.message : String(err)}`;
9041
+ opts.renderer.writeWarning(msg);
9042
+ return { message: msg };
9043
+ }
9044
+ }
9045
+ };
9046
+ }
8403
9047
  function buildYoloCommand(opts) {
8404
9048
  return {
8405
9049
  name: "yolo",
@@ -8458,11 +9102,14 @@ function buildBuiltinSlashCommands(opts) {
8458
9102
  buildClearCommand(opts),
8459
9103
  buildCompactCommand(opts),
8460
9104
  buildContextCommand(opts),
9105
+ buildDevCommand(opts),
8461
9106
  buildCodebaseReindexCommand(opts),
9107
+ buildTechStackCommand(opts),
8462
9108
  buildToolsCommand(opts),
8463
9109
  buildPluginCommand(opts),
8464
9110
  buildPruneCommand(opts),
8465
9111
  buildMcpSlashCommand(opts),
9112
+ buildSuggestCommand(opts),
8466
9113
  buildAuthCommand(opts),
8467
9114
  buildDiagCommand(opts),
8468
9115
  buildStatsCommand(opts),
@@ -8520,13 +9167,13 @@ var MANIFESTS = [
8520
9167
  ];
8521
9168
  async function detectProjectKind(projectRoot) {
8522
9169
  try {
8523
- await fsp4.access(path9.join(projectRoot, ".wrongstack", "AGENTS.md"));
9170
+ await fsp4.access(path10.join(projectRoot, ".wrongstack", "AGENTS.md"));
8524
9171
  return "initialized";
8525
9172
  } catch {
8526
9173
  }
8527
9174
  for (const m of MANIFESTS) {
8528
9175
  try {
8529
- await fsp4.access(path9.join(projectRoot, m));
9176
+ await fsp4.access(path10.join(projectRoot, m));
8530
9177
  return "project";
8531
9178
  } catch {
8532
9179
  }
@@ -8534,8 +9181,8 @@ async function detectProjectKind(projectRoot) {
8534
9181
  return "empty";
8535
9182
  }
8536
9183
  async function scaffoldAgentsMd(projectRoot) {
8537
- const dir = path9.join(projectRoot, ".wrongstack");
8538
- const file = path9.join(dir, "AGENTS.md");
9184
+ const dir = path10.join(projectRoot, ".wrongstack");
9185
+ const file = path10.join(dir, "AGENTS.md");
8539
9186
  const facts = await detectProjectFacts(projectRoot);
8540
9187
  const body = renderAgentsTemplate(facts);
8541
9188
  await fsp4.mkdir(dir, { recursive: true });
@@ -8548,7 +9195,7 @@ async function runProjectCheck(opts) {
8548
9195
  if (kind === "initialized") {
8549
9196
  renderer.write(
8550
9197
  `
8551
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path9.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
9198
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path10.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
8552
9199
  `
8553
9200
  );
8554
9201
  return true;
@@ -8579,7 +9226,7 @@ async function runProjectCheck(opts) {
8579
9226
  }
8580
9227
  return true;
8581
9228
  }
8582
- const gitDir = path9.join(projectRoot, ".git");
9229
+ const gitDir = path10.join(projectRoot, ".git");
8583
9230
  let hasGit = false;
8584
9231
  try {
8585
9232
  await fsp4.access(gitDir);
@@ -8773,7 +9420,7 @@ var ReadlineInputReader = class {
8773
9420
  history = [];
8774
9421
  pending = false;
8775
9422
  constructor(opts = {}) {
8776
- this.historyFile = opts.historyFile ?? path9.join(os2.homedir(), ".wrongstack", "history");
9423
+ this.historyFile = opts.historyFile ?? path10.join(os2.homedir(), ".wrongstack", "history");
8777
9424
  }
8778
9425
  async loadHistory() {
8779
9426
  try {
@@ -8785,7 +9432,7 @@ var ReadlineInputReader = class {
8785
9432
  }
8786
9433
  async saveHistory() {
8787
9434
  try {
8788
- await fsp4.mkdir(path9.dirname(this.historyFile), { recursive: true });
9435
+ await fsp4.mkdir(path10.dirname(this.historyFile), { recursive: true });
8789
9436
  await fsp4.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
8790
9437
  } catch {
8791
9438
  }
@@ -8823,7 +9470,10 @@ var ReadlineInputReader = class {
8823
9470
  const fresh = this.ensure();
8824
9471
  this.installPromptGuard(fresh);
8825
9472
  return new Promise((resolve5) => {
9473
+ let settled = false;
8826
9474
  const settle = (line) => {
9475
+ if (settled) return;
9476
+ settled = true;
8827
9477
  setOutputLineGuard(null);
8828
9478
  resolve5(line);
8829
9479
  };
@@ -8835,6 +9485,7 @@ var ReadlineInputReader = class {
8835
9485
  settle(line);
8836
9486
  });
8837
9487
  fresh.once("close", () => settle(""));
9488
+ fresh.on && fresh.on("error", (_e) => settle(""));
8838
9489
  }).then((result) => {
8839
9490
  this.rl?.close();
8840
9491
  return result;
@@ -9074,20 +9725,20 @@ function assertSafeToDelete(filename, parentDir) {
9074
9725
  if (PROTECTED_BASENAMES.has(filename)) {
9075
9726
  throw new Error(`Refusing to delete protected file: ${filename}`);
9076
9727
  }
9077
- if (filename !== path9.basename(filename)) {
9728
+ if (filename !== path10.basename(filename)) {
9078
9729
  throw new Error(`Refusing to delete path with traversal: ${filename}`);
9079
9730
  }
9080
9731
  if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
9081
9732
  throw new Error(`Refusing to delete unknown file: ${filename}`);
9082
9733
  }
9083
- const resolvedParent = path9.resolve(parentDir);
9734
+ const resolvedParent = path10.resolve(parentDir);
9084
9735
  if (!resolvedParent.endsWith(".wrongstack")) {
9085
9736
  throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
9086
9737
  }
9087
9738
  }
9088
9739
  async function safeDelete(filePath) {
9089
- const dir = path9.dirname(filePath);
9090
- const filename = path9.basename(filePath);
9740
+ const dir = path10.dirname(filePath);
9741
+ const filename = path10.basename(filePath);
9091
9742
  try {
9092
9743
  assertSafeToDelete(filename, dir);
9093
9744
  await fsp4.unlink(filePath);
@@ -9132,16 +9783,16 @@ function diffSummary(oldCfg, newCfg) {
9132
9783
  }
9133
9784
  var defaultHomeDir = () => os2__default.homedir();
9134
9785
  function historyDir(homeFn = defaultHomeDir) {
9135
- return path9.join(homeFn(), ".wrongstack", "config.history", "entries");
9786
+ return path10.join(homeFn(), ".wrongstack", "config.history", "entries");
9136
9787
  }
9137
9788
  function historyIndexPath(homeFn = defaultHomeDir) {
9138
- return path9.join(homeFn(), ".wrongstack", "config.history", "index.json");
9789
+ return path10.join(homeFn(), ".wrongstack", "config.history", "index.json");
9139
9790
  }
9140
9791
  function configPath(homeFn = defaultHomeDir) {
9141
- return path9.join(homeFn(), ".wrongstack", "config.json");
9792
+ return path10.join(homeFn(), ".wrongstack", "config.json");
9142
9793
  }
9143
9794
  function backupLastPath(homeFn = defaultHomeDir) {
9144
- return path9.join(homeFn(), ".wrongstack", "config.json.last");
9795
+ return path10.join(homeFn(), ".wrongstack", "config.json.last");
9145
9796
  }
9146
9797
  function entryId(ts) {
9147
9798
  return ts.replace(/[:.]/g, "-").slice(0, 19);
@@ -9196,17 +9847,17 @@ async function backupCurrent(homeFn = defaultHomeDir) {
9196
9847
  }
9197
9848
  if (content !== void 0) {
9198
9849
  try {
9199
- const bakPath = path9.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
9850
+ const bakPath = path10.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
9200
9851
  await atomicWrite(bakPath, content);
9201
9852
  } catch {
9202
9853
  }
9203
9854
  }
9204
9855
  try {
9205
- const dir = path9.join(homeFn(), ".wrongstack");
9856
+ const dir = path10.join(homeFn(), ".wrongstack");
9206
9857
  const files = await fsp4.readdir(dir);
9207
9858
  const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
9208
9859
  for (const f of baks.slice(10)) {
9209
- await safeDelete(path9.join(dir, f));
9860
+ await safeDelete(path10.join(dir, f));
9210
9861
  }
9211
9862
  } catch {
9212
9863
  }
@@ -9224,7 +9875,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
9224
9875
  };
9225
9876
  try {
9226
9877
  await fsp4.writeFile(
9227
- path9.join(historyDir(homeFn), `${id}.json`),
9878
+ path10.join(historyDir(homeFn), `${id}.json`),
9228
9879
  JSON.stringify(entry, null, 2),
9229
9880
  "utf8"
9230
9881
  );
@@ -9232,7 +9883,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
9232
9883
  throw new FsError({
9233
9884
  message: err instanceof Error ? err.message : String(err),
9234
9885
  code: ERROR_CODES.FS_WRITE_FAILED,
9235
- path: path9.join(historyDir(homeFn), `${id}.json`),
9886
+ path: path10.join(historyDir(homeFn), `${id}.json`),
9236
9887
  cause: err
9237
9888
  });
9238
9889
  }
@@ -9247,7 +9898,7 @@ async function listHistory(homeFn = defaultHomeDir) {
9247
9898
  }
9248
9899
  async function getHistoryEntry(id, homeFn = defaultHomeDir) {
9249
9900
  try {
9250
- const raw = await fsp4.readFile(path9.join(historyDir(homeFn), `${id}.json`), "utf8");
9901
+ const raw = await fsp4.readFile(path10.join(historyDir(homeFn), `${id}.json`), "utf8");
9251
9902
  return JSON.parse(raw);
9252
9903
  } catch {
9253
9904
  return null;
@@ -9313,10 +9964,10 @@ var theme = { primary: color.amber };
9313
9964
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os2__default.homedir()) {
9314
9965
  try {
9315
9966
  const { atomicWrite: atomicWrite14 } = await import('@wrongstack/core');
9316
- const fs29 = await import('fs/promises');
9967
+ const fs30 = await import('fs/promises');
9317
9968
  let existing = {};
9318
9969
  try {
9319
- const raw = await fs29.readFile(configPath2, "utf8");
9970
+ const raw = await fs30.readFile(configPath2, "utf8");
9320
9971
  existing = JSON.parse(raw);
9321
9972
  } catch {
9322
9973
  }
@@ -9652,12 +10303,12 @@ function pickGroupIndex(opts) {
9652
10303
  try {
9653
10304
  let current = 0;
9654
10305
  try {
9655
- const parsed = Number.parseInt(fs14.readFileSync(opts.cursorFile, "utf8").trim(), 10);
10306
+ const parsed = Number.parseInt(fs15.readFileSync(opts.cursorFile, "utf8").trim(), 10);
9656
10307
  if (Number.isFinite(parsed)) current = wrap(parsed);
9657
10308
  } catch {
9658
10309
  }
9659
- fs14.mkdirSync(path9.dirname(opts.cursorFile), { recursive: true });
9660
- fs14.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
10310
+ fs15.mkdirSync(path10.dirname(opts.cursorFile), { recursive: true });
10311
+ fs15.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
9661
10312
  return current;
9662
10313
  } catch {
9663
10314
  }
@@ -9993,14 +10644,14 @@ function summarize(value, name) {
9993
10644
  if (typeof v === "object" && v !== null) {
9994
10645
  const o = v;
9995
10646
  if (name === "edit") {
9996
- const path27 = typeof o["path"] === "string" ? o["path"] : "";
10647
+ const path28 = typeof o["path"] === "string" ? o["path"] : "";
9997
10648
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
9998
- return `${path27} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
10649
+ return `${path28} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
9999
10650
  }
10000
10651
  if (name === "write") {
10001
- const path27 = typeof o["path"] === "string" ? o["path"] : "";
10652
+ const path28 = typeof o["path"] === "string" ? o["path"] : "";
10002
10653
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
10003
- return bytes !== void 0 ? `${path27} ${bytes}B` : path27;
10654
+ return bytes !== void 0 ? `${path28} ${bytes}B` : path28;
10004
10655
  }
10005
10656
  if (typeof o["count"] === "number") {
10006
10657
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -11218,7 +11869,7 @@ var doctorCmd = async (_args, deps) => {
11218
11869
  }
11219
11870
  try {
11220
11871
  await fsp4.mkdir(deps.paths.projectSessions, { recursive: true });
11221
- const probe = path9.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
11872
+ const probe = path10.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
11222
11873
  await fsp4.writeFile(probe, "");
11223
11874
  await fsp4.unlink(probe);
11224
11875
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
@@ -11321,8 +11972,8 @@ var exportCmd = async (args, deps) => {
11321
11972
  return 1;
11322
11973
  }
11323
11974
  if (output) {
11324
- await fsp4.mkdir(path9.dirname(path9.resolve(deps.cwd, output)), { recursive: true });
11325
- await fsp4.writeFile(path9.resolve(deps.cwd, output), rendered, "utf8");
11975
+ await fsp4.mkdir(path10.dirname(path10.resolve(deps.cwd, output)), { recursive: true });
11976
+ await fsp4.writeFile(path10.resolve(deps.cwd, output), rendered, "utf8");
11326
11977
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
11327
11978
  `);
11328
11979
  } else {
@@ -11395,8 +12046,8 @@ var initCmd = async (_args, deps) => {
11395
12046
  const vault = new DefaultSecretVault({ keyFile: deps.paths.secretsKey });
11396
12047
  const encrypted = encryptConfigSecrets(config, vault);
11397
12048
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2), { mode: 384 });
11398
- await fsp4.mkdir(path9.join(deps.projectRoot, ".wrongstack"), { recursive: true });
11399
- const agentsFile = path9.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
12049
+ await fsp4.mkdir(path10.join(deps.projectRoot, ".wrongstack"), { recursive: true });
12050
+ const agentsFile = path10.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
11400
12051
  const projectFacts = await detectProjectFacts(deps.projectRoot);
11401
12052
  await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
11402
12053
  deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
@@ -11853,7 +12504,7 @@ var usageCmd = async (_args, deps) => {
11853
12504
  return 0;
11854
12505
  };
11855
12506
  var projectsCmd = async (_args, deps) => {
11856
- const projectsRoot = path9.join(deps.paths.globalRoot, "projects");
12507
+ const projectsRoot = path10.join(deps.paths.globalRoot, "projects");
11857
12508
  try {
11858
12509
  const entries = await fsp4.readdir(projectsRoot);
11859
12510
  if (entries.length === 0) {
@@ -11863,7 +12514,7 @@ var projectsCmd = async (_args, deps) => {
11863
12514
  for (const hash of entries) {
11864
12515
  try {
11865
12516
  const meta = JSON.parse(
11866
- await fsp4.readFile(path9.join(projectsRoot, hash, "meta.json"), "utf8")
12517
+ await fsp4.readFile(path10.join(projectsRoot, hash, "meta.json"), "utf8")
11867
12518
  );
11868
12519
  deps.renderer.write(
11869
12520
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -12070,20 +12721,20 @@ ${navLines.join(" \xB7 ")}
12070
12721
  async function mutateModelsConfig(deps, mutator) {
12071
12722
  const vault = deps.vault;
12072
12723
  const configPath2 = deps.paths.globalConfig;
12073
- let fileExists = true;
12724
+ let fileExists2 = true;
12074
12725
  let raw;
12075
12726
  try {
12076
12727
  raw = await fsp4.readFile(configPath2, "utf8");
12077
12728
  } catch (err) {
12078
12729
  if (err.code !== "ENOENT") throw err;
12079
- fileExists = false;
12730
+ fileExists2 = false;
12080
12731
  raw = "{}";
12081
12732
  }
12082
12733
  let parsed;
12083
12734
  try {
12084
12735
  parsed = JSON.parse(raw);
12085
12736
  } catch (err) {
12086
- if (fileExists) {
12737
+ if (fileExists2) {
12087
12738
  throw new Error(
12088
12739
  `Refusing to overwrite corrupt config at ${configPath2} (${err.message}).`
12089
12740
  );
@@ -12242,7 +12893,7 @@ async function listFleetRuns(deps) {
12242
12893
  }
12243
12894
  const runs = [];
12244
12895
  for (const id of entries) {
12245
- const runDir = path9.join(deps.paths.projectSessions, id);
12896
+ const runDir = path10.join(deps.paths.projectSessions, id);
12246
12897
  let stat4;
12247
12898
  try {
12248
12899
  stat4 = await fsp4.stat(runDir);
@@ -12255,17 +12906,17 @@ async function listFleetRuns(deps) {
12255
12906
  let subagentCount = 0;
12256
12907
  let subagentsDir;
12257
12908
  try {
12258
- await fsp4.access(path9.join(runDir, "fleet.json"));
12909
+ await fsp4.access(path10.join(runDir, "fleet.json"));
12259
12910
  manifest = true;
12260
12911
  } catch {
12261
12912
  }
12262
12913
  try {
12263
- await fsp4.access(path9.join(runDir, "checkpoint.json"));
12914
+ await fsp4.access(path10.join(runDir, "checkpoint.json"));
12264
12915
  checkpoint = true;
12265
12916
  } catch {
12266
12917
  }
12267
12918
  try {
12268
- subagentsDir = path9.join(runDir, "subagents");
12919
+ subagentsDir = path10.join(runDir, "subagents");
12269
12920
  const files = await fsp4.readdir(subagentsDir);
12270
12921
  subagentCount = files.filter((f) => f.endsWith(".jsonl")).length;
12271
12922
  } catch {
@@ -12294,7 +12945,7 @@ async function listFleetRuns(deps) {
12294
12945
  return 0;
12295
12946
  }
12296
12947
  async function showFleetRun(runId, deps) {
12297
- const runDir = path9.join(deps.paths.projectSessions, runId);
12948
+ const runDir = path10.join(deps.paths.projectSessions, runId);
12298
12949
  let stat4;
12299
12950
  try {
12300
12951
  stat4 = await fsp4.stat(runDir);
@@ -12311,7 +12962,7 @@ async function showFleetRun(runId, deps) {
12311
12962
  deps.renderer.write(color.bold(`
12312
12963
  Fleet Run: ${runId}
12313
12964
  `) + "\n");
12314
- const manifestPath = path9.join(runDir, "fleet.json");
12965
+ const manifestPath = path10.join(runDir, "fleet.json");
12315
12966
  let manifestData = null;
12316
12967
  try {
12317
12968
  manifestData = await fsp4.readFile(manifestPath, "utf8");
@@ -12327,7 +12978,7 @@ Fleet Run: ${runId}
12327
12978
  deps.renderer.write(` ${color.dim("\u25CB")} fleet.json \u2014 not found
12328
12979
  `);
12329
12980
  }
12330
- const checkpointPath = path9.join(runDir, "checkpoint.json");
12981
+ const checkpointPath = path10.join(runDir, "checkpoint.json");
12331
12982
  let checkpointData = null;
12332
12983
  try {
12333
12984
  checkpointData = await fsp4.readFile(checkpointPath, "utf8");
@@ -12374,7 +13025,7 @@ Fleet Run: ${runId}
12374
13025
  } catch {
12375
13026
  }
12376
13027
  }
12377
- const subagentsDir = path9.join(runDir, "subagents");
13028
+ const subagentsDir = path10.join(runDir, "subagents");
12378
13029
  let subagentFiles = [];
12379
13030
  try {
12380
13031
  subagentFiles = await fsp4.readdir(subagentsDir);
@@ -12386,7 +13037,7 @@ Fleet Run: ${runId}
12386
13037
  Subagent transcripts (${subagentFiles.length}):
12387
13038
  `);
12388
13039
  for (const f of subagentFiles.sort()) {
12389
- const filePath = path9.join(subagentsDir, f);
13040
+ const filePath = path10.join(subagentsDir, f);
12390
13041
  let size;
12391
13042
  try {
12392
13043
  const s = await fsp4.stat(filePath);
@@ -12403,7 +13054,7 @@ Fleet Run: ${runId}
12403
13054
  ${color.dim("\u25CB")} No subagent transcripts
12404
13055
  `);
12405
13056
  }
12406
- const sharedDir = path9.join(runDir, "shared");
13057
+ const sharedDir = path10.join(runDir, "shared");
12407
13058
  try {
12408
13059
  const files = await fsp4.readdir(sharedDir);
12409
13060
  deps.renderer.write(`
@@ -12570,7 +13221,7 @@ function findSessionId(args) {
12570
13221
  var rewindCmd = async (args, deps) => {
12571
13222
  const flags = parseRewindFlags(args);
12572
13223
  const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
12573
- const sessionsDir = path9.join(wpaths.globalRoot, "sessions");
13224
+ const sessionsDir = path10.join(wpaths.globalRoot, "sessions");
12574
13225
  const rewind = new DefaultSessionRewinder(sessionsDir, deps.projectRoot);
12575
13226
  let sessionId = findSessionId(args);
12576
13227
  if (!sessionId) {
@@ -12810,10 +13461,10 @@ var auditCmd = async (args, deps) => {
12810
13461
  return verify.ok ? 0 : 1;
12811
13462
  };
12812
13463
  async function listAudits(log, dir, deps) {
12813
- const fs29 = await import('fs/promises');
13464
+ const fs30 = await import('fs/promises');
12814
13465
  let entries;
12815
13466
  try {
12816
- entries = await fs29.readdir(dir);
13467
+ entries = await fs30.readdir(dir);
12817
13468
  } catch {
12818
13469
  deps.renderer.write(
12819
13470
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -13040,10 +13691,10 @@ function roleCat(role) {
13040
13691
  function createProviderForId(providerId, cfg) {
13041
13692
  const savedCfg = cfg.providers?.[providerId];
13042
13693
  const resolvedProviderId = savedCfg?.type ?? providerId;
13043
- const cfgWithType = {
13044
- ...savedCfg ?? { type: providerId, apiKey: cfg.apiKey, baseUrl: cfg.baseUrl },
13045
- type: resolvedProviderId
13046
- };
13694
+ const cfgWithType = Object.assign(
13695
+ { type: resolvedProviderId },
13696
+ savedCfg ?? { apiKey: cfg.apiKey, baseUrl: cfg.baseUrl }
13697
+ );
13047
13698
  try {
13048
13699
  return makeProviderFromConfig(resolvedProviderId, cfgWithType);
13049
13700
  } catch {
@@ -13082,7 +13733,7 @@ Output ONLY a ranked list, one per line:
13082
13733
  const text = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
13083
13734
  const rankings = [];
13084
13735
  for (const line of text.split("\n")) {
13085
- const m = line.match(/^\s*(\d+)[\.\)]\s*Response\s+([A-Z])/i);
13736
+ const m = line.match(/^\s*(\d+)[.)]\s*Response\s+([A-Z])/i);
13086
13737
  if (m) {
13087
13738
  const label = m[2].toUpperCase();
13088
13739
  const idx = labelToIdx.get(label);
@@ -13288,10 +13939,17 @@ var modeldiagCmd = async (args, deps) => {
13288
13939
  const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
13289
13940
  let providerFilter;
13290
13941
  if (providersEqIdx >= 0) {
13291
- providerFilter = benchArgs[providersEqIdx].replace("--providers=", "").split(",").map((s) => s.trim()).filter(Boolean);
13292
- benchArgs.splice(providersEqIdx, 1);
13942
+ const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
13943
+ if (rawFilter) {
13944
+ providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
13945
+ benchArgs.splice(providersEqIdx, 1);
13946
+ }
13293
13947
  }
13294
13948
  const benchRole = benchArgs[0];
13949
+ if (!benchRole) {
13950
+ writeLine(`${color.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`);
13951
+ return 1;
13952
+ }
13295
13953
  const benchPrompt = benchArgs.slice(1).join(" ");
13296
13954
  const cat = roleCat(benchRole);
13297
13955
  const candidates = rankModels(providers, hasKey, cat, 5);
@@ -13706,22 +14364,22 @@ function fmtDuration(ms) {
13706
14364
  const remMin = m - h * 60;
13707
14365
  return `${h}h${remMin}m`;
13708
14366
  }
13709
- function fmtTaskResultLine(r, color58) {
14367
+ function fmtTaskResultLine(r, color62) {
13710
14368
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
13711
14369
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
13712
14370
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
13713
14371
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
13714
- const errKindChip = errKind ? color58.dim(` [${errKind}]`) : "";
13715
- const errSnip = errMsg || errKind ? `${errKindChip}${color58.dim(errTail)}` : "";
14372
+ const errKindChip = errKind ? color62.dim(` [${errKind}]`) : "";
14373
+ const errSnip = errMsg || errKind ? `${errKindChip}${color62.dim(errTail)}` : "";
13716
14374
  switch (r.status) {
13717
14375
  case "success":
13718
- return { mark: color58.green("\u2713"), stats, tail: "" };
14376
+ return { mark: color62.green("\u2713"), stats, tail: "" };
13719
14377
  case "timeout":
13720
- return { mark: color58.yellow("\u23F1"), stats: `${color58.yellow("timeout")} ${stats}`, tail: errSnip };
14378
+ return { mark: color62.yellow("\u23F1"), stats: `${color62.yellow("timeout")} ${stats}`, tail: errSnip };
13721
14379
  case "stopped":
13722
- return { mark: color58.dim("\u2298"), stats: `${color58.dim("stopped")} ${stats}`, tail: errSnip };
14380
+ return { mark: color62.dim("\u2298"), stats: `${color62.dim("stopped")} ${stats}`, tail: errSnip };
13723
14381
  case "failed":
13724
- return { mark: color58.red("\u2717"), stats: `${color58.red("failed")} ${stats}`, tail: errSnip };
14382
+ return { mark: color62.red("\u2717"), stats: `${color62.red("failed")} ${stats}`, tail: errSnip };
13725
14383
  }
13726
14384
  }
13727
14385
 
@@ -13739,7 +14397,7 @@ function resolveBundledSkillsDir() {
13739
14397
  try {
13740
14398
  const req2 = createRequire(import.meta.url);
13741
14399
  const corePkg = req2.resolve("@wrongstack/core/package.json");
13742
- return path9.join(path9.dirname(corePkg), "skills");
14400
+ return path10.join(path10.dirname(corePkg), "skills");
13743
14401
  } catch {
13744
14402
  return void 0;
13745
14403
  }
@@ -13989,7 +14647,7 @@ async function boot(argv) {
13989
14647
  } catch {
13990
14648
  }
13991
14649
  printLaunchHints(renderer, flags, {
13992
- cursorFile: path9.join(wpaths.cacheDir, "hint-cursor")
14650
+ cursorFile: path10.join(wpaths.cacheDir, "hint-cursor")
13993
14651
  });
13994
14652
  }
13995
14653
  return {
@@ -14010,7 +14668,7 @@ async function boot(argv) {
14010
14668
  }
14011
14669
  async function checkGitInCwd(opts) {
14012
14670
  const { cwd, renderer, reader } = opts;
14013
- const cwdGit = path9.join(cwd, ".git");
14671
+ const cwdGit = path10.join(cwd, ".git");
14014
14672
  let hasCwdGit = false;
14015
14673
  try {
14016
14674
  await fsp4.access(cwdGit);
@@ -14051,10 +14709,10 @@ async function checkGitInCwd(opts) {
14051
14709
  }
14052
14710
  }
14053
14711
  }
14054
- const parentDir = path9.dirname(cwd);
14712
+ const parentDir = path10.dirname(cwd);
14055
14713
  if (parentDir !== cwd) {
14056
14714
  try {
14057
- await fsp4.access(path9.join(parentDir, ".git"));
14715
+ await fsp4.access(path10.join(parentDir, ".git"));
14058
14716
  renderer.write(
14059
14717
  ` ${color.dim("\u2139")} A ${color.bold(".git")} repo exists in the parent directory: ${color.dim(parentDir)}
14060
14718
  `
@@ -14371,11 +15029,29 @@ async function predictNextTasks(input, opts) {
14371
15029
  }
14372
15030
  }
14373
15031
  init_sdd();
15032
+ function parseSuggestionsFromOutput(finalText) {
15033
+ const patterns = [
15034
+ /💡\s*Next\s+steps?\s*\n((?:\d+\.\s+.+\n?)+)/i,
15035
+ /##?\s*Next\s+steps?\s*\n((?:\d+\.\s+.+\n?)+)/i,
15036
+ /Next\s+steps?\s*\n((?:\d+\.\s+.+\n?)+)/i
15037
+ ];
15038
+ for (const pat of patterns) {
15039
+ const m = pat.exec(finalText);
15040
+ if (m && m[1]) {
15041
+ const block = m[1].trim();
15042
+ const lines = block.split("\n").filter(Boolean);
15043
+ const suggestions = lines.map((l) => l.replace(/^\d+\.\s*/, "").trim()).filter((s) => s.length > 3);
15044
+ if (suggestions.length > 0) return suggestions.slice(0, 5);
15045
+ }
15046
+ }
15047
+ return null;
15048
+ }
14374
15049
  async function runRepl(opts) {
14375
15050
  if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
14376
15051
  await renderGoalBanner(opts);
14377
15052
  let activeCtrl;
14378
15053
  let interrupts = 0;
15054
+ let autoIterCount = 0;
14379
15055
  let exiting = false;
14380
15056
  const onSigint = () => {
14381
15057
  interrupts++;
@@ -14431,6 +15107,28 @@ async function runRepl(opts) {
14431
15107
  `
14432
15108
  );
14433
15109
  }
15110
+ if (engine.currentState === "stopped") {
15111
+ const goal = await loadGoalSafe(opts);
15112
+ if (goal?.goalState === "completed") {
15113
+ const u = goal.journal.length > 0 ? summarizeUsage(goal) : null;
15114
+ 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`) : "";
15115
+ opts.renderer.write(
15116
+ color.green(`
15117
+ \u{1F3AF} Goal completed!${costLine}
15118
+
15119
+ `) + color.dim(" Goal cleared. Use /goal set <mission> to create a new goal.\n")
15120
+ );
15121
+ if (opts.projectRoot) {
15122
+ try {
15123
+ const { unlink: unlink4 } = await import('fs/promises');
15124
+ await unlink4(goalFilePath(opts.projectRoot));
15125
+ } catch {
15126
+ }
15127
+ }
15128
+ }
15129
+ opts.onAutonomy?.("off");
15130
+ continue;
15131
+ }
14434
15132
  } catch (err) {
14435
15133
  opts.renderer.writeError(
14436
15134
  `[eternal] ${err instanceof Error ? err.message : String(err)}`
@@ -14477,6 +15175,28 @@ async function runRepl(opts) {
14477
15175
  `
14478
15176
  );
14479
15177
  }
15178
+ if (engine.currentState === "stopped") {
15179
+ const goal = await loadGoalSafe(opts);
15180
+ if (goal?.goalState === "completed") {
15181
+ const u = goal.journal.length > 0 ? summarizeUsage(goal) : null;
15182
+ 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`) : "";
15183
+ opts.renderer.write(
15184
+ color.green(`
15185
+ \u{1F3AF} Goal completed!${costLine}
15186
+
15187
+ `) + color.dim(" Goal cleared. Use /goal set <mission> to create a new goal.\n")
15188
+ );
15189
+ if (opts.projectRoot) {
15190
+ try {
15191
+ const { unlink: unlink4 } = await import('fs/promises');
15192
+ await unlink4(goalFilePath(opts.projectRoot));
15193
+ } catch {
15194
+ }
15195
+ }
15196
+ }
15197
+ opts.onAutonomy?.("off");
15198
+ continue;
15199
+ }
14480
15200
  } catch (err) {
14481
15201
  opts.renderer.writeError(
14482
15202
  `[parallel] ${err instanceof Error ? err.message : String(err)}`
@@ -14486,6 +15206,96 @@ async function runRepl(opts) {
14486
15206
  continue;
14487
15207
  }
14488
15208
  }
15209
+ {
15210
+ const mode = opts.getAutonomy?.() ?? "off";
15211
+ const suggestions = opts.getSuggestions?.() ?? [];
15212
+ if (mode === "suggest" && suggestions.length > 0) {
15213
+ const lines = suggestions.map(
15214
+ (s, i) => ` ${color.bold(`${i + 1}.`)} ${color.dim(s)}`
15215
+ );
15216
+ opts.renderer.write(
15217
+ `
15218
+ ${color.cyan(" \u{1F4A1} Suggested next steps")} ${color.dim("(use /next 1, /next 2, or /next 1 2 3)")}
15219
+ ${lines.join("\n")}
15220
+
15221
+ `
15222
+ );
15223
+ }
15224
+ if (mode === "auto" && suggestions.length > 0) {
15225
+ const maxIter = opts.autoProceedMaxIterations ?? 50;
15226
+ if (maxIter > 0 && autoIterCount >= maxIter) {
15227
+ console.log(JSON.stringify({
15228
+ level: "info",
15229
+ event: "auto_proceed_limit_reached",
15230
+ iterations: autoIterCount,
15231
+ maxIterations: maxIter
15232
+ }));
15233
+ opts.renderer.write(
15234
+ `
15235
+ ${color.amber(" \u26A0 Auto-proceed limit reached")} \u2014 ${color.dim(`${autoIterCount} iterations. Waiting for input.`)}
15236
+
15237
+ `
15238
+ );
15239
+ autoIterCount = 0;
15240
+ } else {
15241
+ const top = suggestions[0] ?? "";
15242
+ const delay = opts.autoProceedDelayMs ?? 45e3;
15243
+ if (opts.onValidateAutoProceed) {
15244
+ try {
15245
+ const lastOutput = (
15246
+ /* last known agent output — use the stored suggestions
15247
+ and any recent context the host can provide */
15248
+ ""
15249
+ );
15250
+ const ok = await opts.onValidateAutoProceed(top, lastOutput);
15251
+ if (!ok) {
15252
+ opts.renderer.write(
15253
+ `
15254
+ ${color.amber(" \u26A0 Auto-proceed held")} \u2014 ${color.dim("suggestions need review. Waiting for input.")}
15255
+
15256
+ `
15257
+ );
15258
+ const lines = suggestions.map(
15259
+ (s, i) => ` ${color.bold(`${i + 1}.`)} ${color.dim(s)}`
15260
+ );
15261
+ opts.renderer.write(
15262
+ `${color.dim(" Next steps:")}
15263
+ ${lines.join("\n")}
15264
+
15265
+ `
15266
+ );
15267
+ autoIterCount = 0;
15268
+ } else {
15269
+ const ctrl = new AbortController();
15270
+ activeCtrl = ctrl;
15271
+ try {
15272
+ autoIterCount++;
15273
+ await runAutoProceed(opts, top, delay, ctrl);
15274
+ } finally {
15275
+ activeCtrl = void 0;
15276
+ }
15277
+ continue;
15278
+ }
15279
+ } catch {
15280
+ opts.renderer.write(
15281
+ color.dim(" Auto-proceed validation unavailable \u2014 waiting for input.\n")
15282
+ );
15283
+ autoIterCount = 0;
15284
+ }
15285
+ } else {
15286
+ const ctrl = new AbortController();
15287
+ activeCtrl = ctrl;
15288
+ try {
15289
+ autoIterCount++;
15290
+ await runAutoProceed(opts, top, delay, ctrl);
15291
+ } finally {
15292
+ activeCtrl = void 0;
15293
+ }
15294
+ continue;
15295
+ }
15296
+ }
15297
+ }
15298
+ }
14489
15299
  let raw;
14490
15300
  try {
14491
15301
  raw = await readPossiblyMultiline(opts);
@@ -14579,6 +15389,10 @@ ${color.dim(taskList2)}
14579
15389
  }
14580
15390
  }
14581
15391
  }
15392
+ if (opts.onSuggestionsParsed) {
15393
+ const parsed = parseSuggestionsFromOutput(runResult.finalText);
15394
+ opts.onSuggestionsParsed(parsed);
15395
+ }
14582
15396
  }
14583
15397
  } catch (_runErr) {
14584
15398
  opts.renderer.writeWarning("AI auto-trigger failed. You can continue manually.");
@@ -14739,6 +15553,10 @@ ${color.dim(taskList2)}
14739
15553
  }
14740
15554
  }
14741
15555
  }
15556
+ if (result.status === "done" && result.finalText && opts.onSuggestionsParsed) {
15557
+ const parsed = parseSuggestionsFromOutput(result.finalText);
15558
+ opts.onSuggestionsParsed(parsed);
15559
+ }
14742
15560
  if (opts.tokenCounter && before) {
14743
15561
  const after = opts.tokenCounter.total();
14744
15562
  const costAfter = opts.tokenCounter.estimateCost().total;
@@ -14801,6 +15619,10 @@ ${color.cyan(" Suggested next steps:")}
14801
15619
  ${suggestResult.finalText}
14802
15620
  `
14803
15621
  );
15622
+ if (opts.onSuggestionsParsed) {
15623
+ const parsed = parseSuggestionsFromOutput(suggestResult.finalText);
15624
+ opts.onSuggestionsParsed(parsed);
15625
+ }
14804
15626
  }
14805
15627
  } catch {
14806
15628
  } finally {
@@ -14931,6 +15753,79 @@ async function renderGoalBanner(opts) {
14931
15753
  }
14932
15754
  opts.renderer.write("\n");
14933
15755
  }
15756
+ async function runAutoProceed(opts, suggestion, delayMs, ctrl) {
15757
+ const truncated = suggestion.length > 80 ? `${suggestion.slice(0, 77)}\u2026` : suggestion;
15758
+ console.log(JSON.stringify({
15759
+ level: "info",
15760
+ event: "auto_proceed_started",
15761
+ suggestion: truncated,
15762
+ delayMs
15763
+ }));
15764
+ try {
15765
+ await autoProceedCountdown(opts, delayMs, suggestion, ctrl.signal);
15766
+ const runBlocks = [{ type: "text", text: suggestion }];
15767
+ const runResult = await opts.agent.run(runBlocks, { signal: ctrl.signal });
15768
+ console.log(JSON.stringify({
15769
+ level: "info",
15770
+ event: "auto_proceed_completed",
15771
+ suggestion: truncated,
15772
+ status: runResult.status,
15773
+ iterations: runResult.iterations
15774
+ }));
15775
+ opts.onAgentIterationComplete?.(
15776
+ estimateRequestTokensCalibrated(
15777
+ opts.agent.ctx.messages,
15778
+ opts.agent.ctx.systemPrompt,
15779
+ opts.agent.ctx.tools ?? []
15780
+ ).total
15781
+ );
15782
+ if (runResult.status === "done" && runResult.finalText && opts.onSuggestionsParsed) {
15783
+ const parsed = parseSuggestionsFromOutput(runResult.finalText);
15784
+ opts.onSuggestionsParsed(parsed);
15785
+ }
15786
+ } finally {
15787
+ }
15788
+ }
15789
+ async function autoProceedCountdown(opts, delayMs, suggestion, signal) {
15790
+ const sec = Math.ceil(delayMs / 1e3);
15791
+ const truncated = suggestion.length > 100 ? `${suggestion.slice(0, 97)}\u2026` : suggestion;
15792
+ opts.renderer.write(
15793
+ `
15794
+ ${color.cyan("\u23F3 Auto-proceeding")} in ${sec}s\u2026 ${color.dim("(Ctrl+C to cancel)")}
15795
+ `
15796
+ );
15797
+ opts.renderer.write(`${color.dim(" \u25B8")} ${color.dim(truncated)}
15798
+ `);
15799
+ const start = Date.now();
15800
+ let lastSec = sec;
15801
+ return new Promise((resolve5, reject) => {
15802
+ const onAbort = () => {
15803
+ clearInterval(interval);
15804
+ reject(new DOMException("Aborted", "AbortError"));
15805
+ };
15806
+ signal.addEventListener("abort", onAbort, { once: true });
15807
+ const interval = setInterval(() => {
15808
+ if (signal.aborted) return;
15809
+ const elapsed = Date.now() - start;
15810
+ const remaining = Math.max(0, Math.ceil((delayMs - elapsed) / 1e3));
15811
+ if (remaining <= 0) {
15812
+ clearInterval(interval);
15813
+ signal.removeEventListener("abort", onAbort);
15814
+ opts.renderer.write(color.dim(` \u21B3 Proceeding with: ${truncated}
15815
+ `));
15816
+ resolve5();
15817
+ return;
15818
+ }
15819
+ if (remaining !== lastSec) {
15820
+ lastSec = remaining;
15821
+ opts.renderer.write(
15822
+ color.dim(` \u23F3 ${remaining}s remaining\u2026 (Ctrl+C to cancel)
15823
+ `)
15824
+ );
15825
+ }
15826
+ }, 500);
15827
+ });
15828
+ }
14934
15829
  async function readPossiblyMultiline(opts) {
14935
15830
  const firstPrompt = theme2.primary("\u203A ");
14936
15831
  const contPrompt = color.dim("\xB7 ");
@@ -15023,6 +15918,11 @@ async function execute(deps) {
15023
15918
  getAutonomy,
15024
15919
  onAutonomy,
15025
15920
  getNextPredict,
15921
+ onSuggestionsParsed,
15922
+ getSuggestions,
15923
+ autoProceedDelayMs,
15924
+ autoProceedMaxIterations,
15925
+ onValidateAutoProceed,
15026
15926
  getEternalEngine,
15027
15927
  getParallelEngine,
15028
15928
  subscribeEternalIteration,
@@ -15251,6 +16151,7 @@ async function execute(deps) {
15251
16151
  auditLevel: cfg.session?.auditLevel ?? "standard",
15252
16152
  indexOnStart: cfg.indexing?.onSessionStart !== false,
15253
16153
  maxIterations: cfg.tools?.maxIterations ?? 500,
16154
+ autoProceedMaxIterations: cfg.autonomy?.autoProceedMaxIterations ?? 50,
15254
16155
  debugStream: cfg.debugStream ?? false,
15255
16156
  configScope: cfg.configScope ?? "global",
15256
16157
  enhanceDelayMs: cfg.autonomy?.enhanceDelayMs ?? 6e4
@@ -15334,11 +16235,15 @@ async function execute(deps) {
15334
16235
  autonomy.enhanceDelayMs = s.enhanceDelayMs;
15335
16236
  decrypted.autonomy = autonomy;
15336
16237
  }
16238
+ if (s.autoProceedMaxIterations !== void 0) {
16239
+ const autonomy = decrypted.autonomy ?? {};
16240
+ autonomy.autoProceedMaxIterations = s.autoProceedMaxIterations;
16241
+ decrypted.autonomy = autonomy;
16242
+ }
15337
16243
  const toWrite = targetPath === wpaths.globalConfig ? decrypted : filterSafeForProject(decrypted);
15338
16244
  const encrypted = encryptConfigSecrets$1(toWrite, vault);
15339
16245
  if (targetPath !== wpaths.globalConfig) {
15340
- await fsp4.mkdir(path9.dirname(targetPath), { recursive: true }).catch(() => {
15341
- });
16246
+ await fsp4.mkdir(path10.dirname(targetPath), { recursive: true }).catch((err) => console.debug(`[execution] mkdir failed: ${err}`));
15342
16247
  }
15343
16248
  await atomicWrite(targetPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
15344
16249
  configStore.update({
@@ -15428,13 +16333,15 @@ async function execute(deps) {
15428
16333
  return typeof metaMode === "string" ? metaMode : modeId ?? "default";
15429
16334
  },
15430
16335
  registerDebugStreamCallback: (cb) => {
15431
- void import('@wrongstack/providers').then(
15432
- ({ setDebugStreamCallback }) => setDebugStreamCallback(cb)
16336
+ void import('@wrongstack/providers').then(({ setDebugStreamCallback }) => setDebugStreamCallback(cb)).catch(
16337
+ (err) => console.error("[execution] failed to register debug stream callback:", err)
15433
16338
  );
15434
16339
  },
15435
16340
  restoreDebugStreamCallback: () => {
15436
16341
  void import('@wrongstack/providers').then(
15437
16342
  ({ setDebugStreamCallback, defaultDebugStreamCallback }) => setDebugStreamCallback(defaultDebugStreamCallback)
16343
+ ).catch(
16344
+ (err) => console.error("[execution] failed to restore debug stream callback:", err)
15438
16345
  );
15439
16346
  }
15440
16347
  });
@@ -15466,11 +16373,16 @@ async function execute(deps) {
15466
16373
  supportsVision,
15467
16374
  attachments,
15468
16375
  effectiveMaxContext,
15469
- projectName: path9.basename(projectRoot) || void 0,
16376
+ projectName: path10.basename(projectRoot) || void 0,
15470
16377
  projectRoot,
15471
16378
  getAutonomy,
15472
16379
  onAutonomy,
15473
16380
  getNextPredict,
16381
+ onSuggestionsParsed,
16382
+ getSuggestions,
16383
+ autoProceedDelayMs,
16384
+ autoProceedMaxIterations,
16385
+ onValidateAutoProceed,
15474
16386
  getEternalEngine,
15475
16387
  getParallelEngine,
15476
16388
  skillLoader,
@@ -15481,7 +16393,7 @@ async function execute(deps) {
15481
16393
  onAgentIterationComplete: director ? (tokens) => director.setLeaderContextPressure(tokens) : void 0
15482
16394
  });
15483
16395
  } finally {
15484
- await webuiPromise.catch(() => void 0);
16396
+ await webuiPromise.catch((err) => console.debug(`[execution] webui shutdown failed: ${err}`));
15485
16397
  }
15486
16398
  } else {
15487
16399
  code = await runRepl({
@@ -15494,10 +16406,15 @@ async function execute(deps) {
15494
16406
  supportsVision,
15495
16407
  attachments,
15496
16408
  effectiveMaxContext,
15497
- projectName: path9.basename(projectRoot) || void 0,
16409
+ projectName: path10.basename(projectRoot) || void 0,
15498
16410
  getAutonomy,
15499
16411
  onAutonomy,
15500
16412
  getNextPredict,
16413
+ onSuggestionsParsed,
16414
+ getSuggestions,
16415
+ autoProceedDelayMs,
16416
+ onValidateAutoProceed,
16417
+ autoProceedMaxIterations,
15501
16418
  getEternalEngine,
15502
16419
  getParallelEngine,
15503
16420
  skillLoader,
@@ -15626,7 +16543,7 @@ var MultiAgentHost = class {
15626
16543
  doneCondition: { type: "all_tasks_done" },
15627
16544
  maxConcurrent: this.opts.maxConcurrent ?? 4
15628
16545
  };
15629
- const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path9.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
16546
+ const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path10.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
15630
16547
  this.director = new Director({
15631
16548
  config: coordinatorConfig,
15632
16549
  manifestPath: this.opts.manifestPath,
@@ -15961,10 +16878,30 @@ var MultiAgentHost = class {
15961
16878
  model: opts?.model,
15962
16879
  tools: opts?.tools
15963
16880
  };
15964
- const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig);
16881
+ const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig, description);
15965
16882
  this.fleetManager?.addPendingTask(taskId, subagentId, description);
15966
16883
  return { subagentId, taskId };
15967
16884
  }
16885
+ /**
16886
+ * Spawn a fresh subagent, assign a task, and **await** its completion.
16887
+ *
16888
+ * Unlike `spawn()`, which returns immediately with spawn metadata, this
16889
+ * method blocks until the subagent finishes (success, failure, or timeout)
16890
+ * and returns the full `TaskResult`. Use this when the caller needs the
16891
+ * subagent's actual output — e.g. `/techstack` displaying the generated report
16892
+ * in chat, or `/spawn` showing the result inline.
16893
+ *
16894
+ * Optional `opts` lets the caller override the subagent's provider, model,
16895
+ * and tool slice per spawn.
16896
+ */
16897
+ async spawnAndWait(description, opts) {
16898
+ const { taskId } = await this.spawn(description, opts);
16899
+ if (!this.director) throw new Error("Director is not initialized");
16900
+ const results = await this.director.awaitTasks([taskId]);
16901
+ const result = results[0];
16902
+ if (!result) throw new Error(`Task ${taskId} completed but no result returned`);
16903
+ return result;
16904
+ }
15968
16905
  /**
15969
16906
  * Common spawn + assign logic shared by both director mode and raw
15970
16907
  * coordinator mode. Extracts the identical body from the two branches
@@ -15974,11 +16911,11 @@ var MultiAgentHost = class {
15974
16911
  * Returns `{ subagentId, taskId }`. Caller holds `pending` tracking
15975
16912
  * and event emission — the helper only talks to the coordinator.
15976
16913
  */
15977
- async _spawnAndAssign(subagentConfig) {
16914
+ async _spawnAndAssign(subagentConfig, description = "") {
15978
16915
  const taskId = randomUUID();
15979
16916
  if (!this.director) throw new Error("Director is not initialized");
15980
16917
  const subagentId = await this.director.spawn(subagentConfig);
15981
- await this.director.assign({ id: taskId, description: "", subagentId });
16918
+ await this.director.assign({ id: taskId, description, subagentId });
15982
16919
  return { subagentId, taskId };
15983
16920
  }
15984
16921
  /**
@@ -16095,16 +17032,16 @@ var MultiAgentHost = class {
16095
17032
  if (this.director) return this.director;
16096
17033
  this.opts.directorMode = true;
16097
17034
  if (this.opts.fleetRoot && !this.opts.manifestPath) {
16098
- this.opts.manifestPath = path9.join(this.opts.fleetRoot, "fleet.json");
17035
+ this.opts.manifestPath = path10.join(this.opts.fleetRoot, "fleet.json");
16099
17036
  }
16100
17037
  if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
16101
- this.opts.sharedScratchpadPath = path9.join(this.opts.fleetRoot, "shared");
17038
+ this.opts.sharedScratchpadPath = path10.join(this.opts.fleetRoot, "shared");
16102
17039
  }
16103
17040
  if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
16104
- this.opts.sessionsRoot = path9.join(this.opts.fleetRoot, "subagents");
17041
+ this.opts.sessionsRoot = path10.join(this.opts.fleetRoot, "subagents");
16105
17042
  }
16106
17043
  if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
16107
- this.opts.stateCheckpointPath = path9.join(this.opts.fleetRoot, "director-state.json");
17044
+ this.opts.stateCheckpointPath = path10.join(this.opts.fleetRoot, "director-state.json");
16108
17045
  }
16109
17046
  await this.ensureDirector();
16110
17047
  return this.director ?? null;
@@ -16276,11 +17213,11 @@ var SessionStats = class {
16276
17213
  if (e.name === "bash") this.bashCommands++;
16277
17214
  else if (e.name === "fetch") this.fetches++;
16278
17215
  if (!e.ok) return;
16279
- const path27 = typeof input?.path === "string" ? input.path : void 0;
16280
- if (e.name === "read" && path27) this.readPaths.add(path27);
16281
- else if (e.name === "edit" && path27) this.editedPaths.add(path27);
16282
- else if (e.name === "write" && path27) {
16283
- this.writtenPaths.add(path27);
17216
+ const path28 = typeof input?.path === "string" ? input.path : void 0;
17217
+ if (e.name === "read" && path28) this.readPaths.add(path28);
17218
+ else if (e.name === "edit" && path28) this.editedPaths.add(path28);
17219
+ else if (e.name === "write" && path28) {
17220
+ this.writtenPaths.add(path28);
16284
17221
  const content = typeof input?.content === "string" ? input.content : "";
16285
17222
  this.bytesWritten += Buffer.byteLength(content, "utf8");
16286
17223
  }
@@ -16885,7 +17822,12 @@ async function setupCompaction(params) {
16885
17822
  effectiveMaxContext,
16886
17823
  // Calibrated estimator: recordActualUsage() is called after each API
16887
17824
  // response so this converges on real token counts for compaction decisions.
16888
- (ctx) => estimateRequestTokensCalibrated(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total,
17825
+ (ctx) => estimateRequestTokensCalibrated(
17826
+ ctx.messages,
17827
+ ctx.systemPrompt,
17828
+ ctx.tools ?? [],
17829
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
17830
+ ).total,
16889
17831
  initialPolicy.thresholds,
16890
17832
  {
16891
17833
  aggressiveOn: initialPolicy.aggressiveOn,
@@ -17102,7 +18044,7 @@ function setupMetrics(params) {
17102
18044
  const dumpMetrics = () => {
17103
18045
  if (!metricsSink) return;
17104
18046
  try {
17105
- const out = path9.join(wpaths.projectSessions, "metrics.json");
18047
+ const out = path10.join(wpaths.projectSessions, "metrics.json");
17106
18048
  const snap = metricsSink.snapshot();
17107
18049
  writeFileSync(out, JSON.stringify(snap, null, 2));
17108
18050
  } catch {
@@ -17177,7 +18119,7 @@ async function setupCodebaseIndexing(deps) {
17177
18119
  if (tool?.mutating && FILE_EDIT_TOOLS.has(tool.name)) {
17178
18120
  const fp = payload.toolUse.input?.file_path;
17179
18121
  if (typeof fp === "string" && fp.length > 0) {
17180
- const abs = path9.resolve(payload.ctx.cwd, fp);
18122
+ const abs = path10.resolve(payload.ctx.cwd, fp);
17181
18123
  if (isIndexableFile(abs)) {
17182
18124
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
17183
18125
  }
@@ -17192,11 +18134,11 @@ async function setupCodebaseIndexing(deps) {
17192
18134
  let watcher;
17193
18135
  if (idx.watchExternal) {
17194
18136
  try {
17195
- watcher = fs14.watch(projectRoot, { recursive: true }, (_event, filename) => {
18137
+ watcher = fs15.watch(projectRoot, { recursive: true }, (_event, filename) => {
17196
18138
  if (!filename) return;
17197
18139
  const rel = filename.toString();
17198
18140
  if (isIgnored(rel)) return;
17199
- const abs = path9.resolve(projectRoot, rel);
18141
+ const abs = path10.resolve(projectRoot, rel);
17200
18142
  if (!isIndexableFile(abs)) return;
17201
18143
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
17202
18144
  });
@@ -17443,7 +18385,7 @@ async function setupSession(params) {
17443
18385
  } = params;
17444
18386
  sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
17445
18387
  if (count > 0) renderer.writeInfo(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
17446
- }).catch(() => void 0);
18388
+ }).catch((err) => console.debug(`[session] prune failed: ${err}`));
17447
18389
  let resumeId = typeof flags["resume"] === "string" ? flags["resume"] : void 0;
17448
18390
  const recoveryLock = new RecoveryLock({ dir: wpaths.projectSessions, sessionStore });
17449
18391
  if (!resumeId && !flags["no-recovery"]) {
@@ -17485,9 +18427,9 @@ async function setupSession(params) {
17485
18427
  const sessionRef = { current: session };
17486
18428
  await recoveryLock.write(session?.id).catch(() => void 0);
17487
18429
  const attachments = new DefaultAttachmentStore({
17488
- spoolDir: path9.join(wpaths.projectSessions, session?.id, "attachments")
18430
+ spoolDir: path10.join(wpaths.projectSessions, session?.id, "attachments")
17489
18431
  });
17490
- const queueStore = new QueueStore({ dir: path9.join(wpaths.projectSessions, session?.id) });
18432
+ const queueStore = new QueueStore({ dir: path10.join(wpaths.projectSessions, session?.id) });
17491
18433
  const ctxSignal = new AbortController().signal;
17492
18434
  const context = new Context({
17493
18435
  systemPrompt,
@@ -17500,7 +18442,7 @@ async function setupSession(params) {
17500
18442
  model: config.model
17501
18443
  });
17502
18444
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
17503
- const todosCheckpointPath = path9.join(wpaths.projectSessions, `${session?.id}.todos.json`);
18445
+ const todosCheckpointPath = path10.join(wpaths.projectSessions, `${session?.id}.todos.json`);
17504
18446
  if (resumeId) {
17505
18447
  try {
17506
18448
  const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
@@ -17518,15 +18460,15 @@ async function setupSession(params) {
17518
18460
  todosCheckpointPath,
17519
18461
  session?.id
17520
18462
  );
17521
- const planPath = path9.join(wpaths.projectSessions, `${session?.id}.plan.json`);
18463
+ const planPath = path10.join(wpaths.projectSessions, `${session?.id}.plan.json`);
17522
18464
  context.state.setMeta("plan.path", planPath);
17523
- const taskPath = path9.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
18465
+ const taskPath = path10.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
17524
18466
  context.state.setMeta("task.path", taskPath);
17525
18467
  let dirState;
17526
18468
  if (resumeId) {
17527
18469
  try {
17528
- const fleetRoot = path9.join(wpaths.projectSessions, session?.id);
17529
- dirState = await loadDirectorState(path9.join(fleetRoot, "director-state.json"));
18470
+ const fleetRoot = path10.join(wpaths.projectSessions, session?.id);
18471
+ dirState = await loadDirectorState(path10.join(fleetRoot, "director-state.json"));
17530
18472
  if (dirState) {
17531
18473
  const tCounts = {};
17532
18474
  for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
@@ -17566,7 +18508,7 @@ function resolveBundledSkillsDir2() {
17566
18508
  try {
17567
18509
  const req2 = createRequire(import.meta.url);
17568
18510
  const corePkg = req2.resolve("@wrongstack/core/package.json");
17569
- return path9.join(path9.dirname(corePkg), "skills");
18511
+ return path10.join(path10.dirname(corePkg), "skills");
17570
18512
  } catch {
17571
18513
  return void 0;
17572
18514
  }
@@ -17677,9 +18619,8 @@ async function main(argv) {
17677
18619
  updateInfo
17678
18620
  } = ctx;
17679
18621
  updateInfo = await printUpdateNotice(updateInfo);
17680
- const { setDebugStreamEnabled, setDebugStreamCallback, defaultDebugStreamCallback } = await import('@wrongstack/providers');
18622
+ const { setDebugStreamEnabled } = await import('@wrongstack/providers');
17681
18623
  if (config.debugStream) setDebugStreamEnabled(true);
17682
- setDebugStreamCallback(defaultDebugStreamCallback);
17683
18624
  const pathResolver = new DefaultPathResolver(cwd);
17684
18625
  const events = new EventBus();
17685
18626
  events.setLogger(logger);
@@ -17766,7 +18707,7 @@ async function main(argv) {
17766
18707
  modeId,
17767
18708
  modePrompt,
17768
18709
  modelCapabilities,
17769
- planPath: () => sessionRef.current ? path9.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
18710
+ planPath: () => sessionRef.current ? path10.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
17770
18711
  contributors: [
17771
18712
  // Injects the ETERNAL AUTONOMY block when the user has activated
17772
18713
  // a long-running autonomy engine. Without this, the per-iteration
@@ -18136,6 +19077,7 @@ async function main(argv) {
18136
19077
  })();
18137
19078
  autonomyModeRef.current = autonomyMode;
18138
19079
  let nextPredictEnabled = config.nextPrediction === true;
19080
+ let currentSuggestions = [];
18139
19081
  let eternalEngine = null;
18140
19082
  let parallelEngine = null;
18141
19083
  const eternalListeners = /* @__PURE__ */ new Set();
@@ -18156,12 +19098,12 @@ async function main(argv) {
18156
19098
  }
18157
19099
  }
18158
19100
  };
18159
- const fleetRoot = directorMode ? path9.join(wpaths.projectSessions, session.id) : void 0;
18160
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path9.join(expectDefined(fleetRoot), "fleet.json") : void 0;
18161
- const sharedScratchpadPath = directorMode ? path9.join(expectDefined(fleetRoot), "shared") : void 0;
18162
- const subagentSessionsRoot = directorMode ? path9.join(expectDefined(fleetRoot), "subagents") : void 0;
18163
- const stateCheckpointPath = directorMode ? path9.join(expectDefined(fleetRoot), "director-state.json") : void 0;
18164
- const fleetRootForPromotion = path9.join(wpaths.projectSessions, session.id);
19101
+ const fleetRoot = directorMode ? path10.join(wpaths.projectSessions, session.id) : void 0;
19102
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path10.join(expectDefined(fleetRoot), "fleet.json") : void 0;
19103
+ const sharedScratchpadPath = directorMode ? path10.join(expectDefined(fleetRoot), "shared") : void 0;
19104
+ const subagentSessionsRoot = directorMode ? path10.join(expectDefined(fleetRoot), "subagents") : void 0;
19105
+ const stateCheckpointPath = directorMode ? path10.join(expectDefined(fleetRoot), "director-state.json") : void 0;
19106
+ const fleetRootForPromotion = path10.join(wpaths.projectSessions, session.id);
18165
19107
  const brainQueue = new BrainDecisionQueue(events);
18166
19108
  const brain = new ObservableBrainArbiter(
18167
19109
  new HumanEscalatingBrainArbiter(new DefaultBrainArbiter(), brainQueue),
@@ -18334,6 +19276,25 @@ async function main(argv) {
18334
19276
  const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
18335
19277
  return `Spawned subagent ${subagentId}${tag} for task ${taskId}. Use /agents to track progress.`;
18336
19278
  },
19279
+ onSpawnAndWait: async (description, spawnOpts) => {
19280
+ const result = await multiAgentHost.spawnAndWait(description, spawnOpts);
19281
+ const tags = [];
19282
+ if (spawnOpts?.provider) tags.push(spawnOpts.provider);
19283
+ if (spawnOpts?.model) tags.push(spawnOpts.model);
19284
+ if (spawnOpts?.name) tags.push(spawnOpts.name);
19285
+ const tag = tags.length > 0 ? ` (${tags.join(" / ")})` : "";
19286
+ const secs = (result.durationMs / 1e3).toFixed(result.durationMs < 1e4 ? 1 : 0);
19287
+ const icon = result.status === "success" ? "\u2713" : result.status === "timeout" ? "\u23F1" : result.status === "stopped" ? "\u2298" : "\u2717";
19288
+ const resultPreview = typeof result.result === "string" && result.result.trim() ? `
19289
+ ${color.dim("\u2500".repeat(40))}
19290
+ ${result.result.trim().slice(0, 600)}${result.result.trim().length > 600 ? "\n\u2026" : ""}
19291
+ ${color.dim("\u2500".repeat(40))}` : "";
19292
+ return [
19293
+ `${icon} ${color.bold(tag ? tag.slice(1) : "subagent")} ${result.status} (${result.iterations} iter \xB7 ${result.toolCalls} tools \xB7 ${secs}s)`,
19294
+ resultPreview,
19295
+ result.error ? ` ${color.amber(`error: ${result.error.message}`)}` : ""
19296
+ ].filter(Boolean).join("\n");
19297
+ },
18337
19298
  onAgents: (subagentId) => {
18338
19299
  const s = multiAgentHost.status();
18339
19300
  if (subagentId) {
@@ -18526,7 +19487,7 @@ async function main(argv) {
18526
19487
  return director.spawn(cfg);
18527
19488
  },
18528
19489
  onFleetLog: async (subagentId, mode) => {
18529
- const subagentsRoot = path9.join(fleetRootForPromotion, "subagents");
19490
+ const subagentsRoot = path10.join(fleetRootForPromotion, "subagents");
18530
19491
  let runDirs;
18531
19492
  try {
18532
19493
  runDirs = await fsp4.readdir(subagentsRoot);
@@ -18535,7 +19496,7 @@ async function main(argv) {
18535
19496
  }
18536
19497
  const found = [];
18537
19498
  for (const runId of runDirs) {
18538
- const runDir = path9.join(subagentsRoot, runId);
19499
+ const runDir = path10.join(subagentsRoot, runId);
18539
19500
  let files;
18540
19501
  try {
18541
19502
  files = await fsp4.readdir(runDir);
@@ -18544,7 +19505,7 @@ async function main(argv) {
18544
19505
  }
18545
19506
  for (const f of files) {
18546
19507
  if (!f.endsWith(".jsonl")) continue;
18547
- const full = path9.join(runDir, f);
19508
+ const full = path10.join(runDir, f);
18548
19509
  try {
18549
19510
  const stat4 = await fsp4.stat(full);
18550
19511
  found.push({
@@ -18645,7 +19606,7 @@ async function main(argv) {
18645
19606
  }
18646
19607
  const dir = await multiAgentHost.ensureDirector();
18647
19608
  if (!dir) return "Director is not available.";
18648
- const dirStatePath = path9.join(fleetRootForPromotion, "director-state.json");
19609
+ const dirStatePath = path10.join(fleetRootForPromotion, "director-state.json");
18649
19610
  const prior = await loadDirectorState(dirStatePath);
18650
19611
  if (!prior) {
18651
19612
  return "No prior director-state.json found \u2014 nothing to retry.";
@@ -18714,9 +19675,9 @@ async function main(argv) {
18714
19675
  for (const tool of director2.tools(FLEET_ROSTER)) {
18715
19676
  toolRegistry.register(tool);
18716
19677
  }
18717
- const mp = path9.join(fleetRootForPromotion, "fleet.json");
18718
- const sp = path9.join(fleetRootForPromotion, "shared");
18719
- const ss = path9.join(fleetRootForPromotion, "subagents");
19678
+ const mp = path10.join(fleetRootForPromotion, "fleet.json");
19679
+ const sp = path10.join(fleetRootForPromotion, "shared");
19680
+ const ss = path10.join(fleetRootForPromotion, "subagents");
18720
19681
  const lines = [
18721
19682
  `${color.green("\u2713")} Promoted to director mode.`,
18722
19683
  ` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
@@ -18790,6 +19751,12 @@ Restart WrongStack to load or unload plugin code in this session.`;
18790
19751
  }
18791
19752
  return nextPredictEnabled;
18792
19753
  },
19754
+ onSuggestions: (suggestions) => {
19755
+ if (suggestions !== void 0) {
19756
+ currentSuggestions = suggestions;
19757
+ }
19758
+ return currentSuggestions;
19759
+ },
18793
19760
  onAutonomy: (setTo) => {
18794
19761
  if (setTo !== void 0) {
18795
19762
  autonomyMode = setTo;
@@ -18866,6 +19833,7 @@ Restart WrongStack to load or unload plugin code in this session.`;
18866
19833
  message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
18867
19834
  };
18868
19835
  }
19836
+ return void 0;
18869
19837
  },
18870
19838
  onClear: () => {
18871
19839
  if (flags.tui && !flags["no-tui"]) return;
@@ -19035,6 +20003,54 @@ Restart WrongStack to load or unload plugin code in this session.`;
19035
20003
  return autonomyMode;
19036
20004
  },
19037
20005
  getNextPredict: () => nextPredictEnabled,
20006
+ onSuggestionsParsed: (suggestions) => {
20007
+ currentSuggestions = suggestions ?? [];
20008
+ },
20009
+ getSuggestions: () => currentSuggestions,
20010
+ autoProceedDelayMs: config.autonomy?.autoProceedDelayMs ?? 45e3,
20011
+ autoProceedMaxIterations: config.autonomy?.autoProceedMaxIterations ?? 50,
20012
+ onValidateAutoProceed: async (suggestion, lastOutput) => {
20013
+ try {
20014
+ const resp = await context.provider.complete(
20015
+ {
20016
+ model: context.model,
20017
+ system: [
20018
+ {
20019
+ type: "text",
20020
+ text: "You are a safety validator for an autonomous coding agent. Your ONLY job is to decide whether the agent should auto-proceed with a suggested next step, or whether a human should review first. Reply with exactly one word: YES or NO."
20021
+ }
20022
+ ],
20023
+ messages: [
20024
+ {
20025
+ role: "user",
20026
+ content: [
20027
+ {
20028
+ type: "text",
20029
+ text: `The autonomous agent just completed a turn and generated this top-ranked next-step suggestion:
20030
+
20031
+ "${suggestion}"
20032
+
20033
+ ${lastOutput ? `Recent agent output:
20034
+ ${lastOutput.slice(0, 500)}
20035
+
20036
+ ` : ""}Should the agent auto-proceed with this suggestion, or should a human review first?
20037
+
20038
+ Reply YES to auto-proceed, NO to wait for human input.`
20039
+ }
20040
+ ]
20041
+ }
20042
+ ],
20043
+ maxTokens: 5,
20044
+ temperature: 0
20045
+ },
20046
+ { signal: AbortSignal.timeout(1e4) }
20047
+ );
20048
+ const text = resp.content.filter((b) => b.type === "text").map((b) => "text" in b ? b.text : "").join("").trim().toUpperCase();
20049
+ return text.startsWith("YES");
20050
+ } catch {
20051
+ return false;
20052
+ }
20053
+ },
19038
20054
  getEternalEngine: () => eternalEngine,
19039
20055
  getParallelEngine: () => parallelEngine,
19040
20056
  subscribeEternalIteration: (fn) => {