@wrongstack/cli 0.260.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fsp5 from 'fs/promises';
3
- import * as path39 from 'path';
3
+ import * as path4 from 'path';
4
4
  import { join } from 'path';
5
- import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, DefaultSecretScrubber, resolveProjectDir, GlobalMailbox, TOKENS, ToolRegistry, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, attachDepWatcherBridge, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, wstackGlobalRoot, DEFAULT_CONTEXT_WINDOW_MODE_ID, recentTextTurns, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, DefaultLogger, DefaultModelsRegistry, isStdinTTY, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, noOpVault, decryptConfigSecrets, encryptConfigSecrets, atomicWrite, setQueuedMessagesSnapshot, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, FsError, ConfigError, InputBuilder, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, resolveMailboxIdentity, isSecretField as isSecretField$1 } from '@wrongstack/core';
6
- import { DefaultSecretVault, encryptConfigSecrets as encryptConfigSecrets$1, decryptConfigSecrets as decryptConfigSecrets$1, isSecretField } from '@wrongstack/core/security';
5
+ import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, DefaultSecretScrubber, projectHash, wstackGlobalRoot, resolveProjectDir, GlobalMailbox, TOKENS, ToolRegistry, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, DEFAULT_CONTEXT_WINDOW_MODE_ID, recentTextTurns, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, DefaultLogger, DefaultModelsRegistry, isStdinTTY, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, AutonomousCoordinator, noOpVault, decryptConfigSecrets, encryptConfigSecrets, atomicWrite, setQueuedMessagesSnapshot, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, StreamHangError, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, FsError, ConfigError, InputBuilder, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, resolveMailboxIdentity, isSecretField as isSecretField$1 } from '@wrongstack/core';
6
+ import { decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DefaultSecretVault, isSecretField } from '@wrongstack/core/security';
7
7
  import * as crypto3 from 'crypto';
8
8
  import { createHash, randomBytes, randomUUID } from 'crypto';
9
9
  import { createRequire } from 'module';
10
10
  import * as os from 'os';
11
11
  import os__default from 'os';
12
- import { findFreePort, AutoPhaseWebSocketHandler, handleShellOpen, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
12
+ import { findFreePort, AutoPhaseWebSocketHandler, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
13
13
  import { makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry } from '@wrongstack/providers';
14
- import { getProcessRegistry, builtinToolsPack, TIER1_TOOLS, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, resetIndexCircuitBreaker } from '@wrongstack/tools';
14
+ import { toErrorMessage } from '@wrongstack/core/utils';
15
+ import { getProcessRegistry, builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, TIER2_TOOLS, TIER3_TOOLS, TIER1_TOOLS, resetIndexCircuitBreaker } from '@wrongstack/tools';
15
16
  import { DefaultSessionStore } from '@wrongstack/core/storage';
17
+ import { probeLocalLlm } from '@wrongstack/runtime/probe';
18
+ import * as fs2 from 'fs';
19
+ import { watch, writeFileSync, existsSync, readFileSync } from 'fs';
20
+ import { SkillInstaller } from '@wrongstack/core/skills';
16
21
  import { WebSocketServer, WebSocket } from 'ws';
17
22
  import { spawn, execFile, execFileSync } from 'child_process';
18
23
  import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
19
- import * as fs2 from 'fs';
20
- import { writeFileSync, existsSync, readFileSync } from 'fs';
21
24
  import { fileURLToPath } from 'url';
22
25
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
23
26
  import * as readline from 'readline';
27
+ import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, runEnsemble, renderEnsembleText, EnsembleRegistry, makeACPSubagentRunnerWithStop, findAgentDescriptor } from '@wrongstack/acp';
24
28
  import { parseNextSteps } from '@wrongstack/tui';
25
- import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
26
29
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
27
- import { ACP_AGENTS, SubagentBudget } from '@wrongstack/core/coordination';
30
+ import { SubagentBudget } from '@wrongstack/core/coordination';
28
31
  import { loadBenchConfig, reportHeaderLine, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench } from '@wrongstack/bench';
29
32
  import { allServers } from '@wrongstack/core/infrastructure';
30
33
  import { ToolExecutor } from '@wrongstack/core/execution';
@@ -66,15 +69,57 @@ async function pathExists(file) {
66
69
  return false;
67
70
  }
68
71
  }
72
+ async function hasRootFileWithSuffix(root, suffixes) {
73
+ let names;
74
+ try {
75
+ names = await fsp5.readdir(root);
76
+ } catch {
77
+ return false;
78
+ }
79
+ const lower = suffixes.map((s) => s.toLowerCase());
80
+ return names.some((n) => lower.some((s) => n.toLowerCase().endsWith(s)));
81
+ }
82
+ function parseCiRunCommands(yaml) {
83
+ const commands = [];
84
+ const lines = yaml.split(/\r?\n/);
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i] ?? "";
87
+ const m = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(line);
88
+ if (!m) continue;
89
+ const indent = (m[1] ?? "").length;
90
+ const inline = (m[2] ?? "").trim();
91
+ if (inline && inline !== "|" && inline !== ">" && !/^[|>][+-]?$/.test(inline)) {
92
+ commands.push(inline);
93
+ continue;
94
+ }
95
+ for (let j = i + 1; j < lines.length; j++) {
96
+ const body = lines[j] ?? "";
97
+ if (body.trim() === "") continue;
98
+ const bodyIndent = body.length - body.trimStart().length;
99
+ if (bodyIndent <= indent) break;
100
+ commands.push(body.trim());
101
+ i = j;
102
+ }
103
+ }
104
+ return commands.filter((c) => c !== "" && !/^(#|cd\s|echo\s|export\s|set\s|if\s)/.test(c));
105
+ }
106
+ function applyCiCommands(facts, commands) {
107
+ const find = (re) => commands.find((c) => re.test(c));
108
+ facts.test ??= find(/\b(test|pytest|vitest|jest|go test|cargo test|mix test|rspec)\b/i);
109
+ facts.lint ??= find(/\b(lint|eslint|biome|clippy|ruff|flake8|golangci-lint|fmt --check)\b/i);
110
+ facts.build ??= find(
111
+ /\b(build|compile|tsc|cargo build|go build|mvn .*package|gradle .*build)\b/i
112
+ );
113
+ }
69
114
  async function detectPackageManager2(root, declared) {
70
115
  if (declared) {
71
116
  const name = declared.split("@")[0];
72
117
  if (name) return name;
73
118
  }
74
- if (await pathExists(path39.join(root, "pnpm-lock.yaml"))) return "pnpm";
75
- if (await pathExists(path39.join(root, "bun.lockb"))) return "bun";
76
- if (await pathExists(path39.join(root, "bun.lock"))) return "bun";
77
- if (await pathExists(path39.join(root, "yarn.lock"))) return "yarn";
119
+ if (await pathExists(path4.join(root, "pnpm-lock.yaml"))) return "pnpm";
120
+ if (await pathExists(path4.join(root, "bun.lockb"))) return "bun";
121
+ if (await pathExists(path4.join(root, "bun.lock"))) return "bun";
122
+ if (await pathExists(path4.join(root, "yarn.lock"))) return "yarn";
78
123
  return "npm";
79
124
  }
80
125
  function hasUsableScript(scripts, name) {
@@ -92,10 +137,50 @@ function parseMakeTargets(makefile) {
92
137
  }
93
138
  return targets;
94
139
  }
140
+ async function scanSourceTree(root) {
141
+ const counts = /* @__PURE__ */ new Map();
142
+ const entryPoints = [];
143
+ const topDirs = [];
144
+ let fileCount = 0;
145
+ const MAX_FILES = 5e3;
146
+ const MAX_DEPTH = 6;
147
+ async function walk(dir, depth, rel) {
148
+ if (fileCount >= MAX_FILES || depth > MAX_DEPTH) return;
149
+ let entries;
150
+ try {
151
+ entries = await fsp5.readdir(dir, { withFileTypes: true });
152
+ } catch {
153
+ return;
154
+ }
155
+ for (const e of entries) {
156
+ if (fileCount >= MAX_FILES) return;
157
+ const name = e.name;
158
+ if (e.isDirectory()) {
159
+ if (SCAN_IGNORE_DIRS.has(name) || name.startsWith(".")) continue;
160
+ if (depth === 0) topDirs.push(name);
161
+ await walk(path4.join(dir, name), depth + 1, rel ? `${rel}/${name}` : name);
162
+ } else if (e.isFile()) {
163
+ fileCount++;
164
+ const ext = path4.extname(name).toLowerCase();
165
+ const lang = EXT_LANG[ext];
166
+ if (!lang) continue;
167
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
168
+ const base = name.slice(0, name.length - ext.length).toLowerCase();
169
+ if (entryPoints.length < 10 && ENTRY_BASENAMES.has(base)) {
170
+ entryPoints.push(rel ? `${rel}/${name}` : name);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ await walk(root, 0, "");
176
+ if (counts.size === 0) return void 0;
177
+ const languages = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([lang, n]) => `${lang} (${n})`);
178
+ return { languages, entryPoints, topDirs: topDirs.sort() };
179
+ }
95
180
  async function detectProjectFacts(root) {
96
181
  const facts = { hints: [] };
97
182
  try {
98
- const pkg = JSON.parse(await fsp5.readFile(path39.join(root, "package.json"), "utf8"));
183
+ const pkg = JSON.parse(await fsp5.readFile(path4.join(root, "package.json"), "utf8"));
99
184
  const scripts = pkg.scripts ?? {};
100
185
  const pm = await detectPackageManager2(root, pkg.packageManager);
101
186
  if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
@@ -109,14 +194,14 @@ async function detectProjectFacts(root) {
109
194
  } catch {
110
195
  }
111
196
  try {
112
- if (!await pathExists(path39.join(root, "pyproject.toml"))) throw new Error("not python");
197
+ if (!await pathExists(path4.join(root, "pyproject.toml"))) throw new Error("not python");
113
198
  facts.test ??= "pytest";
114
199
  facts.lint ??= "ruff check .";
115
200
  facts.hints.push("pyproject.toml");
116
201
  } catch {
117
202
  }
118
203
  try {
119
- if (!await pathExists(path39.join(root, "go.mod"))) throw new Error("not go");
204
+ if (!await pathExists(path4.join(root, "go.mod"))) throw new Error("not go");
120
205
  facts.build ??= "go build ./...";
121
206
  facts.test ??= "go test ./...";
122
207
  facts.run ??= "go run .";
@@ -124,7 +209,7 @@ async function detectProjectFacts(root) {
124
209
  } catch {
125
210
  }
126
211
  try {
127
- if (!await pathExists(path39.join(root, "Cargo.toml"))) throw new Error("not rust");
212
+ if (!await pathExists(path4.join(root, "Cargo.toml"))) throw new Error("not rust");
128
213
  facts.build ??= "cargo build";
129
214
  facts.test ??= "cargo test";
130
215
  facts.lint ??= "cargo clippy";
@@ -133,7 +218,7 @@ async function detectProjectFacts(root) {
133
218
  } catch {
134
219
  }
135
220
  try {
136
- const makefile = await fsp5.readFile(path39.join(root, "Makefile"), "utf8");
221
+ const makefile = await fsp5.readFile(path4.join(root, "Makefile"), "utf8");
137
222
  const targets = parseMakeTargets(makefile);
138
223
  facts.build ??= targets.has("build") ? "make build" : "make";
139
224
  if (targets.has("test")) facts.test ??= "make test";
@@ -143,6 +228,118 @@ async function detectProjectFacts(root) {
143
228
  facts.hints.push("Makefile");
144
229
  } catch {
145
230
  }
231
+ try {
232
+ const composer = JSON.parse(await fsp5.readFile(path4.join(root, "composer.json"), "utf8"));
233
+ const scripts = composer.scripts ?? {};
234
+ if ("test" in scripts) facts.test ??= "composer test";
235
+ if ("lint" in scripts) facts.lint ??= "composer lint";
236
+ facts.hints.push("composer.json");
237
+ } catch {
238
+ }
239
+ try {
240
+ if (!await pathExists(path4.join(root, "pom.xml"))) throw new Error("not maven");
241
+ facts.build ??= "mvn package";
242
+ facts.test ??= "mvn test";
243
+ facts.hints.push("pom.xml");
244
+ } catch {
245
+ }
246
+ try {
247
+ const hasGradle = await pathExists(path4.join(root, "build.gradle")) || await pathExists(path4.join(root, "build.gradle.kts"));
248
+ if (!hasGradle) throw new Error("not gradle");
249
+ const g = await pathExists(path4.join(root, "gradlew")) ? "./gradlew" : "gradle";
250
+ facts.build ??= `${g} build`;
251
+ facts.test ??= `${g} test`;
252
+ facts.hints.push("Gradle");
253
+ } catch {
254
+ }
255
+ try {
256
+ const hasDotnet = await pathExists(path4.join(root, "global.json")) || await hasRootFileWithSuffix(root, [".csproj", ".fsproj", ".sln"]);
257
+ if (!hasDotnet) throw new Error("not dotnet");
258
+ facts.build ??= "dotnet build";
259
+ facts.test ??= "dotnet test";
260
+ facts.run ??= "dotnet run";
261
+ facts.hints.push(".NET project");
262
+ } catch {
263
+ }
264
+ try {
265
+ if (!await pathExists(path4.join(root, "mix.exs"))) throw new Error("not elixir");
266
+ facts.build ??= "mix compile";
267
+ facts.test ??= "mix test";
268
+ facts.lint ??= "mix format --check-formatted";
269
+ facts.run ??= "mix run";
270
+ facts.hints.push("mix.exs");
271
+ } catch {
272
+ }
273
+ try {
274
+ if (!await pathExists(path4.join(root, "pubspec.yaml"))) throw new Error("not dart");
275
+ facts.test ??= "dart test";
276
+ facts.lint ??= "dart analyze";
277
+ facts.hints.push("pubspec.yaml");
278
+ } catch {
279
+ }
280
+ try {
281
+ const hasDeno = await pathExists(path4.join(root, "deno.json")) || await pathExists(path4.join(root, "deno.jsonc"));
282
+ if (!hasDeno) throw new Error("not deno");
283
+ facts.test ??= "deno test";
284
+ facts.lint ??= "deno lint";
285
+ facts.hints.push("deno.json");
286
+ } catch {
287
+ }
288
+ try {
289
+ if (!await pathExists(path4.join(root, "Package.swift"))) throw new Error("not swift");
290
+ facts.build ??= "swift build";
291
+ facts.test ??= "swift test";
292
+ facts.run ??= "swift run";
293
+ facts.hints.push("Package.swift");
294
+ } catch {
295
+ }
296
+ try {
297
+ if (!await pathExists(path4.join(root, "Gemfile"))) throw new Error("not ruby");
298
+ if (await pathExists(path4.join(root, "Rakefile"))) facts.test ??= "bundle exec rake test";
299
+ facts.hints.push("Gemfile");
300
+ } catch {
301
+ }
302
+ try {
303
+ if (!await pathExists(path4.join(root, "CMakeLists.txt"))) throw new Error("not cmake");
304
+ facts.build ??= "cmake -B build && cmake --build build";
305
+ facts.test ??= "ctest --test-dir build";
306
+ facts.hints.push("CMakeLists.txt");
307
+ } catch {
308
+ }
309
+ try {
310
+ const hasPip = await pathExists(path4.join(root, "requirements.txt")) || await pathExists(path4.join(root, "setup.py")) || await pathExists(path4.join(root, "setup.cfg"));
311
+ if (!hasPip) throw new Error("not pip");
312
+ facts.test ??= "pytest";
313
+ facts.hints.push("requirements.txt");
314
+ } catch {
315
+ }
316
+ if (!facts.build || !facts.test || !facts.lint) {
317
+ try {
318
+ const wfDir = path4.join(root, ".github", "workflows");
319
+ const wfNames = (await fsp5.readdir(wfDir)).filter((n) => /\.ya?ml$/i.test(n));
320
+ const commands = [];
321
+ for (const n of wfNames) {
322
+ commands.push(...parseCiRunCommands(await fsp5.readFile(path4.join(wfDir, n), "utf8")));
323
+ }
324
+ if (commands.length > 0) {
325
+ const before = { build: facts.build, test: facts.test, lint: facts.lint };
326
+ applyCiCommands(facts, commands);
327
+ if (facts.build !== before.build || facts.test !== before.test || facts.lint !== before.lint) {
328
+ facts.hints.push(".github/workflows");
329
+ }
330
+ }
331
+ } catch {
332
+ }
333
+ }
334
+ if (!facts.build && !facts.test && !facts.run && !facts.lint) {
335
+ const scan = await scanSourceTree(root);
336
+ if (scan) {
337
+ facts.languages = scan.languages;
338
+ if (scan.entryPoints.length > 0) facts.entryPoints = scan.entryPoints;
339
+ if (scan.topDirs.length > 0) facts.topDirs = scan.topDirs;
340
+ facts.hints.push(`source scan: ${scan.languages.join(", ")}`);
341
+ }
342
+ }
146
343
  return facts;
147
344
  }
148
345
  function renderAgentsTemplate(f) {
@@ -150,6 +347,16 @@ function renderAgentsTemplate(f) {
150
347
  const hints = f.hints.length > 0 ? `
151
348
 
152
349
  > Auto-detected: ${f.hints.join(", ")}` : "";
350
+ const runtime = f.languages?.length ? `_CLI, server, browser, worker, library, package?_ \u2014 detected: ${f.languages.join(", ")}` : "_CLI, server, browser, worker, library, package?_";
351
+ const keyFileRows = [];
352
+ for (const ep of f.entryPoints ?? [])
353
+ keyFileRows.push(`| \`${ep}\` | _Likely entry point (detected)_ |`);
354
+ for (const dir of f.topDirs ?? [])
355
+ keyFileRows.push(`| \`${dir}/\` | _Top-level directory (detected)_ |`);
356
+ const keyFiles = keyFileRows.length > 0 ? keyFileRows.join("\n") : `| _src/_ | _Main source entry point(s)_ |
357
+ | _tests/_ | _Test root or convention_ |
358
+ | _docs/_ | _Architecture, runbooks, design notes_ |
359
+ | _scripts/_ | _Automation scripts (CI, release, install, etc.)_ |`;
153
360
  return `# AGENTS.md
154
361
 
155
362
  > **DO NOT DELETE THIS FILE.** It is loaded into WrongStack's system prompt as
@@ -161,7 +368,7 @@ function renderAgentsTemplate(f) {
161
368
 
162
369
  - **Purpose:** _What does this project do and why does it exist?_
163
370
  - **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
164
- - **Runtime / deployment:** _CLI, server, browser, worker, library, package?_${hints}
371
+ - **Runtime / deployment:** ${runtime}${hints}
165
372
 
166
373
  ## How to work safely
167
374
 
@@ -183,10 +390,7 @@ function renderAgentsTemplate(f) {
183
390
 
184
391
  | File / directory | Role |
185
392
  |---|---|
186
- | _src/_ | _Main source entry point(s)_ |
187
- | _tests/_ | _Test root or convention_ |
188
- | _docs/_ | _Architecture, runbooks, design notes_ |
189
- | _scripts/_ | _Automation scripts (CI, release, install, etc.)_ |
393
+ ${keyFiles}
190
394
 
191
395
  ## Architecture notes
192
396
 
@@ -245,8 +449,79 @@ function countToolResults(messages) {
245
449
  function estimateTokens(messages) {
246
450
  return estimateMessageTokens(messages);
247
451
  }
452
+ var EXT_LANG, SCAN_IGNORE_DIRS, ENTRY_BASENAMES;
248
453
  var init_helpers = __esm({
249
454
  "src/slash-commands/helpers.ts"() {
455
+ EXT_LANG = {
456
+ ".ts": "TypeScript",
457
+ ".tsx": "TypeScript",
458
+ ".mts": "TypeScript",
459
+ ".cts": "TypeScript",
460
+ ".js": "JavaScript",
461
+ ".jsx": "JavaScript",
462
+ ".mjs": "JavaScript",
463
+ ".cjs": "JavaScript",
464
+ ".py": "Python",
465
+ ".rb": "Ruby",
466
+ ".go": "Go",
467
+ ".rs": "Rust",
468
+ ".java": "Java",
469
+ ".kt": "Kotlin",
470
+ ".kts": "Kotlin",
471
+ ".c": "C",
472
+ ".h": "C",
473
+ ".cpp": "C++",
474
+ ".cc": "C++",
475
+ ".cxx": "C++",
476
+ ".hpp": "C++",
477
+ ".cs": "C#",
478
+ ".php": "PHP",
479
+ ".swift": "Swift",
480
+ ".scala": "Scala",
481
+ ".clj": "Clojure",
482
+ ".ex": "Elixir",
483
+ ".exs": "Elixir",
484
+ ".erl": "Erlang",
485
+ ".hs": "Haskell",
486
+ ".ml": "OCaml",
487
+ ".dart": "Dart",
488
+ ".lua": "Lua",
489
+ ".jl": "Julia",
490
+ ".sh": "Shell",
491
+ ".bash": "Shell",
492
+ ".ps1": "PowerShell",
493
+ ".pl": "Perl",
494
+ ".zig": "Zig",
495
+ ".nim": "Nim",
496
+ ".sql": "SQL",
497
+ ".vue": "Vue",
498
+ ".svelte": "Svelte"
499
+ };
500
+ SCAN_IGNORE_DIRS = /* @__PURE__ */ new Set([
501
+ ".git",
502
+ "node_modules",
503
+ "dist",
504
+ "build",
505
+ "out",
506
+ "target",
507
+ "vendor",
508
+ ".venv",
509
+ "venv",
510
+ "env",
511
+ "__pycache__",
512
+ ".next",
513
+ ".nuxt",
514
+ ".cache",
515
+ "coverage",
516
+ ".wrongstack",
517
+ ".idea",
518
+ ".vscode",
519
+ "obj",
520
+ ".gradle",
521
+ ".dart_tool",
522
+ "Pods"
523
+ ]);
524
+ ENTRY_BASENAMES = /* @__PURE__ */ new Set(["main", "index", "app", "cli", "server", "__main__"]);
250
525
  }
251
526
  });
252
527
  function normalizeKeys(cfg) {
@@ -267,11 +542,18 @@ function writeKeysBack(cfg, keys) {
267
542
  }
268
543
  cfg.apiKeys = keys;
269
544
  const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
270
- cfg.apiKey = active.apiKey;
545
+ delete cfg.apiKey;
271
546
  if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
272
547
  cfg.activeKey = active.label;
273
548
  }
274
549
  }
550
+ function resolveActiveApiKey(cfg) {
551
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
552
+ const active = cfg.activeKey ? cfg.apiKeys.find((k) => k.label === cfg.activeKey) : void 0;
553
+ return (active ?? cfg.apiKeys[0])?.apiKey;
554
+ }
555
+ return cfg.apiKey && cfg.apiKey.length > 0 ? cfg.apiKey : void 0;
556
+ }
275
557
  function activeLabel(cfg, keys) {
276
558
  if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
277
559
  return keys[0]?.label;
@@ -372,26 +654,26 @@ function fmtDuration(ms) {
372
654
  const remMin = m - h * 60;
373
655
  return `${h}h${remMin}m`;
374
656
  }
375
- function fmtTaskResultLine(r, color72) {
657
+ function fmtTaskResultLine(r, color74) {
376
658
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
377
659
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
378
660
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
379
661
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
380
- const errKindChip = errKind ? color72.dim(` [${errKind}]`) : "";
381
- const errSnip = errMsg || errKind ? `${errKindChip}${color72.dim(errTail)}` : "";
662
+ const errKindChip = errKind ? color74.dim(` [${errKind}]`) : "";
663
+ const errSnip = errMsg || errKind ? `${errKindChip}${color74.dim(errTail)}` : "";
382
664
  switch (r.status) {
383
665
  case "success":
384
- return { mark: color72.green("\u2713"), stats, tail: "" };
666
+ return { mark: color74.green("\u2713"), stats, tail: "" };
385
667
  case "timeout":
386
668
  return {
387
- mark: color72.yellow("\u23F1"),
388
- stats: `${color72.yellow("timeout")} ${stats}`,
669
+ mark: color74.yellow("\u23F1"),
670
+ stats: `${color74.yellow("timeout")} ${stats}`,
389
671
  tail: errSnip
390
672
  };
391
673
  case "stopped":
392
- return { mark: color72.dim("\u2298"), stats: `${color72.dim("stopped")} ${stats}`, tail: errSnip };
674
+ return { mark: color74.dim("\u2298"), stats: `${color74.dim("stopped")} ${stats}`, tail: errSnip };
393
675
  case "failed":
394
- return { mark: color72.red("\u2717"), stats: `${color72.red("failed")} ${stats}`, tail: errSnip };
676
+ return { mark: color74.red("\u2717"), stats: `${color74.red("failed")} ${stats}`, tail: errSnip };
395
677
  }
396
678
  }
397
679
  var init_utils = __esm({
@@ -676,7 +958,7 @@ async function findSpec(store, idOrTitle) {
676
958
  async function gatherProjectContext2(projectRoot) {
677
959
  const parts = [];
678
960
  try {
679
- const pkgPath = path39.join(projectRoot, "package.json");
961
+ const pkgPath = path4.join(projectRoot, "package.json");
680
962
  const pkgRaw = await fsp5.readFile(pkgPath, "utf8");
681
963
  const pkg = JSON.parse(pkgRaw);
682
964
  parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
@@ -694,13 +976,13 @@ async function gatherProjectContext2(projectRoot) {
694
976
  } catch {
695
977
  }
696
978
  try {
697
- const tsconfigPath = path39.join(projectRoot, "tsconfig.json");
979
+ const tsconfigPath = path4.join(projectRoot, "tsconfig.json");
698
980
  await fsp5.access(tsconfigPath);
699
981
  parts.push("Language: TypeScript");
700
982
  } catch {
701
983
  }
702
984
  try {
703
- const srcDir = path39.join(projectRoot, "src");
985
+ const srcDir = path4.join(projectRoot, "src");
704
986
  const entries = await fsp5.readdir(srcDir, { withFileTypes: true });
705
987
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
706
988
  if (dirs.length > 0) parts.push(`Source structure: src/${dirs.join(", src/")}`);
@@ -2125,12 +2407,12 @@ __export(project_utils_exports, {
2125
2407
  touchProjectInManifest: () => touchProjectInManifest
2126
2408
  });
2127
2409
  function projectsJsonPath(globalConfigPath) {
2128
- const base = globalConfigPath ? path39.dirname(globalConfigPath) : wstackGlobalRoot();
2129
- return path39.join(base, "projects.json");
2410
+ const base = globalConfigPath ? path4.dirname(globalConfigPath) : wstackGlobalRoot();
2411
+ return path4.join(base, "projects.json");
2130
2412
  }
2131
2413
  function projectsDataDir(globalConfigPath) {
2132
- const base = globalConfigPath ? path39.dirname(globalConfigPath) : wstackGlobalRoot();
2133
- return path39.join(base, "projects");
2414
+ const base = globalConfigPath ? path4.dirname(globalConfigPath) : wstackGlobalRoot();
2415
+ return path4.join(base, "projects");
2134
2416
  }
2135
2417
  async function loadManifest(globalConfigPath) {
2136
2418
  const file = projectsJsonPath(globalConfigPath);
@@ -2144,12 +2426,12 @@ async function loadManifest(globalConfigPath) {
2144
2426
  }
2145
2427
  async function saveManifest(manifest, globalConfigPath) {
2146
2428
  const file = projectsJsonPath(globalConfigPath);
2147
- await fsp5.mkdir(path39.dirname(file), { recursive: true });
2429
+ await fsp5.mkdir(path4.dirname(file), { recursive: true });
2148
2430
  await fsp5.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
2149
2431
  }
2150
2432
  function generateSlug(root) {
2151
- const base = path39.basename(root).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2152
- const hash = createHash("sha256").update(path39.resolve(root)).digest("hex").slice(0, 6);
2433
+ const base = path4.basename(root).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2434
+ const hash = createHash("sha256").update(path4.resolve(root)).digest("hex").slice(0, 6);
2153
2435
  return `${base}-${hash}`;
2154
2436
  }
2155
2437
  function findProject(manifest, query) {
@@ -2164,29 +2446,29 @@ function findProject(manifest, query) {
2164
2446
  return found;
2165
2447
  }
2166
2448
  async function ensureProjectDataDir(slug, globalConfigPath) {
2167
- const dir = path39.join(projectsDataDir(globalConfigPath), slug);
2449
+ const dir = path4.join(projectsDataDir(globalConfigPath), slug);
2168
2450
  await fsp5.mkdir(dir, { recursive: true });
2169
2451
  return dir;
2170
2452
  }
2171
2453
  async function touchProjectInManifest(opts) {
2172
- const root = path39.resolve(opts.projectRoot);
2454
+ const root = path4.resolve(opts.projectRoot);
2173
2455
  const file = projectsJsonPath(opts.globalConfigPath);
2174
2456
  let entry;
2175
2457
  await withFileLock(file, async () => {
2176
2458
  const manifest = await loadManifest(opts.globalConfigPath);
2177
2459
  const now = (/* @__PURE__ */ new Date()).toISOString();
2178
- entry = manifest.projects.find((p) => path39.resolve(p.root) === root);
2460
+ entry = manifest.projects.find((p) => path4.resolve(p.root) === root);
2179
2461
  if (entry) {
2180
2462
  entry.lastSeen = now;
2181
- if (opts.workingDir) entry.lastWorkingDir = path39.resolve(opts.workingDir);
2463
+ if (opts.workingDir) entry.lastWorkingDir = path4.resolve(opts.workingDir);
2182
2464
  } else {
2183
2465
  entry = {
2184
- name: opts.name ?? path39.basename(root),
2466
+ name: opts.name ?? path4.basename(root),
2185
2467
  root,
2186
2468
  slug: generateSlug(root),
2187
2469
  createdAt: now,
2188
2470
  lastSeen: now,
2189
- lastWorkingDir: opts.workingDir ? path39.resolve(opts.workingDir) : void 0
2471
+ lastWorkingDir: opts.workingDir ? path4.resolve(opts.workingDir) : void 0
2190
2472
  };
2191
2473
  manifest.projects.push(entry);
2192
2474
  }
@@ -2632,7 +2914,7 @@ __export(update_check_exports, {
2632
2914
  getUpdateNotification: () => getUpdateNotification
2633
2915
  });
2634
2916
  function cachePath(homeFn = defaultHomeDir2) {
2635
- return path39.join(homeFn(), ".wrongstack", "update-cache.json");
2917
+ return path4.join(homeFn(), ".wrongstack", "update-cache.json");
2636
2918
  }
2637
2919
  function currentVersion() {
2638
2920
  const req2 = createRequire(import.meta.url);
@@ -2669,7 +2951,7 @@ async function readCache(homeFn = defaultHomeDir2) {
2669
2951
  }
2670
2952
  async function writeCache(entry, homeFn = defaultHomeDir2) {
2671
2953
  try {
2672
- const dir = path39.dirname(cachePath(homeFn));
2954
+ const dir = path4.dirname(cachePath(homeFn));
2673
2955
  await fsp5.mkdir(dir, { recursive: true });
2674
2956
  await fsp5.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
2675
2957
  } catch {
@@ -2755,7 +3037,7 @@ function registerWebuiInstance(p, deps = {}) {
2755
3037
  wsPort: p.wsPort,
2756
3038
  host: p.host,
2757
3039
  projectRoot: p.projectRoot,
2758
- projectName: path39.basename(p.projectRoot) || p.projectRoot,
3040
+ projectName: path4.basename(p.projectRoot) || p.projectRoot,
2759
3041
  startedAt: p.startedAt,
2760
3042
  url: `http://${p.host}:${p.httpPort}`
2761
3043
  },
@@ -2766,7 +3048,7 @@ function registerWebuiInstance(p, deps = {}) {
2766
3048
  function announceWebuiReady(p) {
2767
3049
  const log = p.log ?? ((m) => console.log(m));
2768
3050
  const launch = p.openBrowserFn ?? openBrowser;
2769
- const openUrl = `http://${p.host}:${p.httpPort}`;
3051
+ const openUrl = p.wsToken ? `http://${p.host}:${p.httpPort}?token=${encodeURIComponent(p.wsToken)}` : `http://${p.host}:${p.httpPort}`;
2770
3052
  p.server.on("listening", () => {
2771
3053
  log(
2772
3054
  `
@@ -2819,7 +3101,7 @@ var init_lifecycle = __esm({
2819
3101
  }
2820
3102
  });
2821
3103
  function getVault(globalConfigPath) {
2822
- const keyFile = path39.join(path39.dirname(globalConfigPath ?? ""), ".key");
3104
+ const keyFile = path4.join(path4.dirname(globalConfigPath ?? ""), ".key");
2823
3105
  return new DefaultSecretVault({ keyFile });
2824
3106
  }
2825
3107
  async function loadSavedProviders(globalConfigPath) {
@@ -2853,7 +3135,7 @@ function resolveDistDir() {
2853
3135
  try {
2854
3136
  const requireFromHere = createRequire(import.meta.url);
2855
3137
  const serverEntry = requireFromHere.resolve("@wrongstack/webui/server");
2856
- return path39.resolve(path39.dirname(serverEntry), "..");
3138
+ return path4.resolve(path4.dirname(serverEntry), "..");
2857
3139
  } catch {
2858
3140
  return null;
2859
3141
  }
@@ -2867,7 +3149,9 @@ function startStaticServe(opts, deps = {}) {
2867
3149
  host: opts.host,
2868
3150
  distDir,
2869
3151
  wsPort: opts.wsPort,
2870
- globalRoot: opts.globalRoot
3152
+ globalRoot: opts.globalRoot,
3153
+ onFleetPing: opts.onFleetPing,
3154
+ apiToken: opts.apiToken
2871
3155
  });
2872
3156
  server.listen(opts.httpPort, opts.host);
2873
3157
  return { server, port: opts.httpPort };
@@ -2908,7 +3192,7 @@ async function handleModesList(ctx, ws) {
2908
3192
  payload: {
2909
3193
  modes: [],
2910
3194
  activeId: "default",
2911
- error: err instanceof Error ? err.message : String(err)
3195
+ error: toErrorMessage(err)
2912
3196
  }
2913
3197
  });
2914
3198
  }
@@ -2931,7 +3215,7 @@ async function handleModeSwitch(ctx, ws, id) {
2931
3215
  const payload = await ctx.buildSessionStart({ mode: id });
2932
3216
  ctx.broadcast({ type: "session.start", payload });
2933
3217
  } catch (err) {
2934
- sendResult(ctx, ws, false, err instanceof Error ? err.message : String(err));
3218
+ sendResult(ctx, ws, false, toErrorMessage(err));
2935
3219
  }
2936
3220
  }
2937
3221
  async function handleModelSwitch(ctx, ws, payload) {
@@ -2950,7 +3234,7 @@ async function handleModelSwitch(ctx, ws, payload) {
2950
3234
  ctx,
2951
3235
  ws,
2952
3236
  false,
2953
- `Switch failed: ${err instanceof Error ? err.message : String(err)}`
3237
+ `Switch failed: ${toErrorMessage(err)}`
2954
3238
  );
2955
3239
  }
2956
3240
  }
@@ -2999,7 +3283,7 @@ async function handleModelRefine(ctx, ws, text) {
2999
3283
  payload: {
3000
3284
  refined: text,
3001
3285
  english: text,
3002
- error: err instanceof Error ? err.message : String(err)
3286
+ error: toErrorMessage(err)
3003
3287
  }
3004
3288
  });
3005
3289
  }
@@ -3009,8 +3293,6 @@ var init_agent_config = __esm({
3009
3293
  init_provider_config();
3010
3294
  }
3011
3295
  });
3012
-
3013
- // src/webui-server/ws-handlers/brain.ts
3014
3296
  function sendResult2(ctx, ws, success, message) {
3015
3297
  ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
3016
3298
  }
@@ -3064,7 +3346,7 @@ async function handleBrainAsk(ctx, ws, question) {
3064
3346
  ctx,
3065
3347
  ws,
3066
3348
  false,
3067
- `Brain consultation failed: ${err instanceof Error ? err.message : String(err)}`
3349
+ `Brain consultation failed: ${toErrorMessage(err)}`
3068
3350
  );
3069
3351
  }
3070
3352
  }
@@ -3072,8 +3354,6 @@ var init_brain = __esm({
3072
3354
  "src/webui-server/ws-handlers/brain.ts"() {
3073
3355
  }
3074
3356
  });
3075
-
3076
- // src/webui-server/ws-handlers/connection.ts
3077
3357
  async function handleUserMessage(ctx, ws, content) {
3078
3358
  if (ctx.abortControllers.has(ws)) {
3079
3359
  ctx.send(ws, {
@@ -3106,7 +3386,7 @@ async function handleUserMessage(ctx, ws, content) {
3106
3386
  type: "error",
3107
3387
  payload: {
3108
3388
  phase: "agent.run",
3109
- message: err instanceof Error ? err.message : String(err)
3389
+ message: toErrorMessage(err)
3110
3390
  }
3111
3391
  });
3112
3392
  } finally {
@@ -3355,8 +3635,6 @@ var init_cost_helpers = __esm({
3355
3635
  "src/webui-server/cost-helpers.ts"() {
3356
3636
  }
3357
3637
  });
3358
-
3359
- // src/webui-server/ws-handlers/introspection.ts
3360
3638
  async function handleSkillsList(ctx, ws) {
3361
3639
  if (!ctx.skillLoader) {
3362
3640
  ctx.send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
@@ -3387,7 +3665,7 @@ async function handleSkillsList(ctx, ws) {
3387
3665
  payload: {
3388
3666
  skills: [],
3389
3667
  enabled: true,
3390
- error: err instanceof Error ? err.message : String(err)
3668
+ error: toErrorMessage(err)
3391
3669
  }
3392
3670
  });
3393
3671
  }
@@ -3544,8 +3822,8 @@ function sendResult6(ctx, ws, success, message) {
3544
3822
  ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
3545
3823
  }
3546
3824
  async function handleProjectsList(ctx, ws) {
3547
- const projectsBase = ctx.opts.globalConfigPath ? path39.resolve(path39.dirname(ctx.opts.globalConfigPath)) : wstackGlobalRoot();
3548
- const manifestPath = path39.join(projectsBase, "projects.json");
3825
+ const projectsBase = ctx.opts.globalConfigPath ? path4.resolve(path4.dirname(ctx.opts.globalConfigPath)) : wstackGlobalRoot();
3826
+ const manifestPath = path4.join(projectsBase, "projects.json");
3549
3827
  try {
3550
3828
  const raw = await fsp5.readFile(manifestPath, "utf8");
3551
3829
  const manifest = JSON.parse(raw);
@@ -3558,22 +3836,22 @@ async function handleProjectsSelect(ctx, ws, payload) {
3558
3836
  const { opts } = ctx;
3559
3837
  const { root, name: projectName } = payload;
3560
3838
  try {
3561
- const resolved = path39.resolve(root);
3839
+ const resolved = path4.resolve(root);
3562
3840
  const stat7 = await fsp5.stat(resolved).catch(() => null);
3563
3841
  if (!stat7?.isDirectory()) {
3564
3842
  ctx.send(ws, {
3565
3843
  type: "projects.selected",
3566
3844
  payload: {
3567
3845
  root,
3568
- name: projectName ?? path39.basename(root),
3846
+ name: projectName ?? path4.basename(root),
3569
3847
  message: `Cannot switch: not a directory: ${resolved}`
3570
3848
  }
3571
3849
  });
3572
3850
  return;
3573
3851
  }
3574
3852
  const manifest = await loadManifest(opts.globalConfigPath);
3575
- const entry = manifest.projects.find((p) => path39.resolve(p.root) === resolved);
3576
- const displayName = projectName?.trim() || entry?.name || path39.basename(resolved);
3853
+ const entry = manifest.projects.find((p) => path4.resolve(p.root) === resolved);
3854
+ const displayName = projectName?.trim() || entry?.name || path4.basename(resolved);
3577
3855
  if (entry) {
3578
3856
  entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
3579
3857
  } else {
@@ -3619,8 +3897,8 @@ async function handleProjectsSelect(ctx, ws, payload) {
3619
3897
  });
3620
3898
  } catch {
3621
3899
  }
3622
- const globalRoot = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : wstackGlobalRoot();
3623
- const newSessionsDir = path39.join(resolveProjectDir(resolved, globalRoot), "sessions");
3900
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : wstackGlobalRoot();
3901
+ const newSessionsDir = path4.join(resolveProjectDir(resolved, globalRoot), "sessions");
3624
3902
  await fsp5.mkdir(newSessionsDir, { recursive: true });
3625
3903
  const newStore = new DefaultSessionStore({ dir: newSessionsDir });
3626
3904
  opts.sessionStore = newStore;
@@ -3644,17 +3922,17 @@ async function handleProjectsSelect(ctx, ws, payload) {
3644
3922
  const switchedP = await ctx.buildSessionStart({ reset: true, clearedSessionId: oldSessionId });
3645
3923
  ctx.broadcast({ type: "session.start", payload: switchedP });
3646
3924
  } catch (err) {
3647
- sendResult6(ctx, ws, false, err instanceof Error ? err.message : String(err));
3925
+ sendResult6(ctx, ws, false, toErrorMessage(err));
3648
3926
  }
3649
3927
  }
3650
3928
  async function handleProjectsAdd(ctx, ws, payload) {
3651
3929
  const { root: addRoot, name: addName } = payload;
3652
3930
  try {
3653
- const resolved = path39.resolve(addRoot);
3931
+ const resolved = path4.resolve(addRoot);
3654
3932
  const stat7 = await fsp5.stat(resolved).catch(() => null);
3655
3933
  if (!stat7?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
3656
3934
  const manifest = await loadManifest(ctx.opts.globalConfigPath);
3657
- const existing = manifest.projects.find((p) => path39.resolve(p.root) === resolved);
3935
+ const existing = manifest.projects.find((p) => path4.resolve(p.root) === resolved);
3658
3936
  if (existing) {
3659
3937
  ctx.send(ws, {
3660
3938
  type: "projects.added",
@@ -3667,7 +3945,7 @@ async function handleProjectsAdd(ctx, ws, payload) {
3667
3945
  });
3668
3946
  return;
3669
3947
  }
3670
- const name = addName?.trim() || path39.basename(resolved);
3948
+ const name = addName?.trim() || path4.basename(resolved);
3671
3949
  const slug = projectSlug(resolved);
3672
3950
  await ensureProjectDataDir(slug, ctx.opts.globalConfigPath);
3673
3951
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -3681,10 +3959,10 @@ async function handleProjectsAdd(ctx, ws, payload) {
3681
3959
  ctx.send(ws, {
3682
3960
  type: "projects.added",
3683
3961
  payload: {
3684
- name: path39.basename(addRoot),
3962
+ name: path4.basename(addRoot),
3685
3963
  root: addRoot,
3686
3964
  slug: "",
3687
- message: err instanceof Error ? err.message : String(err)
3965
+ message: toErrorMessage(err)
3688
3966
  }
3689
3967
  });
3690
3968
  }
@@ -3692,8 +3970,8 @@ async function handleProjectsAdd(ctx, ws, payload) {
3692
3970
  async function handleWorkingDirSet(ctx, ws, newPath) {
3693
3971
  try {
3694
3972
  const wdRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
3695
- const resolved = path39.resolve(wdRoot, newPath);
3696
- if (!resolved.startsWith(wdRoot + path39.sep) && resolved !== wdRoot) {
3973
+ const resolved = path4.resolve(wdRoot, newPath);
3974
+ if (!resolved.startsWith(wdRoot + path4.sep) && resolved !== wdRoot) {
3697
3975
  sendResult6(ctx, ws, false, `Path must stay inside the project root: ${wdRoot}`);
3698
3976
  return;
3699
3977
  }
@@ -3706,7 +3984,7 @@ async function handleWorkingDirSet(ctx, ws, newPath) {
3706
3984
  ctx.broadcast({ type: "working_dir.changed", payload: { cwd: resolved, projectRoot: wdRoot } });
3707
3985
  sendResult6(ctx, ws, true, `Working directory set to ${resolved}`);
3708
3986
  } catch (err) {
3709
- sendResult6(ctx, ws, false, err instanceof Error ? err.message : String(err));
3987
+ sendResult6(ctx, ws, false, toErrorMessage(err));
3710
3988
  }
3711
3989
  }
3712
3990
  var init_projects = __esm({
@@ -3714,11 +3992,27 @@ var init_projects = __esm({
3714
3992
  init_project_utils();
3715
3993
  }
3716
3994
  });
3717
-
3718
- // src/webui-server/ws-handlers/providers.ts
3719
3995
  function sendResult7(ctx, ws, success, message) {
3720
3996
  ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
3721
3997
  }
3998
+ function broadcastSaved(ctx, providers) {
3999
+ ctx.broadcast({
4000
+ type: "providers.saved",
4001
+ payload: {
4002
+ providers: Object.entries(providers).map(([id, cfg]) => ({
4003
+ id,
4004
+ family: cfg.family,
4005
+ baseUrl: cfg.baseUrl,
4006
+ apiKeys: normalizeKeys(cfg).map((k) => ({
4007
+ label: k.label,
4008
+ maskedKey: maskedKey(k.apiKey),
4009
+ isActive: k.label === cfg.activeKey,
4010
+ createdAt: k.createdAt
4011
+ }))
4012
+ }))
4013
+ }
4014
+ });
4015
+ }
3722
4016
  async function handleProvidersList(ctx, ws) {
3723
4017
  if (!ctx.modelsRegistry) {
3724
4018
  sendResult7(ctx, ws, false, "Models registry not available");
@@ -3743,7 +4037,7 @@ async function handleProvidersList(ctx, ws) {
3743
4037
  }
3744
4038
  });
3745
4039
  } catch (err) {
3746
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4040
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3747
4041
  }
3748
4042
  }
3749
4043
  async function handleProviderModels(ctx, ws, providerId) {
@@ -3778,7 +4072,7 @@ async function handleProviderModels(ctx, ws, providerId) {
3778
4072
  }
3779
4073
  });
3780
4074
  } catch (err) {
3781
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4075
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3782
4076
  }
3783
4077
  }
3784
4078
  async function handleProvidersSaved(ctx, ws) {
@@ -3801,7 +4095,7 @@ async function handleProvidersSaved(ctx, ws) {
3801
4095
  }
3802
4096
  });
3803
4097
  } catch (err) {
3804
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4098
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3805
4099
  }
3806
4100
  }
3807
4101
  async function handleKeyUpsert(ctx, ws, providerId, label, apiKey) {
@@ -3821,7 +4115,7 @@ async function handleKeyUpsert(ctx, ws, providerId, label, apiKey) {
3821
4115
  await ctx.providerStore.save(providers);
3822
4116
  sendResult7(ctx, ws, true, `Key "${label}" saved for ${providerId}`);
3823
4117
  } catch (err) {
3824
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4118
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3825
4119
  }
3826
4120
  }
3827
4121
  async function handleKeyDelete(ctx, ws, providerId, label) {
@@ -3845,7 +4139,7 @@ async function handleKeyDelete(ctx, ws, providerId, label) {
3845
4139
  await ctx.providerStore.save(providers);
3846
4140
  sendResult7(ctx, ws, true, `Key "${label}" deleted from ${providerId}`);
3847
4141
  } catch (err) {
3848
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4142
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3849
4143
  }
3850
4144
  }
3851
4145
  async function handleKeySetActive(ctx, ws, providerId, label) {
@@ -3862,7 +4156,7 @@ async function handleKeySetActive(ctx, ws, providerId, label) {
3862
4156
  await ctx.providerStore.save(providers);
3863
4157
  sendResult7(ctx, ws, true, `Active key for ${providerId} set to "${label}"`);
3864
4158
  } catch (err) {
3865
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4159
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3866
4160
  }
3867
4161
  }
3868
4162
  async function handleProviderAdd(ctx, ws, payload) {
@@ -3907,7 +4201,7 @@ async function handleProviderAdd(ctx, ws, payload) {
3907
4201
  }
3908
4202
  });
3909
4203
  } catch (err) {
3910
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4204
+ sendResult7(ctx, ws, false, toErrorMessage(err));
3911
4205
  }
3912
4206
  }
3913
4207
  async function handleProviderRemove(ctx, ws, providerId) {
@@ -3921,12 +4215,92 @@ async function handleProviderRemove(ctx, ws, providerId) {
3921
4215
  await ctx.providerStore.save(providers);
3922
4216
  sendResult7(ctx, ws, true, `Provider "${providerId}" removed`);
3923
4217
  } catch (err) {
3924
- sendResult7(ctx, ws, false, err instanceof Error ? err.message : String(err));
4218
+ sendResult7(ctx, ws, false, toErrorMessage(err));
4219
+ }
4220
+ }
4221
+ async function handleProviderClearModels(ctx, ws, providerId) {
4222
+ try {
4223
+ const providers = await ctx.providerStore.load();
4224
+ const cfg = providers[providerId];
4225
+ if (!cfg) {
4226
+ sendResult7(ctx, ws, false, `Unknown provider "${providerId}"`);
4227
+ return;
4228
+ }
4229
+ delete cfg.models;
4230
+ await ctx.providerStore.save(providers);
4231
+ sendResult7(ctx, ws, true, `Cleared model allowlist for ${providerId}`);
4232
+ broadcastSaved(ctx, providers);
4233
+ } catch (err) {
4234
+ sendResult7(ctx, ws, false, toErrorMessage(err));
4235
+ }
4236
+ }
4237
+ async function handleProviderUndoClear(ctx, ws, providerId, previousModels) {
4238
+ try {
4239
+ const providers = await ctx.providerStore.load();
4240
+ const cfg = providers[providerId];
4241
+ if (!cfg) {
4242
+ sendResult7(ctx, ws, false, `Unknown provider "${providerId}"`);
4243
+ return;
4244
+ }
4245
+ cfg.models = [...previousModels];
4246
+ await ctx.providerStore.save(providers);
4247
+ sendResult7(ctx, ws, true, `Restored ${previousModels.length} model(s) for ${providerId}`);
4248
+ broadcastSaved(ctx, providers);
4249
+ } catch (err) {
4250
+ sendResult7(ctx, ws, false, toErrorMessage(err));
4251
+ }
4252
+ }
4253
+ async function handleProviderUpdate(ctx, ws, payload) {
4254
+ try {
4255
+ const providers = await ctx.providerStore.load();
4256
+ const cfg = providers[payload.id];
4257
+ if (!cfg) {
4258
+ sendResult7(ctx, ws, false, `Unknown provider "${payload.id}"`);
4259
+ return;
4260
+ }
4261
+ if (payload.family !== void 0) cfg.family = payload.family;
4262
+ if (payload.baseUrl !== void 0) cfg.baseUrl = payload.baseUrl;
4263
+ if (payload.envVars !== void 0) cfg.envVars = payload.envVars;
4264
+ if (payload.models !== void 0) cfg.models = payload.models;
4265
+ await ctx.providerStore.save(providers);
4266
+ sendResult7(ctx, ws, true, `Updated ${payload.id}`);
4267
+ broadcastSaved(ctx, providers);
4268
+ } catch (err) {
4269
+ sendResult7(ctx, ws, false, toErrorMessage(err));
4270
+ }
4271
+ }
4272
+ async function handleProviderProbe(ctx, ws, providerId, timeoutMs) {
4273
+ const reply = (payload) => ctx.send(ws, { type: "provider.probe", payload: { providerId, ...payload } });
4274
+ try {
4275
+ const providers = await ctx.providerStore.load();
4276
+ const cfg = providers[providerId];
4277
+ if (!cfg) {
4278
+ reply({ ok: false, status: "no_provider" });
4279
+ return;
4280
+ }
4281
+ if (!cfg.baseUrl) {
4282
+ reply({ ok: false, status: "no_base_url" });
4283
+ return;
4284
+ }
4285
+ const keys = normalizeKeys(cfg);
4286
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
4287
+ const result = await probeLocalLlm({
4288
+ baseUrl: cfg.baseUrl,
4289
+ apiKey: active?.apiKey,
4290
+ noAuth: false,
4291
+ scrubber: probeScrubber,
4292
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
4293
+ });
4294
+ reply(result);
4295
+ } catch (err) {
4296
+ reply({ ok: false, status: "unreachable", detail: toErrorMessage(err) });
3925
4297
  }
3926
4298
  }
4299
+ var probeScrubber;
3927
4300
  var init_providers = __esm({
3928
4301
  "src/webui-server/ws-handlers/providers.ts"() {
3929
4302
  init_provider_config();
4303
+ probeScrubber = new DefaultSecretScrubber();
3930
4304
  }
3931
4305
  });
3932
4306
  function sendResult8(ctx, ws, success, message) {
@@ -3934,13 +4308,13 @@ function sendResult8(ctx, ws, success, message) {
3934
4308
  }
3935
4309
  function storeFor(opts) {
3936
4310
  return opts.sessionStore ?? new DefaultSessionStore({
3937
- dir: path39.join(opts.projectRoot ?? opts.agent.ctx.projectRoot, ".wrongstack", "sessions")
4311
+ dir: path4.join(opts.projectRoot ?? opts.agent.ctx.projectRoot, ".wrongstack", "sessions")
3938
4312
  });
3939
4313
  }
3940
4314
  async function handleGoalGet(ctx, _ws) {
3941
4315
  const projectRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
3942
4316
  try {
3943
- const goalPath = path39.join(projectRoot, ".wrongstack", "goal.json");
4317
+ const goalPath = path4.join(projectRoot, ".wrongstack", "goal.json");
3944
4318
  const raw = await fsp5.readFile(goalPath, "utf8");
3945
4319
  ctx.broadcast({ type: "goal.updated", payload: JSON.parse(raw) });
3946
4320
  } catch {
@@ -3968,7 +4342,7 @@ async function handleSessionsList(ctx, ws, limit) {
3968
4342
  } catch (err) {
3969
4343
  ctx.send(ws, {
3970
4344
  type: "sessions.list",
3971
- payload: { sessions: [], error: err instanceof Error ? err.message : String(err) }
4345
+ payload: { sessions: [], error: toErrorMessage(err) }
3972
4346
  });
3973
4347
  }
3974
4348
  }
@@ -4000,7 +4374,7 @@ async function handleSessionNew(ctx, _ws) {
4000
4374
  JSON.stringify({
4001
4375
  level: "warn",
4002
4376
  event: "webui.session_new_store_failed",
4003
- message: err instanceof Error ? err.message : String(err),
4377
+ message: toErrorMessage(err),
4004
4378
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4005
4379
  })
4006
4380
  );
@@ -4016,7 +4390,7 @@ async function handleSessionNew(ctx, _ws) {
4016
4390
  function rewinderFor(opts) {
4017
4391
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot;
4018
4392
  return new DefaultSessionRewinder(
4019
- opts.sessionsDir ?? path39.join(projectRoot, ".wrongstack", "sessions"),
4393
+ opts.sessionsDir ?? path4.join(projectRoot, ".wrongstack", "sessions"),
4020
4394
  projectRoot
4021
4395
  );
4022
4396
  }
@@ -4040,7 +4414,7 @@ async function handleSessionRewind(ctx, ws, checkpointIndex) {
4040
4414
  const payload = await ctx.buildSessionStart({ reset: true });
4041
4415
  ctx.broadcast({ type: "session.start", payload });
4042
4416
  } catch (err) {
4043
- sendResult8(ctx, ws, false, err instanceof Error ? err.message : String(err));
4417
+ sendResult8(ctx, ws, false, toErrorMessage(err));
4044
4418
  }
4045
4419
  }
4046
4420
  async function handleSessionDelete(ctx, ws, id) {
@@ -4052,7 +4426,7 @@ async function handleSessionDelete(ctx, ws, id) {
4052
4426
  await storeFor(ctx.opts).delete(id);
4053
4427
  sendResult8(ctx, ws, true, `Session ${id} deleted`);
4054
4428
  } catch (err) {
4055
- sendResult8(ctx, ws, false, err instanceof Error ? err.message : String(err));
4429
+ sendResult8(ctx, ws, false, toErrorMessage(err));
4056
4430
  }
4057
4431
  }
4058
4432
  function handleSessionSave(ctx, ws) {
@@ -4095,7 +4469,7 @@ async function handleSessionResume(ctx, ws, id) {
4095
4469
  ctx.broadcast({ type: "session.start", payload });
4096
4470
  sendResult8(ctx, ws, true, `Resumed session ${id}`);
4097
4471
  } catch (err) {
4098
- sendResult8(ctx, ws, false, err instanceof Error ? err.message : String(err));
4472
+ sendResult8(ctx, ws, false, toErrorMessage(err));
4099
4473
  }
4100
4474
  }
4101
4475
  var init_sessions = __esm({
@@ -4350,14 +4724,14 @@ async function runWebUI(opts) {
4350
4724
  let customModeStoreP = null;
4351
4725
  const getCustomModeStore = () => {
4352
4726
  customModeStoreP ??= (async () => {
4353
- const dir = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : wstackGlobalRoot();
4727
+ const dir = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : wstackGlobalRoot();
4354
4728
  const store = createCustomModeStore(dir);
4355
4729
  await store.load();
4356
4730
  return store;
4357
4731
  })();
4358
4732
  return customModeStoreP;
4359
4733
  };
4360
- const autoPhaseStoreDir = opts.projectRoot ? path39.join(opts.projectRoot, ".wrongstack", "autophase") : path39.join(os.tmpdir(), ".wrongstack", "autophase");
4734
+ const autoPhaseStoreDir = opts.projectRoot ? path4.join(opts.projectRoot, ".wrongstack", "autophase") : path4.join(os.tmpdir(), ".wrongstack", "autophase");
4361
4735
  const autoPhaseHandler = new AutoPhaseWebSocketHandler(
4362
4736
  opts.agent,
4363
4737
  opts.agent.ctx,
@@ -4388,7 +4762,12 @@ async function runWebUI(opts) {
4388
4762
  "contextAutoCompact",
4389
4763
  "contextStrategy",
4390
4764
  "logLevel",
4391
- "auditLevel"
4765
+ "auditLevel",
4766
+ // Telegram plugin notification settings (parity with the standalone server).
4767
+ "tgConfigured",
4768
+ "tgSessionEnd",
4769
+ "tgDelegate",
4770
+ "tgLongToolMs"
4392
4771
  ];
4393
4772
  const prefSnapshot = () => {
4394
4773
  const snapshot = {};
@@ -4427,6 +4806,12 @@ async function runWebUI(opts) {
4427
4806
  meta["logLevel"] = cfg.log?.["level"] ?? "info";
4428
4807
  meta["auditLevel"] = cfg.session?.["auditLevel"] ?? "standard";
4429
4808
  meta["maxIterations"] = cfg.tools?.["maxIterations"] ?? 500;
4809
+ const tgExt = cfg.extensions?.["telegram"];
4810
+ meta["tgConfigured"] = typeof tgExt?.["botToken"] === "string" && tgExt["botToken"].length > 0;
4811
+ meta["tgSessionEnd"] = tgExt?.["notifyOnSessionEnd"] === true;
4812
+ meta["tgDelegate"] = tgExt?.["notifyOnDelegate"] !== false;
4813
+ const tgMs = tgExt?.["longToolThresholdMs"];
4814
+ meta["tgLongToolMs"] = typeof tgMs === "number" ? tgMs : 3e4;
4430
4815
  } catch {
4431
4816
  }
4432
4817
  }
@@ -4448,7 +4833,7 @@ async function runWebUI(opts) {
4448
4833
  return;
4449
4834
  }
4450
4835
  const vault = new DefaultSecretVault({
4451
- keyFile: path39.join(path39.dirname(configPath2), ".key")
4836
+ keyFile: path4.join(path4.dirname(configPath2), ".key")
4452
4837
  });
4453
4838
  const decrypted = decryptConfigSecrets$1(parsed, vault);
4454
4839
  const autonomyCfg = decrypted.autonomy ?? {};
@@ -4521,6 +4906,16 @@ async function runWebUI(opts) {
4521
4906
  toolsCfg.maxIterations = payload["maxIterations"];
4522
4907
  decrypted.tools = toolsCfg;
4523
4908
  }
4909
+ const tgTouched = typeof payload["tgSessionEnd"] === "boolean" || typeof payload["tgDelegate"] === "boolean" || typeof payload["tgLongToolMs"] === "number";
4910
+ if (tgTouched) {
4911
+ const ext = decrypted.extensions ?? {};
4912
+ const tg = ext["telegram"] ?? {};
4913
+ if (typeof payload["tgSessionEnd"] === "boolean") tg["notifyOnSessionEnd"] = payload["tgSessionEnd"];
4914
+ if (typeof payload["tgDelegate"] === "boolean") tg["notifyOnDelegate"] = payload["tgDelegate"];
4915
+ if (typeof payload["tgLongToolMs"] === "number") tg["longToolThresholdMs"] = payload["tgLongToolMs"];
4916
+ ext["telegram"] = tg;
4917
+ decrypted.extensions = ext;
4918
+ }
4524
4919
  const encrypted = encryptConfigSecrets$1(decrypted, vault);
4525
4920
  await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
4526
4921
  };
@@ -4542,7 +4937,6 @@ async function runWebUI(opts) {
4542
4937
  );
4543
4938
  }
4544
4939
  };
4545
- const authToken = crypto3.randomBytes(16).toString("hex");
4546
4940
  const sessionStartedAt = Date.now();
4547
4941
  async function buildSessionStartPayload(overrides, needsSetup = false) {
4548
4942
  let maxContext = 0;
@@ -4571,7 +4965,7 @@ async function runWebUI(opts) {
4571
4965
  model: opts.agent.ctx.model,
4572
4966
  provider: opts.agent.ctx.provider.id,
4573
4967
  mode: opts.modeId ?? "default",
4574
- projectName: opts.projectRoot ? path39.basename(opts.projectRoot) : void 0,
4968
+ projectName: opts.projectRoot ? path4.basename(opts.projectRoot) : void 0,
4575
4969
  // Frontend reads `projectRoot` from session.start (ws-handlers setEnv) —
4576
4970
  // omitting it left the store's projectRoot empty after a project switch.
4577
4971
  projectRoot: opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "",
@@ -4597,7 +4991,7 @@ async function runWebUI(opts) {
4597
4991
  const projectDir = resolveProjectDir(opts.projectRoot, wstackGlobalRoot());
4598
4992
  const mailbox = new GlobalMailbox(projectDir, opts.events);
4599
4993
  webuiClientId = `webui@${crypto3.randomUUID().slice(0, 8)}`;
4600
- const projectName = opts.projectRoot ? path39.basename(opts.projectRoot) : "unknown";
4994
+ const projectName = opts.projectRoot ? path4.basename(opts.projectRoot) : "unknown";
4601
4995
  await mailbox.registerClient({
4602
4996
  clientId: webuiClientId,
4603
4997
  sessionId: opts.projectRoot,
@@ -4622,13 +5016,19 @@ async function runWebUI(opts) {
4622
5016
  }
4623
5017
  };
4624
5018
  registerWebuiClient();
5019
+ const wsToken = generateAuthToken();
4625
5020
  const wss = new WebSocketServer({ port, host, maxPayload: 1 * 1024 * 1024 });
4626
5021
  console.log(`[WebUI] WebSocket server starting on ws://${host}:${port}`);
5022
+ let fleetBroadcastCli = null;
4627
5023
  const httpServer = startStaticServe({
4628
5024
  host,
4629
5025
  httpPort,
4630
5026
  wsPort,
4631
- globalRoot: path39.dirname(opts.globalConfigPath ?? "")
5027
+ globalRoot: path4.dirname(opts.globalConfigPath ?? ""),
5028
+ onFleetPing: () => {
5029
+ void fleetBroadcastCli?.();
5030
+ },
5031
+ apiToken: wsToken
4632
5032
  });
4633
5033
  if (httpServer) {
4634
5034
  announceWebuiReady({
@@ -4636,14 +5036,15 @@ async function runWebUI(opts) {
4636
5036
  host,
4637
5037
  httpPort,
4638
5038
  wsPort,
4639
- open: !!opts.open
5039
+ open: !!opts.open,
5040
+ wsToken
4640
5041
  });
4641
5042
  } else {
4642
5043
  console.warn(
4643
5044
  `[WebUI] Frontend not served (run \`pnpm --filter @wrongstack/webui build\`). WS bridge still active on ws://${host}:${wsPort}.`
4644
5045
  );
4645
5046
  }
4646
- const registryBaseDir = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : void 0;
5047
+ const registryBaseDir = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : void 0;
4647
5048
  if (opts.projectRoot) {
4648
5049
  registerWebuiInstance({
4649
5050
  pid: process.pid,
@@ -4678,9 +5079,13 @@ async function runWebUI(opts) {
4678
5079
  emitConcurrency();
4679
5080
  eventUnsubscribers.push(
4680
5081
  opts.events.on("iteration.started", (e) => {
5082
+ const maxIt = opts.agent.ctx.meta["maxIterations"];
4681
5083
  broadcast({
4682
5084
  type: "iteration.started",
4683
- payload: { index: e.index }
5085
+ payload: {
5086
+ index: e.index,
5087
+ ...typeof maxIt === "number" ? { maxIterations: maxIt } : {}
5088
+ }
4684
5089
  });
4685
5090
  })
4686
5091
  );
@@ -4941,6 +5346,18 @@ async function runWebUI(opts) {
4941
5346
  broadcast,
4942
5347
  log: (m) => console.log(m)
4943
5348
  };
5349
+ const skillsProjectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
5350
+ const skillsCtx = {
5351
+ skillLoader: opts.skillLoader,
5352
+ skillInstaller: opts.skillLoader ? new SkillInstaller({
5353
+ manifestPath: path4.join(wstackGlobalRoot(), "installed-skills.json"),
5354
+ projectSkillsDir: path4.join(skillsProjectRoot, ".wrongstack", "skills"),
5355
+ globalSkillsDir: path4.join(wstackGlobalRoot(), "skills"),
5356
+ projectHash: skillsProjectRoot ? projectHash(skillsProjectRoot) : "",
5357
+ skillLoader: opts.skillLoader
5358
+ }) : void 0,
5359
+ projectRoot: skillsProjectRoot
5360
+ };
4944
5361
  const worklistCtx = {
4945
5362
  agent: opts.agent,
4946
5363
  sessionId: opts.session.id,
@@ -5005,20 +5422,22 @@ async function runWebUI(opts) {
5005
5422
  console.log(`[WebUI] WebSocket server running on ws://${host}:${port}`);
5006
5423
  setupEvents();
5007
5424
  opts.onListening?.({ httpPort, wsPort, host });
5008
- const globalRoot = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : void 0;
5425
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : void 0;
5009
5426
  if (globalRoot) {
5010
- const statusInterval = setInterval(async () => {
5427
+ const broadcastSessions = async () => {
5011
5428
  try {
5012
5429
  const { SessionRegistry } = await import('@wrongstack/core');
5013
5430
  const registry = new SessionRegistry(globalRoot);
5014
5431
  const sessions = await registry.list();
5015
- const live = sessions.filter((s) => s.status !== "stale").map((s) => ({
5432
+ const mySlug = sessions.find((s) => s.pid === process.pid)?.projectSlug;
5433
+ const live = sessions.filter((s) => s.status !== "stale").filter((s) => mySlug ? s.projectSlug === mySlug : true).map((s) => ({
5016
5434
  sessionId: s.sessionId,
5017
5435
  projectName: s.projectName,
5018
5436
  projectSlug: s.projectSlug,
5019
5437
  projectRoot: s.projectRoot,
5020
5438
  workingDir: s.workingDir,
5021
5439
  gitBranch: s.gitBranch,
5440
+ clientType: s.clientType,
5022
5441
  status: s.status,
5023
5442
  pid: s.pid,
5024
5443
  startedAt: s.startedAt,
@@ -5030,60 +5449,52 @@ async function runWebUI(opts) {
5030
5449
  currentTool: a.currentTool,
5031
5450
  iterations: a.iterations,
5032
5451
  toolCalls: a.toolCalls,
5452
+ costUsd: a.costUsd,
5453
+ tokensIn: a.tokensIn,
5454
+ tokensOut: a.tokensOut,
5455
+ ctxPct: a.ctxPct,
5456
+ model: a.model,
5457
+ partialText: a.partialText,
5033
5458
  lastActivityAt: a.lastActivityAt
5034
5459
  }))
5035
5460
  }));
5036
5461
  broadcast({ type: "sessions.status_update", payload: { sessions: live } });
5037
5462
  } catch {
5038
5463
  }
5039
- }, 5e3);
5464
+ };
5465
+ fleetBroadcastCli = broadcastSessions;
5466
+ const statusInterval = setInterval(() => void broadcastSessions(), 5e3);
5040
5467
  if (statusInterval.unref) statusInterval.unref();
5041
5468
  eventUnsubscribers.push(() => clearInterval(statusInterval));
5042
- }
5043
- });
5044
- wss.on("connection", async (ws, req2) => {
5045
- const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
5046
- const tokenMatches = (provided) => {
5047
- if (!provided) return false;
5048
- const a = Buffer.from(provided);
5049
- const b = Buffer.from(authToken);
5050
- return a.length === b.length && crypto3.timingSafeEqual(a, b);
5051
- };
5052
- try {
5053
- const url = new URL(req2.url ?? "/", `http://localhost:${port}`);
5054
- const token = url.searchParams.get("token");
5055
- const tokenOk = tokenMatches(token);
5056
- const hostHeader = (req2.headers.host ?? "").trim();
5057
- let hostOk = false;
5469
+ let regDebounce;
5058
5470
  try {
5059
- hostOk = !!hostHeader && isLoopback(new URL(`http://${hostHeader}`).hostname);
5471
+ const regWatcher = watch(globalRoot, { persistent: false }, (_event, filename) => {
5472
+ const name = filename ? String(filename) : "";
5473
+ if (!name.startsWith("session-registry.json") || name.endsWith(".lock")) return;
5474
+ if (regDebounce) clearTimeout(regDebounce);
5475
+ regDebounce = setTimeout(() => void broadcastSessions(), 150);
5476
+ });
5477
+ eventUnsubscribers.push(() => {
5478
+ if (regDebounce) clearTimeout(regDebounce);
5479
+ regWatcher.close();
5480
+ });
5060
5481
  } catch {
5061
- hostOk = false;
5062
5482
  }
5063
- if (!hostOk) {
5064
- ws.close(4003, "Forbidden: non-loopback Host header");
5065
- return;
5066
- }
5067
- const origin = req2.headers.origin;
5068
- if (origin) {
5069
- try {
5070
- const { hostname } = new URL(origin);
5071
- if (!isLoopback(hostname) && !tokenOk) {
5072
- ws.close(4003, "Forbidden: non-loopback origin requires auth token");
5073
- return;
5074
- }
5075
- } catch {
5076
- ws.close(4003, "Forbidden: invalid origin");
5077
- return;
5078
- }
5079
- } else {
5080
- if (!tokenOk) {
5081
- ws.close(4003, "Forbidden: auth token required for non-browser clients");
5082
- return;
5083
- }
5084
- }
5085
- } catch {
5086
- ws.close(4001, "Unauthorized: malformed request");
5483
+ void broadcastSessions();
5484
+ }
5485
+ });
5486
+ wss.on("connection", async (ws, req2) => {
5487
+ const ok = verifyClient({
5488
+ origin: req2.headers.origin,
5489
+ url: req2.url ?? "/",
5490
+ hostHeader: req2.headers.host,
5491
+ remoteAddress: req2.socket.remoteAddress,
5492
+ cookieHeader: req2.headers.cookie,
5493
+ wsHost: host,
5494
+ expectedToken: wsToken
5495
+ });
5496
+ if (!ok) {
5497
+ ws.close(4003, "Forbidden");
5087
5498
  return;
5088
5499
  }
5089
5500
  const client = { ws, sessionId: opts.session.id };
@@ -5227,6 +5638,31 @@ async function runWebUI(opts) {
5227
5638
  await handleProviderRemove(wsHandlerCtx, ws, m.payload.providerId);
5228
5639
  break;
5229
5640
  }
5641
+ case "provider.clear_models": {
5642
+ const m = msg;
5643
+ await handleProviderClearModels(wsHandlerCtx, ws, m.payload.providerId);
5644
+ break;
5645
+ }
5646
+ case "provider.undo_clear": {
5647
+ const m = msg;
5648
+ await handleProviderUndoClear(
5649
+ wsHandlerCtx,
5650
+ ws,
5651
+ m.payload.providerId,
5652
+ m.payload.previousModels
5653
+ );
5654
+ break;
5655
+ }
5656
+ case "provider.update": {
5657
+ const m = msg;
5658
+ await handleProviderUpdate(wsHandlerCtx, ws, m.payload);
5659
+ break;
5660
+ }
5661
+ case "provider.probe": {
5662
+ const m = msg;
5663
+ await handleProviderProbe(wsHandlerCtx, ws, m.payload.providerId, m.payload.timeoutMs);
5664
+ break;
5665
+ }
5230
5666
  case "todos.get": {
5231
5667
  handleTodosGet(worklistCtx, ws);
5232
5668
  break;
@@ -5383,10 +5819,84 @@ async function runWebUI(opts) {
5383
5819
  }
5384
5820
  return handleMemoryForget(ws, msg, opts.memoryStore);
5385
5821
  }
5822
+ // ── MCP operations — MCP servers are read directly from config file ──
5823
+ case "mcp.list": {
5824
+ const servers = [];
5825
+ if (opts.globalConfigPath) {
5826
+ try {
5827
+ const raw = await fsp5.readFile(opts.globalConfigPath, "utf8");
5828
+ const cfg = JSON.parse(raw);
5829
+ const mcpServers = cfg.mcpServers;
5830
+ if (mcpServers) {
5831
+ for (const [name, serverCfg] of Object.entries(mcpServers)) {
5832
+ servers.push({
5833
+ name,
5834
+ transport: serverCfg.transport ?? "stdio",
5835
+ status: "stopped",
5836
+ enabled: serverCfg.enabled ?? true,
5837
+ // Conditional spreads keep these absent (not explicitly
5838
+ // `undefined`) to satisfy exactOptionalPropertyTypes.
5839
+ ...serverCfg.description !== void 0 && { description: serverCfg.description },
5840
+ ...serverCfg.allowedTools !== void 0 && { tools: serverCfg.allowedTools }
5841
+ });
5842
+ }
5843
+ }
5844
+ } catch {
5845
+ }
5846
+ }
5847
+ send(ws, { type: "mcp.list", payload: { servers } });
5848
+ break;
5849
+ }
5850
+ case "mcp.add":
5851
+ case "mcp.remove":
5852
+ case "mcp.update":
5853
+ case "mcp.wake":
5854
+ case "mcp.sleep":
5855
+ case "mcp.discover":
5856
+ case "mcp.enable":
5857
+ case "mcp.disable":
5858
+ case "mcp.restart": {
5859
+ send(ws, {
5860
+ type: "mcp.operation_result",
5861
+ payload: {
5862
+ success: false,
5863
+ message: 'MCP management operations require the standalone WebUI server. Please run "wrongstack webui" instead of "wrongstack --webui".'
5864
+ }
5865
+ });
5866
+ break;
5867
+ }
5386
5868
  case "skills.list": {
5387
5869
  await handleSkillsList(introspectionCtx, ws);
5388
5870
  break;
5389
5871
  }
5872
+ case "skills.content": {
5873
+ await handleSkillsContent(ws, skillsCtx, msg);
5874
+ break;
5875
+ }
5876
+ case "skills.install": {
5877
+ await handleSkillsInstall(ws, skillsCtx, msg);
5878
+ break;
5879
+ }
5880
+ case "skills.uninstall": {
5881
+ await handleSkillsUninstall(ws, skillsCtx, msg);
5882
+ break;
5883
+ }
5884
+ case "skills.update": {
5885
+ await handleSkillsUpdate(ws, skillsCtx, msg);
5886
+ break;
5887
+ }
5888
+ case "skills.create": {
5889
+ await handleSkillsCreate(ws, skillsCtx, msg);
5890
+ break;
5891
+ }
5892
+ case "skills.edit": {
5893
+ await handleSkillsEdit(ws, skillsCtx, msg);
5894
+ break;
5895
+ }
5896
+ case "skills.export": {
5897
+ await handleSkillsExport(ws, skillsCtx);
5898
+ break;
5899
+ }
5390
5900
  case "modes.list": {
5391
5901
  await handleModesList(agentConfigCtx, ws);
5392
5902
  break;
@@ -5501,11 +6011,16 @@ async function runWebUI(opts) {
5501
6011
  break;
5502
6012
  }
5503
6013
  // Collaboration messages — the CLI webui-server doesn't run a
5504
- // full collab hub; silently acknowledge and ignore.
6014
+ // full collab hub; silently acknowledge and ignore. request_pause /
6015
+ // resume are included so the CollabPanel's pause/resume buttons don't
6016
+ // trip the "Unhandled message type" warning (the standalone webui
6017
+ // server is the one that wires the real CollaborationWebSocketHandler).
5505
6018
  case "collab.join":
5506
6019
  case "collab.leave":
5507
6020
  case "collab.annotate":
5508
6021
  case "collab.resolve":
6022
+ case "collab.request_pause":
6023
+ case "collab.resume":
5509
6024
  break;
5510
6025
  case "projects.list": {
5511
6026
  await handleProjectsList(projectsCtx, ws);
@@ -5554,7 +6069,7 @@ async function runWebUI(opts) {
5554
6069
  // ── Mailbox operations — project-level inter-agent messaging ────
5555
6070
  case "mailbox.messages": {
5556
6071
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
5557
- const globalRoot = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : "";
6072
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
5558
6073
  if (!projectRoot || !globalRoot) {
5559
6074
  send(ws, {
5560
6075
  type: "mailbox.messages",
@@ -5603,7 +6118,7 @@ async function runWebUI(opts) {
5603
6118
  }
5604
6119
  case "mailbox.agents": {
5605
6120
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
5606
- const globalRoot = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : "";
6121
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
5607
6122
  if (!projectRoot || !globalRoot) {
5608
6123
  send(ws, {
5609
6124
  type: "mailbox.agents",
@@ -5646,7 +6161,7 @@ async function runWebUI(opts) {
5646
6161
  }
5647
6162
  case "mailbox.clear": {
5648
6163
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
5649
- const globalRoot = opts.globalConfigPath ? path39.dirname(opts.globalConfigPath) : "";
6164
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
5650
6165
  if (!projectRoot || !globalRoot) {
5651
6166
  send(ws, { type: "mailbox.cleared", payload: { error: "No project root available" } });
5652
6167
  break;
@@ -5664,6 +6179,32 @@ async function runWebUI(opts) {
5664
6179
  }
5665
6180
  break;
5666
6181
  }
6182
+ case "mailbox.purge": {
6183
+ const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6184
+ const globalRoot = opts.globalConfigPath ? path4.dirname(opts.globalConfigPath) : "";
6185
+ if (!projectRoot || !globalRoot) {
6186
+ send(ws, { type: "mailbox.purged", payload: { error: "No project root available" } });
6187
+ break;
6188
+ }
6189
+ try {
6190
+ const mbDir = resolveProjectDir(projectRoot, globalRoot);
6191
+ const mb = new GlobalMailbox(mbDir);
6192
+ const payload = msg;
6193
+ const result = await mb.purgeStale(payload.payload);
6194
+ send(ws, { type: "mailbox.purged", payload: result });
6195
+ } catch (err) {
6196
+ send(ws, {
6197
+ type: "mailbox.purged",
6198
+ payload: { error: err instanceof Error ? err.message : String(err) }
6199
+ });
6200
+ }
6201
+ break;
6202
+ }
6203
+ case "git.info": {
6204
+ const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
6205
+ await handleGitInfo(ws, projectRoot);
6206
+ break;
6207
+ }
5667
6208
  default: {
5668
6209
  const msgType = msg.type;
5669
6210
  if (msgType.startsWith("autophase.")) {
@@ -6276,7 +6817,7 @@ var ReadlineInputReader = class {
6276
6817
  history = [];
6277
6818
  pending = false;
6278
6819
  constructor(opts = {}) {
6279
- this.historyFile = opts.historyFile ?? path39.join(wstackGlobalRoot(), "history");
6820
+ this.historyFile = opts.historyFile ?? path4.join(wstackGlobalRoot(), "history");
6280
6821
  }
6281
6822
  async loadHistory() {
6282
6823
  try {
@@ -6288,7 +6829,7 @@ var ReadlineInputReader = class {
6288
6829
  }
6289
6830
  async saveHistory() {
6290
6831
  try {
6291
- await fsp5.mkdir(path39.dirname(this.historyFile), { recursive: true });
6832
+ await fsp5.mkdir(path4.dirname(this.historyFile), { recursive: true });
6292
6833
  await fsp5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
6293
6834
  } catch {
6294
6835
  }
@@ -6620,7 +7161,7 @@ function pickGroupIndex(opts) {
6620
7161
  if (Number.isFinite(parsed)) current = wrap(parsed);
6621
7162
  } catch {
6622
7163
  }
6623
- fs2.mkdirSync(path39.dirname(opts.cursorFile), { recursive: true });
7164
+ fs2.mkdirSync(path4.dirname(opts.cursorFile), { recursive: true });
6624
7165
  fs2.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
6625
7166
  return current;
6626
7167
  } catch {
@@ -6673,11 +7214,11 @@ function assertSafeToDelete(filename, parentDir) {
6673
7214
  throw new FsError({
6674
7215
  message: `Refusing to delete protected file: ${filename}`,
6675
7216
  code: ERROR_CODES.FS_DELETE_FAILED,
6676
- path: path39.join(parentDir, filename),
7217
+ path: path4.join(parentDir, filename),
6677
7218
  context: { reason: "protected_basename" }
6678
7219
  });
6679
7220
  }
6680
- if (filename !== path39.basename(filename)) {
7221
+ if (filename !== path4.basename(filename)) {
6681
7222
  throw new FsError({
6682
7223
  message: `Refusing to delete path with traversal: ${filename}`,
6683
7224
  code: ERROR_CODES.FS_DELETE_FAILED,
@@ -6689,11 +7230,11 @@ function assertSafeToDelete(filename, parentDir) {
6689
7230
  throw new FsError({
6690
7231
  message: `Refusing to delete unknown file: ${filename}`,
6691
7232
  code: ERROR_CODES.FS_DELETE_FAILED,
6692
- path: path39.join(parentDir, filename),
7233
+ path: path4.join(parentDir, filename),
6693
7234
  context: { reason: "unknown_file_pattern" }
6694
7235
  });
6695
7236
  }
6696
- const resolvedParent = path39.resolve(parentDir);
7237
+ const resolvedParent = path4.resolve(parentDir);
6697
7238
  if (!resolvedParent.endsWith(".wrongstack")) {
6698
7239
  throw new FsError({
6699
7240
  message: `Unexpected parent directory for bak prune: ${resolvedParent}`,
@@ -6704,8 +7245,8 @@ function assertSafeToDelete(filename, parentDir) {
6704
7245
  }
6705
7246
  }
6706
7247
  async function safeDelete(filePath) {
6707
- const dir = path39.dirname(filePath);
6708
- const filename = path39.basename(filePath);
7248
+ const dir = path4.dirname(filePath);
7249
+ const filename = path4.basename(filePath);
6709
7250
  try {
6710
7251
  assertSafeToDelete(filename, dir);
6711
7252
  await fsp5.unlink(filePath);
@@ -6750,16 +7291,16 @@ function diffSummary(oldCfg, newCfg) {
6750
7291
  }
6751
7292
  var defaultHomeDir = () => os__default.homedir();
6752
7293
  function historyDir(homeFn = defaultHomeDir) {
6753
- return path39.join(homeFn(), ".wrongstack", "config.history", "entries");
7294
+ return path4.join(homeFn(), ".wrongstack", "config.history", "entries");
6754
7295
  }
6755
7296
  function historyIndexPath(homeFn = defaultHomeDir) {
6756
- return path39.join(homeFn(), ".wrongstack", "config.history", "index.json");
7297
+ return path4.join(homeFn(), ".wrongstack", "config.history", "index.json");
6757
7298
  }
6758
7299
  function configPath(homeFn = defaultHomeDir) {
6759
- return path39.join(homeFn(), ".wrongstack", "config.json");
7300
+ return path4.join(homeFn(), ".wrongstack", "config.json");
6760
7301
  }
6761
7302
  function backupLastPath(homeFn = defaultHomeDir) {
6762
- return path39.join(homeFn(), ".wrongstack", "config.json.last");
7303
+ return path4.join(homeFn(), ".wrongstack", "config.json.last");
6763
7304
  }
6764
7305
  function entryId(ts) {
6765
7306
  return ts.replace(/[:.]/g, "-").slice(0, 19);
@@ -6769,7 +7310,7 @@ async function ensureHistoryDir(homeFn = defaultHomeDir) {
6769
7310
  await fsp5.mkdir(historyDir(homeFn), { recursive: true });
6770
7311
  } catch (err) {
6771
7312
  throw new FsError({
6772
- message: err instanceof Error ? err.message : String(err),
7313
+ message: toErrorMessage(err),
6773
7314
  code: ERROR_CODES.FS_MKDIR_FAILED,
6774
7315
  path: historyDir(homeFn),
6775
7316
  cause: err
@@ -6790,7 +7331,7 @@ async function writeIndex(idx, homeFn = defaultHomeDir) {
6790
7331
  await atomicWrite(historyIndexPath(homeFn), JSON.stringify(idx, null, 2));
6791
7332
  } catch (err) {
6792
7333
  throw new FsError({
6793
- message: err instanceof Error ? err.message : String(err),
7334
+ message: toErrorMessage(err),
6794
7335
  code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
6795
7336
  path: historyIndexPath(homeFn),
6796
7337
  cause: err
@@ -6811,30 +7352,30 @@ async function backupCurrent(homeFn = defaultHomeDir) {
6811
7352
  await atomicWrite(last, content);
6812
7353
  } catch (err) {
6813
7354
  writeErr(
6814
- `[config-history] .last backup failed: ${err instanceof Error ? err.message : String(err)}`
7355
+ `[config-history] .last backup failed: ${toErrorMessage(err)}`
6815
7356
  );
6816
7357
  }
6817
7358
  }
6818
7359
  if (content !== void 0) {
6819
7360
  try {
6820
- const bakPath = path39.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
7361
+ const bakPath = path4.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
6821
7362
  await atomicWrite(bakPath, content);
6822
7363
  } catch (err) {
6823
7364
  writeErr(
6824
- `[config-history] timestamped backup failed: ${err instanceof Error ? err.message : String(err)}`
7365
+ `[config-history] timestamped backup failed: ${toErrorMessage(err)}`
6825
7366
  );
6826
7367
  }
6827
7368
  }
6828
7369
  try {
6829
- const dir = path39.join(homeFn(), ".wrongstack");
7370
+ const dir = path4.join(homeFn(), ".wrongstack");
6830
7371
  const files = await fsp5.readdir(dir);
6831
7372
  const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
6832
7373
  for (const f of baks.slice(10)) {
6833
- await safeDelete(path39.join(dir, f));
7374
+ await safeDelete(path4.join(dir, f));
6834
7375
  }
6835
7376
  } catch (err) {
6836
7377
  writeErr(
6837
- `[config-history] backup prune failed: ${err instanceof Error ? err.message : String(err)}`
7378
+ `[config-history] backup prune failed: ${toErrorMessage(err)}`
6838
7379
  );
6839
7380
  }
6840
7381
  }
@@ -6851,15 +7392,15 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
6851
7392
  };
6852
7393
  try {
6853
7394
  await fsp5.writeFile(
6854
- path39.join(historyDir(homeFn), `${id}.json`),
7395
+ path4.join(historyDir(homeFn), `${id}.json`),
6855
7396
  JSON.stringify(entry, null, 2),
6856
7397
  "utf8"
6857
7398
  );
6858
7399
  } catch (err) {
6859
7400
  throw new FsError({
6860
- message: err instanceof Error ? err.message : String(err),
7401
+ message: toErrorMessage(err),
6861
7402
  code: ERROR_CODES.FS_WRITE_FAILED,
6862
- path: path39.join(historyDir(homeFn), `${id}.json`),
7403
+ path: path4.join(historyDir(homeFn), `${id}.json`),
6863
7404
  cause: err
6864
7405
  });
6865
7406
  }
@@ -6874,7 +7415,7 @@ async function listHistory(homeFn = defaultHomeDir) {
6874
7415
  }
6875
7416
  async function getHistoryEntry(id, homeFn = defaultHomeDir) {
6876
7417
  try {
6877
- const raw = await fsp5.readFile(path39.join(historyDir(homeFn), `${id}.json`), "utf8");
7418
+ const raw = await fsp5.readFile(path4.join(historyDir(homeFn), `${id}.json`), "utf8");
6878
7419
  return JSON.parse(raw);
6879
7420
  } catch {
6880
7421
  return null;
@@ -6985,16 +7526,14 @@ async function buildPickableProviders(modelsRegistry, config) {
6985
7526
  }
6986
7527
  return out;
6987
7528
  }
6988
-
6989
- // src/picker.ts
6990
7529
  var theme = { primary: color.amber };
6991
7530
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os__default.homedir()) {
6992
7531
  try {
6993
- const { atomicWrite: atomicWrite17 } = await import('@wrongstack/core');
6994
- const fs39 = await import('fs/promises');
7532
+ const { atomicWrite: atomicWrite16 } = await import('@wrongstack/core');
7533
+ const fs38 = await import('fs/promises');
6995
7534
  let existing = {};
6996
7535
  try {
6997
- const raw = await fs39.readFile(configPath2, "utf8");
7536
+ const raw = await fs38.readFile(configPath2, "utf8");
6998
7537
  existing = JSON.parse(raw);
6999
7538
  } catch {
7000
7539
  }
@@ -7008,12 +7547,12 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
7008
7547
  JSON.stringify({
7009
7548
  level: "warn",
7010
7549
  event: "picker.backup_failed",
7011
- message: err instanceof Error ? err.message : String(err),
7550
+ message: toErrorMessage(err),
7012
7551
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7013
7552
  })
7014
7553
  );
7015
7554
  }
7016
- await atomicWrite17(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7555
+ await atomicWrite16(configPath2, JSON.stringify(existing, null, 2), { mode: 384 });
7017
7556
  try {
7018
7557
  await appendHistory(
7019
7558
  oldCfg,
@@ -7029,7 +7568,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
7029
7568
  JSON.stringify({
7030
7569
  level: "warn",
7031
7570
  event: "picker.save_failed",
7032
- message: err instanceof Error ? err.message : String(err),
7571
+ message: toErrorMessage(err),
7033
7572
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7034
7573
  })
7035
7574
  );
@@ -7335,7 +7874,8 @@ function buildAuthCommand(opts) {
7335
7874
  {
7336
7875
  encrypt: (v) => v,
7337
7876
  decrypt: (v) => v,
7338
- isEncrypted: () => false
7877
+ isEncrypted: () => false,
7878
+ keyVersion: 1
7339
7879
  }
7340
7880
  );
7341
7881
  } catch {
@@ -7676,7 +8216,7 @@ function formatPhaseList(graph) {
7676
8216
  }
7677
8217
  async function gatherProjectContext(projectRoot) {
7678
8218
  try {
7679
- const raw = await fsp5.readFile(path39.join(projectRoot, "package.json"), "utf8");
8219
+ const raw = await fsp5.readFile(path4.join(projectRoot, "package.json"), "utf8");
7680
8220
  const pkg = JSON.parse(raw);
7681
8221
  const parts = [
7682
8222
  `Project: ${String(pkg.name ?? "unknown")}`,
@@ -8027,7 +8567,7 @@ function buildCodebaseReindexCommand(opts) {
8027
8567
  ${color.yellow(` ${r.errors.length} file(s) had errors`)}` : "");
8028
8568
  return { message: summary };
8029
8569
  } catch (err) {
8030
- const msg = `${color.red("Codebase reindex failed:")} ${err instanceof Error ? err.message : String(err)}`;
8570
+ const msg = `${color.red("Codebase reindex failed:")} ${toErrorMessage(err)}`;
8031
8571
  return { message: msg };
8032
8572
  }
8033
8573
  }
@@ -8114,7 +8654,7 @@ async function historyCommand(opts, sessionId, args) {
8114
8654
  } catch (err) {
8115
8655
  return {
8116
8656
  message: color.yellow(
8117
- `Failed to read session: ${err instanceof Error ? err.message : String(err)}`
8657
+ `Failed to read session: ${toErrorMessage(err)}`
8118
8658
  )
8119
8659
  };
8120
8660
  }
@@ -8616,7 +9156,7 @@ async function spawnAgent(opts, role, _task, _name, header) {
8616
9156
  opts.renderer.write(msg);
8617
9157
  return { message: msg };
8618
9158
  } catch (err) {
8619
- const msg = `${color.red("\u2717 Spawn failed")}: ${err instanceof Error ? err.message : String(err)}`;
9159
+ const msg = `${color.red("\u2717 Spawn failed")}: ${toErrorMessage(err)}`;
8620
9160
  opts.renderer.writeWarning(msg);
8621
9161
  return { message: msg };
8622
9162
  }
@@ -9102,17 +9642,17 @@ function diagnoseConfig(cfg, plugins = []) {
9102
9642
  function scanPlaintextSecrets(node, prefix, findings) {
9103
9643
  if (!isPlainObject(node)) return;
9104
9644
  for (const [key, value] of Object.entries(node)) {
9105
- const path40 = prefix ? `${prefix}.${key}` : key;
9645
+ const path39 = prefix ? `${prefix}.${key}` : key;
9106
9646
  if (typeof value === "string") {
9107
9647
  if (value.length > 0 && isSecretField$1(key) && !value.startsWith(ENC_PREFIX)) {
9108
9648
  findings.push({
9109
- path: path40,
9649
+ path: path39,
9110
9650
  problem: "looks like a plaintext secret (not vault-encrypted) \u2014 it will be encrypted on next boot",
9111
9651
  severity: "warning"
9112
9652
  });
9113
9653
  }
9114
9654
  } else if (isPlainObject(value)) {
9115
- scanPlaintextSecrets(value, path40, findings);
9655
+ scanPlaintextSecrets(value, path39, findings);
9116
9656
  }
9117
9657
  }
9118
9658
  }
@@ -9133,7 +9673,7 @@ function resolvePersistPath(deps) {
9133
9673
  return deps.globalConfigPath;
9134
9674
  }
9135
9675
  async function ensureProjectDir(filePath) {
9136
- const dir = path39.dirname(filePath);
9676
+ const dir = path4.dirname(filePath);
9137
9677
  try {
9138
9678
  await fsp5.mkdir(dir, { recursive: true });
9139
9679
  } catch {
@@ -9157,7 +9697,8 @@ var PROJECT_SAFE_FIELDS = /* @__PURE__ */ new Set([
9157
9697
  "session",
9158
9698
  "indexing",
9159
9699
  "tools",
9160
- "launch"
9700
+ "launch",
9701
+ "circuitBreaker"
9161
9702
  ]);
9162
9703
  function filterSafeForProject(cfg) {
9163
9704
  const out = {};
@@ -9337,8 +9878,8 @@ function buildDoctorCommand(opts) {
9337
9878
  return ` ${icon} ${color.cyan(f.path)} \u2014 ${f.problem}${fix}`;
9338
9879
  }
9339
9880
  async function findParsableBackup(file) {
9340
- const dir = path39.dirname(file);
9341
- const base = path39.basename(file);
9881
+ const dir = path4.dirname(file);
9882
+ const base = path4.basename(file);
9342
9883
  const candidates = [`${base}.last`];
9343
9884
  try {
9344
9885
  const siblings = await fsp5.readdir(dir);
@@ -9349,7 +9890,7 @@ function buildDoctorCommand(opts) {
9349
9890
  }
9350
9891
  for (const name of candidates) {
9351
9892
  try {
9352
- const raw = await fsp5.readFile(path39.join(dir, name), "utf8");
9893
+ const raw = await fsp5.readFile(path4.join(dir, name), "utf8");
9353
9894
  JSON.parse(raw);
9354
9895
  return { name, raw };
9355
9896
  } catch {
@@ -9415,7 +9956,7 @@ function buildDoctorCommand(opts) {
9415
9956
  parsed = JSON.parse(raw);
9416
9957
  } catch (err) {
9417
9958
  errorCount++;
9418
- const msg = err instanceof Error ? err.message : String(err);
9959
+ const msg = toErrorMessage(err);
9419
9960
  lines.push(` ${color.red("\u2717")} invalid JSON \u2014 ${msg}`);
9420
9961
  if (!applyFixes) {
9421
9962
  fixableCount++;
@@ -9466,7 +10007,7 @@ function buildDoctorCommand(opts) {
9466
10007
  await atomicWrite(target.file, JSON.stringify(report.fixed, null, 2));
9467
10008
  if (!target.isProject) {
9468
10009
  try {
9469
- const homeFn = () => path39.dirname(path39.dirname(target.file));
10010
+ const homeFn = () => path4.dirname(path4.dirname(target.file));
9470
10011
  await appendHistory(parsed, report.fixed, "config doctor auto-fix", homeFn);
9471
10012
  } catch {
9472
10013
  }
@@ -9554,6 +10095,65 @@ function buildEnhanceCommand(opts) {
9554
10095
  }
9555
10096
  };
9556
10097
  }
10098
+ function buildEnsembleCommand(_opts) {
10099
+ return {
10100
+ name: "ensemble",
10101
+ category: "Agent",
10102
+ description: "Fan a task out to multiple ACP agents in parallel (claude-code, gemini-cli, codex-cli, etc.).",
10103
+ argsHint: "<agent-ids-csv> <task description>",
10104
+ help: [
10105
+ "Fan a single task out to multiple ACP-supporting agents in parallel.",
10106
+ "",
10107
+ "Usage:",
10108
+ " /ensemble <agent-ids-csv> <task description>",
10109
+ "",
10110
+ "Examples:",
10111
+ ' /ensemble claude-code,gemini-cli "review this diff"',
10112
+ ' /ensemble claude-code,codex-cli "refactor auth/session.ts"',
10113
+ ' /ensemble claude-code,gemini-cli,codex-cli "explain the v1 protocol"',
10114
+ "",
10115
+ "Each agent runs in its own process. Agents not installed on the host",
10116
+ "are skipped with a warning. The command waits for all agents to finish",
10117
+ "and reports per-agent outcomes (success / failed / skipped / cancelled).",
10118
+ "",
10119
+ "Use /acp list (or wstack acp list) to see which agents are detected."
10120
+ ].join("\n"),
10121
+ async run(args) {
10122
+ const trimmed = args.trim();
10123
+ if (!trimmed) {
10124
+ return {
10125
+ message: 'Usage: /ensemble <agent-ids-csv> <task description>\n\nExamples:\n /ensemble claude-code,gemini-cli "review this diff"\n /ensemble claude-code,codex-cli "refactor auth/session.ts"\n\nRun `wstack acp list` to see which agents are detected on this host.'
10126
+ };
10127
+ }
10128
+ const spaceIdx = trimmed.search(/\s/);
10129
+ if (spaceIdx === -1) {
10130
+ return {
10131
+ message: 'Task description is required.\n\nUsage: /ensemble <agent-ids-csv> <task description>\nExample: /ensemble claude-code,gemini-cli "explain this code"'
10132
+ };
10133
+ }
10134
+ const agentIds = trimmed.slice(0, spaceIdx);
10135
+ const task = stripSurroundingQuotes(trimmed.slice(spaceIdx + 1).trim());
10136
+ if (!task) {
10137
+ return { message: "Task description is required." };
10138
+ }
10139
+ try {
10140
+ const result = await runEnsemble({ agentIds, task });
10141
+ return { message: renderEnsembleText(result) };
10142
+ } catch (err) {
10143
+ return { message: `Ensemble failed: ${toErrorMessage(err)}` };
10144
+ }
10145
+ }
10146
+ };
10147
+ }
10148
+ function stripSurroundingQuotes(s) {
10149
+ if (s.length < 2) return s;
10150
+ const first = s[0];
10151
+ const last = s[s.length - 1];
10152
+ if ((first === '"' || first === "'") && first === last) {
10153
+ return s.slice(1, -1);
10154
+ }
10155
+ return s;
10156
+ }
9557
10157
  function parseModelRef(ref) {
9558
10158
  const trimmed = ref.trim();
9559
10159
  const slash = trimmed.indexOf("/");
@@ -9854,7 +10454,7 @@ function buildFallbackCommand(opts) {
9854
10454
  };
9855
10455
  } catch (err) {
9856
10456
  return {
9857
- message: `${color.red("fallback error")}: ${err instanceof Error ? err.message : String(err)}`
10457
+ message: `${color.red("fallback error")}: ${toErrorMessage(err)}`
9858
10458
  };
9859
10459
  }
9860
10460
  }
@@ -11071,7 +11671,7 @@ async function handleSpawn(opts, subargs) {
11071
11671
  const id = await opts.onFleetSpawn(role);
11072
11672
  spawned.push(id);
11073
11673
  } catch (err) {
11074
- const warnMsg = `${color.red("\u2717 Spawn failed")} for slot ${i + 1}: ${err instanceof Error ? err.message : String(err)}`;
11674
+ const warnMsg = `${color.red("\u2717 Spawn failed")} for slot ${i + 1}: ${toErrorMessage(err)}`;
11075
11675
  opts.renderer.writeWarning(warnMsg);
11076
11676
  }
11077
11677
  }
@@ -11125,7 +11725,7 @@ async function handleDispatch(opts, subargs) {
11125
11725
  lines.push(` ${color.green("\u2713 spawned")} ${color.bold(decision.role)} as ${color.dim(id)}`);
11126
11726
  } catch (err) {
11127
11727
  lines.push(
11128
- ` ${color.amber("\u26A0 spawn failed:")} ${err instanceof Error ? err.message : String(err)}`
11728
+ ` ${color.amber("\u26A0 spawn failed:")} ${toErrorMessage(err)}`
11129
11729
  );
11130
11730
  }
11131
11731
  } else {
@@ -11587,8 +12187,8 @@ function buildInitCommand(opts) {
11587
12187
  description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
11588
12188
  async run(_args, ctx) {
11589
12189
  const root = ctx?.projectRoot ?? opts.projectRoot ?? process.cwd();
11590
- const dir = path39.join(root, ".wrongstack");
11591
- const file = path39.join(dir, "AGENTS.md");
12190
+ const dir = path4.join(root, ".wrongstack");
12191
+ const file = path4.join(dir, "AGENTS.md");
11592
12192
  const isFirstInit = !await fileExists(file);
11593
12193
  const detected = await detectProjectFacts(root);
11594
12194
  const body = renderAgentsTemplate(detected);
@@ -11596,7 +12196,7 @@ function buildInitCommand(opts) {
11596
12196
  await fsp5.writeFile(file, body, "utf8");
11597
12197
  let nodePkg = false;
11598
12198
  try {
11599
- await fsp5.access(path39.join(root, "package.json"));
12199
+ await fsp5.access(path4.join(root, "package.json"));
11600
12200
  nodePkg = true;
11601
12201
  } catch {
11602
12202
  }
@@ -11758,6 +12358,7 @@ function buildMailboxCommand(opts) {
11758
12358
  " /mailbox broadcast <message> Message every agent on the project.",
11759
12359
  " /mailbox history [n] Last n messages on the project (default 20).",
11760
12360
  " /mailbox clear Delete all messages from the mailbox.",
12361
+ " /mailbox purge Remove stale/orphaned messages (completed >1d, incomplete >7d).",
11761
12362
  "",
11762
12363
  "Examples:",
11763
12364
  " /mailbox broadcast pausing deploys, hold off on main",
@@ -11866,8 +12467,17 @@ function buildMailboxCommand(opts) {
11866
12467
  await mb.clearAll();
11867
12468
  return { message: color.green("\u2713 All messages deleted from the mailbox.") };
11868
12469
  }
12470
+ if (sub === "purge") {
12471
+ const result = await mb.purgeStale();
12472
+ if (result.totalPurged === 0) {
12473
+ return { message: color.green("\u2713 No stale messages found. Mailbox is clean.") };
12474
+ }
12475
+ return {
12476
+ message: `\u2713 Purged ${result.totalPurged} message(s): ${result.completedPurged} completed, ${result.incompletePurged} incomplete. ${result.remaining} message(s) remain.`
12477
+ };
12478
+ }
11869
12479
  return {
11870
- message: `Unknown subcommand "${sub}". Use: /mailbox [agents|online|send|broadcast|history|clear]`
12480
+ message: `Unknown subcommand "${sub}". Use: /mailbox [agents|online|send|broadcast|history|clear purge]`
11871
12481
  };
11872
12482
  }
11873
12483
  };
@@ -12280,9 +12890,9 @@ function stateBadge(state) {
12280
12890
  return color.dim(state);
12281
12891
  }
12282
12892
  }
12283
- async function readConfig(path40) {
12893
+ async function readConfig(path39) {
12284
12894
  try {
12285
- return JSON.parse(await fsp5.readFile(path40, "utf8"));
12895
+ return JSON.parse(await fsp5.readFile(path39, "utf8"));
12286
12896
  } catch {
12287
12897
  return {};
12288
12898
  }
@@ -12290,11 +12900,11 @@ async function readConfig(path40) {
12290
12900
  function isMcpServerRecord(value) {
12291
12901
  return !!value && typeof value === "object" && !Array.isArray(value);
12292
12902
  }
12293
- async function writeConfig(path40, cfg) {
12903
+ async function writeConfig(path39, cfg) {
12294
12904
  const raw = JSON.stringify(cfg, null, 2);
12295
- const tmp = path40 + ".tmp";
12905
+ const tmp = path39 + ".tmp";
12296
12906
  await fsp5.writeFile(tmp, raw, "utf8");
12297
- await fsp5.rename(tmp, path40);
12907
+ await fsp5.rename(tmp, path39);
12298
12908
  }
12299
12909
 
12300
12910
  // src/slash-commands/mcp.ts
@@ -12484,7 +13094,7 @@ async function runCompact(opts) {
12484
13094
  responseText = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
12485
13095
  } catch (err) {
12486
13096
  return {
12487
- message: `LLM call failed: ${err instanceof Error ? err.message : String(err)}`
13097
+ message: `LLM call failed: ${toErrorMessage(err)}`
12488
13098
  };
12489
13099
  }
12490
13100
  if (!responseText) {
@@ -12500,7 +13110,7 @@ ${responseText.slice(0, 500)}` };
12500
13110
  parsed = JSON.parse(jsonMatch[0]);
12501
13111
  } catch (err) {
12502
13112
  return {
12503
- message: `Failed to parse LLM response: ${err instanceof Error ? err.message : String(err)}
13113
+ message: `Failed to parse LLM response: ${toErrorMessage(err)}
12504
13114
 
12505
13115
  Raw response:
12506
13116
  ${responseText.slice(0, 500)}`
@@ -12558,7 +13168,7 @@ ${responseText.slice(0, 500)}`
12558
13168
  }
12559
13169
  } catch (err) {
12560
13170
  errors.push(
12561
- `${op.action} failed for ${op.targets.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
13171
+ `${op.action} failed for ${op.targets.join(", ")}: ${toErrorMessage(err)}`
12562
13172
  );
12563
13173
  }
12564
13174
  }
@@ -13195,7 +13805,7 @@ function buildModelsCommand(opts) {
13195
13805
  };
13196
13806
  } catch (err) {
13197
13807
  return {
13198
- message: `${color.red("models error")}: ${err instanceof Error ? err.message : String(err)}`
13808
+ message: `${color.red("models error")}: ${toErrorMessage(err)}`
13199
13809
  };
13200
13810
  }
13201
13811
  }
@@ -13879,7 +14489,7 @@ async function killSession(sessionId) {
13879
14489
  } catch (err) {
13880
14490
  return {
13881
14491
  message: color.red(
13882
- `Failed to kill session: ${err instanceof Error ? err.message : String(err)}`
14492
+ `Failed to kill session: ${toErrorMessage(err)}`
13883
14493
  )
13884
14494
  };
13885
14495
  }
@@ -14250,7 +14860,7 @@ function buildSetModelCommand(opts) {
14250
14860
  };
14251
14861
  } catch (err) {
14252
14862
  return {
14253
- message: `${color.red("setmodel error")}: ${err instanceof Error ? err.message : String(err)}`
14863
+ message: `${color.red("setmodel error")}: ${toErrorMessage(err)}`
14254
14864
  };
14255
14865
  }
14256
14866
  }
@@ -14353,7 +14963,7 @@ function buildSuggestCommand(opts) {
14353
14963
  opts.onSuggestions?.(suggestions);
14354
14964
  return { message: formatSuggestions(suggestions) };
14355
14965
  } catch (err) {
14356
- const msg = `Suggestion generation failed: ${err instanceof Error ? err.message : String(err)}`;
14966
+ const msg = `Suggestion generation failed: ${toErrorMessage(err)}`;
14357
14967
  opts.renderer.writeWarning(msg);
14358
14968
  return { message: msg };
14359
14969
  }
@@ -14413,6 +15023,73 @@ function formatSuggestions(suggestions) {
14413
15023
  return lines.join("\n");
14414
15024
  }
14415
15025
 
15026
+ // src/slash-commands/coordinator.ts
15027
+ function buildCoordinatorCommand(opts) {
15028
+ return {
15029
+ name: "coordinator",
15030
+ category: "Agent",
15031
+ description: "Start, stop, or inspect the AutonomousCoordinator \u2014 the fleet brain that auctions tasks and consults Brain for risky decisions.",
15032
+ help: [
15033
+ "Usage:",
15034
+ " /coordinator start <goal> Start the coordinator with a goal",
15035
+ " /coordinator stop Stop the running coordinator",
15036
+ " /coordinator status Show current coordinator status",
15037
+ "",
15038
+ "The AutonomousCoordinator runs alongside the agent loop and:",
15039
+ " \u2022 Maintains a shared knowledge graph of facts and decisions",
15040
+ " \u2022 Breaks work into a task DAG and auctions tasks to subagents",
15041
+ " \u2022 Consults the Brain for risky decisions",
15042
+ " \u2022 Uses ConsensusProtocol to vote on multi-agent changes",
15043
+ "",
15044
+ "It is separate from /autonomy eternal \u2014 both can run concurrently."
15045
+ ].join("\n"),
15046
+ async run(args) {
15047
+ const trimmed = args.trim();
15048
+ const [verbRaw, ...rest] = trimmed.split(/\s+/);
15049
+ const verb = (verbRaw ?? "").toLowerCase();
15050
+ if (verb === "start") {
15051
+ const goal = rest.join(" ").trim();
15052
+ if (!goal) {
15053
+ return { message: "Usage: /coordinator start <goal>\nA goal is required to start the coordinator." };
15054
+ }
15055
+ opts.onCoordinatorStart?.(goal);
15056
+ return {
15057
+ message: `AutonomousCoordinator started with goal: "${goal}"
15058
+ Use /coordinator status to monitor progress.`
15059
+ };
15060
+ }
15061
+ if (verb === "stop") {
15062
+ opts.onCoordinatorStop?.();
15063
+ return { message: "AutonomousCoordinator stop signal sent." };
15064
+ }
15065
+ if (verb === "status") {
15066
+ const canStart = opts.onCoordinatorStart != null;
15067
+ const canStop = opts.onCoordinatorStop != null;
15068
+ return {
15069
+ message: [
15070
+ `Coordinator wired: start=${canStart ? "yes" : "no"}, stop=${canStop ? "yes" : "no"}`,
15071
+ "Use F9 in the TUI for the full coordinator panel."
15072
+ ].join("\n")
15073
+ };
15074
+ }
15075
+ return {
15076
+ message: [
15077
+ "Usage:",
15078
+ " /coordinator start <goal> Start with a goal",
15079
+ " /coordinator stop Stop the coordinator",
15080
+ " /coordinator status Show status",
15081
+ "",
15082
+ "The coordinator is a fleet brain that:",
15083
+ " \u2022 Auctions tasks to subagents via TaskAuctioneer",
15084
+ " \u2022 Maintains a shared KnowledgeGraph",
15085
+ " \u2022 Consults Brain for risky decisions",
15086
+ " \u2022 Uses ConsensusProtocol for multi-agent votes"
15087
+ ].join("\n")
15088
+ };
15089
+ }
15090
+ };
15091
+ }
15092
+
14416
15093
  // src/slash-commands/mouse.ts
14417
15094
  function buildMouseCommand(_opts) {
14418
15095
  return {
@@ -14593,7 +15270,7 @@ async function listProjectsCommand(opts, ctx) {
14593
15270
  return { message: lines.join("\n") };
14594
15271
  }
14595
15272
  async function addProjectCommand(opts, ctx, targetPath, displayName) {
14596
- const resolved = path39.resolve(ctx?.projectRoot ?? ctx?.cwd ?? process.cwd(), targetPath);
15273
+ const resolved = path4.resolve(ctx?.projectRoot ?? ctx?.cwd ?? process.cwd(), targetPath);
14597
15274
  try {
14598
15275
  await fsp5.access(resolved);
14599
15276
  } catch {
@@ -14610,7 +15287,7 @@ async function addProjectCommand(opts, ctx, targetPath, displayName) {
14610
15287
  message: color.yellow(`Project already registered: "${existing.name}" (${existing.slug})`)
14611
15288
  };
14612
15289
  }
14613
- const name = displayName?.trim() || path39.basename(resolved);
15290
+ const name = displayName?.trim() || path4.basename(resolved);
14614
15291
  const slug = generateSlug(resolved);
14615
15292
  const now = (/* @__PURE__ */ new Date()).toISOString();
14616
15293
  await ensureProjectDataDir(slug, opts.paths?.globalConfig);
@@ -14663,7 +15340,7 @@ async function removeProjectCommand(opts, _ctx, slugOrName) {
14663
15340
  };
14664
15341
  }
14665
15342
  async function switchProjectCommand(opts, ctx, target, displayName) {
14666
- const resolved = path39.resolve(ctx?.projectRoot ?? ctx?.cwd ?? process.cwd(), target);
15343
+ const resolved = path4.resolve(ctx?.projectRoot ?? ctx?.cwd ?? process.cwd(), target);
14667
15344
  try {
14668
15345
  await fsp5.access(resolved);
14669
15346
  } catch {
@@ -14677,8 +15354,8 @@ async function switchProjectCommand(opts, ctx, target, displayName) {
14677
15354
  try {
14678
15355
  const req2 = createRequire(import.meta.url);
14679
15356
  const pkgPath = req2.resolve("@wrongstack/cli/package.json");
14680
- const pkgDir = path39.dirname(pkgPath);
14681
- cliPath = path39.join(pkgDir, "dist", "index.js");
15357
+ const pkgDir = path4.dirname(pkgPath);
15358
+ cliPath = path4.join(pkgDir, "dist", "index.js");
14682
15359
  await fsp5.access(cliPath);
14683
15360
  } catch {
14684
15361
  cliPath = process.argv[1] ?? "";
@@ -14695,13 +15372,13 @@ async function switchProjectCommand(opts, ctx, target, displayName) {
14695
15372
  if (existing) {
14696
15373
  existing.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
14697
15374
  } else {
14698
- const name = displayName?.trim() || path39.basename(resolved);
15375
+ const name = displayName?.trim() || path4.basename(resolved);
14699
15376
  const slug = generateSlug(resolved);
14700
15377
  manifest.projects.push({ name, root: resolved, slug, lastSeen: (/* @__PURE__ */ new Date()).toISOString() });
14701
15378
  await ensureProjectDataDir(slug, opts.paths?.globalConfig);
14702
15379
  }
14703
15380
  await saveManifest(manifest, opts.paths?.globalConfig);
14704
- const targetName = displayName?.trim() || path39.basename(resolved);
15381
+ const targetName = displayName?.trim() || path4.basename(resolved);
14705
15382
  const canSwitch = await confirmProjectSwitch(opts, targetName);
14706
15383
  if (!canSwitch) return { message: "" };
14707
15384
  const nodeExe = process.execPath;
@@ -14817,8 +15494,8 @@ async function spawnInProject(opts, _ctx, root, projectName) {
14817
15494
  try {
14818
15495
  const req2 = createRequire(import.meta.url);
14819
15496
  const pkgPath = req2.resolve("@wrongstack/cli/package.json");
14820
- const pkgDir = path39.dirname(pkgPath);
14821
- cliPath = path39.join(pkgDir, "dist", "index.js");
15497
+ const pkgDir = path4.dirname(pkgPath);
15498
+ cliPath = path4.join(pkgDir, "dist", "index.js");
14822
15499
  await fsp5.access(cliPath);
14823
15500
  } catch {
14824
15501
  cliPath = process.argv[1] ?? "";
@@ -14835,7 +15512,7 @@ async function spawnInProject(opts, _ctx, root, projectName) {
14835
15512
  if (existing) {
14836
15513
  existing.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
14837
15514
  } else {
14838
- const name = projectName || path39.basename(root);
15515
+ const name = projectName || path4.basename(root);
14839
15516
  const slug = generateSlug(root);
14840
15517
  manifest.projects.push({ name, root, slug, lastSeen: (/* @__PURE__ */ new Date()).toISOString() });
14841
15518
  await ensureProjectDataDir(slug, opts.paths?.globalConfig);
@@ -14867,8 +15544,8 @@ async function handleNewSession(_opts, _ctx) {
14867
15544
  try {
14868
15545
  const req2 = createRequire(import.meta.url);
14869
15546
  const pkgPath = req2.resolve("@wrongstack/cli/package.json");
14870
- const pkgDir = path39.dirname(pkgPath);
14871
- cliPath = path39.join(pkgDir, "dist", "index.js");
15547
+ const pkgDir = path4.dirname(pkgPath);
15548
+ cliPath = path4.join(pkgDir, "dist", "index.js");
14872
15549
  await fsp5.access(cliPath);
14873
15550
  } catch {
14874
15551
  cliPath = process.argv[1] ?? "";
@@ -14979,7 +15656,7 @@ function buildReviewCommand(opts) {
14979
15656
  for (const f of allChanged) {
14980
15657
  if (f.path.startsWith(".wrongstack/")) continue;
14981
15658
  try {
14982
- await fsp5.access(path39.join(cwd, f.path));
15659
+ await fsp5.access(path4.join(cwd, f.path));
14983
15660
  existing.push(f);
14984
15661
  } catch {
14985
15662
  }
@@ -14990,7 +15667,7 @@ function buildReviewCommand(opts) {
14990
15667
  const filesWithContent = [];
14991
15668
  for (const f of existing.slice(0, 30)) {
14992
15669
  try {
14993
- const content = await fsp5.readFile(path39.join(cwd, f.path), "utf8");
15670
+ const content = await fsp5.readFile(path4.join(cwd, f.path), "utf8");
14994
15671
  filesWithContent.push({ ...f, content });
14995
15672
  } catch {
14996
15673
  }
@@ -15024,10 +15701,13 @@ function buildSettingsCommand(opts) {
15024
15701
  " /settings hints on|off Show or suppress rotating launch hints",
15025
15702
  " /settings debug-stream on|off Raw SSE hex-dump to stderr for debugging",
15026
15703
  " /settings config-scope global|project Save settings globally or per-project",
15704
+ " /settings fs-access unrestricted|project File-tool access scope (project = confine to project root)",
15027
15705
  " /settings refine on|off Enable/disable prompt refinement",
15028
15706
  " /settings refine-delay <seconds> Countdown duration for refine preview",
15029
15707
  " /settings refine-language original|english Default language for refinement",
15030
15708
  " /settings semver-part patch|minor|major|auto Default part for /semver and the semver_bump tool",
15709
+ " /settings breaker on|off Enable/disable the process circuit breaker (gates bash/exec)",
15710
+ " /settings breaker-timeout <seconds> Auto kill/reset delay when the breaker trips (0 = manual)",
15031
15711
  " /settings defaults Show built-in default values",
15032
15712
  "",
15033
15713
  "Settings are persisted to ~/.wrongstack/config.json."
@@ -15039,10 +15719,14 @@ function buildSettingsCommand(opts) {
15039
15719
  const hints = opts.configStore.get().hints !== false;
15040
15720
  const debugStream = opts.configStore.get().debugStream === true;
15041
15721
  const configScope = opts.configStore.get().configScope ?? "global";
15722
+ const fsAccess = opts.configStore.get().tools?.restrictToProjectRoot === true ? "project" : "unrestricted";
15042
15723
  const enhanceEnabled = autonomy?.enhance ?? true;
15043
15724
  const enhanceDelay = autonomy?.enhanceDelayMs ?? 6e4;
15044
15725
  const enhanceLanguage = autonomy?.enhanceLanguage ?? "original";
15045
15726
  const semverPart = opts.configStore.get().extensions?.["semver-bump"]?.["defaultPart"] ?? "patch";
15727
+ const cb = opts.configStore.get().circuitBreaker;
15728
+ const breakerEnabled = cb?.enabled === true;
15729
+ const breakerTimeout = cb?.autoKillResetMs ?? 6e4;
15046
15730
  return [
15047
15731
  `${color.bold("WrongStack")} ${color.dim("\u2014 Settings")}`,
15048
15732
  "",
@@ -15051,10 +15735,12 @@ function buildSettingsCommand(opts) {
15051
15735
  ` launch hints: ${hints ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings hints on|off")}`,
15052
15736
  ` debug stream: ${debugStream ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings debug-stream on|off")}`,
15053
15737
  ` config scope: ${color.cyan(configScope)} ${color.dim("change: /settings config-scope global|project")}`,
15738
+ ` filesystem access: ${color.cyan(fsAccess)} ${color.dim("change: /settings fs-access unrestricted|project")}`,
15054
15739
  ` refine: ${enhanceEnabled ? color.cyan("on") : color.dim("off")} ${color.dim("change: /settings refine on|off")}`,
15055
15740
  ` refine-delay: ${color.cyan(formatDelay(enhanceDelay))} ${color.dim("change: /settings refine-delay <seconds>")}`,
15056
15741
  ` refine-language: ${color.cyan(enhanceLanguage)} ${color.dim("change: /settings refine-language original|english")}`,
15057
15742
  ` semver default part: ${color.cyan(semverPart)} ${color.dim("change: /settings semver-part patch|minor|major|auto")}`,
15743
+ ` circuit breaker: ${breakerEnabled ? color.cyan("on") : color.dim("off")} (kill/reset ${breakerTimeout > 0 ? formatDelay(breakerTimeout) : color.dim("manual")}) ${color.dim("change: /settings breaker on|off")}`,
15058
15744
  "",
15059
15745
  color.dim(" Persisted to ~/.wrongstack/config.json \xB7 /settings help for more")
15060
15746
  ].join("\n");
@@ -15167,6 +15853,25 @@ function buildSettingsCommand(opts) {
15167
15853
  const label = raw === "project" ? `${color.cyan("project")} \u2014 settings saved to <project>/.wrongstack/config.json` : `${color.cyan("global")} \u2014 settings saved to ~/.wrongstack/config.json`;
15168
15854
  return { message: `${color.green("\u2713")} config scope \u2192 ${label}` };
15169
15855
  }
15856
+ if (sub === "fs-access") {
15857
+ const raw = (rest[0] ?? "").toLowerCase();
15858
+ if (!["unrestricted", "project"].includes(raw)) {
15859
+ return { message: `${color.amber("Usage:")} /settings fs-access unrestricted|project` };
15860
+ }
15861
+ const restrict = raw === "project";
15862
+ await persistConfigSetting(persistDeps, (cfg) => {
15863
+ const tools = cfg.tools ?? {};
15864
+ tools.restrictToProjectRoot = restrict;
15865
+ cfg.tools = tools;
15866
+ const features = cfg.features ?? {};
15867
+ features.allowOutsideProjectRoot = !restrict;
15868
+ cfg.features = features;
15869
+ });
15870
+ const label = restrict ? `${color.cyan("project")} \u2014 file tools confined to the project root` : `${color.cyan("unrestricted")} \u2014 file tools may access paths outside the project root`;
15871
+ return {
15872
+ message: `${color.green("\u2713")} filesystem access \u2192 ${label} ${color.dim("(restart or re-open the session to apply)")}`
15873
+ };
15874
+ }
15170
15875
  if (sub === "refine") {
15171
15876
  const raw = (rest[0] ?? "").toLowerCase();
15172
15877
  if (!["on", "off"].includes(raw)) {
@@ -15227,19 +15932,55 @@ function buildSettingsCommand(opts) {
15227
15932
  message: `${color.green("\u2713")} semver default part \u2192 ${color.bold(raw)} ${color.dim("saved to global config; used when /semver or semver_bump gets no explicit part")}`
15228
15933
  };
15229
15934
  }
15935
+ if (sub === "breaker") {
15936
+ const raw = (rest[0] ?? "").toLowerCase();
15937
+ if (!["on", "off"].includes(raw)) {
15938
+ return { message: `${color.amber("Usage:")} /settings breaker on|off` };
15939
+ }
15940
+ const on = raw === "on";
15941
+ await persistConfigSetting(persistDeps, (cfg) => {
15942
+ const cb = cfg.circuitBreaker;
15943
+ cfg.circuitBreaker = { ...cb ?? {}, enabled: on };
15944
+ });
15945
+ getProcessRegistry().setBreakerConfig({ enabled: on });
15946
+ return {
15947
+ message: `${color.green("\u2713")} circuit breaker \u2192 ${on ? color.cyan("on") : color.dim("off")} ${color.dim(on ? "bash/exec gated on repeated failures; trips arm the kill/reset countdown" : "bash/exec always proceed")}`
15948
+ };
15949
+ }
15950
+ if (sub === "breaker-timeout") {
15951
+ const raw = rest[0];
15952
+ if (raw === void 0) {
15953
+ return {
15954
+ message: `${color.amber("Usage:")} /settings breaker-timeout <seconds> ${color.dim("(0 = manual recovery only)")}`
15955
+ };
15956
+ }
15957
+ const seconds = Number.parseFloat(raw);
15958
+ if (Number.isNaN(seconds) || seconds < 0) {
15959
+ return {
15960
+ message: `${color.red("Invalid number")}: "${raw}". Enter seconds, e.g. /settings breaker-timeout 60`
15961
+ };
15962
+ }
15963
+ const ms = Math.round(seconds * 1e3);
15964
+ await persistConfigSetting(persistDeps, (cfg) => {
15965
+ const cb = cfg.circuitBreaker;
15966
+ cfg.circuitBreaker = { ...cb ?? {}, autoKillResetMs: ms };
15967
+ });
15968
+ getProcessRegistry().setBreakerConfig({ autoKillResetMs: ms });
15969
+ return {
15970
+ message: `${color.green("\u2713")} breaker kill/reset timeout \u2192 ${ms > 0 ? formatDelay(ms) : color.dim("manual")} ${color.dim(ms > 0 ? "statusline shows a countdown when the breaker trips" : "breaker trips require /kill reset")}`
15971
+ };
15972
+ }
15230
15973
  return {
15231
- message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "refine", "refine-delay", "refine-language", "semver-part", "defaults"], "settings")}`
15974
+ message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["delay", "mode", "hints", "debug-stream", "config-scope", "fs-access", "refine", "refine-delay", "refine-language", "semver-part", "breaker", "breaker-timeout", "defaults"], "settings")}`
15232
15975
  };
15233
15976
  } catch (err) {
15234
15977
  return {
15235
- message: `${color.red("Settings error")}: ${err instanceof Error ? err.message : String(err)}`
15978
+ message: `${color.red("Settings error")}: ${toErrorMessage(err)}`
15236
15979
  };
15237
15980
  }
15238
15981
  }
15239
15982
  };
15240
15983
  }
15241
-
15242
- // src/slash-commands/spawn-agents.ts
15243
15984
  function buildSpawnCommand(opts) {
15244
15985
  return {
15245
15986
  name: "spawn",
@@ -15276,7 +16017,7 @@ function buildSpawnCommand(opts) {
15276
16017
  const summary = Object.keys(parsed).length > 0 ? await opts.onSpawn(description, parsed) : await opts.onSpawn(description);
15277
16018
  return { message: summary };
15278
16019
  } catch (err) {
15279
- return { message: `Spawn failed: ${err instanceof Error ? err.message : String(err)}` };
16020
+ return { message: `Spawn failed: ${toErrorMessage(err)}` };
15280
16021
  }
15281
16022
  }
15282
16023
  };
@@ -15363,7 +16104,7 @@ var DEFAULTS = {
15363
16104
  working_dir: true
15364
16105
  };
15365
16106
  function resolveConfigPath() {
15366
- return process.env[CONFIG_ENV] ?? path39.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
16107
+ return process.env[CONFIG_ENV] ?? path4.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
15367
16108
  }
15368
16109
  async function loadStatuslineConfig() {
15369
16110
  const p = resolveConfigPath();
@@ -15377,11 +16118,11 @@ async function loadStatuslineConfig() {
15377
16118
  async function saveStatuslineConfig(cfg) {
15378
16119
  const p = resolveConfigPath();
15379
16120
  try {
15380
- await fsp5.mkdir(path39.dirname(p), { recursive: true });
16121
+ await fsp5.mkdir(path4.dirname(p), { recursive: true });
15381
16122
  await atomicWrite(p, JSON.stringify(cfg, null, 2));
15382
16123
  } catch (err) {
15383
16124
  throw new FsError({
15384
- message: err instanceof Error ? err.message : String(err),
16125
+ message: toErrorMessage(err),
15385
16126
  code: err instanceof Error && err.message.includes("mkdir") ? ERROR_CODES.FS_MKDIR_FAILED : ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
15386
16127
  path: p,
15387
16128
  cause: err
@@ -15542,7 +16283,7 @@ function buildTasksCommand(_opts) {
15542
16283
  return {
15543
16284
  name: "tasks",
15544
16285
  category: "Inspect",
15545
- description: "Manage structured tasks with dependencies, types, and priorities: /tasks [show|add <title>|start|done|fail|status <id> <status>|promote <id>|planify <id>|clear]",
16286
+ description: 'Manage structured tasks with dependencies, types, and priorities: /tasks [show|add <title>|start|done|fail|status <id> <status>|promote <id>|planify <id>|clear]. Tasks are session-persistent (survive resume, isolated to this session). Use `scope: "project"` to share across sessions. For simpler per-turn todos use /todos. For multi-phase roadmaps use /plan.',
15546
16287
  help: [
15547
16288
  "Usage:",
15548
16289
  " /tasks Show task progress + list",
@@ -15559,7 +16300,8 @@ function buildTasksCommand(_opts) {
15559
16300
  " /tasks clear Remove all tasks",
15560
16301
  "",
15561
16302
  "Types: feature, bugfix, refactor, docs, test, chore",
15562
- "Priorities: critical, high, medium, low"
16303
+ "Priorities: critical, high, medium, low",
16304
+ 'Scope: tasks are session-isolated by default; use scope:"project" in the task tool to share across sessions'
15563
16305
  ].join("\n"),
15564
16306
  async run(args, ctx) {
15565
16307
  if (!ctx) {
@@ -15611,12 +16353,26 @@ ${formatPlan(updated)}`;
15611
16353
  return file;
15612
16354
  }
15613
16355
  const parts = restJoined.split(/\s+/);
15614
- const title = parts[0] ?? "";
15615
- const type = validateType(parts[1] ?? "") ?? "feature";
15616
- const priority = validatePriority(parts[2] ?? "") ?? "medium";
16356
+ let type = "feature";
16357
+ let priority = "medium";
16358
+ if (parts.length > 1) {
16359
+ const p = validatePriority(parts[parts.length - 1] ?? "");
16360
+ if (p) {
16361
+ priority = p;
16362
+ parts.pop();
16363
+ if (parts.length > 1) {
16364
+ const t = validateType(parts[parts.length - 1] ?? "");
16365
+ if (t) {
16366
+ type = t;
16367
+ parts.pop();
16368
+ }
16369
+ }
16370
+ }
16371
+ }
16372
+ const title = parts.join(" ");
15617
16373
  const now = (/* @__PURE__ */ new Date()).toISOString();
15618
16374
  file.tasks.push({
15619
- id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
16375
+ id: `task_${randomUUID()}`,
15620
16376
  title,
15621
16377
  type,
15622
16378
  priority,
@@ -15739,7 +16495,7 @@ ${formatTaskProgress(file.tasks)}`;
15739
16495
  ];
15740
16496
  if (found.item.description) {
15741
16497
  todos.push({
15742
- id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
16498
+ id: `todo_${randomUUID()}`,
15743
16499
  content: found.item.description.slice(0, 200),
15744
16500
  status: "pending",
15745
16501
  promotedFromTask: found.item.id
@@ -15778,7 +16534,7 @@ ${formatTaskProgress(file.tasks)}`;
15778
16534
  "clear"
15779
16535
  ],
15780
16536
  "tasks"
15781
- );
16537
+ ) + "\n\nRelated: /plan (session-persistent roadmap) | /todos (per-turn list)";
15782
16538
  return file;
15783
16539
  }
15784
16540
  return file;
@@ -15789,13 +16545,13 @@ ${formatTaskProgress(file.tasks)}`;
15789
16545
  }
15790
16546
  async function discoverPackageFiles(projectRoot) {
15791
16547
  const files = [];
15792
- const rootPkg = path39.join(projectRoot, "package.json");
16548
+ const rootPkg = path4.join(projectRoot, "package.json");
15793
16549
  try {
15794
16550
  await fsp5.access(rootPkg);
15795
16551
  files.push(rootPkg);
15796
16552
  } catch {
15797
16553
  }
15798
- const workspaceFile = path39.join(projectRoot, "pnpm-workspace.yaml");
16554
+ const workspaceFile = path4.join(projectRoot, "pnpm-workspace.yaml");
15799
16555
  try {
15800
16556
  await fsp5.access(workspaceFile);
15801
16557
  const content = await fsp5.readFile(workspaceFile, "utf8");
@@ -15805,12 +16561,12 @@ async function discoverPackageFiles(projectRoot) {
15805
16561
  const globs = rawGlobs.split(/[\s,]+/).filter(Boolean).map((g) => g.replace(/['"]/g, ""));
15806
16562
  for (const g of globs) {
15807
16563
  const dirPrefix = g.replace(/\/?\*$/, "").replace(/\/\*$/, "");
15808
- const dir = path39.join(projectRoot, dirPrefix);
16564
+ const dir = path4.join(projectRoot, dirPrefix);
15809
16565
  try {
15810
16566
  const entries = await fsp5.readdir(dir, { withFileTypes: true });
15811
16567
  for (const e of entries) {
15812
16568
  if (!e.isDirectory()) continue;
15813
- const subPkg = path39.join(dir, e.name, "package.json");
16569
+ const subPkg = path4.join(dir, e.name, "package.json");
15814
16570
  try {
15815
16571
  await fsp5.access(subPkg);
15816
16572
  files.push(subPkg);
@@ -15825,7 +16581,7 @@ async function discoverPackageFiles(projectRoot) {
15825
16581
  return files;
15826
16582
  }
15827
16583
  function buildTechStackTask(opts) {
15828
- const pkgList = opts.packageFiles.map((f) => ` - ${path39.relative(opts.projectRoot, f)}`).join("\n");
16584
+ const pkgList = opts.packageFiles.map((f) => ` - ${path4.relative(opts.projectRoot, f)}`).join("\n");
15829
16585
  const header = opts.isInit ? [
15830
16586
  "## Tech Stack Audit \u2014 First-Time Project Init",
15831
16587
  "",
@@ -15947,7 +16703,7 @@ function buildTechStackCommand(opts) {
15947
16703
  }
15948
16704
  } catch (err) {
15949
16705
  discoveryNote = color.red(
15950
- `Could not scan for package files: ${err instanceof Error ? err.message : String(err)}`
16706
+ `Could not scan for package files: ${toErrorMessage(err)}`
15951
16707
  );
15952
16708
  }
15953
16709
  const task = buildTechStackTask({
@@ -15975,7 +16731,7 @@ function buildTechStackCommand(opts) {
15975
16731
  const summary = await opts.onSpawnAndWait(task, { name });
15976
16732
  return { message: summary };
15977
16733
  } catch (err) {
15978
- const msg = `Tech stack scan failed: ${err instanceof Error ? err.message : String(err)}`;
16734
+ const msg = `Tech stack scan failed: ${toErrorMessage(err)}`;
15979
16735
  opts.renderer.writeWarning(msg);
15980
16736
  return { message: msg };
15981
16737
  }
@@ -16088,39 +16844,196 @@ function buildTelegramSetupCommand(opts) {
16088
16844
  };
16089
16845
  }
16090
16846
  try {
16091
- await persistTelegramConfig(persistDeps, (telegram) => {
16092
- telegram.botToken = botToken;
16093
- if (chatId) {
16094
- telegram.notifyChatId = /^\d+$/.test(chatId) ? Number(chatId) : chatId;
16847
+ await persistTelegramConfig(persistDeps, (telegram) => {
16848
+ telegram.botToken = botToken;
16849
+ if (chatId) {
16850
+ telegram.notifyChatId = /^\d+$/.test(chatId) ? Number(chatId) : chatId;
16851
+ }
16852
+ if (telegram.notifyOnSessionEnd === void 0) {
16853
+ telegram.notifyOnSessionEnd = true;
16854
+ }
16855
+ });
16856
+ const chatLine = chatId ? `
16857
+ Default chat: ${color.green(chatId)}` : `
16858
+ ${color.dim("No default chat set. You can add it later: /telegram-setup <token> <chatId>")}`;
16859
+ return {
16860
+ message: [
16861
+ `${color.green("\u2713")} Telegram configured successfully!`,
16862
+ "",
16863
+ `Bot: ${color.bold(`@${bot.username ?? bot.first_name}`)} ${color.dim(`(id=${bot.id})`)}`,
16864
+ `Name: ${bot.first_name}`,
16865
+ chatLine,
16866
+ "",
16867
+ `${color.amber("\u26A0")} Restart WrongStack for the plugin to pick up the new config.`,
16868
+ "",
16869
+ "After restart, try:",
16870
+ ` ${color.cyan("/telegram:status")} \u2014 check bot connection`,
16871
+ ` ${color.cyan("/telegram:send")} \u2014 send a test message`
16872
+ ].join("\n")
16873
+ };
16874
+ } catch (err) {
16875
+ return {
16876
+ message: [
16877
+ `${color.red("\u2717")} Failed to save config.`,
16878
+ `Error: ${err.message}`
16879
+ ].join("\n")
16880
+ };
16881
+ }
16882
+ }
16883
+ };
16884
+ }
16885
+ init_helpers();
16886
+ var HELP2 = [
16887
+ "Usage:",
16888
+ " /telegram-settings Show current Telegram notification settings",
16889
+ " /telegram-settings session-end on|off Notify on session end",
16890
+ " /telegram-settings delegate on|off Notify when a delegated subagent finishes",
16891
+ " /telegram-settings long-tool <ms|off> Notify for tools slower than <ms> (0/off = disabled)",
16892
+ " /telegram-settings poll <seconds> Bot polling interval (1\u201360)",
16893
+ " /telegram-settings chat <chatId> Default chat for notifications",
16894
+ "",
16895
+ "Aliases: /tg-settings",
16896
+ "",
16897
+ "Settings apply immediately \u2014 no restart needed."
16898
+ ].join("\n");
16899
+ function buildTelegramSettingsCommand(opts) {
16900
+ function currentView() {
16901
+ const config = opts.configStore.get();
16902
+ const tg = config.extensions?.telegram ?? {};
16903
+ const sessionEnd = tg.notifyOnSessionEnd === true;
16904
+ const delegate = tg.notifyOnDelegate !== false;
16905
+ const longToolMs = typeof tg.longToolThresholdMs === "number" ? tg.longToolThresholdMs : 3e4;
16906
+ const longTool = longToolMs > 0 ? `${longToolMs}ms` : "off";
16907
+ const poll = typeof tg.pollIntervalSec === "number" ? `${tg.pollIntervalSec}s` : "2s";
16908
+ const chat = tg.notifyChatId !== void 0 && tg.notifyChatId !== null ? String(tg.notifyChatId) : "not set";
16909
+ const hasToken = typeof tg.botToken === "string" && tg.botToken.length > 0;
16910
+ return [
16911
+ `${color.bold("Telegram")} ${color.dim("\u2014 Notification Settings")}`,
16912
+ "",
16913
+ ` session end: ${sessionEnd ? color.cyan("on") : color.dim("off")} ${color.dim("change: /telegram-settings session-end on|off")}`,
16914
+ ` delegate done: ${delegate ? color.cyan("on") : color.dim("off")} ${color.dim("change: /telegram-settings delegate on|off")}`,
16915
+ ` long tool: ${color.cyan(longTool)} ${color.dim("change: /telegram-settings long-tool <ms|off>")}`,
16916
+ ` poll interval: ${color.cyan(poll)} ${color.dim("change: /telegram-settings poll <seconds>")}`,
16917
+ ` notify chat: ${color.cyan(chat)} ${color.dim("change: /telegram-settings chat <chatId>")}`,
16918
+ "",
16919
+ hasToken ? color.dim(" Bot token configured. Changes apply immediately.") : `${color.amber("\u26A0")} No bot token configured. Run: /telegram-setup <botToken> [chatId]`
16920
+ ].join("\n");
16921
+ }
16922
+ return {
16923
+ name: "telegram-settings",
16924
+ category: "Config",
16925
+ aliases: ["tg-settings"],
16926
+ description: "Toggle which agent events are reported to Telegram.",
16927
+ argsHint: "[session-end|delegate|long-tool|poll|chat <value>]",
16928
+ help: HELP2,
16929
+ async run(args) {
16930
+ const { cmd: sub, rest } = parseSubcommand(args);
16931
+ if (sub === "help" || sub === "--help" || sub === "-h") {
16932
+ return { message: HELP2 };
16933
+ }
16934
+ if (!opts.configStore || !opts.paths?.globalConfig) {
16935
+ return { message: `${color.red("Error")} config store not available.` };
16936
+ }
16937
+ if (!sub) {
16938
+ return { message: currentView() };
16939
+ }
16940
+ const persistDeps = {
16941
+ configStore: opts.configStore,
16942
+ globalConfigPath: opts.paths.globalConfig,
16943
+ vault: noOpVault
16944
+ };
16945
+ try {
16946
+ if (sub === "session-end") {
16947
+ const raw = (rest[0] ?? "").toLowerCase();
16948
+ if (!["on", "off"].includes(raw)) {
16949
+ return { message: `${color.amber("Usage:")} /telegram-settings session-end on|off` };
16095
16950
  }
16096
- if (telegram.notifyOnSessionEnd === void 0) {
16097
- telegram.notifyOnSessionEnd = true;
16951
+ const on = raw === "on";
16952
+ await persistTelegramConfig(persistDeps, (tg) => {
16953
+ tg.notifyOnSessionEnd = on;
16954
+ });
16955
+ return {
16956
+ message: `${color.green("\u2713")} session-end \u2192 ${on ? color.cyan("on") : color.dim("off")}`
16957
+ };
16958
+ }
16959
+ if (sub === "delegate") {
16960
+ const raw = (rest[0] ?? "").toLowerCase();
16961
+ if (!["on", "off"].includes(raw)) {
16962
+ return { message: `${color.amber("Usage:")} /telegram-settings delegate on|off` };
16098
16963
  }
16099
- });
16100
- const chatLine = chatId ? `
16101
- Default chat: ${color.green(chatId)}` : `
16102
- ${color.dim("No default chat set. You can add it later: /telegram-setup <token> <chatId>")}`;
16964
+ const on = raw === "on";
16965
+ await persistTelegramConfig(persistDeps, (tg) => {
16966
+ tg.notifyOnDelegate = on;
16967
+ });
16968
+ return {
16969
+ message: `${color.green("\u2713")} delegate \u2192 ${on ? color.cyan("on") : color.dim("off")}`
16970
+ };
16971
+ }
16972
+ if (sub === "long-tool") {
16973
+ const raw = rest[0];
16974
+ if (raw === void 0) {
16975
+ return {
16976
+ message: `${color.amber("Usage:")} /telegram-settings long-tool <ms|off> ${color.dim("(0 or off disables)")}`
16977
+ };
16978
+ }
16979
+ if (raw === "off") {
16980
+ await persistTelegramConfig(persistDeps, (tg) => {
16981
+ tg.longToolThresholdMs = 0;
16982
+ });
16983
+ return {
16984
+ message: `${color.green("\u2713")} long-tool \u2192 ${color.dim("off")}`
16985
+ };
16986
+ }
16987
+ const ms = Number.parseInt(raw, 10);
16988
+ if (Number.isNaN(ms) || ms < 0) {
16989
+ return {
16990
+ message: `${color.red("Invalid number")}: "${raw}". Enter milliseconds, e.g. /telegram-settings long-tool 15000`
16991
+ };
16992
+ }
16993
+ await persistTelegramConfig(persistDeps, (tg) => {
16994
+ tg.longToolThresholdMs = ms;
16995
+ });
16996
+ return {
16997
+ message: `${color.green("\u2713")} long-tool \u2192 ${color.cyan(`${ms}ms`)}`
16998
+ };
16999
+ }
17000
+ if (sub === "poll") {
17001
+ const raw = rest[0];
17002
+ if (raw === void 0) {
17003
+ return { message: `${color.amber("Usage:")} /telegram-settings poll <seconds> ${color.dim("(1\u201360)")}` };
17004
+ }
17005
+ const sec = Number.parseInt(raw, 10);
17006
+ if (Number.isNaN(sec) || sec < 1 || sec > 60) {
17007
+ return {
17008
+ message: `${color.red("Invalid value")}: "${raw}". Enter seconds between 1 and 60.`
17009
+ };
17010
+ }
17011
+ await persistTelegramConfig(persistDeps, (tg) => {
17012
+ tg.pollIntervalSec = sec;
17013
+ });
17014
+ return {
17015
+ message: `${color.green("\u2713")} poll \u2192 ${color.cyan(`${sec}s`)}`
17016
+ };
17017
+ }
17018
+ if (sub === "chat") {
17019
+ const raw = rest[0];
17020
+ if (!raw) {
17021
+ return { message: `${color.amber("Usage:")} /telegram-settings chat <chatId>` };
17022
+ }
17023
+ const chatId = /^\d+$/.test(raw) ? Number(raw) : raw;
17024
+ await persistTelegramConfig(persistDeps, (tg) => {
17025
+ tg.notifyChatId = chatId;
17026
+ });
17027
+ return {
17028
+ message: `${color.green("\u2713")} notify chat \u2192 ${color.cyan(raw)}`
17029
+ };
17030
+ }
16103
17031
  return {
16104
- message: [
16105
- `${color.green("\u2713")} Telegram configured successfully!`,
16106
- "",
16107
- `Bot: ${color.bold(`@${bot.username ?? bot.first_name}`)} ${color.dim(`(id=${bot.id})`)}`,
16108
- `Name: ${bot.first_name}`,
16109
- chatLine,
16110
- "",
16111
- `${color.amber("\u26A0")} Restart WrongStack for the plugin to pick up the new config.`,
16112
- "",
16113
- "After restart, try:",
16114
- ` ${color.cyan("/telegram:status")} \u2014 check bot connection`,
16115
- ` ${color.cyan("/telegram:send")} \u2014 send a test message`
16116
- ].join("\n")
17032
+ message: `${color.red("Unknown setting")} "${sub}". ${unknownSubcommand(sub, ["session-end", "delegate", "long-tool", "poll", "chat"], "telegram-settings")}`
16117
17033
  };
16118
17034
  } catch (err) {
16119
17035
  return {
16120
- message: [
16121
- `${color.red("\u2717")} Failed to save config.`,
16122
- `Error: ${err.message}`
16123
- ].join("\n")
17036
+ message: `${color.red("Settings error")}: ${toErrorMessage(err)}`
16124
17037
  };
16125
17038
  }
16126
17039
  }
@@ -16153,7 +17066,7 @@ function buildTodosCommand(opts) {
16153
17066
  return {
16154
17067
  name: "todos",
16155
17068
  category: "Inspect",
16156
- description: "Inspect or edit the live todo list: /todos [show|clear|add|done|remove|rm <id|index>]",
17069
+ description: "Inspect or edit the live todo list: /todos [show|clear|add|done|remove|rm <id|index>]. For multi-phase work use /plan (session-persistent roadmap). For structured typed work with priorities and dependencies use /tasks (session-persistent).",
16157
17070
  async run(args) {
16158
17071
  const ctx = opts.context;
16159
17072
  if (!ctx) return { message: "No active context." };
@@ -16207,7 +17120,7 @@ function buildTodosCommand(opts) {
16207
17120
  }
16208
17121
  default:
16209
17122
  return {
16210
- message: unknownSubcommand(cmd, ["show", "clear", "add", "done", "remove"], "todos")
17123
+ message: unknownSubcommand(cmd, ["show", "clear", "add", "done", "remove"], "todos") + "\n\nRelated: /plan (session-persistent roadmap) | /tasks (structured tasks with priorities)"
16211
17124
  };
16212
17125
  }
16213
17126
  }
@@ -16253,7 +17166,7 @@ function buildWorkingDirCommand(_opts) {
16253
17166
  }
16254
17167
  const trimmed = args.trim();
16255
17168
  if (!trimmed) {
16256
- const rel2 = path39.relative(ctx.projectRoot, ctx.workingDir) || ".";
17169
+ const rel2 = path4.relative(ctx.projectRoot, ctx.workingDir) || ".";
16257
17170
  return {
16258
17171
  message: [
16259
17172
  `Working directory: ${color.bold(ctx.workingDir)}`,
@@ -16262,10 +17175,10 @@ function buildWorkingDirCommand(_opts) {
16262
17175
  ].join("\n")
16263
17176
  };
16264
17177
  }
16265
- const resolved = path39.isAbsolute(trimmed) ? path39.resolve(trimmed) : path39.resolve(ctx.projectRoot, trimmed);
16266
- const root = path39.resolve(ctx.projectRoot);
16267
- const rel = path39.relative(root, resolved);
16268
- if (rel.startsWith("..") || path39.isAbsolute(rel)) {
17178
+ const resolved = path4.isAbsolute(trimmed) ? path4.resolve(trimmed) : path4.resolve(ctx.projectRoot, trimmed);
17179
+ const root = path4.resolve(ctx.projectRoot);
17180
+ const rel = path4.relative(root, resolved);
17181
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
16269
17182
  return {
16270
17183
  message: color.red(
16271
17184
  `Directory "${trimmed}" is outside the project root.
@@ -16287,11 +17200,11 @@ function buildWorkingDirCommand(_opts) {
16287
17200
  ctx.setWorkingDir(resolved);
16288
17201
  } catch (err) {
16289
17202
  return {
16290
- message: color.red(err instanceof Error ? err.message : String(err))
17203
+ message: color.red(toErrorMessage(err))
16291
17204
  };
16292
17205
  }
16293
- const prevRel = path39.relative(ctx.projectRoot, previous) || ".";
16294
- const newRel = path39.relative(ctx.projectRoot, resolved) || ".";
17206
+ const prevRel = path4.relative(ctx.projectRoot, previous) || ".";
17207
+ const newRel = path4.relative(ctx.projectRoot, resolved) || ".";
16295
17208
  return {
16296
17209
  message: [
16297
17210
  color.green(` \u2713 ${prevRel} \u2192 ${color.bold(newRel)}`),
@@ -16434,6 +17347,7 @@ function buildBuiltinSlashCommands(opts) {
16434
17347
  buildDirectorCommand(opts),
16435
17348
  buildFleetCommand(opts),
16436
17349
  buildEnhanceCommand(opts),
17350
+ buildEnsembleCommand(),
16437
17351
  buildMemoryCommand(opts),
16438
17352
  buildTodosCommand(opts),
16439
17353
  buildTasksCommand(),
@@ -16444,6 +17358,7 @@ function buildBuiltinSlashCommands(opts) {
16444
17358
  buildMouseCommand(),
16445
17359
  buildAutonomyCommand(opts),
16446
17360
  buildGoalCommand(opts),
17361
+ buildCoordinatorCommand(opts),
16447
17362
  buildBrainCommand(opts),
16448
17363
  buildBtwCommand(opts),
16449
17364
  buildNextCommand(opts),
@@ -16456,6 +17371,7 @@ function buildBuiltinSlashCommands(opts) {
16456
17371
  buildWorktreeCommand(opts),
16457
17372
  buildSettingsCommand(opts),
16458
17373
  buildTelegramSetupCommand(opts),
17374
+ buildTelegramSettingsCommand(opts),
16459
17375
  buildSetModelCommand(opts),
16460
17376
  buildFallbackCommand(opts),
16461
17377
  buildModelCapsCommand(opts),
@@ -16475,8 +17391,6 @@ function buildBuiltinSlashCommands(opts) {
16475
17391
  })
16476
17392
  ];
16477
17393
  }
16478
-
16479
- // src/pre-launch.ts
16480
17394
  var MANIFESTS = [
16481
17395
  "package.json",
16482
17396
  "pyproject.toml",
@@ -16491,13 +17405,13 @@ var MANIFESTS = [
16491
17405
  ];
16492
17406
  async function detectProjectKind(projectRoot) {
16493
17407
  try {
16494
- await fsp5.access(path39.join(projectRoot, ".wrongstack", "AGENTS.md"));
17408
+ await fsp5.access(path4.join(projectRoot, ".wrongstack", "AGENTS.md"));
16495
17409
  return "initialized";
16496
17410
  } catch {
16497
17411
  }
16498
17412
  for (const m of MANIFESTS) {
16499
17413
  try {
16500
- await fsp5.access(path39.join(projectRoot, m));
17414
+ await fsp5.access(path4.join(projectRoot, m));
16501
17415
  return "project";
16502
17416
  } catch {
16503
17417
  }
@@ -16505,8 +17419,8 @@ async function detectProjectKind(projectRoot) {
16505
17419
  return "empty";
16506
17420
  }
16507
17421
  async function scaffoldAgentsMd(projectRoot) {
16508
- const dir = path39.join(projectRoot, ".wrongstack");
16509
- const file = path39.join(dir, "AGENTS.md");
17422
+ const dir = path4.join(projectRoot, ".wrongstack");
17423
+ const file = path4.join(dir, "AGENTS.md");
16510
17424
  const facts = await detectProjectFacts(projectRoot);
16511
17425
  const body = renderAgentsTemplate(facts);
16512
17426
  await fsp5.mkdir(dir, { recursive: true });
@@ -16519,7 +17433,7 @@ async function runProjectCheck(opts) {
16519
17433
  if (kind === "initialized") {
16520
17434
  renderer.write(
16521
17435
  `
16522
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path39.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
17436
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path4.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
16523
17437
  `
16524
17438
  );
16525
17439
  return true;
@@ -16544,13 +17458,13 @@ async function runProjectCheck(opts) {
16544
17458
  `);
16545
17459
  } catch (err) {
16546
17460
  renderer.writeError(
16547
- `Failed to scaffold AGENTS.md: ${err instanceof Error ? err.message : String(err)}`
17461
+ `Failed to scaffold AGENTS.md: ${toErrorMessage(err)}`
16548
17462
  );
16549
17463
  }
16550
17464
  }
16551
17465
  return true;
16552
17466
  }
16553
- const gitDir = path39.join(projectRoot, ".git");
17467
+ const gitDir = path4.join(projectRoot, ".git");
16554
17468
  let hasGit = false;
16555
17469
  try {
16556
17470
  await fsp5.access(gitDir);
@@ -16589,7 +17503,7 @@ async function runProjectCheck(opts) {
16589
17503
  `);
16590
17504
  } catch (err) {
16591
17505
  renderer.writeError(
16592
- `git init failed: ${err instanceof Error ? err.message : String(err)}
17506
+ `git init failed: ${toErrorMessage(err)}
16593
17507
  `
16594
17508
  );
16595
17509
  }
@@ -16785,11 +17699,11 @@ async function countProjectFiles(projectRoot, threshold) {
16785
17699
  for (const e of entries) {
16786
17700
  if (SKIP_DIRS.has(e.name)) continue;
16787
17701
  if (count >= threshold) return;
16788
- const full = path39.join(dir, e.name);
17702
+ const full = path4.join(dir, e.name);
16789
17703
  if (e.isDirectory()) {
16790
17704
  await walk(full);
16791
17705
  } else if (e.isFile()) {
16792
- if (INDEXABLE_EXTS.has(path39.extname(e.name))) {
17706
+ if (INDEXABLE_EXTS.has(path4.extname(e.name))) {
16793
17707
  count++;
16794
17708
  }
16795
17709
  }
@@ -17130,14 +18044,14 @@ function summarize(value, name) {
17130
18044
  if (typeof v === "object" && v !== null) {
17131
18045
  const o = v;
17132
18046
  if (name === "edit") {
17133
- const path40 = typeof o["path"] === "string" ? o["path"] : "";
18047
+ const path39 = typeof o["path"] === "string" ? o["path"] : "";
17134
18048
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
17135
- return `${path40} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
18049
+ return `${path39} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
17136
18050
  }
17137
18051
  if (name === "write") {
17138
- const path40 = typeof o["path"] === "string" ? o["path"] : "";
18052
+ const path39 = typeof o["path"] === "string" ? o["path"] : "";
17139
18053
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
17140
- return bytes !== void 0 ? `${path40} ${bytes}B` : path40;
18054
+ return bytes !== void 0 ? `${path39} ${bytes}B` : path39;
17141
18055
  }
17142
18056
  if (typeof o["count"] === "number") {
17143
18057
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -17148,6 +18062,17 @@ function summarize(value, name) {
17148
18062
 
17149
18063
  // src/boot.ts
17150
18064
  init_project_utils();
18065
+ function resolveCmdFromCatalog(subagentId) {
18066
+ const desc = findAgentDescriptor(subagentId);
18067
+ if (!desc) return null;
18068
+ const out = {
18069
+ command: desc.acp.command,
18070
+ args: [...desc.acp.args ?? []],
18071
+ role: subagentId
18072
+ };
18073
+ if (desc.acp.env) out.env = desc.acp.env;
18074
+ return out;
18075
+ }
17151
18076
  var acpCmd = async (args, deps) => {
17152
18077
  const sub = args[0];
17153
18078
  if (!sub || sub === "server" || sub === "serve") {
@@ -17162,6 +18087,9 @@ Usage:
17162
18087
  wstack acp list List available ACP agents
17163
18088
  wstack acp spawn <id> <task>
17164
18089
  Spawn an ACP agent as a subagent and wait for result
18090
+ wstack acp parallel <agent-id-csv> <task>
18091
+ Fan a task out to multiple ACP agents in parallel
18092
+ and aggregate the results
17165
18093
  wstack acp help Show this help
17166
18094
 
17167
18095
  ACP Mode:
@@ -17171,9 +18099,17 @@ ACP Mode:
17171
18099
  Press Ctrl+C to stop.
17172
18100
 
17173
18101
  spawn:
17174
- Spawns a named ACP agent (cline, gemini-cli, copilot, openhands, goose)
17175
- with the given task and waits for its result.
18102
+ Spawns a named ACP agent (claude-code, gemini-cli, codex-cli, copilot,
18103
+ cline, goose, openhands, qwen-code, kiro-cli, opencode, mistral-vibe,
18104
+ cursor) with the given task and waits for its result.
17176
18105
  Example: wstack acp spawn cline "fix the login bug"
18106
+
18107
+ parallel:
18108
+ Runs the same task on a comma-separated list of ACP agents concurrently.
18109
+ Example: wstack acp parallel claude-code,gemini-cli,codex-cli "review this diff"
18110
+ Each agent's result is rendered under a clearly-marked header. Returns 0
18111
+ if at least one agent succeeds, 1 if all fail. Agents that aren't
18112
+ installed are skipped with a warning.
17177
18113
  `);
17178
18114
  return 0;
17179
18115
  }
@@ -17183,20 +18119,20 @@ spawn:
17183
18119
  if (sub === "spawn") {
17184
18120
  return spawnACPAgent(args.slice(1), deps);
17185
18121
  }
18122
+ if (sub === "parallel") {
18123
+ return parallelACPAgents(args.slice(1), deps);
18124
+ }
17186
18125
  deps.renderer.writeError(`Unknown acp subcommand: ${sub}
17187
18126
  `);
17188
18127
  deps.renderer.write("Run `wstack acp help` for usage.\n");
17189
18128
  return 1;
17190
18129
  };
17191
18130
  async function runACPServer(deps) {
17192
- const toolRegistry = deps.toolRegistry;
17193
- const tools = toolRegistry?.list() ?? [];
17194
18131
  deps.renderer.writeInfo("Starting WrongStack ACP server...\n");
17195
- deps.renderer.writeInfo(`Exposing ${tools.length} tool(s) via ACP protocol.
17196
- `);
17197
18132
  deps.renderer.writeInfo("Waiting for ACP client connection on stdin/stdout...\n");
18133
+ deps.renderer.writeInfo("(default runTurn is a no-op echo \u2014 wire makeACPServerAgentTurn for a real agent)\n");
17198
18134
  deps.renderer.writeInfo("Press Ctrl+C to stop.\n");
17199
- const server = new WrongStackACPServer({ tools });
18135
+ const server = new WrongStackACPServer({});
17200
18136
  const shutdown = () => {
17201
18137
  deps.renderer.writeWarning("\nShutting down ACP server...");
17202
18138
  server.stop();
@@ -17215,16 +18151,25 @@ async function runACPServer(deps) {
17215
18151
  }
17216
18152
  return 0;
17217
18153
  }
17218
- function listACPAgents(deps) {
17219
- deps.renderer.write("Available ACP agents:\n\n");
17220
- for (const a of ACP_AGENTS) {
17221
- const id = a.id ?? a.role;
17222
- const name = a.name ?? a.role ?? "";
17223
- const desc = a.prompt?.slice(0, 50) ?? "";
17224
- deps.renderer.write(` ${id.padEnd(16)} ${name.padEnd(20)} ${desc}\u2026
18154
+ async function listACPAgents(deps) {
18155
+ const registry = new EnsembleRegistry();
18156
+ const detected = await registry.list();
18157
+ deps.renderer.write("Detected ACP agents:\n\n");
18158
+ const installed = detected.filter((a) => a.installed);
18159
+ const missing = detected.filter((a) => !a.installed);
18160
+ for (const a of installed) {
18161
+ const ver = a.version ? ` (${a.version.split("\n")[0]})` : "";
18162
+ deps.renderer.write(` \u2713 ${a.id.padEnd(16)} ${a.displayName}${ver}
18163
+ `);
18164
+ }
18165
+ for (const a of missing) {
18166
+ deps.renderer.write(` \u2717 ${a.id.padEnd(16)} ${a.displayName} (${a.reason ?? "not installed"})
17225
18167
  `);
17226
18168
  }
17227
- deps.renderer.write("\nUse `wstack acp spawn <agent> <task>` to delegate a task.\n");
18169
+ deps.renderer.write(`
18170
+ ${installed.length} of ${detected.length} agents available.
18171
+ `);
18172
+ deps.renderer.write("Use `wstack acp spawn <agent-id> <task>` to delegate a task.\n");
17228
18173
  return 0;
17229
18174
  }
17230
18175
  async function spawnACPAgent(args, deps) {
@@ -17240,7 +18185,7 @@ async function spawnACPAgent(args, deps) {
17240
18185
  deps.renderer.write("Task description is required.\n");
17241
18186
  return 1;
17242
18187
  }
17243
- const cmd = ACP_AGENT_COMMANDS[subagentId];
18188
+ const cmd = ACP_AGENT_COMMANDS[subagentId] ?? resolveCmdFromCatalog(subagentId);
17244
18189
  if (!cmd) {
17245
18190
  deps.renderer.writeError(`Unknown ACP agent: ${subagentId}
17246
18191
  `);
@@ -17294,10 +18239,11 @@ async function spawnACPAgent(args, deps) {
17294
18239
  );
17295
18240
  return 0;
17296
18241
  } catch (err) {
17297
- deps.renderer.writeError(
17298
- `ACP agent error: ${err instanceof Error ? err.message : String(err)}
17299
- `
17300
- );
18242
+ const e = err;
18243
+ const detail = e.kind ? `[${e.kind}] ` : "";
18244
+ const message = e.message ?? (err instanceof Error ? err.message : String(err));
18245
+ deps.renderer.writeError(`ACP agent error: ${detail}${message}
18246
+ `);
17301
18247
  return 1;
17302
18248
  } finally {
17303
18249
  cleanup();
@@ -17305,6 +18251,94 @@ async function spawnACPAgent(args, deps) {
17305
18251
  process.off("SIGTERM", cleanup);
17306
18252
  }
17307
18253
  }
18254
+ async function parallelACPAgents(args, deps) {
18255
+ const [csv, ...taskParts] = args;
18256
+ if (!csv) {
18257
+ deps.renderer.writeError("Usage: wstack acp parallel <agent-id-csv> <task>\n");
18258
+ deps.renderer.write('Example: wstack acp parallel claude-code,gemini-cli "review this diff"\n');
18259
+ return 1;
18260
+ }
18261
+ const task = taskParts.join(" ");
18262
+ if (!task) {
18263
+ deps.renderer.writeError("Usage: wstack acp parallel <agent-id-csv> <task>\n");
18264
+ deps.renderer.writeError("Task description is required.\n");
18265
+ return 1;
18266
+ }
18267
+ const ac = new AbortController();
18268
+ const onSignal = () => ac.abort();
18269
+ process.on("SIGINT", onSignal);
18270
+ process.on("SIGTERM", onSignal);
18271
+ try {
18272
+ const result = await runEnsemble({
18273
+ agentIds: csv,
18274
+ task,
18275
+ resolveCmd: resolveCmdFromCatalog,
18276
+ signal: ac.signal
18277
+ });
18278
+ const skipped = result.results.filter((r) => r.status === "skipped");
18279
+ if (skipped.length > 0) {
18280
+ deps.renderer.writeWarning(
18281
+ `Skipping ${skipped.length} agent(s) not installed: ${skipped.map((s) => `${s.agentId} (${s.reason ?? "not installed"})`).join(", ")}
18282
+ `
18283
+ );
18284
+ }
18285
+ if (result.summary.succeeded + result.summary.failed + result.summary.cancelled === 0) {
18286
+ deps.renderer.writeError("No installed agents to run.\n");
18287
+ deps.renderer.write("Run `wstack acp list` to see what is available.\n");
18288
+ return 1;
18289
+ }
18290
+ const fannedOut = result.results.filter((r) => r.status !== "skipped").map((r) => r.agentId).join(", ");
18291
+ deps.renderer.writeInfo(
18292
+ `Fanning out to ${result.summary.succeeded + result.summary.failed + result.summary.cancelled} agent(s): ${fannedOut}
18293
+ `
18294
+ );
18295
+ deps.renderer.writeInfo(`Task: ${result.task}
18296
+
18297
+ `);
18298
+ for (const r of result.results) {
18299
+ if (r.status === "skipped") continue;
18300
+ deps.renderer.write(`
18301
+ === ${r.agentId} ===
18302
+ `);
18303
+ if (r.status === "success") {
18304
+ deps.renderer.write(r.result && r.result.length > 0 ? r.result : "(no result)");
18305
+ deps.renderer.write(
18306
+ `
18307
+ [${r.agentId}] success ${r.durationMs}ms iterations=${r.iterations} toolCalls=${r.toolCalls}
18308
+ `
18309
+ );
18310
+ } else if (r.status === "failed") {
18311
+ deps.renderer.writeError(
18312
+ `[${r.error?.kind ?? "unknown"}] ${r.error?.message ?? "failed"}
18313
+ `
18314
+ );
18315
+ deps.renderer.write(
18316
+ `[${r.agentId}] failed ${r.durationMs}ms
18317
+ `
18318
+ );
18319
+ } else {
18320
+ deps.renderer.writeError(
18321
+ `[${r.error?.kind ?? "aborted"}] ${r.error?.message ?? "cancelled"}
18322
+ `
18323
+ );
18324
+ deps.renderer.write(
18325
+ `[${r.agentId}] cancelled ${r.durationMs}ms
18326
+ `
18327
+ );
18328
+ }
18329
+ }
18330
+ const { succeeded, failed, cancelled, skipped: skip } = result.summary;
18331
+ deps.renderer.write(
18332
+ `
18333
+ Parallel summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancelled, ${skip} skipped.
18334
+ `
18335
+ );
18336
+ return succeeded > 0 ? 0 : 1;
18337
+ } finally {
18338
+ process.off("SIGINT", onSignal);
18339
+ process.off("SIGTERM", onSignal);
18340
+ }
18341
+ }
17308
18342
  var auditCmd = async (args, deps) => {
17309
18343
  const wpaths = resolveWstackPaths({
17310
18344
  projectRoot: deps.projectRoot,
@@ -17354,14 +18388,14 @@ var auditCmd = async (args, deps) => {
17354
18388
  return verify.ok ? 0 : 1;
17355
18389
  };
17356
18390
  async function listAudits(log, dir, deps) {
17357
- const fs39 = await import('fs/promises');
17358
- const path40 = await import('path');
18391
+ const fs38 = await import('fs/promises');
18392
+ const path39 = await import('path');
17359
18393
  const out = [];
17360
18394
  let foundRoot = true;
17361
18395
  const scan = async (scanDir, prefix, depth) => {
17362
18396
  let entries;
17363
18397
  try {
17364
- entries = await fs39.readdir(scanDir, { withFileTypes: true });
18398
+ entries = await fs38.readdir(scanDir, { withFileTypes: true });
17365
18399
  } catch {
17366
18400
  if (depth === 0) foundRoot = false;
17367
18401
  return;
@@ -17369,7 +18403,7 @@ async function listAudits(log, dir, deps) {
17369
18403
  for (const entry of entries) {
17370
18404
  if (entry.name.startsWith(".")) continue;
17371
18405
  if (entry.isDirectory()) {
17372
- if (depth === 0) await scan(path40.join(scanDir, entry.name), entry.name, depth + 1);
18406
+ if (depth === 0) await scan(path39.join(scanDir, entry.name), entry.name, depth + 1);
17373
18407
  continue;
17374
18408
  }
17375
18409
  if (!entry.isFile() || !entry.name.endsWith(".audit.jsonl")) continue;
@@ -18092,6 +19126,35 @@ ${color.amber("?")} Pick: `)).trim().toLowerCase();
18092
19126
  }
18093
19127
  }
18094
19128
 
19129
+ // src/auth-menu/local.ts
19130
+ init_provider_config_utils();
19131
+
19132
+ // src/auth-menu/local.ts
19133
+ var LOCAL_LLM_PRESETS = [
19134
+ {
19135
+ id: "ollama",
19136
+ label: "Ollama",
19137
+ defaultBaseUrl: "http://localhost:11434/v1",
19138
+ noAuth: true,
19139
+ hint: "https://ollama.com \u2014 port 11434, no auth"
19140
+ },
19141
+ {
19142
+ id: "vllm",
19143
+ label: "vLLM",
19144
+ defaultBaseUrl: "http://localhost:8000/v1",
19145
+ noAuth: false,
19146
+ hint: "https://docs.vllm.ai \u2014 port 8000, optional Bearer"
19147
+ },
19148
+ {
19149
+ id: "lmstudio",
19150
+ label: "LM Studio",
19151
+ defaultBaseUrl: "http://localhost:1234/v1",
19152
+ noAuth: false,
19153
+ hint: "https://lmstudio.ai \u2014 port 1234, optional Bearer"
19154
+ }
19155
+ ];
19156
+ new Map(LOCAL_LLM_PRESETS.map((p) => [p.id, p]));
19157
+
18095
19158
  // src/subcommands/handlers/auth.ts
18096
19159
  init_provider_config_utils();
18097
19160
  var authCmd = async (args, deps) => {
@@ -18334,7 +19397,7 @@ async function resolveWstackEntry() {
18334
19397
  try {
18335
19398
  const req2 = createRequire(import.meta.url);
18336
19399
  const pkgPath = req2.resolve("@wrongstack/cli/package.json");
18337
- const entry = path39.join(path39.dirname(pkgPath), "dist", "index.js");
19400
+ const entry = path4.join(path4.dirname(pkgPath), "dist", "index.js");
18338
19401
  await fsp5.access(entry);
18339
19402
  return entry;
18340
19403
  } catch {
@@ -18349,9 +19412,9 @@ async function benchRun(_args, deps) {
18349
19412
  const outBase = flagStr(deps, "out") ?? "bench-results";
18350
19413
  let config;
18351
19414
  try {
18352
- config = await loadBenchConfig(path39.resolve(deps.cwd, modelsPath));
19415
+ config = await loadBenchConfig(path4.resolve(deps.cwd, modelsPath));
18353
19416
  } catch (err) {
18354
- deps.renderer.writeError(err instanceof Error ? err.message : String(err));
19417
+ deps.renderer.writeError(toErrorMessage(err));
18355
19418
  return 1;
18356
19419
  }
18357
19420
  const concurrencyRaw = flagStr(deps, "concurrency");
@@ -18360,8 +19423,8 @@ async function benchRun(_args, deps) {
18360
19423
  if (c > 0) config.concurrency = c;
18361
19424
  }
18362
19425
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18363
- const outDir = path39.resolve(deps.cwd, outBase, stamp);
18364
- const predictionsDir = path39.join(outDir, "predictions");
19426
+ const outDir = path4.resolve(deps.cwd, outBase, stamp);
19427
+ const predictionsDir = path4.join(outDir, "predictions");
18365
19428
  let suite;
18366
19429
  let grade;
18367
19430
  let isSwebench = false;
@@ -18373,14 +19436,14 @@ async function benchRun(_args, deps) {
18373
19436
  }
18374
19437
  const languagesRaw = flagStr(deps, "languages");
18375
19438
  const languages = languagesRaw ? languagesRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
18376
- suite = createPolyglotSuite({ polyglotDir: path39.resolve(deps.cwd, polyglotDir), languages });
19439
+ suite = createPolyglotSuite({ polyglotDir: path4.resolve(deps.cwd, polyglotDir), languages });
18377
19440
  grade = (a) => gradePolyglot(a);
18378
19441
  } else if (suiteId === "swebench") {
18379
19442
  isSwebench = true;
18380
19443
  const datasetDir = flagStr(deps, "dataset-dir");
18381
19444
  const docker = flagBool(deps, "docker");
18382
19445
  suite = createSwebenchSuite({
18383
- datasetDir: datasetDir ? path39.resolve(deps.cwd, datasetDir) : void 0,
19446
+ datasetDir: datasetDir ? path4.resolve(deps.cwd, datasetDir) : void 0,
18384
19447
  docker
18385
19448
  });
18386
19449
  grade = (a) => gradeSwebench({ ...a, predictionsDir });
@@ -18405,12 +19468,12 @@ async function benchRun(_args, deps) {
18405
19468
  onProgress: (msg) => deps.renderer.write(color.dim(msg) + "\n")
18406
19469
  });
18407
19470
  } catch (err) {
18408
- deps.renderer.writeError(err instanceof Error ? err.message : String(err));
19471
+ deps.renderer.writeError(toErrorMessage(err));
18409
19472
  return 1;
18410
19473
  }
18411
19474
  await writeJsonArtifacts(outDir, report);
18412
19475
  const md = renderMarkdownReport(report);
18413
- await fsp5.writeFile(path39.join(outDir, "report.md"), md, "utf8");
19476
+ await fsp5.writeFile(path4.join(outDir, "report.md"), md, "utf8");
18414
19477
  deps.renderer.write("\n" + md + "\n");
18415
19478
  if (isSwebench) {
18416
19479
  for (const cell of config.cells) {
@@ -18423,7 +19486,7 @@ async function benchRun(_args, deps) {
18423
19486
  "Grade with the official SWE-bench harness: python -m swebench.harness.run_evaluation --predictions_path <file> --run_id <id>"
18424
19487
  );
18425
19488
  }
18426
- deps.renderer.writeInfo(`Report written to ${path39.join(outDir, "report.md")}`);
19489
+ deps.renderer.writeInfo(`Report written to ${path4.join(outDir, "report.md")}`);
18427
19490
  return 0;
18428
19491
  }
18429
19492
  async function benchReport(args, deps) {
@@ -18432,18 +19495,18 @@ async function benchReport(args, deps) {
18432
19495
  deps.renderer.writeError("Usage: wstack bench report <run-directory>");
18433
19496
  return 1;
18434
19497
  }
18435
- const outDir = path39.resolve(deps.cwd, dir);
19498
+ const outDir = path4.resolve(deps.cwd, dir);
18436
19499
  let summary;
18437
19500
  try {
18438
19501
  summary = await readSummary(outDir);
18439
19502
  } catch (err) {
18440
19503
  deps.renderer.writeError(
18441
- `cannot read summary.json in ${outDir}: ${err instanceof Error ? err.message : String(err)}`
19504
+ `cannot read summary.json in ${outDir}: ${toErrorMessage(err)}`
18442
19505
  );
18443
19506
  return 1;
18444
19507
  }
18445
19508
  const md = renderMarkdownReport(summary);
18446
- await fsp5.writeFile(path39.join(outDir, "report.md"), md, "utf8");
19509
+ await fsp5.writeFile(path4.join(outDir, "report.md"), md, "utf8");
18447
19510
  deps.renderer.write("\n" + md + "\n");
18448
19511
  return 0;
18449
19512
  }
@@ -18458,7 +19521,7 @@ async function benchList(_args, deps) {
18458
19521
  const modelsPath = flagStr(deps, "models");
18459
19522
  if (modelsPath) {
18460
19523
  try {
18461
- const config = await loadBenchConfig(path39.resolve(deps.cwd, modelsPath));
19524
+ const config = await loadBenchConfig(path4.resolve(deps.cwd, modelsPath));
18462
19525
  deps.renderer.write("\n" + color.bold("Model cells\n"));
18463
19526
  for (const cell of config.cells) {
18464
19527
  deps.renderer.write(
@@ -18476,7 +19539,7 @@ async function benchList(_args, deps) {
18476
19539
  });
18477
19540
  deps.renderer.write("\n" + color.dim(`Harness: ${fp}`) + "\n");
18478
19541
  } catch (err) {
18479
- deps.renderer.writeError(err instanceof Error ? err.message : String(err));
19542
+ deps.renderer.writeError(toErrorMessage(err));
18480
19543
  return 1;
18481
19544
  }
18482
19545
  }
@@ -18513,14 +19576,14 @@ var doctorCmd = async (_args, deps) => {
18513
19576
  checks.push({
18514
19577
  name: "provider",
18515
19578
  status: "fail",
18516
- detail: "no provider configured \u2014 run `wstack init` or `wstack auth`"
19579
+ detail: "no provider configured \u2014 run `wstack auth` to set up"
18517
19580
  });
18518
19581
  else checks.push({ name: "provider", status: "ok", detail: cfg.provider });
18519
19582
  if (!cfg.model)
18520
19583
  checks.push({
18521
19584
  name: "model",
18522
19585
  status: "fail",
18523
- detail: "no model configured \u2014 run `wstack init`"
19586
+ detail: "no model configured \u2014 run `wstack auth` to configure"
18524
19587
  });
18525
19588
  else checks.push({ name: "model", status: "ok", detail: cfg.model });
18526
19589
  if (cfg.provider) {
@@ -18560,7 +19623,7 @@ var doctorCmd = async (_args, deps) => {
18560
19623
  checks.push({
18561
19624
  name: "models cache",
18562
19625
  status: "warn",
18563
- detail: `read failed: ${err instanceof Error ? err.message : String(err)}`
19626
+ detail: `read failed: ${toErrorMessage(err)}`
18564
19627
  });
18565
19628
  }
18566
19629
  try {
@@ -18575,7 +19638,7 @@ var doctorCmd = async (_args, deps) => {
18575
19638
  }
18576
19639
  try {
18577
19640
  await fsp5.mkdir(deps.paths.projectSessions, { recursive: true });
18578
- const probe = path39.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
19641
+ const probe = path4.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
18579
19642
  await fsp5.writeFile(probe, "");
18580
19643
  await fsp5.unlink(probe);
18581
19644
  checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
@@ -18583,7 +19646,7 @@ var doctorCmd = async (_args, deps) => {
18583
19646
  checks.push({
18584
19647
  name: "sessions writable",
18585
19648
  status: "fail",
18586
- detail: `cannot write to ${deps.paths.projectSessions}: ${err instanceof Error ? err.message : String(err)}`
19649
+ detail: `cannot write to ${deps.paths.projectSessions}: ${toErrorMessage(err)}`
18587
19650
  });
18588
19651
  }
18589
19652
  const mcpEntries = Object.entries(cfg.mcpServers ?? {});
@@ -18674,12 +19737,12 @@ var exportCmd = async (args, deps) => {
18674
19737
  try {
18675
19738
  rendered = await reader.export(sessionId, { format, includeTools, includeDiagnostics });
18676
19739
  } catch (err) {
18677
- deps.renderer.writeError(`Export failed: ${err instanceof Error ? err.message : String(err)}`);
19740
+ deps.renderer.writeError(`Export failed: ${toErrorMessage(err)}`);
18678
19741
  return 1;
18679
19742
  }
18680
19743
  if (output) {
18681
- await fsp5.mkdir(path39.dirname(path39.resolve(deps.cwd, output)), { recursive: true });
18682
- await fsp5.writeFile(path39.resolve(deps.cwd, output), rendered, "utf8");
19744
+ await fsp5.mkdir(path4.dirname(path4.resolve(deps.cwd, output)), { recursive: true });
19745
+ await fsp5.writeFile(path4.resolve(deps.cwd, output), rendered, "utf8");
18683
19746
  deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
18684
19747
  `);
18685
19748
  } else {
@@ -18688,80 +19751,23 @@ var exportCmd = async (args, deps) => {
18688
19751
  }
18689
19752
  return 0;
18690
19753
  };
18691
-
18692
- // src/subcommands/handlers/init.ts
18693
- init_helpers();
18694
19754
  var initCmd = async (_args, deps) => {
18695
- deps.renderer.write(color.bold("WrongStack init\n"));
18696
- deps.renderer.writeInfo("Loading provider catalog from models.dev (cached locally)\u2026");
18697
- let providers;
18698
- try {
18699
- providers = await deps.modelsRegistry.listProviders();
18700
- } catch (err) {
18701
- deps.renderer.writeError(
18702
- `Failed to load provider catalog: ${err instanceof Error ? err.message : err}`
18703
- );
18704
- return 1;
18705
- }
18706
- const detected = providers.filter((p) => p.family !== "unsupported").filter((p) => p.envVars.some((v) => process.env[v]));
18707
- const ranked = detected.length > 0 ? detected : providers.filter((p) => ["anthropic", "openai", "google"].includes(p.id));
18708
- if (detected.length > 0)
18709
- deps.renderer.write(
18710
- `Detected API keys for: ${detected.map((p) => p.name).join(", ")}
19755
+ deps.renderer.write(color.bold("WrongStack init (deprecated)\n"));
19756
+ deps.renderer.write(
19757
+ `
19758
+ ${color.amber("\u26A0 This command is deprecated.")}
19759
+
19760
+ Use ${color.bold("wstack auth")} to set up providers, add API keys,
19761
+ and configure your default model in one interactive workflow.
19762
+
19763
+ ${color.dim("Examples:")}
19764
+ ${color.cyan("wstack auth")} Interactive setup menu
19765
+ ${color.cyan("wstack auth anthropic")} Add Anthropic API key directly
19766
+ ${color.cyan("wstack auth local")} Add local LLM (Ollama, vLLM, LM Studio)
19767
+
19768
+ Run ${color.bold("wstack auth --help")} for more options.
18711
19769
  `
18712
- );
18713
- const defaultId = ranked[0]?.id ?? "anthropic";
18714
- const providerAnswer = (await deps.reader.readLine(`Provider [${defaultId}]: `)).trim();
18715
- if (providerAnswer === "q") {
18716
- deps.renderer.write(color.dim("Cancelled.\n"));
18717
- return 0;
18718
- }
18719
- const providerId = providerAnswer || defaultId;
18720
- const provider = await deps.modelsRegistry.getProvider(providerId);
18721
- if (!provider) {
18722
- deps.renderer.writeError(`Provider "${providerId}" not found in models.dev catalog.`);
18723
- return 1;
18724
- }
18725
- if (provider.family === "unsupported") {
18726
- deps.renderer.writeError(
18727
- `Provider "${providerId}" uses ${provider.npm} which has no built-in transport. Install a plugin to enable it.`
18728
- );
18729
- return 1;
18730
- }
18731
- const suggestedModel = await deps.modelsRegistry.suggestModel(providerId) ?? "";
18732
- const modelHint = suggestedModel ? ` [${suggestedModel}]` : "";
18733
- const modelAnswer = (await deps.reader.readLine(`Model${modelHint}: `)).trim();
18734
- if (modelAnswer === "q") {
18735
- deps.renderer.write(color.dim("Cancelled.\n"));
18736
- return 0;
18737
- }
18738
- const modelId = modelAnswer || suggestedModel;
18739
- if (!modelId) {
18740
- deps.renderer.writeError("No model selected. Aborting.");
18741
- return 1;
18742
- }
18743
- const envHit = provider.envVars.map((v) => process.env[v]).find(Boolean);
18744
- let apiKey = "";
18745
- if (!envHit) {
18746
- apiKey = (await deps.reader.readLine(
18747
- `API key (stored in ${deps.paths.globalConfig}; empty = expect ${provider.envVars[0] ?? "env var"}): `
18748
- )).trim();
18749
- } else {
18750
- deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
18751
- }
18752
- await fsp5.mkdir(deps.paths.globalRoot, { recursive: true });
18753
- const config = { version: 1, provider: providerId, model: modelId };
18754
- if (apiKey) config.apiKey = apiKey;
18755
- const vault = new DefaultSecretVault({ keyFile: deps.paths.secretsKey });
18756
- const encrypted = encryptConfigSecrets$1(config, vault);
18757
- await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2), { mode: 384 });
18758
- await fsp5.mkdir(path39.join(deps.projectRoot, ".wrongstack"), { recursive: true });
18759
- const agentsFile = path39.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
18760
- const projectFacts = await detectProjectFacts(deps.projectRoot);
18761
- await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
18762
- deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
18763
- deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
18764
- deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
19770
+ );
18765
19771
  return 0;
18766
19772
  };
18767
19773
  var AllowAllPermissionPolicy = class extends AutoApprovePermissionPolicy {
@@ -18777,7 +19783,7 @@ function parseToolsFlag(flags) {
18777
19783
  );
18778
19784
  return set.size > 0 ? set : null;
18779
19785
  }
18780
- function makeServeContext(cwd, projectRoot, signal) {
19786
+ function makeServeContext(cwd, projectRoot, signal, restrictFsToRoot = true) {
18781
19787
  const provider = {
18782
19788
  id: "mcp-serve",
18783
19789
  capabilities: { maxContext: 0 },
@@ -18804,6 +19810,7 @@ function makeServeContext(cwd, projectRoot, signal) {
18804
19810
  tokenCounter,
18805
19811
  cwd,
18806
19812
  projectRoot,
19813
+ allowOutsideProjectRoot: !restrictFsToRoot,
18807
19814
  model: "mcp-serve",
18808
19815
  tools: []
18809
19816
  });
@@ -18829,7 +19836,12 @@ async function serveMcpStdio(deps) {
18829
19836
  registry.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
18830
19837
  }
18831
19838
  const controller = new AbortController();
18832
- const ctx = makeServeContext(deps.cwd, deps.projectRoot, controller.signal);
19839
+ const ctx = makeServeContext(
19840
+ deps.cwd,
19841
+ deps.projectRoot,
19842
+ controller.signal,
19843
+ deps.config.tools?.restrictToProjectRoot ?? false
19844
+ );
18833
19845
  const permissionPolicy = yolo ? new AllowAllPermissionPolicy() : new AutoApprovePermissionPolicy();
18834
19846
  const executor = new ToolExecutor(registry, {
18835
19847
  permissionPolicy,
@@ -19647,7 +20659,7 @@ var modeldiagCmd = async (args, deps) => {
19647
20659
  ` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color.amber(fmtMs(latency).padEnd(8))} ${color.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
19648
20660
  );
19649
20661
  } catch (err) {
19650
- const errMsg = err instanceof Error ? err.message : String(err);
20662
+ const errMsg = toErrorMessage(err);
19651
20663
  writeLine(
19652
20664
  ` ${label} ${color.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color.red("FAILED")} ${color.dim(errMsg.slice(0, 40))}`
19653
20665
  );
@@ -20119,7 +21131,7 @@ var usageCmd = async (_args, deps) => {
20119
21131
  return 0;
20120
21132
  };
20121
21133
  var projectsCmd = async (_args, deps) => {
20122
- const projectsRoot = path39.join(deps.paths.globalRoot, "projects");
21134
+ const projectsRoot = path4.join(deps.paths.globalRoot, "projects");
20123
21135
  try {
20124
21136
  const entries = await fsp5.readdir(projectsRoot);
20125
21137
  if (entries.length === 0) {
@@ -20129,7 +21141,7 @@ var projectsCmd = async (_args, deps) => {
20129
21141
  for (const hash of entries) {
20130
21142
  try {
20131
21143
  const meta = JSON.parse(
20132
- await fsp5.readFile(path39.join(projectsRoot, hash, "meta.json"), "utf8")
21144
+ await fsp5.readFile(path4.join(projectsRoot, hash, "meta.json"), "utf8")
20133
21145
  );
20134
21146
  deps.renderer.write(
20135
21147
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -20578,7 +21590,7 @@ function findSessionId(args) {
20578
21590
  var rewindCmd = async (args, deps) => {
20579
21591
  const flags = parseRewindFlags(args);
20580
21592
  const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
20581
- const sessionsDir = path39.join(wpaths.globalRoot, "sessions");
21593
+ const sessionsDir = path4.join(wpaths.globalRoot, "sessions");
20582
21594
  const rewind = new DefaultSessionRewinder(sessionsDir, deps.projectRoot);
20583
21595
  let sessionId = findSessionId(args);
20584
21596
  if (!sessionId) {
@@ -20691,7 +21703,7 @@ ${result.errors.length} error(s):
20691
21703
  }
20692
21704
  return 0;
20693
21705
  } catch (err) {
20694
- deps.renderer.writeError(err instanceof Error ? err.message : String(err));
21706
+ deps.renderer.writeError(toErrorMessage(err));
20695
21707
  return 1;
20696
21708
  }
20697
21709
  };
@@ -20723,7 +21735,7 @@ async function listFleetRuns(deps) {
20723
21735
  }
20724
21736
  const runs = [];
20725
21737
  for (const id of entries) {
20726
- const runDir = path39.join(deps.paths.projectSessions, id);
21738
+ const runDir = path4.join(deps.paths.projectSessions, id);
20727
21739
  let stat7;
20728
21740
  try {
20729
21741
  stat7 = await fsp5.stat(runDir);
@@ -20736,17 +21748,17 @@ async function listFleetRuns(deps) {
20736
21748
  let subagentCount = 0;
20737
21749
  let subagentsDir;
20738
21750
  try {
20739
- await fsp5.access(path39.join(runDir, "fleet.json"));
21751
+ await fsp5.access(path4.join(runDir, "fleet.json"));
20740
21752
  manifest = true;
20741
21753
  } catch {
20742
21754
  }
20743
21755
  try {
20744
- await fsp5.access(path39.join(runDir, "checkpoint.json"));
21756
+ await fsp5.access(path4.join(runDir, "checkpoint.json"));
20745
21757
  checkpoint = true;
20746
21758
  } catch {
20747
21759
  }
20748
21760
  try {
20749
- subagentsDir = path39.join(runDir, "subagents");
21761
+ subagentsDir = path4.join(runDir, "subagents");
20750
21762
  const files = await fsp5.readdir(subagentsDir);
20751
21763
  subagentCount = files.filter((f) => f.endsWith(".jsonl")).length;
20752
21764
  } catch {
@@ -20773,7 +21785,7 @@ async function listFleetRuns(deps) {
20773
21785
  return 0;
20774
21786
  }
20775
21787
  async function showFleetRun(runId, deps) {
20776
- const runDir = path39.join(deps.paths.projectSessions, runId);
21788
+ const runDir = path4.join(deps.paths.projectSessions, runId);
20777
21789
  let stat7;
20778
21790
  try {
20779
21791
  stat7 = await fsp5.stat(runDir);
@@ -20790,7 +21802,7 @@ async function showFleetRun(runId, deps) {
20790
21802
  deps.renderer.write(color.bold(`
20791
21803
  Fleet Run: ${runId}
20792
21804
  `) + "\n");
20793
- const manifestPath = path39.join(runDir, "fleet.json");
21805
+ const manifestPath = path4.join(runDir, "fleet.json");
20794
21806
  let manifestData = null;
20795
21807
  try {
20796
21808
  manifestData = await fsp5.readFile(manifestPath, "utf8");
@@ -20808,7 +21820,7 @@ Fleet Run: ${runId}
20808
21820
  deps.renderer.write(` ${color.dim("\u25CB")} fleet.json \u2014 not found
20809
21821
  `);
20810
21822
  }
20811
- const checkpointPath = path39.join(runDir, "checkpoint.json");
21823
+ const checkpointPath = path4.join(runDir, "checkpoint.json");
20812
21824
  let checkpointData = null;
20813
21825
  try {
20814
21826
  checkpointData = await fsp5.readFile(checkpointPath, "utf8");
@@ -20855,7 +21867,7 @@ Fleet Run: ${runId}
20855
21867
  } catch {
20856
21868
  }
20857
21869
  }
20858
- const subagentsDir = path39.join(runDir, "subagents");
21870
+ const subagentsDir = path4.join(runDir, "subagents");
20859
21871
  let subagentFiles = [];
20860
21872
  try {
20861
21873
  subagentFiles = await fsp5.readdir(subagentsDir);
@@ -20867,7 +21879,7 @@ Fleet Run: ${runId}
20867
21879
  Subagent transcripts (${subagentFiles.length}):
20868
21880
  `);
20869
21881
  for (const f of subagentFiles.sort()) {
20870
- const filePath = path39.join(subagentsDir, f);
21882
+ const filePath = path4.join(subagentsDir, f);
20871
21883
  let size;
20872
21884
  try {
20873
21885
  const s = await fsp5.stat(filePath);
@@ -20884,7 +21896,7 @@ Fleet Run: ${runId}
20884
21896
  ${color.dim("\u25CB")} No subagent transcripts
20885
21897
  `);
20886
21898
  }
20887
- const sharedDir = path39.join(runDir, "shared");
21899
+ const sharedDir = path4.join(runDir, "shared");
20888
21900
  try {
20889
21901
  const files = await fsp5.readdir(sharedDir);
20890
21902
  deps.renderer.write(`
@@ -21125,8 +22137,8 @@ var helpCmd = async (_args, deps) => {
21125
22137
  ' wstack --eternal "<mission>" Launch eternal-autonomy loop against a goal \u2014 Ctrl+C to stop',
21126
22138
  " wstack resume [<id>] Resume a session",
21127
22139
  " wstack sessions List recent sessions",
21128
- " wstack init Pick provider + model from models.dev",
21129
- " wstack auth Interactive key manager (list/add/update/delete)",
22140
+ " wstack auth Interactive setup + key manager (add/edit/delete)",
22141
+ " wstack auth <provider> Add API key for a provider",
21130
22142
  " wstack auth list Quick listing of saved providers and keys",
21131
22143
  " wstack auth status <id> Detailed view of one provider",
21132
22144
  " wstack auth remove <id> Delete a provider (asks for confirmation)",
@@ -21211,7 +22223,7 @@ function resolveBundledSkillsDir() {
21211
22223
  try {
21212
22224
  const req2 = createRequire(import.meta.url);
21213
22225
  const corePkg = req2.resolve("@wrongstack/core/package.json");
21214
- return path39.join(path39.dirname(corePkg), "skills");
22226
+ return path4.join(path4.dirname(corePkg), "skills");
21215
22227
  } catch {
21216
22228
  return void 0;
21217
22229
  }
@@ -21230,7 +22242,7 @@ async function boot(argv) {
21230
22242
  try {
21231
22243
  bootResult = await bootConfig(flags);
21232
22244
  } catch (err) {
21233
- writeErr(`Config error: ${err instanceof Error ? err.message : String(err)}
22245
+ writeErr(`Config error: ${toErrorMessage(err)}
21234
22246
  `);
21235
22247
  return 2;
21236
22248
  }
@@ -21283,7 +22295,7 @@ async function boot(argv) {
21283
22295
  await modelsRegistry.refresh();
21284
22296
  logger.info("models.dev catalog refreshed");
21285
22297
  } catch (err) {
21286
- const msg = err instanceof Error ? err.message : String(err);
22298
+ const msg = toErrorMessage(err);
21287
22299
  logger.warn(`models.dev refresh failed (${msg}); using cached catalog`);
21288
22300
  }
21289
22301
  }
@@ -21432,7 +22444,7 @@ async function boot(argv) {
21432
22444
  const created = await registerProjectAtBoot({ projectRoot, cwd, wpaths });
21433
22445
  if (created && isInteractiveTTY) {
21434
22446
  renderer.write(
21435
- color.dim(` \u2713 Registered "${path39.basename(projectRoot)}" in projects.json.
22447
+ color.dim(` \u2713 Registered "${path4.basename(projectRoot)}" in projects.json.
21436
22448
  `)
21437
22449
  );
21438
22450
  }
@@ -21512,7 +22524,7 @@ async function boot(argv) {
21512
22524
  } catch {
21513
22525
  }
21514
22526
  printLaunchHints(renderer, flags, {
21515
- cursorFile: path39.join(wpaths.cacheDir, "hint-cursor")
22527
+ cursorFile: path4.join(wpaths.cacheDir, "hint-cursor")
21516
22528
  });
21517
22529
  } else {
21518
22530
  const effectiveChoices = config.launch ? {
@@ -21550,7 +22562,7 @@ async function boot(argv) {
21550
22562
  }
21551
22563
  async function checkGitInCwd(opts) {
21552
22564
  const { cwd, renderer, reader } = opts;
21553
- const cwdGit = path39.join(cwd, ".git");
22565
+ const cwdGit = path4.join(cwd, ".git");
21554
22566
  let hasCwdGit = false;
21555
22567
  try {
21556
22568
  await fsp5.access(cwdGit);
@@ -21584,16 +22596,16 @@ async function checkGitInCwd(opts) {
21584
22596
  hasCwdGit = true;
21585
22597
  } catch (err) {
21586
22598
  renderer.writeError(
21587
- `git init failed: ${err instanceof Error ? err.message : String(err)}
22599
+ `git init failed: ${toErrorMessage(err)}
21588
22600
  `
21589
22601
  );
21590
22602
  }
21591
22603
  }
21592
22604
  }
21593
- const parentDir = path39.dirname(cwd);
22605
+ const parentDir = path4.dirname(cwd);
21594
22606
  if (parentDir !== cwd) {
21595
22607
  try {
21596
- await fsp5.access(path39.join(parentDir, ".git"));
22608
+ await fsp5.access(path4.join(parentDir, ".git"));
21597
22609
  renderer.write(
21598
22610
  ` ${color.dim("\u2139")} A ${color.bold(".git")} repo exists in the parent directory: ${color.dim(parentDir)}
21599
22611
  `
@@ -21605,7 +22617,7 @@ async function checkGitInCwd(opts) {
21605
22617
  async function registerProjectAtBoot(opts) {
21606
22618
  const { projectRoot, cwd, wpaths } = opts;
21607
22619
  const manifest = await loadManifest(wpaths.globalConfig);
21608
- const existed = manifest.projects.some((p) => path39.resolve(p.root) === path39.resolve(projectRoot));
22620
+ const existed = manifest.projects.some((p) => path4.resolve(p.root) === path4.resolve(projectRoot));
21609
22621
  await touchProjectInManifest({
21610
22622
  projectRoot,
21611
22623
  globalConfigPath: wpaths.globalConfig,
@@ -21613,9 +22625,26 @@ async function registerProjectAtBoot(opts) {
21613
22625
  });
21614
22626
  return !existed;
21615
22627
  }
22628
+ function toolsForTier(tier, allTools) {
22629
+ switch (tier) {
22630
+ case "off":
22631
+ return allTools;
22632
+ case "minimal":
22633
+ case "light":
22634
+ return TIER1_TOOLS;
22635
+ case "medium":
22636
+ return [...TIER1_TOOLS, ...TIER2_TOOLS];
22637
+ case "aggressive": {
22638
+ const t2WithoutTask = TIER2_TOOLS.filter((t) => t.name !== "task");
22639
+ const t3WithoutSetCwd = TIER3_TOOLS.filter((t) => t.name !== "setWorkingDir");
22640
+ return [...TIER1_TOOLS, ...t2WithoutTask, ...t3WithoutSetCwd];
22641
+ }
22642
+ }
22643
+ }
21616
22644
  function registerBuiltinTools(deps) {
22645
+ const tier = normalizeTokenSavingTier(deps.config.features.tokenSavingMode);
21617
22646
  const allTools = builtinToolsPack.tools ?? [];
21618
- const toolsToRegister = deps.config.features.tokenSavingMode ? TIER1_TOOLS : allTools;
22647
+ const toolsToRegister = toolsForTier(tier, allTools);
21619
22648
  deps.toolRegistry.registerAllOrThrow([...toolsToRegister], builtinToolsPack.name);
21620
22649
  deps.toolRegistry.registerDefault(
21621
22650
  createContextManagerTool({ compactor: deps.compactor })
@@ -21806,7 +22835,7 @@ function resolveBundledSkillsDir2() {
21806
22835
  try {
21807
22836
  const req2 = createRequire(import.meta.url);
21808
22837
  const corePkg = req2.resolve("@wrongstack/core/package.json");
21809
- return path39.join(path39.dirname(corePkg), "skills");
22838
+ return path4.join(path4.dirname(corePkg), "skills");
21810
22839
  } catch {
21811
22840
  return void 0;
21812
22841
  }
@@ -22294,7 +23323,7 @@ async function runRepl(opts) {
22294
23323
  clientMailbox.registerClient({
22295
23324
  clientId,
22296
23325
  sessionId: replProjectRoot,
22297
- name: `REPL [${path39.basename(replProjectRoot)}]`,
23326
+ name: `REPL [${path4.basename(replProjectRoot)}]`,
22298
23327
  source: "repl",
22299
23328
  pid: process.pid
22300
23329
  }).then(() => {
@@ -22359,7 +23388,7 @@ async function runRepl(opts) {
22359
23388
  }
22360
23389
  } catch (err) {
22361
23390
  opts.renderer.writeError(
22362
- `[eternal] ${err instanceof Error ? err.message : String(err)}`
23391
+ `[eternal] ${toErrorMessage(err)}`
22363
23392
  );
22364
23393
  }
22365
23394
  await new Promise((resolve11) => setTimeout(resolve11, 250));
@@ -22435,7 +23464,7 @@ async function runRepl(opts) {
22435
23464
  }
22436
23465
  } catch (err) {
22437
23466
  opts.renderer.writeError(
22438
- `[parallel] ${err instanceof Error ? err.message : String(err)}`
23467
+ `[parallel] ${toErrorMessage(err)}`
22439
23468
  );
22440
23469
  }
22441
23470
  await new Promise((resolve11) => setTimeout(resolve11, 250));
@@ -22508,7 +23537,7 @@ ${lines.join("\n")}
22508
23537
  if (res?.message) opts.renderer.write(`${res.message}
22509
23538
  `);
22510
23539
  } catch (err) {
22511
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
23540
+ opts.renderer.writeError(toErrorMessage(err));
22512
23541
  }
22513
23542
  continue;
22514
23543
  }
@@ -22597,7 +23626,7 @@ ${color.dim(taskList2)}
22597
23626
  }
22598
23627
  }
22599
23628
  } catch (err) {
22600
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
23629
+ opts.renderer.writeError(toErrorMessage(err));
22601
23630
  }
22602
23631
  continue;
22603
23632
  }
@@ -22786,7 +23815,7 @@ ${color.dim(
22786
23815
  }
22787
23816
  } catch (err) {
22788
23817
  opts.renderer.writeError(
22789
- `[autonomy] ${err instanceof Error ? err.message : String(err)}`
23818
+ `[autonomy] ${toErrorMessage(err)}`
22790
23819
  );
22791
23820
  } finally {
22792
23821
  activeCtrl = void 0;
@@ -22857,7 +23886,7 @@ ${color.dim(lines)}
22857
23886
  }
22858
23887
  }
22859
23888
  } catch (err) {
22860
- opts.renderer.writeError(err instanceof Error ? err.message : String(err));
23889
+ opts.renderer.writeError(toErrorMessage(err));
22861
23890
  } finally {
22862
23891
  activeCtrl = void 0;
22863
23892
  }
@@ -22884,7 +23913,7 @@ async function pasteClipboardImage(builder, opts) {
22884
23913
  `));
22885
23914
  } catch (err) {
22886
23915
  opts.renderer.writeError(
22887
- `Clipboard image error: ${err instanceof Error ? err.message : String(err)}`
23916
+ `Clipboard image error: ${toErrorMessage(err)}`
22888
23917
  );
22889
23918
  }
22890
23919
  }
@@ -22922,7 +23951,7 @@ async function renderGoalBanner(opts) {
22922
23951
  await opts.slashRegistry.dispatch("/autonomy eternal", opts.agent.ctx);
22923
23952
  } catch (err) {
22924
23953
  opts.renderer.writeError(
22925
- `Auto-resume failed: ${err instanceof Error ? err.message : String(err)}`
23954
+ `Auto-resume failed: ${toErrorMessage(err)}`
22926
23955
  );
22927
23956
  }
22928
23957
  } else {
@@ -23111,6 +24140,7 @@ function printBanner(renderer, projectName) {
23111
24140
  }
23112
24141
 
23113
24142
  // src/execution.ts
24143
+ init_provider_config_utils();
23114
24144
  init_utils();
23115
24145
  async function execute(deps) {
23116
24146
  const {
@@ -23134,6 +24164,7 @@ async function execute(deps) {
23134
24164
  effectiveMaxContext,
23135
24165
  queueStore,
23136
24166
  context,
24167
+ mailbox,
23137
24168
  stats,
23138
24169
  detachTodosCheckpoint,
23139
24170
  savedProviderCfg,
@@ -23223,8 +24254,8 @@ async function execute(deps) {
23223
24254
  timeoutMs: 3e5
23224
24255
  };
23225
24256
  const subagentId = await dir.spawn(cfg);
23226
- const { randomUUID: randomUUID6 } = await import('crypto');
23227
- const taskId = randomUUID6();
24257
+ const { randomUUID: randomUUID7 } = await import('crypto');
24258
+ const taskId = randomUUID7();
23228
24259
  await dir.assign({
23229
24260
  id: taskId,
23230
24261
  description: taskDesc,
@@ -23329,8 +24360,8 @@ async function execute(deps) {
23329
24360
  result = await agent.run(query, { signal: ctrl.signal });
23330
24361
  } finally {
23331
24362
  process.off("SIGINT", onSigint);
23332
- const { getProcessRegistry: getProcessRegistry2 } = await import('@wrongstack/tools');
23333
- getProcessRegistry2().killAll();
24363
+ const { getProcessRegistry: getProcessRegistry3 } = await import('@wrongstack/tools');
24364
+ getProcessRegistry3().killAll();
23334
24365
  }
23335
24366
  const after = tokenCounter.total();
23336
24367
  const costAfter = tokenCounter.estimateCost().total;
@@ -23389,7 +24420,7 @@ async function execute(deps) {
23389
24420
  const { runTui } = await import('@wrongstack/tui');
23390
24421
  renderer.setSilent(true);
23391
24422
  const banneredFamily = savedProviderCfg?.family ?? resolvedProvider?.family;
23392
- const banneredKey = savedProviderCfg?.apiKey ?? config.apiKey ?? (resolvedProvider?.envVars ?? savedProviderCfg?.envVars ?? []).map((v) => process.env[v]).find((v) => !!v);
24423
+ const banneredKey = (savedProviderCfg ? resolveActiveApiKey(savedProviderCfg) : void 0) ?? config.apiKey ?? (resolvedProvider?.envVars ?? savedProviderCfg?.envVars ?? []).map((v) => process.env[v]).find((v) => !!v);
23393
24424
  const banneredKeyTail = banneredKey && banneredKey.length >= 3 ? banneredKey.slice(-3) : void 0;
23394
24425
  const autoPhaseHandlers = /* @__PURE__ */ new Map();
23395
24426
  const subscribeAutoPhase = (handler) => {
@@ -23437,6 +24468,80 @@ async function execute(deps) {
23437
24468
  };
23438
24469
  const PROJECT_SWITCH_EXIT_CODE = 42;
23439
24470
  let pendingProjectSwitch = null;
24471
+ const coordinatorEvents = /* @__PURE__ */ new Set();
24472
+ let autonomousCoordinator = null;
24473
+ const onDirectorReady = (dir) => {
24474
+ if (autonomousCoordinator) return;
24475
+ const transcript = context.session.transcriptPath;
24476
+ const sessionDir = transcript ? path4.dirname(transcript) : wpaths.projectDir;
24477
+ const llmProvider = {
24478
+ decide: async (prompt) => {
24479
+ const sysPrompt = [
24480
+ {
24481
+ type: "text",
24482
+ text: 'You are the autonomous brain of a multi-agent coordination system. Pick the best option for the decision described and reply with JSON: {"optionId":"<id>","rationale":"<short why>"}.'
24483
+ }
24484
+ ];
24485
+ const userPrompt = {
24486
+ type: "text",
24487
+ text: `Decision: ${prompt.question}
24488
+
24489
+ Context: ${prompt.context}
24490
+
24491
+ Options:
24492
+ ${prompt.options.map((o, i) => ` ${i + 1}. [${o.id}] ${o.label}${o.consequence ? ` \u2014 ${o.consequence}` : ""}`).join("\n")}
24493
+
24494
+ Risk: ${prompt.risk}
24495
+
24496
+ Reply with ONLY the JSON object.`
24497
+ };
24498
+ const resp = await context.provider.complete(
24499
+ {
24500
+ model: context.model,
24501
+ system: sysPrompt,
24502
+ messages: [
24503
+ {
24504
+ role: "user",
24505
+ content: [userPrompt]
24506
+ }
24507
+ ],
24508
+ maxTokens: 1024,
24509
+ temperature: 0
24510
+ },
24511
+ { signal: context.signal }
24512
+ );
24513
+ const text = resp.content.filter((b) => b.type === "text").map((b) => b.text).join("\n").trim();
24514
+ const cleaned = text.replace(/^```(?:json)?\s*/i, "").replace(/```$/, "").trim();
24515
+ try {
24516
+ const parsed = JSON.parse(cleaned);
24517
+ const optId = parsed.optionId ?? prompt.options[0]?.id ?? "";
24518
+ return { optionId: optId, rationale: parsed.rationale ?? "" };
24519
+ } catch {
24520
+ return { optionId: prompt.options[0]?.id ?? "", rationale: text };
24521
+ }
24522
+ }
24523
+ };
24524
+ autonomousCoordinator = new AutonomousCoordinator({
24525
+ sessionDir,
24526
+ fleet: dir.fleet,
24527
+ mailbox,
24528
+ selfAgentId: `leader@${context.session.id ?? "unknown"}`,
24529
+ selfAgentName: "Leader",
24530
+ llmProvider,
24531
+ onCoordinatorEvent: (event) => {
24532
+ for (const fn of coordinatorEvents) fn(event);
24533
+ }
24534
+ });
24535
+ deps.onCoordinatorStop = () => autonomousCoordinator?.stop();
24536
+ };
24537
+ if (director) onDirectorReady(director);
24538
+ const offDirectorSpawned = events.onPattern("subagent.spawned", () => {
24539
+ const dir = director;
24540
+ if (dir) {
24541
+ offDirectorSpawned();
24542
+ onDirectorReady(dir);
24543
+ }
24544
+ });
23440
24545
  try {
23441
24546
  code = await runTui({
23442
24547
  agent,
@@ -23520,13 +24625,15 @@ async function execute(deps) {
23520
24625
  featureMemory: cfg.features?.memory !== false,
23521
24626
  featureSkills: cfg.features?.skills !== false,
23522
24627
  featureModelsRegistry: cfg.features?.modelsRegistry !== false,
23523
- featureTokenSaving: cfg.features?.tokenSavingMode ?? false,
24628
+ featureTokenSaving: normalizeTokenSavingTier(cfg.features?.tokenSavingMode),
24629
+ allowOutsideProjectRoot: cfg.features?.allowOutsideProjectRoot ?? true,
23524
24630
  contextAutoCompact: cfg.context?.autoCompact !== false,
23525
24631
  contextStrategy: cfg.context?.strategy ?? "hybrid",
23526
24632
  logLevel: cfg.log?.level ?? "info",
23527
24633
  auditLevel: cfg.session?.auditLevel ?? "standard",
23528
24634
  indexOnStart: cfg.indexing?.onSessionStart !== false,
23529
24635
  maxIterations: cfg.tools?.maxIterations ?? 500,
24636
+ restrictFsToRoot: cfg.tools?.restrictToProjectRoot ?? false,
23530
24637
  autoProceedMaxIterations: cfg.autonomy?.autoProceedMaxIterations ?? 50,
23531
24638
  debugStream: cfg.debugStream ?? false,
23532
24639
  configScope: cfg.configScope ?? "global",
@@ -23534,7 +24641,9 @@ async function execute(deps) {
23534
24641
  enhanceEnabled: cfg.autonomy?.enhance ?? true,
23535
24642
  enhanceLanguage: cfg.autonomy?.enhanceLanguage === "english" ? "english" : "original",
23536
24643
  mouseMode: autonomy?.mouseMode ?? false,
23537
- autonomyNextPrompt: cfg.autonomy?.autonomyNextPrompt ?? "auto {{suggestion}}"
24644
+ autonomyNextPrompt: cfg.autonomy?.autonomyNextPrompt ?? "auto {{suggestion}}",
24645
+ breakerEnabled: cfg.circuitBreaker?.enabled === true,
24646
+ breakerAutoKillResetMs: cfg.circuitBreaker?.autoKillResetMs ?? 6e4
23538
24647
  };
23539
24648
  },
23540
24649
  async saveSettings(s) {
@@ -23559,9 +24668,11 @@ async function execute(deps) {
23559
24668
  if (s.enhanceEnabled !== void 0) a["enhance"] = s.enhanceEnabled;
23560
24669
  if (s.enhanceLanguage !== void 0) a["enhanceLanguage"] = s.enhanceLanguage;
23561
24670
  if (s.autonomyNextPrompt !== void 0) a["autonomyNextPrompt"] = s.autonomyNextPrompt;
24671
+ if (s.autoProceedMaxIterations !== void 0)
24672
+ a["autoProceedMaxIterations"] = s.autoProceedMaxIterations;
23562
24673
  }
23563
24674
  );
23564
- if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.nextPrediction !== void 0 || s.debugStream !== void 0 || s.configScope !== void 0 || s.enhanceDelayMs !== void 0) {
24675
+ if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0 || s.allowOutsideProjectRoot !== void 0 || s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0 || s.logLevel !== void 0 || s.auditLevel !== void 0 || s.indexOnStart !== void 0 || s.maxIterations !== void 0 || s.restrictFsToRoot !== void 0 || s.nextPrediction !== void 0 || s.debugStream !== void 0 || s.configScope !== void 0 || s.enhanceDelayMs !== void 0) {
23565
24676
  const configScope = s.configScope ?? configStore.get().configScope ?? "global";
23566
24677
  const targetPath = configScope === "project" && wpaths.inProjectConfig ? wpaths.inProjectConfig : wpaths.globalConfig;
23567
24678
  let raw;
@@ -23578,7 +24689,7 @@ async function execute(deps) {
23578
24689
  if (s.nextPrediction !== void 0) {
23579
24690
  decrypted.nextPrediction = s.nextPrediction;
23580
24691
  }
23581
- if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0) {
24692
+ if (s.featureMcp !== void 0 || s.featurePlugins !== void 0 || s.featureMemory !== void 0 || s.featureSkills !== void 0 || s.featureModelsRegistry !== void 0 || s.featureTokenSaving !== void 0 || s.allowOutsideProjectRoot !== void 0) {
23582
24693
  const feats = decrypted.features ?? {};
23583
24694
  if (s.featureMcp !== void 0) feats.mcp = s.featureMcp;
23584
24695
  if (s.featurePlugins !== void 0) feats.plugins = s.featurePlugins;
@@ -23588,6 +24699,8 @@ async function execute(deps) {
23588
24699
  feats.modelsRegistry = s.featureModelsRegistry;
23589
24700
  if (s.featureTokenSaving !== void 0)
23590
24701
  feats.tokenSavingMode = s.featureTokenSaving;
24702
+ if (s.allowOutsideProjectRoot !== void 0)
24703
+ feats.allowOutsideProjectRoot = s.allowOutsideProjectRoot;
23591
24704
  decrypted.features = feats;
23592
24705
  }
23593
24706
  if (s.contextAutoCompact !== void 0 || s.contextStrategy !== void 0) {
@@ -23611,11 +24724,18 @@ async function execute(deps) {
23611
24724
  idx.onSessionStart = s.indexOnStart;
23612
24725
  decrypted.indexing = idx;
23613
24726
  }
23614
- if (s.maxIterations !== void 0) {
24727
+ if (s.maxIterations !== void 0 || s.restrictFsToRoot !== void 0) {
23615
24728
  const tools = decrypted.tools ?? {};
23616
- tools.maxIterations = s.maxIterations;
24729
+ if (s.maxIterations !== void 0) tools.maxIterations = s.maxIterations;
24730
+ if (s.restrictFsToRoot !== void 0)
24731
+ tools.restrictToProjectRoot = s.restrictFsToRoot;
23617
24732
  decrypted.tools = tools;
23618
24733
  }
24734
+ if (s.restrictFsToRoot !== void 0) {
24735
+ const features = decrypted.features ?? {};
24736
+ features.allowOutsideProjectRoot = !s.restrictFsToRoot;
24737
+ decrypted.features = features;
24738
+ }
23619
24739
  if (s.debugStream !== void 0) {
23620
24740
  decrypted.debugStream = s.debugStream;
23621
24741
  const { setDebugStreamEnabled } = await import('@wrongstack/providers');
@@ -23637,7 +24757,7 @@ async function execute(deps) {
23637
24757
  const toWrite = targetPath === wpaths.globalConfig ? decrypted : filterSafeForProject(decrypted);
23638
24758
  const encrypted = encryptConfigSecrets(toWrite, noOpVault);
23639
24759
  if (targetPath !== wpaths.globalConfig) {
23640
- await fsp5.mkdir(path39.dirname(targetPath), { recursive: true });
24760
+ await fsp5.mkdir(path4.dirname(targetPath), { recursive: true });
23641
24761
  }
23642
24762
  await atomicWrite(targetPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
23643
24763
  configStore.update({
@@ -23647,7 +24767,7 @@ async function execute(deps) {
23647
24767
  ...s.logLevel !== void 0 ? { log: decrypted.log } : {},
23648
24768
  ...s.auditLevel !== void 0 ? { session: decrypted.session } : {},
23649
24769
  ...s.indexOnStart !== void 0 ? { indexing: decrypted.indexing } : {},
23650
- ...s.maxIterations !== void 0 ? { tools: decrypted.tools } : {},
24770
+ ...s.maxIterations !== void 0 || s.restrictFsToRoot !== void 0 ? { tools: decrypted.tools } : {},
23651
24771
  ...s.debugStream !== void 0 ? { debugStream: s.debugStream } : {},
23652
24772
  ...s.configScope !== void 0 ? { configScope: s.configScope } : {},
23653
24773
  ...s.enhanceDelayMs !== void 0 ? {
@@ -23673,6 +24793,7 @@ async function execute(deps) {
23673
24793
  if (s.streamFleet !== void 0) {
23674
24794
  fleetStreamController?.setEnabled(s.streamFleet);
23675
24795
  }
24796
+ deps.applyLiveSettings?.(s);
23676
24797
  return null;
23677
24798
  } catch (err) {
23678
24799
  const message = err instanceof Error ? err.message : String(err);
@@ -23697,6 +24818,29 @@ async function execute(deps) {
23697
24818
  confirmExit: config.autonomy?.["confirmExit"] ?? true,
23698
24819
  director,
23699
24820
  fleetRoster,
24821
+ // ── AutonomousCoordinator: project-level multi-session coordination ─────────
24822
+ // The coordinator tracks goals, tasks, knowledge, and consensus across all
24823
+ // active sessions in the same project. It runs independently of the leader
24824
+ // agent and is accessible to any session in the project via the GlobalMailbox.
24825
+ getAutonomousCoordinator: () => autonomousCoordinator,
24826
+ subscribeCoordinatorEvents: (fn) => {
24827
+ coordinatorEvents.add(fn);
24828
+ return () => {
24829
+ coordinatorEvents.delete(fn);
24830
+ };
24831
+ },
24832
+ onCoordinatorStart: (goal) => {
24833
+ if (!autonomousCoordinator) {
24834
+ console.error("[coordinator] not ready \u2014 no director yet (spawn a subagent first)");
24835
+ return;
24836
+ }
24837
+ autonomousCoordinator.run({ goal: goal ?? "" }).catch((err) => {
24838
+ console.error("[coordinator] run() failed:", err);
24839
+ });
24840
+ },
24841
+ onCoordinatorStop: () => {
24842
+ autonomousCoordinator?.stop();
24843
+ },
23700
24844
  // /clear: signal the TUI to wipe entries and reset fleet/leader stats
23701
24845
  // AND bump the context chip version — so the display reflects a
23702
24846
  // completely fresh session after the backend has been cleared.
@@ -23714,7 +24858,7 @@ async function execute(deps) {
23714
24858
  agentsMonitorController,
23715
24859
  getLiveSessions: async () => {
23716
24860
  const { SessionRegistry } = await import('@wrongstack/core');
23717
- const globalRoot = path39.dirname(wpaths.globalConfig);
24861
+ const globalRoot = path4.dirname(wpaths.globalConfig);
23718
24862
  const registry = new SessionRegistry(globalRoot);
23719
24863
  const sessions = await registry.list();
23720
24864
  return sessions.filter((s) => s.status !== "stale").map((s) => ({
@@ -23843,7 +24987,7 @@ async function execute(deps) {
23843
24987
  if (!sessionStore) return null;
23844
24988
  try {
23845
24989
  const { SessionRegistry } = await import('@wrongstack/core');
23846
- const registry = new SessionRegistry(path39.dirname(wpaths.globalConfig));
24990
+ const registry = new SessionRegistry(path4.dirname(wpaths.globalConfig));
23847
24991
  const live = (await registry.list()).find(
23848
24992
  (s) => s.sessionId === sessionId && s.status !== "stale" && s.pid !== process.pid
23849
24993
  );
@@ -23981,7 +25125,7 @@ async function execute(deps) {
23981
25125
  if (slug === "new-session") {
23982
25126
  pendingProjectSwitch = {
23983
25127
  root: projectRoot,
23984
- name: path39.basename(projectRoot) || projectRoot
25128
+ name: path4.basename(projectRoot) || projectRoot
23985
25129
  };
23986
25130
  }
23987
25131
  return;
@@ -24035,7 +25179,7 @@ ${parts.join("\n")}
24035
25179
  },
24036
25180
  // `wrongstack quick` sets flags.quick — open the F3 agents monitor by default.
24037
25181
  initialAgentsMonitorOpen: !!flags.quick,
24038
- tokenSavingMode: config.features.tokenSavingMode,
25182
+ tokenSavingMode: normalizeTokenSavingTier(config.features.tokenSavingMode) !== "off",
24039
25183
  toolCount: agent.tools.list().length
24040
25184
  });
24041
25185
  if (code === PROJECT_SWITCH_EXIT_CODE && pendingProjectSwitch) {
@@ -24047,8 +25191,8 @@ ${parts.join("\n")}
24047
25191
  try {
24048
25192
  const req2 = createRequire8(import.meta.url);
24049
25193
  const pkgPath = req2.resolve("@wrongstack/cli/package.json");
24050
- const pkgDir = path39.dirname(pkgPath);
24051
- cliPath = path39.join(pkgDir, "dist", "index.js");
25194
+ const pkgDir = path4.dirname(pkgPath);
25195
+ cliPath = path4.join(pkgDir, "dist", "index.js");
24052
25196
  await fsp5.access(cliPath);
24053
25197
  } catch {
24054
25198
  cliPath = process.argv[1] ?? "";
@@ -24080,6 +25224,7 @@ ${parts.join("\n")}
24080
25224
  }
24081
25225
  } finally {
24082
25226
  renderer.setSilent(false);
25227
+ offDirectorSpawned();
24083
25228
  }
24084
25229
  } else if (flags.webui) {
24085
25230
  agent.disableInteractiveConfirmation();
@@ -24108,6 +25253,23 @@ ${parts.join("\n")}
24108
25253
  modeStore,
24109
25254
  modeId,
24110
25255
  needsSetup,
25256
+ // Print the "open this" banner only once the server is actually
25257
+ // listening, using the RESOLVED ports. The requested port
25258
+ // (flags.port) auto-advances past busy ports inside runWebUI, so a
25259
+ // banner printed up-front with flags.port lies whenever 3456/3457 are
25260
+ // taken (a second instance, leftover sockets). Bind is 127.0.0.1-only,
25261
+ // so the host must be the literal IPv4 loopback — `localhost` resolves
25262
+ // to `::1` first on Windows and never reaches the server.
25263
+ onListening: ({ httpPort: boundHttpPort }) => {
25264
+ renderer.writeInfo(
25265
+ color.green(
25266
+ ` \u2726 WebUI running \u2192 ${color.bold(`http://127.0.0.1:${boundHttpPort}`)}`
25267
+ )
25268
+ );
25269
+ renderer.writeInfo(
25270
+ color.dim(" Press Ctrl+C in this terminal to stop the WebUI server.\n")
25271
+ );
25272
+ },
24111
25273
  // Make autonomy.switch from the browser flip the CLI's real
24112
25274
  // autonomy mode — context.meta alone never reaches the run loop.
24113
25275
  onAutonomySwitch: (mode) => {
@@ -24116,12 +25278,6 @@ ${parts.join("\n")}
24116
25278
  }
24117
25279
  }
24118
25280
  });
24119
- renderer.writeInfo(
24120
- color.green(
24121
- ` \u2726 WebUI running \u2192 ${color.bold(`http://localhost:${Number.parseInt(String(flags.port ?? "3457"), 10)}`)}`
24122
- )
24123
- );
24124
- renderer.writeInfo(color.dim(" Press Ctrl+C in this terminal to stop the WebUI server.\n"));
24125
25281
  const webuiExit = new Promise((resolve11) => {
24126
25282
  const onSigint = () => {
24127
25283
  renderer.setSilent(false);
@@ -24156,7 +25312,7 @@ ${parts.join("\n")}
24156
25312
  supportsVision,
24157
25313
  attachments,
24158
25314
  effectiveMaxContext,
24159
- projectName: path39.basename(projectRoot) || void 0,
25315
+ projectName: path4.basename(projectRoot) || void 0,
24160
25316
  getAutonomy,
24161
25317
  onAutonomy,
24162
25318
  getNextPredict,
@@ -24194,6 +25350,7 @@ ${parts.join("\n")}
24194
25350
  }
24195
25351
  } finally {
24196
25352
  fleetStatusLine?.stop();
25353
+ deps.onCoordinatorStop?.();
24197
25354
  try {
24198
25355
  stats.render(renderer);
24199
25356
  } catch (_err) {
@@ -24378,7 +25535,7 @@ var MultiAgentHost = class _MultiAgentHost {
24378
25535
  doneCondition: { type: "all_tasks_done" },
24379
25536
  maxConcurrent: this.opts.maxConcurrent ?? 4
24380
25537
  };
24381
- const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path39.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
25538
+ const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path4.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
24382
25539
  this.director = new Director({
24383
25540
  config: coordinatorConfig,
24384
25541
  manifestPath: this.opts.manifestPath,
@@ -24577,6 +25734,8 @@ var MultiAgentHost = class _MultiAgentHost {
24577
25734
  tokenCounter: this.deps.tokenCounter,
24578
25735
  cwd: subCwd,
24579
25736
  projectRoot: this.deps.projectRoot,
25737
+ // Subagents inherit the leader's filesystem-access scope.
25738
+ allowOutsideProjectRoot: config.features?.allowOutsideProjectRoot ?? !(config.tools?.restrictToProjectRoot ?? false),
24580
25739
  model: effModel,
24581
25740
  tools: this.filterTools(tools),
24582
25741
  // Distinct mailbox identity: without these, every subagent fell back
@@ -24935,16 +26094,16 @@ var MultiAgentHost = class _MultiAgentHost {
24935
26094
  if (this.director) return this.director;
24936
26095
  this.opts.directorMode = true;
24937
26096
  if (this.opts.fleetRoot && !this.opts.manifestPath) {
24938
- this.opts.manifestPath = path39.join(this.opts.fleetRoot, "fleet.json");
26097
+ this.opts.manifestPath = path4.join(this.opts.fleetRoot, "fleet.json");
24939
26098
  }
24940
26099
  if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
24941
- this.opts.sharedScratchpadPath = path39.join(this.opts.fleetRoot, "shared");
26100
+ this.opts.sharedScratchpadPath = path4.join(this.opts.fleetRoot, "shared");
24942
26101
  }
24943
26102
  if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
24944
- this.opts.sessionsRoot = path39.join(this.opts.fleetRoot, "subagents");
26103
+ this.opts.sessionsRoot = path4.join(this.opts.fleetRoot, "subagents");
24945
26104
  }
24946
26105
  if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
24947
- this.opts.stateCheckpointPath = path39.join(this.opts.fleetRoot, "director-state.json");
26106
+ this.opts.stateCheckpointPath = path4.join(this.opts.fleetRoot, "director-state.json");
24948
26107
  }
24949
26108
  await this.ensureDirector();
24950
26109
  return this.director ?? null;
@@ -25073,11 +26232,11 @@ var SessionStats = class {
25073
26232
  if (tool.name === "bash") this.bashCommands++;
25074
26233
  else if (tool.name === "fetch") this.fetches++;
25075
26234
  if (!tool.ok) return;
25076
- const path40 = typeof input?.path === "string" ? input.path : void 0;
25077
- if (tool.name === "read" && path40) this.readPaths.add(path40);
25078
- else if (tool.name === "edit" && path40) this.editedPaths.add(path40);
25079
- else if (tool.name === "write" && path40) {
25080
- this.writtenPaths.add(path40);
26235
+ const path39 = typeof input?.path === "string" ? input.path : void 0;
26236
+ if (tool.name === "read" && path39) this.readPaths.add(path39);
26237
+ else if (tool.name === "edit" && path39) this.editedPaths.add(path39);
26238
+ else if (tool.name === "write" && path39) {
26239
+ this.writtenPaths.add(path39);
25081
26240
  const content = typeof input?.content === "string" ? input.content : "";
25082
26241
  this.bytesWritten += Buffer.byteLength(content, "utf8");
25083
26242
  }
@@ -25389,7 +26548,7 @@ async function setupCodebaseIndexing(deps) {
25389
26548
  if (tool?.mutating && FILE_EDIT_TOOLS.has(tool.name)) {
25390
26549
  const fp = payload.toolUse.input?.file_path;
25391
26550
  if (typeof fp === "string" && fp.length > 0) {
25392
- const abs = path39.resolve(payload.ctx.cwd, fp);
26551
+ const abs = path4.resolve(payload.ctx.cwd, fp);
25393
26552
  if (isIndexableFile(abs)) {
25394
26553
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
25395
26554
  }
@@ -25408,7 +26567,7 @@ async function setupCodebaseIndexing(deps) {
25408
26567
  if (!filename) return;
25409
26568
  const rel = filename.toString();
25410
26569
  if (isIgnored(rel)) return;
25411
- const abs = path39.resolve(projectRoot, rel);
26570
+ const abs = path4.resolve(projectRoot, rel);
25412
26571
  if (!isIndexableFile(abs)) return;
25413
26572
  enqueueReindex({ projectRoot, files: [abs], debounceMs, onError });
25414
26573
  });
@@ -25462,7 +26621,7 @@ function setupMetrics(params) {
25462
26621
  const dumpMetrics = () => {
25463
26622
  if (!metricsSink) return;
25464
26623
  try {
25465
- const out = path39.join(wpaths.projectSessions, "metrics.json");
26624
+ const out = path4.join(wpaths.projectSessions, "metrics.json");
25466
26625
  const snap = metricsSink.snapshot();
25467
26626
  writeFileSync(out, JSON.stringify(snap, null, 2));
25468
26627
  } catch {
@@ -25486,7 +26645,7 @@ function setupMetrics(params) {
25486
26645
  });
25487
26646
  } catch (err) {
25488
26647
  logger.warn(
25489
- `metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`
26648
+ `metrics endpoint failed to start: ${toErrorMessage(err)}`
25490
26649
  );
25491
26650
  }
25492
26651
  }
@@ -25542,9 +26701,11 @@ async function setupCompaction(params) {
25542
26701
  context.meta["contextWindowMode"] = initialPolicy.id;
25543
26702
  context.meta["contextWindowPolicy"] = initialPolicy;
25544
26703
  let autoCompactor;
25545
- if (config.context.autoCompact !== false && effectiveMaxContext > 0) {
26704
+ let resolvedBridge;
26705
+ if (effectiveMaxContext > 0) {
25546
26706
  const auditLevel = resolveAuditLevel(fullConfig ?? config);
25547
26707
  const sessionBridge = providedBridge ?? createSessionEventBridge(sessionWriter, auditLevel);
26708
+ resolvedBridge = sessionBridge;
25548
26709
  autoCompactor = new AutoCompactionMiddleware(
25549
26710
  compactor,
25550
26711
  effectiveMaxContext,
@@ -25568,9 +26729,10 @@ async function setupCompaction(params) {
25568
26729
  sessionBridge
25569
26730
  }
25570
26731
  );
26732
+ autoCompactor.setEnabled(config.context.autoCompact !== false);
25571
26733
  pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
25572
26734
  }
25573
- return { effectiveMaxContext, autoCompactor };
26735
+ return { effectiveMaxContext, autoCompactor, sessionBridge: resolvedBridge };
25574
26736
  }
25575
26737
  function createAgent(params) {
25576
26738
  const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
@@ -25676,7 +26838,7 @@ async function setupSession(params) {
25676
26838
  `Resumed session ${resumed.data.metadata.id} \u2014 ${restoredMessages.length} messages, ${restoredToolCalls.length} tool executions, ${resumed.data.usage.input + resumed.data.usage.output} tokens used previously.`
25677
26839
  );
25678
26840
  } catch (err) {
25679
- renderer.writeError(`Resume failed: ${err instanceof Error ? err.message : String(err)}`);
26841
+ renderer.writeError(`Resume failed: ${toErrorMessage(err)}`);
25680
26842
  throw Object.assign(new Error("RESUME_FAILED"), { exitCode: 2 });
25681
26843
  }
25682
26844
  } else {
@@ -25699,7 +26861,7 @@ async function setupSession(params) {
25699
26861
  );
25700
26862
  });
25701
26863
  const attachments = new DefaultAttachmentStore({
25702
- spoolDir: path39.join(wpaths.projectSessions, session?.id, "attachments")
26864
+ spoolDir: path4.join(wpaths.projectSessions, session?.id, "attachments")
25703
26865
  });
25704
26866
  const ctxSignal = new AbortController().signal;
25705
26867
  const traceId = randomBytes(16).toString("hex");
@@ -25711,6 +26873,11 @@ async function setupSession(params) {
25711
26873
  tokenCounter,
25712
26874
  cwd,
25713
26875
  projectRoot,
26876
+ // Filesystem-access scope: derived from features.allowOutsideProjectRoot,
26877
+ // falling back to the old tools.restrictToProjectRoot config key (inverted:
26878
+ // restrict=false in old config → allow=true in new). Togglable live via
26879
+ // `/settings` ("Allow outside project").
26880
+ allowOutsideProjectRoot: config.features?.allowOutsideProjectRoot ?? !(config.tools?.restrictToProjectRoot ?? false),
25714
26881
  model: config.model,
25715
26882
  agentId: "leader",
25716
26883
  agentName: "Leader Agent",
@@ -25722,11 +26889,11 @@ async function setupSession(params) {
25722
26889
  };
25723
26890
  if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
25724
26891
  const queueStore = new QueueStore({
25725
- dir: path39.join(wpaths.projectSessions, session?.id),
26892
+ dir: path4.join(wpaths.projectSessions, session?.id),
25726
26893
  ...eventsBus ? { events: eventsBus } : {},
25727
26894
  ...traceId ? { traceId } : {}
25728
26895
  });
25729
- const todosCheckpointPath = path39.join(wpaths.projectSessions, `${session?.id}.todos.json`);
26896
+ const todosCheckpointPath = path4.join(wpaths.projectSessions, `${session?.id}.todos.json`);
25730
26897
  if (resumeId) {
25731
26898
  try {
25732
26899
  const restoredTodos = await loadTodosCheckpoint(
@@ -25750,15 +26917,15 @@ async function setupSession(params) {
25750
26917
  eventsBus,
25751
26918
  traceId
25752
26919
  );
25753
- const planPath = path39.join(wpaths.projectSessions, `${session?.id}.plan.json`);
26920
+ const planPath = path4.join(wpaths.projectSessions, `${session?.id}.plan.json`);
25754
26921
  context.state.setMeta("plan.path", planPath);
25755
- const taskPath = path39.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
26922
+ const taskPath = path4.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
25756
26923
  context.state.setMeta("task.path", taskPath);
25757
26924
  let dirState;
25758
26925
  if (resumeId) {
25759
26926
  try {
25760
- const fleetRoot = path39.join(wpaths.projectSessions, session?.id);
25761
- dirState = await loadDirectorState(path39.join(fleetRoot, "director-state.json"));
26927
+ const fleetRoot = path4.join(wpaths.projectSessions, session?.id);
26928
+ dirState = await loadDirectorState(path4.join(fleetRoot, "director-state.json"));
25762
26929
  if (dirState) {
25763
26930
  const tCounts = {};
25764
26931
  for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
@@ -26005,7 +27172,7 @@ async function main(argv) {
26005
27172
  projectGoal: wpaths.projectGoal,
26006
27173
  projectSessions: wpaths.projectSessions
26007
27174
  },
26008
- pathJoiner: { join: (a, b) => path39.join(a, b) },
27175
+ pathJoiner: { join: (a, b) => path4.join(a, b) },
26009
27176
  systemPromptBuilderToken: TOKENS.SystemPromptBuilder
26010
27177
  });
26011
27178
  const toolRegistry = new ToolRegistry();
@@ -26034,7 +27201,10 @@ async function main(argv) {
26034
27201
  const evOn = (event, handler) => {
26035
27202
  events.on(event, handler);
26036
27203
  teardownHandlers.push(
26037
- () => events.off(event, handler)
27204
+ () => (
27205
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic event dispatcher signature
27206
+ events.off(event, handler)
27207
+ )
26038
27208
  );
26039
27209
  };
26040
27210
  evOn("provider.response", (e) => {
@@ -26097,6 +27267,49 @@ async function main(argv) {
26097
27267
  writeErr(color.red(` \u2717 ${p.description}
26098
27268
  `));
26099
27269
  });
27270
+ const cliClientId = `cli@${randomBytes(4).toString("hex")}`;
27271
+ let cliToolCalls = 0;
27272
+ let cliInputTokens = 0;
27273
+ let cliOutputTokens = 0;
27274
+ let cliCacheTokens = 0;
27275
+ let cliCostUsd = 0;
27276
+ const emitClientStatus = () => {
27277
+ events.emit("client.status", {
27278
+ clientType: "cli",
27279
+ clientId: cliClientId,
27280
+ projectHash: wpaths.projectSlug,
27281
+ agentCount: 1,
27282
+ model: config.model,
27283
+ mode: activeMode?.id ?? "off",
27284
+ toolCalls: cliToolCalls,
27285
+ inputTokens: cliInputTokens,
27286
+ outputTokens: cliOutputTokens,
27287
+ cacheTokens: cliCacheTokens,
27288
+ costUsd: cliCostUsd,
27289
+ timestamp: Date.now(),
27290
+ projectSlug: wpaths.projectSlug
27291
+ });
27292
+ };
27293
+ evOn("tool.executed", () => {
27294
+ cliToolCalls++;
27295
+ emitClientStatus();
27296
+ });
27297
+ evOn("provider.response", (e) => {
27298
+ if (e.usage) {
27299
+ cliInputTokens = e.usage.input ?? cliInputTokens;
27300
+ cliOutputTokens = e.usage.output ?? cliOutputTokens;
27301
+ cliCacheTokens = (e.usage.cacheRead ?? 0) + (e.usage.cacheWrite ?? 0);
27302
+ }
27303
+ emitClientStatus();
27304
+ });
27305
+ evOn("token.accounted", (e) => {
27306
+ cliCostUsd = e.cost.total;
27307
+ emitClientStatus();
27308
+ });
27309
+ evOn("iteration.completed", () => {
27310
+ emitClientStatus();
27311
+ });
27312
+ emitClientStatus();
26100
27313
  const promptBuilder = container.resolve(TOKENS.SystemPromptBuilder);
26101
27314
  let onlineAgents = [];
26102
27315
  try {
@@ -26140,10 +27353,10 @@ async function main(argv) {
26140
27353
  memoryStore.withTraceId(sessResult.traceId);
26141
27354
  let tracker;
26142
27355
  try {
26143
- const { getSessionRegistry, AgentStatusTracker } = await import('@wrongstack/core');
27356
+ const { getSessionRegistry, AgentStatusTracker, FleetNotifier } = await import('@wrongstack/core');
26144
27357
  const registry = getSessionRegistry(wpaths.globalRoot);
26145
- const projectSlug2 = path39.basename(wpaths.projectDir);
26146
- const projectName = path39.basename(projectRoot);
27358
+ const projectSlug2 = path4.basename(wpaths.projectDir);
27359
+ const projectName = path4.basename(projectRoot);
26147
27360
  let gitBranch;
26148
27361
  try {
26149
27362
  const { execSync } = await import('child_process');
@@ -26163,13 +27376,22 @@ async function main(argv) {
26163
27376
  projectName,
26164
27377
  workingDir: context.workingDir,
26165
27378
  gitBranch,
27379
+ // The TUI and the REPL both boot through cli-main; `tuiOwnsScreen`
27380
+ // distinguishes the surface so the Fleet HQ map can label this session.
27381
+ clientType: tuiOwnsScreen ? "tui" : "cli",
26166
27382
  pid: process.pid,
26167
27383
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
26168
27384
  });
26169
- tracker = new AgentStatusTracker({ events, registry });
27385
+ const fleetNotifier = new FleetNotifier({
27386
+ baseDir: wpaths.globalRoot,
27387
+ projectRoot,
27388
+ selfPid: process.pid
27389
+ });
27390
+ tracker = new AgentStatusTracker({ events, registry, onUpdate: () => fleetNotifier.notify() });
26170
27391
  tracker.start();
26171
27392
  const cleanup = async () => {
26172
27393
  try {
27394
+ fleetNotifier.dispose();
26173
27395
  await registry.markClosing();
26174
27396
  tracker?.stop();
26175
27397
  } catch {
@@ -26242,7 +27464,7 @@ async function main(argv) {
26242
27464
  if (e.ok && (e.name === "write" || e.name === "edit" || e.name === "replace" || e.name === "patch")) {
26243
27465
  const filePath = e.input?.path;
26244
27466
  if (filePath) {
26245
- const projectDir = path39.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
27467
+ const projectDir = path4.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
26246
27468
  void recordFileAction(
26247
27469
  { storageDir: projectDir, projectRoot },
26248
27470
  {
@@ -26372,7 +27594,7 @@ async function main(argv) {
26372
27594
  toolRegistry,
26373
27595
  events,
26374
27596
  log: logger,
26375
- lazyMode: config.features.tokenSavingMode
27597
+ lazyMode: normalizeTokenSavingTier(config.features.tokenSavingMode) !== "off"
26376
27598
  });
26377
27599
  if (config.features.mcp) {
26378
27600
  for (const cfg of Object.values(config.mcpServers ?? {})) {
@@ -26408,7 +27630,7 @@ async function main(argv) {
26408
27630
  let depWatcherDispose;
26409
27631
  if (dwCfg?.["enabled"] === true) {
26410
27632
  try {
26411
- const projectDir = path39.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
27633
+ const projectDir = path4.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
26412
27634
  const dwMailbox = new GlobalMailbox(projectDir, events);
26413
27635
  depWatcherDispose = attachDepWatcherBridge({
26414
27636
  events,
@@ -26507,12 +27729,12 @@ async function main(argv) {
26507
27729
  }
26508
27730
  }
26509
27731
  };
26510
- const fleetRoot = directorMode ? path39.join(wpaths.projectSessions, session.id) : void 0;
26511
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path39.join(expectDefined(fleetRoot), "fleet.json") : void 0;
26512
- const sharedScratchpadPath = directorMode ? path39.join(expectDefined(fleetRoot), "shared") : void 0;
26513
- const subagentSessionsRoot = directorMode ? path39.join(expectDefined(fleetRoot), "subagents") : void 0;
26514
- const stateCheckpointPath = directorMode ? path39.join(expectDefined(fleetRoot), "director-state.json") : void 0;
26515
- const fleetRootForPromotion = path39.join(wpaths.projectSessions, session.id);
27732
+ const fleetRoot = directorMode ? path4.join(wpaths.projectSessions, session.id) : void 0;
27733
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path4.join(expectDefined(fleetRoot), "fleet.json") : void 0;
27734
+ const sharedScratchpadPath = directorMode ? path4.join(expectDefined(fleetRoot), "shared") : void 0;
27735
+ const subagentSessionsRoot = directorMode ? path4.join(expectDefined(fleetRoot), "subagents") : void 0;
27736
+ const stateCheckpointPath = directorMode ? path4.join(expectDefined(fleetRoot), "director-state.json") : void 0;
27737
+ const fleetRootForPromotion = path4.join(wpaths.projectSessions, session.id);
26516
27738
  const brainSettings = {
26517
27739
  maxAutoRisk: "medium"
26518
27740
  };
@@ -26655,7 +27877,7 @@ async function main(argv) {
26655
27877
  let techStackConsumerDispose;
26656
27878
  if (dwCfg?.["enabled"] === true) {
26657
27879
  try {
26658
- const projectDir = path39.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
27880
+ const projectDir = path4.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
26659
27881
  const tsMailbox = new GlobalMailbox(projectDir, events);
26660
27882
  const fileAuthorOpts = {
26661
27883
  storageDir: projectDir,
@@ -26688,7 +27910,7 @@ async function main(argv) {
26688
27910
  let pkgOutdatedDispose;
26689
27911
  if (dwCfg?.["enabled"] === true) {
26690
27912
  try {
26691
- const projectDir = path39.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
27913
+ const projectDir = path4.join(wpaths.globalRoot, "projects", wpaths.projectSlug);
26692
27914
  const pkgMailbox = new GlobalMailbox(projectDir, events);
26693
27915
  const pkgTrackerOpts = {
26694
27916
  storageDir: projectDir,
@@ -27062,7 +28284,7 @@ ${color.dim("\u2500".repeat(40))}` : "";
27062
28284
  return director.spawn(cfg);
27063
28285
  },
27064
28286
  onFleetLog: async (subagentId, mode) => {
27065
- const subagentsRoot = path39.join(fleetRootForPromotion, "subagents");
28287
+ const subagentsRoot = path4.join(fleetRootForPromotion, "subagents");
27066
28288
  let runDirs;
27067
28289
  try {
27068
28290
  runDirs = await fsp5.readdir(subagentsRoot);
@@ -27071,7 +28293,7 @@ ${color.dim("\u2500".repeat(40))}` : "";
27071
28293
  }
27072
28294
  const found = [];
27073
28295
  for (const runId of runDirs) {
27074
- const runDir = path39.join(subagentsRoot, runId);
28296
+ const runDir = path4.join(subagentsRoot, runId);
27075
28297
  let files;
27076
28298
  try {
27077
28299
  files = await fsp5.readdir(runDir);
@@ -27080,7 +28302,7 @@ ${color.dim("\u2500".repeat(40))}` : "";
27080
28302
  }
27081
28303
  for (const f of files) {
27082
28304
  if (!f.endsWith(".jsonl")) continue;
27083
- const full = path39.join(runDir, f);
28305
+ const full = path4.join(runDir, f);
27084
28306
  try {
27085
28307
  const stat7 = await fsp5.stat(full);
27086
28308
  found.push({
@@ -27181,7 +28403,7 @@ ${color.dim("\u2500".repeat(40))}` : "";
27181
28403
  }
27182
28404
  const dir = await multiAgentHost.ensureDirector();
27183
28405
  if (!dir) return "Director is not available.";
27184
- const dirStatePath = path39.join(fleetRootForPromotion, "director-state.json");
28406
+ const dirStatePath = path4.join(fleetRootForPromotion, "director-state.json");
27185
28407
  const prior = await loadDirectorState(dirStatePath);
27186
28408
  if (!prior) {
27187
28409
  return "No prior director-state.json found \u2014 nothing to retry.";
@@ -27250,9 +28472,9 @@ ${color.dim("\u2500".repeat(40))}` : "";
27250
28472
  for (const tool of director2.tools(FLEET_ROSTER)) {
27251
28473
  toolRegistry.register(tool);
27252
28474
  }
27253
- const mp = path39.join(fleetRootForPromotion, "fleet.json");
27254
- const sp = path39.join(fleetRootForPromotion, "shared");
27255
- const ss = path39.join(fleetRootForPromotion, "subagents");
28475
+ const mp = path4.join(fleetRootForPromotion, "fleet.json");
28476
+ const sp = path4.join(fleetRootForPromotion, "shared");
28477
+ const ss = path4.join(fleetRootForPromotion, "subagents");
27256
28478
  const lines = [
27257
28479
  `${color.green("\u2713")} Promoted to director mode.`,
27258
28480
  ` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
@@ -27553,6 +28775,10 @@ Restart WrongStack to load or unload plugin code in this session.`;
27553
28775
  tokenCounter,
27554
28776
  config,
27555
28777
  configStore,
28778
+ // Project-scoped mailbox — the AutonomousCoordinator in execution.ts
28779
+ // subscribes to it so goals/tasks/knowledge are shared with other
28780
+ // terminals working on the same project.
28781
+ mailbox: brainMailbox,
27556
28782
  renderer,
27557
28783
  reader,
27558
28784
  session,
@@ -27592,6 +28818,41 @@ Restart WrongStack to load or unload plugin code in this session.`;
27592
28818
  return autonomyMode;
27593
28819
  },
27594
28820
  getNextPredict: () => nextPredictEnabled,
28821
+ applyLiveSettings: (s) => {
28822
+ try {
28823
+ if (s.yolo !== void 0) {
28824
+ container.resolve(TOKENS.PermissionPolicy).setYolo?.(s.yolo);
28825
+ config = patchConfig(config, { yolo: s.yolo });
28826
+ }
28827
+ if (s.nextPrediction !== void 0) {
28828
+ nextPredictEnabled = s.nextPrediction;
28829
+ config = patchConfig(config, { nextPrediction: s.nextPrediction });
28830
+ }
28831
+ if (s.enhanceEnabled !== void 0) {
28832
+ enhanceController?.setEnabled(s.enhanceEnabled);
28833
+ }
28834
+ if (s.maxIterations !== void 0) {
28835
+ agent.maxIterations = s.maxIterations;
28836
+ }
28837
+ if (s.logLevel !== void 0) {
28838
+ container.resolve(TOKENS.Logger).level = s.logLevel;
28839
+ }
28840
+ if (s.auditLevel !== void 0) {
28841
+ sessionBridge.setAuditLevel(s.auditLevel);
28842
+ }
28843
+ if (s.contextAutoCompact !== void 0) {
28844
+ autoCompactor?.setEnabled(s.contextAutoCompact);
28845
+ }
28846
+ if (s.restrictFsToRoot !== void 0) {
28847
+ context.allowOutsideProjectRoot = !s.restrictFsToRoot;
28848
+ config = patchConfig(config, {
28849
+ features: { ...config.features, allowOutsideProjectRoot: !s.restrictFsToRoot },
28850
+ tools: { ...config.tools, restrictToProjectRoot: s.restrictFsToRoot }
28851
+ });
28852
+ }
28853
+ } catch {
28854
+ }
28855
+ },
27595
28856
  onSuggestionsParsed: (suggestions) => {
27596
28857
  currentSuggestions = suggestions ?? [];
27597
28858
  setSuggestions(suggestions ?? []);
@@ -27674,7 +28935,9 @@ Reply YES to auto-proceed, NO to wait for human input.`
27674
28935
  getBrainLog: () => brainLog,
27675
28936
  // Clean up SessionStats event listeners and all EventBus handlers when the REPL exits.
27676
28937
  onDestroy: () => {
27677
- teardownHandlers.forEach((fn) => fn());
28938
+ teardownHandlers.forEach((fn) => {
28939
+ fn();
28940
+ });
27678
28941
  stats.destroy(events);
27679
28942
  }
27680
28943
  });