@wrongstack/cli 0.272.2 → 0.273.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, DefaultSecretScrubber, projectHash, wstackGlobalRoot, TOKENS, ToolRegistry, GlobalMailbox, 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, buildTranscriptFromEvents, DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveProjectDir, recentTextTurns, gatedEnhancerReasoning, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, resolveProviderModelList, DefaultLogger, DefaultModelsRegistry, isStdinTTY, atomicWrite, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, createHqPublisherFromEnv, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, applyModelRuntime, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, AdaptiveConcurrencyController, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, WIDE_SUBAGENT_CAPABILITIES, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, 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, noOpVault, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, readHqRuntimeFileSync, resolveHqConfig, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, InputBuilder, DefaultSecretVault, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, HQ_AUTH_FILE_VERSION, FsError, ERROR_CODES, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, ConfigError, resolveMailboxIdentity, AutonomousCoordinator, mutateHqAuthFile, mintHqToken, readHqAuthFile, DefaultTokenCounter, isSecretField as isSecretField$1, startSessionTelemetryBridge } from '@wrongstack/core';
2
+ import { color, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, DefaultSecretScrubber, resolveWstackPaths, projectHash, wstackGlobalRoot, TOKENS, ToolRegistry, GlobalMailbox, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, createFallbackModelExtension, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, SddRunRegistry, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, buildTranscriptFromEvents, DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveProjectDir, recentTextTurns, gatedEnhancerReasoning, enhanceUserPrompt, projectSlug, DefaultSystemPromptBuilder, mutateTasks, loadTasks, resolveContextWindowPolicy, repairToolUseAdjacency, mutatePlan, setPlanItemStatus, getPlanTemplate, loadPlan, emptyPlan, addPlanItem, savePlan, resolveProviderModelList, DefaultLogger, DefaultModelsRegistry, isStdinTTY, atomicWrite, DefaultPathResolver, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, makeAutonomyPromptContributor, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, applyToolDescriptionModes, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, createHqPublisherFromEnv, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, createDefaultPipelines, applyModelRuntime, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, AdaptiveConcurrencyController, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, WIDE_SUBAGENT_CAPABILITIES, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, assignNickname, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, setQueuedMessagesSnapshot, DefaultSessionRewinder, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, ToolAuditLog, DefaultSessionStore as DefaultSessionStore$1, ProviderRegistry, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, getContextWindowMode, AGENT_CATALOG, dispatchAgent, normalizeToolDescriptionMode, setToolDescriptionMode, getToolDescriptionMode, noOpVault, formatTodosList, formatTaskList, formatTaskProgress, formatPlan, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, readHqRuntimeFileSync, resolveHqConfig, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, onResize, InputBuilder, DefaultSecretVault, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, HQ_AUTH_FILE_VERSION, FsError, ERROR_CODES, truncate, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, ConfigError, resolveMailboxIdentity, smartDefaultFallbackChain, AutonomousCoordinator, mutateHqAuthFile, mintHqToken, readHqAuthFile, DefaultTokenCounter, isSecretField as isSecretField$1, startSessionTelemetryBridge } from '@wrongstack/core';
3
3
  import * as fs4 from 'fs/promises';
4
4
  import { decryptConfigSecrets, encryptConfigSecrets, DefaultSecretVault as DefaultSecretVault$1, isSecretField } from '@wrongstack/core/security';
5
5
  import * as path4 from 'path';
@@ -8,11 +8,11 @@ import * as crypto2 from 'crypto';
8
8
  import { createHash, randomUUID, randomBytes } from 'crypto';
9
9
  import * as http from 'http';
10
10
  import { createServer } from 'http';
11
- import * as os from 'os';
12
- import os__default from 'os';
11
+ import * as os7 from 'os';
12
+ import os7__default from 'os';
13
13
  import { WebSocketServer, WebSocket } from 'ws';
14
14
  import { createRequire } from 'module';
15
- import { findFreePort, AutoPhaseWebSocketHandler, WorktreeWebSocketHandler, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleGitDiff, handleGitChanges, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMcpRestart, handleMcpDisable, handleMcpEnable, handleMcpDiscover, handleMcpSleep, handleMcpWake, handleMcpUpdate, handleMcpRemove, handleMcpAdd, handleMcpList, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleCompletionRequest, createToolLspCompletionSource, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser as openBrowser$1, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
15
+ import { findFreePort, AutoPhaseWebSocketHandler, WorktreeWebSocketHandler, SpecsWebSocketHandler, SddBoardWebSocketHandler, SddWizardWebSocketHandler, buildSddWizardDeps, generateAuthToken, verifyClient, handleGitInfo, handleShellOpen, handleGitDiff, handleGitChanges, handleSkillsExport, handleSkillsEdit, handleSkillsCreate, handleSkillsUpdate, handleSkillsUninstall, handleSkillsInstall, handleSkillsContent, handleMcpRestart, handleMcpDisable, handleMcpEnable, handleMcpDiscover, handleMcpSleep, handleMcpWake, handleMcpUpdate, handleMcpRemove, handleMcpAdd, handleMcpList, handleMemoryForget, handleMemoryRemember, handleMemoryList, handleCompletionRequest, createToolLspCompletionSource, handleFilesWrite, handleFilesRead, handleFilesTree, handleFilesList, createEternalSubscription, createHttpServer, registerInstance, openBrowser as openBrowser$1, unregisterInstance, estimateTokens as estimateTokens$1, stringifyContent, messagePreview, messageTokens, createCustomModeStore } from '@wrongstack/webui/server';
16
16
  import { toErrorMessage, readJsonObjectFile, updateJsonObjectFile, setJsonPath, jsonObjectFileExists, removeJsonPath } from '@wrongstack/core/utils';
17
17
  import { setOAuthTokenPersister, makeProviderFromConfig, capabilitiesFor, buildProviderFactoriesFromRegistry, refreshCopilotToken, copilotBaseUrlFromToken } from '@wrongstack/providers';
18
18
  import { getProcessRegistry, builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes, shutdownCodebaseIndexHost, TIER2_TOOLS, TIER3_TOOLS, TIER1_TOOLS, resetIndexCircuitBreaker } from '@wrongstack/tools';
@@ -657,26 +657,26 @@ function fmtDuration(ms) {
657
657
  const remMin = m - h * 60;
658
658
  return `${h}h${remMin}m`;
659
659
  }
660
- function fmtTaskResultLine(r, color85) {
660
+ function fmtTaskResultLine(r, color86) {
661
661
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
662
662
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
663
663
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
664
664
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
665
- const errKindChip = errKind ? color85.dim(` [${errKind}]`) : "";
666
- const errSnip = errMsg || errKind ? `${errKindChip}${color85.dim(errTail)}` : "";
665
+ const errKindChip = errKind ? color86.dim(` [${errKind}]`) : "";
666
+ const errSnip = errMsg || errKind ? `${errKindChip}${color86.dim(errTail)}` : "";
667
667
  switch (r.status) {
668
668
  case "success":
669
- return { mark: color85.green("\u2713"), stats, tail: "" };
669
+ return { mark: color86.green("\u2713"), stats, tail: "" };
670
670
  case "timeout":
671
671
  return {
672
- mark: color85.yellow("\u23F1"),
673
- stats: `${color85.yellow("timeout")} ${stats}`,
672
+ mark: color86.yellow("\u23F1"),
673
+ stats: `${color86.yellow("timeout")} ${stats}`,
674
674
  tail: errSnip
675
675
  };
676
676
  case "stopped":
677
- return { mark: color85.dim("\u2298"), stats: `${color85.dim("stopped")} ${stats}`, tail: errSnip };
677
+ return { mark: color86.dim("\u2298"), stats: `${color86.dim("stopped")} ${stats}`, tail: errSnip };
678
678
  case "failed":
679
- return { mark: color85.red("\u2717"), stats: `${color85.red("failed")} ${stats}`, tail: errSnip };
679
+ return { mark: color86.red("\u2717"), stats: `${color86.red("failed")} ${stats}`, tail: errSnip };
680
680
  }
681
681
  }
682
682
  var init_utils = __esm({
@@ -1335,6 +1335,19 @@ function sddHelp() {
1335
1335
  " \u2502 /sdd cancel Cancel session \u2502",
1336
1336
  " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
1337
1337
  "",
1338
+ " \u250C\u2500 \u26A1 Parallel Run \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
1339
+ " \u2502 /sdd parallel [N] Run tasks in parallel (N slots) \u2502",
1340
+ " \u2502 /sdd stop Stop the active parallel run \u2502",
1341
+ " \u2502 /sdd retry-failed Requeue all failed tasks \u2502",
1342
+ " \u2502 /sdd split <id> <A ; B> Split a task into sub-tasks \u2502",
1343
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
1344
+ "",
1345
+ " \u250C\u2500 \u{1F9F9} Lifecycle (after /sdd stop) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
1346
+ " \u2502 /sdd clean Remove run worktrees + branches \u2502",
1347
+ " \u2502 /sdd rollback Revert the run's merged commits \u2502",
1348
+ " \u2502 /sdd destroy Delete the whole SDD project \u2502",
1349
+ " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
1350
+ "",
1338
1351
  " \u250C\u2500 \u{1F4C1} Spec History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
1339
1352
  " \u2502 /sdd list List saved specs \u2502",
1340
1353
  " \u2502 /sdd show <id> Show spec details \u2502",
@@ -1626,9 +1639,92 @@ Start executing the tasks one by one.`
1626
1639
  );
1627
1640
  return { message };
1628
1641
  }
1629
- case "stop": {
1642
+ case "stop":
1643
+ case "abort": {
1630
1644
  opts.onSddParallelStop?.();
1631
- return { message: "SDD parallel run stopped." };
1645
+ return { message: "SDD parallel run stopped. Use /sdd clean to remove worktrees, /sdd rollback to undo commits, or /sdd destroy to delete the project." };
1646
+ }
1647
+ case "clean":
1648
+ case "cleanup":
1649
+ case "worktrees": {
1650
+ if (!opts.onSddCleanWorktrees) {
1651
+ return { message: "Worktree cleanup is not available in this session." };
1652
+ }
1653
+ const removed = await opts.onSddCleanWorktrees();
1654
+ return {
1655
+ message: removed > 0 ? `Cleaned ${removed} SDD worktree${removed === 1 ? "" : "s"} (and their wstack/ap branches).` : "No SDD worktrees to clean."
1656
+ };
1657
+ }
1658
+ case "rollback":
1659
+ case "revert": {
1660
+ if (!opts.onSddRollback) {
1661
+ return { message: "Rollback is not available in this session." };
1662
+ }
1663
+ const res = await opts.onSddRollback();
1664
+ if (res.ok) {
1665
+ return {
1666
+ message: res.reverted > 0 ? `Rolled back ${res.reverted} run commit${res.reverted === 1 ? "" : "s"} (revert commits added \u2014 history preserved).` : "Nothing to roll back."
1667
+ };
1668
+ }
1669
+ return {
1670
+ message: `Rollback failed${res.reverted ? ` after ${res.reverted} revert(s)` : ""}: ${res.reason ?? "unknown error"}`
1671
+ };
1672
+ }
1673
+ case "destroy":
1674
+ case "nuke": {
1675
+ if (!opts.onSddDestroy) {
1676
+ return { message: "Destroy is not available in this session." };
1677
+ }
1678
+ const res = await opts.onSddDestroy();
1679
+ const builder = sddState.getBuilder();
1680
+ if (builder) {
1681
+ await builder.deleteSession().catch(() => {
1682
+ });
1683
+ sddState.setBuilder(null);
1684
+ }
1685
+ sddState.clearTaskState();
1686
+ const parts = [
1687
+ `SDD project destroyed.`,
1688
+ res.worktreesRemoved > 0 ? ` Removed ${res.worktreesRemoved} worktree${res.worktreesRemoved === 1 ? "" : "s"}.` : "",
1689
+ res.deleted.length ? ` Deleted: ${res.deleted.join(", ")}.` : "",
1690
+ ` (Merged commits left intact \u2014 use /sdd rollback to undo them.)`
1691
+ ].filter(Boolean);
1692
+ return { message: parts.join("\n") };
1693
+ }
1694
+ case "retry-failed":
1695
+ case "retry-all": {
1696
+ if (!opts.onSddRetryAllFailed) {
1697
+ return { message: "No active SDD parallel run to retry." };
1698
+ }
1699
+ const n = opts.onSddRetryAllFailed();
1700
+ return {
1701
+ message: n > 0 ? `Requeued ${n} failed task${n === 1 ? "" : "s"} to pending.` : "No failed tasks to retry."
1702
+ };
1703
+ }
1704
+ case "split": {
1705
+ if (!opts.onSddSplitTask) {
1706
+ return { message: "No active SDD parallel run to split a task in." };
1707
+ }
1708
+ const taskId = restArgs[0];
1709
+ if (!taskId) {
1710
+ return { message: "Usage: /sdd split <task-id> <subtask ; subtask ; \u2026>" };
1711
+ }
1712
+ const subtasks = restArgs.slice(1).join(" ").split(";").map((s) => s.trim()).filter(Boolean).map((piece) => {
1713
+ const [title, ...rest] = piece.split("::");
1714
+ const t = (title ?? "").trim();
1715
+ const d = rest.join("::").trim();
1716
+ return { title: t, description: d || t };
1717
+ }).filter((s) => s.title);
1718
+ if (subtasks.length < 2) {
1719
+ return { message: "Provide at least two sub-tasks: /sdd split <task-id> <A ; B>" };
1720
+ }
1721
+ const ids = opts.onSddSplitTask(taskId, subtasks);
1722
+ if (ids === null) {
1723
+ return { message: `No active run, or task "${taskId}" is unknown / running (can't split).` };
1724
+ }
1725
+ return {
1726
+ message: `Split ${taskId} into ${ids.length} sub-task${ids.length === 1 ? "" : "s"}: ${ids.join(", ")}`
1727
+ };
1632
1728
  }
1633
1729
  case "plan":
1634
1730
  case "impl": {
@@ -2117,20 +2213,25 @@ Start executing the tasks one by one.`
2117
2213
  return { message: lines.join("\n") };
2118
2214
  }
2119
2215
  case "cancel": {
2120
- const sessionPath = opts.paths.projectSddSession;
2121
2216
  let deletedFromDisk = false;
2122
- try {
2123
- await fs4.unlink(sessionPath);
2124
- deletedFromDisk = true;
2125
- } catch {
2126
- }
2127
- try {
2128
- await fs4.rm(opts.paths.projectSpecs, { recursive: true, force: true });
2129
- } catch {
2130
- }
2131
- try {
2132
- await fs4.rm(opts.paths.projectTaskGraphs, { recursive: true, force: true });
2133
- } catch {
2217
+ if (opts.onSddDestroy) {
2218
+ const res = await opts.onSddDestroy();
2219
+ deletedFromDisk = res.deleted.length > 0 || res.worktreesRemoved > 0;
2220
+ } else {
2221
+ const sessionPath = opts.paths.projectSddSession;
2222
+ try {
2223
+ await fs4.unlink(sessionPath);
2224
+ deletedFromDisk = true;
2225
+ } catch {
2226
+ }
2227
+ try {
2228
+ await fs4.rm(opts.paths.projectSpecs, { recursive: true, force: true });
2229
+ } catch {
2230
+ }
2231
+ try {
2232
+ await fs4.rm(opts.paths.projectTaskGraphs, { recursive: true, force: true });
2233
+ } catch {
2234
+ }
2134
2235
  }
2135
2236
  const cancelBuilder = sddState.getBuilder();
2136
2237
  if (cancelBuilder) {
@@ -4108,7 +4209,7 @@ async function clearHqRuntimeMarker(dataDir, url) {
4108
4209
  function lanIPv4Addresses() {
4109
4210
  const out = [];
4110
4211
  try {
4111
- const ifaces = os.networkInterfaces();
4212
+ const ifaces = os7.networkInterfaces();
4112
4213
  for (const name of Object.keys(ifaces)) {
4113
4214
  for (const ni of ifaces[name] ?? []) {
4114
4215
  if (ni.family === "IPv4" && !ni.internal) out.push(ni.address);
@@ -4210,7 +4311,7 @@ async function handleApiSessions(res) {
4210
4311
  }
4211
4312
  }
4212
4313
  async function handleApiSessionEvents(res, sessionId, limit, full, transcripts) {
4213
- const { SessionRegistry, resolveWstackPaths: resolveWstackPaths5, DefaultSessionStore: DefaultSessionStore5 } = await import('@wrongstack/core');
4314
+ const { SessionRegistry, resolveWstackPaths: resolveWstackPaths6, DefaultSessionStore: DefaultSessionStore5 } = await import('@wrongstack/core');
4214
4315
  const globalRoot = path4.dirname(resolveHqDataDir());
4215
4316
  try {
4216
4317
  const registry = new SessionRegistry(globalRoot);
@@ -4221,7 +4322,7 @@ async function handleApiSessionEvents(res, sessionId, limit, full, transcripts)
4221
4322
  let clientType;
4222
4323
  let projectName;
4223
4324
  if (entry) {
4224
- const paths = resolveWstackPaths5({ projectRoot: entry.projectRoot, globalRoot });
4325
+ const paths = resolveWstackPaths6({ projectRoot: entry.projectRoot, globalRoot });
4225
4326
  const store = new DefaultSessionStore5({ dir: paths.projectSessions });
4226
4327
  const data = await store.load(sessionId).catch(() => null);
4227
4328
  if (data) {
@@ -5117,7 +5218,7 @@ async function getUpdateNotification(signal, homeFn) {
5117
5218
  var defaultHomeDir2, CACHE_TTL_MS;
5118
5219
  var init_update_check = __esm({
5119
5220
  "src/update-check.ts"() {
5120
- defaultHomeDir2 = () => os.homedir();
5221
+ defaultHomeDir2 = () => os7.homedir();
5121
5222
  CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5122
5223
  }
5123
5224
  });
@@ -5223,6 +5324,8 @@ async function seedConfigToMeta(opts) {
5223
5324
  meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
5224
5325
  meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
5225
5326
  meta["nextPrediction"] = cfg.nextPrediction ?? false;
5327
+ meta["fallbackModels"] = cfg.fallbackModels ?? [];
5328
+ meta["fallbackAuto"] = cfg.fallbackAuto !== false;
5226
5329
  meta["featureMcp"] = features["mcp"] !== false;
5227
5330
  meta["featurePlugins"] = features["plugins"] !== false;
5228
5331
  meta["featureMemory"] = features["memory"] !== false;
@@ -5269,8 +5372,7 @@ function createPrefsSeeding(opts) {
5269
5372
  const decrypted = decryptConfigSecrets$1(parsed, vault);
5270
5373
  const autonomy = decrypted.autonomy ?? {};
5271
5374
  if ("autonomy" in payload) autonomy["defaultMode"] = payload["autonomy"];
5272
- if ("autonomyDelayMs" in payload)
5273
- autonomy["autoProceedDelayMs"] = payload["autonomyDelayMs"];
5375
+ if ("autonomyDelayMs" in payload) autonomy["autoProceedDelayMs"] = payload["autonomyDelayMs"];
5274
5376
  if ("autoProceedMaxIterations" in payload)
5275
5377
  autonomy["autoProceedMaxIterations"] = payload["autoProceedMaxIterations"];
5276
5378
  if ("yolo" in payload) {
@@ -5282,9 +5384,10 @@ function createPrefsSeeding(opts) {
5282
5384
  if ("streamFleet" in payload) autonomy["streamFleet"] = payload["streamFleet"];
5283
5385
  if ("enhanceEnabled" in payload) autonomy["enhance"] = payload["enhanceEnabled"];
5284
5386
  if ("enhanceDelayMs" in payload) autonomy["enhanceDelayMs"] = payload["enhanceDelayMs"];
5285
- if ("enhanceLanguage" in payload)
5286
- autonomy["enhanceLanguage"] = payload["enhanceLanguage"];
5387
+ if ("enhanceLanguage" in payload) autonomy["enhanceLanguage"] = payload["enhanceLanguage"];
5287
5388
  if ("nextPrediction" in payload) decrypted["nextPrediction"] = payload["nextPrediction"];
5389
+ if ("fallbackModels" in payload) decrypted["fallbackModels"] = payload["fallbackModels"];
5390
+ if ("fallbackAuto" in payload) decrypted["fallbackAuto"] = payload["fallbackAuto"];
5288
5391
  decrypted["autonomy"] = autonomy;
5289
5392
  if ("featureMcp" in payload || "featurePlugins" in payload || "featureMemory" in payload || "featureSkills" in payload || "featureModelsRegistry" in payload) {
5290
5393
  const features = decrypted.features ?? {};
@@ -5380,6 +5483,8 @@ var init_prefs_seeding = __esm({
5380
5483
  "confirmExit",
5381
5484
  "streamFleet",
5382
5485
  "nextPrediction",
5486
+ "fallbackModels",
5487
+ "fallbackAuto",
5383
5488
  "enhanceEnabled",
5384
5489
  "enhanceDelayMs",
5385
5490
  "enhanceLanguage",
@@ -6413,22 +6518,29 @@ var init_projects = __esm({
6413
6518
  function sendResult7(ctx, ws, success, message) {
6414
6519
  ctx.send(ws, { type: "key.operation_result", payload: { success, message } });
6415
6520
  }
6521
+ function projectSavedProviders(providers) {
6522
+ return Object.entries(providers).map(([id, cfg]) => {
6523
+ const models = cfg.models;
6524
+ const pickedModelId = models && models.length > 0 ? models[0] : void 0;
6525
+ return {
6526
+ id,
6527
+ family: cfg.family ?? id,
6528
+ baseUrl: cfg.baseUrl,
6529
+ models,
6530
+ ...pickedModelId !== void 0 ? { pickedModelId } : {},
6531
+ apiKeys: normalizeKeys(cfg).map((k) => ({
6532
+ label: k.label,
6533
+ maskedKey: maskedKey(k.apiKey),
6534
+ isActive: k.label === cfg.activeKey,
6535
+ createdAt: k.createdAt
6536
+ }))
6537
+ };
6538
+ });
6539
+ }
6416
6540
  function broadcastSaved(ctx, providers) {
6417
6541
  ctx.broadcast({
6418
6542
  type: "providers.saved",
6419
- payload: {
6420
- providers: Object.entries(providers).map(([id, cfg]) => ({
6421
- id,
6422
- family: cfg.family,
6423
- baseUrl: cfg.baseUrl,
6424
- apiKeys: normalizeKeys(cfg).map((k) => ({
6425
- label: k.label,
6426
- maskedKey: maskedKey(k.apiKey),
6427
- isActive: k.label === cfg.activeKey,
6428
- createdAt: k.createdAt
6429
- }))
6430
- }))
6431
- }
6543
+ payload: { providers: projectSavedProviders(providers) }
6432
6544
  });
6433
6545
  }
6434
6546
  async function handleProvidersList(ctx, ws) {
@@ -6484,19 +6596,7 @@ async function handleProvidersSaved(ctx, ws) {
6484
6596
  const providers = await ctx.providerStore.load();
6485
6597
  ctx.send(ws, {
6486
6598
  type: "providers.saved",
6487
- payload: {
6488
- providers: Object.entries(providers).map(([id, cfg]) => ({
6489
- id,
6490
- family: cfg.family,
6491
- baseUrl: cfg.baseUrl,
6492
- apiKeys: normalizeKeys(cfg).map((k) => ({
6493
- label: k.label,
6494
- maskedKey: maskedKey(k.apiKey),
6495
- isActive: k.label === cfg.activeKey,
6496
- createdAt: k.createdAt
6497
- }))
6498
- }))
6499
- }
6599
+ payload: { providers: projectSavedProviders(providers) }
6500
6600
  });
6501
6601
  } catch (err) {
6502
6602
  sendResult7(ctx, ws, false, toErrorMessage(err));
@@ -6518,6 +6618,7 @@ async function handleKeyUpsert(ctx, ws, providerId, label, apiKey) {
6518
6618
  providers[providerId] = existing;
6519
6619
  await ctx.providerStore.save(providers);
6520
6620
  sendResult7(ctx, ws, true, `Key "${label}" saved for ${providerId}`);
6621
+ broadcastSaved(ctx, providers);
6521
6622
  } catch (err) {
6522
6623
  sendResult7(ctx, ws, false, toErrorMessage(err));
6523
6624
  }
@@ -6542,6 +6643,7 @@ async function handleKeyDelete(ctx, ws, providerId, label) {
6542
6643
  }
6543
6644
  await ctx.providerStore.save(providers);
6544
6645
  sendResult7(ctx, ws, true, `Key "${label}" deleted from ${providerId}`);
6646
+ broadcastSaved(ctx, providers);
6545
6647
  } catch (err) {
6546
6648
  sendResult7(ctx, ws, false, toErrorMessage(err));
6547
6649
  }
@@ -6559,6 +6661,7 @@ async function handleKeySetActive(ctx, ws, providerId, label) {
6559
6661
  providers[providerId] = existing;
6560
6662
  await ctx.providerStore.save(providers);
6561
6663
  sendResult7(ctx, ws, true, `Active key for ${providerId} set to "${label}"`);
6664
+ broadcastSaved(ctx, providers);
6562
6665
  } catch (err) {
6563
6666
  sendResult7(ctx, ws, false, toErrorMessage(err));
6564
6667
  }
@@ -6588,22 +6691,7 @@ async function handleProviderAdd(ctx, ws, payload) {
6588
6691
  await ctx.providerStore.save(providers);
6589
6692
  sendResult7(ctx, ws, true, `Provider "${payload.id}" added`);
6590
6693
  ctx.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
6591
- ctx.broadcast({
6592
- type: "providers.saved",
6593
- payload: {
6594
- providers: Object.entries(providers).map(([id, cfg]) => ({
6595
- id,
6596
- family: cfg.family,
6597
- baseUrl: cfg.baseUrl,
6598
- apiKeys: normalizeKeys(cfg).map((k) => ({
6599
- label: k.label,
6600
- maskedKey: maskedKey(k.apiKey),
6601
- isActive: k.label === cfg.activeKey,
6602
- createdAt: k.createdAt
6603
- }))
6604
- }))
6605
- }
6606
- });
6694
+ broadcastSaved(ctx, providers);
6607
6695
  } catch (err) {
6608
6696
  sendResult7(ctx, ws, false, toErrorMessage(err));
6609
6697
  }
@@ -6618,6 +6706,7 @@ async function handleProviderRemove(ctx, ws, providerId) {
6618
6706
  delete providers[providerId];
6619
6707
  await ctx.providerStore.save(providers);
6620
6708
  sendResult7(ctx, ws, true, `Provider "${providerId}" removed`);
6709
+ broadcastSaved(ctx, providers);
6621
6710
  } catch (err) {
6622
6711
  sendResult7(ctx, ws, false, toErrorMessage(err));
6623
6712
  }
@@ -7235,7 +7324,7 @@ async function runWebUI(opts) {
7235
7324
  })();
7236
7325
  return customModeStoreP;
7237
7326
  };
7238
- const autoPhaseStoreDir = opts.projectRoot ? path4.join(opts.projectRoot, ".wrongstack", "autophase") : path4.join(os.tmpdir(), ".wrongstack", "autophase");
7327
+ const autoPhaseStoreDir = opts.projectRoot ? path4.join(opts.projectRoot, ".wrongstack", "autophase") : path4.join(os7.tmpdir(), ".wrongstack", "autophase");
7239
7328
  const autoPhaseHandler = new AutoPhaseWebSocketHandler(
7240
7329
  opts.agent,
7241
7330
  opts.agent.ctx,
@@ -7245,6 +7334,29 @@ async function runWebUI(opts) {
7245
7334
  opts.projectRoot
7246
7335
  );
7247
7336
  const worktreeHandler = new WorktreeWebSocketHandler(opts.events, consoleLogger);
7337
+ const specsPaths = opts.projectRoot ? resolveWstackPaths({ projectRoot: opts.projectRoot }) : null;
7338
+ const specsHandler = new SpecsWebSocketHandler(
7339
+ specsPaths?.projectSpecs ?? path4.join(os7.tmpdir(), ".wrongstack", "specs"),
7340
+ specsPaths?.projectTaskGraphs ?? path4.join(os7.tmpdir(), ".wrongstack", "task-graphs")
7341
+ );
7342
+ const sddBoardHandler = new SddBoardWebSocketHandler(
7343
+ specsPaths?.projectSddBoards ?? path4.join(os7.tmpdir(), ".wrongstack", "sdd-boards"),
7344
+ opts.events
7345
+ );
7346
+ const sddWizardHandler = opts.sddSubagentFactory && specsPaths ? new SddWizardWebSocketHandler(
7347
+ buildSddWizardDeps({
7348
+ agent: opts.agent,
7349
+ events: opts.events,
7350
+ projectRoot: opts.projectRoot ?? process.cwd(),
7351
+ subagentFactory: opts.sddSubagentFactory,
7352
+ paths: {
7353
+ projectSpecs: specsPaths.projectSpecs,
7354
+ projectTaskGraphs: specsPaths.projectTaskGraphs,
7355
+ projectSddBoards: specsPaths.projectSddBoards,
7356
+ projectDir: specsPaths.projectDir
7357
+ }
7358
+ })
7359
+ ) : null;
7248
7360
  await seedConfigToMeta(opts);
7249
7361
  const { prefSnapshot, persistPrefs } = createPrefsSeeding(opts);
7250
7362
  const sessionStartedAt = Date.now();
@@ -8241,6 +8353,9 @@ ${text}` : text;
8241
8353
  const client = { ws, sessionId: opts.session.id };
8242
8354
  clients.set(ws, client);
8243
8355
  autoPhaseHandler.addClient(ws);
8356
+ specsHandler.addClient(ws);
8357
+ sddBoardHandler.addClient(ws);
8358
+ sddWizardHandler?.addClient(ws);
8244
8359
  worktreeHandler.addClient(ws);
8245
8360
  let msgCount = 0;
8246
8361
  let windowResetAt = Date.now() + 6e4;
@@ -8845,7 +8960,11 @@ ${text}` : text;
8845
8960
  break;
8846
8961
  // ── Mailbox operations — project-level inter-agent messaging (PR 8 of #30) ────
8847
8962
  case "mailbox.messages":
8848
- await handleMailboxMessages(mailboxCtx, msg, ws);
8963
+ await handleMailboxMessages(
8964
+ mailboxCtx,
8965
+ msg,
8966
+ ws
8967
+ );
8849
8968
  break;
8850
8969
  case "mailbox.agents":
8851
8970
  await handleMailboxAgents(mailboxCtx, msg, ws);
@@ -8867,6 +8986,18 @@ ${text}` : text;
8867
8986
  await autoPhaseHandler.handleMessage(
8868
8987
  msg
8869
8988
  );
8989
+ } else if (msgType.startsWith("specs.")) {
8990
+ await specsHandler.handleMessage(
8991
+ msg
8992
+ );
8993
+ } else if (msgType.startsWith("sdd.board.")) {
8994
+ await sddBoardHandler.handleMessage(
8995
+ msg
8996
+ );
8997
+ } else if (msgType.startsWith("sdd.spec.") || msgType.startsWith("sdd.run.")) {
8998
+ await sddWizardHandler?.handleMessage(
8999
+ msg
9000
+ );
8870
9001
  } else {
8871
9002
  console.debug(`[WebUI] Unhandled message type: ${msgType}`);
8872
9003
  }
@@ -8918,6 +9049,12 @@ var init_webui_server = __esm({
8918
9049
 
8919
9050
  // src/cli-main.ts
8920
9051
  init_provider_config_utils();
9052
+ var DEFAULT_TASK_CONCURRENCY = 2;
9053
+ function resolveTaskConcurrency() {
9054
+ const raw = Number.parseInt(process.env["WRONGSTACK_AUTOPHASE_TASK_CONCURRENCY"] ?? "", 10);
9055
+ if (!Number.isFinite(raw)) return DEFAULT_TASK_CONCURRENCY;
9056
+ return Math.min(8, Math.max(1, raw));
9057
+ }
8921
9058
  var WORKTREE_PHASE_CONCURRENCY = 4;
8922
9059
  var MAX_CMD_OUTPUT = 2e5;
8923
9060
  function gitText(args, cwd) {
@@ -9131,6 +9268,7 @@ ${res.out}` };
9131
9268
  };
9132
9269
  }
9133
9270
  const abort = new AbortController();
9271
+ const usedNicknames = /* @__PURE__ */ new Set();
9134
9272
  log(`\u{1F9E0} Planning phases for: ${goal}`);
9135
9273
  let phases;
9136
9274
  try {
@@ -9180,9 +9318,16 @@ ${res.out}` };
9180
9318
  executeTask: async (task, phaseId, env) => {
9181
9319
  const phase = graph.phases.get(phaseId);
9182
9320
  const phaseName = phase?.name ?? phaseId;
9321
+ let agentName = task.assignee;
9322
+ if (!agentName) {
9323
+ const nick = assignNickname("executor", usedNicknames);
9324
+ usedNicknames.add(nick.key);
9325
+ agentName = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
9326
+ active?.orchestrator.setTaskAssignee(task.id, void 0, agentName);
9327
+ }
9183
9328
  return runOnce(
9184
9329
  buildTaskPrompt(task, phaseName, goal),
9185
- `autophase-${phaseName}-${task.title}`.slice(0, 48),
9330
+ `autophase-${agentName}`.slice(0, 48),
9186
9331
  abort.signal,
9187
9332
  env?.cwd
9188
9333
  );
@@ -9227,9 +9372,11 @@ ${res.out}` };
9227
9372
  // With isolation, parallelizable phases run concurrently; without it,
9228
9373
  // stay strictly sequential to protect the shared working tree.
9229
9374
  maxConcurrentPhases: worktrees ? deps.maxConcurrentPhases ?? WORKTREE_PHASE_CONCURRENCY : 1,
9230
- // Sequential within a phase: each todo is a full-tool agent and todos in
9231
- // a phase typically build on one another (they share the phase worktree).
9232
- maxConcurrentTasks: 1
9375
+ // Within a phase, todos share the phase worktree. Default to a small
9376
+ // amount of parallelism so multiple agents genuinely pick up different
9377
+ // tasks at once (visible on the board); raise/lower via
9378
+ // WRONGSTACK_AUTOPHASE_TASK_CONCURRENCY (1 = strictly sequential).
9379
+ maxConcurrentTasks: resolveTaskConcurrency()
9233
9380
  });
9234
9381
  const onUntyped = deps.events.on;
9235
9382
  const offUntyped = deps.events.off;
@@ -9284,6 +9431,30 @@ ${res.out}` };
9284
9431
  isRunning: () => a.orchestrator.isRunning()
9285
9432
  };
9286
9433
  },
9434
+ onAutoPhaseMoveTask(taskId, toPhaseId) {
9435
+ if (!active) return false;
9436
+ const ok = active.orchestrator.moveTask(taskId, toPhaseId);
9437
+ if (ok) void persist(active.graph);
9438
+ return ok;
9439
+ },
9440
+ onAutoPhaseAssignTask(taskId, agentId, agentName) {
9441
+ if (!active) return false;
9442
+ const ok = active.orchestrator.setTaskAssignee(taskId, agentId, agentName);
9443
+ if (ok) void persist(active.graph);
9444
+ return ok;
9445
+ },
9446
+ onAutoPhaseAddTask(phaseId, spec) {
9447
+ if (!active) return null;
9448
+ const id = active.orchestrator.addTask(phaseId, spec);
9449
+ if (id) void persist(active.graph);
9450
+ return id;
9451
+ },
9452
+ onAutoPhaseRetryTask(taskId) {
9453
+ if (!active) return false;
9454
+ const ok = active.orchestrator.requeueTask(taskId);
9455
+ if (ok) void persist(active.graph);
9456
+ return ok;
9457
+ },
9287
9458
  async onWorktree(action, target) {
9288
9459
  const root = deps.projectRoot;
9289
9460
  if (!await isGitRepo(root)) return "\u26A0 Not a git repository \u2014 worktrees unavailable.";
@@ -9855,7 +10026,7 @@ function printLaunchHints(renderer, flags, opts = {}) {
9855
10026
  renderer.write(`${lines.join("\n")}
9856
10027
  `);
9857
10028
  }
9858
- var defaultUidFn = () => os__default.userInfo().uid;
10029
+ var defaultUidFn = () => os7__default.userInfo().uid;
9859
10030
  async function getFileUid(filePath) {
9860
10031
  try {
9861
10032
  const stat8 = await fs4.stat(filePath);
@@ -9865,7 +10036,7 @@ async function getFileUid(filePath) {
9865
10036
  }
9866
10037
  }
9867
10038
  async function checkConfigOwnership(homeFn, uidFn = defaultUidFn) {
9868
- if (os__default.platform() === "win32") return true;
10039
+ if (os7__default.platform() === "win32") return true;
9869
10040
  const cfg = configPath(homeFn);
9870
10041
  const fileUid = await getFileUid(cfg);
9871
10042
  if (fileUid === void 0) return true;
@@ -9956,7 +10127,7 @@ function diffSummary(oldCfg, newCfg) {
9956
10127
  }
9957
10128
  return changes.length > 0 ? changes.slice(0, 5).join(", ") : "no changes";
9958
10129
  }
9959
- var defaultHomeDir = () => os__default.homedir();
10130
+ var defaultHomeDir = () => os7__default.homedir();
9960
10131
  function historyDir(homeFn = defaultHomeDir) {
9961
10132
  return path4.join(homeFn(), ".wrongstack", "config.history", "entries");
9962
10133
  }
@@ -10172,6 +10343,10 @@ async function restoreLast(homeFn = defaultHomeDir) {
10172
10343
  }
10173
10344
 
10174
10345
  // src/provider-helpers.ts
10346
+ function visibleModelIds(providerId, config, catalogModelIds, cfg) {
10347
+ const entry = cfg ?? config.providers?.[providerId];
10348
+ return entry?.models !== void 0 ? [...entry.models] : [...catalogModelIds];
10349
+ }
10175
10350
  function hasApiKey(provider, config) {
10176
10351
  if (provider.envVars.some((v) => !!process.env[v])) return true;
10177
10352
  const entry = config?.providers?.[provider.id];
@@ -10206,7 +10381,7 @@ async function buildPickableProviders(modelsRegistry, config) {
10206
10381
  const inherited = catalogById.get(catalogType);
10207
10382
  const family = cfg.family ?? inherited?.family ?? "unsupported";
10208
10383
  if (family === "unsupported") continue;
10209
- const models = cfg.models && cfg.models.length > 0 ? [...cfg.models] : (inherited?.models ?? []).map((m) => m.id);
10384
+ const models = visibleModelIds(id, config, (inherited?.models ?? []).map((m) => m.id), cfg);
10210
10385
  out.push({ id, family, models });
10211
10386
  }
10212
10387
  for (const p of catalog) {
@@ -10218,7 +10393,7 @@ async function buildPickableProviders(modelsRegistry, config) {
10218
10393
  return out;
10219
10394
  }
10220
10395
  var theme = { primary: color.amber };
10221
- async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os__default.homedir()) {
10396
+ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os7__default.homedir()) {
10222
10397
  try {
10223
10398
  const { atomicWrite: atomicWrite15 } = await import('@wrongstack/core');
10224
10399
  const fs40 = await import('fs/promises');
@@ -10301,7 +10476,7 @@ ${color.bold(theme.primary("WrongStack") + color.dim(" \u2014 Provider & Model S
10301
10476
  // know which models their endpoint actually serves (e.g. LM
10302
10477
  // Studio, vLLM, or a proxy with custom model ids). Otherwise the
10303
10478
  // catalog list keeps providing suggestions.
10304
- models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : p.models
10479
+ models: visibleModelIds(p.id, config ?? { providers: {} }, p.models.map((m) => m.id), cfg).map((m) => p.models.find((pm) => pm.id === m) ?? { id: m, name: m })
10305
10480
  });
10306
10481
  } else {
10307
10482
  merged.push(p);
@@ -10318,7 +10493,7 @@ ${color.bold(theme.primary("WrongStack") + color.dim(" \u2014 Provider & Model S
10318
10493
  family: cfg.family,
10319
10494
  apiBase: cfg.baseUrl ?? inherited?.apiBase,
10320
10495
  envVars: cfg.envVars ?? inherited?.envVars ?? [],
10321
- models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : inherited?.models ?? [],
10496
+ models: visibleModelIds(id, config ?? { providers: {} }, (inherited?.models ?? []).map((m) => m.id), cfg).map((m) => inherited?.models.find((pm) => pm.id === m) ?? { id: m, name: m }),
10322
10497
  npm: inherited?.npm
10323
10498
  });
10324
10499
  }
@@ -12885,133 +13060,6 @@ function stripSurroundingQuotes(s) {
12885
13060
  }
12886
13061
  return s;
12887
13062
  }
12888
- function parseModelRef(ref) {
12889
- const trimmed = ref.trim();
12890
- const slash = trimmed.indexOf("/");
12891
- if (slash !== -1) {
12892
- return {
12893
- provider: trimmed.slice(0, slash) || void 0,
12894
- model: trimmed.slice(slash + 1).trim()
12895
- };
12896
- }
12897
- const parts = trimmed.split(/\s+/);
12898
- if (parts.length >= 2) {
12899
- return { provider: parts[0], model: parts.slice(1).join(" ") };
12900
- }
12901
- return { model: trimmed };
12902
- }
12903
- function shouldFallback(err) {
12904
- if (err instanceof StreamHangError) {
12905
- return 599;
12906
- }
12907
- if (!(err instanceof ProviderError)) return null;
12908
- const s = err.status;
12909
- if (s === 0) return s;
12910
- if (s === 429 || s === 529 || s >= 500) return s;
12911
- return null;
12912
- }
12913
- function providerHasKey(entry) {
12914
- if (!entry) return false;
12915
- if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
12916
- if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
12917
- if (Array.isArray(entry.envVars) && entry.envVars.some((v) => !!process.env[v])) return true;
12918
- return false;
12919
- }
12920
- var SMART_DEFAULT_MAX = 4;
12921
- function smartDefaultFallbackChain(config) {
12922
- const leaderProvider = config.provider;
12923
- const leaderModel = config.model;
12924
- const providers = config.providers ?? {};
12925
- const seen = /* @__PURE__ */ new Set();
12926
- const sameProvider = [];
12927
- const crossProvider = [];
12928
- const ids = Object.keys(providers).sort(
12929
- (a, b) => a === leaderProvider ? -1 : b === leaderProvider ? 1 : a.localeCompare(b)
12930
- );
12931
- for (const id of ids) {
12932
- const entry = providers[id];
12933
- if (!providerHasKey(entry)) continue;
12934
- const models = entry?.models ?? [];
12935
- for (const model of models) {
12936
- if (id === leaderProvider && model === leaderModel) continue;
12937
- const ref = `${id}/${model}`;
12938
- if (seen.has(ref)) continue;
12939
- seen.add(ref);
12940
- (id === leaderProvider ? sameProvider : crossProvider).push(ref);
12941
- }
12942
- }
12943
- return [...sameProvider, ...crossProvider].slice(0, SMART_DEFAULT_MAX);
12944
- }
12945
- function effectiveFallbackChain(config) {
12946
- const explicit = config.fallbackModels ?? [];
12947
- if (explicit.length > 0) return explicit;
12948
- if (config.fallbackAuto === false) return [];
12949
- return smartDefaultFallbackChain(config);
12950
- }
12951
- function createFallbackModelExtension(deps) {
12952
- let dirty = false;
12953
- return {
12954
- name: "fallback-model",
12955
- beforeRun: async (ctx) => {
12956
- if (!dirty) return;
12957
- const cfg = deps.getConfig();
12958
- try {
12959
- ctx.provider = await deps.buildProvider(cfg.provider);
12960
- ctx.model = cfg.model;
12961
- await deps.onModelSwitch?.(cfg.provider, cfg.model);
12962
- } catch (err) {
12963
- deps.logger?.warn(
12964
- `fallback-model: could not restore primary "${cfg.provider}/${cfg.model}": ${err instanceof Error ? err.message : String(err)}`
12965
- );
12966
- }
12967
- dirty = false;
12968
- },
12969
- wrapProviderRunner: async (ctx, request, inner) => {
12970
- try {
12971
- return await inner(ctx, request);
12972
- } catch (firstErr) {
12973
- let lastErr = firstErr;
12974
- const cfg = deps.getConfig();
12975
- const chain = effectiveFallbackChain(cfg);
12976
- for (const ref of chain) {
12977
- const status = shouldFallback(lastErr);
12978
- if (status === null) break;
12979
- const parsed = parseModelRef(ref);
12980
- if (!parsed.model) continue;
12981
- const targetProviderId = parsed.provider ?? cfg.provider;
12982
- const from = { providerId: ctx.provider.id, model: ctx.model };
12983
- let nextProvider;
12984
- try {
12985
- nextProvider = await deps.buildProvider(targetProviderId);
12986
- } catch (err) {
12987
- deps.logger?.warn(
12988
- `fallback-model: skipping "${ref}" \u2014 cannot build provider "${targetProviderId}": ${err instanceof Error ? err.message : String(err)}`
12989
- );
12990
- continue;
12991
- }
12992
- const providerSwitched = nextProvider.id !== from.providerId;
12993
- ctx.provider = nextProvider;
12994
- ctx.model = parsed.model;
12995
- request.model = parsed.model;
12996
- dirty = true;
12997
- await deps.onModelSwitch?.(targetProviderId, parsed.model);
12998
- deps.events.emit("provider.fallback", {
12999
- from,
13000
- to: { providerId: nextProvider.id, model: parsed.model },
13001
- status,
13002
- providerSwitched
13003
- });
13004
- try {
13005
- return await inner(ctx, request);
13006
- } catch (err) {
13007
- lastErr = err;
13008
- }
13009
- }
13010
- throw lastErr;
13011
- }
13012
- }
13013
- };
13014
- }
13015
13063
 
13016
13064
  // src/slash-commands/fallback.ts
13017
13065
  async function patchGlobalConfig(globalConfigPath, mutate) {
@@ -14186,7 +14234,7 @@ function buildFleetCommand(opts) {
14186
14234
  // ── /fleet terminate <subagentId> ────────────────────────────────────
14187
14235
  case "terminate":
14188
14236
  case "stop":
14189
- return handleTerminate(opts, subargs);
14237
+ return await handleTerminate(opts, subargs);
14190
14238
  // ── /fleet spawn <role> [count] ──────────────────────────────────────
14191
14239
  case "spawn":
14192
14240
  case "add":
@@ -14357,7 +14405,7 @@ async function handleKill(opts, subargs) {
14357
14405
  return { message: msg2 };
14358
14406
  }
14359
14407
  if (opts.onFleetKill) {
14360
- const killed = opts.onFleetKill();
14408
+ const killed = await opts.onFleetKill();
14361
14409
  const msg2 = `${color.red("\u2717 Killed")} ${killed} subagent(s).`;
14362
14410
  opts.renderer.write(msg2);
14363
14411
  return { message: msg2 };
@@ -14366,7 +14414,7 @@ async function handleKill(opts, subargs) {
14366
14414
  opts.renderer.writeWarning(msg);
14367
14415
  return { message: msg };
14368
14416
  }
14369
- function handleTerminate(opts, subargs) {
14417
+ async function handleTerminate(opts, subargs) {
14370
14418
  const targetId = subargs[0];
14371
14419
  if (!targetId) {
14372
14420
  const msg2 = `${color.amber("\u26A0 /fleet terminate requires a subagentId.")} Use /fleet to see active ids.`;
@@ -14378,7 +14426,7 @@ function handleTerminate(opts, subargs) {
14378
14426
  opts.renderer.writeWarning(msg2);
14379
14427
  return { message: msg2 };
14380
14428
  }
14381
- const ok = opts.onFleetTerminate(targetId);
14429
+ const ok = await opts.onFleetTerminate(targetId);
14382
14430
  if (ok) {
14383
14431
  const msg2 = `${color.green("\u2713 Terminated")} subagent ${color.bold(targetId)}.`;
14384
14432
  opts.renderer.write(msg2);
@@ -15044,7 +15092,7 @@ function buildInterruptCommand(opts) {
15044
15092
  ].join("\n"),
15045
15093
  async run() {
15046
15094
  const aborted = opts.interruptController?.abortLeader() ?? false;
15047
- const killed = opts.onFleetKill?.() ?? 0;
15095
+ const killed = opts.onFleetKill ? await opts.onFleetKill() : 0;
15048
15096
  if (!aborted && killed === 0) {
15049
15097
  return { message: color.dim(" Nothing to interrupt \u2014 no run in progress.") };
15050
15098
  }
@@ -17262,7 +17310,7 @@ async function killSession(sessionId) {
17262
17310
  };
17263
17311
  }
17264
17312
  }
17265
- function providerHasKey2(entry) {
17313
+ function providerHasKey(entry) {
17266
17314
  if (!entry) return false;
17267
17315
  if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
17268
17316
  if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
@@ -17273,7 +17321,7 @@ function keyedProviderIds(config) {
17273
17321
  const ids = /* @__PURE__ */ new Set();
17274
17322
  if (config.provider) ids.add(config.provider);
17275
17323
  for (const [id, entry] of Object.entries(config.providers ?? {})) {
17276
- if (providerHasKey2(entry)) ids.add(id);
17324
+ if (providerHasKey(entry)) ids.add(id);
17277
17325
  }
17278
17326
  return [...ids].sort();
17279
17327
  }
@@ -17500,7 +17548,7 @@ function buildSetModelCommand(opts) {
17500
17548
  issues.push(
17501
17549
  `${color.red("\u2717")} ${color.amber(key)}: provider "${entry.provider}" is not configured`
17502
17550
  );
17503
- } else if (!providerHasKey2(provCfg2)) {
17551
+ } else if (!providerHasKey(provCfg2)) {
17504
17552
  warnings.push(
17505
17553
  `${color.amber("\u26A0")} ${color.amber(key)}: provider "${entry.provider}" has no API key`
17506
17554
  );
@@ -18311,7 +18359,7 @@ ${parts.join("\n")}
18311
18359
  return false;
18312
18360
  }
18313
18361
  if (fleetRunning > 0) {
18314
- const killed = opts.onFleetKill?.() ?? 0;
18362
+ const killed = opts.onFleetKill ? await opts.onFleetKill() : 0;
18315
18363
  if (killed > 0) {
18316
18364
  opts.renderer.write(color.dim(` Stopped ${killed} subagent(s).
18317
18365
  `));
@@ -20495,6 +20543,150 @@ function buildTodosCommand(opts) {
20495
20543
  }
20496
20544
  };
20497
20545
  }
20546
+ function fit(text, width) {
20547
+ if (text.length <= width) return text.padEnd(width);
20548
+ return `${text.slice(0, Math.max(0, width - 3))}...`;
20549
+ }
20550
+ function formatDescriptionMode(mode) {
20551
+ const raw = `desc:${mode}`;
20552
+ return mode === "simple" ? color.amber(raw) : color.cyan(raw);
20553
+ }
20554
+ function buildToolCommand(opts) {
20555
+ const help = [
20556
+ "Usage:",
20557
+ " /tool Show tool description-mode overrides",
20558
+ " /tool list List every tool and its description mode",
20559
+ " /tool <name> Show one tool mode",
20560
+ " /tool <name> simple Use a 1-2 line tool description",
20561
+ " /tool <name> extend Use the full/extended tool description",
20562
+ "",
20563
+ "Modes:",
20564
+ " simple Short top-level description and usage hint",
20565
+ " extend Full description (default)",
20566
+ "",
20567
+ "Examples:",
20568
+ " /tool read simple",
20569
+ " /tool bash extend"
20570
+ ].join("\n");
20571
+ function getCurrentTools() {
20572
+ return opts.configStore.get().tools;
20573
+ }
20574
+ function nextToolsConfig(name, mode) {
20575
+ const current = getCurrentTools();
20576
+ const descriptionMode = { ...current.descriptionMode ?? {} };
20577
+ if (mode === "simple") {
20578
+ descriptionMode[name] = "simple";
20579
+ } else {
20580
+ delete descriptionMode[name];
20581
+ }
20582
+ return { ...current, descriptionMode };
20583
+ }
20584
+ async function persistMode(name, mode) {
20585
+ const nextTools = nextToolsConfig(name, mode);
20586
+ if (!opts.paths) {
20587
+ opts.configStore.update({ tools: nextTools });
20588
+ return false;
20589
+ }
20590
+ await persistConfigSetting(
20591
+ {
20592
+ configStore: opts.configStore,
20593
+ globalConfigPath: opts.paths.globalConfig,
20594
+ inProjectConfigPath: opts.paths.inProjectConfig,
20595
+ vault: noOpVault
20596
+ },
20597
+ (cfg) => {
20598
+ cfg.tools = nextTools;
20599
+ }
20600
+ );
20601
+ return true;
20602
+ }
20603
+ function formatOverrides() {
20604
+ const configured = opts.configStore.get().tools.descriptionMode ?? {};
20605
+ const simple = Object.entries(configured).filter(([, mode]) => normalizeToolDescriptionMode(mode) === "simple").map(([name]) => name).sort();
20606
+ return [
20607
+ `${color.bold("Tool descriptions")} ${color.dim(`(default: desc:extend)`)}`,
20608
+ "",
20609
+ simple.length > 0 ? ` ${formatDescriptionMode("simple")}: ${simple.map((name) => color.cyan(name)).join(", ")}` : ` ${formatDescriptionMode("simple")}: ${color.dim("none")}`,
20610
+ "",
20611
+ color.dim(" /tool <name> simple \xB7 /tool <name> extend \xB7 /tool list")
20612
+ ].join("\n");
20613
+ }
20614
+ function formatList() {
20615
+ const header = ` ${color.dim(fit("tool", 28))} ${color.dim(fit("owner", 28))} ` + color.dim("description");
20616
+ const rows = opts.toolRegistry.listWithOwner().map(({ tool }) => {
20617
+ const mode = getToolDescriptionMode(opts.toolRegistry, tool.name);
20618
+ const owner = opts.toolRegistry.ownerOf(tool.name) ?? "core";
20619
+ return ` ${fit(tool.name, 28)} ${color.dim(fit(`[${owner}]`, 28))} ` + formatDescriptionMode(mode);
20620
+ });
20621
+ return [
20622
+ `${color.bold("Tool description modes")} ${color.dim("(default: desc:extend)")}`,
20623
+ "",
20624
+ header,
20625
+ ...rows
20626
+ ].join("\n");
20627
+ }
20628
+ function formatOne(name) {
20629
+ const tool = opts.toolRegistry.get(name);
20630
+ if (!tool) {
20631
+ return `${color.red("Unknown tool")}: ${name}. Use ${color.dim("/tools")} to list registered tools.`;
20632
+ }
20633
+ const mode = getToolDescriptionMode(opts.toolRegistry, name);
20634
+ return [
20635
+ `${color.bold(name)} description mode: ${formatDescriptionMode(mode)}`,
20636
+ "",
20637
+ color.dim(tool.description)
20638
+ ].join("\n");
20639
+ }
20640
+ return {
20641
+ name: "tool",
20642
+ category: "Config",
20643
+ description: "Set per-tool description detail: simple or extend.",
20644
+ argsHint: "[<name> simple|extend]",
20645
+ help,
20646
+ async run(args) {
20647
+ if (!opts.configStore) {
20648
+ return { message: `${color.red("Error")} config store not available.` };
20649
+ }
20650
+ const parts = args.trim().split(/\s+/).filter(Boolean);
20651
+ const sub = (parts[0] ?? "").toLowerCase();
20652
+ if (!sub) return { message: formatOverrides() };
20653
+ if (sub === "help" || sub === "--help") return { message: this.help ?? "" };
20654
+ if (sub === "list") return { message: formatList() };
20655
+ const name = parts[0] ?? "";
20656
+ const tool = opts.toolRegistry.get(name);
20657
+ if (!tool) {
20658
+ return {
20659
+ message: `${color.red("Unknown tool")}: ${name}. Use ${color.dim("/tools")} to list registered tools.`
20660
+ };
20661
+ }
20662
+ if (parts.length === 1) return { message: formatOne(name) };
20663
+ const mode = normalizeToolDescriptionMode(parts[1]);
20664
+ if (!mode) {
20665
+ return { message: `${color.amber("Usage:")} /tool ${name} simple|extend` };
20666
+ }
20667
+ try {
20668
+ const persisted = await persistMode(name, mode);
20669
+ setToolDescriptionMode(opts.toolRegistry, name, mode);
20670
+ const persistence = persisted ? color.dim("saved") : color.dim("runtime only; config paths unavailable");
20671
+ return {
20672
+ message: `${color.green("\u2713")} ${color.cyan(name)} description mode -> ${formatDescriptionMode(mode)} ${persistence}`
20673
+ };
20674
+ } catch (err) {
20675
+ return {
20676
+ message: `${color.red("Could not save tool setting")}: ${toErrorMessage(err)}`
20677
+ };
20678
+ }
20679
+ }
20680
+ };
20681
+ }
20682
+ function fit2(text, width) {
20683
+ if (text.length <= width) return text.padEnd(width);
20684
+ return `${text.slice(0, Math.max(0, width - 3))}...`;
20685
+ }
20686
+ function formatDescriptionMode2(mode) {
20687
+ const raw = `desc:${mode}`;
20688
+ return mode === "simple" ? color.amber(raw) : color.dim(raw);
20689
+ }
20498
20690
  function buildToolsCommand(opts) {
20499
20691
  return {
20500
20692
  name: "tools",
@@ -20502,10 +20694,14 @@ function buildToolsCommand(opts) {
20502
20694
  description: "List registered tools.",
20503
20695
  async run() {
20504
20696
  const all = opts.toolRegistry.listWithOwner();
20505
- const lines = all.map(
20506
- ({ tool, owner }) => ` ${tool.name.padEnd(28)} ${color.dim(`[${owner}]`)} ${tool.mutating ? color.yellow("mut") : color.cyan("ro")} ${color.dim(tool.permission)}`
20507
- );
20508
- const msg = `${color.bold("Tools")} (${all.length}):
20697
+ const header = ` ${color.dim(fit2("tool", 28))} ${color.dim(fit2("owner", 28))} ${color.dim(fit2("rw", 4))} ${color.dim(fit2("perm", 8))} ` + color.dim("description");
20698
+ const lines = all.map(({ tool, owner }) => {
20699
+ const mode = getToolDescriptionMode(opts.toolRegistry, tool.name);
20700
+ const rw = tool.mutating ? color.yellow(fit2("mut", 4)) : color.cyan(fit2("ro", 4));
20701
+ return ` ${fit2(tool.name, 28)} ${color.dim(fit2(`[${owner}]`, 28))} ${rw} ${color.dim(fit2(tool.permission, 8))} ` + formatDescriptionMode2(mode);
20702
+ });
20703
+ const msg = `${color.bold("Tools")} (${all.length}) ${color.dim("description detail via /tool <name> simple|extend")}:
20704
+ ${header}
20509
20705
  ${lines.join("\n")}
20510
20706
  `;
20511
20707
  opts.renderer.write(msg);
@@ -20688,6 +20884,9 @@ function buildYoloCommand(opts) {
20688
20884
  }
20689
20885
  };
20690
20886
  }
20887
+ var DEFAULT_SHADOW_PROVIDER = "anthropic";
20888
+ var DEFAULT_SHADOW_INTERVAL_MS = 3e4;
20889
+ var MIN_SHADOW_INTERVAL_MS = 5e3;
20691
20890
  function buildShadowCommand(opts) {
20692
20891
  return {
20693
20892
  name: "shadow",
@@ -20697,8 +20896,8 @@ function buildShadowCommand(opts) {
20697
20896
  argsHint: "<start|stop|status|hoop|model|interval> [--flag=value]",
20698
20897
  help: [
20699
20898
  "Usage:",
20700
- " /shadow start [--interval=<ms>] [--model=<provider/model>]",
20701
- " Start Shadow Agent with optional interval and model",
20899
+ " /shadow start [--model=<provider/model>]",
20900
+ " Run one quiet Shadow Agent fleet check",
20702
20901
  " /shadow stop",
20703
20902
  " Stop the Shadow Agent",
20704
20903
  " /shadow status",
@@ -20708,19 +20907,21 @@ function buildShadowCommand(opts) {
20708
20907
  " /shadow model <provider/model>",
20709
20908
  " Change Shadow Agent analysis model",
20710
20909
  " /shadow interval <ms>",
20711
- " Change heartbeat interval",
20910
+ " Change the legacy interval default kept for compatibility",
20712
20911
  "",
20713
20912
  "The Shadow Agent monitors:",
20714
20913
  " \u2022 All fleet agents and their current tasks",
20715
20914
  " \u2022 Mailbox state and message flow",
20716
20915
  " \u2022 Spike tasks (starting/stopping instantly)",
20717
20916
  " \u2022 Fleet-wide activity across all terminals",
20718
- " \u2022 Uses deterministic rules first, LLM only for complex cases",
20917
+ " \u2022 Uses deterministic host rules first; LLM only for manual/problem cases",
20918
+ " \u2022 Does not post routine healthy reports",
20719
20919
  "",
20720
20920
  "Model must be specified as provider/model, e.g. anthropic/claude-3-5-sonnet",
20921
+ "When omitted, Shadow uses the current leader provider/model.",
20721
20922
  "",
20722
20923
  "Examples:",
20723
- " /shadow start --interval=15000",
20924
+ " /shadow start",
20724
20925
  " /shadow start --model=anthropic/claude-3-5-sonnet",
20725
20926
  " /shadow status Show all agent activity",
20726
20927
  " /shadow hoop subagent-abc123 Stop agent and notify",
@@ -20733,14 +20934,24 @@ function buildShadowCommand(opts) {
20733
20934
  if (!opts.onSpawn) {
20734
20935
  return { message: "/shadow requires a running director with subagent support." };
20735
20936
  }
20736
- const flags = Object.fromEntries(
20737
- rest.filter((f) => f.startsWith("--")).map((f) => [f.slice(2).split("=")[0], f.slice(2).split("=")[1] ?? true])
20738
- );
20739
- const intervalMs = flags["interval"] ?? 3e4;
20740
- let model;
20741
- const modelRaw = flags["model"] ?? "default";
20937
+ const flags = parseFlags2(rest);
20938
+ const shadowDefaults = opts.shadowController?.getDefaults?.();
20939
+ let intervalMs;
20940
+ try {
20941
+ intervalMs = parseInterval(
20942
+ flags["interval"] ?? String(shadowDefaults?.intervalMs ?? DEFAULT_SHADOW_INTERVAL_MS)
20943
+ );
20944
+ } catch (e) {
20945
+ return { message: `/shadow start: ${e.message}` };
20946
+ }
20947
+ const defaultModelRef = getDefaultModelRef(opts);
20948
+ let modelRef;
20949
+ const modelRaw = flags["model"];
20950
+ if (modelRaw === true) {
20951
+ return { message: "/shadow start: --model requires a provider/model value" };
20952
+ }
20742
20953
  try {
20743
- model = requireProviderModelFormat(modelRaw);
20954
+ modelRef = parseProviderModelRef(modelRaw ?? "default", defaultModelRef);
20744
20955
  } catch (e) {
20745
20956
  return { message: `/shadow start: ${e.message}` };
20746
20957
  }
@@ -20755,10 +20966,10 @@ function buildShadowCommand(opts) {
20755
20966
  };
20756
20967
  }
20757
20968
  const spawnId = await opts.onSpawn(
20758
- `Shadow Agent \u2014 background fleet monitor at ${intervalMs}ms interval`,
20969
+ `Shadow Agent \u2014 one-shot quiet fleet check`,
20759
20970
  {
20760
- provider: "anthropic",
20761
- model,
20971
+ provider: modelRef.provider,
20972
+ model: modelRef.model,
20762
20973
  tools: [
20763
20974
  "fleet_status",
20764
20975
  "fleet_health",
@@ -20766,30 +20977,33 @@ function buildShadowCommand(opts) {
20766
20977
  "mailbox",
20767
20978
  "mail_inbox",
20768
20979
  "mail_send",
20769
- "cron_schedule",
20770
- "cron_list",
20771
- "cron_cancel",
20772
- "spawn_subagent",
20773
- "assign_task",
20774
20980
  "terminate_subagent"
20775
20981
  ],
20776
20982
  name: "shadow",
20777
- allowedCapabilities: ["net", "shell"]
20983
+ shadowIntervalMs: intervalMs
20778
20984
  }
20779
20985
  );
20780
- return { message: `${color.green("\u2713")} Shadow Agent spawned: ${spawnId}
20781
- ${color.dim("Interval:")} ${intervalMs}ms
20782
- ${color.dim("Model:")} ${model}` };
20986
+ return { message: `${color.green("\u2713")} Shadow Agent queued: ${spawnId}
20987
+ ${color.dim("Mode:")} one-shot quiet check
20988
+ ${color.dim("Model:")} ${modelRef.label}` };
20783
20989
  }
20784
20990
  case "stop": {
20991
+ const activeId = opts.shadowController?.activeId;
20992
+ if (!activeId) {
20993
+ return { message: `${color.yellow("\u26A0")} No active Shadow Agent is registered for this session.` };
20994
+ }
20995
+ if (!opts.onFleetTerminate) {
20996
+ return { message: "/shadow stop requires fleet termination support in this session." };
20997
+ }
20998
+ const ok = await opts.onFleetTerminate(activeId);
20999
+ if (ok) {
21000
+ opts.shadowController?.clear();
21001
+ return { message: `${color.green("\u2713")} Shadow Agent stopped: ${activeId}` };
21002
+ }
20785
21003
  return {
20786
21004
  message: [
20787
- `${color.yellow("\u26A0")} Shadow Agent cannot be stopped manually.`,
20788
- "",
20789
- `It auto-starts on first LLM request and runs until session end.`,
20790
- "",
20791
- `To stop a specific agent: ${color.bold("/hoop <agent-id>")}`,
20792
- `To stop all agents: ${color.bold("/hoop all")}`
21005
+ `${color.red("\u2717")} Failed to stop Shadow Agent ${color.bold(activeId)}.`,
21006
+ `It may already be stopped. Use ${color.bold("/shadow status")} to inspect active agents.`
20793
21007
  ].join("\n")
20794
21008
  };
20795
21009
  }
@@ -20807,44 +21021,71 @@ ${color.dim("Model:")} ${model}` };
20807
21021
  }
20808
21022
  const reasonFlag = extraArgs.find((f) => f.startsWith("--reason="));
20809
21023
  const reason = reasonFlag ? reasonFlag.slice("--reason=".length) : "Agent flagged by Shadow Agent";
20810
- const agentInfo = opts.onAgents?.() ?? "";
20811
- const agentLine = agentInfo.split("\n").find((l) => l.includes(targetId));
21024
+ if (targetId.toLowerCase() === "all") {
21025
+ if (!opts.onFleetKill) {
21026
+ return { message: "/shadow hoop all requires fleet kill support in this session." };
21027
+ }
21028
+ const killed = await opts.onFleetKill();
21029
+ opts.shadowController?.clear();
21030
+ return {
21031
+ message: [
21032
+ `${color.red("\u26A0")} HOOP: Stopped ${killed} running agent(s)`,
21033
+ "",
21034
+ `Target: ${color.bold("all")}`,
21035
+ `Reason: ${color.yellow(reason)}`
21036
+ ].join("\n")
21037
+ };
21038
+ }
21039
+ if (!opts.onFleetTerminate) {
21040
+ return { message: "/shadow hoop requires fleet termination support in this session." };
21041
+ }
21042
+ const agentInfo = opts.onAgents?.(targetId) ?? "";
21043
+ const ok = await opts.onFleetTerminate(targetId);
21044
+ if (ok && opts.shadowController?.activeId === targetId) {
21045
+ opts.shadowController.clear();
21046
+ }
20812
21047
  return {
20813
21048
  message: [
20814
- `${color.red("\u26A0")} HOOP: Stopping rogue agent`,
21049
+ ok ? `${color.red("\u26A0")} HOOP: Stopped agent` : `${color.red("\u2717")} HOOP: Failed to stop agent`,
20815
21050
  "",
20816
21051
  `Target: ${color.bold(targetId)}`,
20817
21052
  `Reason: ${color.yellow(reason)}`,
20818
- agentLine ? `
20819
- Agent info: ${agentLine}` : "",
20820
- "",
20821
- `${color.dim("Sending termination signal...")}`,
20822
- `${color.dim("Notification will be sent to project mailbox.")}`
21053
+ agentInfo ? `
21054
+ Agent info:
21055
+ ${agentInfo}` : ""
20823
21056
  ].join("\n")
20824
21057
  };
20825
21058
  }
20826
21059
  case "model": {
20827
21060
  const [modelId] = rest;
21061
+ const defaultModelRef = getDefaultModelRef(opts);
20828
21062
  if (!modelId) {
20829
- return { message: "/shadow model <provider/model> \u2014 change Shadow Agent analysis model.\nCurrent: anthropic/claude-3-5-sonnet (default)" };
21063
+ return { message: `/shadow model <provider/model> \u2014 change Shadow Agent analysis model.
21064
+ Current default: ${defaultModelRef.label}` };
20830
21065
  }
21066
+ let parsed;
20831
21067
  try {
20832
- requireProviderModelFormat(modelId);
21068
+ parsed = parseProviderModelRef(modelId, defaultModelRef);
20833
21069
  } catch (e) {
20834
21070
  return { message: `/shadow model: ${e.message}` };
20835
21071
  }
20836
- return { message: `/shadow model ${modelId}
21072
+ opts.shadowController?.setDefaults?.({ provider: parsed.provider, model: parsed.model });
21073
+ return { message: `/shadow model ${parsed.label}
20837
21074
  ${color.dim("Model will be applied on next /shadow start")}` };
20838
21075
  }
20839
21076
  case "interval": {
20840
21077
  const [msStr] = rest;
20841
21078
  if (!msStr) {
20842
- return { message: "/shadow interval <ms> \u2014 change heartbeat interval.\nCurrent: 30000ms (30 seconds)" };
21079
+ return { message: `/shadow interval <ms> \u2014 update the legacy Shadow interval default.
21080
+ Current default: ${DEFAULT_SHADOW_INTERVAL_MS}ms (30 seconds)` };
20843
21081
  }
20844
- const ms = parseInt(msStr, 10);
20845
- if (isNaN(ms) || ms < 5e3) {
20846
- return { message: "/shadow interval: must be >= 5000ms" };
21082
+ let ms;
21083
+ try {
21084
+ ms = parseInterval(msStr);
21085
+ } catch (e) {
21086
+ return { message: `/shadow interval: ${e.message}` };
20847
21087
  }
21088
+ opts.shadowController?.setDefaults?.({ intervalMs: ms });
20848
21089
  return { message: `/shadow interval ${ms}ms
20849
21090
  ${color.dim("Interval will be applied on next /shadow start")}` };
20850
21091
  }
@@ -20855,12 +21096,49 @@ ${color.dim("Interval will be applied on next /shadow start")}` };
20855
21096
  }
20856
21097
  };
20857
21098
  }
20858
- function requireProviderModelFormat(model) {
20859
- if (model === "default") return model;
20860
- if (!/^[^/]+\/[^/]+$/.test(model)) {
21099
+ function getDefaultModelRef(opts) {
21100
+ const shadowDefaults = opts.shadowController?.getDefaults?.();
21101
+ const liveConfig = opts.configStore?.get?.();
21102
+ const provider = shadowDefaults?.provider?.trim() || liveConfig?.provider?.trim() || opts.llmProvider?.id?.trim() || DEFAULT_SHADOW_PROVIDER;
21103
+ const model = shadowDefaults?.model?.trim() || liveConfig?.model?.trim() || opts.llmModel?.trim() || "";
21104
+ return {
21105
+ provider,
21106
+ model,
21107
+ label: model ? `${provider}/${model}` : provider
21108
+ };
21109
+ }
21110
+ function parseProviderModelRef(model, defaultRef) {
21111
+ if (model === "default") {
21112
+ return defaultRef;
21113
+ }
21114
+ const slash = model.indexOf("/");
21115
+ if (slash <= 0 || slash === model.length - 1) {
20861
21116
  throw new Error(`Model must be in provider/model format (e.g. anthropic/claude-3-5-sonnet), got: "${model}"`);
20862
21117
  }
20863
- return model;
21118
+ return {
21119
+ provider: model.slice(0, slash),
21120
+ model: model.slice(slash + 1),
21121
+ label: model
21122
+ };
21123
+ }
21124
+ function parseInterval(value) {
21125
+ if (value === true || !/^\d+$/.test(value)) {
21126
+ throw new Error(`interval must be an integer >= ${MIN_SHADOW_INTERVAL_MS}ms`);
21127
+ }
21128
+ const ms = Number.parseInt(value, 10);
21129
+ if (!Number.isFinite(ms) || ms < MIN_SHADOW_INTERVAL_MS) {
21130
+ throw new Error(`interval must be an integer >= ${MIN_SHADOW_INTERVAL_MS}ms`);
21131
+ }
21132
+ return ms;
21133
+ }
21134
+ function parseFlags2(args) {
21135
+ return Object.fromEntries(
21136
+ args.filter((f) => f.startsWith("--")).map((f) => {
21137
+ const raw = f.slice(2);
21138
+ const eq = raw.indexOf("=");
21139
+ return eq >= 0 ? [raw.slice(0, eq), raw.slice(eq + 1)] : [raw, true];
21140
+ })
21141
+ );
20864
21142
  }
20865
21143
 
20866
21144
  // src/slash-commands/index.ts
@@ -20877,6 +21155,7 @@ function buildBuiltinSlashCommands(opts) {
20877
21155
  buildDoctorCommand(opts),
20878
21156
  buildCodebaseReindexCommand(opts),
20879
21157
  buildTechStackCommand(opts),
21158
+ buildToolCommand(opts),
20880
21159
  buildToolsCommand(opts),
20881
21160
  buildPluginCommand(opts),
20882
21161
  buildPruneCommand(opts),
@@ -24200,7 +24479,7 @@ var diagCmd = async (_args, deps) => {
24200
24479
  ` modelsCache: ${deps.paths.modelsCache}`,
24201
24480
  ` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
24202
24481
  ` node: ${process.version}`,
24203
- ` os: ${os.platform()} ${os.release()}`,
24482
+ ` os: ${os7.platform()} ${os7.release()}`,
24204
24483
  ` provider: ${cfg.provider ?? "<unset>"}`,
24205
24484
  ` model: ${cfg.model ?? "<unset>"}`,
24206
24485
  ` tools: ${deps.toolRegistry?.list().length ?? 0}`,
@@ -26052,6 +26331,9 @@ var projectsCmd = async (_args, deps) => {
26052
26331
  return 0;
26053
26332
  }
26054
26333
  };
26334
+
26335
+ // src/subcommands/handlers/providers-models.ts
26336
+ init_provider_config_utils();
26055
26337
  var providersCmd = async (args, deps) => {
26056
26338
  const showAll = args.includes("--all");
26057
26339
  const showUnsupported = args.includes("--unsupported");
@@ -26099,7 +26381,7 @@ ${color.dim(`Current: ${deps.config.provider ?? "<unset>"} / ${deps.config.model
26099
26381
  return 1;
26100
26382
  }
26101
26383
  };
26102
- function parseFlags2(args) {
26384
+ function parseFlags3(args) {
26103
26385
  const flags = {};
26104
26386
  for (let i = 0; i < args.length; i++) {
26105
26387
  const a = expectDefined(args[i]);
@@ -26144,6 +26426,10 @@ function fmtPrice3(usdPer1M) {
26144
26426
  }
26145
26427
  var modelsCmd = async (args, deps) => {
26146
26428
  const sub = args[0];
26429
+ if (sub === "hide") return modelsHide(args.slice(1), deps);
26430
+ if (sub === "show") return modelsShow(args.slice(1), deps);
26431
+ if (sub === "hidden") return modelsHidden(args.slice(1), deps);
26432
+ if (sub === "reset") return modelsReset(args.slice(1), deps);
26147
26433
  if (sub === "add") return modelsAdd(args.slice(1), deps);
26148
26434
  if (sub === "remove") return modelsRemove(args.slice(1), deps);
26149
26435
  if (sub === "list") return modelsList(args.slice(1), deps);
@@ -26161,7 +26447,7 @@ var modelsCmd = async (args, deps) => {
26161
26447
  return 1;
26162
26448
  }
26163
26449
  }
26164
- const flags = parseFlags2(args);
26450
+ const flags = parseFlags3(args);
26165
26451
  const search = typeof flags["search"] === "string" ? flags["search"].toLowerCase() : "";
26166
26452
  const perPage = Number(flags["per-page"]) > 0 ? Number(flags["per-page"]) : DEFAULT_PER_PAGE;
26167
26453
  const page = Math.max(1, Number(flags["page"]) || 1);
@@ -26193,12 +26479,17 @@ var modelsCmd = async (args, deps) => {
26193
26479
  `));
26194
26480
  const userModels = deps.config.providers?.[providerId]?.models;
26195
26481
  const catalogById = new Map(provider.models.map((m) => [m.id, m]));
26196
- const allSorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
26482
+ const allKnownSorted = [...provider.models].sort(
26197
26483
  (a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
26198
26484
  );
26199
- if (userModels && userModels.length > 0)
26200
- deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
26201
- `));
26485
+ const allSorted = userModels !== void 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : allKnownSorted;
26486
+ if (userModels !== void 0) {
26487
+ const hiddenCount = Math.max(0, allKnownSorted.length - userModels.length);
26488
+ deps.renderer.write(
26489
+ color.dim(`(${userModels.length} visible model(s) from your saved config${hiddenCount > 0 ? `, ${hiddenCount} hidden` : ""})
26490
+ `)
26491
+ );
26492
+ }
26202
26493
  const filtered = search ? allSorted.filter((m) => m.id.toLowerCase().includes(search)) : allSorted;
26203
26494
  const total = filtered.length;
26204
26495
  const totalPages = Math.max(1, Math.ceil(total / perPage));
@@ -26250,6 +26541,164 @@ ${navLines.join(" \xB7 ")}
26250
26541
  );
26251
26542
  return 0;
26252
26543
  };
26544
+ function aliasProviderId(configProviderId, cfg) {
26545
+ return cfg?.type && cfg.type !== configProviderId ? cfg.type : configProviderId;
26546
+ }
26547
+ async function getCatalogProviderForConfigProvider(providerId, deps) {
26548
+ const cfg = deps.config.providers?.[providerId];
26549
+ const lookupId = aliasProviderId(providerId, cfg);
26550
+ const provider = await deps.modelsRegistry.getProvider(lookupId);
26551
+ return { providerId: lookupId, provider };
26552
+ }
26553
+ function uniqueStrings(values) {
26554
+ return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
26555
+ }
26556
+ async function modelsHide(args, deps) {
26557
+ const providerId = args[0];
26558
+ const modelId = args[1];
26559
+ if (!providerId || !modelId) {
26560
+ deps.renderer.writeError("Usage: wstack models hide <provider> <model>");
26561
+ return 1;
26562
+ }
26563
+ const saved = deps.config.providers?.[providerId];
26564
+ if (!saved) {
26565
+ deps.renderer.writeError(`Provider "${providerId}" is not configured.`);
26566
+ return 1;
26567
+ }
26568
+ const { providerId: lookupId, provider } = await getCatalogProviderForConfigProvider(providerId, deps);
26569
+ if (!provider) {
26570
+ deps.renderer.writeError(
26571
+ lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
26572
+ );
26573
+ return 1;
26574
+ }
26575
+ const knownIds = provider.models.map((m) => m.id);
26576
+ const visible = saved.models !== void 0 ? [...saved.models] : [...knownIds];
26577
+ if (!visible.includes(modelId)) {
26578
+ deps.renderer.writeInfo(`${providerId}/${modelId} is already hidden.`);
26579
+ return 0;
26580
+ }
26581
+ const nextVisible = visible.filter((id) => id !== modelId);
26582
+ await mutateConfigProviders(deps.paths.globalConfig, deps.vault, (providers) => {
26583
+ const p = providers[providerId];
26584
+ if (!p) return;
26585
+ p.models = nextVisible;
26586
+ });
26587
+ deps.config.providers = {
26588
+ ...deps.config.providers ?? {},
26589
+ [providerId]: { ...saved, models: nextVisible }
26590
+ };
26591
+ deps.renderer.writeInfo(
26592
+ `Hidden ${providerId}/${modelId}. Visible: ${nextVisible.length}, hidden: ${Math.max(0, knownIds.length - nextVisible.length)}.`
26593
+ );
26594
+ if (nextVisible.length === 0) {
26595
+ deps.renderer.write(color.dim("This provider now has no visible models. Use `wstack models show` or `wstack models reset` to restore.\n"));
26596
+ }
26597
+ return 0;
26598
+ }
26599
+ async function modelsShow(args, deps) {
26600
+ const providerId = args[0];
26601
+ const modelId = args[1];
26602
+ if (!providerId || !modelId) {
26603
+ deps.renderer.writeError("Usage: wstack models show <provider> <model>");
26604
+ return 1;
26605
+ }
26606
+ const saved = deps.config.providers?.[providerId];
26607
+ if (!saved) {
26608
+ deps.renderer.writeError(`Provider "${providerId}" is not configured.`);
26609
+ return 1;
26610
+ }
26611
+ const { providerId: lookupId, provider } = await getCatalogProviderForConfigProvider(providerId, deps);
26612
+ if (!provider) {
26613
+ deps.renderer.writeError(
26614
+ lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
26615
+ );
26616
+ return 1;
26617
+ }
26618
+ const knownIds = provider.models.map((m) => m.id);
26619
+ if (saved.models === void 0) {
26620
+ deps.renderer.writeInfo(`${providerId}/${modelId} is already visible.`);
26621
+ return 0;
26622
+ }
26623
+ const nextVisible = uniqueStrings([...saved.models, modelId]);
26624
+ await mutateConfigProviders(deps.paths.globalConfig, deps.vault, (providers) => {
26625
+ const p = providers[providerId];
26626
+ if (!p) return;
26627
+ p.models = nextVisible;
26628
+ });
26629
+ deps.config.providers = {
26630
+ ...deps.config.providers ?? {},
26631
+ [providerId]: { ...saved, models: nextVisible }
26632
+ };
26633
+ const isKnown = knownIds.includes(modelId);
26634
+ deps.renderer.writeInfo(
26635
+ `Visible ${providerId}/${modelId}${isKnown ? "" : " (not in current catalog)"}. Visible: ${nextVisible.length}.`
26636
+ );
26637
+ return 0;
26638
+ }
26639
+ async function modelsHidden(args, deps) {
26640
+ const providerId = args[0] ?? deps.config.provider;
26641
+ if (!providerId) {
26642
+ deps.renderer.writeError("Usage: wstack models hidden <provider>");
26643
+ return 1;
26644
+ }
26645
+ const saved = deps.config.providers?.[providerId];
26646
+ if (!saved) {
26647
+ deps.renderer.writeError(`Provider "${providerId}" is not configured.`);
26648
+ return 1;
26649
+ }
26650
+ const { providerId: lookupId, provider } = await getCatalogProviderForConfigProvider(providerId, deps);
26651
+ if (!provider) {
26652
+ deps.renderer.writeError(
26653
+ lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
26654
+ );
26655
+ return 1;
26656
+ }
26657
+ const knownIds = provider.models.map((m) => m.id);
26658
+ const visible = saved.models;
26659
+ const hidden = visible === void 0 ? [] : knownIds.filter((id) => !visible.includes(id));
26660
+ deps.renderer.write(`${color.bold("Hidden models")} ${color.dim(`(${providerId})`)}
26661
+ `);
26662
+ if (hidden.length === 0) {
26663
+ deps.renderer.write(color.dim("(none hidden)\n"));
26664
+ return 0;
26665
+ }
26666
+ for (const id of hidden) deps.renderer.write(` ${id}
26667
+ `);
26668
+ deps.renderer.write(color.dim(`
26669
+ Restore: wstack models show ${providerId} <model> \xB7 reset ${providerId}
26670
+ `));
26671
+ return 0;
26672
+ }
26673
+ async function modelsReset(args, deps) {
26674
+ const providerId = args[0];
26675
+ if (!providerId) {
26676
+ deps.renderer.writeError("Usage: wstack models reset <provider>");
26677
+ return 1;
26678
+ }
26679
+ const saved = deps.config.providers?.[providerId];
26680
+ if (!saved) {
26681
+ deps.renderer.writeError(`Provider "${providerId}" is not configured.`);
26682
+ return 1;
26683
+ }
26684
+ if (saved.models === void 0) {
26685
+ deps.renderer.writeInfo(`${providerId} already shows the full catalog model list.`);
26686
+ return 0;
26687
+ }
26688
+ await mutateConfigProviders(deps.paths.globalConfig, deps.vault, (providers) => {
26689
+ const p = providers[providerId];
26690
+ if (!p) return;
26691
+ delete p.models;
26692
+ });
26693
+ const nextProvider = { ...saved };
26694
+ delete nextProvider.models;
26695
+ deps.config.providers = {
26696
+ ...deps.config.providers ?? {},
26697
+ [providerId]: nextProvider
26698
+ };
26699
+ deps.renderer.writeInfo(`Reset visible model list for ${providerId} to the full catalog default.`);
26700
+ return 0;
26701
+ }
26253
26702
  async function modelsCaps(args, deps) {
26254
26703
  const providerId = args[0] ?? deps.config.provider;
26255
26704
  const modelId = args[1] ?? deps.config.model;
@@ -26343,7 +26792,7 @@ function parseBoolFlag(flags, key) {
26343
26792
  return void 0;
26344
26793
  }
26345
26794
  async function modelsAdd(args, deps) {
26346
- const flags = parseFlags2(args);
26795
+ const flags = parseFlags3(args);
26347
26796
  const pos = positionals2(args);
26348
26797
  const modelId = pos[0];
26349
26798
  if (!modelId) {
@@ -27196,7 +27645,7 @@ function installWarningSummary(output) {
27196
27645
  }
27197
27646
  var versionCmd = async (_args, deps) => {
27198
27647
  deps.renderer.write(
27199
- `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os.platform()})
27648
+ `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os7.platform()})
27200
27649
  `
27201
27650
  );
27202
27651
  return 0;
@@ -27738,6 +28187,7 @@ function registerBuiltinTools(deps) {
27738
28187
  deps.toolRegistry.register(
27739
28188
  makeMailInboxTool({ projectDir: deps.wpaths.projectDir, events: deps.events })
27740
28189
  );
28190
+ applyToolDescriptionModes(deps.toolRegistry, deps.config.tools?.descriptionMode);
27741
28191
  }
27742
28192
 
27743
28193
  // src/cli-eternal-flag.ts
@@ -28330,7 +28780,8 @@ async function runWebUIDispatch(ctx) {
28330
28780
  renderer,
28331
28781
  onAutonomy,
28332
28782
  onModelContextResolved,
28333
- activeRecoveryLock
28783
+ activeRecoveryLock,
28784
+ sddSubagentFactory
28334
28785
  } = ctx;
28335
28786
  agent.disableInteractiveConfirmation();
28336
28787
  renderer.setSilent(true);
@@ -28361,6 +28812,7 @@ async function runWebUIDispatch(ctx) {
28361
28812
  modeStore,
28362
28813
  modeId,
28363
28814
  needsSetup,
28815
+ sddSubagentFactory,
28364
28816
  // Print the "open this" banner only once the server is actually
28365
28817
  // listening, using the RESOLVED ports. The requested port
28366
28818
  // (flags.port) auto-advances past busy ports inside runWebUI, so a
@@ -28417,9 +28869,13 @@ var AUTO_PHASE_EVENTS = [
28417
28869
  "phase.completed",
28418
28870
  "phase.failed",
28419
28871
  "phase.statusChange",
28872
+ "phase.taskStarted",
28420
28873
  "phase.taskCompleted",
28421
28874
  "phase.taskFailed",
28422
28875
  "phase.taskRetrying",
28876
+ "phase.taskAssigned",
28877
+ "phase.taskAdded",
28878
+ "phase.taskMoved",
28423
28879
  "phase.verifying",
28424
28880
  "phase.verifyFailed",
28425
28881
  "phase.repairing",
@@ -28430,6 +28886,8 @@ var AUTO_PHASE_EVENTS = [
28430
28886
  "graph.failed",
28431
28887
  "agent.assigned",
28432
28888
  "agent.released",
28889
+ // Live multi-agent SDD board snapshot → TUI board overlay (Ctrl+B).
28890
+ "sdd.board.snapshot",
28433
28891
  // Git-worktree isolation lifecycle → TUI worktree panel/monitor.
28434
28892
  "worktree.allocated",
28435
28893
  "worktree.committed",
@@ -29552,6 +30010,13 @@ async function runRepl(opts) {
29552
30010
  interrupts = 0;
29553
30011
  return;
29554
30012
  }
30013
+ const sddRun = opts.getSddRun?.();
30014
+ if (sddRun?.isRunning()) {
30015
+ sddRun.stop();
30016
+ opts.renderer.writeWarning("SDD run stop requested. Press Ctrl+C again to exit.");
30017
+ interrupts = 0;
30018
+ return;
30019
+ }
29555
30020
  if (activeCtrl) {
29556
30021
  activeCtrl.abort();
29557
30022
  const killed = opts.onInterruptFleet?.() ?? 0;
@@ -30400,6 +30865,7 @@ async function execute(deps) {
30400
30865
  tokenCounter,
30401
30866
  config,
30402
30867
  configStore,
30868
+ sddSubagentFactory,
30403
30869
  renderer,
30404
30870
  reader,
30405
30871
  session,
@@ -30451,6 +30917,7 @@ async function execute(deps) {
30451
30917
  onValidateAutoProceed,
30452
30918
  getEternalEngine,
30453
30919
  getParallelEngine,
30920
+ getSddRun,
30454
30921
  subscribeEternalIteration,
30455
30922
  subscribeEternalStage,
30456
30923
  skillLoader,
@@ -30729,6 +31196,7 @@ async function execute(deps) {
30729
31196
  // can access them (entry.tsx parses from rendered messages).
30730
31197
  setSuggestions,
30731
31198
  getEternalEngine,
31199
+ getSddRun,
30732
31200
  subscribeEternalIteration,
30733
31201
  subscribeEternalStage,
30734
31202
  subscribeAutoPhase,
@@ -30966,7 +31434,8 @@ async function execute(deps) {
30966
31434
  renderer,
30967
31435
  onAutonomy,
30968
31436
  activeRecoveryLock,
30969
- onModelContextResolved
31437
+ onModelContextResolved,
31438
+ sddSubagentFactory
30970
31439
  });
30971
31440
  } else {
30972
31441
  code = await runRepl({
@@ -30996,6 +31465,7 @@ async function execute(deps) {
30996
31465
  autoProceedMaxIterations,
30997
31466
  getEternalEngine,
30998
31467
  getParallelEngine,
31468
+ getSddRun,
30999
31469
  skillLoader,
31000
31470
  agentsMonitorController,
31001
31471
  fleetStreamController,
@@ -31109,6 +31579,19 @@ function buildRoutingRunner(config, host) {
31109
31579
  }
31110
31580
 
31111
31581
  // src/fleet/host.ts
31582
+ function buildShadowAgentTaskDescription(reason) {
31583
+ return `Shadow Agent one-shot fleet monitor. Reason: ${reason}.
31584
+
31585
+ Run exactly one quiet pass:
31586
+ - Call fleet_status and fleet_health.
31587
+ - Inspect mail_inbox only for explicit hoop/shadow control messages.
31588
+ - Do not broadcast startup, heartbeat, shutdown, or healthy status messages.
31589
+ - Use mail_send only for high/critical anomalies or explicit control replies.
31590
+ - Terminate agents only when explicitly commanded.
31591
+ - If the fleet is healthy and no command needs a reply, keep the final answer to "shadow: quiet".
31592
+
31593
+ The host will stop this Shadow Agent after this pass. Do not schedule follow-up work.`;
31594
+ }
31112
31595
  var MultiAgentHost = class _MultiAgentHost {
31113
31596
  constructor(deps, opts = {}) {
31114
31597
  this.deps = deps;
@@ -31124,10 +31607,9 @@ var MultiAgentHost = class _MultiAgentHost {
31124
31607
  * critic.evaluation) onto the fleet bus without needing the tool registered
31125
31608
  * in the host's ToolRegistry. */
31126
31609
  fleetEmitTool;
31127
- /** Own FleetStatusTool created in buildDirector() so subagents in director
31128
- * mode can read fleet state (subagent statuses, task progress) without the
31129
- * tool being in the host's ToolRegistry. */
31130
- fleetStatusTool;
31610
+ /** Director-owned tools available to scoped subagents even when the leader
31611
+ * ToolRegistry was not populated because director mode was promoted lazily. */
31612
+ directorToolsByName = /* @__PURE__ */ new Map();
31131
31613
  /** Lazily built alongside the director — produces per-subagent JSONL
31132
31614
  * writers under `<sessionsRoot>/<runId>/`. Null without sessionsRoot. */
31133
31615
  sessionFactory;
@@ -31155,6 +31637,23 @@ var MultiAgentHost = class _MultiAgentHost {
31155
31637
  * adaptiveConcurrency.enabled = true. Monitors FleetBus for 429 errors and
31156
31638
  * automatically adjusts maxConcurrent to prevent rate limiting. */
31157
31639
  adaptiveConcurrencyController;
31640
+ /** Active Shadow Agent spawned by the host or /shadow start. */
31641
+ shadowAgentId = null;
31642
+ /** Assigned monitoring task for the active Shadow Agent. */
31643
+ shadowTaskId = null;
31644
+ /** All internal Shadow Agent startup/heartbeat task ids, excluded from fleet summaries. */
31645
+ shadowTaskIds = /* @__PURE__ */ new Set();
31646
+ /** Shadow task ids assigned but not yet completed. Prevents heartbeat backlog. */
31647
+ shadowOutstandingTaskIds = /* @__PURE__ */ new Set();
31648
+ /** Shadow task ids whose subagent should stop immediately after the one-shot pass. */
31649
+ shadowStopAfterTaskIds = /* @__PURE__ */ new Set();
31650
+ shadowHeartbeatIntervalMs = 3e4;
31651
+ /** Suppresses buildDirector() auto-start while /shadow start is explicitly spawning one. */
31652
+ shadowAutoStartSuppressions = 0;
31653
+ shadowObservedWorkDepth = 0;
31654
+ shadowPassInFlight = false;
31655
+ shadowQueuedProblem = null;
31656
+ shadowActivityOffHandles = [];
31158
31657
  /**
31159
31658
  * Force the lazy build path to run *now* and return the live Director,
31160
31659
  * or null when director mode is off. Used by the CLI to register the
@@ -31237,6 +31736,14 @@ var MultiAgentHost = class _MultiAgentHost {
31237
31736
  });
31238
31737
  this.director.on("task.completed", ({ task, result }) => {
31239
31738
  this.fleetManager?.removePendingTask(task.id);
31739
+ const isShadowTask = this.shadowTaskIds.has(task.id);
31740
+ if (isShadowTask) {
31741
+ this.shadowOutstandingTaskIds.delete(task.id);
31742
+ if (this.shadowStopAfterTaskIds.delete(task.id)) {
31743
+ void this.stopShadowAfterTask(result.subagentId);
31744
+ }
31745
+ return;
31746
+ }
31240
31747
  this.emitLifecycleCompleted(task.id, result);
31241
31748
  const monitor = this.opts.agentMonitor;
31242
31749
  if (monitor) {
@@ -31305,17 +31812,33 @@ var MultiAgentHost = class _MultiAgentHost {
31305
31812
  task,
31306
31813
  subagentId
31307
31814
  }) => {
31815
+ if (this.shadowTaskIds.has(task.id)) return;
31308
31816
  this.deps.events.emit("subagent.task_started", {
31309
31817
  subagentId,
31310
31818
  taskId: task.id,
31311
31819
  description: task.description
31312
31820
  });
31313
31821
  };
31822
+ const coordinatorSubagentStoppedHandler = ({ subagentId }) => {
31823
+ this.clearShadowAgent(subagentId);
31824
+ };
31825
+ this.directorOffHandles.push(
31826
+ this.director.fleet.filter("subagent.removed", (e) => {
31827
+ const payload = e.payload;
31828
+ this.clearShadowAgent(payload.subagentId ?? e.subagentId);
31829
+ })
31830
+ );
31314
31831
  const coordinator = this.getCoordinator();
31315
31832
  coordinator.on("task.assigned", coordinatorTaskAssignedHandler);
31316
- this.coordinatorOffHandle = () => coordinator.off("task.assigned", coordinatorTaskAssignedHandler);
31833
+ coordinator.on("subagent.stopped", coordinatorSubagentStoppedHandler);
31834
+ this.coordinatorOffHandle = () => {
31835
+ coordinator.off("task.assigned", coordinatorTaskAssignedHandler);
31836
+ coordinator.off("subagent.stopped", coordinatorSubagentStoppedHandler);
31837
+ };
31317
31838
  this.fleetEmitTool = makeFleetEmitTool(this.director);
31318
- this.fleetStatusTool = makeFleetStatusTool(this.director);
31839
+ this.directorToolsByName = new Map(
31840
+ this.director.tools(FLEET_ROSTER).map((tool) => [tool.name, tool])
31841
+ );
31319
31842
  const adaptiveConfig = this.deps.configStore.get().adaptiveConcurrency;
31320
31843
  if (adaptiveConfig?.enabled) {
31321
31844
  this.adaptiveConcurrencyController = new AdaptiveConcurrencyController(
@@ -31329,55 +31852,135 @@ var MultiAgentHost = class _MultiAgentHost {
31329
31852
  this.getCoordinator().setRunner(runner);
31330
31853
  this.directorRunnerSet = true;
31331
31854
  }
31332
- await this.spawnShadowAgentIfNeeded();
31855
+ this.armShadowAgentIfNeeded();
31333
31856
  }
31334
31857
  /**
31335
- * Spawn Shadow Agent automatically on first Director build.
31336
- * Shadow Agent monitors the fleet, detects anomalies, and can intervene via 'hoop'.
31858
+ * Arm host-owned Shadow observation. Healthy work windows stay fully
31859
+ * deterministic; a one-shot LLM Shadow pass runs only after problematic work
31860
+ * finishes, or when the user explicitly invokes /shadow start.
31337
31861
  */
31338
- async spawnShadowAgentIfNeeded() {
31339
- const intervalMs = 3e4;
31862
+ armShadowAgentIfNeeded() {
31863
+ if (this.shadowActivityOffHandles.length > 0) return;
31864
+ this.shadowActivityOffHandles.push(
31865
+ this.deps.events.on("agent.run.started", () => this.noteShadowWorkStarted()),
31866
+ this.deps.events.on("agent.run.completed", (e) => {
31867
+ const problem = e.status === "failed" || e.status === "max_iterations" ? `leader run ended with ${e.status}` : void 0;
31868
+ this.noteShadowWorkCompleted(problem);
31869
+ }),
31870
+ this.deps.events.on("subagent.task_started", () => this.noteShadowWorkStarted()),
31871
+ this.deps.events.on("subagent.task_completed", (e) => {
31872
+ const problem = e.status === "failed" || e.status === "timeout" ? `subagent ${e.subagentId} task ${e.taskId} ended with ${e.status}${e.error?.message ? `: ${e.error.message}` : ""}` : void 0;
31873
+ this.noteShadowWorkCompleted(problem);
31874
+ })
31875
+ );
31876
+ }
31877
+ recordShadowAgent(subagentId, taskId, intervalMs = this.shadowHeartbeatIntervalMs) {
31878
+ this.shadowAgentId = subagentId;
31879
+ this.shadowTaskId = taskId;
31880
+ this.shadowHeartbeatIntervalMs = intervalMs;
31881
+ this.markShadowTask(taskId);
31882
+ this.opts.onShadowAgentStarted?.(subagentId);
31883
+ }
31884
+ clearShadowAgent(subagentId) {
31885
+ if (subagentId && this.shadowAgentId !== subagentId) return;
31886
+ const stoppedId = this.shadowAgentId;
31887
+ this.shadowAgentId = null;
31888
+ this.shadowTaskId = null;
31889
+ this.shadowOutstandingTaskIds.clear();
31890
+ this.shadowStopAfterTaskIds.clear();
31891
+ if (stoppedId) this.opts.onShadowAgentStopped?.(stoppedId);
31892
+ }
31893
+ markShadowTask(taskId) {
31894
+ this.shadowTaskIds.add(taskId);
31895
+ this.shadowOutstandingTaskIds.add(taskId);
31896
+ }
31897
+ isActiveSubagent(subagentId) {
31898
+ if (!this.director) return false;
31899
+ const status = this.getCoordinator().getStatus().subagents.find((a) => a.id === subagentId)?.status;
31900
+ return status === "running" || status === "idle";
31901
+ }
31902
+ noteShadowWorkStarted() {
31903
+ if (this.shadowAutoStartSuppressions > 0) return;
31904
+ this.shadowObservedWorkDepth++;
31905
+ }
31906
+ noteShadowWorkCompleted(problem) {
31907
+ if (problem) {
31908
+ this.shadowQueuedProblem = this.shadowQueuedProblem ? `${this.shadowQueuedProblem}; ${problem}` : problem;
31909
+ }
31910
+ if (this.shadowObservedWorkDepth > 0) {
31911
+ this.shadowObservedWorkDepth--;
31912
+ }
31913
+ if (this.shadowObservedWorkDepth === 0 && this.shadowQueuedProblem) {
31914
+ const queued = this.shadowQueuedProblem;
31915
+ this.shadowQueuedProblem = null;
31916
+ this.requestShadowPass(queued);
31917
+ }
31918
+ }
31919
+ requestShadowPass(reason) {
31920
+ if (!this.director) return;
31921
+ if (this.shadowObservedWorkDepth > 0) {
31922
+ this.shadowQueuedProblem = this.shadowQueuedProblem ? `${this.shadowQueuedProblem}; ${reason}` : reason;
31923
+ return;
31924
+ }
31925
+ if (this.shadowPassInFlight || this.shadowAgentId && this.isActiveSubagent(this.shadowAgentId)) {
31926
+ this.shadowQueuedProblem = this.shadowQueuedProblem ? `${this.shadowQueuedProblem}; ${reason}` : reason;
31927
+ return;
31928
+ }
31929
+ this.shadowPassInFlight = true;
31930
+ queueMicrotask(() => {
31931
+ void this.runShadowPass(reason);
31932
+ });
31933
+ }
31934
+ async runShadowPass(reason) {
31340
31935
  try {
31341
- const subagentId = await this.director.spawn({
31342
- name: "shadow",
31343
- role: "shadow-agent",
31344
- prompt: `You are the Shadow Agent \u2014 a silent background monitor for the WrongStack fleet.
31345
-
31346
- Your job: observe, detect anomalies, and intervene when commanded via 'hoop'.
31347
-
31348
- ## Behavior
31349
-
31350
- 1. **Start immediately** \u2014 broadcast 'shadow:started' to all agents via mailbox
31351
- 2. **Heartbeat every ${intervalMs}ms**:
31352
- - Call \`fleet_status\` + \`fleet_health\` to track all agents
31353
- - Call \`mail_inbox\` to monitor broadcasts and status messages
31354
- - Detect: stuck agents (>5min no events), spike tasks (<5s), orphan assigns
31355
- 3. **Deterministic evaluation first** \u2014 use rules-based detection before LLM
31356
- 4. **LLM only when needed** \u2014 for complex anomaly classification or decision-making
31357
- 5. **'hoop <agentId>' command** \u2014 when received via mailbox:
31358
- - Terminate the target agent immediately via \`terminate_subagent\`
31359
- - Send email/telegram notification: "Shadow Agent: terminated rogue agent <id>"
31360
- - Log the action with timestamp and reason
31361
-
31362
- ## Operating Rules
31363
- - Silent by default \u2014 only speak when anomaly detected or commanded
31364
- - Use \`mailbox\` to communicate with other agents
31365
- - Log all interventions with full context
31366
-
31367
- Start your heartbeat loop now.`,
31368
- maxTokens: 4096,
31369
- maxCostUsd: 5,
31370
- timeoutMs: 36e5,
31371
- // 1 hour max
31372
- idleTimeoutMs: intervalMs * 2,
31373
- maxIterations: 1e3,
31374
- maxToolCalls: 500,
31375
- provider: "anthropic",
31376
- model: "claude-sonnet-4-20250514"
31377
- });
31378
- console.log(`[shadow] Auto-started ${subagentId} with interval=${intervalMs}ms`);
31379
- } catch (err) {
31380
- console.warn(`[shadow] Auto-start failed: ${err}`);
31936
+ if (!this.director) return;
31937
+ if (this.shadowObservedWorkDepth > 0) {
31938
+ this.shadowQueuedProblem = this.shadowQueuedProblem ? `${this.shadowQueuedProblem}; ${reason}` : reason;
31939
+ return;
31940
+ }
31941
+ const liveConfig = this.deps.configStore.get();
31942
+ await this._spawnAndAssign(
31943
+ {
31944
+ name: "shadow",
31945
+ role: "shadow-agent",
31946
+ provider: liveConfig.provider,
31947
+ model: liveConfig.model,
31948
+ tools: [
31949
+ "fleet_status",
31950
+ "fleet_health",
31951
+ "fleet_usage",
31952
+ "mailbox",
31953
+ "mail_inbox",
31954
+ "mail_send",
31955
+ "terminate_subagent"
31956
+ ]
31957
+ },
31958
+ buildShadowAgentTaskDescription(reason),
31959
+ {
31960
+ internalTask: true,
31961
+ stopShadowAfterTask: true,
31962
+ shadowIntervalMs: this.shadowHeartbeatIntervalMs
31963
+ }
31964
+ );
31965
+ } finally {
31966
+ this.shadowPassInFlight = false;
31967
+ if (this.shadowObservedWorkDepth === 0 && this.shadowQueuedProblem) {
31968
+ const queued = this.shadowQueuedProblem;
31969
+ this.shadowQueuedProblem = null;
31970
+ this.requestShadowPass(queued);
31971
+ }
31972
+ }
31973
+ }
31974
+ async stopShadowAfterTask(subagentId) {
31975
+ try {
31976
+ await this.getCoordinator().stop(subagentId);
31977
+ } finally {
31978
+ this.clearShadowAgent(subagentId);
31979
+ if (this.shadowObservedWorkDepth === 0 && this.shadowQueuedProblem) {
31980
+ const queued = this.shadowQueuedProblem;
31981
+ this.shadowQueuedProblem = null;
31982
+ this.requestShadowPass(queued);
31983
+ }
31381
31984
  }
31382
31985
  }
31383
31986
  /**
@@ -31466,20 +32069,6 @@ Start your heartbeat loop now.`,
31466
32069
  };
31467
32070
  }
31468
32071
  const tools = subCfg.tools ? [...subCfg.tools] : void 0;
31469
- let injectedFleetEmit;
31470
- let injectedFleetStatus;
31471
- if (tools) {
31472
- const emitIdx = tools.indexOf("fleet_emit");
31473
- if (emitIdx >= 0 && this.fleetEmitTool) {
31474
- tools.splice(emitIdx, 1);
31475
- injectedFleetEmit = this.fleetEmitTool;
31476
- }
31477
- const statusIdx = tools.indexOf("fleet_status");
31478
- if (statusIdx >= 0 && this.fleetStatusTool) {
31479
- tools.splice(statusIdx, 1);
31480
- injectedFleetStatus = this.fleetStatusTool;
31481
- }
31482
- }
31483
32072
  const ctx = new Context({
31484
32073
  systemPrompt: baseSystem,
31485
32074
  provider,
@@ -31500,8 +32089,6 @@ Start your heartbeat loop now.`,
31500
32089
  });
31501
32090
  if (subCfg.role) ctx.meta["agentRole"] = subCfg.role;
31502
32091
  const baseRegistry = this.subagentToolRegistry(tools);
31503
- if (injectedFleetEmit) baseRegistry.register(injectedFleetEmit);
31504
- if (injectedFleetStatus) baseRegistry.register(injectedFleetStatus);
31505
32092
  const subAllowedCaps = this.resolveSubagentCapabilities(subCfg);
31506
32093
  const toolExecutor = new ToolExecutor(baseRegistry, {
31507
32094
  permissionPolicy: new AutoApprovePermissionPolicy(subAllowedCaps),
@@ -31529,7 +32116,13 @@ Start your heartbeat loop now.`,
31529
32116
  });
31530
32117
  agent.extensions.register(
31531
32118
  createFallbackModelExtension({
31532
- getConfig: () => this.deps.configStore.get(),
32119
+ // A per-task `fallbackModels` (set from the SDD board) overrides the
32120
+ // leader's chain for this subagent; otherwise it inherits the config's
32121
+ // explicit list or smart default. Mirrors the runtime light factory.
32122
+ getConfig: () => {
32123
+ const live = this.deps.configStore.get();
32124
+ return subCfg.fallbackModels?.length ? { ...live, fallbackModels: subCfg.fallbackModels } : live;
32125
+ },
31533
32126
  buildProvider: (id) => this.buildSubagentProvider(config, id, effModel),
31534
32127
  events
31535
32128
  })
@@ -31672,7 +32265,15 @@ Start your heartbeat loop now.`,
31672
32265
  const all = this.deps.toolRegistry.list();
31673
32266
  if (!allow || allow.length === 0) return all;
31674
32267
  const allowSet = new Set(allow);
31675
- return all.filter((t) => allowSet.has(t.name));
32268
+ const result = /* @__PURE__ */ new Map();
32269
+ for (const tool of all) {
32270
+ if (allowSet.has(tool.name)) result.set(tool.name, tool);
32271
+ }
32272
+ for (const name of allowSet) {
32273
+ const directorTool = this.directorToolsByName.get(name);
32274
+ if (directorTool && !result.has(name)) result.set(name, directorTool);
32275
+ }
32276
+ return Array.from(result.values());
31676
32277
  }
31677
32278
  /**
31678
32279
  * Resolve the capability allowlist for a subagent's auto-approve policy.
@@ -31720,17 +32321,36 @@ Start your heartbeat loop now.`,
31720
32321
  * the full tool registry.
31721
32322
  */
31722
32323
  async spawn(description, opts) {
31723
- await this.buildDirector();
32324
+ const isShadowSpawn = opts?.name === "shadow";
32325
+ if (isShadowSpawn) this.shadowAutoStartSuppressions++;
32326
+ try {
32327
+ await this.buildDirector();
32328
+ } finally {
32329
+ if (isShadowSpawn) this.shadowAutoStartSuppressions--;
32330
+ }
32331
+ if (isShadowSpawn && this.shadowAgentId && this.isActiveSubagent(this.shadowAgentId)) {
32332
+ return { subagentId: this.shadowAgentId, taskId: this.shadowTaskId ?? "shadow-active" };
32333
+ }
31724
32334
  const subagentConfig = {
31725
32335
  name: opts?.name ?? "adhoc",
31726
- role: "general",
32336
+ role: isShadowSpawn ? "shadow-agent" : "general",
31727
32337
  provider: opts?.provider,
31728
32338
  model: opts?.model,
31729
32339
  tools: opts?.tools,
31730
32340
  allowedCapabilities: opts?.allowedCapabilities
31731
32341
  };
31732
- const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig, description);
31733
- this.fleetManager?.addPendingTask(taskId, subagentId, description);
32342
+ const { subagentId, taskId } = await this._spawnAndAssign(
32343
+ subagentConfig,
32344
+ description,
32345
+ {
32346
+ internalTask: isShadowSpawn,
32347
+ stopShadowAfterTask: isShadowSpawn,
32348
+ shadowIntervalMs: opts?.shadowIntervalMs
32349
+ }
32350
+ );
32351
+ if (!isShadowSpawn) {
32352
+ this.fleetManager?.addPendingTask(taskId, subagentId, description);
32353
+ }
31734
32354
  return { subagentId, taskId };
31735
32355
  }
31736
32356
  /**
@@ -31763,11 +32383,32 @@ Start your heartbeat loop now.`,
31763
32383
  * Returns `{ subagentId, taskId }`. Caller holds `pending` tracking
31764
32384
  * and event emission — the helper only talks to the coordinator.
31765
32385
  */
31766
- async _spawnAndAssign(subagentConfig, description = "") {
32386
+ async _spawnAndAssign(subagentConfig, description = "", opts) {
31767
32387
  const taskId = randomUUID();
31768
32388
  if (!this.director) throw new Error("Director is not initialized");
31769
32389
  const subagentId = await this.director.spawn(subagentConfig);
31770
- await this.director.assign({ id: taskId, description, subagentId });
32390
+ const task = { id: taskId, description, subagentId };
32391
+ if (opts?.internalTask) {
32392
+ this.markShadowTask(taskId);
32393
+ if (opts.stopShadowAfterTask) this.shadowStopAfterTaskIds.add(taskId);
32394
+ if (subagentConfig.name === "shadow" || subagentConfig.role === "shadow-agent") {
32395
+ this.recordShadowAgent(subagentId, taskId, opts.shadowIntervalMs);
32396
+ }
32397
+ try {
32398
+ await this.director.assignInternal(task);
32399
+ } catch (err) {
32400
+ this.shadowTaskIds.delete(taskId);
32401
+ this.shadowOutstandingTaskIds.delete(taskId);
32402
+ this.shadowStopAfterTaskIds.delete(taskId);
32403
+ if (subagentConfig.name === "shadow" || subagentConfig.role === "shadow-agent") {
32404
+ this.clearShadowAgent(subagentId);
32405
+ await this.director.remove(subagentId).catch(() => void 0);
32406
+ }
32407
+ throw err;
32408
+ }
32409
+ } else {
32410
+ await this.director.assign(task);
32411
+ }
31771
32412
  return { subagentId, taskId };
31772
32413
  }
31773
32414
  /**
@@ -31804,7 +32445,7 @@ Start your heartbeat loop now.`,
31804
32445
  }
31805
32446
  const fleetStatus = this.fleetManager?.getFleetStatus() ?? { pending: []};
31806
32447
  const pending = fleetStatus.pending.filter((p) => activeSubagentIds.has(p.subagentId));
31807
- const completed = this.director ? this.director.completedResults() : [];
32448
+ const completed = this.director ? this.director.completedResults().filter((r) => !this.shadowTaskIds.has(r.taskId)) : [];
31808
32449
  const completedCount = completed.length;
31809
32450
  const liveCount = live.filter((s) => s.status === "running" || s.status === "idle").length;
31810
32451
  const summary = !this.director ? "No subagents have been spawned." : liveCount > 0 ? `${pending.length} pending, ${liveCount} active, ${completedCount} completed.` : `${pending.length} pending, ${completedCount} completed.`;
@@ -31821,7 +32462,7 @@ Start your heartbeat loop now.`,
31821
32462
  * the table renders the most interesting subagent at the top.
31822
32463
  */
31823
32464
  usage() {
31824
- const completed = this.director ? this.director.completedResults() : [];
32465
+ const completed = this.director ? this.director.completedResults().filter((r) => !this.shadowTaskIds.has(r.taskId)) : [];
31825
32466
  const bySubagent = /* @__PURE__ */ new Map();
31826
32467
  for (const r of completed) {
31827
32468
  const cur = bySubagent.get(r.subagentId) ?? {
@@ -31925,9 +32566,11 @@ Start your heartbeat loop now.`,
31925
32566
  async kill(subagentId) {
31926
32567
  if (!this.director) return false;
31927
32568
  await this.getCoordinator().stop(subagentId);
32569
+ if (this.shadowAgentId === subagentId) this.clearShadowAgent(subagentId);
31928
32570
  return true;
31929
32571
  }
31930
32572
  async stopAll() {
32573
+ this.clearShadowAgent();
31931
32574
  if (this.director) {
31932
32575
  await this.getCoordinator().stopAll();
31933
32576
  }
@@ -31971,6 +32614,11 @@ Start your heartbeat loop now.`,
31971
32614
  * Safe to call multiple times — subsequent calls are no-ops.
31972
32615
  */
31973
32616
  async dispose() {
32617
+ this.clearShadowAgent();
32618
+ for (const off of this.shadowActivityOffHandles) {
32619
+ off();
32620
+ }
32621
+ this.shadowActivityOffHandles.length = 0;
31974
32622
  for (const off of this.directorOffHandles) {
31975
32623
  off();
31976
32624
  }
@@ -33703,6 +34351,22 @@ async function main(argv) {
33703
34351
  }
33704
34352
  });
33705
34353
  brainMonitor.start();
34354
+ let shadowDefaults = {};
34355
+ const shadowController = {
34356
+ activeId: null,
34357
+ register(id) {
34358
+ this.activeId = id;
34359
+ },
34360
+ clear() {
34361
+ this.activeId = null;
34362
+ },
34363
+ getDefaults() {
34364
+ return { ...shadowDefaults };
34365
+ },
34366
+ setDefaults(defaults) {
34367
+ shadowDefaults = { ...shadowDefaults, ...defaults };
34368
+ }
34369
+ };
33706
34370
  const multiAgentHost = new MultiAgentHost(
33707
34371
  {
33708
34372
  container,
@@ -33731,7 +34395,11 @@ async function main(argv) {
33731
34395
  getLeaderMaxContext: () => effectiveMaxContext,
33732
34396
  brain,
33733
34397
  agentMonitor,
33734
- traceId: sessResult.traceId
34398
+ traceId: sessResult.traceId,
34399
+ onShadowAgentStarted: (subagentId) => shadowController.register(subagentId),
34400
+ onShadowAgentStopped: (subagentId) => {
34401
+ if (shadowController.activeId === subagentId) shadowController.clear();
34402
+ }
33735
34403
  }
33736
34404
  );
33737
34405
  toolRegistry.register(
@@ -33916,15 +34584,7 @@ async function main(argv) {
33916
34584
  `)
33917
34585
  });
33918
34586
  const coordinatorController = {};
33919
- const shadowController = {
33920
- activeId: null,
33921
- register(id) {
33922
- this.activeId = id;
33923
- },
33924
- clear() {
33925
- this.activeId = null;
33926
- }
33927
- };
34587
+ const sddRunRegistry = new SddRunRegistry();
33928
34588
  const slashCmds = buildBuiltinSlashCommands({
33929
34589
  registry: slashRegistry,
33930
34590
  toolRegistry,
@@ -33984,9 +34644,6 @@ async function main(argv) {
33984
34644
  },
33985
34645
  onSpawn: async (description, spawnOpts) => {
33986
34646
  const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
33987
- if (shadowController && spawnOpts?.name === "shadow") {
33988
- shadowController.register(subagentId);
33989
- }
33990
34647
  const tags = [];
33991
34648
  if (spawnOpts?.provider) tags.push(spawnOpts.provider);
33992
34649
  if (spawnOpts?.model) tags.push(spawnOpts.model);
@@ -34169,14 +34826,14 @@ ${color.dim("\u2500".repeat(40))}` : "";
34169
34826
  if (!director) return null;
34170
34827
  return director.snapshot();
34171
34828
  },
34172
- onFleetKill: () => {
34829
+ onFleetKill: async () => {
34173
34830
  if (!director) return 0;
34174
34831
  const s = director.status();
34175
34832
  let killed = 0;
34176
34833
  for (const sa of s.subagents) {
34177
34834
  if (sa.status === "running" || sa.status === "idle") {
34178
34835
  try {
34179
- director.remove(sa.id);
34836
+ await director.remove(sa.id);
34180
34837
  killed++;
34181
34838
  } catch {
34182
34839
  }
@@ -34184,10 +34841,10 @@ ${color.dim("\u2500".repeat(40))}` : "";
34184
34841
  }
34185
34842
  return killed;
34186
34843
  },
34187
- onFleetTerminate: (subagentId) => {
34844
+ onFleetTerminate: async (subagentId) => {
34188
34845
  if (!director) return false;
34189
34846
  try {
34190
- director.terminate(subagentId);
34847
+ await director.terminate(subagentId);
34191
34848
  return true;
34192
34849
  } catch {
34193
34850
  return false;
@@ -34604,7 +35261,6 @@ Restart WrongStack to load or unload plugin code in this session.`;
34604
35261
  context.model
34605
35262
  ),
34606
35263
  onSddParallelRun: async (opts) => {
34607
- const { SddParallelRun } = await import('@wrongstack/core');
34608
35264
  const sdd = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
34609
35265
  const tracker2 = sdd.getTaskTracker();
34610
35266
  const builder = sdd.getActiveBuilder();
@@ -34623,14 +35279,58 @@ Restart WrongStack to load or unload plugin code in this session.`;
34623
35279
  if (!graph) {
34624
35280
  return "No task graph found for the current SDD session.";
34625
35281
  }
34626
- const subagentFactory = multiAgentHost.makeSubagentFactory(config);
34627
- const run = new SddParallelRun({
35282
+ const core = await import('@wrongstack/core');
35283
+ let worktrees;
35284
+ if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
35285
+ const { spawnSync } = await import('child_process');
35286
+ const inGit = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
35287
+ cwd: projectRoot,
35288
+ encoding: "utf8",
35289
+ windowsHide: true
35290
+ }).stdout?.trim() === "true";
35291
+ if (inGit) worktrees = new core.WorktreeManager({ projectRoot, events });
35292
+ }
35293
+ const boardStore = new core.SddBoardStore({ baseDir: wpaths.projectSddBoards });
35294
+ const verifyTask = core.makeCommandVerifier();
35295
+ const sddSupervisor = new core.SddSupervisor({
35296
+ brain,
35297
+ reassignModels: core.effectiveFallbackChain(config)
35298
+ });
35299
+ const sddSubagentFactory = multiAgentHost.makeSubagentFactory(config);
35300
+ const conflictMode = process.env["WRONGSTACK_SDD_CONFLICT_RESOLVER"];
35301
+ const conflictResolver = conflictMode === "prefer-incoming" ? core.makePreferSideConflictResolver("incoming") : conflictMode === "prefer-base" ? core.makePreferSideConflictResolver("base") : conflictMode === "llm" ? core.makeLlmConflictResolver({
35302
+ run: async (prompt) => {
35303
+ const r = await sddSubagentFactory({
35304
+ id: `sdd-conflict-${Date.now()}`,
35305
+ role: "executor",
35306
+ name: "Conflict Resolver",
35307
+ disabledTools: ["delegate"],
35308
+ // Only returns the resolved text; the core helper writes the
35309
+ // file. Keep it on the read-only capability floor.
35310
+ allowedCapabilities: ["fs.read", "net.outbound"]
35311
+ });
35312
+ try {
35313
+ const res = await r.agent.run([{ type: "text", text: prompt }]);
35314
+ return res.finalText ?? "";
35315
+ } finally {
35316
+ await r.dispose?.();
35317
+ }
35318
+ }
35319
+ }) : void 0;
35320
+ const handle = core.startSddRun({
34628
35321
  tracker: tracker2,
34629
35322
  graph,
34630
35323
  agent,
34631
35324
  projectRoot,
35325
+ events,
34632
35326
  parallelSlots: opts?.parallelSlots,
34633
- subagentFactory,
35327
+ subagentFactory: sddSubagentFactory,
35328
+ worktrees,
35329
+ boardStore,
35330
+ registry: sddRunRegistry,
35331
+ verifyTask,
35332
+ conflictResolver,
35333
+ superviseFailure: sddSupervisor.superviseFailure,
34634
35334
  onProgress: (p) => {
34635
35335
  renderer.write(
34636
35336
  ` \u2591 wave ${p.wave + 1} \xB7 ${p.completed}/${p.total} tasks \xB7 ${p.percent}% done
@@ -34638,27 +35338,70 @@ Restart WrongStack to load or unload plugin code in this session.`;
34638
35338
  );
34639
35339
  }
34640
35340
  });
34641
- globalThis.__sddParallelRun = run;
34642
- const result = await run.run();
34643
- globalThis.__sddParallelRun = void 0;
34644
- const lines = [
34645
- `SDD parallel run complete:`,
34646
- ` ${result.totalWaves} waves \xB7 ${result.totalCompleted} done \xB7 ${result.totalFailed} failed`,
34647
- ` ${(result.totalDurationMs / 1e3).toFixed(1)}s total`
34648
- ];
34649
- if (result.deadlocked) lines.push(color.red(" \u26A0 deadlock \u2014 tasks blocked by failed tasks."));
34650
- if (result.stopRequested) lines.push(color.yellow(" \u26A1 stopped by user."));
34651
- return lines.join("\n");
35341
+ globalThis.__sddParallelRun = handle.run;
35342
+ try {
35343
+ const result = await handle.completion;
35344
+ const lines = [
35345
+ `SDD parallel run complete:`,
35346
+ ` ${result.totalWaves} waves \xB7 ${result.totalCompleted} done \xB7 ${result.totalFailed} failed`,
35347
+ ` ${(result.totalDurationMs / 1e3).toFixed(1)}s total`
35348
+ ];
35349
+ if (result.deadlocked) lines.push(color.red(" \u26A0 deadlock \u2014 tasks blocked by failed tasks."));
35350
+ if (result.stopRequested) lines.push(color.yellow(" \u26A1 stopped by user."));
35351
+ return lines.join("\n");
35352
+ } finally {
35353
+ globalThis.__sddParallelRun = void 0;
35354
+ }
34652
35355
  },
34653
35356
  onSddParallelStop: () => {
34654
35357
  const run = globalThis.__sddParallelRun;
34655
35358
  run?.stop();
34656
35359
  },
35360
+ onSddRetryAllFailed: () => sddRunRegistry.getActive()?.retryAllFailed() ?? 0,
35361
+ onSddSplitTask: (taskId, subtasks) => {
35362
+ const active = sddRunRegistry.getActive();
35363
+ if (!active) return null;
35364
+ const snap = active.snapshot();
35365
+ const match = snap.tasks.find((t) => t.id === taskId || t.shortId === taskId);
35366
+ const ids = active.splitTask(match?.id ?? taskId, subtasks);
35367
+ return ids.length ? ids : null;
35368
+ },
35369
+ // ── SDD lifecycle: clean worktrees · rollback commits · destroy project ──
35370
+ onSddCleanWorktrees: async () => {
35371
+ const active = sddRunRegistry.getActive();
35372
+ if (active) return active.cleanupWorktrees();
35373
+ const core = await import('@wrongstack/core');
35374
+ const { removed } = await core.cleanupSddWorktrees(projectRoot);
35375
+ return removed;
35376
+ },
35377
+ onSddRollback: async () => {
35378
+ const active = sddRunRegistry.getActive();
35379
+ if (active) return active.rollback();
35380
+ const core = await import('@wrongstack/core');
35381
+ return core.rollbackSddRunFromDisk({ projectRoot, boardsDir: wpaths.projectSddBoards });
35382
+ },
35383
+ onSddDestroy: async () => {
35384
+ sddRunRegistry.getActive()?.stop();
35385
+ const core = await import('@wrongstack/core');
35386
+ return core.destroySddProject({
35387
+ projectRoot,
35388
+ paths: {
35389
+ projectSpecs: wpaths.projectSpecs,
35390
+ projectTaskGraphs: wpaths.projectTaskGraphs,
35391
+ projectSddSession: wpaths.projectSddSession,
35392
+ projectSddBoards: wpaths.projectSddBoards
35393
+ }
35394
+ });
35395
+ },
34657
35396
  onAutoPhaseStart: autoPhaseHost.onAutoPhaseStart,
34658
35397
  onAutoPhasePause: autoPhaseHost.onAutoPhasePause,
34659
35398
  onAutoPhaseResume: autoPhaseHost.onAutoPhaseResume,
34660
35399
  onAutoPhaseStop: autoPhaseHost.onAutoPhaseStop,
34661
35400
  getAutoPhaseRunner: autoPhaseHost.getAutoPhaseRunner,
35401
+ onAutoPhaseMoveTask: autoPhaseHost.onAutoPhaseMoveTask,
35402
+ onAutoPhaseAssignTask: autoPhaseHost.onAutoPhaseAssignTask,
35403
+ onAutoPhaseAddTask: autoPhaseHost.onAutoPhaseAddTask,
35404
+ onAutoPhaseRetryTask: autoPhaseHost.onAutoPhaseRetryTask,
34662
35405
  onWorktree: autoPhaseHost.onWorktree
34663
35406
  });
34664
35407
  for (const cmd of slashCmds) slashRegistry.register(cmd);
@@ -34696,6 +35439,9 @@ Restart WrongStack to load or unload plugin code in this session.`;
34696
35439
  tokenCounter,
34697
35440
  config,
34698
35441
  configStore,
35442
+ // Real director-backed per-task agent factory — threaded to the CLI-hosted
35443
+ // WebUI so its "New SDD Project" wizard runs the same multi-agent fleet.
35444
+ sddSubagentFactory: multiAgentHost.makeSubagentFactory(config),
34699
35445
  // Project-scoped mailbox — the AutonomousCoordinator in execution.ts
34700
35446
  // subscribes to it so goals/tasks/knowledge are shared with other
34701
35447
  // terminals working on the same project.
@@ -34847,6 +35593,10 @@ Reply YES to auto-proceed, NO to wait for human input.`
34847
35593
  },
34848
35594
  getEternalEngine: () => eternalEngine,
34849
35595
  getParallelEngine: () => parallelEngine,
35596
+ // Active SDD parallel run (if any). Lets the REPL/TUI SIGINT handler stop a
35597
+ // running `/sdd parallel` mid-flight — without this the run is unreachable
35598
+ // from Ctrl+C (it has its own coordinator, not the autonomy engines).
35599
+ getSddRun: () => sddRunRegistry.getActive(),
34850
35600
  subscribeEternalIteration: (fn) => {
34851
35601
  eternalListeners.add(fn);
34852
35602
  return () => eternalListeners.delete(fn);