@wrongstack/cli 0.282.0 → 0.282.2

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.
Files changed (3) hide show
  1. package/dist/index.js +2128 -1201
  2. package/dist/index.js.map +1 -1
  3. package/package.json +15 -15
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { CODEX_MODELS, color, ToolCapabilities, PHASE_EVENT_NAMES, readBundledInstructionText, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, ConfigError, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, createHqPersistence, HqCommandAuditLog, HqAlertEngine, toAlertMessage, validateHqCommand, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, FetchError, removeColumn, addColumn, getTask, removeTask, getBoard, assignTask, updateTaskAssignment, listBoards, copyTaskToBoard, transferTaskToBoard, moveTask, updateTask, addTask, generateBoardFromDescription, createBoard, parseLinesIntoTasks, removeBoard, duplicateBoard, updateBoard, wstackGlobalRoot, DefaultSecretScrubber, resolveWstackPaths, watchProviderConfig, PromptUsageStore, TOKENS, ToolRegistry, GlobalMailbox, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, shellHooksEqual, countShellHooks, normalizeTokenSavingTier, SlashCommandRegistry, attachDepWatcherBridge, createFallbackModelExtension, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, SddRunRegistry, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, buildTranscriptFromEvents, DefaultLogger, DefaultModelsRegistry, isStdinTTY, FsError, atomicWrite, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, sessionScopedPath as sessionScopedPath$1, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, makeFleetStatusTool, applyToolDescriptionModes, applyToolResultRenderModes, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, createHqPublisherFromEnv, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, applyModelRuntime, installDesignStudioMiddleware, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, WorktreeManager, Director, makeFleetEmitTool, AdaptiveConcurrencyController, resolveSubagentModelTarget, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, mergeModelRuntime, ToolValidationError, WIDE_SUBAGENT_CAPABILITIES, resolveProjectDir, FleetSupervisor, AgentError, PhaseStore, PhaseOrchestrator, assignNickname, AutoPhasePlanner, PhaseGraphBuilder, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, CHIMERA_REVIEW_PROMPT, setQueuedMessagesSnapshot, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, ProviderRegistry, makePreferSideConflictResolver, makeAgentSubagentRunner, NULL_FLEET_BUS, ERROR_CODES, buildChildEnv, exportBoardAsMarkdown, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENT_CATALOG, dispatchAgent, normalizeToolDescriptionMode, getToolDescriptionMode, noOpVault, formatTodosList, loadTasks, formatTaskList, formatTaskProgress, mutateTasks, emptyPlan, addPlanItem, savePlan, formatPlan, recordCompletedWorkEvidence, saveCompletedWorkCheckpoint, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, getDesignKitLoader, getDesignState, clearActiveKit, clearPersistedActiveKit, recordOverrides, setDesignOverrides, loadActiveKit, isDesignStack, applyTokenOverrides, materializeTokens, setActiveKit, readHqRuntimeFileSync, resolveHqConfig, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, resolveModelMatrix, resolveModelTargetFromEntry, fallbackProfileChain, parseModelRef, normalizeModelRef, onResize, hasOpenTodos, InputBuilder, DefaultSecretVault as DefaultSecretVault$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DEFAULT_CONTEXT_WINDOW_MODE_ID, HQ_AUTH_FILE_VERSION, truncate, addDependency, addNoteToTask, addCheckToTask, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, normalizeToolResultRenderMode, getToolResultRenderMode, setToolResultRenderMode, resolveMailboxIdentity, smartDefaultFallbackChain, AutonomousCoordinator, mutateHqAuthFile, mintHqToken, readHqAuthFile, areDependenciesMet, findBlockedTasks, renderInstructionTemplate, recentTextTurns, gatedEnhancerReasoning, enhanceUserPrompt, mutatePlan, setPlanItemStatus, getPlanTemplate, resolveProviderModelList, ParseError, startSessionTelemetryBridge, startFleetTelemetryBridge, startBrainTelemetryBridge, startWorktreeTelemetryBridge, startToolTelemetryBridge, startCostTelemetryBridge } from '@wrongstack/core';
2
+ import { CODEX_MODELS, color, ToolCapabilities, PHASE_EVENT_NAMES, readBundledInstructionText, writeErr, loadPlugins, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, withFileLock, ConfigError, resolveHqDataDir, ensureHqFirstRunAuthFile, DEFAULT_HQ_REDACTION_POLICY, createHqPersistence, HqCommandAuditLog, HqAlertEngine, toAlertMessage, validateHqCommand, watchHqAuthFile, parseHqFrame, HQ_PROTOCOL_VERSION, parseHqEventPayload, scrubAndTruncateHqPreview, FetchError, removeColumn, addColumn, getTask, removeTask, getBoard, assignTask, updateTaskAssignment, listBoards, updateGoalMetricOnTask, addGoalMetricToTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, copyTaskToBoard, transferTaskToBoard, moveTask, updateTask, mergeTasks, splitTask, addTask, syncBoardFromTaskGraph, deserializeTaskGraph, exportBoardToTaskGraph, serializeTaskGraph, getKanbanOrchestrationSnapshot, listReadyTasks, generateBoardFromDescription, createBoard, parseLinesIntoTasks, removeBoard, duplicateBoard, updateBoard, wstackGlobalRoot, DefaultSecretScrubber, resolveWstackPaths, watchProviderConfig, TOKENS, PromptUsageStore, ToolRegistry, GlobalMailbox, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, shellHooksEqual, countShellHooks, normalizeTokenSavingTier, allServers as allServers$1, SlashCommandRegistry, attachDepWatcherBridge, createFallbackModelExtension, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, createTieredBrainArbiter, DefaultBrainArbiter, BrainMonitor, mailboxSessionTag, createDelegateTool, FLEET_ROSTER, createMcpControlTool, startTechStackConsumer, startPackageOutdatedWatcher, SddRunRegistry, recordFileAction, createAutonomyBrain, DefaultPluginAPI, SpecVersioning, buildTranscriptFromEvents, DefaultLogger, DefaultModelsRegistry, isStdinTTY, FsError, atomicWrite, EventBus, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, sessionScopedPath as sessionScopedPath$1, createContextManagerTool, makeMailboxTool, makeMailSendTool, makeMailInboxTool, makeFleetStatusTool, applyToolDescriptionModes, applyToolResultRenderModes, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, createHqPublisherFromEnv, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, Context, QueueStore, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, applyModelRuntime, installDesignStudioMiddleware, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, FleetManager, makeDirectorSessionFactory, WorktreeManager, Director, makeFleetEmitTool, AdaptiveConcurrencyController, resolveSubagentModelTarget, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, mergeModelRuntime, ToolValidationError, WIDE_SUBAGENT_CAPABILITIES, resolveProjectDir, FleetSupervisor, AgentError, PhaseStore, PhaseOrchestrator, assignNickname, AutoPhasePlanner, PhaseGraphBuilder, makeLLMClassifier, writeOut, ParallelEternalEngine, EternalAutonomyEngine, CHIMERA_REVIEW_PROMPT, setQueuedMessagesSnapshot, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, ProviderRegistry, makePreferSideConflictResolver, makeAgentSubagentRunner, NULL_FLEET_BUS, ERROR_CODES, buildChildEnv, exportBoardAsMarkdown, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENT_CATALOG, dispatchAgent, normalizeToolDescriptionMode, getToolDescriptionMode, noOpVault, formatTodosList, loadTasks, formatTaskList, formatTaskProgress, mutateTasks, emptyPlan, addPlanItem, savePlan, formatPlan, recordCompletedWorkEvidence, saveCompletedWorkCheckpoint, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, getDesignKitLoader, getDesignState, clearActiveKit, clearPersistedActiveKit, recordOverrides, setDesignOverrides, loadActiveKit, isDesignStack, applyTokenOverrides, materializeTokens, setActiveKit, readHqRuntimeFileSync, resolveHqConfig, MATRIX_PHASE_KEYS, matrixKeyKind, phaseForRole, resolveModelMatrix, resolveModelTargetFromEntry, fallbackProfileChain, parseModelRef, normalizeModelRef, onResize, hasOpenTodos, InputBuilder, DefaultSecretVault as DefaultSecretVault$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, DEFAULT_CONTEXT_WINDOW_MODE_ID, HQ_AUTH_FILE_VERSION, truncate, createBoardFromTaskGraph, addDependency, addNoteToTask, addCheckToTask, estimateMessageTokens, AGENTS_BY_PHASE, validateAgainstSchema, normalizeToolResultRenderMode, getToolResultRenderMode, setToolResultRenderMode, resolveMailboxIdentity, smartDefaultFallbackChain, AutonomousCoordinator, mutateHqAuthFile, mintHqToken, readHqAuthFile, areDependenciesMet, findBlockedTasks, renderInstructionTemplate, recentTextTurns, gatedEnhancerReasoning, enhanceUserPrompt, mutatePlan, setPlanItemStatus, getPlanTemplate, resolveProviderModelList, ParseError, startSessionTelemetryBridge, startFleetTelemetryBridge, startBrainTelemetryBridge, startWorktreeTelemetryBridge, startToolTelemetryBridge, startCostTelemetryBridge } from '@wrongstack/core';
3
3
  import * as fs2 from 'fs/promises';
4
4
  import { access, readFile } from 'fs/promises';
5
5
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
@@ -8,7 +8,7 @@ import { join } from 'path';
8
8
  import * as crypto from 'crypto';
9
9
  import { createHash, randomUUID, randomBytes, timingSafeEqual } from 'crypto';
10
10
  import { createRequire } from 'module';
11
- import * as fs27 from 'fs';
11
+ import * as fs28 from 'fs';
12
12
  import { watch, writeFileSync, existsSync, realpathSync } from 'fs';
13
13
  import * as http from 'http';
14
14
  import { createServer } from 'http';
@@ -34,7 +34,7 @@ import { parseNextSteps } from '@wrongstack/tui';
34
34
  import { WrongStackACPServer, makeACPServerAgentTurn, ACPSessionStore, WsBridgeTransport, ACPProtocolHandler } from '@wrongstack/acp/agent';
35
35
  import { allServers, DefaultPathResolver, DefaultTokenCounter } from '@wrongstack/core/infrastructure';
36
36
  import { ToolExecutor } from '@wrongstack/core/execution';
37
- import { loadBenchConfig, reportHeaderLine, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench } from '@wrongstack/bench';
37
+ import { loadBenchConfig, reportHeaderLine, computeToolManifestHash, readSummary, renderMarkdownReport, createPolyglotSuite, createSwebenchSuite, createLocalManifestSuite, runBenchmark, writeJsonArtifacts, collectCellPredictions, writePredictionsJsonl, gradePolyglot, gradeSwebench, gradeLocalManifest } from '@wrongstack/bench';
38
38
  import { createAgentMonitorService, acquireOrJoin, release, finalize, readLiveLock } from '@wrongstack/core/coordination';
39
39
  import * as net from 'net';
40
40
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
@@ -639,6 +639,956 @@ var init_helpers = __esm({
639
639
  ENTRY_BASENAMES = /* @__PURE__ */ new Set(["main", "index", "app", "cli", "server", "__main__"]);
640
640
  }
641
641
  });
642
+ async function runPluginManagementCommand(args, deps) {
643
+ const sub = args[0];
644
+ if (!sub || sub === "list" || sub === "status") {
645
+ return {
646
+ code: 0,
647
+ level: "output",
648
+ message: renderConfiguredPlugins(deps.config)
649
+ };
650
+ }
651
+ if (sub === "official" || sub === "officials") {
652
+ return {
653
+ code: 0,
654
+ level: "output",
655
+ message: renderOfficialPlugins(deps.config)
656
+ };
657
+ }
658
+ if (sub === "report" || sub === "audit" || sub === "menu") {
659
+ return {
660
+ code: 0,
661
+ level: "output",
662
+ message: renderPluginAuditReport(deps.config)
663
+ };
664
+ }
665
+ if (sub === "add" || sub === "install") {
666
+ const spec = args[1];
667
+ if (!spec) {
668
+ return errorResult("Usage: wstack plugin add <specifier|official-alias> [--disabled]");
669
+ }
670
+ return upsertPlugin(
671
+ resolvePluginSpecifier(spec),
672
+ { enabled: !args.includes("--disabled") },
673
+ deps,
674
+ "Added"
675
+ );
676
+ }
677
+ if (sub === "remove" || sub === "rm" || sub === "uninstall") {
678
+ const spec = args[1];
679
+ if (!spec) {
680
+ return errorResult("Usage: wstack plugin remove <specifier|official-alias>");
681
+ }
682
+ return removePlugin(resolvePluginSpecifier(spec), deps);
683
+ }
684
+ if (sub === "enable" || sub === "disable") {
685
+ const spec = args[1];
686
+ if (!spec) {
687
+ return errorResult(`Usage: wstack plugin ${sub} <specifier|official-alias>`);
688
+ }
689
+ return upsertPlugin(
690
+ resolvePluginSpecifier(spec),
691
+ { enabled: sub === "enable" },
692
+ deps,
693
+ sub === "enable" ? "Enabled" : "Disabled"
694
+ );
695
+ }
696
+ if (sub === "toggle") {
697
+ const spec = args[1];
698
+ if (!spec) {
699
+ return errorResult("Usage: wstack plugin toggle <plugin-name|official-alias>");
700
+ }
701
+ return togglePlugin(resolvePluginToggleSpecifier(spec), deps);
702
+ }
703
+ if (sub === "llm") {
704
+ return runPluginLlmCommand(args.slice(1), deps);
705
+ }
706
+ return errorResult(
707
+ `Unknown plugin subcommand: ${sub}
708
+ Usage: wstack plugin [list|status|report|menu|official|add|install|remove|enable|disable|toggle|llm]`
709
+ );
710
+ }
711
+ function renderPluginLlmOverviews(config, sessionDefault) {
712
+ const rows = [];
713
+ for (const [pluginName2, ext] of Object.entries(config.extensions ?? {})) {
714
+ const llm = ext?.["llm"];
715
+ if (llm && typeof llm === "object") {
716
+ const o = llm;
717
+ rows.push({
718
+ name: pluginName2,
719
+ provider: o.provider ?? "(session)",
720
+ model: o.model ?? "(session)"
721
+ });
722
+ }
723
+ }
724
+ const lines = [`Per-plugin LLM routing (session default: ${sessionDefault}):`];
725
+ if (rows.length === 0) {
726
+ lines.push(
727
+ " No plugin overrides \u2014 every plugin follows the session default.",
728
+ " Set one with: wstack plugin llm <plugin> <provider> [model]"
729
+ );
730
+ } else {
731
+ rows.sort((a, b) => a.name.localeCompare(b.name));
732
+ for (const r of rows) {
733
+ lines.push(` ${r.name.padEnd(24)} provider=${r.provider.padEnd(16)} model=${r.model}`);
734
+ }
735
+ lines.push("", "Clear one with: wstack plugin llm <plugin> --clear");
736
+ }
737
+ return lines.join("\n");
738
+ }
739
+ async function runPluginLlmCommand(args, deps) {
740
+ const sessionDefault = `${deps.config.provider ?? "?"} / ${deps.config.model ?? "?"}`;
741
+ if (args.length === 0 || args[0] === "list") {
742
+ return {
743
+ code: 0,
744
+ level: "output",
745
+ message: renderPluginLlmOverviews(deps.config, sessionDefault)
746
+ };
747
+ }
748
+ const name = args[0];
749
+ if (!name || name.startsWith("-")) return errorResult(PLUGIN_LLM_USAGE);
750
+ const currentOverride = deps.config.extensions?.[name]?.["llm"];
751
+ if (args.length === 1) {
752
+ const lines = [`LLM routing for plugin "${name}":`];
753
+ if (currentOverride && typeof currentOverride === "object") {
754
+ const o = currentOverride;
755
+ lines.push(
756
+ ` override: provider=${o.provider ?? "(session)"} model=${o.model ?? "(session)"}`,
757
+ ` session default: ${sessionDefault}`
758
+ );
759
+ } else {
760
+ lines.push(
761
+ ` no override \u2014 follows the session default (${sessionDefault}).`,
762
+ ` Set one with: wstack plugin llm ${name} <provider> [model]`
763
+ );
764
+ }
765
+ return { code: 0, level: "output", message: lines.join("\n") };
766
+ }
767
+ const existing = await readConfig(deps.configPath);
768
+ const fileExtensions = isRecord(existing.extensions) ? existing.extensions : {};
769
+ const pluginExt = isRecord(fileExtensions[name]) ? { ...fileExtensions[name] } : {};
770
+ if (args[1] === "--clear" || args[1] === "clear") {
771
+ if (!("llm" in pluginExt) && !currentOverride) {
772
+ return { code: 0, level: "info", message: `Plugin "${name}" has no LLM override.` };
773
+ }
774
+ delete pluginExt["llm"];
775
+ if (Object.keys(pluginExt).length === 0) delete fileExtensions[name];
776
+ else fileExtensions[name] = pluginExt;
777
+ existing.extensions = fileExtensions;
778
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
779
+ const merged2 = mergeExtensionsForPatch(deps.config, fileExtensions);
780
+ const mergedEntry = merged2[name];
781
+ if (mergedEntry) {
782
+ delete mergedEntry["llm"];
783
+ if (Object.keys(mergedEntry).length === 0) delete merged2[name];
784
+ }
785
+ return {
786
+ code: 0,
787
+ level: "info",
788
+ message: `Cleared LLM override for "${name}" \u2014 it now follows the session default (${sessionDefault}). Takes effect immediately for api.llm calls.`,
789
+ patch: { extensions: merged2 }
790
+ };
791
+ }
792
+ const providerArg = args[1];
793
+ const modelArg = args[2];
794
+ if (!providerArg || providerArg.startsWith("--")) return errorResult(PLUGIN_LLM_USAGE);
795
+ const provider = providerArg === "-" ? void 0 : providerArg;
796
+ const model = modelArg && modelArg !== "-" ? modelArg : void 0;
797
+ if (!provider && !model) return errorResult(PLUGIN_LLM_USAGE);
798
+ const llm = {};
799
+ if (provider) llm["provider"] = provider;
800
+ if (model) llm["model"] = model;
801
+ pluginExt["llm"] = llm;
802
+ fileExtensions[name] = pluginExt;
803
+ existing.extensions = fileExtensions;
804
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
805
+ const configured = Object.keys(deps.config.providers ?? {});
806
+ const note = provider && configured.length > 0 && !configured.includes(provider) ? `
807
+ Note: "${provider}" is not in config.providers (configured: ${configured.join(", ")}). Catalog providers (anthropic, openai, \u2026) still resolve if credentials are available.` : "";
808
+ const merged = mergeExtensionsForPatch(deps.config, fileExtensions);
809
+ return {
810
+ code: 0,
811
+ level: "info",
812
+ message: `Plugin "${name}" now routes api.llm calls via provider=${provider ?? "(session)"} model=${model ?? "(session)"}. Session default stays ${sessionDefault}. Takes effect immediately for api.llm calls.${note}`,
813
+ patch: { extensions: merged }
814
+ };
815
+ }
816
+ function mergeExtensionsForPatch(config, fileExtensions) {
817
+ const merged = {};
818
+ for (const [k, v] of Object.entries(config.extensions ?? {})) {
819
+ merged[k] = { ...v };
820
+ }
821
+ for (const [k, v] of Object.entries(fileExtensions)) {
822
+ if (isRecord(v)) merged[k] = { ...merged[k] ?? {}, ...v };
823
+ }
824
+ for (const k of Object.keys(merged)) {
825
+ if (!(k in fileExtensions) && !(k in (config.extensions ?? {}))) delete merged[k];
826
+ }
827
+ return merged;
828
+ }
829
+ function resolvePluginSpecifier(input) {
830
+ return OFFICIAL_ALIASES.get(input.toLowerCase()) ?? input;
831
+ }
832
+ function resolvePluginToggleSpecifier(input) {
833
+ if (input.toLowerCase() === "@wrongstack/telegram") return "telegram";
834
+ if (auditEntryFor(input)) return input;
835
+ return resolvePluginSpecifier(input);
836
+ }
837
+ function renderOfficialPlugins(config) {
838
+ return [
839
+ "Official plugins:",
840
+ ...OFFICIAL_PLUGINS.map((p) => {
841
+ const state = config ? officialPluginState(config, p.specifier) : "";
842
+ const status = state ? `${state.padEnd(14)} ` : "";
843
+ return ` ${p.alias.padEnd(12)} ${status}${p.specifier.padEnd(24)} ${p.description}`;
844
+ }),
845
+ "",
846
+ "Use `wstack plugin add <alias>` or `/plugin install <alias>`."
847
+ ].join("\n");
848
+ }
849
+ function renderConfiguredPlugins(config) {
850
+ const plugins = config.plugins ?? [];
851
+ if (plugins.length === 0) {
852
+ return [
853
+ "No plugins configured.",
854
+ "Use `wstack plugin add <specifier>` or `/plugin install <official-alias>`."
855
+ ].join("\n");
856
+ }
857
+ return plugins.map((p) => {
858
+ const name = pluginName(p);
859
+ const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
860
+ const official = OFFICIAL_PLUGINS.find((entry) => entry.specifier === name);
861
+ const suffix = official ? ` (${official.alias})` : "";
862
+ return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
863
+ }).join("\n");
864
+ }
865
+ function renderPluginAuditReport(config) {
866
+ const configured = config.plugins ?? [];
867
+ const lines = [
868
+ "Plugin audit report:",
869
+ " State shows the effective boot state. Default-active plugins run unless disabled in config.",
870
+ " Built-in audit rows are toggleable unless a future row explicitly marks itself locked.",
871
+ "",
872
+ " Built-in audit entries:"
873
+ ];
874
+ for (const entry of PLUGIN_AUDIT_ENTRIES) {
875
+ const { state, source } = effectiveAuditState(config, entry);
876
+ const policy = entry.canDisable ? "toggleable" : lockedReason(entry);
877
+ lines.push(
878
+ ` ${entry.name.padEnd(24)} ${state.padEnd(8)} ${source.padEnd(8)} risk=${entry.risk.padEnd(6)} ${policy.padEnd(24)} ${entry.summary}`
879
+ );
880
+ }
881
+ const extra = configured.filter(
882
+ (plugin) => !PLUGIN_AUDIT_ENTRIES.some((entry) => pluginMatchesToggleSpec(plugin, entry.name))
883
+ );
884
+ if (extra.length > 0) {
885
+ lines.push("", " Configured plugins outside the audit list:");
886
+ for (const plugin of extra) {
887
+ const name = pluginName(plugin);
888
+ const state = typeof plugin === "object" && plugin.enabled === false ? "disabled" : "enabled";
889
+ lines.push(
890
+ ` ${name.padEnd(24)} ${state.padEnd(8)} config risk=custom user-configured plugin`
891
+ );
892
+ }
893
+ }
894
+ lines.push(
895
+ "",
896
+ "Use `/plugin menu` in the TUI, or `wstack plugin toggle <name>` for toggleable rows."
897
+ );
898
+ return lines.join("\n");
899
+ }
900
+ async function readConfig(file) {
901
+ try {
902
+ return JSON.parse(await fs2.readFile(file, "utf8"));
903
+ } catch {
904
+ return {};
905
+ }
906
+ }
907
+ function pluginName(p) {
908
+ return typeof p === "string" ? p : p.name;
909
+ }
910
+ function pluginEntry(spec, enabled) {
911
+ return enabled ? spec : { name: spec, enabled: false };
912
+ }
913
+ function pluginMatchesToggleSpec(p, spec) {
914
+ const name = pluginName(p);
915
+ if (spec === "telegram") return name === "telegram" || name === "@wrongstack/telegram";
916
+ return name === spec;
917
+ }
918
+ function effectiveAuditState(config, entry) {
919
+ const configured = (config.plugins ?? []).find(
920
+ (plugin) => pluginMatchesToggleSpec(plugin, entry.name)
921
+ );
922
+ if (configured) {
923
+ return {
924
+ state: typeof configured === "object" && configured.enabled === false ? "disabled" : "enabled",
925
+ source: "config"
926
+ };
927
+ }
928
+ return {
929
+ state: entry.defaultState === "active" ? "enabled" : "disabled",
930
+ source: "default"
931
+ };
932
+ }
933
+ function lockedReason(entry) {
934
+ if (entry.canDisable) return "toggleable";
935
+ if (entry.name === "secret-scanner") {
936
+ return "locked: safety guard";
937
+ }
938
+ return "locked: core surface";
939
+ }
940
+ function auditEntryFor(spec) {
941
+ return PLUGIN_AUDIT_ENTRIES.find((entry) => entry.name === spec);
942
+ }
943
+ function officialPluginState(config, spec) {
944
+ const match = (config.plugins ?? []).find((p) => pluginName(p) === spec);
945
+ if (!match) return "not configured";
946
+ return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
947
+ }
948
+ async function togglePlugin(spec, deps) {
949
+ const auditEntry = auditEntryFor(spec);
950
+ if (auditEntry?.canDisable === false) {
951
+ return errorResult(`Plugin "${spec}" is locked and cannot be toggled from the plugin menu.`);
952
+ }
953
+ const existing = await readConfig(deps.configPath);
954
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
955
+ const idx = plugins.findIndex((p) => pluginMatchesToggleSpec(p, spec));
956
+ const configured = idx >= 0 ? plugins[idx] : void 0;
957
+ const enabled = configured ? !(typeof configured === "object" && configured.enabled === false) : auditEntry?.defaultState === "active";
958
+ if (!configured && !auditEntry) {
959
+ return errorResult(
960
+ `Plugin "${spec}" is not configured. Use "wstack plugin add ${spec}" first.`
961
+ );
962
+ }
963
+ let nextPlugins;
964
+ let nextEnabled;
965
+ if (enabled) {
966
+ nextEnabled = false;
967
+ nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
968
+ if (auditEntry?.defaultState !== "inactive") {
969
+ nextPlugins.push({ name: spec, enabled: false });
970
+ }
971
+ } else {
972
+ nextEnabled = true;
973
+ if (auditEntry?.defaultState === "active") {
974
+ nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
975
+ } else {
976
+ nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
977
+ nextPlugins.push(spec);
978
+ }
979
+ }
980
+ const features = {
981
+ ...isRecord(deps.config.features) ? deps.config.features : {},
982
+ ...isRecord(existing.features) ? existing.features : {},
983
+ plugins: true
984
+ };
985
+ existing.plugins = nextPlugins;
986
+ existing.features = features;
987
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
988
+ const restartRequired = spec === "branch-guard" && !nextEnabled ? void 0 : true;
989
+ return {
990
+ code: 0,
991
+ level: "info",
992
+ message: `${nextEnabled ? "Enabled" : "Disabled"} "${spec}". Config written to ${deps.configPath}.` + (spec === "branch-guard" && !nextEnabled ? " The running branch-guard hook will disable itself on config update." : ""),
993
+ patch: { plugins: nextPlugins, features },
994
+ restartRequired
995
+ };
996
+ }
997
+ async function upsertPlugin(spec, opts, deps, verb) {
998
+ const existing = await readConfig(deps.configPath);
999
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
1000
+ const idx = plugins.findIndex((p) => pluginName(p) === spec);
1001
+ const nextEntry = pluginEntry(spec, opts.enabled);
1002
+ if (idx >= 0) plugins[idx] = nextEntry;
1003
+ else plugins.push(nextEntry);
1004
+ const features = {
1005
+ ...isRecord(deps.config.features) ? deps.config.features : {},
1006
+ ...isRecord(existing.features) ? existing.features : {},
1007
+ plugins: true
1008
+ };
1009
+ existing.plugins = plugins;
1010
+ existing.features = features;
1011
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
1012
+ const restartRequired = spec === "branch-guard" && !opts.enabled ? void 0 : true;
1013
+ return {
1014
+ code: 0,
1015
+ level: "info",
1016
+ message: `${verb} "${spec}" (${opts.enabled ? "enabled" : "disabled"}). Config written to ${deps.configPath}.` + (spec === "branch-guard" && !opts.enabled ? " The running branch-guard hook will disable itself on config update." : ""),
1017
+ patch: { plugins, features },
1018
+ restartRequired
1019
+ };
1020
+ }
1021
+ async function removePlugin(spec, deps) {
1022
+ const existing = await readConfig(deps.configPath);
1023
+ const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
1024
+ const next = plugins.filter((p) => pluginName(p) !== spec);
1025
+ if (next.length === plugins.length) {
1026
+ return errorResult(`Plugin "${spec}" not in config.`);
1027
+ }
1028
+ existing.plugins = next;
1029
+ await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
1030
+ return {
1031
+ code: 0,
1032
+ level: "info",
1033
+ message: `Removed "${spec}" from config.`,
1034
+ patch: { plugins: next },
1035
+ restartRequired: true
1036
+ };
1037
+ }
1038
+ function errorResult(message) {
1039
+ return { code: 1, level: "error", message };
1040
+ }
1041
+ function isRecord(value) {
1042
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1043
+ }
1044
+ var OFFICIAL_PLUGINS, PLUGIN_AUDIT_ENTRIES, OFFICIAL_ALIASES, PLUGIN_LLM_USAGE;
1045
+ var init_plugin_management = __esm({
1046
+ "src/plugin-management.ts"() {
1047
+ OFFICIAL_PLUGINS = [
1048
+ {
1049
+ alias: "telegram",
1050
+ specifier: "@wrongstack/telegram",
1051
+ description: "Telegram bridge for prompts, notifications, and slash commands."
1052
+ },
1053
+ {
1054
+ alias: "lsp",
1055
+ specifier: "@wrongstack/plug-lsp",
1056
+ description: "Language Server Protocol tools for code intelligence."
1057
+ }
1058
+ ];
1059
+ PLUGIN_AUDIT_ENTRIES = [
1060
+ {
1061
+ name: "wstack-prompts",
1062
+ risk: "medium",
1063
+ summary: "Prompt library and prompt authoring commands.",
1064
+ defaultState: "active",
1065
+ canDisable: true
1066
+ },
1067
+ {
1068
+ name: "wstack-sync",
1069
+ risk: "medium",
1070
+ summary: "Cloud sync commands for prompts, skills, settings, memory, and history.",
1071
+ defaultState: "active",
1072
+ canDisable: true
1073
+ },
1074
+ {
1075
+ name: "wstack-git",
1076
+ risk: "high",
1077
+ summary: "Git commands for commit, status checks, and push.",
1078
+ defaultState: "active",
1079
+ canDisable: true
1080
+ },
1081
+ {
1082
+ name: "wstack-observability",
1083
+ risk: "low",
1084
+ summary: "Runtime metrics and health slash commands.",
1085
+ defaultState: "active",
1086
+ canDisable: true
1087
+ },
1088
+ {
1089
+ name: "wstack-security",
1090
+ risk: "high",
1091
+ summary: "Security scanning backend used by first-party security flows.",
1092
+ defaultState: "active",
1093
+ canDisable: true
1094
+ },
1095
+ {
1096
+ name: "wstack-chimera",
1097
+ risk: "medium",
1098
+ summary: "Post-session code quality review agent.",
1099
+ defaultState: "active",
1100
+ canDisable: true
1101
+ },
1102
+ {
1103
+ name: "wstack-skills",
1104
+ risk: "medium",
1105
+ summary: "Skill library, authoring, install, update, and uninstall commands.",
1106
+ defaultState: "active",
1107
+ canDisable: true
1108
+ },
1109
+ {
1110
+ name: "wstack-plan",
1111
+ risk: "medium",
1112
+ summary: "Strategic plan board slash command.",
1113
+ defaultState: "active",
1114
+ canDisable: true
1115
+ },
1116
+ {
1117
+ name: "agent-handoff",
1118
+ risk: "medium",
1119
+ summary: "Mailbox-backed handoff notes and task transfer helpers.",
1120
+ defaultState: "active",
1121
+ canDisable: true
1122
+ },
1123
+ {
1124
+ name: "auto-doc",
1125
+ risk: "medium",
1126
+ summary: "Generates JSDoc/TSDoc comments for source files.",
1127
+ defaultState: "inactive",
1128
+ canDisable: true
1129
+ },
1130
+ {
1131
+ name: "auto-i18n-extractor",
1132
+ risk: "low",
1133
+ summary: "Finds hardcoded user-facing UI strings and suggests i18n keys when explicitly enabled.",
1134
+ defaultState: "inactive",
1135
+ canDisable: true
1136
+ },
1137
+ {
1138
+ name: "accessibility-auditor",
1139
+ risk: "medium",
1140
+ summary: "Scans UI files for common accessibility issues when explicitly enabled.",
1141
+ defaultState: "inactive",
1142
+ canDisable: true
1143
+ },
1144
+ {
1145
+ name: "git-autocommit",
1146
+ risk: "high",
1147
+ summary: "Stages files and creates AI-generated conventional commits.",
1148
+ defaultState: "inactive",
1149
+ canDisable: true
1150
+ },
1151
+ {
1152
+ name: "shell-check",
1153
+ risk: "low",
1154
+ summary: "Runs shellcheck on shell scripts.",
1155
+ defaultState: "inactive",
1156
+ canDisable: true
1157
+ },
1158
+ {
1159
+ name: "cost-tracker",
1160
+ risk: "low",
1161
+ summary: "Tracks token usage and estimated session cost.",
1162
+ defaultState: "active",
1163
+ canDisable: true
1164
+ },
1165
+ {
1166
+ name: "file-watcher",
1167
+ risk: "medium",
1168
+ summary: "Watches project files and emits change events.",
1169
+ defaultState: "inactive",
1170
+ canDisable: true
1171
+ },
1172
+ {
1173
+ name: "cron",
1174
+ risk: "medium",
1175
+ summary: "Schedules recurring in-session tasks.",
1176
+ defaultState: "inactive",
1177
+ canDisable: true
1178
+ },
1179
+ {
1180
+ name: "template-engine",
1181
+ risk: "medium",
1182
+ summary: "Expands and writes file templates.",
1183
+ defaultState: "inactive",
1184
+ canDisable: true
1185
+ },
1186
+ {
1187
+ name: "semver-bump",
1188
+ risk: "high",
1189
+ summary: "Computes version bumps, changelogs, and tags.",
1190
+ defaultState: "inactive",
1191
+ canDisable: true
1192
+ },
1193
+ {
1194
+ name: "secret-scanner",
1195
+ risk: "high",
1196
+ summary: "Blocks or redacts credential leaks in tool input and output.",
1197
+ defaultState: "active",
1198
+ canDisable: true
1199
+ },
1200
+ {
1201
+ name: "todo-tracker",
1202
+ risk: "low",
1203
+ summary: "Persistent project-scoped todo backlog.",
1204
+ defaultState: "active",
1205
+ canDisable: true
1206
+ },
1207
+ {
1208
+ name: "token-budget",
1209
+ risk: "medium",
1210
+ summary: "Warns or stops when token budgets are exceeded.",
1211
+ defaultState: "active",
1212
+ canDisable: true
1213
+ },
1214
+ {
1215
+ name: "lint-gate",
1216
+ risk: "medium",
1217
+ summary: "Runs lint checks before write/edit commits.",
1218
+ defaultState: "inactive",
1219
+ canDisable: true
1220
+ },
1221
+ {
1222
+ name: "branch-guard",
1223
+ risk: "high",
1224
+ summary: "Blocks commits, pushes, and merges on protected branches.",
1225
+ defaultState: "active",
1226
+ canDisable: true
1227
+ },
1228
+ {
1229
+ name: "diff-summary",
1230
+ risk: "low",
1231
+ summary: "Injects compact git diff context after edits.",
1232
+ defaultState: "active",
1233
+ canDisable: true
1234
+ },
1235
+ {
1236
+ name: "commit-validator",
1237
+ risk: "medium",
1238
+ summary: "Validates conventional commit format before commits.",
1239
+ defaultState: "active",
1240
+ canDisable: true
1241
+ },
1242
+ {
1243
+ name: "format-on-save",
1244
+ risk: "medium",
1245
+ summary: "Runs formatter after write/edit tool calls.",
1246
+ defaultState: "inactive",
1247
+ canDisable: true
1248
+ },
1249
+ {
1250
+ name: "test-runner-gate",
1251
+ risk: "medium",
1252
+ summary: "Runs relevant tests after source edits when explicitly enabled.",
1253
+ defaultState: "inactive",
1254
+ canDisable: true
1255
+ },
1256
+ {
1257
+ name: "import-organizer",
1258
+ risk: "medium",
1259
+ summary: "Sorts imports and applies safe linter fixes after edits.",
1260
+ defaultState: "inactive",
1261
+ canDisable: true
1262
+ },
1263
+ {
1264
+ name: "knowledge-graph",
1265
+ risk: "low",
1266
+ summary: "Stores and queries durable project knowledge graph facts.",
1267
+ defaultState: "active",
1268
+ canDisable: true
1269
+ },
1270
+ {
1271
+ name: "todo-listener",
1272
+ risk: "low",
1273
+ summary: "Broadcasts todo tool updates to the project mailbox when explicitly enabled.",
1274
+ defaultState: "inactive",
1275
+ canDisable: true
1276
+ },
1277
+ {
1278
+ name: "session-recap",
1279
+ risk: "low",
1280
+ summary: "Posts a session recap to the project mailbox on stop.",
1281
+ defaultState: "inactive",
1282
+ canDisable: true
1283
+ },
1284
+ {
1285
+ name: "spec-linker",
1286
+ risk: "low",
1287
+ summary: "Finds unlinked plugin references in markdown edits.",
1288
+ defaultState: "inactive",
1289
+ canDisable: true
1290
+ },
1291
+ {
1292
+ name: "doc-sync-guard",
1293
+ risk: "low",
1294
+ summary: "Warns when docs drift from recently changed public source files when explicitly enabled.",
1295
+ defaultState: "inactive",
1296
+ canDisable: true
1297
+ },
1298
+ {
1299
+ name: "loop-breaker",
1300
+ risk: "low",
1301
+ summary: "Detects runaway tool-call loops; warns, then blocks repeats.",
1302
+ defaultState: "active",
1303
+ canDisable: true
1304
+ },
1305
+ {
1306
+ name: "path-guard",
1307
+ risk: "medium",
1308
+ summary: "Blocks writes and destructive commands on protected paths.",
1309
+ defaultState: "active",
1310
+ canDisable: true
1311
+ },
1312
+ {
1313
+ name: "context-pins",
1314
+ risk: "low",
1315
+ summary: "Pins durable facts into the system prompt across compactions.",
1316
+ defaultState: "active",
1317
+ canDisable: true
1318
+ },
1319
+ {
1320
+ name: "checkpoint",
1321
+ risk: "medium",
1322
+ summary: "Snapshots files before edits and restores pre-edit states.",
1323
+ defaultState: "active",
1324
+ canDisable: true
1325
+ },
1326
+ {
1327
+ name: "error-lens",
1328
+ risk: "low",
1329
+ summary: "Distills failed command output into compact error digests.",
1330
+ defaultState: "active",
1331
+ canDisable: true
1332
+ },
1333
+ {
1334
+ name: "dep-guard",
1335
+ risk: "medium",
1336
+ summary: "Supervises dependency installs: deny list and typosquat warnings.",
1337
+ defaultState: "active",
1338
+ canDisable: true
1339
+ },
1340
+ {
1341
+ name: "dependency-vulnerability-gate",
1342
+ risk: "high",
1343
+ summary: "Audits installed dependencies and blocks vulnerable installs.",
1344
+ defaultState: "active",
1345
+ canDisable: true
1346
+ },
1347
+ {
1348
+ name: "license-audit-gate",
1349
+ risk: "high",
1350
+ summary: "Audits dependency licenses after package installs.",
1351
+ defaultState: "inactive",
1352
+ canDisable: true
1353
+ },
1354
+ {
1355
+ name: "security-hotspot-scanner",
1356
+ risk: "high",
1357
+ summary: "Scans source files for common security anti-patterns on demand or when explicitly enabled.",
1358
+ defaultState: "inactive",
1359
+ canDisable: true
1360
+ },
1361
+ {
1362
+ name: "api-compatibility-gate",
1363
+ risk: "medium",
1364
+ summary: "Warns when public entry-point exports are removed when explicitly enabled.",
1365
+ defaultState: "inactive",
1366
+ canDisable: true
1367
+ },
1368
+ {
1369
+ name: "dead-code-detector",
1370
+ risk: "low",
1371
+ summary: "Scans for suspicious exported symbols that appear unused on demand or when explicitly enabled.",
1372
+ defaultState: "inactive",
1373
+ canDisable: true
1374
+ },
1375
+ {
1376
+ name: "migration-planner",
1377
+ risk: "low",
1378
+ summary: "Builds dependency or framework migration checklists.",
1379
+ defaultState: "inactive",
1380
+ canDisable: true
1381
+ },
1382
+ {
1383
+ name: "schema-evolution-guard",
1384
+ risk: "high",
1385
+ summary: "Warns on destructive database or API schema changes.",
1386
+ defaultState: "inactive",
1387
+ canDisable: true
1388
+ },
1389
+ {
1390
+ name: "semantic-search-indexer",
1391
+ risk: "low",
1392
+ summary: "Builds an in-memory keyword index for project source search.",
1393
+ defaultState: "inactive",
1394
+ canDisable: true
1395
+ },
1396
+ {
1397
+ name: "config-validator",
1398
+ risk: "low",
1399
+ summary: "Validates JSON/YAML/TOML files right after write/edit.",
1400
+ defaultState: "active",
1401
+ canDisable: true
1402
+ },
1403
+ {
1404
+ name: "notify-hub",
1405
+ risk: "medium",
1406
+ summary: "Sends session events and ad-hoc notifications to a webhook.",
1407
+ defaultState: "inactive",
1408
+ canDisable: true
1409
+ },
1410
+ {
1411
+ name: "changelog-writer",
1412
+ risk: "low",
1413
+ summary: "Collects session work and writes Keep-a-Changelog entries.",
1414
+ defaultState: "inactive",
1415
+ canDisable: true
1416
+ },
1417
+ {
1418
+ name: "injection-shield",
1419
+ risk: "low",
1420
+ summary: "Flags prompt-injection patterns in tool output.",
1421
+ defaultState: "active",
1422
+ canDisable: true
1423
+ },
1424
+ {
1425
+ name: "llm-cache",
1426
+ risk: "medium",
1427
+ summary: "Caches identical provider requests (opt-in; wraps every LLM call).",
1428
+ defaultState: "inactive",
1429
+ canDisable: true
1430
+ },
1431
+ {
1432
+ name: "model-router",
1433
+ risk: "medium",
1434
+ summary: "Routes each LLM call to a different model by size/tool rules (opt-in).",
1435
+ defaultState: "inactive",
1436
+ canDisable: true
1437
+ },
1438
+ {
1439
+ name: "pr-drafter",
1440
+ risk: "low",
1441
+ summary: "Generates session pull request draft markdown.",
1442
+ defaultState: "inactive",
1443
+ canDisable: true
1444
+ },
1445
+ {
1446
+ name: "prompt-firewall",
1447
+ risk: "high",
1448
+ summary: "Detects/redacts credential leaks on the provider wire (opt-in).",
1449
+ defaultState: "inactive",
1450
+ canDisable: true
1451
+ },
1452
+ {
1453
+ name: "auto-escalate",
1454
+ risk: "medium",
1455
+ summary: "Retries with an escalated model on transient provider errors (opt-in).",
1456
+ defaultState: "inactive",
1457
+ canDisable: true
1458
+ },
1459
+ {
1460
+ name: "token-throttle",
1461
+ risk: "medium",
1462
+ summary: "Rolling-window tokens/min rate limiting via provider-call delays (opt-in).",
1463
+ defaultState: "inactive",
1464
+ canDisable: true
1465
+ },
1466
+ {
1467
+ name: "plugin-stack-observer",
1468
+ risk: "low",
1469
+ summary: "Reports provider wrapper stack order and health.",
1470
+ defaultState: "inactive",
1471
+ canDisable: true
1472
+ },
1473
+ {
1474
+ name: "test-coverage-gate",
1475
+ risk: "medium",
1476
+ summary: "Checks coverage thresholds after test runs.",
1477
+ defaultState: "inactive",
1478
+ canDisable: true
1479
+ },
1480
+ {
1481
+ name: "test-flake-detector",
1482
+ risk: "medium",
1483
+ summary: "Runs tests repeatedly to identify flaky failures.",
1484
+ defaultState: "inactive",
1485
+ canDisable: true
1486
+ },
1487
+ {
1488
+ name: "performance-regression-gate",
1489
+ risk: "medium",
1490
+ summary: "Compares benchmark results and reports performance regressions.",
1491
+ defaultState: "inactive",
1492
+ canDisable: true
1493
+ },
1494
+ {
1495
+ name: "type-gate",
1496
+ risk: "medium",
1497
+ summary: "Runs TypeScript type checks after relevant edits when explicitly enabled.",
1498
+ defaultState: "inactive",
1499
+ canDisable: true
1500
+ },
1501
+ {
1502
+ name: "code-metrics",
1503
+ risk: "low",
1504
+ summary: "Computes LOC, comment ratio, function count, and approximate cyclomatic complexity on demand or when explicitly enabled.",
1505
+ defaultState: "inactive",
1506
+ canDisable: true
1507
+ },
1508
+ {
1509
+ name: "duplicate-code-detector",
1510
+ risk: "low",
1511
+ summary: "Detects duplicate or similar code blocks across source files on demand or when explicitly enabled.",
1512
+ defaultState: "inactive",
1513
+ canDisable: true
1514
+ },
1515
+ {
1516
+ name: "feature-flag-tracker",
1517
+ risk: "low",
1518
+ summary: "Scans source files for feature-flag usage and reports flag inventory on demand or when explicitly enabled.",
1519
+ defaultState: "inactive",
1520
+ canDisable: true
1521
+ },
1522
+ {
1523
+ name: "interface-contract-guard",
1524
+ risk: "medium",
1525
+ summary: "Warns when TypeScript interfaces change or lack implementers when explicitly enabled.",
1526
+ defaultState: "inactive",
1527
+ canDisable: true
1528
+ },
1529
+ {
1530
+ name: "refactor-suggester",
1531
+ risk: "low",
1532
+ summary: "Flags long functions, deep nesting, many parameters, magic numbers, and console.log on demand or when explicitly enabled.",
1533
+ defaultState: "inactive",
1534
+ canDisable: true
1535
+ },
1536
+ {
1537
+ name: "release-notes-generator",
1538
+ risk: "low",
1539
+ summary: "Groups conventional commits into a release-notes markdown draft.",
1540
+ defaultState: "inactive",
1541
+ canDisable: true
1542
+ },
1543
+ {
1544
+ name: "smart-rename",
1545
+ risk: "medium",
1546
+ summary: "Whole-word symbol rename with preview and optional apply.",
1547
+ defaultState: "inactive",
1548
+ canDisable: true
1549
+ },
1550
+ {
1551
+ name: "test-generator",
1552
+ risk: "low",
1553
+ summary: "Generates a unit-test skeleton from exported functions/classes.",
1554
+ defaultState: "inactive",
1555
+ canDisable: true
1556
+ },
1557
+ {
1558
+ name: "@wrongstack/plug-lsp",
1559
+ risk: "medium",
1560
+ summary: "Language Server Protocol tools and slash commands.",
1561
+ defaultState: "inactive",
1562
+ canDisable: true
1563
+ },
1564
+ {
1565
+ name: "telegram",
1566
+ risk: "medium",
1567
+ summary: "Telegram bridge for messages, approvals, and notifications.",
1568
+ defaultState: "inactive",
1569
+ canDisable: true
1570
+ }
1571
+ ];
1572
+ OFFICIAL_ALIASES = new Map(
1573
+ OFFICIAL_PLUGINS.flatMap((p) => [
1574
+ [p.alias, p.specifier],
1575
+ [p.specifier, p.specifier]
1576
+ ])
1577
+ );
1578
+ PLUGIN_LLM_USAGE = [
1579
+ "Usage:",
1580
+ " wstack plugin llm List every per-plugin LLM override.",
1581
+ " wstack plugin llm <plugin> Show one plugin's LLM override.",
1582
+ " wstack plugin llm <plugin> <provider> [model] Route this plugin through a provider (and model).",
1583
+ " wstack plugin llm <plugin> - <model> Keep the session provider, override only the model.",
1584
+ " wstack plugin llm <plugin> --clear Remove the override (back to the session default).",
1585
+ "",
1586
+ "The override is stored as config.extensions.<plugin>.llm and is used by",
1587
+ "every api.llm call the plugin makes. Without an override, plugins follow",
1588
+ "the active session provider/model (the chat leader)."
1589
+ ].join("\n");
1590
+ }
1591
+ });
642
1592
  function createApi(ownerName, base) {
643
1593
  return new DefaultPluginAPI({ ownerName, ...base });
644
1594
  }
@@ -697,9 +1647,11 @@ var init_utils = __esm({
697
1647
  // src/wiring/plugins.ts
698
1648
  var plugins_exports = {};
699
1649
  __export(plugins_exports, {
1650
+ BUILTIN_PLUGIN_CONFIG_NAMES: () => BUILTIN_PLUGIN_CONFIG_NAMES,
700
1651
  BUILTIN_PLUGIN_FACTORIES: () => BUILTIN_PLUGIN_FACTORIES,
701
1652
  DEPRECATED_PLUGIN_NAMES: () => DEPRECATED_PLUGIN_NAMES,
702
1653
  _resetDeprecatedWarningsForTests: () => _resetDeprecatedWarningsForTests,
1654
+ builtinPluginNameFromSpec: () => builtinPluginNameFromSpec,
703
1655
  pluginNameFromSpec: () => pluginNameFromSpec,
704
1656
  setupPlugins: () => setupPlugins,
705
1657
  warnIfDeprecatedPluginName: () => warnIfDeprecatedPluginName
@@ -724,6 +1676,13 @@ function pluginNameFromSpec(spec) {
724
1676
  if (!last) return null;
725
1677
  return last.split("?")[0] ?? null;
726
1678
  }
1679
+ function builtinPluginNameFromSpec(spec) {
1680
+ if (BUILTIN_PLUGIN_CONFIG_ALIASES[spec]) return BUILTIN_PLUGIN_CONFIG_ALIASES[spec];
1681
+ const bareName = pluginNameFromSpec(spec);
1682
+ if (BUILTIN_PLUGIN_CONFIG_NAMES.has(spec)) return bareName ?? spec;
1683
+ if (bareName && BUILTIN_PLUGIN_CONFIG_NAMES.has(bareName)) return bareName;
1684
+ return null;
1685
+ }
727
1686
  async function setupPlugins(params) {
728
1687
  const {
729
1688
  config,
@@ -751,13 +1710,25 @@ async function setupPlugins(params) {
751
1710
  const disabledBuiltins = new Set(
752
1711
  (config.plugins ?? []).filter(
753
1712
  (p) => typeof p === "object" && p.enabled === false
754
- ).map((p) => p.name)
1713
+ ).flatMap((p) => {
1714
+ const builtinName = builtinPluginNameFromSpec(p.name);
1715
+ return builtinName && builtinName !== p.name ? [p.name, builtinName] : [p.name];
1716
+ })
755
1717
  );
756
1718
  if (paths && config.features?.plugins !== false) {
757
1719
  for (const factory of BUILTIN_PLUGIN_FACTORIES) {
758
1720
  try {
759
1721
  const plugin = await factory();
760
1722
  if (!plugin) continue;
1723
+ const auditEntry = PLUGIN_AUDIT_ENTRIES.find((entry) => entry.name === plugin.name);
1724
+ const explicitlyEnabled = (config.plugins ?? []).some((entry) => {
1725
+ const spec = typeof entry === "string" ? entry : entry.name;
1726
+ if (typeof entry === "object" && entry.enabled === false) return false;
1727
+ return (builtinPluginNameFromSpec(spec) ?? spec) === plugin.name;
1728
+ });
1729
+ if (auditEntry?.defaultState === "inactive" && !explicitlyEnabled) {
1730
+ continue;
1731
+ }
761
1732
  if (disabledBuiltins.has(plugin.name)) {
762
1733
  log.info(`[setupPlugins] built-in plugin "${plugin.name}" disabled by config`);
763
1734
  continue;
@@ -778,6 +1749,9 @@ async function setupPlugins(params) {
778
1749
  if (bareName && warnIfDeprecatedPluginName(bareName, log)) {
779
1750
  continue;
780
1751
  }
1752
+ if (builtinPluginNameFromSpec(spec)) {
1753
+ continue;
1754
+ }
781
1755
  try {
782
1756
  const mod = await import(spec);
783
1757
  if (mod.default) userPlugins.push(mod.default);
@@ -857,9 +1831,10 @@ function buildPluginOptions(config) {
857
1831
  }
858
1832
  return options;
859
1833
  }
860
- var DEPRECATED_PLUGIN_NAMES, deprecatedWarningsEmitted, BUILTIN_PLUGIN_FACTORIES;
1834
+ var DEPRECATED_PLUGIN_NAMES, deprecatedWarningsEmitted, BUILTIN_PLUGIN_CONFIG_ALIASES, BUILTIN_PLUGIN_CONFIG_NAMES, BUILTIN_PLUGIN_FACTORIES;
861
1835
  var init_plugins = __esm({
862
1836
  "src/wiring/plugins.ts"() {
1837
+ init_plugin_management();
863
1838
  init_plugin_api_factory();
864
1839
  init_utils();
865
1840
  DEPRECATED_PLUGIN_NAMES = {
@@ -867,6 +1842,15 @@ var init_plugins = __esm({
867
1842
  "json-path": "use the built-in `json` tool with action: query | validate | transform | merge"
868
1843
  };
869
1844
  deprecatedWarningsEmitted = /* @__PURE__ */ new Set();
1845
+ BUILTIN_PLUGIN_CONFIG_ALIASES = {
1846
+ lsp: "@wrongstack/plug-lsp",
1847
+ "@wrongstack/telegram": "telegram"
1848
+ };
1849
+ BUILTIN_PLUGIN_CONFIG_NAMES = /* @__PURE__ */ new Set([
1850
+ ...PLUGIN_AUDIT_ENTRIES.map((entry) => entry.name),
1851
+ ...PLUGIN_AUDIT_ENTRIES.map((entry) => `@wrongstack/plugins/${entry.name}`),
1852
+ ...Object.keys(BUILTIN_PLUGIN_CONFIG_ALIASES)
1853
+ ]);
870
1854
  BUILTIN_PLUGIN_FACTORIES = [
871
1855
  async () => {
872
1856
  const { createPromptsPlugin } = await import('@wrongstack/core');
@@ -3664,7 +4648,7 @@ async function boot(){
3664
4648
  dagre = dmod && dmod.default && dmod.default.graphlib ? dmod.default : dmod;
3665
4649
  if(!React || !createRoot || !RF){ throw new Error('cdn-incomplete'); }
3666
4650
  } catch(e){
3667
- try { console.error('HQ: CDN load failed, using offline fallback \u2014', e && e.message); } catch(_e){}
4651
+ try { console.error(e); } catch(_e){}
3668
4652
  renderFallback();
3669
4653
  connectWs();
3670
4654
  primeSnapshot();
@@ -4230,12 +5214,12 @@ function resolveHqDistDir() {
4230
5214
  try {
4231
5215
  const entry = requireFromHere.resolve("@wrongstack/webui-hq");
4232
5216
  const dir = path5.dirname(entry);
4233
- if (fs27.existsSync(path5.join(dir, "index.html"))) {
5217
+ if (fs28.existsSync(path5.join(dir, "index.html"))) {
4234
5218
  cachedDistDir = dir;
4235
5219
  return dir;
4236
5220
  }
4237
5221
  const distSub = path5.join(path5.dirname(entry), "dist");
4238
- if (fs27.existsSync(path5.join(distSub, "index.html"))) {
5222
+ if (fs28.existsSync(path5.join(distSub, "index.html"))) {
4239
5223
  cachedDistDir = distSub;
4240
5224
  return distSub;
4241
5225
  }
@@ -4254,10 +5238,10 @@ function serveHqStatic(_req, res, urlPath, distDir) {
4254
5238
  if (!isInsideDist(filePath, distDir)) {
4255
5239
  return { handled: false };
4256
5240
  }
4257
- if (!fs27.existsSync(filePath) || fs27.statSync(filePath).isDirectory()) {
5241
+ if (!fs28.existsSync(filePath) || fs28.statSync(filePath).isDirectory()) {
4258
5242
  const indexPath = path5.join(distDir, "index.html");
4259
- if (fs27.existsSync(indexPath)) {
4260
- const html = fs27.readFileSync(indexPath, "utf8");
5243
+ if (fs28.existsSync(indexPath)) {
5244
+ const html = fs28.readFileSync(indexPath, "utf8");
4261
5245
  res.writeHead(200, { "Content-Type": MIME[".html"] });
4262
5246
  res.end(html);
4263
5247
  return { handled: true };
@@ -4266,7 +5250,7 @@ function serveHqStatic(_req, res, urlPath, distDir) {
4266
5250
  }
4267
5251
  const ext = path5.extname(filePath).toLowerCase();
4268
5252
  const mime = MIME[ext] ?? "application/octet-stream";
4269
- const data = fs27.readFileSync(filePath);
5253
+ const data = fs28.readFileSync(filePath);
4270
5254
  res.writeHead(200, { "Content-Type": mime });
4271
5255
  res.end(data);
4272
5256
  return { handled: true };
@@ -6404,6 +7388,29 @@ var init_cost_helpers = __esm({
6404
7388
  function currentSessionId2(ctx) {
6405
7389
  return ctx.agent.ctx.session?.id ?? ctx.sessionId;
6406
7390
  }
7391
+ function toolEntries(ctx) {
7392
+ const registry = ctx.agent.tools;
7393
+ if (typeof registry.listWithOwner === "function") {
7394
+ return [
7395
+ ...registry.listWithOwner(),
7396
+ ...typeof registry.listDisabled === "function" ? registry.listDisabled() : []
7397
+ ];
7398
+ }
7399
+ return (registry.list?.() ?? []).map((tool) => ({ tool, owner: "core" }));
7400
+ }
7401
+ function persistDisabledTool(ctx, name, disabled) {
7402
+ const configStore = ctx.configStore;
7403
+ if (!configStore) return;
7404
+ const currentTools = configStore.get().tools ?? {};
7405
+ const disabledTools = new Set(currentTools.disabledTools ?? []);
7406
+ if (disabled) disabledTools.add(name);
7407
+ else disabledTools.delete(name);
7408
+ configStore.update({ tools: { ...currentTools, disabledTools: Array.from(disabledTools) } });
7409
+ }
7410
+ function toolNameFromPayload(payload) {
7411
+ const name = payload?.name;
7412
+ return typeof name === "string" && name.trim().length > 0 ? name.trim() : void 0;
7413
+ }
6407
7414
  async function handleSkillsList(ctx, ws) {
6408
7415
  if (!ctx.skillLoader) {
6409
7416
  ctx.send(ws, { type: "skills.list", payload: { skills: [], enabled: false } });
@@ -6440,17 +7447,42 @@ async function handleSkillsList(ctx, ws) {
6440
7447
  }
6441
7448
  }
6442
7449
  function handleToolsList(ctx, ws) {
6443
- const list = ctx.agent.tools.list().map((t) => {
6444
- const schema = t.inputSchema ?? {};
7450
+ const registry = ctx.agent.tools;
7451
+ const list = toolEntries(ctx).map(({ tool, owner }) => {
7452
+ const schema = tool.inputSchema ?? {};
6445
7453
  const params = schema.properties ? Object.keys(schema.properties) : [];
6446
7454
  return {
6447
- name: t.name,
6448
- description: t.description ?? "",
6449
- params
7455
+ name: tool.name,
7456
+ owner,
7457
+ description: tool.description ?? "",
7458
+ params,
7459
+ disabled: registry.isDisabled?.(tool.name) ?? false,
7460
+ mutating: !!tool.mutating,
7461
+ permission: tool.permission ?? "auto"
6450
7462
  };
6451
7463
  });
6452
7464
  ctx.send(ws, { type: "tools.list", payload: { tools: list } });
6453
7465
  }
7466
+ function handleToolDisable(ctx, ws, payload) {
7467
+ const name = toolNameFromPayload(payload);
7468
+ if (!name) {
7469
+ ctx.send(ws, { type: "error", payload: { message: "tool.disable requires a name" } });
7470
+ return;
7471
+ }
7472
+ const ok2 = ctx.agent.tools.disable(name);
7473
+ if (ok2) persistDisabledTool(ctx, name, true);
7474
+ ctx.send(ws, { type: "tool.disabled", payload: { name, ok: ok2 } });
7475
+ }
7476
+ function handleToolEnable(ctx, ws, payload) {
7477
+ const name = toolNameFromPayload(payload);
7478
+ if (!name) {
7479
+ ctx.send(ws, { type: "error", payload: { message: "tool.enable requires a name" } });
7480
+ return;
7481
+ }
7482
+ const ok2 = ctx.agent.tools.enable(name);
7483
+ if (ok2) persistDisabledTool(ctx, name, false);
7484
+ ctx.send(ws, { type: "tool.enabled", payload: { name, ok: ok2 } });
7485
+ }
6454
7486
  function handleDiagGet(ctx, ws) {
6455
7487
  const actx = ctx.agent.ctx;
6456
7488
  const tools = ctx.agent.tools.list();
@@ -7982,8 +9014,9 @@ async function handleKanbanMessage(ctx, ws, msg) {
7982
9014
  fail(ctx, ws, type, "boardId required");
7983
9015
  return;
7984
9016
  }
7985
- const removed = await removeBoard(projectRoot, id);
7986
- ok(ctx, ws, "kanban.delete", { removed });
9017
+ const board = await getBoard(projectRoot, id);
9018
+ const removed = board ? await removeBoard(projectRoot, board.id) : false;
9019
+ ok(ctx, ws, "kanban.delete", { removed, boardId: board?.id ?? id });
7987
9020
  return;
7988
9021
  }
7989
9022
  // ── Board generate ──
@@ -8007,6 +9040,88 @@ async function handleKanbanMessage(ctx, ws, msg) {
8007
9040
  ok(ctx, ws, "kanban.generate", fresh ?? board);
8008
9041
  return;
8009
9042
  }
9043
+ // ── Ready tasks ──
9044
+ case "kanban.task.ready": {
9045
+ const tasks = await listReadyTasks(projectRoot, {
9046
+ ...payload?.boardId ? { boardId: payload.boardId } : {},
9047
+ ...payload?.query ? { query: payload.query } : {},
9048
+ ...payload?.assignedAgent ? { assignedAgent: payload.assignedAgent } : {},
9049
+ ...payload?.priority ? { priority: payload.priority } : {},
9050
+ ...payload?.label ? { label: payload.label } : {},
9051
+ ...payload?.chainId ? { chainId: payload.chainId } : {},
9052
+ ...typeof payload?.limit === "number" ? { limit: payload.limit } : {}
9053
+ });
9054
+ ok(ctx, ws, type, tasks);
9055
+ return;
9056
+ }
9057
+ case "kanban.snapshot": {
9058
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
9059
+ ...payload?.boardId ? { boardId: payload.boardId } : {},
9060
+ ...payload?.query ? { query: payload.query } : {},
9061
+ ...payload?.assignedAgent ? { assignedAgent: payload.assignedAgent } : {},
9062
+ ...payload?.priority ? { priority: payload.priority } : {},
9063
+ ...payload?.status ? { status: payload.status } : {},
9064
+ ...payload?.label ? { label: payload.label } : {},
9065
+ ...payload?.chainId ? { chainId: payload.chainId } : {}
9066
+ });
9067
+ ok(ctx, ws, type, snapshot);
9068
+ return;
9069
+ }
9070
+ case "kanban.taskgraph.export": {
9071
+ const boardId = payload?.boardId;
9072
+ if (!boardId) {
9073
+ fail(ctx, ws, type, "boardId required");
9074
+ return;
9075
+ }
9076
+ const exported = await exportBoardToTaskGraph(projectRoot, boardId, {
9077
+ ...payload?.graphId ? { graphId: payload.graphId } : {},
9078
+ ...payload?.specId ? { specId: payload.specId } : {},
9079
+ ...payload?.title ? { title: payload.title } : {},
9080
+ ...typeof payload?.preserveOriginTaskIds === "boolean" ? { preserveOriginTaskIds: payload.preserveOriginTaskIds } : {},
9081
+ ...typeof payload?.includeArchived === "boolean" ? { includeArchived: payload.includeArchived } : {}
9082
+ });
9083
+ if (!exported) {
9084
+ fail(ctx, ws, type, `Board not found: ${boardId}`);
9085
+ return;
9086
+ }
9087
+ ok(ctx, ws, type, {
9088
+ board: exported.board,
9089
+ taskGraph: serializeTaskGraph(exported.graph)
9090
+ });
9091
+ return;
9092
+ }
9093
+ case "kanban.taskgraph.sync": {
9094
+ const boardId = payload?.boardId;
9095
+ const taskGraph = payload?.taskGraph;
9096
+ if (!boardId || !taskGraph) {
9097
+ fail(ctx, ws, type, "boardId and taskGraph required");
9098
+ return;
9099
+ }
9100
+ const result = await syncBoardFromTaskGraph(
9101
+ projectRoot,
9102
+ boardId,
9103
+ deserializeTaskGraph(taskGraph),
9104
+ {
9105
+ ...payload?.title ? { title: payload.title } : {},
9106
+ ...payload?.description ? { description: payload.description } : {},
9107
+ ...payload?.tags ? { tags: payload.tags } : {},
9108
+ ...payload?.generatedBy ? { generatedBy: payload.generatedBy } : {},
9109
+ ...payload?.sourceSystem ? { sourceSystem: payload.sourceSystem } : {},
9110
+ ...payload?.phaseId ? { phaseId: payload.phaseId } : {},
9111
+ ...typeof payload?.includeCompletedTasks === "boolean" ? { includeCompletedTasks: payload.includeCompletedTasks } : {},
9112
+ ...typeof payload?.archiveMissingTasks === "boolean" ? { archiveMissingTasks: payload.archiveMissingTasks } : {},
9113
+ ...typeof payload?.preserveManualDependencies === "boolean" ? { preserveManualDependencies: payload.preserveManualDependencies } : {}
9114
+ }
9115
+ );
9116
+ result ? ok(ctx, ws, type, {
9117
+ board: result.board,
9118
+ taskIdMap: Object.fromEntries(result.taskIdMap),
9119
+ createdTaskIds: result.createdTaskIds,
9120
+ updatedTaskIds: result.updatedTaskIds,
9121
+ archivedTaskIds: result.archivedTaskIds
9122
+ }) : fail(ctx, ws, type, `Board not found: ${boardId}`);
9123
+ return;
9124
+ }
8010
9125
  // ── Task add ──
8011
9126
  case "kanban.task.add": {
8012
9127
  const boardId = payload?.boardId;
@@ -8031,6 +9146,65 @@ async function handleKanbanMessage(ctx, ws, msg) {
8031
9146
  ok(ctx, ws, "kanban.task.add", result.task);
8032
9147
  return;
8033
9148
  }
9149
+ // ── Task split ──
9150
+ case "kanban.task.split": {
9151
+ const boardId = payload?.boardId;
9152
+ const taskId = payload?.taskId;
9153
+ const childTitles = payload?.childTitles;
9154
+ if (!boardId || !taskId || !childTitles?.length) {
9155
+ fail(ctx, ws, type, "boardId, taskId, and childTitles required");
9156
+ return;
9157
+ }
9158
+ const result = await splitTask(projectRoot, boardId, taskId, {
9159
+ titles: childTitles,
9160
+ ...payload?.targetColumnId ? { columnId: payload.targetColumnId } : {},
9161
+ ...typeof payload?.inheritAssignment === "boolean" ? { inheritAssignment: payload.inheritAssignment } : {},
9162
+ ...typeof payload?.inheritLabels === "boolean" ? { inheritLabels: payload.inheritLabels } : {},
9163
+ ...typeof payload?.inheritSuccessCriteria === "boolean" ? { inheritSuccessCriteria: payload.inheritSuccessCriteria } : {},
9164
+ ...typeof payload?.inheritGoalMetrics === "boolean" ? { inheritGoalMetrics: payload.inheritGoalMetrics } : {},
9165
+ ...typeof payload?.inheritDependencies === "boolean" ? { inheritDependencies: payload.inheritDependencies } : {},
9166
+ ...typeof payload?.chainChildren === "boolean" ? { chainChildren: payload.chainChildren } : {},
9167
+ ...typeof payload?.rewireDependents === "boolean" ? { rewireDependents: payload.rewireDependents } : {}
9168
+ });
9169
+ if (!result) {
9170
+ fail(ctx, ws, type, "Board or task not found");
9171
+ return;
9172
+ }
9173
+ ok(ctx, ws, type, {
9174
+ board: result.board,
9175
+ parent: result.parent,
9176
+ children: result.children
9177
+ });
9178
+ return;
9179
+ }
9180
+ // ── Task merge ──
9181
+ case "kanban.task.merge": {
9182
+ const boardId = payload?.boardId;
9183
+ const taskIds = payload?.taskIds;
9184
+ const title = payload?.title;
9185
+ if (!boardId || !taskIds?.length || !title) {
9186
+ fail(ctx, ws, type, "boardId, taskIds, and title required");
9187
+ return;
9188
+ }
9189
+ const result = await mergeTasks(projectRoot, boardId, {
9190
+ taskIds,
9191
+ title,
9192
+ ...payload?.description ? { description: payload.description } : {},
9193
+ ...payload?.targetColumnId ? { targetColumnId: payload.targetColumnId } : {},
9194
+ ...typeof payload?.preserveAssignment === "boolean" ? { preserveAssignment: payload.preserveAssignment } : {},
9195
+ ...typeof payload?.closeSourceTasks === "boolean" ? { closeSourceTasks: payload.closeSourceTasks } : {}
9196
+ });
9197
+ if (!result) {
9198
+ fail(ctx, ws, type, "Board or task not found");
9199
+ return;
9200
+ }
9201
+ ok(ctx, ws, type, {
9202
+ board: result.board,
9203
+ task: result.task,
9204
+ sourceTasks: result.sourceTasks
9205
+ });
9206
+ return;
9207
+ }
8034
9208
  // ── Task update ──
8035
9209
  case "kanban.task.update": {
8036
9210
  const boardId = payload?.boardId;
@@ -8105,6 +9279,123 @@ async function handleKanbanMessage(ctx, ws, msg) {
8105
9279
  ok(ctx, ws, type, result.targetBoard);
8106
9280
  return;
8107
9281
  }
9282
+ // ── Task chain ──
9283
+ case "kanban.task.chain": {
9284
+ const boardId = payload?.boardId;
9285
+ const taskIds = payload?.taskIds;
9286
+ if (!boardId || !taskIds?.length) {
9287
+ fail(ctx, ws, type, "boardId and taskIds required");
9288
+ return;
9289
+ }
9290
+ const result = await setTaskChain(projectRoot, boardId, {
9291
+ taskIds,
9292
+ ...payload?.chainId ? { chainId: payload.chainId } : {},
9293
+ ...typeof payload?.enforceDependencies === "boolean" ? { enforceDependencies: payload.enforceDependencies } : {}
9294
+ });
9295
+ if (!result) {
9296
+ fail(ctx, ws, type, "Board or task not found");
9297
+ return;
9298
+ }
9299
+ ok(ctx, ws, type, {
9300
+ board: result.board,
9301
+ chainId: result.chainId,
9302
+ tasks: result.tasks
9303
+ });
9304
+ return;
9305
+ }
9306
+ case "kanban.task.chain.get": {
9307
+ const boardId = payload?.boardId;
9308
+ const taskOrChainId = payload?.taskId ?? payload?.chainId;
9309
+ if (!boardId || !taskOrChainId) {
9310
+ fail(ctx, ws, type, "boardId and taskId or chainId required");
9311
+ return;
9312
+ }
9313
+ const result = await getTaskChain(projectRoot, boardId, taskOrChainId);
9314
+ if (!result) {
9315
+ fail(ctx, ws, type, "Chain not found");
9316
+ return;
9317
+ }
9318
+ ok(ctx, ws, type, {
9319
+ board: result.board,
9320
+ chainId: result.chainId,
9321
+ tasks: result.tasks
9322
+ });
9323
+ return;
9324
+ }
9325
+ // ── Task claim/release ──
9326
+ case "kanban.task.claim": {
9327
+ const result = await claimReadyTask(projectRoot, {
9328
+ ...payload?.boardId ? { boardId: payload.boardId } : {},
9329
+ ...payload?.taskId ? { taskId: payload.taskId } : {},
9330
+ ...payload?.agentId ? { agentId: payload.agentId } : {},
9331
+ ...payload?.name ? { name: payload.name } : {},
9332
+ ...payload?.role ? { role: payload.role } : {},
9333
+ ...payload?.provider ? { provider: payload.provider } : {},
9334
+ ...payload?.model ? { model: payload.model } : {},
9335
+ ...payload?.fallbackProfile ? { fallbackProfile: payload.fallbackProfile } : {},
9336
+ ...payload?.fallbackModels ? { fallbackModels: payload.fallbackModels } : {},
9337
+ ...payload?.tools ? { tools: payload.tools } : {},
9338
+ ...payload?.allowedCapabilities ? { allowedCapabilities: payload.allowedCapabilities } : {},
9339
+ ...payload?.assignee ? { assignee: payload.assignee } : {},
9340
+ status: payload?.assignmentStatus ?? "queued"
9341
+ });
9342
+ result ? ok(ctx, ws, type, { board: result.board, task: result.task }) : fail(ctx, ws, type, "No ready kanban task matched the claim");
9343
+ return;
9344
+ }
9345
+ case "kanban.task.release": {
9346
+ const boardId = payload?.boardId;
9347
+ const taskId = payload?.taskId;
9348
+ if (!boardId || !taskId) {
9349
+ fail(ctx, ws, type, "boardId and taskId required");
9350
+ return;
9351
+ }
9352
+ const board = await releaseTaskClaim(projectRoot, boardId, taskId, {
9353
+ ...payload?.status ? { status: payload.status } : {},
9354
+ ...payload?.reason ? { reason: payload.reason } : {},
9355
+ ...typeof payload?.clearAssignee === "boolean" ? { clearAssignee: payload.clearAssignee } : {}
9356
+ });
9357
+ board ? ok(ctx, ws, type, board) : fail(ctx, ws, type, "Board or task not found");
9358
+ return;
9359
+ }
9360
+ // ── Goal metrics ──
9361
+ case "kanban.task.metric.add": {
9362
+ const boardId = payload?.boardId;
9363
+ const taskId = payload?.taskId;
9364
+ const name = payload?.name;
9365
+ if (!boardId || !taskId || !name) {
9366
+ fail(ctx, ws, type, "boardId, taskId, and name required");
9367
+ return;
9368
+ }
9369
+ const board = await addGoalMetricToTask(projectRoot, boardId, taskId, {
9370
+ name,
9371
+ ...payload?.status ? { status: payload.status } : {},
9372
+ ...payload?.target !== void 0 ? { target: payload.target } : {},
9373
+ ...payload?.current !== void 0 ? { current: payload.current } : {},
9374
+ ...payload?.unit ? { unit: payload.unit } : {},
9375
+ ...payload?.notes ? { notes: payload.notes } : {}
9376
+ });
9377
+ board ? ok(ctx, ws, type, board) : fail(ctx, ws, type, "Board or task not found");
9378
+ return;
9379
+ }
9380
+ case "kanban.task.metric.update": {
9381
+ const boardId = payload?.boardId;
9382
+ const taskId = payload?.taskId;
9383
+ const metricId = payload?.metricId;
9384
+ if (!boardId || !taskId || !metricId) {
9385
+ fail(ctx, ws, type, "boardId, taskId, and metricId required");
9386
+ return;
9387
+ }
9388
+ const board = await updateGoalMetricOnTask(projectRoot, boardId, taskId, metricId, {
9389
+ ...payload?.name ? { name: payload.name } : {},
9390
+ ...payload?.status ? { status: payload.status } : {},
9391
+ ...payload?.target !== void 0 ? { target: payload.target } : {},
9392
+ ...payload?.current !== void 0 ? { current: payload.current } : {},
9393
+ ...payload?.unit ? { unit: payload.unit } : {},
9394
+ ...payload?.notes ? { notes: payload.notes } : {}
9395
+ });
9396
+ board ? ok(ctx, ws, type, board) : fail(ctx, ws, type, "Metric not found");
9397
+ return;
9398
+ }
8108
9399
  // ── Task assign ──
8109
9400
  case "kanban.task.assign": {
8110
9401
  const bId = payload?.boardId;
@@ -8183,7 +9474,7 @@ async function handleKanbanMessage(ctx, ws, msg) {
8183
9474
  const completedTask = completed?.tasks.find((candidate) => candidate.id === task.id) ?? task;
8184
9475
  ctx.broadcast({
8185
9476
  type: "kanban.task.update",
8186
- payload: { success: true, data: completedTask }
9477
+ payload: { success: true, data: { boardId: board.id, task: completedTask } }
8187
9478
  });
8188
9479
  ctx.broadcast({
8189
9480
  type: "kanban.list",
@@ -8199,6 +9490,7 @@ async function handleKanbanMessage(ctx, ws, msg) {
8199
9490
  lastResult: summary
8200
9491
  });
8201
9492
  ok(ctx, ws, "kanban.task.dispatch", {
9493
+ boardId: board.id,
8202
9494
  task: updated?.tasks.find((candidate) => candidate.id === task.id) ?? task,
8203
9495
  summary
8204
9496
  });
@@ -8221,12 +9513,22 @@ async function handleKanbanMessage(ctx, ws, msg) {
8221
9513
  fail(ctx, ws, type, "boardId and taskId required");
8222
9514
  return;
8223
9515
  }
9516
+ const task = await getTask(projectRoot, rmBoardId, rmTaskId);
9517
+ if (!task) {
9518
+ fail(ctx, ws, type, "Board or task not found");
9519
+ return;
9520
+ }
8224
9521
  const board = await removeTask(projectRoot, rmBoardId, rmTaskId);
8225
9522
  if (!board) {
8226
9523
  fail(ctx, ws, type, "Board or task not found");
8227
9524
  return;
8228
9525
  }
8229
- ok(ctx, ws, "kanban.task.remove", { removed: true });
9526
+ ok(ctx, ws, "kanban.task.remove", {
9527
+ removed: true,
9528
+ boardId: board.id,
9529
+ taskId: task.id,
9530
+ board
9531
+ });
8230
9532
  return;
8231
9533
  }
8232
9534
  // ── Task get ──
@@ -8281,7 +9583,12 @@ async function handleKanbanMessage(ctx, ws, msg) {
8281
9583
  fail(ctx, ws, type, `Column not found: ${rmColId}`);
8282
9584
  return;
8283
9585
  }
8284
- ok(ctx, ws, "kanban.column.remove", { removed: true });
9586
+ ok(ctx, ws, "kanban.column.remove", {
9587
+ removed: true,
9588
+ boardId: updated.id,
9589
+ columnId: rmColId,
9590
+ board: updated
9591
+ });
8285
9592
  return;
8286
9593
  }
8287
9594
  default:
@@ -8296,6 +9603,15 @@ async function handleKanbanMessage(ctx, ws, msg) {
8296
9603
  function buildKanbanAgentPrompt2(board, task, assignment) {
8297
9604
  const dependencies = (task.dependsOn ?? []).map((depId) => board.tasks.find((candidate) => candidate.id === depId)).filter((dep) => Boolean(dep)).map((dep) => `- ${dep.title} [${dep.status}] (${dep.id})`);
8298
9605
  const checks = task.successCriteria?.map((check) => `- ${check.description}`).join("\n");
9606
+ const metrics = task.goalMetrics?.map(
9607
+ (metric) => `- ${metric.name}: ${metric.current ?? "n/a"}${metric.target !== void 0 ? ` / ${metric.target}` : ""}${metric.unit ? ` ${metric.unit}` : ""} [${metric.status}]`
9608
+ ).join("\n");
9609
+ const chain = task.chain ? [
9610
+ `chainId: ${task.chain.chainId}`,
9611
+ `order: ${task.chain.order}`,
9612
+ task.chain.previousTaskId ? `previous: ${task.chain.previousTaskId}` : "",
9613
+ task.chain.nextTaskId ? `next: ${task.chain.nextTaskId}` : ""
9614
+ ].filter(Boolean).join("\n") : "";
8299
9615
  const routing = [
8300
9616
  assignment?.role ? `role: ${assignment.role}` : "",
8301
9617
  assignment?.provider ? `provider: ${assignment.provider}` : "",
@@ -8314,10 +9630,14 @@ function buildKanbanAgentPrompt2(board, task, assignment) {
8314
9630
  ${task.description}` : "",
8315
9631
  routing.length ? `Routing hints:
8316
9632
  ${routing.join("\n")}` : "",
9633
+ chain ? `Task chain:
9634
+ ${chain}` : "",
8317
9635
  dependencies.length ? `Dependencies:
8318
9636
  ${dependencies.join("\n")}` : "",
8319
9637
  checks ? `Success criteria:
8320
9638
  ${checks}` : "",
9639
+ metrics ? `Goal metrics:
9640
+ ${metrics}` : "",
8321
9641
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
8322
9642
  "",
8323
9643
  "Work the task end-to-end. Use the kanban tool, not direct file edits, to update this task.",
@@ -8629,6 +9949,8 @@ function createMessageRouter(deps) {
8629
9949
  });
8630
9950
  },
8631
9951
  "tools.list": (_msg, ws) => handleToolsList(introspectionCtx, ws),
9952
+ "tool.disable": (msg, ws) => handleToolDisable(introspectionCtx, ws, msg.payload),
9953
+ "tool.enable": (msg, ws) => handleToolEnable(introspectionCtx, ws, msg.payload),
8632
9954
  // ── Autonomy ──
8633
9955
  "autonomy.switch": (msg, ws) => handleAutonomySwitch(prefsCtx, ws, msg.payload.mode),
8634
9956
  // ── Brain ──
@@ -10251,7 +11573,16 @@ async function runWebUI(opts) {
10251
11573
  );
10252
11574
  }
10253
11575
  },
10254
- { warn: (m) => console.warn(`[WebUI] Config watcher: ${m}`) }
11576
+ {
11577
+ warn: (m) => console.warn(
11578
+ JSON.stringify({
11579
+ level: "warn",
11580
+ event: "webui.config_watcher",
11581
+ message: m,
11582
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11583
+ })
11584
+ )
11585
+ }
10255
11586
  );
10256
11587
  credentialWatcherClose = watcher.close;
10257
11588
  }
@@ -10273,6 +11604,7 @@ async function runWebUI(opts) {
10273
11604
  projectRoot: opts.projectRoot,
10274
11605
  sessionId: opts.session.id,
10275
11606
  sessionStartedAt,
11607
+ configStore: opts.agent.container?.safeResolve?.(TOKENS.ConfigStore),
10276
11608
  send: send2,
10277
11609
  broadcast,
10278
11610
  log: (m) => console.log(m)
@@ -10846,11 +12178,12 @@ async function runClaudeOAuthLogin(deps, opts = {}) {
10846
12178
  const saved = await saveClaudeTokens(deps, providerId, tokens, models);
10847
12179
  if (!saved) return 1;
10848
12180
  deps.renderer.write(color.green("\n \u2713 Signed in with Claude!\n"));
10849
- const modelHint = models.find((m) => m.includes("sonnet")) ?? models[0] ?? "claude-sonnet-4-6";
12181
+ const modelHint = models[0];
10850
12182
  deps.renderer.writeInfo(
10851
12183
  ` Saved as provider ${color.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
10852
- Use: ${color.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
10853
- ` + color.dim(" Tokens refresh automatically before they expire.\n")
12184
+ ` + (modelHint ? ` Use: ${color.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
12185
+ ` : ` No model list was discovered; choose a model before starting a run.
12186
+ `) + color.dim(" Tokens refresh automatically before they expire.\n")
10854
12187
  );
10855
12188
  return 0;
10856
12189
  } catch (err) {
@@ -10880,8 +12213,6 @@ async function saveClaudeTokens(deps, providerId, tokens, models) {
10880
12213
  p.family = "anthropic-oauth";
10881
12214
  if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
10882
12215
  if (models.length > 0) p.models = models;
10883
- else if (!p.models || p.models.length === 0)
10884
- p.models = ["claude-sonnet-4-6", "claude-opus-4-8"];
10885
12216
  const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
10886
12217
  keys.push(entry);
10887
12218
  writeKeysBack(p, keys);
@@ -15863,6 +17194,10 @@ function buildBrainCommand(opts) {
15863
17194
  const trimmed = args.trim();
15864
17195
  const [sub, ...rest] = trimmed.split(/\s+/);
15865
17196
  const subcommand = (sub ?? "").toLowerCase();
17197
+ if ((subcommand === "" || subcommand === "status") && opts.onPanelOpen?.current) {
17198
+ const opened = opts.onPanelOpen.current("brainOpen");
17199
+ if (opened) return { message: "" };
17200
+ }
15866
17201
  if (subcommand === "risk") {
15867
17202
  if (!opts.brainSettings) {
15868
17203
  const msg3 = "Brain settings are not available in this session.";
@@ -16669,6 +18004,10 @@ ${description}`
16669
18004
  return { message: `Task ${taskId.slice(0, 8)} marked failed: ${reason}` };
16670
18005
  }
16671
18006
  if (verb === "status") {
18007
+ if (opts.onPanelOpen?.current) {
18008
+ const opened = opts.onPanelOpen.current("toggleCoordinatorMonitor");
18009
+ if (opened) return { message: "" };
18010
+ }
16672
18011
  const statusFn = getStatus();
16673
18012
  if (statusFn) {
16674
18013
  const stats = await statusFn();
@@ -19848,6 +21187,10 @@ function buildGoalCommand(opts) {
19848
21187
  "every iteration to decide what to do next."
19849
21188
  ].join("\n"),
19850
21189
  async run(args) {
21190
+ if (!args.trim() && opts.onPanelOpen?.current) {
21191
+ const opened = opts.onPanelOpen.current("toggleGoalPanel");
21192
+ if (opened) return { message: "" };
21193
+ }
19851
21194
  const trimmed = args.trim();
19852
21195
  const [verbRaw, ...rest] = trimmed.split(/\s+/);
19853
21196
  const verb = (verbRaw ?? "").toLowerCase();
@@ -20066,6 +21409,10 @@ function buildHelpCommand(opts) {
20066
21409
  ].join("\n"),
20067
21410
  async run(args) {
20068
21411
  const query = args.trim();
21412
+ if (!query && opts.onPanelOpen?.current) {
21413
+ opts.onPanelOpen.current("helpOpen");
21414
+ return { message: "" };
21415
+ }
20069
21416
  if (query) {
20070
21417
  const needle = query.startsWith("/") ? query.slice(1) : query;
20071
21418
  let match;
@@ -20286,15 +21633,16 @@ function formatBoardDetail(board) {
20286
21633
  } else {
20287
21634
  for (const task of colTasks) {
20288
21635
  const depMarker = task.dependsOn?.length ? DIM(` \u26D3\uFE0F${task.dependsOn.length}`) : "";
21636
+ const chainMarker = task.chain ? DIM(` #${task.chain.order + 1}`) : "";
20289
21637
  const agentMarker = task.assignedAgent ? DIM(` \u{1F464}${task.assignedAgent}`) : "";
20290
21638
  if (task.status === "completed") {
20291
- lines.push(` \u2705 ${task.title}${agentMarker}${depMarker}`);
21639
+ lines.push(` \u2705 ${task.title}${agentMarker}${depMarker}${chainMarker}`);
20292
21640
  } else if (task.status === "blocked") {
20293
- lines.push(` \u{1F6AB} ${task.title}${agentMarker}${depMarker}`);
21641
+ lines.push(` \u{1F6AB} ${task.title}${agentMarker}${depMarker}${chainMarker}`);
20294
21642
  } else if (task.status === "in_progress") {
20295
- lines.push(` \u{1F504} ${task.title}${agentMarker}${depMarker}`);
21643
+ lines.push(` \u{1F504} ${task.title}${agentMarker}${depMarker}${chainMarker}`);
20296
21644
  } else {
20297
- lines.push(` \u25CB ${task.title}${agentMarker}${depMarker}`);
21645
+ lines.push(` \u25CB ${task.title}${agentMarker}${depMarker}${chainMarker}`);
20298
21646
  }
20299
21647
  lines.push(
20300
21648
  ` ${DIM(task.id.slice(0, 8))} ${fmtPriority(task.priority)} ${fmtStatus(task.status)}`
@@ -20316,6 +21664,19 @@ function formatTaskDetail(board, task) {
20316
21664
  lines.push(` ${LABEL("Column")}: ${col?.title ?? task.columnId}`);
20317
21665
  if (task.assignedAgent) lines.push(` ${LABEL("Agent")}: ${task.assignedAgent}`);
20318
21666
  if (task.assignee) lines.push(` ${LABEL("Assignee")}: ${task.assignee}`);
21667
+ if (task.chain) {
21668
+ lines.push(` ${LABEL("Chain")}: ${task.chain.chainId} #${task.chain.order + 1}`);
21669
+ if (task.chain.previousTaskId)
21670
+ lines.push(` ${LABEL("Prev")}: ${task.chain.previousTaskId}`);
21671
+ if (task.chain.nextTaskId) lines.push(` ${LABEL("Next")}: ${task.chain.nextTaskId}`);
21672
+ }
21673
+ if (task.parentTaskId) lines.push(` ${LABEL("Parent")}: ${task.parentTaskId}`);
21674
+ if (task.childTaskIds?.length)
21675
+ lines.push(` ${LABEL("Children")}: ${task.childTaskIds.join(", ")}`);
21676
+ if (task.mergedIntoTaskId) lines.push(` ${LABEL("Merged into")}: ${task.mergedIntoTaskId}`);
21677
+ if (task.mergedFromTaskIds?.length) {
21678
+ lines.push(` ${LABEL("Merged from")}: ${task.mergedFromTaskIds.join(", ")}`);
21679
+ }
20319
21680
  if (task.dependsOn?.length) {
20320
21681
  const depNames = task.dependsOn.map((depId) => {
20321
21682
  const dep = board.tasks.find((t) => t.id === depId);
@@ -20342,6 +21703,15 @@ function formatTaskDetail(board, task) {
20342
21703
  );
20343
21704
  }
20344
21705
  }
21706
+ if (task.goalMetrics?.length) {
21707
+ lines.push("");
21708
+ lines.push(HEADING(" Goal Metrics:"));
21709
+ for (const metric of task.goalMetrics) {
21710
+ const target = metric.target !== void 0 ? ` / ${metric.target}${metric.unit ? ` ${metric.unit}` : ""}` : "";
21711
+ lines.push(` ${metric.name}: ${metric.current ?? "\u2014"}${target} ${DIM(metric.status)}`);
21712
+ if (metric.notes) lines.push(` ${DIM(metric.notes)}`);
21713
+ }
21714
+ }
20345
21715
  if (task.estimatedHours || task.actualHours) {
20346
21716
  lines.push("");
20347
21717
  lines.push(
@@ -20380,6 +21750,53 @@ function formatDependencyChain(board, taskId, depth = 0) {
20380
21750
  }
20381
21751
  return lines.join("\n");
20382
21752
  }
21753
+ function formatReadyTasks(results) {
21754
+ if (!results.length) return DIM("No ready kanban tasks.");
21755
+ const lines = [HEADING(`Ready Tasks (${results.length})`), ""];
21756
+ for (const result of results) {
21757
+ const task = result.task;
21758
+ const routing = task.assignment?.provider || task.assignment?.model ? ` ${DIM(`[${[task.assignment?.provider, task.assignment?.model].filter(Boolean).join("/")}]`)}` : "";
21759
+ const chain = task.chain ? ` ${DIM(`#${task.chain.order + 1}`)}` : "";
21760
+ lines.push(
21761
+ ` ${LABEL(result.board.id.slice(0, 8))}:${DIM(task.id.slice(0, 8))} ${task.title}${routing}${chain}`
21762
+ );
21763
+ }
21764
+ return lines.join("\n");
21765
+ }
21766
+ function formatKanbanSnapshot(snapshot) {
21767
+ const lines = [
21768
+ HEADING("Kanban Snapshot"),
21769
+ "",
21770
+ ` ${LABEL("Boards")}: ${snapshot.boards.length}`,
21771
+ ` ${LABEL("Ready")}: ${snapshot.ready.length}`,
21772
+ ` ${LABEL("Queued")}: ${snapshot.queued.length}`,
21773
+ ` ${LABEL("Running")}: ${snapshot.running.length}`,
21774
+ ` ${LABEL("Blocked")}: ${snapshot.blocked.length}`,
21775
+ ` ${LABEL("Review")}: ${snapshot.review.length}`,
21776
+ ` ${LABEL("Failed")}: ${snapshot.failed.length}`,
21777
+ ` ${LABEL("Completed")}: ${snapshot.completed.length}`
21778
+ ];
21779
+ if (snapshot.ready.length) {
21780
+ lines.push("", HEADING("Next Ready:"));
21781
+ for (const result of snapshot.ready.slice(0, 8)) {
21782
+ lines.push(
21783
+ ` ${result.board.id.slice(0, 8)}:${result.task.id.slice(0, 8)} ${result.task.title}`
21784
+ );
21785
+ }
21786
+ }
21787
+ return lines.join("\n");
21788
+ }
21789
+ function formatTaskChain(tasks) {
21790
+ if (!tasks.length) return DIM("No tasks in chain.");
21791
+ return [
21792
+ HEADING(`Task Chain (${tasks[0]?.chain?.chainId ?? "unknown"})`),
21793
+ "",
21794
+ ...tasks.map((task, index) => {
21795
+ const dep = task.dependsOn?.includes(tasks[index - 1]?.id ?? "") ? DIM(" dep") : "";
21796
+ return ` ${index + 1}. ${task.title} ${DIM(task.id.slice(0, 8))} ${fmtStatus(task.status)}${dep}`;
21797
+ })
21798
+ ].join("\n");
21799
+ }
20383
21800
  function buildKanbanCommand(opts) {
20384
21801
  const { projectRoot } = opts;
20385
21802
  return {
@@ -20396,14 +21813,21 @@ function buildKanbanCommand(opts) {
20396
21813
  " /kanban show <boardId> Show board details (columns + tasks)",
20397
21814
  " /kanban delete <boardId> Delete a board",
20398
21815
  " /kanban rename <boardId> <t> Rename a board",
21816
+ " /kanban snapshot [boardId] Show agent queue snapshot",
20399
21817
  "",
20400
21818
  " /kanban task add <boardId> <title> Add a task to board",
20401
21819
  " /kanban task <boardId> List tasks on a board",
21820
+ " /kanban task ready [boardId] List dependency-ready tasks",
21821
+ " /kanban task claim [boardId] <agent> Atomically claim next ready task",
21822
+ " /kanban task release <boardId> <taskId> Release a queued/running claim",
20402
21823
  " /kanban task show <boardId> <taskId> Show task details",
20403
21824
  " /kanban task move <boardId> <taskId> <col> Move task to column",
20404
21825
  " /kanban task done <boardId> <taskId> Mark task completed",
20405
21826
  " /kanban task block <boardId> <taskId> Mark task blocked",
20406
21827
  " /kanban task remove <boardId> <taskId> Delete a task",
21828
+ " /kanban task split <boardId> <taskId> <a> | <b> Split task into children",
21829
+ " /kanban task merge <boardId> <taskA,taskB> <title> Merge tasks",
21830
+ " /kanban task chain <boardId> <taskA> <taskB> [...] Ordered task chain",
20407
21831
  " /kanban task copy <fromBoard> <taskId> <toBoard> [columnId]",
20408
21832
  " /kanban task transfer <fromBoard> <taskId> <toBoard> [columnId]",
20409
21833
  "",
@@ -20411,6 +21835,8 @@ function buildKanbanCommand(opts) {
20411
21835
  " /kanban task assign <boardId> <taskId> <agent> [--provider=p --model=m --fallback=f]",
20412
21836
  " /kanban task dispatch <boardId> <taskId> [--provider=p --model=m --name=n]",
20413
21837
  " /kanban task depend <boardId> <taskId> <depId> Add dependency",
21838
+ " /kanban task metric add <boardId> <taskId> <name> [target] [unit]",
21839
+ " /kanban task metric set <boardId> <taskId> <metricId> <current> [met|missed|waived|pending]",
20414
21840
  " /kanban task note <boardId> <taskId> <text> Add task note",
20415
21841
  "",
20416
21842
  " /kanban task check add <boardId> <taskId> <desc> Add success check",
@@ -20420,6 +21846,9 @@ function buildKanbanCommand(opts) {
20420
21846
  "",
20421
21847
  " /kanban generate <description> Auto-generate board from text",
20422
21848
  " /kanban export <boardId> Export board as markdown",
21849
+ " /kanban graph export <boardId> [graphId] Save board as SDD TaskGraph",
21850
+ " /kanban graph import <graphId> Create board from SDD TaskGraph",
21851
+ " /kanban graph sync <boardId> <graphId> Sync board from SDD TaskGraph",
20423
21852
  " /kanban deps <boardId> <taskId> Show dependency chain"
20424
21853
  ].join("\n"),
20425
21854
  async run(args) {
@@ -20495,12 +21924,21 @@ function buildKanbanCommand(opts) {
20495
21924
  ${board.tasks.length + tasks.length} tasks created`
20496
21925
  };
20497
21926
  }
21927
+ if (cmd === "snapshot" || cmd === "queue") {
21928
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
21929
+ ...rest[0] ? { boardId: rest[0] } : {}
21930
+ });
21931
+ return { message: formatKanbanSnapshot(snapshot) };
21932
+ }
20498
21933
  if (cmd === "export") {
20499
21934
  if (!rest[0]) return { message: color.red("Usage: /kanban export <boardId>") };
20500
21935
  const board = await getBoard(projectRoot, rest[0]);
20501
21936
  if (!board) return { message: color.red(`Board not found: ${rest[0]}`) };
20502
21937
  return { message: exportBoardAsMarkdown(board) };
20503
21938
  }
21939
+ if (cmd === "graph" || cmd === "taskgraph") {
21940
+ return handleGraphSubcommand(opts, projectRoot, rest, showHelp);
21941
+ }
20504
21942
  if (cmd === "deps" || cmd === "dependencies") {
20505
21943
  if (rest.length < 2)
20506
21944
  return { message: color.red("Usage: /kanban deps <boardId> <taskId>") };
@@ -20525,7 +21963,9 @@ function buildKanbanCommand(opts) {
20525
21963
  "delete",
20526
21964
  "rename",
20527
21965
  "generate",
21966
+ "snapshot",
20528
21967
  "export",
21968
+ "graph",
20529
21969
  "deps",
20530
21970
  "task",
20531
21971
  "column"
@@ -20536,11 +21976,93 @@ function buildKanbanCommand(opts) {
20536
21976
  }
20537
21977
  };
20538
21978
  }
21979
+ async function handleGraphSubcommand(opts, projectRoot, args, showHelp) {
21980
+ const [sub, first, second] = args;
21981
+ if (!sub) return showHelp();
21982
+ if (!opts.paths?.projectTaskGraphs) {
21983
+ return { message: color.red("TaskGraph store path is not available in this session.") };
21984
+ }
21985
+ const store = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
21986
+ if (sub === "export") {
21987
+ const boardId = first;
21988
+ const graphId = second;
21989
+ if (!boardId) {
21990
+ return { message: color.red("Usage: /kanban graph export <boardId> [graphId]") };
21991
+ }
21992
+ const exported = await exportBoardToTaskGraph(projectRoot, boardId, {
21993
+ ...graphId ? { graphId } : {}
21994
+ });
21995
+ if (!exported) return { message: color.red(`Board not found: ${boardId}`) };
21996
+ await store.save(exported.graph);
21997
+ return {
21998
+ message: `${color.green("\u2705 TaskGraph exported:")} ${exported.graph.id}
21999
+ ${exported.graph.nodes.size} node(s) saved from ${exported.board.title}`
22000
+ };
22001
+ }
22002
+ if (sub === "import") {
22003
+ const graphId = first;
22004
+ if (!graphId) return { message: color.red("Usage: /kanban graph import <graphId>") };
22005
+ const graph = await store.load(graphId);
22006
+ if (!graph) return { message: color.red(`TaskGraph not found: ${graphId}`) };
22007
+ const imported = await createBoardFromTaskGraph(projectRoot, graph, {
22008
+ sourceSystem: "sdd",
22009
+ includeCompletedTasks: true
22010
+ });
22011
+ return {
22012
+ message: `${color.green("\u2705 Kanban board imported:")} ${imported.board.title}
22013
+ ${color.dim(imported.board.id)} \xB7 ${imported.board.tasks.length} task(s)`
22014
+ };
22015
+ }
22016
+ if (sub === "sync") {
22017
+ const boardId = first;
22018
+ const graphId = second;
22019
+ if (!boardId || !graphId) {
22020
+ return { message: color.red("Usage: /kanban graph sync <boardId> <graphId>") };
22021
+ }
22022
+ const graph = await store.load(graphId);
22023
+ if (!graph) return { message: color.red(`TaskGraph not found: ${graphId}`) };
22024
+ const result = await syncBoardFromTaskGraph(projectRoot, boardId, graph, {
22025
+ sourceSystem: "sdd",
22026
+ includeCompletedTasks: true
22027
+ });
22028
+ if (!result) return { message: color.red(`Board not found: ${boardId}`) };
22029
+ return {
22030
+ message: `${color.green("\u2705 Kanban board synced:")} ${result.board.title}
22031
+ ${result.createdTaskIds.length} created \xB7 ${result.updatedTaskIds.length} updated \xB7 ${result.archivedTaskIds.length} archived`
22032
+ };
22033
+ }
22034
+ return { message: color.red("Usage: /kanban graph export|import|sync ...") };
22035
+ }
20539
22036
  async function handleTaskSubcommand(opts, projectRoot, args, showHelp) {
20540
22037
  const [sub, boardId, ...rest] = args;
20541
- if (!sub || !boardId) {
22038
+ if (!sub) {
20542
22039
  return showHelp();
20543
22040
  }
22041
+ if (sub === "ready") {
22042
+ const results = await listReadyTasks(projectRoot, {
22043
+ ...boardId ? { boardId } : {},
22044
+ limit: 50
22045
+ });
22046
+ return { message: formatReadyTasks(results) };
22047
+ }
22048
+ if (sub === "claim") {
22049
+ const scopedBoardId = rest.length > 0 ? boardId : void 0;
22050
+ const agentId = rest.length > 0 ? rest[0] : boardId;
22051
+ if (!agentId) {
22052
+ return { message: color.red("Usage: /kanban task claim [boardId] <agentId>") };
22053
+ }
22054
+ const result = await claimReadyTask(projectRoot, {
22055
+ ...scopedBoardId ? { boardId: scopedBoardId } : {},
22056
+ agentId,
22057
+ status: "queued"
22058
+ });
22059
+ if (!result) return { message: color.yellow("No ready kanban task matched the claim.") };
22060
+ return {
22061
+ message: `${color.green("\u2705 Task claimed:")} ${result.task.title}
22062
+ ${color.dim(`${result.board.id}:${result.task.id}`)}`
22063
+ };
22064
+ }
22065
+ if (!boardId) return showHelp();
20544
22066
  if (sub === "list" || sub === "ls" || sub === "") {
20545
22067
  const board2 = await getBoard(projectRoot, boardId);
20546
22068
  if (!board2) return { message: color.red(`Board not found: ${boardId}`) };
@@ -20606,6 +22128,83 @@ async function handleTaskSubcommand(opts, projectRoot, args, showHelp) {
20606
22128
  if (!updated) return { message: color.red("Task not found") };
20607
22129
  return { message: color.green("\u2705 Task removed.") };
20608
22130
  }
22131
+ if (sub === "release") {
22132
+ const taskId = rest[0];
22133
+ if (!taskId) return { message: color.red("Usage: /kanban task release <boardId> <taskId>") };
22134
+ const updated = await releaseTaskClaim(projectRoot, boardId, taskId, {
22135
+ reason: rest.slice(1).join(" ") || "released from slash command"
22136
+ });
22137
+ if (!updated) return { message: color.red("Task not found") };
22138
+ return { message: color.green("\u2705 Task claim released.") };
22139
+ }
22140
+ if (sub === "split") {
22141
+ const taskId = rest[0];
22142
+ const titleText = rest.slice(1).join(" ");
22143
+ const titles = titleText.split("|").map((title) => title.trim()).filter(Boolean);
22144
+ if (!taskId || titles.length === 0) {
22145
+ return {
22146
+ message: color.red(
22147
+ "Usage: /kanban task split <boardId> <taskId> <child title> | <child title>"
22148
+ )
22149
+ };
22150
+ }
22151
+ const result = await splitTask(projectRoot, boardId, taskId, {
22152
+ titles,
22153
+ chainChildren: true,
22154
+ rewireDependents: true
22155
+ });
22156
+ if (!result) return { message: color.red("Board or task not found") };
22157
+ return {
22158
+ message: `${color.green("\u2705 Task split into children:")}
22159
+ ${result.children.map((task, index) => ` ${index + 1}. ${task.title} ${color.dim(task.id.slice(0, 8))}`).join("\n")}`
22160
+ };
22161
+ }
22162
+ if (sub === "merge") {
22163
+ const taskIds = splitCsv(rest[0] ?? "");
22164
+ const title = rest.slice(1).join(" ");
22165
+ if (taskIds.length < 2 || !title) {
22166
+ return {
22167
+ message: color.red("Usage: /kanban task merge <boardId> <taskA,taskB> <merged title>")
22168
+ };
22169
+ }
22170
+ const result = await mergeTasks(projectRoot, boardId, {
22171
+ taskIds,
22172
+ title,
22173
+ closeSourceTasks: true
22174
+ });
22175
+ if (!result) return { message: color.red("Board or task not found") };
22176
+ return {
22177
+ message: `${color.green("\u2705 Tasks merged:")} ${result.task.title}
22178
+ ${color.dim(result.task.id)}`
22179
+ };
22180
+ }
22181
+ if (sub === "chain") {
22182
+ const chainSub = rest[0];
22183
+ if (chainSub === "show") {
22184
+ const taskOrChainId = rest[1];
22185
+ if (!taskOrChainId) {
22186
+ return { message: color.red("Usage: /kanban task chain <boardId> show <taskId|chainId>") };
22187
+ }
22188
+ const result2 = await getTaskChain(projectRoot, boardId, taskOrChainId);
22189
+ if (!result2) return { message: color.red("Chain not found") };
22190
+ return { message: formatTaskChain(result2.tasks) };
22191
+ }
22192
+ const taskIds = rest;
22193
+ if (taskIds.length < 2) {
22194
+ return {
22195
+ message: color.red("Usage: /kanban task chain <boardId> <taskA> <taskB> [...]")
22196
+ };
22197
+ }
22198
+ const result = await setTaskChain(projectRoot, boardId, {
22199
+ taskIds,
22200
+ enforceDependencies: true
22201
+ });
22202
+ if (!result) return { message: color.red("Board or task not found") };
22203
+ return {
22204
+ message: `${color.green("\u2705 Chain set:")} ${result.chainId}
22205
+ ${formatTaskChain(result.tasks)}`
22206
+ };
22207
+ }
20609
22208
  if (sub === "copy" || sub === "transfer" || sub === "move-board") {
20610
22209
  const taskId = rest[0];
20611
22210
  const targetBoardId = rest[1];
@@ -20750,6 +22349,54 @@ ${summary}` };
20750
22349
  if (!updated) return { message: color.red("Failed to add dependency") };
20751
22350
  return { message: color.green(`\u2705 Dependency added: ${task.title} \u2192 ${depId}`) };
20752
22351
  }
22352
+ if (sub === "metric") {
22353
+ const metricSub = rest[0];
22354
+ const taskId = rest[1];
22355
+ if (metricSub === "add") {
22356
+ const name = rest[2];
22357
+ const target = rest[3];
22358
+ const unit = rest[4];
22359
+ if (!taskId || !name) {
22360
+ return {
22361
+ message: color.red(
22362
+ "Usage: /kanban task metric add <boardId> <taskId> <name> [target] [unit]"
22363
+ )
22364
+ };
22365
+ }
22366
+ const updated = await addGoalMetricToTask(projectRoot, boardId, taskId, {
22367
+ name,
22368
+ ...target !== void 0 ? { target } : {},
22369
+ ...unit !== void 0 ? { unit } : {}
22370
+ });
22371
+ if (!updated) return { message: color.red("Task not found") };
22372
+ return { message: color.green(`\u2705 Metric added: ${name}`) };
22373
+ }
22374
+ if (metricSub === "set" || metricSub === "update") {
22375
+ const metricId = rest[2];
22376
+ const current = rest[3];
22377
+ const status = rest[4];
22378
+ const validStatuses = ["pending", "met", "missed", "waived"];
22379
+ if (!taskId || !metricId || current === void 0) {
22380
+ return {
22381
+ message: color.red(
22382
+ "Usage: /kanban task metric set <boardId> <taskId> <metricId> <current> [met|missed|waived|pending]"
22383
+ )
22384
+ };
22385
+ }
22386
+ if (status && !validStatuses.includes(status)) {
22387
+ return { message: color.red(`Invalid metric status. Valid: ${validStatuses.join(", ")}`) };
22388
+ }
22389
+ const updated = await updateGoalMetricOnTask(projectRoot, boardId, taskId, metricId, {
22390
+ current,
22391
+ ...status ? { status } : {}
22392
+ });
22393
+ if (!updated) return { message: color.red("Metric not found") };
22394
+ return { message: color.green("\u2705 Metric updated.") };
22395
+ }
22396
+ return {
22397
+ message: color.red("Usage: /kanban task metric add|set <boardId> <taskId> ...")
22398
+ };
22399
+ }
20753
22400
  if (sub === "note") {
20754
22401
  const taskId = rest[0];
20755
22402
  const content = rest.slice(1).join(" ");
@@ -20852,6 +22499,15 @@ function parseKanbanAgentFlags(args) {
20852
22499
  function buildKanbanAgentPrompt(board, task, assignment) {
20853
22500
  const dependencyLines = (task.dependsOn ?? []).map((depId) => board.tasks.find((candidate) => candidate.id === depId)).filter((dep) => Boolean(dep)).map((dep) => `- ${dep.title} [${dep.status}] (${dep.id})`);
20854
22501
  const checks = task.successCriteria?.map((check) => `- ${check.description}`).join("\n");
22502
+ const metrics = task.goalMetrics?.map(
22503
+ (metric) => `- ${metric.name}: ${metric.current ?? "n/a"}${metric.target !== void 0 ? ` / ${metric.target}` : ""}${metric.unit ? ` ${metric.unit}` : ""} [${metric.status}]`
22504
+ ).join("\n");
22505
+ const chain = task.chain ? [
22506
+ `chainId: ${task.chain.chainId}`,
22507
+ `order: ${task.chain.order}`,
22508
+ task.chain.previousTaskId ? `previous: ${task.chain.previousTaskId}` : "",
22509
+ task.chain.nextTaskId ? `next: ${task.chain.nextTaskId}` : ""
22510
+ ].filter(Boolean).join("\n") : "";
20855
22511
  const routing = [
20856
22512
  assignment?.role ? `role: ${assignment.role}` : "",
20857
22513
  assignment?.provider ? `provider: ${assignment.provider}` : "",
@@ -20870,10 +22526,14 @@ function buildKanbanAgentPrompt(board, task, assignment) {
20870
22526
  ${task.description}` : "",
20871
22527
  routing.length ? `Routing hints:
20872
22528
  ${routing.join("\n")}` : "",
22529
+ chain ? `Task chain:
22530
+ ${chain}` : "",
20873
22531
  dependencyLines.length ? `Dependencies:
20874
22532
  ${dependencyLines.join("\n")}` : "",
20875
22533
  checks ? `Success criteria:
20876
22534
  ${checks}` : "",
22535
+ metrics ? `Goal metrics:
22536
+ ${metrics}` : "",
20877
22537
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
20878
22538
  "",
20879
22539
  "Work the task end-to-end. Use the kanban tool, not direct file edits, to update this task.",
@@ -21750,7 +23410,7 @@ function buildMcpSlashCommand(opts) {
21750
23410
  argsHint: "[list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
21751
23411
  help: [
21752
23412
  "Usage:",
21753
- " /mcp List available and configured servers.",
23413
+ " /mcp Open interactive server picker (TUI) or list servers.",
21754
23414
  " /mcp list Same.",
21755
23415
  " /mcp add <name> Add server preset to config (disabled).",
21756
23416
  " /mcp add <name> --enable Add and immediately enable.",
@@ -21766,10 +23426,15 @@ function buildMcpSlashCommand(opts) {
21766
23426
  " /mcp restart brave-search"
21767
23427
  ].join("\n"),
21768
23428
  async run(args) {
23429
+ const trimmed = args.trim();
23430
+ if ((trimmed === "" || trimmed === "list") && opts.onPanelOpen?.current) {
23431
+ const opened = opts.onPanelOpen.current("mcpPickerOpen");
23432
+ if (opened) return { message: "" };
23433
+ }
21769
23434
  if (!opts.onMcp) {
21770
23435
  return { message: "MCP management is not available in this session." };
21771
23436
  }
21772
- const result = await opts.onMcp(args.trim());
23437
+ const result = await opts.onMcp(trimmed);
21773
23438
  return { message: result };
21774
23439
  }
21775
23440
  };
@@ -22717,6 +24382,7 @@ function clearSuggestions() {
22717
24382
  function buildNextCommand(opts) {
22718
24383
  return {
22719
24384
  name: "next",
24385
+ aliases: ["enxt"],
22720
24386
  category: "Agent",
22721
24387
  description: "Show or select next-step suggestions. /next 1 to execute, /next list to view.",
22722
24388
  argsHint: "[on|off|toggle|list|clear|1 2 3...]",
@@ -22800,7 +24466,7 @@ function handleSelection(input, opts) {
22800
24466
  if (suggestions.length === 0) {
22801
24467
  return {
22802
24468
  message: color.amber(
22803
- "No suggestions available. Run /suggest to generate, or finish any open todos first \u2014 <next_steps> is suppressed while todos are pending."
24469
+ "No suggestions available. Run /suggest to generate, or finish any open todos first \u2014 <nextsteps> is suppressed while todos are pending."
22804
24470
  )
22805
24471
  };
22806
24472
  }
@@ -24203,6 +25869,10 @@ function buildAgentsCommand(opts) {
24203
25869
  category: "Agent",
24204
25870
  description: "Monitor subagent activity: /agents [stream on|off|status|list|show <id>]",
24205
25871
  async run(args) {
25872
+ if ((!args.trim() || args.trim() === "list") && opts.onPanelOpen?.current) {
25873
+ const opened = opts.onPanelOpen.current("toggleAgentsMonitor");
25874
+ if (opened) return { message: "" };
25875
+ }
24206
25876
  const monitor = opts.agentMonitor;
24207
25877
  const { cmd, rest } = parseSubcommand(args);
24208
25878
  const restJoined = rest.join(" ").trim();
@@ -25843,7 +27513,6 @@ function buildSettingsCommand(opts) {
25843
27513
  }
25844
27514
  };
25845
27515
  }
25846
- var DEFAULT_SHADOW_PROVIDER = "anthropic";
25847
27516
  var DEFAULT_SHADOW_INTERVAL_MS = 3e4;
25848
27517
  var MIN_SHADOW_INTERVAL_MS = 5e3;
25849
27518
  function buildShadowCommand(opts) {
@@ -25876,12 +27545,12 @@ function buildShadowCommand(opts) {
25876
27545
  " \u2022 Uses deterministic host rules first; LLM only for manual/problem cases",
25877
27546
  " \u2022 Does not post routine healthy reports",
25878
27547
  "",
25879
- "Model must be specified as provider/model, e.g. anthropic/claude-3-5-sonnet",
27548
+ "Model must be specified as provider/model, e.g. provider/configured-model",
25880
27549
  "When omitted, Shadow uses the current leader provider/model.",
25881
27550
  "",
25882
27551
  "Examples:",
25883
27552
  " /shadow start",
25884
- " /shadow start --model=anthropic/claude-3-5-sonnet",
27553
+ " /shadow start --model=provider/configured-model",
25885
27554
  " /shadow status Show all agent activity",
25886
27555
  " /shadow hoop subagent-abc123 Stop agent and notify",
25887
27556
  " /shadow hoop subagent-abc --reason=looping Stop with reason"
@@ -25889,6 +27558,13 @@ function buildShadowCommand(opts) {
25889
27558
  async run(args, _ctx) {
25890
27559
  const [action, ...rest] = args.trim().split(/\s+/);
25891
27560
  switch (action) {
27561
+ case void 0:
27562
+ case "":
27563
+ if (opts.onPanelOpen?.current) {
27564
+ const opened = opts.onPanelOpen.current("shadowOpen");
27565
+ if (opened) return { message: "" };
27566
+ }
27567
+ return { message: this.help ?? "/shadow \u2014 use /shadow help for usage" };
25892
27568
  case "start": {
25893
27569
  if (!opts.onSpawn) {
25894
27570
  return { message: "/shadow requires a running director with subagent support." };
@@ -25903,6 +27579,16 @@ function buildShadowCommand(opts) {
25903
27579
  } catch (e) {
25904
27580
  return { message: `/shadow start: ${e.message}` };
25905
27581
  }
27582
+ if (opts.shadowController?.activeId != null) {
27583
+ return {
27584
+ message: [
27585
+ `${color.yellow("\u26A0")} A Shadow Agent is already running (${opts.shadowController.activeId.slice(0, 8)}).`,
27586
+ "",
27587
+ "Only one Shadow Agent instance is allowed per session.",
27588
+ "Use /shadow status to view the current instance."
27589
+ ].join("\n")
27590
+ };
27591
+ }
25906
27592
  const defaultModelRef = getDefaultModelRef(opts);
25907
27593
  let modelRef;
25908
27594
  const modelRaw = flags["model"];
@@ -25914,14 +27600,9 @@ function buildShadowCommand(opts) {
25914
27600
  } catch (e) {
25915
27601
  return { message: `/shadow start: ${e.message}` };
25916
27602
  }
25917
- if (opts.shadowController?.activeId != null) {
27603
+ if (!modelRef.provider || !modelRef.model) {
25918
27604
  return {
25919
- message: [
25920
- `${color.yellow("\u26A0")} A Shadow Agent is already running (${opts.shadowController.activeId.slice(0, 8)}).`,
25921
- "",
25922
- "Only one Shadow Agent instance is allowed per session.",
25923
- "Use /shadow status to view the current instance."
25924
- ].join("\n")
27605
+ message: "/shadow start: no leader provider/model is configured; pass --model=provider/model"
25925
27606
  };
25926
27607
  }
25927
27608
  const spawnId = await opts.onSpawn(
@@ -26058,7 +27739,7 @@ ${color.dim("Interval will be applied on next /shadow start")}` };
26058
27739
  function getDefaultModelRef(opts) {
26059
27740
  const shadowDefaults = opts.shadowController?.getDefaults?.();
26060
27741
  const liveConfig = opts.configStore?.get?.();
26061
- const provider = shadowDefaults?.provider?.trim() || liveConfig?.provider?.trim() || opts.llmProvider?.id?.trim() || DEFAULT_SHADOW_PROVIDER;
27742
+ const provider = shadowDefaults?.provider?.trim() || liveConfig?.provider?.trim() || opts.llmProvider?.id?.trim() || "";
26062
27743
  const model = shadowDefaults?.model?.trim() || liveConfig?.model?.trim() || opts.llmModel?.trim() || "";
26063
27744
  return {
26064
27745
  provider,
@@ -26073,7 +27754,7 @@ function parseProviderModelRef(model, defaultRef) {
26073
27754
  const slash = model.indexOf("/");
26074
27755
  if (slash <= 0 || slash === model.length - 1) {
26075
27756
  throw new ToolValidationError({
26076
- message: `Model must be in provider/model format (e.g. anthropic/claude-3-5-sonnet), got: "${model}"`,
27757
+ message: `Model must be in provider/model format (e.g. provider/configured-model), got: "${model}"`,
26077
27758
  field: "model",
26078
27759
  context: { received: model }
26079
27760
  });
@@ -26260,12 +27941,12 @@ var STATUSLINE_CONFIG_KEYS = [
26260
27941
  function resolveConfigPath() {
26261
27942
  return process.env[CONFIG_ENV] ?? path5.join(process.env.HOME ?? os9.homedir(), ".wrongstack", "statusline.json");
26262
27943
  }
26263
- function isRecord(value) {
27944
+ function isRecord2(value) {
26264
27945
  return typeof value === "object" && value !== null && !Array.isArray(value);
26265
27946
  }
26266
27947
  function normalizeStatuslineConfig(value) {
26267
27948
  const cfg = { ...DEFAULTS };
26268
- if (!isRecord(value)) return cfg;
27949
+ if (!isRecord2(value)) return cfg;
26269
27950
  for (const key of STATUSLINE_CONFIG_KEYS) {
26270
27951
  const raw = value[key];
26271
27952
  if (typeof raw === "boolean") cfg[key] = raw;
@@ -26273,11 +27954,11 @@ function normalizeStatuslineConfig(value) {
26273
27954
  return cfg;
26274
27955
  }
26275
27956
  function isMissingKnownStatuslineKeys(value) {
26276
- if (!isRecord(value)) return true;
27957
+ if (!isRecord2(value)) return true;
26277
27958
  return STATUSLINE_CONFIG_KEYS.some((key) => typeof value[key] !== "boolean");
26278
27959
  }
26279
27960
  function hasFsCode(err, code) {
26280
- return isRecord(err) && err["code"] === code;
27961
+ return isRecord2(err) && err["code"] === code;
26281
27962
  }
26282
27963
  async function loadStatuslineConfig() {
26283
27964
  const p = resolveConfigPath();
@@ -27863,6 +29544,10 @@ function buildToolsCommand(opts) {
27863
29544
  ({ tool, owner }) => tool.name.toLowerCase().includes(filter) || owner.toLowerCase().includes(filter)
27864
29545
  ) : allTools;
27865
29546
  const disabled = reg.listDisabled();
29547
+ if (opts.onPanelOpen?.current) {
29548
+ const opened = opts.onPanelOpen.current("toolsPickerOpen");
29549
+ if (opened) return { message: "" };
29550
+ }
27866
29551
  if (filter && all.length === 0) {
27867
29552
  const msg2 = `${color.bold("Tools")} \u2014 no tool name or owner matched "${filter}".`;
27868
29553
  opts.renderer.write(msg2);
@@ -29270,6 +30955,7 @@ function buildAcpServerAgentFactory(deps, options = {}) {
29270
30955
  events,
29271
30956
  confirmAwaiter: void 0,
29272
30957
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? 12e4,
30958
+ maxToolTimeoutMs: config.tools?.maxToolTimeoutMs ?? 3e5,
29273
30959
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? 1e5,
29274
30960
  tracer: void 0
29275
30961
  });
@@ -30475,6 +32161,9 @@ function printUsage(deps) {
30475
32161
  color.dim(
30476
32162
  " wstack bench run --suite polyglot --polyglot-dir ./polyglot --models bench.config.json --limit 5"
30477
32163
  ),
32164
+ color.dim(
32165
+ " wstack bench run --suite local --suite-dir ./evals --models bench.config.json"
32166
+ ),
30478
32167
  color.dim(" wstack bench report ./bench-results/2026-06-14T10-00-00"),
30479
32168
  ""
30480
32169
  ].join("\n") + "\n"
@@ -30542,11 +32231,26 @@ async function benchRun(_args, deps) {
30542
32231
  docker
30543
32232
  });
30544
32233
  grade = (a) => gradeSwebench({ ...a, predictionsDir });
32234
+ } else if (suiteId === "local" || suiteId === "manifest") {
32235
+ const suiteDirRaw = flagStr(deps, "suite-dir");
32236
+ const manifestRaw = flagStr(deps, "manifest");
32237
+ if (!suiteDirRaw && !manifestRaw) {
32238
+ deps.renderer.writeError(
32239
+ "--suite-dir <path> or --manifest <file> is required for the local suite."
32240
+ );
32241
+ return 1;
32242
+ }
32243
+ const manifestFile = manifestRaw ? path5.resolve(deps.cwd, manifestRaw) : void 0;
32244
+ const suiteDir = suiteDirRaw ? path5.resolve(deps.cwd, suiteDirRaw) : path5.dirname(manifestFile);
32245
+ suite = createLocalManifestSuite({ suiteDir, manifestFile });
32246
+ grade = (a) => gradeLocalManifest(a);
30545
32247
  } else {
30546
- deps.renderer.writeError(`unknown suite "${suiteId}" (expected: polyglot | swebench)`);
32248
+ deps.renderer.writeError(`unknown suite "${suiteId}" (expected: polyglot | swebench | local)`);
30547
32249
  return 1;
30548
32250
  }
30549
- const toolNames = deps.toolRegistry?.list().map((t) => t.name) ?? [];
32251
+ const tools = deps.toolRegistry?.list() ?? [];
32252
+ const toolNames = tools.map((t) => t.name);
32253
+ const toolManifestHash = computeToolManifestHash(tools);
30550
32254
  const wstackEntry = await resolveWstackEntry();
30551
32255
  deps.renderer.writeInfo(`Running ${suiteId} across ${config.cells.length} model(s)\u2026`);
30552
32256
  let report;
@@ -30557,6 +32261,7 @@ async function benchRun(_args, deps) {
30557
32261
  config,
30558
32262
  cliVersion: CLI_VERSION,
30559
32263
  toolNames,
32264
+ toolManifestHash,
30560
32265
  nodeBin: process.execPath,
30561
32266
  wstackEntry,
30562
32267
  limit,
@@ -30613,6 +32318,9 @@ async function benchList(_args, deps) {
30613
32318
  deps.renderer.write(
30614
32319
  " swebench " + color.dim("SWE-bench Verified (end-to-end) \u2014 Phase 2, Docker-gated\n")
30615
32320
  );
32321
+ deps.renderer.write(
32322
+ " local " + color.dim("Project-defined manifest tasks with command/file graders\n")
32323
+ );
30616
32324
  const modelsPath = flagStr(deps, "models");
30617
32325
  if (modelsPath) {
30618
32326
  try {
@@ -30624,12 +32332,14 @@ async function benchList(_args, deps) {
30624
32332
  `
30625
32333
  );
30626
32334
  }
32335
+ const tools = deps.toolRegistry?.list() ?? [];
30627
32336
  const fp = reportHeaderLine({
30628
32337
  cliVersion: CLI_VERSION,
30629
- toolNames: deps.toolRegistry?.list().map((t) => t.name) ?? [],
32338
+ toolNames: tools.map((t) => t.name),
30630
32339
  maxIterations: config.maxIterations,
30631
32340
  yolo: true,
30632
32341
  subsetId: "(computed at run time)",
32342
+ toolManifestHash: computeToolManifestHash(tools),
30633
32343
  hash: "(computed at run time)"
30634
32344
  });
30635
32345
  deps.renderer.write("\n" + color.dim(`Harness: ${fp}`) + "\n");
@@ -31734,6 +33444,7 @@ async function serveMcpStdio(deps) {
31734
33444
  const executor = new ToolExecutor(registry, {
31735
33445
  permissionPolicy,
31736
33446
  secretScrubber: new DefaultSecretScrubber(),
33447
+ maxToolTimeoutMs: deps.config.tools?.maxToolTimeoutMs ?? 3e5,
31737
33448
  perIterationOutputCapBytes: 1e6
31738
33449
  });
31739
33450
  const allowed = await selectExposedTools(registry, ctx, permissionPolicy, whitelist);
@@ -31882,7 +33593,7 @@ async function addMcpServer(args, deps) {
31882
33593
  const serverCfg = { ...factory };
31883
33594
  serverCfg.enabled = enable;
31884
33595
  const existing = await readJsonObjectFile(deps.paths.globalConfig);
31885
- const mcpServers = isRecord2(existing.mcpServers) ? existing.mcpServers : {};
33596
+ const mcpServers = isRecord3(existing.mcpServers) ? existing.mcpServers : {};
31886
33597
  if (mcpServers[name])
31887
33598
  deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
31888
33599
  `);
@@ -31902,7 +33613,7 @@ async function removeMcpServer(name, deps) {
31902
33613
  return 1;
31903
33614
  }
31904
33615
  const existing = await readJsonObjectFile(deps.paths.globalConfig);
31905
- const mcpServers = isRecord2(existing.mcpServers) ? existing.mcpServers : {};
33616
+ const mcpServers = isRecord3(existing.mcpServers) ? existing.mcpServers : {};
31906
33617
  if (!mcpServers[name]) {
31907
33618
  deps.renderer.writeError(`Server "${name}" not in config.
31908
33619
  `);
@@ -31915,7 +33626,7 @@ async function removeMcpServer(name, deps) {
31915
33626
  `);
31916
33627
  return 0;
31917
33628
  }
31918
- function isRecord2(value) {
33629
+ function isRecord3(value) {
31919
33630
  return typeof value === "object" && value !== null && !Array.isArray(value);
31920
33631
  }
31921
33632
  var MODEL_PROFILES = [
@@ -31928,15 +33639,6 @@ var MODEL_PROFILES = [
31928
33639
  costTier: "premium",
31929
33640
  speedTier: "slow"
31930
33641
  },
31931
- {
31932
- provider: "anthropic",
31933
- pattern: /claude-sonnet/i,
31934
- family: "Claude Sonnet",
31935
- strengths: ["coding", "balanced"],
31936
- bestFor: ["coding", "general"],
31937
- costTier: "standard",
31938
- speedTier: "fast"
31939
- },
31940
33642
  {
31941
33643
  provider: "anthropic",
31942
33644
  pattern: /claude-haiku/i,
@@ -32628,1146 +34330,206 @@ ${color.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
32628
34330
  writeLine(color.dim("No providers selected."));
32629
34331
  return 0;
32630
34332
  }
32631
- }
32632
- const leaderModel = config.model ?? "unknown";
32633
- const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds2.includes(pid)) : [];
32634
- const warningLine = unknownProviders.length > 0 ? ` ${color.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
32635
- ` : "";
32636
- writeLine(`${color.bold("Model Competency Evaluation")}`);
32637
- writeLine(
32638
- color.dim(
32639
- `Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
32640
- )
32641
- );
32642
- writeLine(warningLine);
32643
- writeLine(color.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
32644
- writeLine();
32645
- const collected = /* @__PURE__ */ new Map();
32646
- let total = 0;
32647
- let ok2 = 0;
32648
- for (const pid of targetProviderIds) {
32649
- const prov = createProviderForId(pid, config);
32650
- if (!prov) {
32651
- writeLine(color.dim(` \u2298 ${pid}: provider unavailable, skipping`));
32652
- continue;
32653
- }
32654
- for (const cat of targetCategories) {
32655
- const task = EVAL_TASKS[cat];
32656
- if (!task) continue;
32657
- const candidates = rankModels(providers, hasKey, cat, modelsPerCat).filter(
32658
- (c) => c.provider === pid
32659
- );
32660
- if (candidates.length === 0) continue;
32661
- if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
32662
- for (const c of candidates) {
32663
- total++;
32664
- const modelKey = `${pid}/${c.model}`;
32665
- try {
32666
- const start = Date.now();
32667
- const resp = await prov.complete(
32668
- {
32669
- model: c.model,
32670
- system: [{ type: "text", text: "Be thorough and correct." }],
32671
- messages: [
32672
- { role: "user", content: [{ type: "text", text: task.prompt }] }
32673
- ],
32674
- maxTokens: 1024
32675
- },
32676
- { signal: AbortSignal.timeout(45e3) }
32677
- );
32678
- const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
32679
- const respUsage = resp.usage;
32680
- collected.get(cat)?.set(modelKey, {
32681
- model: modelKey,
32682
- latency: Date.now() - start,
32683
- tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
32684
- text: respText
32685
- });
32686
- ok2++;
32687
- } catch {
32688
- collected.get(cat)?.set(modelKey, {
32689
- model: modelKey,
32690
- latency: -1,
32691
- tokens: 0,
32692
- text: ""
32693
- });
32694
- }
32695
- }
32696
- }
32697
- }
32698
- writeLine(`${color.dim(`Phase 1: ${ok2}/${total} calls succeeded`)}`);
32699
- writeLine();
32700
- if (collected.size === 0) {
32701
- writeLine(color.amber("No responses collected. Check provider configuration."));
32702
- return 0;
32703
- }
32704
- const leaderProvider = createProviderForId(config.provider, config);
32705
- if (leaderProvider) {
32706
- writeLine(`${color.bold("Phase 2")} \u2014 ${color.dim("leader ranks responses")}`);
32707
- writeLine();
32708
- }
32709
- const rankings = /* @__PURE__ */ new Map();
32710
- for (const [cat, responses] of collected) {
32711
- const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
32712
- if (valid.length < 2) {
32713
- if (valid.length === 1) {
32714
- const m = valid[0]?.model;
32715
- if (m !== void 0) {
32716
- if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
32717
- rankings.get(m)?.set(cat, { rank: 1, total: 1 });
32718
- }
32719
- }
32720
- continue;
32721
- }
32722
- const task = EVAL_TASKS[cat];
32723
- if (leaderProvider) {
32724
- const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
32725
- for (let i = 0; i < valid.length; i++) {
32726
- const m = valid[ranked[i] ?? i]?.model;
32727
- if (m === void 0) continue;
32728
- if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
32729
- rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
32730
- }
32731
- } else {
32732
- for (const r of valid) {
32733
- if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
32734
- rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
32735
- }
32736
- }
32737
- }
32738
- writeLine(`${color.bold("Competency Report")}`);
32739
- writeLine();
32740
- const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
32741
- const catList = [...collected.keys()];
32742
- const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
32743
- const cw = 12;
32744
- writeLine(
32745
- ` ${color.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
32746
- );
32747
- writeLine(` ${color.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
32748
- for (const model of allModels) {
32749
- const mr = rankings.get(model);
32750
- const provFromModel = model.split("/")[0] ?? "";
32751
- const modelColor = provFromModel === config.provider ? color.cyan : color.green;
32752
- let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
32753
- for (const cat of catList) {
32754
- const e = mr.get(cat);
32755
- if (e) {
32756
- const pct2 = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
32757
- const pc = pct2 >= 80 ? color.green : pct2 >= 50 ? color.amber : color.red;
32758
- row += `${pc(`#${e.rank} ${pct2}%`.padEnd(cw + 2))}`;
32759
- } else {
32760
- row += color.dim("\u2014".padEnd(cw + 2));
32761
- }
32762
- }
32763
- writeLine(row);
32764
- }
32765
- writeLine();
32766
- writeLine(color.dim("#1 100% = best in category. \u2014 = not tested."));
32767
- writeLine();
32768
- writeLine(color.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
32769
- writeLine(
32770
- color.dim(
32771
- "Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
32772
- )
32773
- );
32774
- writeLine(
32775
- color.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
32776
- );
32777
- return 0;
32778
- }
32779
- writeLine(`${color.bold("API Key Status")}`);
32780
- writeLine();
32781
- for (const prov of providers) {
32782
- const k = hasKey(prov.id);
32783
- writeLine(` ${checkMark(k)} ${color.bold(prov.id.padEnd(18))} ${color.dim(prov.name)}`);
32784
- }
32785
- writeLine();
32786
- writeLine(`${color.dim(`Leader: ${config.provider}/${config.model}`)}`);
32787
- writeLine();
32788
- writeLine(`${color.bold("Model Capabilities")} ${color.dim("\u2014 matched to known profiles")}`);
32789
- writeLine();
32790
- for (const prov of providers) {
32791
- if (!hasKey(prov.id)) continue;
32792
- writeLine(` ${color.bold(prov.id)} ${color.dim(`(${prov.name})`)}`);
32793
- const tiers = {
32794
- premium: [],
32795
- standard: [],
32796
- budget: [],
32797
- unknown: []
32798
- };
32799
- for (const m of prov.models ?? []) {
32800
- const profile = findProfile(prov.id, m.id);
32801
- tiers[profile?.costTier ?? "unknown"]?.push(m);
32802
- }
32803
- for (const tier of ["premium", "standard", "budget", "unknown"]) {
32804
- const tierModels = tiers[tier];
32805
- if (tierModels.length === 0) continue;
32806
- const label = tier === "unknown" ? color.dim("unmatched") : `${costLabel(tier)} ${tier}`;
32807
- writeLine(` ${label}`);
32808
- for (const m of tierModels) {
32809
- const cap = m.capabilities;
32810
- const ctx = cap?.contextWindow ?? 0;
32811
- const maxOut = cap?.maxOutputTokens ?? 0;
32812
- const profile = findProfile(prov.id, m.id);
32813
- const family = profile ? `${speedLabel(profile.speedTier)} ${color.green(profile.family)}` : color.dim("no profile match");
32814
- const pricing = m.pricing ? `${color.dim("in")}${fmtPrice2(m.pricing.input)} ${color.dim("out")}${fmtPrice2(m.pricing.output)}` : color.dim("pricing ?");
32815
- writeLine(
32816
- ` ${color.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens2(ctx).padEnd(6)}` : color.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens2(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
32817
- );
32818
- }
32819
- }
32820
- writeLine();
32821
- }
32822
- await renderSuggest();
32823
- writeLine();
32824
- writeLine(color.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
32825
- writeLine(color.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
32826
- return 0;
32827
- };
32828
- var OFFICIAL_PLUGINS = [
32829
- {
32830
- alias: "telegram",
32831
- specifier: "@wrongstack/telegram",
32832
- description: "Telegram bridge for prompts, notifications, and slash commands."
32833
- },
32834
- {
32835
- alias: "lsp",
32836
- specifier: "@wrongstack/plug-lsp",
32837
- description: "Language Server Protocol tools for code intelligence."
32838
- }
32839
- ];
32840
- var PLUGIN_AUDIT_ENTRIES = [
32841
- {
32842
- name: "wstack-prompts",
32843
- risk: "medium",
32844
- summary: "Prompt library and prompt authoring commands.",
32845
- defaultState: "active",
32846
- canDisable: false
32847
- },
32848
- {
32849
- name: "wstack-sync",
32850
- risk: "medium",
32851
- summary: "Cloud sync commands for prompts, skills, settings, memory, and history.",
32852
- defaultState: "active",
32853
- canDisable: false
32854
- },
32855
- {
32856
- name: "wstack-git",
32857
- risk: "high",
32858
- summary: "Git commands for commit, status checks, and push.",
32859
- defaultState: "active",
32860
- canDisable: false
32861
- },
32862
- {
32863
- name: "wstack-observability",
32864
- risk: "low",
32865
- summary: "Runtime metrics and health slash commands.",
32866
- defaultState: "active",
32867
- canDisable: false
32868
- },
32869
- {
32870
- name: "wstack-security",
32871
- risk: "high",
32872
- summary: "Security scanning backend used by first-party security flows.",
32873
- defaultState: "active",
32874
- canDisable: false
32875
- },
32876
- {
32877
- name: "wstack-chimera",
32878
- risk: "medium",
32879
- summary: "Post-session code quality review agent.",
32880
- defaultState: "active",
32881
- canDisable: false
32882
- },
32883
- {
32884
- name: "wstack-skills",
32885
- risk: "medium",
32886
- summary: "Skill library, authoring, install, update, and uninstall commands.",
32887
- defaultState: "active",
32888
- canDisable: false
32889
- },
32890
- {
32891
- name: "wstack-plan",
32892
- risk: "medium",
32893
- summary: "Strategic plan board slash command.",
32894
- defaultState: "active",
32895
- canDisable: false
32896
- },
32897
- {
32898
- name: "agent-handoff",
32899
- risk: "medium",
32900
- summary: "Mailbox-backed handoff notes and task transfer helpers.",
32901
- defaultState: "active",
32902
- canDisable: true
32903
- },
32904
- {
32905
- name: "auto-doc",
32906
- risk: "medium",
32907
- summary: "Generates JSDoc/TSDoc comments for source files.",
32908
- defaultState: "active",
32909
- canDisable: true
32910
- },
32911
- {
32912
- name: "auto-i18n-extractor",
32913
- risk: "low",
32914
- summary: "Finds hardcoded user-facing UI strings and suggests i18n keys when explicitly enabled.",
32915
- defaultState: "inactive",
32916
- canDisable: true
32917
- },
32918
- {
32919
- name: "accessibility-auditor",
32920
- risk: "medium",
32921
- summary: "Scans UI files for common accessibility issues when explicitly enabled.",
32922
- defaultState: "inactive",
32923
- canDisable: true
32924
- },
32925
- {
32926
- name: "git-autocommit",
32927
- risk: "high",
32928
- summary: "Stages files and creates AI-generated conventional commits.",
32929
- defaultState: "active",
32930
- canDisable: true
32931
- },
32932
- {
32933
- name: "shell-check",
32934
- risk: "low",
32935
- summary: "Runs shellcheck on shell scripts.",
32936
- defaultState: "active",
32937
- canDisable: true
32938
- },
32939
- {
32940
- name: "cost-tracker",
32941
- risk: "low",
32942
- summary: "Tracks token usage and estimated session cost.",
32943
- defaultState: "active",
32944
- canDisable: true
32945
- },
32946
- {
32947
- name: "file-watcher",
32948
- risk: "medium",
32949
- summary: "Watches project files and emits change events.",
32950
- defaultState: "active",
32951
- canDisable: true
32952
- },
32953
- {
32954
- name: "cron",
32955
- risk: "medium",
32956
- summary: "Schedules recurring in-session tasks.",
32957
- defaultState: "active",
32958
- canDisable: true
32959
- },
32960
- {
32961
- name: "template-engine",
32962
- risk: "medium",
32963
- summary: "Expands and writes file templates.",
32964
- defaultState: "active",
32965
- canDisable: true
32966
- },
32967
- {
32968
- name: "semver-bump",
32969
- risk: "high",
32970
- summary: "Computes version bumps, changelogs, and tags.",
32971
- defaultState: "active",
32972
- canDisable: true
32973
- },
32974
- {
32975
- name: "secret-scanner",
32976
- risk: "high",
32977
- summary: "Blocks or redacts credential leaks in tool input and output.",
32978
- defaultState: "active",
32979
- canDisable: false
32980
- },
32981
- {
32982
- name: "todo-tracker",
32983
- risk: "low",
32984
- summary: "Persistent project-scoped todo backlog.",
32985
- defaultState: "active",
32986
- canDisable: true
32987
- },
32988
- {
32989
- name: "token-budget",
32990
- risk: "medium",
32991
- summary: "Warns or stops when token budgets are exceeded.",
32992
- defaultState: "active",
32993
- canDisable: true
32994
- },
32995
- {
32996
- name: "lint-gate",
32997
- risk: "medium",
32998
- summary: "Runs lint checks before write/edit commits.",
32999
- defaultState: "active",
33000
- canDisable: true
33001
- },
33002
- {
33003
- name: "branch-guard",
33004
- risk: "high",
33005
- summary: "Blocks commits, pushes, and merges on protected branches.",
33006
- defaultState: "active",
33007
- canDisable: false
33008
- },
33009
- {
33010
- name: "diff-summary",
33011
- risk: "low",
33012
- summary: "Injects compact git diff context after edits.",
33013
- defaultState: "active",
33014
- canDisable: true
33015
- },
33016
- {
33017
- name: "commit-validator",
33018
- risk: "medium",
33019
- summary: "Validates conventional commit format before commits.",
33020
- defaultState: "active",
33021
- canDisable: true
33022
- },
33023
- {
33024
- name: "format-on-save",
33025
- risk: "medium",
33026
- summary: "Runs formatter after write/edit tool calls.",
33027
- defaultState: "active",
33028
- canDisable: true
33029
- },
33030
- {
33031
- name: "test-runner-gate",
33032
- risk: "medium",
33033
- summary: "Runs relevant tests after source edits when explicitly enabled.",
33034
- defaultState: "inactive",
33035
- canDisable: true
33036
- },
33037
- {
33038
- name: "import-organizer",
33039
- risk: "medium",
33040
- summary: "Sorts imports and applies safe linter fixes after edits.",
33041
- defaultState: "active",
33042
- canDisable: true
33043
- },
33044
- {
33045
- name: "knowledge-graph",
33046
- risk: "low",
33047
- summary: "Stores and queries durable project knowledge graph facts.",
33048
- defaultState: "active",
33049
- canDisable: true
33050
- },
33051
- {
33052
- name: "todo-listener",
33053
- risk: "low",
33054
- summary: "Broadcasts todo tool updates to the project mailbox when explicitly enabled.",
33055
- defaultState: "inactive",
33056
- canDisable: true
33057
- },
33058
- {
33059
- name: "session-recap",
33060
- risk: "low",
33061
- summary: "Posts a session recap to the project mailbox on stop.",
33062
- defaultState: "active",
33063
- canDisable: true
33064
- },
33065
- {
33066
- name: "spec-linker",
33067
- risk: "low",
33068
- summary: "Finds unlinked plugin references in markdown edits.",
33069
- defaultState: "active",
33070
- canDisable: true
33071
- },
33072
- {
33073
- name: "doc-sync-guard",
33074
- risk: "low",
33075
- summary: "Warns when docs drift from recently changed public source files when explicitly enabled.",
33076
- defaultState: "inactive",
33077
- canDisable: true
33078
- },
33079
- {
33080
- name: "loop-breaker",
33081
- risk: "low",
33082
- summary: "Detects runaway tool-call loops; warns, then blocks repeats.",
33083
- defaultState: "active",
33084
- canDisable: true
33085
- },
33086
- {
33087
- name: "path-guard",
33088
- risk: "medium",
33089
- summary: "Blocks writes and destructive commands on protected paths.",
33090
- defaultState: "active",
33091
- canDisable: true
33092
- },
33093
- {
33094
- name: "context-pins",
33095
- risk: "low",
33096
- summary: "Pins durable facts into the system prompt across compactions.",
33097
- defaultState: "active",
33098
- canDisable: true
33099
- },
33100
- {
33101
- name: "checkpoint",
33102
- risk: "medium",
33103
- summary: "Snapshots files before edits and restores pre-edit states.",
33104
- defaultState: "active",
33105
- canDisable: true
33106
- },
33107
- {
33108
- name: "error-lens",
33109
- risk: "low",
33110
- summary: "Distills failed command output into compact error digests.",
33111
- defaultState: "active",
33112
- canDisable: true
33113
- },
33114
- {
33115
- name: "dep-guard",
33116
- risk: "medium",
33117
- summary: "Supervises dependency installs: deny list and typosquat warnings.",
33118
- defaultState: "active",
33119
- canDisable: true
33120
- },
33121
- {
33122
- name: "dependency-vulnerability-gate",
33123
- risk: "high",
33124
- summary: "Audits installed dependencies and blocks vulnerable installs.",
33125
- defaultState: "active",
33126
- canDisable: true
33127
- },
33128
- {
33129
- name: "license-audit-gate",
33130
- risk: "high",
33131
- summary: "Audits dependency licenses after package installs.",
33132
- defaultState: "active",
33133
- canDisable: true
33134
- },
33135
- {
33136
- name: "security-hotspot-scanner",
33137
- risk: "high",
33138
- summary: "Scans source files for common security anti-patterns on demand or when explicitly enabled.",
33139
- defaultState: "inactive",
33140
- canDisable: true
33141
- },
33142
- {
33143
- name: "api-compatibility-gate",
33144
- risk: "medium",
33145
- summary: "Warns when public entry-point exports are removed when explicitly enabled.",
33146
- defaultState: "inactive",
33147
- canDisable: true
33148
- },
33149
- {
33150
- name: "dead-code-detector",
33151
- risk: "low",
33152
- summary: "Scans for suspicious exported symbols that appear unused on demand or when explicitly enabled.",
33153
- defaultState: "inactive",
33154
- canDisable: true
33155
- },
33156
- {
33157
- name: "migration-planner",
33158
- risk: "low",
33159
- summary: "Builds dependency or framework migration checklists.",
33160
- defaultState: "active",
33161
- canDisable: true
33162
- },
33163
- {
33164
- name: "schema-evolution-guard",
33165
- risk: "high",
33166
- summary: "Warns on destructive database or API schema changes.",
33167
- defaultState: "active",
33168
- canDisable: true
33169
- },
33170
- {
33171
- name: "semantic-search-indexer",
33172
- risk: "low",
33173
- summary: "Builds an in-memory keyword index for project source search.",
33174
- defaultState: "active",
33175
- canDisable: true
33176
- },
33177
- {
33178
- name: "config-validator",
33179
- risk: "low",
33180
- summary: "Validates JSON/YAML/TOML files right after write/edit.",
33181
- defaultState: "active",
33182
- canDisable: true
33183
- },
33184
- {
33185
- name: "notify-hub",
33186
- risk: "medium",
33187
- summary: "Sends session events and ad-hoc notifications to a webhook.",
33188
- defaultState: "active",
33189
- canDisable: true
33190
- },
33191
- {
33192
- name: "changelog-writer",
33193
- risk: "low",
33194
- summary: "Collects session work and writes Keep-a-Changelog entries.",
33195
- defaultState: "active",
33196
- canDisable: true
33197
- },
33198
- {
33199
- name: "injection-shield",
33200
- risk: "low",
33201
- summary: "Flags prompt-injection patterns in tool output.",
33202
- defaultState: "active",
33203
- canDisable: true
33204
- },
33205
- {
33206
- name: "llm-cache",
33207
- risk: "medium",
33208
- summary: "Caches identical provider requests (opt-in; wraps every LLM call).",
33209
- defaultState: "active",
33210
- canDisable: true
33211
- },
33212
- {
33213
- name: "model-router",
33214
- risk: "medium",
33215
- summary: "Routes each LLM call to a different model by size/tool rules (opt-in).",
33216
- defaultState: "active",
33217
- canDisable: true
33218
- },
33219
- {
33220
- name: "pr-drafter",
33221
- risk: "low",
33222
- summary: "Generates session pull request draft markdown.",
33223
- defaultState: "active",
33224
- canDisable: true
33225
- },
33226
- {
33227
- name: "prompt-firewall",
33228
- risk: "high",
33229
- summary: "Detects/redacts credential leaks on the provider wire (opt-in).",
33230
- defaultState: "active",
33231
- canDisable: true
33232
- },
33233
- {
33234
- name: "auto-escalate",
33235
- risk: "medium",
33236
- summary: "Retries with an escalated model on transient provider errors (opt-in).",
33237
- defaultState: "active",
33238
- canDisable: true
33239
- },
33240
- {
33241
- name: "token-throttle",
33242
- risk: "medium",
33243
- summary: "Rolling-window tokens/min rate limiting via provider-call delays (opt-in).",
33244
- defaultState: "active",
33245
- canDisable: true
33246
- },
33247
- {
33248
- name: "plugin-stack-observer",
33249
- risk: "low",
33250
- summary: "Reports provider wrapper stack order and health.",
33251
- defaultState: "active",
33252
- canDisable: true
33253
- },
33254
- {
33255
- name: "test-coverage-gate",
33256
- risk: "medium",
33257
- summary: "Checks coverage thresholds after test runs.",
33258
- defaultState: "active",
33259
- canDisable: true
33260
- },
33261
- {
33262
- name: "test-flake-detector",
33263
- risk: "medium",
33264
- summary: "Runs tests repeatedly to identify flaky failures.",
33265
- defaultState: "active",
33266
- canDisable: true
33267
- },
33268
- {
33269
- name: "performance-regression-gate",
33270
- risk: "medium",
33271
- summary: "Compares benchmark results and reports performance regressions.",
33272
- defaultState: "active",
33273
- canDisable: true
33274
- },
33275
- {
33276
- name: "type-gate",
33277
- risk: "medium",
33278
- summary: "Runs TypeScript type checks after relevant edits when explicitly enabled.",
33279
- defaultState: "inactive",
33280
- canDisable: true
33281
- },
33282
- {
33283
- name: "code-metrics",
33284
- risk: "low",
33285
- summary: "Computes LOC, comment ratio, function count, and approximate cyclomatic complexity on demand or when explicitly enabled.",
33286
- defaultState: "inactive",
33287
- canDisable: true
33288
- },
33289
- {
33290
- name: "duplicate-code-detector",
33291
- risk: "low",
33292
- summary: "Detects duplicate or similar code blocks across source files on demand or when explicitly enabled.",
33293
- defaultState: "inactive",
33294
- canDisable: true
33295
- },
33296
- {
33297
- name: "feature-flag-tracker",
33298
- risk: "low",
33299
- summary: "Scans source files for feature-flag usage and reports flag inventory on demand or when explicitly enabled.",
33300
- defaultState: "inactive",
33301
- canDisable: true
33302
- },
33303
- {
33304
- name: "interface-contract-guard",
33305
- risk: "medium",
33306
- summary: "Warns when TypeScript interfaces change or lack implementers when explicitly enabled.",
33307
- defaultState: "inactive",
33308
- canDisable: true
33309
- },
33310
- {
33311
- name: "refactor-suggester",
33312
- risk: "low",
33313
- summary: "Flags long functions, deep nesting, many parameters, magic numbers, and console.log on demand or when explicitly enabled.",
33314
- defaultState: "inactive",
33315
- canDisable: true
33316
- },
33317
- {
33318
- name: "release-notes-generator",
33319
- risk: "low",
33320
- summary: "Groups conventional commits into a release-notes markdown draft.",
33321
- defaultState: "active",
33322
- canDisable: true
33323
- },
33324
- {
33325
- name: "smart-rename",
33326
- risk: "medium",
33327
- summary: "Whole-word symbol rename with preview and optional apply.",
33328
- defaultState: "active",
33329
- canDisable: true
33330
- },
33331
- {
33332
- name: "test-generator",
33333
- risk: "low",
33334
- summary: "Generates a unit-test skeleton from exported functions/classes.",
33335
- defaultState: "active",
33336
- canDisable: true
33337
- },
33338
- {
33339
- name: "@wrongstack/plug-lsp",
33340
- risk: "medium",
33341
- summary: "Language Server Protocol tools and slash commands.",
33342
- defaultState: "active",
33343
- canDisable: true
33344
- },
33345
- {
33346
- name: "telegram",
33347
- risk: "medium",
33348
- summary: "Telegram bridge for messages, approvals, and notifications.",
33349
- defaultState: "active",
33350
- canDisable: true
33351
- }
33352
- ];
33353
- var OFFICIAL_ALIASES = new Map(
33354
- OFFICIAL_PLUGINS.flatMap((p) => [
33355
- [p.alias, p.specifier],
33356
- [p.specifier, p.specifier]
33357
- ])
33358
- );
33359
- async function runPluginManagementCommand(args, deps) {
33360
- const sub = args[0];
33361
- if (!sub || sub === "list" || sub === "status") {
33362
- return {
33363
- code: 0,
33364
- level: "output",
33365
- message: renderConfiguredPlugins(deps.config)
33366
- };
33367
- }
33368
- if (sub === "official" || sub === "officials") {
33369
- return {
33370
- code: 0,
33371
- level: "output",
33372
- message: renderOfficialPlugins(deps.config)
33373
- };
33374
- }
33375
- if (sub === "report" || sub === "audit" || sub === "menu") {
33376
- return {
33377
- code: 0,
33378
- level: "output",
33379
- message: renderPluginAuditReport(deps.config)
33380
- };
33381
- }
33382
- if (sub === "add" || sub === "install") {
33383
- const spec = args[1];
33384
- if (!spec) {
33385
- return errorResult("Usage: wstack plugin add <specifier|official-alias> [--disabled]");
33386
- }
33387
- return upsertPlugin(
33388
- resolvePluginSpecifier(spec),
33389
- { enabled: !args.includes("--disabled") },
33390
- deps,
33391
- "Added"
33392
- );
33393
- }
33394
- if (sub === "remove" || sub === "rm" || sub === "uninstall") {
33395
- const spec = args[1];
33396
- if (!spec) {
33397
- return errorResult("Usage: wstack plugin remove <specifier|official-alias>");
33398
- }
33399
- return removePlugin(resolvePluginSpecifier(spec), deps);
33400
- }
33401
- if (sub === "enable" || sub === "disable") {
33402
- const spec = args[1];
33403
- if (!spec) {
33404
- return errorResult(`Usage: wstack plugin ${sub} <specifier|official-alias>`);
33405
- }
33406
- return upsertPlugin(
33407
- resolvePluginSpecifier(spec),
33408
- { enabled: sub === "enable" },
33409
- deps,
33410
- sub === "enable" ? "Enabled" : "Disabled"
33411
- );
33412
- }
33413
- if (sub === "toggle") {
33414
- const spec = args[1];
33415
- if (!spec) {
33416
- return errorResult("Usage: wstack plugin toggle <plugin-name|official-alias>");
33417
- }
33418
- return togglePlugin(resolvePluginToggleSpecifier(spec), deps);
33419
- }
33420
- if (sub === "llm") {
33421
- return runPluginLlmCommand(args.slice(1), deps);
33422
- }
33423
- return errorResult(
33424
- `Unknown plugin subcommand: ${sub}
33425
- Usage: wstack plugin [list|status|report|menu|official|add|install|remove|enable|disable|toggle|llm]`
33426
- );
33427
- }
33428
- var PLUGIN_LLM_USAGE = [
33429
- "Usage:",
33430
- " wstack plugin llm List every per-plugin LLM override.",
33431
- " wstack plugin llm <plugin> Show one plugin's LLM override.",
33432
- " wstack plugin llm <plugin> <provider> [model] Route this plugin through a provider (and model).",
33433
- " wstack plugin llm <plugin> - <model> Keep the session provider, override only the model.",
33434
- " wstack plugin llm <plugin> --clear Remove the override (back to the session default).",
33435
- "",
33436
- "The override is stored as config.extensions.<plugin>.llm and is used by",
33437
- "every api.llm call the plugin makes. Without an override, plugins follow",
33438
- "the active session provider/model (the chat leader)."
33439
- ].join("\n");
33440
- function renderPluginLlmOverviews(config, sessionDefault) {
33441
- const rows = [];
33442
- for (const [pluginName2, ext] of Object.entries(config.extensions ?? {})) {
33443
- const llm = ext?.["llm"];
33444
- if (llm && typeof llm === "object") {
33445
- const o = llm;
33446
- rows.push({
33447
- name: pluginName2,
33448
- provider: o.provider ?? "(session)",
33449
- model: o.model ?? "(session)"
33450
- });
33451
- }
33452
- }
33453
- const lines = [`Per-plugin LLM routing (session default: ${sessionDefault}):`];
33454
- if (rows.length === 0) {
33455
- lines.push(
33456
- " No plugin overrides \u2014 every plugin follows the session default.",
33457
- " Set one with: wstack plugin llm <plugin> <provider> [model]"
34333
+ }
34334
+ const leaderModel = config.model ?? "unknown";
34335
+ const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds2.includes(pid)) : [];
34336
+ const warningLine = unknownProviders.length > 0 ? ` ${color.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
34337
+ ` : "";
34338
+ writeLine(`${color.bold("Model Competency Evaluation")}`);
34339
+ writeLine(
34340
+ color.dim(
34341
+ `Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
34342
+ )
33458
34343
  );
33459
- } else {
33460
- rows.sort((a, b) => a.name.localeCompare(b.name));
33461
- for (const r of rows) {
33462
- lines.push(` ${r.name.padEnd(24)} provider=${r.provider.padEnd(16)} model=${r.model}`);
34344
+ writeLine(warningLine);
34345
+ writeLine(color.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
34346
+ writeLine();
34347
+ const collected = /* @__PURE__ */ new Map();
34348
+ let total = 0;
34349
+ let ok2 = 0;
34350
+ for (const pid of targetProviderIds) {
34351
+ const prov = createProviderForId(pid, config);
34352
+ if (!prov) {
34353
+ writeLine(color.dim(` \u2298 ${pid}: provider unavailable, skipping`));
34354
+ continue;
34355
+ }
34356
+ for (const cat of targetCategories) {
34357
+ const task = EVAL_TASKS[cat];
34358
+ if (!task) continue;
34359
+ const candidates = rankModels(providers, hasKey, cat, modelsPerCat).filter(
34360
+ (c) => c.provider === pid
34361
+ );
34362
+ if (candidates.length === 0) continue;
34363
+ if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
34364
+ for (const c of candidates) {
34365
+ total++;
34366
+ const modelKey = `${pid}/${c.model}`;
34367
+ try {
34368
+ const start = Date.now();
34369
+ const resp = await prov.complete(
34370
+ {
34371
+ model: c.model,
34372
+ system: [{ type: "text", text: "Be thorough and correct." }],
34373
+ messages: [
34374
+ { role: "user", content: [{ type: "text", text: task.prompt }] }
34375
+ ],
34376
+ maxTokens: 1024
34377
+ },
34378
+ { signal: AbortSignal.timeout(45e3) }
34379
+ );
34380
+ const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
34381
+ const respUsage = resp.usage;
34382
+ collected.get(cat)?.set(modelKey, {
34383
+ model: modelKey,
34384
+ latency: Date.now() - start,
34385
+ tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
34386
+ text: respText
34387
+ });
34388
+ ok2++;
34389
+ } catch {
34390
+ collected.get(cat)?.set(modelKey, {
34391
+ model: modelKey,
34392
+ latency: -1,
34393
+ tokens: 0,
34394
+ text: ""
34395
+ });
34396
+ }
34397
+ }
34398
+ }
33463
34399
  }
33464
- lines.push("", "Clear one with: wstack plugin llm <plugin> --clear");
33465
- }
33466
- return lines.join("\n");
33467
- }
33468
- async function runPluginLlmCommand(args, deps) {
33469
- const sessionDefault = `${deps.config.provider ?? "?"} / ${deps.config.model ?? "?"}`;
33470
- if (args.length === 0 || args[0] === "list") {
33471
- return {
33472
- code: 0,
33473
- level: "output",
33474
- message: renderPluginLlmOverviews(deps.config, sessionDefault)
33475
- };
33476
- }
33477
- const name = args[0];
33478
- if (!name || name.startsWith("-")) return errorResult(PLUGIN_LLM_USAGE);
33479
- const currentOverride = deps.config.extensions?.[name]?.["llm"];
33480
- if (args.length === 1) {
33481
- const lines = [`LLM routing for plugin "${name}":`];
33482
- if (currentOverride && typeof currentOverride === "object") {
33483
- const o = currentOverride;
33484
- lines.push(
33485
- ` override: provider=${o.provider ?? "(session)"} model=${o.model ?? "(session)"}`,
33486
- ` session default: ${sessionDefault}`
33487
- );
33488
- } else {
33489
- lines.push(
33490
- ` no override \u2014 follows the session default (${sessionDefault}).`,
33491
- ` Set one with: wstack plugin llm ${name} <provider> [model]`
33492
- );
34400
+ writeLine(`${color.dim(`Phase 1: ${ok2}/${total} calls succeeded`)}`);
34401
+ writeLine();
34402
+ if (collected.size === 0) {
34403
+ writeLine(color.amber("No responses collected. Check provider configuration."));
34404
+ return 0;
33493
34405
  }
33494
- return { code: 0, level: "output", message: lines.join("\n") };
33495
- }
33496
- const existing = await readConfig(deps.configPath);
33497
- const fileExtensions = isRecord3(existing.extensions) ? existing.extensions : {};
33498
- const pluginExt = isRecord3(fileExtensions[name]) ? { ...fileExtensions[name] } : {};
33499
- if (args[1] === "--clear" || args[1] === "clear") {
33500
- if (!("llm" in pluginExt) && !currentOverride) {
33501
- return { code: 0, level: "info", message: `Plugin "${name}" has no LLM override.` };
34406
+ const leaderProvider = createProviderForId(config.provider, config);
34407
+ if (leaderProvider) {
34408
+ writeLine(`${color.bold("Phase 2")} \u2014 ${color.dim("leader ranks responses")}`);
34409
+ writeLine();
33502
34410
  }
33503
- delete pluginExt["llm"];
33504
- if (Object.keys(pluginExt).length === 0) delete fileExtensions[name];
33505
- else fileExtensions[name] = pluginExt;
33506
- existing.extensions = fileExtensions;
33507
- await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
33508
- const merged2 = mergeExtensionsForPatch(deps.config, fileExtensions);
33509
- const mergedEntry = merged2[name];
33510
- if (mergedEntry) {
33511
- delete mergedEntry["llm"];
33512
- if (Object.keys(mergedEntry).length === 0) delete merged2[name];
34411
+ const rankings = /* @__PURE__ */ new Map();
34412
+ for (const [cat, responses] of collected) {
34413
+ const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
34414
+ if (valid.length < 2) {
34415
+ if (valid.length === 1) {
34416
+ const m = valid[0]?.model;
34417
+ if (m !== void 0) {
34418
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
34419
+ rankings.get(m)?.set(cat, { rank: 1, total: 1 });
34420
+ }
34421
+ }
34422
+ continue;
34423
+ }
34424
+ const task = EVAL_TASKS[cat];
34425
+ if (leaderProvider) {
34426
+ const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
34427
+ for (let i = 0; i < valid.length; i++) {
34428
+ const m = valid[ranked[i] ?? i]?.model;
34429
+ if (m === void 0) continue;
34430
+ if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
34431
+ rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
34432
+ }
34433
+ } else {
34434
+ for (const r of valid) {
34435
+ if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
34436
+ rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
34437
+ }
34438
+ }
33513
34439
  }
33514
- return {
33515
- code: 0,
33516
- level: "info",
33517
- message: `Cleared LLM override for "${name}" \u2014 it now follows the session default (${sessionDefault}). Takes effect immediately for api.llm calls.`,
33518
- patch: { extensions: merged2 }
33519
- };
33520
- }
33521
- const providerArg = args[1];
33522
- const modelArg = args[2];
33523
- if (!providerArg || providerArg.startsWith("--")) return errorResult(PLUGIN_LLM_USAGE);
33524
- const provider = providerArg === "-" ? void 0 : providerArg;
33525
- const model = modelArg && modelArg !== "-" ? modelArg : void 0;
33526
- if (!provider && !model) return errorResult(PLUGIN_LLM_USAGE);
33527
- const llm = {};
33528
- if (provider) llm["provider"] = provider;
33529
- if (model) llm["model"] = model;
33530
- pluginExt["llm"] = llm;
33531
- fileExtensions[name] = pluginExt;
33532
- existing.extensions = fileExtensions;
33533
- await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
33534
- const configured = Object.keys(deps.config.providers ?? {});
33535
- const note = provider && configured.length > 0 && !configured.includes(provider) ? `
33536
- Note: "${provider}" is not in config.providers (configured: ${configured.join(", ")}). Catalog providers (anthropic, openai, \u2026) still resolve if credentials are available.` : "";
33537
- const merged = mergeExtensionsForPatch(deps.config, fileExtensions);
33538
- return {
33539
- code: 0,
33540
- level: "info",
33541
- message: `Plugin "${name}" now routes api.llm calls via provider=${provider ?? "(session)"} model=${model ?? "(session)"}. Session default stays ${sessionDefault}. Takes effect immediately for api.llm calls.${note}`,
33542
- patch: { extensions: merged }
33543
- };
33544
- }
33545
- function mergeExtensionsForPatch(config, fileExtensions) {
33546
- const merged = {};
33547
- for (const [k, v] of Object.entries(config.extensions ?? {})) {
33548
- merged[k] = { ...v };
33549
- }
33550
- for (const [k, v] of Object.entries(fileExtensions)) {
33551
- if (isRecord3(v)) merged[k] = { ...merged[k] ?? {}, ...v };
33552
- }
33553
- for (const k of Object.keys(merged)) {
33554
- if (!(k in fileExtensions) && !(k in (config.extensions ?? {}))) delete merged[k];
33555
- }
33556
- return merged;
33557
- }
33558
- function resolvePluginSpecifier(input) {
33559
- return OFFICIAL_ALIASES.get(input.toLowerCase()) ?? input;
33560
- }
33561
- function resolvePluginToggleSpecifier(input) {
33562
- if (input.toLowerCase() === "@wrongstack/telegram") return "telegram";
33563
- if (auditEntryFor(input)) return input;
33564
- return resolvePluginSpecifier(input);
33565
- }
33566
- function renderOfficialPlugins(config) {
33567
- return [
33568
- "Official plugins:",
33569
- ...OFFICIAL_PLUGINS.map((p) => {
33570
- const state = config ? officialPluginState(config, p.specifier) : "";
33571
- const status = state ? `${state.padEnd(14)} ` : "";
33572
- return ` ${p.alias.padEnd(12)} ${status}${p.specifier.padEnd(24)} ${p.description}`;
33573
- }),
33574
- "",
33575
- "Use `wstack plugin add <alias>` or `/plugin install <alias>`."
33576
- ].join("\n");
33577
- }
33578
- function renderConfiguredPlugins(config) {
33579
- const plugins = config.plugins ?? [];
33580
- if (plugins.length === 0) {
33581
- return [
33582
- "No plugins configured.",
33583
- "Use `wstack plugin add <specifier>` or `/plugin install <official-alias>`."
33584
- ].join("\n");
33585
- }
33586
- return plugins.map((p) => {
33587
- const name = pluginName(p);
33588
- const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
33589
- const official = OFFICIAL_PLUGINS.find((entry) => entry.specifier === name);
33590
- const suffix = official ? ` (${official.alias})` : "";
33591
- return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
33592
- }).join("\n");
33593
- }
33594
- function renderPluginAuditReport(config) {
33595
- const configured = config.plugins ?? [];
33596
- const lines = [
33597
- "Plugin audit report:",
33598
- " State shows the effective boot state. Default-active plugins run unless disabled in config.",
33599
- " Locked rows are visible in the menu but cannot be toggled there.",
33600
- "",
33601
- " Built-in audit entries:"
33602
- ];
33603
- for (const entry of PLUGIN_AUDIT_ENTRIES) {
33604
- const { state, source } = effectiveAuditState(config, entry);
33605
- const policy = entry.canDisable ? "toggleable" : lockedReason(entry);
33606
- lines.push(
33607
- ` ${entry.name.padEnd(24)} ${state.padEnd(8)} ${source.padEnd(8)} risk=${entry.risk.padEnd(6)} ${policy.padEnd(24)} ${entry.summary}`
34440
+ writeLine(`${color.bold("Competency Report")}`);
34441
+ writeLine();
34442
+ const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
34443
+ const catList = [...collected.keys()];
34444
+ const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
34445
+ const cw = 12;
34446
+ writeLine(
34447
+ ` ${color.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
33608
34448
  );
33609
- }
33610
- const extra = configured.filter(
33611
- (plugin) => !PLUGIN_AUDIT_ENTRIES.some((entry) => pluginMatchesToggleSpec(plugin, entry.name))
33612
- );
33613
- if (extra.length > 0) {
33614
- lines.push("", " Configured plugins outside the audit list:");
33615
- for (const plugin of extra) {
33616
- const name = pluginName(plugin);
33617
- const state = typeof plugin === "object" && plugin.enabled === false ? "disabled" : "enabled";
33618
- lines.push(
33619
- ` ${name.padEnd(24)} ${state.padEnd(8)} config risk=custom user-configured plugin`
33620
- );
34449
+ writeLine(` ${color.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
34450
+ for (const model of allModels) {
34451
+ const mr = rankings.get(model);
34452
+ const provFromModel = model.split("/")[0] ?? "";
34453
+ const modelColor = provFromModel === config.provider ? color.cyan : color.green;
34454
+ let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
34455
+ for (const cat of catList) {
34456
+ const e = mr.get(cat);
34457
+ if (e) {
34458
+ const pct2 = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
34459
+ const pc = pct2 >= 80 ? color.green : pct2 >= 50 ? color.amber : color.red;
34460
+ row += `${pc(`#${e.rank} ${pct2}%`.padEnd(cw + 2))}`;
34461
+ } else {
34462
+ row += color.dim("\u2014".padEnd(cw + 2));
34463
+ }
34464
+ }
34465
+ writeLine(row);
33621
34466
  }
34467
+ writeLine();
34468
+ writeLine(color.dim("#1 100% = best in category. \u2014 = not tested."));
34469
+ writeLine();
34470
+ writeLine(color.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
34471
+ writeLine(
34472
+ color.dim(
34473
+ "Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
34474
+ )
34475
+ );
34476
+ writeLine(
34477
+ color.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
34478
+ );
34479
+ return 0;
33622
34480
  }
33623
- lines.push(
33624
- "",
33625
- "Use `/plugin menu` in the TUI, or `wstack plugin toggle <name>` for toggleable rows."
33626
- );
33627
- return lines.join("\n");
33628
- }
33629
- async function readConfig(file) {
33630
- try {
33631
- return JSON.parse(await fs2.readFile(file, "utf8"));
33632
- } catch {
33633
- return {};
34481
+ writeLine(`${color.bold("API Key Status")}`);
34482
+ writeLine();
34483
+ for (const prov of providers) {
34484
+ const k = hasKey(prov.id);
34485
+ writeLine(` ${checkMark(k)} ${color.bold(prov.id.padEnd(18))} ${color.dim(prov.name)}`);
33634
34486
  }
33635
- }
33636
- function pluginName(p) {
33637
- return typeof p === "string" ? p : p.name;
33638
- }
33639
- function pluginEntry(spec, enabled) {
33640
- return enabled ? spec : { name: spec, enabled: false };
33641
- }
33642
- function pluginMatchesToggleSpec(p, spec) {
33643
- const name = pluginName(p);
33644
- if (spec === "telegram") return name === "telegram" || name === "@wrongstack/telegram";
33645
- return name === spec;
33646
- }
33647
- function effectiveAuditState(config, entry) {
33648
- const configured = (config.plugins ?? []).find(
33649
- (plugin) => pluginMatchesToggleSpec(plugin, entry.name)
33650
- );
33651
- if (configured) {
33652
- return {
33653
- state: typeof configured === "object" && configured.enabled === false ? "disabled" : "enabled",
33654
- source: "config"
34487
+ writeLine();
34488
+ writeLine(`${color.dim(`Leader: ${config.provider}/${config.model}`)}`);
34489
+ writeLine();
34490
+ writeLine(`${color.bold("Model Capabilities")} ${color.dim("\u2014 matched to known profiles")}`);
34491
+ writeLine();
34492
+ for (const prov of providers) {
34493
+ if (!hasKey(prov.id)) continue;
34494
+ writeLine(` ${color.bold(prov.id)} ${color.dim(`(${prov.name})`)}`);
34495
+ const tiers = {
34496
+ premium: [],
34497
+ standard: [],
34498
+ budget: [],
34499
+ unknown: []
33655
34500
  };
33656
- }
33657
- return {
33658
- state: entry.defaultState === "active" ? "enabled" : "disabled",
33659
- source: "default"
33660
- };
33661
- }
33662
- function lockedReason(entry) {
33663
- if (entry.canDisable) return "toggleable";
33664
- if (entry.name === "secret-scanner" || entry.name === "branch-guard") {
33665
- return "locked: safety guard";
33666
- }
33667
- return "locked: core surface";
33668
- }
33669
- function auditEntryFor(spec) {
33670
- return PLUGIN_AUDIT_ENTRIES.find((entry) => entry.name === spec);
33671
- }
33672
- function officialPluginState(config, spec) {
33673
- const match = (config.plugins ?? []).find((p) => pluginName(p) === spec);
33674
- if (!match) return "not configured";
33675
- return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
33676
- }
33677
- async function togglePlugin(spec, deps) {
33678
- const auditEntry = auditEntryFor(spec);
33679
- if (auditEntry?.canDisable === false) {
33680
- return errorResult(`Plugin "${spec}" is locked and cannot be toggled from the plugin menu.`);
33681
- }
33682
- const existing = await readConfig(deps.configPath);
33683
- const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
33684
- const idx = plugins.findIndex((p) => pluginMatchesToggleSpec(p, spec));
33685
- const configured = idx >= 0 ? plugins[idx] : void 0;
33686
- const enabled = configured ? !(typeof configured === "object" && configured.enabled === false) : auditEntry?.defaultState === "active";
33687
- if (!configured && !auditEntry) {
33688
- return errorResult(
33689
- `Plugin "${spec}" is not configured. Use "wstack plugin add ${spec}" first.`
33690
- );
33691
- }
33692
- let nextPlugins;
33693
- let nextEnabled;
33694
- if (enabled) {
33695
- nextEnabled = false;
33696
- nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
33697
- nextPlugins.push({ name: spec, enabled: false });
33698
- } else {
33699
- nextEnabled = true;
33700
- if (auditEntry?.defaultState === "active") {
33701
- nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
33702
- } else {
33703
- nextPlugins = plugins.filter((p) => !pluginMatchesToggleSpec(p, spec));
33704
- nextPlugins.push(spec);
34501
+ for (const m of prov.models ?? []) {
34502
+ const profile = findProfile(prov.id, m.id);
34503
+ tiers[profile?.costTier ?? "unknown"]?.push(m);
33705
34504
  }
34505
+ for (const tier of ["premium", "standard", "budget", "unknown"]) {
34506
+ const tierModels = tiers[tier];
34507
+ if (tierModels.length === 0) continue;
34508
+ const label = tier === "unknown" ? color.dim("unmatched") : `${costLabel(tier)} ${tier}`;
34509
+ writeLine(` ${label}`);
34510
+ for (const m of tierModels) {
34511
+ const cap = m.capabilities;
34512
+ const ctx = cap?.contextWindow ?? 0;
34513
+ const maxOut = cap?.maxOutputTokens ?? 0;
34514
+ const profile = findProfile(prov.id, m.id);
34515
+ const family = profile ? `${speedLabel(profile.speedTier)} ${color.green(profile.family)}` : color.dim("no profile match");
34516
+ const pricing = m.pricing ? `${color.dim("in")}${fmtPrice2(m.pricing.input)} ${color.dim("out")}${fmtPrice2(m.pricing.output)}` : color.dim("pricing ?");
34517
+ writeLine(
34518
+ ` ${color.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens2(ctx).padEnd(6)}` : color.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens2(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
34519
+ );
34520
+ }
34521
+ }
34522
+ writeLine();
33706
34523
  }
33707
- const features = {
33708
- ...isRecord3(deps.config.features) ? deps.config.features : {},
33709
- ...isRecord3(existing.features) ? existing.features : {},
33710
- plugins: true
33711
- };
33712
- existing.plugins = nextPlugins;
33713
- existing.features = features;
33714
- await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
33715
- return {
33716
- code: 0,
33717
- level: "info",
33718
- message: `${nextEnabled ? "Enabled" : "Disabled"} "${spec}". Config written to ${deps.configPath}.`,
33719
- patch: { plugins: nextPlugins, features },
33720
- restartRequired: true
33721
- };
33722
- }
33723
- async function upsertPlugin(spec, opts, deps, verb) {
33724
- const existing = await readConfig(deps.configPath);
33725
- const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
33726
- const idx = plugins.findIndex((p) => pluginName(p) === spec);
33727
- const nextEntry = pluginEntry(spec, opts.enabled);
33728
- if (idx >= 0) plugins[idx] = nextEntry;
33729
- else plugins.push(nextEntry);
33730
- const features = {
33731
- ...isRecord3(deps.config.features) ? deps.config.features : {},
33732
- ...isRecord3(existing.features) ? existing.features : {},
33733
- plugins: true
33734
- };
33735
- existing.plugins = plugins;
33736
- existing.features = features;
33737
- await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
33738
- return {
33739
- code: 0,
33740
- level: "info",
33741
- message: `${verb} "${spec}" (${opts.enabled ? "enabled" : "disabled"}). Config written to ${deps.configPath}.`,
33742
- patch: { plugins, features },
33743
- restartRequired: true
33744
- };
33745
- }
33746
- async function removePlugin(spec, deps) {
33747
- const existing = await readConfig(deps.configPath);
33748
- const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
33749
- const next = plugins.filter((p) => pluginName(p) !== spec);
33750
- if (next.length === plugins.length) {
33751
- return errorResult(`Plugin "${spec}" not in config.`);
33752
- }
33753
- existing.plugins = next;
33754
- await atomicWrite(deps.configPath, JSON.stringify(existing, null, 2), { mode: 384 });
33755
- return {
33756
- code: 0,
33757
- level: "info",
33758
- message: `Removed "${spec}" from config.`,
33759
- patch: { plugins: next },
33760
- restartRequired: true
33761
- };
33762
- }
33763
- function errorResult(message) {
33764
- return { code: 1, level: "error", message };
33765
- }
33766
- function isRecord3(value) {
33767
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
33768
- }
34524
+ await renderSuggest();
34525
+ writeLine();
34526
+ writeLine(color.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
34527
+ writeLine(color.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
34528
+ return 0;
34529
+ };
33769
34530
 
33770
34531
  // src/subcommands/handlers/plugin-usage.ts
34532
+ init_plugin_management();
33771
34533
  var pluginCmd = async (args, deps) => {
33772
34534
  const result = await runPluginManagementCommand(args, {
33773
34535
  config: deps.config,
@@ -36240,7 +37002,14 @@ async function handleHqShortCircuit(flags) {
36240
37002
  try {
36241
37003
  await handle2.close();
36242
37004
  } catch (err) {
36243
- console.error(`HQ server close error: ${String(err)}`);
37005
+ console.error(
37006
+ JSON.stringify({
37007
+ level: "error",
37008
+ event: "hq.server_close_failed",
37009
+ message: String(err),
37010
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
37011
+ })
37012
+ );
36244
37013
  }
36245
37014
  resolve13();
36246
37015
  };
@@ -36698,7 +37467,16 @@ Reply with ONLY the JSON object.`
36698
37467
  coordinatorController["onCoordinatorStart"] = (goal) => {
36699
37468
  const coordinator = state.autonomousCoordinator;
36700
37469
  if (!coordinator) return;
36701
- coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => console.error("[coordinator] run() failed:", err));
37470
+ coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch(
37471
+ (err) => console.error(
37472
+ JSON.stringify({
37473
+ level: "error",
37474
+ event: "coordinator.run_failed",
37475
+ message: err instanceof Error ? err.message : String(err),
37476
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
37477
+ })
37478
+ )
37479
+ );
36702
37480
  };
36703
37481
  coordinatorController["onCoordinatorStop"] = () => state.autonomousCoordinator?.stop();
36704
37482
  coordinatorController["onCoordinatorTasks"] = async () => {
@@ -38312,7 +39090,7 @@ ${color.dim(
38312
39090
  activeCtrl = void 0;
38313
39091
  }
38314
39092
  } else if (autonomy === "suggest" && !hasOpenTodos(opts.agent.ctx.todos)) {
38315
- const suggestPrompt = 'Based on what you just did, suggest 3 concrete next steps. Format: numbered list, one line each, no explanation. If there is nothing meaningful left, say "No further steps needed."';
39093
+ const suggestPrompt = 'Based on what you just did, suggest 3 concrete next steps. If you include suggestions, wrap them in a balanced <nextsteps>...</nextsteps> block, with one numbered prompt per line and no explanation. If there is nothing meaningful left, say "No further steps needed."';
38316
39094
  const suggestBlocks = [{ type: "text", text: suggestPrompt }];
38317
39095
  const suggestCtrl = new AbortController();
38318
39096
  activeCtrl = suggestCtrl;
@@ -38680,6 +39458,16 @@ async function execute(deps) {
38680
39458
  saveStatuslineHiddenItems,
38681
39459
  getPluginItems,
38682
39460
  onPluginToggle,
39461
+ getMcpServers,
39462
+ onMcpToggle,
39463
+ onMcpRestart,
39464
+ getToolsItems,
39465
+ onToolToggle,
39466
+ getBrainData,
39467
+ onBrainRiskLevel,
39468
+ getShadowData,
39469
+ onShadowStart,
39470
+ onShadowStop,
38683
39471
  authHost,
38684
39472
  agentsMonitorController,
38685
39473
  onPanelOpen,
@@ -38977,7 +39765,7 @@ async function execute(deps) {
38977
39765
  // shared suggestion store so `/next 1`, `/next 1 2 3` work without
38978
39766
  // requiring `/suggest` first. Called unconditionally on every done
38979
39767
  // turn. The live todo list is passed through so we suppress
38980
- // <next_steps> while the in-flight todo loop is still in progress
39768
+ // <nextsteps> while the in-flight todo loop is still in progress
38981
39769
  // — finishing the open todos comes before offering new prompt
38982
39770
  // options.
38983
39771
  onSuggestionsParsed: (finalText) => {
@@ -39034,12 +39822,26 @@ async function execute(deps) {
39034
39822
  onCoordinatorStart: (goal) => {
39035
39823
  const coordinator = ensureAutonomousCoordinator();
39036
39824
  if (!coordinator) {
39037
- console.error("[coordinator] not ready \u2014 no director available");
39825
+ console.error(
39826
+ JSON.stringify({
39827
+ level: "error",
39828
+ event: "coordinator.not_ready",
39829
+ message: "no director available",
39830
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
39831
+ })
39832
+ );
39038
39833
  return;
39039
39834
  }
39040
39835
  if (state.coordinatorRun) return;
39041
39836
  state.coordinatorRun = coordinator.run({ goal: goal ?? "Improve the codebase", runUntilComplete: true }).then(() => void 0).catch((err) => {
39042
- console.error("[coordinator] run() failed:", err);
39837
+ console.error(
39838
+ JSON.stringify({
39839
+ level: "error",
39840
+ event: "coordinator.run_failed",
39841
+ message: err instanceof Error ? err.message : String(err),
39842
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
39843
+ })
39844
+ );
39043
39845
  }).finally(() => {
39044
39846
  state.coordinatorRun = null;
39045
39847
  });
@@ -39152,6 +39954,16 @@ async function execute(deps) {
39152
39954
  saveStatuslineHiddenItems,
39153
39955
  getPluginItems,
39154
39956
  onPluginToggle,
39957
+ getMcpServers,
39958
+ onMcpToggle,
39959
+ onMcpRestart,
39960
+ getToolsItems,
39961
+ onToolToggle,
39962
+ getBrainData,
39963
+ onBrainRiskLevel,
39964
+ getShadowData,
39965
+ onShadowStart,
39966
+ onShadowStop,
39155
39967
  authHost,
39156
39968
  agentsMonitorController,
39157
39969
  getLiveSessions: () => getLiveSessions({ state }),
@@ -39709,6 +40521,7 @@ var DEFAULT_SUBAGENT_HIDDEN_TOOLS = /* @__PURE__ */ new Set([
39709
40521
  "delegate",
39710
40522
  "spawn_subagent",
39711
40523
  "assign_task",
40524
+ "kanban_queue",
39712
40525
  "await_tasks",
39713
40526
  "ask_subagent",
39714
40527
  "ask_result",
@@ -40331,6 +41144,7 @@ var MultiAgentHost = class _MultiAgentHost {
40331
41144
  events,
40332
41145
  confirmAwaiter: void 0,
40333
41146
  iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? 12e4,
41147
+ maxToolTimeoutMs: config.tools?.maxToolTimeoutMs ?? 3e5,
40334
41148
  perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? 1e5,
40335
41149
  tracer: void 0
40336
41150
  });
@@ -41047,6 +41861,7 @@ var MultiAgentHost = class _MultiAgentHost {
41047
41861
  }
41048
41862
  }
41049
41863
  };
41864
+ init_plugin_management();
41050
41865
 
41051
41866
  // src/session-stats.ts
41052
41867
  init_utils();
@@ -41333,7 +42148,7 @@ async function setupCodebaseIndexing(deps) {
41333
42148
  let watcher;
41334
42149
  if (idx.watchExternal) {
41335
42150
  try {
41336
- watcher = fs27.watch(projectRoot, { recursive: true }, (_event, filename) => {
42151
+ watcher = fs28.watch(projectRoot, { recursive: true }, (_event, filename) => {
41337
42152
  if (!filename) return;
41338
42153
  const rel = filename.toString();
41339
42154
  if (isIgnored(rel)) return;
@@ -41677,6 +42492,7 @@ function createAgent(params) {
41677
42492
  events: params.events,
41678
42493
  confirmAwaiter: params.confirmAwaiter,
41679
42494
  iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
42495
+ maxToolTimeoutMs: params.config.tools.maxToolTimeoutMs ?? 3e5,
41680
42496
  perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
41681
42497
  tracer: params.tracer,
41682
42498
  logger,
@@ -42726,9 +43542,12 @@ async function main(argv) {
42726
43542
  cacheDir: wpaths.cacheDir
42727
43543
  });
42728
43544
  if (config.features.mcp) {
43545
+ const presets = allServers$1();
42729
43546
  for (const cfg of Object.values(config.mcpServers ?? {})) {
43547
+ const preset = presets[cfg.name];
43548
+ const merged = preset ? { ...preset, ...cfg } : cfg;
42730
43549
  try {
42731
- await mcpRegistry.start(cfg);
43550
+ await mcpRegistry.start(merged);
42732
43551
  } catch (err) {
42733
43552
  logger.warn(`MCP server "${cfg.name}" failed to start`, err);
42734
43553
  }
@@ -44357,6 +45176,114 @@ Restart WrongStack to load or unload plugin code in this session.`;
44357
45176
  saveStatuslineHiddenItems,
44358
45177
  getPluginItems: getPluginPickerItems,
44359
45178
  onPluginToggle: togglePluginFromPicker,
45179
+ getMcpServers: () => {
45180
+ const servers = config.mcpServers ?? {};
45181
+ const liveStatus = mcpRegistry.list();
45182
+ const liveMap = new Map(liveStatus.map((s) => [s.name, s]));
45183
+ return Object.entries(servers).map(([name, cfg]) => {
45184
+ const live = liveMap.get(name);
45185
+ return {
45186
+ name,
45187
+ enabled: cfg.enabled !== false,
45188
+ status: live ? live.state : "stopped",
45189
+ transport: cfg.transport ?? "stdio",
45190
+ description: cfg.description,
45191
+ toolCount: live?.toolCount ?? 0,
45192
+ lazy: cfg.lazy
45193
+ };
45194
+ });
45195
+ },
45196
+ onMcpToggle: async (name) => {
45197
+ const { enableMcp, disableMcp, listMcp } = await import('@wrongstack/mcp');
45198
+ const deps = { configPath: wpaths.globalConfig, registry: mcpRegistry, presets: allServers$1() };
45199
+ const live = mcpRegistry.list().find((s) => s.name === name);
45200
+ const isCurrentlyEnabled = live !== void 0 && live.state !== "idle";
45201
+ const result = isCurrentlyEnabled ? await disableMcp(name, deps) : await enableMcp(name, deps);
45202
+ const items = await listMcp(deps);
45203
+ return {
45204
+ items: items.map((s) => ({ name: s.name, enabled: s.enabled, status: s.status, transport: s.transport, description: s.description, toolCount: s.tools.length, lazy: s.lazy })),
45205
+ message: result.ok ? result.server ? `${result.server.status === "connected" ? "\u25CF" : "\u25CB"} ${name}` : result.message : void 0,
45206
+ error: result.ok ? void 0 : result.message
45207
+ };
45208
+ },
45209
+ onMcpRestart: async (name) => {
45210
+ const { listMcp } = await import('@wrongstack/mcp');
45211
+ const deps = { configPath: wpaths.globalConfig, registry: mcpRegistry, presets: allServers$1() };
45212
+ try {
45213
+ await mcpRegistry.restart(name);
45214
+ } catch (err) {
45215
+ const message = err instanceof Error ? err.message : String(err);
45216
+ const items2 = await listMcp(deps);
45217
+ return { items: items2.map((s) => ({ name: s.name, enabled: s.enabled, status: s.status, transport: s.transport, description: s.description, toolCount: s.tools.length, lazy: s.lazy })), message: void 0, error: message };
45218
+ }
45219
+ const items = await listMcp(deps);
45220
+ return { items: items.map((s) => ({ name: s.name, enabled: s.enabled, status: s.status, transport: s.transport, description: s.description, toolCount: s.tools.length, lazy: s.lazy })), message: `Restarted "${name}".`, error: void 0 };
45221
+ },
45222
+ getToolsItems: () => {
45223
+ const all = toolRegistry.listWithOwner().map(({ tool, owner }) => {
45224
+ const descMode = getToolDescriptionMode(toolRegistry, tool.name);
45225
+ return {
45226
+ name: tool.name,
45227
+ owner,
45228
+ category: tool.category ?? "Other",
45229
+ enabled: !toolRegistry.isDisabled(tool.name),
45230
+ mutating: tool.mutating,
45231
+ permission: tool.permission,
45232
+ descMode,
45233
+ description: tool.description
45234
+ };
45235
+ });
45236
+ return all;
45237
+ },
45238
+ onToolToggle: async (name) => {
45239
+ const disabled = new Set(config.tools?.disabledTools ?? []);
45240
+ const isCurrentlyDisabled = toolRegistry.isDisabled(name);
45241
+ if (isCurrentlyDisabled) {
45242
+ toolRegistry.enable(name);
45243
+ disabled.delete(name);
45244
+ } else {
45245
+ toolRegistry.disable(name);
45246
+ disabled.add(name);
45247
+ }
45248
+ const nextTools = { ...config.tools ?? {}, disabledTools: Array.from(disabled) };
45249
+ config = patchConfig(config, { tools: nextTools });
45250
+ configStore.update({ tools: nextTools });
45251
+ const items = toolRegistry.listWithOwner().map(({ tool, owner }) => ({
45252
+ name: tool.name,
45253
+ owner,
45254
+ category: tool.category ?? "Other",
45255
+ enabled: !toolRegistry.isDisabled(tool.name),
45256
+ mutating: tool.mutating,
45257
+ permission: tool.permission,
45258
+ descMode: getToolDescriptionMode(toolRegistry, tool.name),
45259
+ description: tool.description
45260
+ }));
45261
+ return {
45262
+ items,
45263
+ message: isCurrentlyDisabled ? `Enabled "${name}".` : `Disabled "${name}".`,
45264
+ error: void 0
45265
+ };
45266
+ },
45267
+ getBrainData: () => {
45268
+ const ceiling = brainSettings?.maxAutoRisk ?? "medium";
45269
+ const log = brainLog.slice(-20).map((entry) => ({
45270
+ kind: entry.kind,
45271
+ question: entry.question,
45272
+ outcome: entry.outcome,
45273
+ age: typeof entry.at === "number" ? (() => {
45274
+ const s = Math.max(0, Math.round((Date.now() - entry.at) / 1e3));
45275
+ if (s < 60) return `${s}s`;
45276
+ if (s < 3600) return `${Math.round(s / 60)}m`;
45277
+ return `${Math.round(s / 3600)}h`;
45278
+ })() : ""
45279
+ }));
45280
+ return { riskLevel: ceiling, log };
45281
+ },
45282
+ onBrainRiskLevel: (level) => {
45283
+ if (!brainSettings) return "Brain settings not available.";
45284
+ brainSettings.maxAutoRisk = level;
45285
+ return void 0;
45286
+ },
44360
45287
  // Interactive /auth panel host — provider/key CRUD, catalog/local adds
44361
45288
  // and OAuth sign-in, all executed CLI-side against the encrypted config
44362
45289
  // (secrets never enter the TUI; key values arrive pre-masked).