@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.
- package/dist/index.js +2128 -1201
- package/dist/index.js.map +1 -1
- 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,
|
|
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
|
|
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
|
-
).
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
5241
|
+
if (!fs28.existsSync(filePath) || fs28.statSync(filePath).isDirectory()) {
|
|
4258
5242
|
const indexPath = path5.join(distDir, "index.html");
|
|
4259
|
-
if (
|
|
4260
|
-
const html =
|
|
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 =
|
|
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
|
|
6444
|
-
|
|
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:
|
|
6448
|
-
|
|
6449
|
-
|
|
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
|
|
7986
|
-
|
|
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", {
|
|
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", {
|
|
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
|
-
{
|
|
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
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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(
|
|
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 <
|
|
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.
|
|
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=
|
|
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 (
|
|
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() ||
|
|
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.
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
33460
|
-
|
|
33461
|
-
|
|
33462
|
-
|
|
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
|
-
|
|
33465
|
-
|
|
33466
|
-
|
|
33467
|
-
|
|
33468
|
-
|
|
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
|
-
|
|
33495
|
-
|
|
33496
|
-
|
|
33497
|
-
|
|
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
|
-
|
|
33504
|
-
|
|
33505
|
-
|
|
33506
|
-
|
|
33507
|
-
|
|
33508
|
-
|
|
33509
|
-
|
|
33510
|
-
|
|
33511
|
-
|
|
33512
|
-
|
|
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
|
-
|
|
33515
|
-
|
|
33516
|
-
|
|
33517
|
-
|
|
33518
|
-
|
|
33519
|
-
|
|
33520
|
-
|
|
33521
|
-
|
|
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
|
-
|
|
33611
|
-
|
|
33612
|
-
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
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
|
-
|
|
33624
|
-
|
|
33625
|
-
|
|
33626
|
-
|
|
33627
|
-
|
|
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
|
-
|
|
33637
|
-
|
|
33638
|
-
}
|
|
33639
|
-
|
|
33640
|
-
|
|
33641
|
-
|
|
33642
|
-
|
|
33643
|
-
|
|
33644
|
-
|
|
33645
|
-
|
|
33646
|
-
|
|
33647
|
-
|
|
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
|
-
|
|
33658
|
-
|
|
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
|
-
|
|
33708
|
-
|
|
33709
|
-
|
|
33710
|
-
|
|
33711
|
-
|
|
33712
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
// <
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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).
|