@wrongstack/cli 0.5.7 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1331 -355
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as
|
|
2
|
+
import * as path23 from 'path';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import * as fsp2 from 'fs/promises';
|
|
5
5
|
import { readdir, readFile } from 'fs/promises';
|
|
6
|
-
import { color,
|
|
6
|
+
import { color, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, SlashCommandRegistry, createDelegateTool, FLEET_ROSTER, createMcpControlTool, EternalAutonomyEngine, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeAgentSubagentRunner, NULL_FLEET_BUS, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, DefaultPluginAPI, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, formatPlanTemplates, getPlanTemplate, addPlanItem, formatPlan, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, loadGoal, goalFilePath, summarizeUsage, emptyGoal, saveGoal, buildGoalPreamble, formatGoal, InputBuilder, projectHash, defaultOrchestrator, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, allServers as allServers$1 } from '@wrongstack/core';
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import * as os6 from 'os';
|
|
9
9
|
import os6__default from 'os';
|
|
@@ -11,7 +11,6 @@ import * as crypto from 'crypto';
|
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
12
|
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
|
|
13
13
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
14
|
-
import { writeFileSync } from 'fs';
|
|
15
14
|
import { MCPRegistry } from '@wrongstack/mcp';
|
|
16
15
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
|
|
17
16
|
import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
|
|
@@ -19,7 +18,10 @@ import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
|
19
18
|
import * as readline from 'readline';
|
|
20
19
|
import { spawn } from 'child_process';
|
|
21
20
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
21
|
+
import { allServers } from '@wrongstack/core/infrastructure';
|
|
22
22
|
import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
|
|
23
|
+
import { ToolExecutor } from '@wrongstack/core/execution';
|
|
24
|
+
import { writeFileSync } from 'fs';
|
|
23
25
|
|
|
24
26
|
var __defProp = Object.defineProperty;
|
|
25
27
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -261,8 +263,8 @@ function buildSddCommand(opts) {
|
|
|
261
263
|
async run(args) {
|
|
262
264
|
const ctx = opts.context;
|
|
263
265
|
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
264
|
-
const specsDir =
|
|
265
|
-
const graphsDir =
|
|
266
|
+
const specsDir = path23.join(projectRoot, ".wrongstack", "specs");
|
|
267
|
+
const graphsDir = path23.join(projectRoot, ".wrongstack", "task-graphs");
|
|
266
268
|
const specStore = new SpecStore({ baseDir: specsDir });
|
|
267
269
|
new TaskGraphStore({ baseDir: graphsDir });
|
|
268
270
|
const versioning = new SpecVersioning();
|
|
@@ -278,7 +280,7 @@ function buildSddCommand(opts) {
|
|
|
278
280
|
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
279
281
|
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
280
282
|
if (!sddState.getBuilder() && !forceFlag) {
|
|
281
|
-
const sessionPath =
|
|
283
|
+
const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
282
284
|
try {
|
|
283
285
|
await fsp2.access(sessionPath);
|
|
284
286
|
const projectContext2 = await gatherProjectContext(projectRoot);
|
|
@@ -313,7 +315,7 @@ function buildSddCommand(opts) {
|
|
|
313
315
|
projectContext,
|
|
314
316
|
minQuestions: 2,
|
|
315
317
|
maxQuestions: 10,
|
|
316
|
-
sessionPath:
|
|
318
|
+
sessionPath: path23.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
317
319
|
}));
|
|
318
320
|
const builder = sddState.getBuilder();
|
|
319
321
|
builder.startSession(title);
|
|
@@ -581,7 +583,7 @@ Start executing the tasks one by one.`
|
|
|
581
583
|
};
|
|
582
584
|
}
|
|
583
585
|
case "cancel": {
|
|
584
|
-
const sessionPath =
|
|
586
|
+
const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
585
587
|
let deletedFromDisk = false;
|
|
586
588
|
try {
|
|
587
589
|
await fsp2.unlink(sessionPath);
|
|
@@ -605,7 +607,7 @@ Start executing the tasks one by one.`
|
|
|
605
607
|
if (sddState.getBuilder()) {
|
|
606
608
|
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
607
609
|
}
|
|
608
|
-
const sessionPath =
|
|
610
|
+
const sessionPath = path23.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
609
611
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
610
612
|
sddState.setBuilder(new AISpecBuilder({
|
|
611
613
|
store: specStore,
|
|
@@ -831,7 +833,7 @@ function sddHelp() {
|
|
|
831
833
|
async function gatherProjectContext(projectRoot) {
|
|
832
834
|
const parts = [];
|
|
833
835
|
try {
|
|
834
|
-
const pkgPath =
|
|
836
|
+
const pkgPath = path23.join(projectRoot, "package.json");
|
|
835
837
|
const pkgRaw = await fsp2.readFile(pkgPath, "utf8");
|
|
836
838
|
const pkg = JSON.parse(pkgRaw);
|
|
837
839
|
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
@@ -847,13 +849,13 @@ async function gatherProjectContext(projectRoot) {
|
|
|
847
849
|
} catch {
|
|
848
850
|
}
|
|
849
851
|
try {
|
|
850
|
-
const tsconfigPath =
|
|
852
|
+
const tsconfigPath = path23.join(projectRoot, "tsconfig.json");
|
|
851
853
|
await fsp2.access(tsconfigPath);
|
|
852
854
|
parts.push("Language: TypeScript");
|
|
853
855
|
} catch {
|
|
854
856
|
}
|
|
855
857
|
try {
|
|
856
|
-
const srcDir =
|
|
858
|
+
const srcDir = path23.join(projectRoot, "src");
|
|
857
859
|
const entries = await fsp2.readdir(srcDir, { withFileTypes: true });
|
|
858
860
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
859
861
|
if (dirs.length > 0) {
|
|
@@ -976,7 +978,7 @@ __export(update_check_exports, {
|
|
|
976
978
|
getUpdateNotification: () => getUpdateNotification
|
|
977
979
|
});
|
|
978
980
|
function cachePath(homeFn = defaultHomeDir2) {
|
|
979
|
-
return
|
|
981
|
+
return path23.join(homeFn(), ".wrongstack", "update-cache.json");
|
|
980
982
|
}
|
|
981
983
|
function currentVersion() {
|
|
982
984
|
const req2 = createRequire(import.meta.url);
|
|
@@ -1013,7 +1015,7 @@ async function readCache(homeFn = defaultHomeDir2) {
|
|
|
1013
1015
|
}
|
|
1014
1016
|
async function writeCache(entry, homeFn = defaultHomeDir2) {
|
|
1015
1017
|
try {
|
|
1016
|
-
const dir =
|
|
1018
|
+
const dir = path23.dirname(cachePath(homeFn));
|
|
1017
1019
|
await fsp2.mkdir(dir, { recursive: true });
|
|
1018
1020
|
await fsp2.writeFile(cachePath(homeFn), JSON.stringify(entry, null, 2), "utf8");
|
|
1019
1021
|
} catch {
|
|
@@ -1102,7 +1104,7 @@ async function runWebUI(opts) {
|
|
|
1102
1104
|
let abortController = null;
|
|
1103
1105
|
const authToken = crypto.randomBytes(16).toString("hex");
|
|
1104
1106
|
const wss = new WebSocketServer({ port, host: "127.0.0.1", maxPayload: 1 * 1024 * 1024 });
|
|
1105
|
-
console.log(`[WebUI] WebSocket server starting on ws://
|
|
1107
|
+
console.log(`[WebUI] WebSocket server starting on ws://127.0.0.1:${port}`);
|
|
1106
1108
|
const eventUnsubscribers = [];
|
|
1107
1109
|
function setupEvents() {
|
|
1108
1110
|
for (const unsub of eventUnsubscribers) unsub();
|
|
@@ -1196,10 +1198,29 @@ async function runWebUI(opts) {
|
|
|
1196
1198
|
});
|
|
1197
1199
|
})
|
|
1198
1200
|
);
|
|
1201
|
+
if (opts.subscribeEternalIteration) {
|
|
1202
|
+
eventUnsubscribers.push(
|
|
1203
|
+
opts.subscribeEternalIteration((entry) => {
|
|
1204
|
+
broadcast({
|
|
1205
|
+
type: "eternal.iteration",
|
|
1206
|
+
payload: {
|
|
1207
|
+
iteration: entry.iteration,
|
|
1208
|
+
at: entry.at,
|
|
1209
|
+
source: entry.source,
|
|
1210
|
+
task: entry.task,
|
|
1211
|
+
status: entry.status,
|
|
1212
|
+
note: entry.note,
|
|
1213
|
+
tokens: entry.tokens,
|
|
1214
|
+
costUsd: entry.costUsd
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
})
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1199
1220
|
}
|
|
1200
1221
|
return new Promise((resolve4) => {
|
|
1201
1222
|
wss.on("listening", () => {
|
|
1202
|
-
console.log(`[WebUI] WebSocket server running on ws://
|
|
1223
|
+
console.log(`[WebUI] WebSocket server running on ws://127.0.0.1:${port}`);
|
|
1203
1224
|
setupEvents();
|
|
1204
1225
|
});
|
|
1205
1226
|
wss.on("connection", (ws, req2) => {
|
|
@@ -1584,26 +1605,40 @@ async function runWebUI(opts) {
|
|
|
1584
1605
|
return {};
|
|
1585
1606
|
}
|
|
1586
1607
|
if (!parsed.providers) return {};
|
|
1587
|
-
const keyFile =
|
|
1608
|
+
const keyFile = path23.join(path23.dirname(opts.globalConfigPath), ".key");
|
|
1588
1609
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1589
1610
|
return decryptConfigSecrets$1(parsed.providers, vault);
|
|
1590
1611
|
}
|
|
1591
1612
|
async function saveProviders(providers) {
|
|
1592
1613
|
if (!opts.globalConfigPath) return;
|
|
1593
1614
|
let raw;
|
|
1615
|
+
let fileExists = true;
|
|
1594
1616
|
try {
|
|
1595
1617
|
raw = await fsp2.readFile(opts.globalConfigPath, "utf8");
|
|
1596
|
-
} catch {
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
if (err.code !== "ENOENT") {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`Refusing to mutate ${opts.globalConfigPath}: ${err.message}`,
|
|
1622
|
+
{ cause: err }
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
fileExists = false;
|
|
1597
1626
|
raw = "{}";
|
|
1598
1627
|
}
|
|
1599
1628
|
let parsed;
|
|
1600
1629
|
try {
|
|
1601
1630
|
parsed = JSON.parse(raw);
|
|
1602
|
-
} catch {
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
if (fileExists) {
|
|
1633
|
+
throw new Error(
|
|
1634
|
+
`Refusing to overwrite corrupt config at ${opts.globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
|
|
1635
|
+
{ cause: err }
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1603
1638
|
parsed = {};
|
|
1604
1639
|
}
|
|
1605
1640
|
parsed.providers = providers;
|
|
1606
|
-
const keyFile =
|
|
1641
|
+
const keyFile = path23.join(path23.dirname(opts.globalConfigPath), ".key");
|
|
1607
1642
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1608
1643
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1609
1644
|
await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
@@ -1618,19 +1653,6 @@ var init_webui_server = __esm({
|
|
|
1618
1653
|
}
|
|
1619
1654
|
});
|
|
1620
1655
|
|
|
1621
|
-
// src/plugin-api-factory.ts
|
|
1622
|
-
var plugin_api_factory_exports = {};
|
|
1623
|
-
__export(plugin_api_factory_exports, {
|
|
1624
|
-
default: () => createApi
|
|
1625
|
-
});
|
|
1626
|
-
function createApi(ownerName, base) {
|
|
1627
|
-
return new DefaultPluginAPI({ ownerName, ...base });
|
|
1628
|
-
}
|
|
1629
|
-
var init_plugin_api_factory = __esm({
|
|
1630
|
-
"src/plugin-api-factory.ts"() {
|
|
1631
|
-
}
|
|
1632
|
-
});
|
|
1633
|
-
|
|
1634
1656
|
// src/slash-commands/commit-llm.ts
|
|
1635
1657
|
async function generateCommitMessageWithLLM(diff, opts) {
|
|
1636
1658
|
const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
|
|
@@ -1788,7 +1810,7 @@ function parseSpawnFlags(input) {
|
|
|
1788
1810
|
return { description: rest.trim(), opts };
|
|
1789
1811
|
}
|
|
1790
1812
|
async function bootConfig(flags) {
|
|
1791
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
1813
|
+
const cwd = typeof flags["cwd"] === "string" ? path23.resolve(flags["cwd"]) : process.cwd();
|
|
1792
1814
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
1793
1815
|
const projectRoot = pathResolver.projectRoot;
|
|
1794
1816
|
const userHome = os6.homedir();
|
|
@@ -1855,7 +1877,7 @@ var ReadlineInputReader = class {
|
|
|
1855
1877
|
history = [];
|
|
1856
1878
|
pending = false;
|
|
1857
1879
|
constructor(opts = {}) {
|
|
1858
|
-
this.historyFile = opts.historyFile ??
|
|
1880
|
+
this.historyFile = opts.historyFile ?? path23.join(os6.homedir(), ".wrongstack", "history");
|
|
1859
1881
|
}
|
|
1860
1882
|
async loadHistory() {
|
|
1861
1883
|
try {
|
|
@@ -1867,7 +1889,7 @@ var ReadlineInputReader = class {
|
|
|
1867
1889
|
}
|
|
1868
1890
|
async saveHistory() {
|
|
1869
1891
|
try {
|
|
1870
|
-
await fsp2.mkdir(
|
|
1892
|
+
await fsp2.mkdir(path23.dirname(this.historyFile), { recursive: true });
|
|
1871
1893
|
await fsp2.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
1872
1894
|
} catch {
|
|
1873
1895
|
}
|
|
@@ -2094,20 +2116,20 @@ function assertSafeToDelete(filename, parentDir) {
|
|
|
2094
2116
|
if (PROTECTED_BASENAMES.has(filename)) {
|
|
2095
2117
|
throw new Error(`Refusing to delete protected file: ${filename}`);
|
|
2096
2118
|
}
|
|
2097
|
-
if (filename !==
|
|
2119
|
+
if (filename !== path23.basename(filename)) {
|
|
2098
2120
|
throw new Error(`Refusing to delete path with traversal: ${filename}`);
|
|
2099
2121
|
}
|
|
2100
2122
|
if (!filename.startsWith("config.json.") || !filename.endsWith(".bak")) {
|
|
2101
2123
|
throw new Error(`Refusing to delete unknown file: ${filename}`);
|
|
2102
2124
|
}
|
|
2103
|
-
const resolvedParent =
|
|
2125
|
+
const resolvedParent = path23.resolve(parentDir);
|
|
2104
2126
|
if (!resolvedParent.endsWith(".wrongstack")) {
|
|
2105
2127
|
throw new Error(`Unexpected parent directory for bak prune: ${resolvedParent}`);
|
|
2106
2128
|
}
|
|
2107
2129
|
}
|
|
2108
2130
|
async function safeDelete(filePath) {
|
|
2109
|
-
const dir =
|
|
2110
|
-
const filename =
|
|
2131
|
+
const dir = path23.dirname(filePath);
|
|
2132
|
+
const filename = path23.basename(filePath);
|
|
2111
2133
|
try {
|
|
2112
2134
|
assertSafeToDelete(filename, dir);
|
|
2113
2135
|
await fsp2.unlink(filePath);
|
|
@@ -2152,16 +2174,16 @@ function diffSummary(oldCfg, newCfg) {
|
|
|
2152
2174
|
}
|
|
2153
2175
|
var defaultHomeDir = () => os6__default.homedir();
|
|
2154
2176
|
function historyDir(homeFn = defaultHomeDir) {
|
|
2155
|
-
return
|
|
2177
|
+
return path23.join(homeFn(), ".wrongstack", "config.history", "entries");
|
|
2156
2178
|
}
|
|
2157
2179
|
function historyIndexPath(homeFn = defaultHomeDir) {
|
|
2158
|
-
return
|
|
2180
|
+
return path23.join(homeFn(), ".wrongstack", "config.history", "index.json");
|
|
2159
2181
|
}
|
|
2160
2182
|
function configPath(homeFn = defaultHomeDir) {
|
|
2161
|
-
return
|
|
2183
|
+
return path23.join(homeFn(), ".wrongstack", "config.json");
|
|
2162
2184
|
}
|
|
2163
2185
|
function backupLastPath(homeFn = defaultHomeDir) {
|
|
2164
|
-
return
|
|
2186
|
+
return path23.join(homeFn(), ".wrongstack", "config.json.last");
|
|
2165
2187
|
}
|
|
2166
2188
|
function entryId(ts) {
|
|
2167
2189
|
return ts.replace(/[:.]/g, "-").slice(0, 19);
|
|
@@ -2179,7 +2201,7 @@ async function readIndex(homeFn = defaultHomeDir) {
|
|
|
2179
2201
|
}
|
|
2180
2202
|
async function writeIndex(idx, homeFn = defaultHomeDir) {
|
|
2181
2203
|
await ensureHistoryDir(homeFn);
|
|
2182
|
-
await
|
|
2204
|
+
await atomicWrite(historyIndexPath(homeFn), JSON.stringify(idx, null, 2));
|
|
2183
2205
|
}
|
|
2184
2206
|
async function backupCurrent(homeFn = defaultHomeDir) {
|
|
2185
2207
|
const cfg = configPath(homeFn);
|
|
@@ -2198,17 +2220,17 @@ async function backupCurrent(homeFn = defaultHomeDir) {
|
|
|
2198
2220
|
}
|
|
2199
2221
|
if (content !== void 0) {
|
|
2200
2222
|
try {
|
|
2201
|
-
const bakPath =
|
|
2223
|
+
const bakPath = path23.join(homeFn(), ".wrongstack", `config.json.${ts}.bak`);
|
|
2202
2224
|
await atomicWrite(bakPath, content);
|
|
2203
2225
|
} catch {
|
|
2204
2226
|
}
|
|
2205
2227
|
}
|
|
2206
2228
|
try {
|
|
2207
|
-
const dir =
|
|
2229
|
+
const dir = path23.join(homeFn(), ".wrongstack");
|
|
2208
2230
|
const files = await fsp2.readdir(dir);
|
|
2209
2231
|
const baks = files.filter((f) => f.startsWith("config.json.") && f.endsWith(".bak")).sort().reverse();
|
|
2210
2232
|
for (const f of baks.slice(10)) {
|
|
2211
|
-
await safeDelete(
|
|
2233
|
+
await safeDelete(path23.join(dir, f));
|
|
2212
2234
|
}
|
|
2213
2235
|
} catch {
|
|
2214
2236
|
}
|
|
@@ -2225,7 +2247,7 @@ async function appendHistory(oldCfg, newCfg, description, homeFn = defaultHomeDi
|
|
|
2225
2247
|
diffSummary: diffSummary(oldCfg, newCfg)
|
|
2226
2248
|
};
|
|
2227
2249
|
await fsp2.writeFile(
|
|
2228
|
-
|
|
2250
|
+
path23.join(historyDir(homeFn), `${id}.json`),
|
|
2229
2251
|
JSON.stringify(entry, null, 2),
|
|
2230
2252
|
"utf8"
|
|
2231
2253
|
);
|
|
@@ -2240,7 +2262,7 @@ async function listHistory(homeFn = defaultHomeDir) {
|
|
|
2240
2262
|
}
|
|
2241
2263
|
async function getHistoryEntry(id, homeFn = defaultHomeDir) {
|
|
2242
2264
|
try {
|
|
2243
|
-
const raw = await fsp2.readFile(
|
|
2265
|
+
const raw = await fsp2.readFile(path23.join(historyDir(homeFn), `${id}.json`), "utf8");
|
|
2244
2266
|
return JSON.parse(raw);
|
|
2245
2267
|
} catch {
|
|
2246
2268
|
return null;
|
|
@@ -2299,11 +2321,11 @@ async function restoreLast(homeFn = defaultHomeDir) {
|
|
|
2299
2321
|
var theme = { primary: color.amber };
|
|
2300
2322
|
async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? __require("os").homedir()) {
|
|
2301
2323
|
try {
|
|
2302
|
-
const { atomicWrite:
|
|
2303
|
-
const
|
|
2324
|
+
const { atomicWrite: atomicWrite8 } = await import('@wrongstack/core');
|
|
2325
|
+
const fs20 = await import('fs/promises');
|
|
2304
2326
|
let existing = {};
|
|
2305
2327
|
try {
|
|
2306
|
-
const raw = await
|
|
2328
|
+
const raw = await fs20.readFile(configPath2, "utf8");
|
|
2307
2329
|
existing = JSON.parse(raw);
|
|
2308
2330
|
} catch {
|
|
2309
2331
|
}
|
|
@@ -2311,7 +2333,7 @@ async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => p
|
|
|
2311
2333
|
existing.provider = provider;
|
|
2312
2334
|
existing.model = model;
|
|
2313
2335
|
await backupCurrent(homeFn);
|
|
2314
|
-
await
|
|
2336
|
+
await atomicWrite8(configPath2, JSON.stringify(existing, null, 2));
|
|
2315
2337
|
try {
|
|
2316
2338
|
await appendHistory(
|
|
2317
2339
|
oldCfg,
|
|
@@ -2440,8 +2462,12 @@ ${color.bold(theme.primary("WrongStack") + color.dim(" \u2014 Provider & Model S
|
|
|
2440
2462
|
const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color.dim(`[Enter = ${defaultProvider}]`)}` : "";
|
|
2441
2463
|
const providerAnswer = (await reader.readLine(
|
|
2442
2464
|
`
|
|
2443
|
-
${color.amber("?")} Select provider (1-${ordered.length})${defaultHint}: `
|
|
2465
|
+
${color.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color.dim("[q to quit]")}: `
|
|
2444
2466
|
)).trim();
|
|
2467
|
+
if (providerAnswer.toLowerCase() === "q") {
|
|
2468
|
+
renderer.write(color.dim("Cancelled.\n"));
|
|
2469
|
+
return void 0;
|
|
2470
|
+
}
|
|
2445
2471
|
if (!providerAnswer) {
|
|
2446
2472
|
if (defaultIdx !== void 0) {
|
|
2447
2473
|
const def = ordered[defaultIdx - 1];
|
|
@@ -2503,15 +2529,23 @@ async function pickModel(provider, registry, renderer, reader, defaultModel) {
|
|
|
2503
2529
|
if (offset < models.length) {
|
|
2504
2530
|
const more = (await reader.readLine(
|
|
2505
2531
|
`
|
|
2506
|
-
${color.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number
|
|
2532
|
+
${color.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number, ${color.dim("Enter")} for more, or ${color.dim("q")} to quit: `
|
|
2507
2533
|
)).trim();
|
|
2534
|
+
if (more.toLowerCase() === "q") {
|
|
2535
|
+
renderer.write(color.dim("Cancelled.\n"));
|
|
2536
|
+
return void 0;
|
|
2537
|
+
}
|
|
2508
2538
|
if (!more) continue;
|
|
2509
2539
|
return resolveModelSelection(more, models, provider, registry, renderer);
|
|
2510
2540
|
}
|
|
2511
2541
|
}
|
|
2512
2542
|
const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color.dim(`[Enter = ${defaultModel}]`)}` : "";
|
|
2513
2543
|
const answer = (await reader.readLine(`
|
|
2514
|
-
${color.amber("?")} Select model (1-${models.length})${defaultHint}: `)).trim();
|
|
2544
|
+
${color.amber("?")} Select model (1-${models.length})${defaultHint} ${color.dim("[q to quit]")}: `)).trim();
|
|
2545
|
+
if (answer.toLowerCase() === "q") {
|
|
2546
|
+
renderer.write(color.dim("Cancelled.\n"));
|
|
2547
|
+
return void 0;
|
|
2548
|
+
}
|
|
2515
2549
|
if (!answer) {
|
|
2516
2550
|
if (defaultIdxInModels >= 0 && defaultModel) {
|
|
2517
2551
|
renderer.write(
|
|
@@ -2569,10 +2603,10 @@ async function detectPackageManager(root, declared) {
|
|
|
2569
2603
|
const name = declared.split("@")[0];
|
|
2570
2604
|
if (name) return name;
|
|
2571
2605
|
}
|
|
2572
|
-
if (await pathExists(
|
|
2573
|
-
if (await pathExists(
|
|
2574
|
-
if (await pathExists(
|
|
2575
|
-
if (await pathExists(
|
|
2606
|
+
if (await pathExists(path23.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2607
|
+
if (await pathExists(path23.join(root, "bun.lockb"))) return "bun";
|
|
2608
|
+
if (await pathExists(path23.join(root, "bun.lock"))) return "bun";
|
|
2609
|
+
if (await pathExists(path23.join(root, "yarn.lock"))) return "yarn";
|
|
2576
2610
|
return "npm";
|
|
2577
2611
|
}
|
|
2578
2612
|
function hasUsableScript(scripts, name) {
|
|
@@ -2593,7 +2627,7 @@ function parseMakeTargets(makefile) {
|
|
|
2593
2627
|
async function detectProjectFacts(root) {
|
|
2594
2628
|
const facts = { hints: [] };
|
|
2595
2629
|
try {
|
|
2596
|
-
const pkg = JSON.parse(await fsp2.readFile(
|
|
2630
|
+
const pkg = JSON.parse(await fsp2.readFile(path23.join(root, "package.json"), "utf8"));
|
|
2597
2631
|
const scripts = pkg.scripts ?? {};
|
|
2598
2632
|
const pm = await detectPackageManager(root, pkg.packageManager);
|
|
2599
2633
|
if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
|
|
@@ -2607,14 +2641,14 @@ async function detectProjectFacts(root) {
|
|
|
2607
2641
|
} catch {
|
|
2608
2642
|
}
|
|
2609
2643
|
try {
|
|
2610
|
-
if (!await pathExists(
|
|
2644
|
+
if (!await pathExists(path23.join(root, "pyproject.toml"))) throw new Error("not python");
|
|
2611
2645
|
facts.test ??= "pytest";
|
|
2612
2646
|
facts.lint ??= "ruff check .";
|
|
2613
2647
|
facts.hints.push("pyproject.toml");
|
|
2614
2648
|
} catch {
|
|
2615
2649
|
}
|
|
2616
2650
|
try {
|
|
2617
|
-
if (!await pathExists(
|
|
2651
|
+
if (!await pathExists(path23.join(root, "go.mod"))) throw new Error("not go");
|
|
2618
2652
|
facts.build ??= "go build ./...";
|
|
2619
2653
|
facts.test ??= "go test ./...";
|
|
2620
2654
|
facts.run ??= "go run .";
|
|
@@ -2622,7 +2656,7 @@ async function detectProjectFacts(root) {
|
|
|
2622
2656
|
} catch {
|
|
2623
2657
|
}
|
|
2624
2658
|
try {
|
|
2625
|
-
if (!await pathExists(
|
|
2659
|
+
if (!await pathExists(path23.join(root, "Cargo.toml"))) throw new Error("not rust");
|
|
2626
2660
|
facts.build ??= "cargo build";
|
|
2627
2661
|
facts.test ??= "cargo test";
|
|
2628
2662
|
facts.lint ??= "cargo clippy";
|
|
@@ -2631,7 +2665,7 @@ async function detectProjectFacts(root) {
|
|
|
2631
2665
|
} catch {
|
|
2632
2666
|
}
|
|
2633
2667
|
try {
|
|
2634
|
-
const makefile = await fsp2.readFile(
|
|
2668
|
+
const makefile = await fsp2.readFile(path23.join(root, "Makefile"), "utf8");
|
|
2635
2669
|
const targets = parseMakeTargets(makefile);
|
|
2636
2670
|
facts.build ??= targets.has("build") ? "make build" : "make";
|
|
2637
2671
|
if (targets.has("test")) facts.test ??= "make test";
|
|
@@ -2645,52 +2679,79 @@ async function detectProjectFacts(root) {
|
|
|
2645
2679
|
}
|
|
2646
2680
|
function renderAgentsTemplate(f) {
|
|
2647
2681
|
const cmd = (s) => s ? `\`${s}\`` : "_TODO_";
|
|
2682
|
+
const hints = f.hints.length > 0 ? `
|
|
2683
|
+
|
|
2684
|
+
> Auto-detected: ${f.hints.join(", ")}` : "";
|
|
2648
2685
|
return `# AGENTS.md
|
|
2649
2686
|
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2687
|
+
> **DO NOT DELETE THIS FILE.** It is loaded into WrongStack's system prompt as
|
|
2688
|
+
> persistent project context. Previous content here may contain decisions,
|
|
2689
|
+
> architecture notes, domain knowledge, or verification history that should be
|
|
2690
|
+
> preserved. Merge additions rather than replacing.
|
|
2653
2691
|
|
|
2654
2692
|
## Project brief
|
|
2655
2693
|
|
|
2656
|
-
- **Purpose:** _What does this project do
|
|
2694
|
+
- **Purpose:** _What does this project do and why does it exist?_
|
|
2657
2695
|
- **Primary users:** _Who uses it: developers, operators, customers, internal systems?_
|
|
2658
|
-
- **Runtime/deployment:**
|
|
2659
|
-
- **Main entry points:** _Which files or commands should an agent inspect first?_
|
|
2696
|
+
- **Runtime / deployment:** _CLI, server, browser, worker, library, package?_${hints}
|
|
2660
2697
|
|
|
2661
2698
|
## How to work safely
|
|
2662
2699
|
|
|
2663
2700
|
- _Project-specific rules the agent should always follow._
|
|
2664
2701
|
- _Files, generated artifacts, migrations, or config the agent should not edit without asking._
|
|
2665
|
-
- _Preferred style or architecture choices
|
|
2702
|
+
- _Preferred style or architecture choices not obvious from the code._
|
|
2703
|
+
- _Known fragile areas or historical bugs that deserve extra caution._
|
|
2666
2704
|
|
|
2667
2705
|
## Commands
|
|
2668
2706
|
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2707
|
+
| Command | Script |
|
|
2708
|
+
|---------|--------|
|
|
2709
|
+
| Build | ${cmd(f.build)} |
|
|
2710
|
+
| Test | ${cmd(f.test)} |
|
|
2711
|
+
| Lint | ${cmd(f.lint)} |
|
|
2712
|
+
| Run locally | ${cmd(f.run)} |
|
|
2713
|
+
|
|
2714
|
+
## Key files and entry points
|
|
2715
|
+
|
|
2716
|
+
| File / directory | Role |
|
|
2717
|
+
|---|---|
|
|
2718
|
+
| _src/_ | _Main source entry point(s)_ |
|
|
2719
|
+
| _tests/_ | _Test root or convention_ |
|
|
2720
|
+
| _docs/_ | _Architecture, runbooks, design notes_ |
|
|
2721
|
+
| _scripts/_ | _Automation scripts (CI, release, install, etc.)_ |
|
|
2673
2722
|
|
|
2674
2723
|
## Architecture notes
|
|
2675
2724
|
|
|
2676
2725
|
_Summarize the important modules, data flow, boundaries, and ownership rules.
|
|
2677
|
-
Mention anything a newcomer might misread._
|
|
2726
|
+
Mention anything a newcomer might misread or that looks unusual but is intentional._
|
|
2727
|
+
|
|
2728
|
+
### Dependency layers
|
|
2729
|
+
|
|
2730
|
+
_Describe the key dependency direction or layered structure, e.g.: "core has no
|
|
2731
|
+
runtime deps; cli assembles everything above it."_
|
|
2732
|
+
|
|
2733
|
+
### Extension points
|
|
2734
|
+
|
|
2735
|
+
_Plugin, MCP, extension hooks, custom tools \u2014 what's wired up and how._
|
|
2678
2736
|
|
|
2679
2737
|
## Domain knowledge
|
|
2680
2738
|
|
|
2681
2739
|
_Business rules, acronyms, invariants, external services, and notes where the
|
|
2682
|
-
code looks unusual but is intentional.
|
|
2740
|
+
code looks unusual but is intentional. E.g.: "IDs are ULIDs, not UUIDs", "the
|
|
2741
|
+
\`draft\` flag means uncommitted billing metadata", "MCP servers are restarted
|
|
2742
|
+
on disconnect with exponential backoff, up to 3 attempts"._
|
|
2683
2743
|
|
|
2684
2744
|
## Verification checklist
|
|
2685
2745
|
|
|
2686
2746
|
- _What should be run after code changes?_
|
|
2687
2747
|
- _What manual smoke test proves the common path still works?_
|
|
2688
2748
|
- _What failure modes deserve extra attention?_
|
|
2749
|
+
- _Any known flaky tests or environment-dependent behavior?_
|
|
2689
2750
|
|
|
2690
2751
|
## Useful pointers
|
|
2691
2752
|
|
|
2692
|
-
- _Docs, dashboards, runbooks, issue trackers, design notes,
|
|
2693
|
-
`;
|
|
2753
|
+
- _Docs, dashboards, runbooks, issue trackers, design notes, owner contacts._
|
|
2754
|
+
- _Related projects or repositories._`;
|
|
2694
2755
|
}
|
|
2695
2756
|
function countTurnPairs(messages) {
|
|
2696
2757
|
let count = 0;
|
|
@@ -3094,7 +3155,7 @@ function buildStatsCommand(opts) {
|
|
|
3094
3155
|
function buildFleetCommand(opts) {
|
|
3095
3156
|
return {
|
|
3096
3157
|
name: "fleet",
|
|
3097
|
-
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|retry [taskId]|log <id>|stream on|off|help]",
|
|
3158
|
+
description: "Inspect or control the subagent fleet: /fleet [status|usage|kill <id>|manifest|concurrency [N]|retry [taskId]|log <id>|stream on|off|help]",
|
|
3098
3159
|
help: [
|
|
3099
3160
|
"Usage:",
|
|
3100
3161
|
" /fleet Show fleet status (alias for /fleet status).",
|
|
@@ -3102,6 +3163,8 @@ function buildFleetCommand(opts) {
|
|
|
3102
3163
|
" /fleet usage Per-subagent runtime cost.",
|
|
3103
3164
|
" /fleet kill <id> Terminate a running subagent.",
|
|
3104
3165
|
" /fleet manifest Print the director manifest.",
|
|
3166
|
+
" /fleet concurrency Show the current concurrent-subagent ceiling.",
|
|
3167
|
+
" /fleet concurrency N Set the ceiling to N (>= 1). Lowering does not preempt running tasks.",
|
|
3105
3168
|
" /fleet retry List interrupted tasks from the last run.",
|
|
3106
3169
|
" /fleet retry <taskId> Re-spawn the matching subagent and re-assign the task.",
|
|
3107
3170
|
" /fleet retry all Re-assign every interrupted task at once.",
|
|
@@ -3127,6 +3190,9 @@ function buildFleetCommand(opts) {
|
|
|
3127
3190
|
if (!target) return { message: "Usage: /fleet kill <subagent-id>" };
|
|
3128
3191
|
return { message: await opts.onFleet("kill", target) };
|
|
3129
3192
|
}
|
|
3193
|
+
case "concurrency": {
|
|
3194
|
+
return { message: await opts.onFleet("concurrency", target) };
|
|
3195
|
+
}
|
|
3130
3196
|
case "retry": {
|
|
3131
3197
|
if (!opts.onFleetRetry) {
|
|
3132
3198
|
return { message: "Retry is only available when director mode is active." };
|
|
@@ -3264,20 +3330,10 @@ function buildHelpCommand(opts) {
|
|
|
3264
3330
|
function buildInitCommand(opts) {
|
|
3265
3331
|
return {
|
|
3266
3332
|
name: "init",
|
|
3267
|
-
description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
|
|
3268
|
-
async run(
|
|
3269
|
-
const
|
|
3270
|
-
const
|
|
3271
|
-
const file = path21.join(dir, "AGENTS.md");
|
|
3272
|
-
try {
|
|
3273
|
-
await fsp2.access(file);
|
|
3274
|
-
if (!force) {
|
|
3275
|
-
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
3276
|
-
opts.renderer.writeWarning(msg2);
|
|
3277
|
-
return { message: msg2 };
|
|
3278
|
-
}
|
|
3279
|
-
} catch {
|
|
3280
|
-
}
|
|
3333
|
+
description: "Create or update .wrongstack/AGENTS.md project context for the system prompt.",
|
|
3334
|
+
async run(_args, ctx) {
|
|
3335
|
+
const dir = path23.join(ctx.projectRoot, ".wrongstack");
|
|
3336
|
+
const file = path23.join(dir, "AGENTS.md");
|
|
3281
3337
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
3282
3338
|
const body = renderAgentsTemplate(detected);
|
|
3283
3339
|
await fsp2.mkdir(dir, { recursive: true });
|
|
@@ -3298,6 +3354,223 @@ No project type auto-detected. Edit the file with project context and instructio
|
|
|
3298
3354
|
}
|
|
3299
3355
|
};
|
|
3300
3356
|
}
|
|
3357
|
+
function parseMcpArgs(args) {
|
|
3358
|
+
const trimmed = args.trim();
|
|
3359
|
+
if (!trimmed || trimmed === "list") return { action: "list", name: "" };
|
|
3360
|
+
const parts = trimmed.split(/\s+/);
|
|
3361
|
+
const action = parts[0];
|
|
3362
|
+
const name = parts[1] ?? "";
|
|
3363
|
+
const enable = parts.includes("--enable") || parts.includes("-e");
|
|
3364
|
+
switch (action) {
|
|
3365
|
+
case "add":
|
|
3366
|
+
return name ? { action: "add", name, enable } : null;
|
|
3367
|
+
case "remove":
|
|
3368
|
+
return name ? { action: "remove", name } : null;
|
|
3369
|
+
case "enable":
|
|
3370
|
+
return name ? { action: "enable", name } : null;
|
|
3371
|
+
case "disable":
|
|
3372
|
+
return name ? { action: "disable", name } : null;
|
|
3373
|
+
case "restart":
|
|
3374
|
+
return name ? { action: "restart", name } : null;
|
|
3375
|
+
default:
|
|
3376
|
+
return null;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
async function runMcpManagementCommand(parsed, deps) {
|
|
3380
|
+
const { config, configPath: configPath2, mcpRegistry, allServerPresets } = deps;
|
|
3381
|
+
const configured = config.mcpServers ?? {};
|
|
3382
|
+
switch (parsed.action) {
|
|
3383
|
+
case "list":
|
|
3384
|
+
return renderList(configured, mcpRegistry, allServerPresets);
|
|
3385
|
+
case "add":
|
|
3386
|
+
return runAdd(parsed.name, parsed.enable ?? false, configured, configPath2, allServerPresets);
|
|
3387
|
+
case "remove":
|
|
3388
|
+
return runRemove(parsed.name, configured, configPath2, mcpRegistry);
|
|
3389
|
+
case "enable":
|
|
3390
|
+
return runEnable(parsed.name, configured, configPath2, mcpRegistry);
|
|
3391
|
+
case "disable":
|
|
3392
|
+
return runDisable(parsed.name, configured, configPath2, mcpRegistry);
|
|
3393
|
+
case "restart":
|
|
3394
|
+
return runRestart(parsed.name, mcpRegistry);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
function renderList(configured, mcpRegistry, all) {
|
|
3398
|
+
const lines = [];
|
|
3399
|
+
const liveStatus = mcpRegistry.list();
|
|
3400
|
+
const liveMap = new Map(liveStatus.map((s) => [s.name, s]));
|
|
3401
|
+
const configuredNames = new Set(Object.keys(configured));
|
|
3402
|
+
if (configuredNames.size > 0) {
|
|
3403
|
+
lines.push(color.bold("Configured servers:"));
|
|
3404
|
+
for (const [name, cfg] of Object.entries(configured)) {
|
|
3405
|
+
const live = liveMap.get(name);
|
|
3406
|
+
const toolCount = live ? color.dim(` (${live.toolCount} tools)`) : "";
|
|
3407
|
+
const enabled = cfg.enabled === false ? `${color.dim("disabled")} ` : `${color.green("\u25CF enabled")} `;
|
|
3408
|
+
const stateStr = live ? stateBadge(live.state) : color.dim("\u25CB not running");
|
|
3409
|
+
lines.push(` ${color.bold(name)} ${enabled}${stateStr}${toolCount}`);
|
|
3410
|
+
if (cfg.description) lines.push(` ${color.dim(cfg.description)}`);
|
|
3411
|
+
}
|
|
3412
|
+
lines.push("");
|
|
3413
|
+
}
|
|
3414
|
+
const unconfigured = Object.entries(all).filter(([n]) => !configuredNames.has(n));
|
|
3415
|
+
lines.push(color.bold("Available presets (run `/mcp add <name> --enable` to enable):"));
|
|
3416
|
+
if (unconfigured.length === 0) {
|
|
3417
|
+
lines.push(` ${color.dim("All presets are already configured.")}`);
|
|
3418
|
+
} else {
|
|
3419
|
+
for (const [name, cfg] of unconfigured) {
|
|
3420
|
+
const warn = cfg.permission === "deny" ? color.red(" \u26A0") : "";
|
|
3421
|
+
lines.push(` ${color.bold(name)} ${cfg.description ?? cfg.transport}${warn}`);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
lines.push("");
|
|
3425
|
+
lines.push(color.dim(" /mcp add <name> [--enable] /mcp remove <name>"));
|
|
3426
|
+
lines.push(color.dim(" /mcp enable <name> /mcp disable <name>"));
|
|
3427
|
+
lines.push(color.dim(" /mcp restart <name> (runtime restart)"));
|
|
3428
|
+
return lines.join("\n");
|
|
3429
|
+
}
|
|
3430
|
+
async function runAdd(name, enable, configured, configPath2, all) {
|
|
3431
|
+
const preset = all[name];
|
|
3432
|
+
if (!preset) {
|
|
3433
|
+
const known = Object.keys(all).join(", ");
|
|
3434
|
+
return `Unknown server "${name}". Available: ${known}`;
|
|
3435
|
+
}
|
|
3436
|
+
if (configured[name]) {
|
|
3437
|
+
const full2 = await readConfig(configPath2);
|
|
3438
|
+
full2.mcpServers = {
|
|
3439
|
+
...full2.mcpServers ?? {},
|
|
3440
|
+
[name]: { ...preset, ...configured[name], enabled: enable }
|
|
3441
|
+
};
|
|
3442
|
+
await writeConfig(configPath2, full2);
|
|
3443
|
+
return `${color.green("Updated")} "${name}" (${enable ? "enabled" : "disabled"}). Config written.`;
|
|
3444
|
+
}
|
|
3445
|
+
const full = await readConfig(configPath2);
|
|
3446
|
+
const mcpServers = { ...full.mcpServers ?? {}, [name]: { ...preset, enabled: enable } };
|
|
3447
|
+
full.mcpServers = mcpServers;
|
|
3448
|
+
await writeConfig(configPath2, full);
|
|
3449
|
+
const verb = enable ? "Enabled" : "Added (disabled \u2014 /mcp enable to start)";
|
|
3450
|
+
return `${color.green(verb)} "${name}" (${preset.transport}). Config written to ${configPath2}.`;
|
|
3451
|
+
}
|
|
3452
|
+
async function runRemove(name, configured, configPath2, mcpRegistry) {
|
|
3453
|
+
if (!configured[name]) return `Server "${name}" is not in config.`;
|
|
3454
|
+
await mcpRegistry.stop(name).catch(() => {
|
|
3455
|
+
});
|
|
3456
|
+
const full = await readConfig(configPath2);
|
|
3457
|
+
const mcpServers = { ...full.mcpServers ?? {} };
|
|
3458
|
+
delete mcpServers[name];
|
|
3459
|
+
full.mcpServers = mcpServers;
|
|
3460
|
+
await writeConfig(configPath2, full);
|
|
3461
|
+
return `${color.yellow("Removed")} "${name}" from config.`;
|
|
3462
|
+
}
|
|
3463
|
+
async function runEnable(name, configured, configPath2, mcpRegistry) {
|
|
3464
|
+
const cfg = configured[name];
|
|
3465
|
+
if (!cfg) return `Server "${name}" is not in config. Run \`/mcp add ${name} --enable\` first.`;
|
|
3466
|
+
if (cfg.enabled !== false) {
|
|
3467
|
+
try {
|
|
3468
|
+
await mcpRegistry.restart(name);
|
|
3469
|
+
return `${color.green("\u25CF")} "${name}" is already enabled and running.`;
|
|
3470
|
+
} catch {
|
|
3471
|
+
await mcpRegistry.start({ ...cfg, enabled: true });
|
|
3472
|
+
return `${color.green("Enabled")} "${name}" and started.`;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
const full = await readConfig(configPath2);
|
|
3476
|
+
const mcpServers = { ...full.mcpServers ?? {} };
|
|
3477
|
+
mcpServers[name] = { ...mcpServers[name], enabled: true };
|
|
3478
|
+
full.mcpServers = mcpServers;
|
|
3479
|
+
await writeConfig(configPath2, full);
|
|
3480
|
+
try {
|
|
3481
|
+
await mcpRegistry.restart(name);
|
|
3482
|
+
} catch {
|
|
3483
|
+
await mcpRegistry.start({ ...cfg, enabled: true });
|
|
3484
|
+
}
|
|
3485
|
+
return `${color.green("Enabled")} "${name}" and started.`;
|
|
3486
|
+
}
|
|
3487
|
+
async function runDisable(name, configured, configPath2, mcpRegistry) {
|
|
3488
|
+
const cfg = configured[name];
|
|
3489
|
+
if (!cfg) return `Server "${name}" is not in config.`;
|
|
3490
|
+
await mcpRegistry.stop(name).catch(() => {
|
|
3491
|
+
});
|
|
3492
|
+
const full = await readConfig(configPath2);
|
|
3493
|
+
const mcpServers = { ...full.mcpServers ?? {} };
|
|
3494
|
+
mcpServers[name] = { ...mcpServers[name], enabled: false };
|
|
3495
|
+
full.mcpServers = mcpServers;
|
|
3496
|
+
await writeConfig(configPath2, full);
|
|
3497
|
+
return `${color.yellow("Disabled")} "${name}" and stopped.`;
|
|
3498
|
+
}
|
|
3499
|
+
async function runRestart(name, mcpRegistry) {
|
|
3500
|
+
const live = mcpRegistry.list();
|
|
3501
|
+
if (!live.find((s) => s.name === name)) {
|
|
3502
|
+
return `Server "${name}" is not currently running. Add it with \`/mcp add ${name} --enable\`.`;
|
|
3503
|
+
}
|
|
3504
|
+
try {
|
|
3505
|
+
await mcpRegistry.restart(name);
|
|
3506
|
+
return `${color.green("\u2713")} Restarted "${name}".`;
|
|
3507
|
+
} catch (err) {
|
|
3508
|
+
return `${color.red("\u2717")} Failed to restart "${name}": ${err instanceof Error ? err.message : String(err)}`;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
function stateBadge(state) {
|
|
3512
|
+
switch (state) {
|
|
3513
|
+
case "connected":
|
|
3514
|
+
return color.green("\u25CF connected");
|
|
3515
|
+
case "connecting":
|
|
3516
|
+
return color.cyan("\u25D0 connecting");
|
|
3517
|
+
case "reconnecting":
|
|
3518
|
+
return color.cyan("\u25D1 reconnecting");
|
|
3519
|
+
case "disconnected":
|
|
3520
|
+
return color.dim("\u25CB disconnected");
|
|
3521
|
+
case "failed":
|
|
3522
|
+
return color.red("\u2717 failed");
|
|
3523
|
+
default:
|
|
3524
|
+
return color.dim(state);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
async function readConfig(path24) {
|
|
3528
|
+
try {
|
|
3529
|
+
return JSON.parse(await fsp2.readFile(path24, "utf8"));
|
|
3530
|
+
} catch {
|
|
3531
|
+
return {};
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
async function writeConfig(path24, cfg) {
|
|
3535
|
+
const raw = JSON.stringify(cfg, null, 2);
|
|
3536
|
+
const tmp = path24 + ".tmp";
|
|
3537
|
+
await fsp2.writeFile(tmp, raw, "utf8");
|
|
3538
|
+
await fsp2.rename(tmp, path24);
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// src/slash-commands/mcp.ts
|
|
3542
|
+
function buildMcpSlashCommand(opts) {
|
|
3543
|
+
return {
|
|
3544
|
+
name: "mcp",
|
|
3545
|
+
description: "Manage MCP servers: /mcp [list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
|
|
3546
|
+
aliases: ["mcp-servers"],
|
|
3547
|
+
argsHint: "[list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
|
|
3548
|
+
help: [
|
|
3549
|
+
"Usage:",
|
|
3550
|
+
" /mcp List available and configured servers.",
|
|
3551
|
+
" /mcp list Same.",
|
|
3552
|
+
" /mcp add <name> Add server preset to config (disabled).",
|
|
3553
|
+
" /mcp add <name> --enable Add and immediately enable.",
|
|
3554
|
+
" /mcp remove <name> Remove server from config.",
|
|
3555
|
+
" /mcp enable <name> Enable server in config + start it.",
|
|
3556
|
+
" /mcp disable <name> Disable server in config + stop it.",
|
|
3557
|
+
" /mcp restart <name> Stop and restart a running server (REPL only).",
|
|
3558
|
+
"",
|
|
3559
|
+
"Examples:",
|
|
3560
|
+
" /mcp",
|
|
3561
|
+
" /mcp add filesystem --enable",
|
|
3562
|
+
" /mcp enable github",
|
|
3563
|
+
" /mcp restart brave-search"
|
|
3564
|
+
].join("\n"),
|
|
3565
|
+
async run(args) {
|
|
3566
|
+
if (!opts.onMcp) {
|
|
3567
|
+
return { message: "MCP management is not available in this session." };
|
|
3568
|
+
}
|
|
3569
|
+
const result = await opts.onMcp(args.trim());
|
|
3570
|
+
return { message: result };
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3301
3574
|
|
|
3302
3575
|
// src/slash-commands/memory.ts
|
|
3303
3576
|
function buildMemoryCommand(opts) {
|
|
@@ -3769,21 +4042,24 @@ function buildAutonomyCommand(opts) {
|
|
|
3769
4042
|
description: "Toggle or query autonomy mode (self-driving agent).",
|
|
3770
4043
|
help: [
|
|
3771
4044
|
"Usage:",
|
|
3772
|
-
" /autonomy
|
|
3773
|
-
" /autonomy off
|
|
3774
|
-
" /autonomy suggest
|
|
3775
|
-
" /autonomy on
|
|
3776
|
-
" /autonomy
|
|
4045
|
+
" /autonomy Show current autonomy status",
|
|
4046
|
+
" /autonomy off Disabled \u2014 agent stops after each turn (default)",
|
|
4047
|
+
" /autonomy suggest Show next-step suggestions after each turn",
|
|
4048
|
+
" /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
|
|
4049
|
+
" /autonomy eternal Sittin-sene mode \u2014 runs forever against /goal",
|
|
4050
|
+
" /autonomy stop Stop eternal mode (no-op for other modes)",
|
|
4051
|
+
" /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 off",
|
|
3777
4052
|
"",
|
|
3778
4053
|
"Modes:",
|
|
3779
4054
|
" off \u2014 Normal interactive mode. Agent stops and waits.",
|
|
3780
4055
|
" suggest \u2014 After each turn, agent suggests next steps. You pick.",
|
|
3781
4056
|
" auto \u2014 After each turn, agent picks the best next step and continues.",
|
|
3782
4057
|
" Runs indefinitely until you press Esc or Ctrl+C.",
|
|
4058
|
+
" eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
|
|
4059
|
+
" Force-enables YOLO. Runs until /autonomy stop or Ctrl+C twice.",
|
|
3783
4060
|
"",
|
|
3784
|
-
"In auto
|
|
3785
|
-
"Ctrl+C to stop
|
|
3786
|
-
"the conversation history."
|
|
4061
|
+
"In auto/eternal modes the agent works autonomously. Press Esc to redirect,",
|
|
4062
|
+
"Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
|
|
3787
4063
|
].join("\n"),
|
|
3788
4064
|
async run(args) {
|
|
3789
4065
|
const arg = args.trim().toLowerCase();
|
|
@@ -3792,14 +4068,64 @@ function buildAutonomyCommand(opts) {
|
|
|
3792
4068
|
opts.renderer.writeWarning(msg2);
|
|
3793
4069
|
return { message: msg2 };
|
|
3794
4070
|
}
|
|
3795
|
-
if (!arg) {
|
|
4071
|
+
if (!arg || arg === "status") {
|
|
3796
4072
|
const current = opts.onAutonomy();
|
|
3797
4073
|
const labels2 = {
|
|
3798
4074
|
off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
|
|
3799
4075
|
suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
|
|
3800
|
-
auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}
|
|
4076
|
+
auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
|
|
4077
|
+
eternal: `${color.red("ETERNAL")} ${color.dim("(sittin-sene \u2014 goal-driven, YOLO, until /autonomy stop)")}`
|
|
3801
4078
|
};
|
|
3802
|
-
const
|
|
4079
|
+
const lines = [`Autonomy mode: ${labels2[current]}`];
|
|
4080
|
+
try {
|
|
4081
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
4082
|
+
if (goal) {
|
|
4083
|
+
const u = summarizeUsage(goal);
|
|
4084
|
+
lines.push(color.dim(` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`));
|
|
4085
|
+
lines.push(color.dim(` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`));
|
|
4086
|
+
if (u.iterationsWithUsage > 0) {
|
|
4087
|
+
lines.push(
|
|
4088
|
+
color.dim(
|
|
4089
|
+
` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
|
|
4090
|
+
)
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
const recent = goal.journal.slice(-10);
|
|
4094
|
+
const failed = recent.filter((e) => e.status === "failure").length;
|
|
4095
|
+
if (failed > 0) {
|
|
4096
|
+
lines.push(color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`));
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
} catch {
|
|
4100
|
+
}
|
|
4101
|
+
const msg2 = lines.join("\n");
|
|
4102
|
+
opts.renderer.write(msg2);
|
|
4103
|
+
return { message: msg2 };
|
|
4104
|
+
}
|
|
4105
|
+
if (arg === "stop" || arg === "halt" || arg === "kill") {
|
|
4106
|
+
if (!opts.onEternalStop) {
|
|
4107
|
+
const msg3 = "No eternal-mode controller wired in this session.";
|
|
4108
|
+
opts.renderer.writeWarning(msg3);
|
|
4109
|
+
return { message: msg3 };
|
|
4110
|
+
}
|
|
4111
|
+
opts.onEternalStop();
|
|
4112
|
+
opts.onAutonomy("off");
|
|
4113
|
+
let summaryLine = "";
|
|
4114
|
+
try {
|
|
4115
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
4116
|
+
if (goal) {
|
|
4117
|
+
const u = summarizeUsage(goal);
|
|
4118
|
+
if (u.iterationsWithUsage > 0) {
|
|
4119
|
+
summaryLine = "\n" + color.dim(
|
|
4120
|
+
` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
|
|
4121
|
+
);
|
|
4122
|
+
} else if (goal.iterations > 0) {
|
|
4123
|
+
summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
} catch {
|
|
4127
|
+
}
|
|
4128
|
+
const msg2 = `${color.amber("Eternal mode stop requested.")} The current iteration will finish, then the loop exits.${summaryLine}`;
|
|
3803
4129
|
opts.renderer.write(msg2);
|
|
3804
4130
|
return { message: msg2 };
|
|
3805
4131
|
}
|
|
@@ -3810,20 +4136,47 @@ function buildAutonomyCommand(opts) {
|
|
|
3810
4136
|
newMode = "off";
|
|
3811
4137
|
} else if (arg === "suggest" || arg === "suggestions") {
|
|
3812
4138
|
newMode = "suggest";
|
|
4139
|
+
} else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
|
|
4140
|
+
newMode = "eternal";
|
|
3813
4141
|
} else if (arg === "toggle" || arg === "cycle") {
|
|
3814
4142
|
const current = opts.onAutonomy() ?? "off";
|
|
3815
|
-
const cycle = ["off", "suggest", "auto"];
|
|
4143
|
+
const cycle = ["off", "suggest", "auto", "eternal"];
|
|
3816
4144
|
newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
|
|
3817
4145
|
} else {
|
|
3818
|
-
const msg2 = `Unknown argument: ${arg}. Use /autonomy on,
|
|
4146
|
+
const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, stop, or toggle.`;
|
|
3819
4147
|
opts.renderer.writeWarning(msg2);
|
|
3820
4148
|
return { message: msg2 };
|
|
3821
4149
|
}
|
|
4150
|
+
if (newMode === "eternal") {
|
|
4151
|
+
const goal = await loadGoal(goalFilePath(opts.projectRoot));
|
|
4152
|
+
if (!goal) {
|
|
4153
|
+
const msg3 = `${color.red("Eternal mode requires a goal.")} Run \`/goal set <mission>\` first.`;
|
|
4154
|
+
opts.renderer.writeWarning(msg3);
|
|
4155
|
+
return { message: msg3 };
|
|
4156
|
+
}
|
|
4157
|
+
if (!opts.onEternalStart) {
|
|
4158
|
+
const msg3 = "Eternal mode controller is not wired in this session.";
|
|
4159
|
+
opts.renderer.writeWarning(msg3);
|
|
4160
|
+
return { message: msg3 };
|
|
4161
|
+
}
|
|
4162
|
+
if (opts.onYolo) opts.onYolo(true);
|
|
4163
|
+
opts.onAutonomy(newMode);
|
|
4164
|
+
opts.onEternalStart();
|
|
4165
|
+
const msg2 = `${color.red("Autonomy mode: ETERNAL")} \u2014 engine launching against goal: ${color.bold(goal.goal)}
|
|
4166
|
+
${color.dim("YOLO forced ON. Use /autonomy stop to end. Journal at /goal journal.")}`;
|
|
4167
|
+
opts.renderer.write(msg2);
|
|
4168
|
+
return { message: msg2 };
|
|
4169
|
+
}
|
|
4170
|
+
const previous = opts.onAutonomy();
|
|
4171
|
+
if (previous === "eternal" && opts.onEternalStop) {
|
|
4172
|
+
opts.onEternalStop();
|
|
4173
|
+
}
|
|
3822
4174
|
opts.onAutonomy(newMode);
|
|
3823
4175
|
const labels = {
|
|
3824
4176
|
off: `${color.green("OFF")} \u2014 agent stops after each turn`,
|
|
3825
4177
|
suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
|
|
3826
|
-
auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically
|
|
4178
|
+
auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
|
|
4179
|
+
eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`
|
|
3827
4180
|
};
|
|
3828
4181
|
const msg = `Autonomy mode: ${labels[newMode]}`;
|
|
3829
4182
|
opts.renderer.write(msg);
|
|
@@ -3831,6 +4184,124 @@ function buildAutonomyCommand(opts) {
|
|
|
3831
4184
|
}
|
|
3832
4185
|
};
|
|
3833
4186
|
}
|
|
4187
|
+
var KNOWN_VERBS = /* @__PURE__ */ new Set([
|
|
4188
|
+
"",
|
|
4189
|
+
"show",
|
|
4190
|
+
"status",
|
|
4191
|
+
"set",
|
|
4192
|
+
"new",
|
|
4193
|
+
"clear",
|
|
4194
|
+
"reset",
|
|
4195
|
+
"journal",
|
|
4196
|
+
"log"
|
|
4197
|
+
]);
|
|
4198
|
+
function buildGoalCommand(opts) {
|
|
4199
|
+
return {
|
|
4200
|
+
name: "goal",
|
|
4201
|
+
description: "Set, inspect, or clear the long-running autonomous mission used by /autonomy eternal.",
|
|
4202
|
+
help: [
|
|
4203
|
+
"Usage:",
|
|
4204
|
+
" /goal Show current goal + recent journal",
|
|
4205
|
+
" /goal set <text> Set a new goal (overwrites previous)",
|
|
4206
|
+
" /goal clear Clear the goal (stops eternal mode if running)",
|
|
4207
|
+
" /goal status Same as /goal (alias)",
|
|
4208
|
+
" /goal journal [N] Show last N journal entries (default 25)",
|
|
4209
|
+
"",
|
|
4210
|
+
"Goals live in <projectRoot>/.wrongstack/goal.json and persist across sessions.",
|
|
4211
|
+
"A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
|
|
4212
|
+
"every iteration to decide what to do next."
|
|
4213
|
+
].join("\n"),
|
|
4214
|
+
async run(args) {
|
|
4215
|
+
const trimmed = args.trim();
|
|
4216
|
+
const [verbRaw, ...rest] = trimmed.split(/\s+/);
|
|
4217
|
+
const verb = (verbRaw ?? "").toLowerCase();
|
|
4218
|
+
const restJoined = rest.join(" ").trim();
|
|
4219
|
+
const goalPath = goalFilePath(opts.projectRoot);
|
|
4220
|
+
const verbForDispatch = verb && !KNOWN_VERBS.has(verb) ? "set" : verb;
|
|
4221
|
+
const setText = verbForDispatch === "set" && !KNOWN_VERBS.has(verb) ? trimmed : restJoined;
|
|
4222
|
+
switch (verbForDispatch) {
|
|
4223
|
+
case "":
|
|
4224
|
+
case "show":
|
|
4225
|
+
case "status": {
|
|
4226
|
+
const current = await loadGoal(goalPath);
|
|
4227
|
+
if (!current) {
|
|
4228
|
+
const msg2 = "No goal set. Use `/goal set <mission text>` to create one.";
|
|
4229
|
+
opts.renderer.write(msg2);
|
|
4230
|
+
return { message: msg2 };
|
|
4231
|
+
}
|
|
4232
|
+
const msg = formatGoal(current);
|
|
4233
|
+
opts.renderer.write(msg);
|
|
4234
|
+
return { message: msg };
|
|
4235
|
+
}
|
|
4236
|
+
case "set":
|
|
4237
|
+
case "new": {
|
|
4238
|
+
if (!setText) {
|
|
4239
|
+
const msg2 = "Usage: /goal set <mission text>";
|
|
4240
|
+
opts.renderer.writeWarning(msg2);
|
|
4241
|
+
return { message: msg2 };
|
|
4242
|
+
}
|
|
4243
|
+
const existing = await loadGoal(goalPath);
|
|
4244
|
+
const next = existing ? { ...existing, goal: setText, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal(setText);
|
|
4245
|
+
await saveGoal(goalPath, next);
|
|
4246
|
+
const shortGoal = setText.length > 80 ? `${setText.slice(0, 80)}\u2026` : setText;
|
|
4247
|
+
const msg = `\u{1F3AF} ${color.green("Goal locked:")} ${shortGoal}
|
|
4248
|
+
${color.dim(`Stored in ${goalPath} \u2014 Esc / /steer to redirect, Ctrl+C to stop.`)}`;
|
|
4249
|
+
opts.renderer.write(msg);
|
|
4250
|
+
return { message: msg, runText: buildGoalPreamble(setText) };
|
|
4251
|
+
}
|
|
4252
|
+
case "clear":
|
|
4253
|
+
case "reset": {
|
|
4254
|
+
const existing = await loadGoal(goalPath);
|
|
4255
|
+
if (!existing) {
|
|
4256
|
+
const msg2 = "No goal to clear.";
|
|
4257
|
+
opts.renderer.write(msg2);
|
|
4258
|
+
return { message: msg2 };
|
|
4259
|
+
}
|
|
4260
|
+
const { unlink: unlink4 } = await import('fs/promises');
|
|
4261
|
+
try {
|
|
4262
|
+
await unlink4(goalPath);
|
|
4263
|
+
} catch {
|
|
4264
|
+
}
|
|
4265
|
+
if (opts.onEternalStop) opts.onEternalStop();
|
|
4266
|
+
const msg = `${color.amber("Goal cleared.")} Eternal mode will stop on next cycle.`;
|
|
4267
|
+
opts.renderer.write(msg);
|
|
4268
|
+
return { message: msg };
|
|
4269
|
+
}
|
|
4270
|
+
case "journal":
|
|
4271
|
+
case "log": {
|
|
4272
|
+
const current = await loadGoal(goalPath);
|
|
4273
|
+
if (!current) {
|
|
4274
|
+
const msg2 = "No goal set.";
|
|
4275
|
+
opts.renderer.write(msg2);
|
|
4276
|
+
return { message: msg2 };
|
|
4277
|
+
}
|
|
4278
|
+
const n = restJoined ? Math.max(1, Number.parseInt(restJoined, 10) || 25) : 25;
|
|
4279
|
+
if (current.journal.length === 0) {
|
|
4280
|
+
const msg2 = "Journal is empty.";
|
|
4281
|
+
opts.renderer.write(msg2);
|
|
4282
|
+
return { message: msg2 };
|
|
4283
|
+
}
|
|
4284
|
+
const tail = current.journal.slice(-n);
|
|
4285
|
+
const lines = tail.map((e) => {
|
|
4286
|
+
const mark = e.status === "success" ? color.green("\u2713") : e.status === "failure" ? color.red("\u2717") : e.status === "aborted" ? color.amber("\u2298") : color.dim("\xB7");
|
|
4287
|
+
const note = e.note ? color.dim(` \u2014 ${e.note}`) : "";
|
|
4288
|
+
return `${color.dim(`#${e.iteration}`)} ${mark} ${color.dim(`[${e.source}]`)} ${e.task}${note}`;
|
|
4289
|
+
});
|
|
4290
|
+
const header = `Journal (last ${tail.length} of ${current.journal.length}):`;
|
|
4291
|
+
const msg = `${header}
|
|
4292
|
+
${lines.join("\n")}`;
|
|
4293
|
+
opts.renderer.write(msg);
|
|
4294
|
+
return { message: msg };
|
|
4295
|
+
}
|
|
4296
|
+
default: {
|
|
4297
|
+
const msg = `Unknown subcommand "${verb}". Try: show | set <text> | clear | journal [N]`;
|
|
4298
|
+
opts.renderer.writeWarning(msg);
|
|
4299
|
+
return { message: msg };
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
3834
4305
|
|
|
3835
4306
|
// src/slash-commands/mode.ts
|
|
3836
4307
|
function buildModeCommand(opts) {
|
|
@@ -3940,6 +4411,15 @@ ${lines.join("\n\n")}
|
|
|
3940
4411
|
}
|
|
3941
4412
|
};
|
|
3942
4413
|
}
|
|
4414
|
+
function getProviderFromContext(ctx, opts) {
|
|
4415
|
+
if (opts.llmProvider && typeof opts.llmProvider.complete === "function") {
|
|
4416
|
+
return { provider: opts.llmProvider, model: opts.llmModel };
|
|
4417
|
+
}
|
|
4418
|
+
if (ctx.provider && typeof ctx.provider.complete === "function") {
|
|
4419
|
+
return { provider: ctx.provider, model: ctx.model };
|
|
4420
|
+
}
|
|
4421
|
+
return null;
|
|
4422
|
+
}
|
|
3943
4423
|
function buildSecurityCommand(opts) {
|
|
3944
4424
|
return {
|
|
3945
4425
|
name: "security",
|
|
@@ -3991,11 +4471,11 @@ async function handleScan(args, ctx, opts) {
|
|
|
3991
4471
|
const options = parseArgs2(args);
|
|
3992
4472
|
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
3993
4473
|
try {
|
|
3994
|
-
const
|
|
3995
|
-
if (!
|
|
4474
|
+
const providerInfo = getProviderFromContext(ctx, opts);
|
|
4475
|
+
if (!providerInfo) {
|
|
3996
4476
|
return { message: "\u274C Security scan requires an active LLM provider. No provider configured." };
|
|
3997
4477
|
}
|
|
3998
|
-
const result = await defaultOrchestrator.run(
|
|
4478
|
+
const result = await defaultOrchestrator.run(providerInfo, {
|
|
3999
4479
|
projectRoot,
|
|
4000
4480
|
scanOptions: {
|
|
4001
4481
|
depth: options.depth || "standard",
|
|
@@ -4044,11 +4524,11 @@ async function handleScan(args, ctx, opts) {
|
|
|
4044
4524
|
async function handleAudit(ctx, opts) {
|
|
4045
4525
|
const projectRoot = ctx.projectRoot || opts.projectRoot;
|
|
4046
4526
|
try {
|
|
4047
|
-
const
|
|
4048
|
-
if (!
|
|
4527
|
+
const providerInfo = getProviderFromContext(ctx, opts);
|
|
4528
|
+
if (!providerInfo) {
|
|
4049
4529
|
return { message: "\u274C Security audit requires an active LLM provider. No provider configured." };
|
|
4050
4530
|
}
|
|
4051
|
-
const result = await defaultOrchestrator.run(
|
|
4531
|
+
const result = await defaultOrchestrator.run(providerInfo, {
|
|
4052
4532
|
projectRoot,
|
|
4053
4533
|
reportOptions: { format: "markdown" }
|
|
4054
4534
|
});
|
|
@@ -4159,12 +4639,111 @@ function getHelpMessage() {
|
|
|
4159
4639
|
|
|
4160
4640
|
Run \`/security scan\` to start.`;
|
|
4161
4641
|
}
|
|
4642
|
+
var CONFIG_ENV = "WRONGSTACK_STATUSLINE_CONFIG";
|
|
4643
|
+
var DEFAULTS = {
|
|
4644
|
+
todos: true,
|
|
4645
|
+
plan: true,
|
|
4646
|
+
fleet: true,
|
|
4647
|
+
git: true,
|
|
4648
|
+
elapsed: true,
|
|
4649
|
+
context: true,
|
|
4650
|
+
cost: true
|
|
4651
|
+
};
|
|
4652
|
+
function resolveConfigPath() {
|
|
4653
|
+
return process.env[CONFIG_ENV] ?? path23.join(process.env.HOME ?? "", ".wrongstack", "statusline.json");
|
|
4654
|
+
}
|
|
4655
|
+
async function loadStatuslineConfig() {
|
|
4656
|
+
const p = resolveConfigPath();
|
|
4657
|
+
try {
|
|
4658
|
+
const raw = await fsp2.readFile(p, "utf8");
|
|
4659
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
4660
|
+
} catch {
|
|
4661
|
+
return { ...DEFAULTS };
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
async function saveStatuslineConfig(cfg) {
|
|
4665
|
+
const p = resolveConfigPath();
|
|
4666
|
+
await fsp2.mkdir(path23.dirname(p), { recursive: true });
|
|
4667
|
+
await atomicWrite(p, JSON.stringify(cfg, null, 2));
|
|
4668
|
+
}
|
|
4669
|
+
function buildStatuslineCommand(deps) {
|
|
4670
|
+
return {
|
|
4671
|
+
name: "statusline",
|
|
4672
|
+
aliases: ["sl"],
|
|
4673
|
+
description: "Customize status bar chips: /statusline [item] [on|off] or /statusline reset",
|
|
4674
|
+
help: [
|
|
4675
|
+
"Usage: /statusline [item] [on|off]",
|
|
4676
|
+
" /statusline \u2014 show current config",
|
|
4677
|
+
" /statusline <item> on \u2014 enable a chip",
|
|
4678
|
+
" /statusline <item> off \u2014 disable a chip",
|
|
4679
|
+
" /statusline reset \u2014 restore defaults",
|
|
4680
|
+
"",
|
|
4681
|
+
"Available items: todos, plan, fleet, git, elapsed, context, cost",
|
|
4682
|
+
"Persistent across sessions (saved to ~/.wrongstack/statusline.json)."
|
|
4683
|
+
].join("\n"),
|
|
4684
|
+
async run(args) {
|
|
4685
|
+
const cfg = await deps.getConfig();
|
|
4686
|
+
const trimmed = args.trim();
|
|
4687
|
+
const parts = trimmed.split(/\s+/);
|
|
4688
|
+
const [item, action] = parts;
|
|
4689
|
+
if (!item) {
|
|
4690
|
+
const lines = ["StatusBar chips:"];
|
|
4691
|
+
const items = [
|
|
4692
|
+
"todos",
|
|
4693
|
+
"plan",
|
|
4694
|
+
"fleet",
|
|
4695
|
+
"git",
|
|
4696
|
+
"elapsed",
|
|
4697
|
+
"context",
|
|
4698
|
+
"cost"
|
|
4699
|
+
];
|
|
4700
|
+
for (const k of items) {
|
|
4701
|
+
const val = cfg[k];
|
|
4702
|
+
if (val === void 0) continue;
|
|
4703
|
+
lines.push(` ${val ? "\u25CF" : "\u25CB"} ${k}`);
|
|
4704
|
+
}
|
|
4705
|
+
return { message: lines.join("\n") };
|
|
4706
|
+
}
|
|
4707
|
+
if (item === "reset") {
|
|
4708
|
+
await deps.setConfig({ ...DEFAULTS });
|
|
4709
|
+
deps.setHiddenItems([]);
|
|
4710
|
+
return { message: "StatusBar config reset to defaults." };
|
|
4711
|
+
}
|
|
4712
|
+
const validItems = [
|
|
4713
|
+
"todos",
|
|
4714
|
+
"plan",
|
|
4715
|
+
"fleet",
|
|
4716
|
+
"git",
|
|
4717
|
+
"elapsed",
|
|
4718
|
+
"context",
|
|
4719
|
+
"cost"
|
|
4720
|
+
];
|
|
4721
|
+
if (!validItems.includes(item)) {
|
|
4722
|
+
return {
|
|
4723
|
+
message: `Unknown item "${item}". Available: ${validItems.join(", ")}. Usage: /statusline <item> on|off`
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
const onOff = action?.toLowerCase();
|
|
4727
|
+
if (!onOff || onOff !== "on" && onOff !== "off") {
|
|
4728
|
+
return { message: `Usage: /statusline ${item} on|off` };
|
|
4729
|
+
}
|
|
4730
|
+
const next = { ...cfg, [item]: onOff === "on" };
|
|
4731
|
+
await deps.setConfig(next);
|
|
4732
|
+
if (onOff === "off") {
|
|
4733
|
+
deps.setHiddenItems([...deps.hiddenItems, item]);
|
|
4734
|
+
} else {
|
|
4735
|
+
deps.setHiddenItems(deps.hiddenItems.filter((i) => i !== item));
|
|
4736
|
+
}
|
|
4737
|
+
return { message: `statusline ${item}: ${onOff}` };
|
|
4738
|
+
}
|
|
4739
|
+
};
|
|
4740
|
+
}
|
|
4162
4741
|
function makeInstaller(opts, projectRoot, global) {
|
|
4163
|
-
const globalRoot =
|
|
4742
|
+
const globalRoot = path23.join(os6.homedir(), ".wrongstack");
|
|
4164
4743
|
return new SkillInstaller({
|
|
4165
|
-
manifestPath:
|
|
4166
|
-
projectSkillsDir:
|
|
4167
|
-
globalSkillsDir:
|
|
4744
|
+
manifestPath: path23.join(globalRoot, "installed-skills.json"),
|
|
4745
|
+
projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
|
|
4746
|
+
globalSkillsDir: path23.join(globalRoot, "skills"),
|
|
4168
4747
|
projectHash: projectHash(projectRoot),
|
|
4169
4748
|
skillLoader: opts.skillLoader
|
|
4170
4749
|
});
|
|
@@ -4336,6 +4915,7 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
4336
4915
|
buildSkillUpdateCommand(opts),
|
|
4337
4916
|
buildSkillUninstallCommand(opts),
|
|
4338
4917
|
buildPluginCommand(opts),
|
|
4918
|
+
buildMcpSlashCommand(opts),
|
|
4339
4919
|
buildDiagCommand(opts),
|
|
4340
4920
|
buildStatsCommand(opts),
|
|
4341
4921
|
buildSpawnCommand(opts),
|
|
@@ -4352,12 +4932,22 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
4352
4932
|
buildLoadCommand(opts),
|
|
4353
4933
|
buildYoloCommand(opts),
|
|
4354
4934
|
buildAutonomyCommand(opts),
|
|
4935
|
+
buildGoalCommand(opts),
|
|
4355
4936
|
buildModeCommand(opts),
|
|
4356
4937
|
buildExitCommand(opts),
|
|
4357
4938
|
buildCommitCommand(),
|
|
4358
4939
|
buildGitcheckCommand(),
|
|
4359
4940
|
buildPushCommand(),
|
|
4360
|
-
buildSecurityCommand(opts)
|
|
4941
|
+
buildSecurityCommand(opts),
|
|
4942
|
+
buildStatuslineCommand({
|
|
4943
|
+
cwd: opts.cwd,
|
|
4944
|
+
hiddenItems: opts.statuslineHiddenItems ?? [],
|
|
4945
|
+
setHiddenItems: opts.setStatuslineHiddenItems ?? (() => {
|
|
4946
|
+
}),
|
|
4947
|
+
getConfig: opts.statuslineConfig?.get ?? (async () => ({})),
|
|
4948
|
+
setConfig: opts.statuslineConfig?.set ?? (async () => {
|
|
4949
|
+
})
|
|
4950
|
+
})
|
|
4361
4951
|
];
|
|
4362
4952
|
}
|
|
4363
4953
|
|
|
@@ -4376,13 +4966,13 @@ var MANIFESTS = [
|
|
|
4376
4966
|
];
|
|
4377
4967
|
async function detectProjectKind(projectRoot) {
|
|
4378
4968
|
try {
|
|
4379
|
-
await fsp2.access(
|
|
4969
|
+
await fsp2.access(path23.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
4380
4970
|
return "initialized";
|
|
4381
4971
|
} catch {
|
|
4382
4972
|
}
|
|
4383
4973
|
for (const m of MANIFESTS) {
|
|
4384
4974
|
try {
|
|
4385
|
-
await fsp2.access(
|
|
4975
|
+
await fsp2.access(path23.join(projectRoot, m));
|
|
4386
4976
|
return "project";
|
|
4387
4977
|
} catch {
|
|
4388
4978
|
}
|
|
@@ -4390,8 +4980,8 @@ async function detectProjectKind(projectRoot) {
|
|
|
4390
4980
|
return "empty";
|
|
4391
4981
|
}
|
|
4392
4982
|
async function scaffoldAgentsMd(projectRoot) {
|
|
4393
|
-
const dir =
|
|
4394
|
-
const file =
|
|
4983
|
+
const dir = path23.join(projectRoot, ".wrongstack");
|
|
4984
|
+
const file = path23.join(dir, "AGENTS.md");
|
|
4395
4985
|
const facts = await detectProjectFacts(projectRoot);
|
|
4396
4986
|
const body = renderAgentsTemplate(facts);
|
|
4397
4987
|
await fsp2.mkdir(dir, { recursive: true });
|
|
@@ -4404,7 +4994,7 @@ async function runProjectCheck(opts) {
|
|
|
4404
4994
|
if (kind === "initialized") {
|
|
4405
4995
|
renderer.write(
|
|
4406
4996
|
`
|
|
4407
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
4997
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path23.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
4408
4998
|
`
|
|
4409
4999
|
);
|
|
4410
5000
|
return true;
|
|
@@ -4416,8 +5006,12 @@ async function runProjectCheck(opts) {
|
|
|
4416
5006
|
`
|
|
4417
5007
|
);
|
|
4418
5008
|
const answer2 = (await reader.readLine(
|
|
4419
|
-
` ${color.amber("?")} Scaffold ${color.bold("AGENTS.md")} now? ${color.dim("[y/N]")} `
|
|
5009
|
+
` ${color.amber("?")} Scaffold ${color.bold("AGENTS.md")} now? ${color.dim("[y/N/q]")} `
|
|
4420
5010
|
)).trim().toLowerCase();
|
|
5011
|
+
if (answer2 === "q") {
|
|
5012
|
+
renderer.write(color.dim(" Cancelled.\n"));
|
|
5013
|
+
return false;
|
|
5014
|
+
}
|
|
4421
5015
|
if (answer2 === "y" || answer2 === "yes") {
|
|
4422
5016
|
try {
|
|
4423
5017
|
const file = await scaffoldAgentsMd(projectRoot);
|
|
@@ -4431,7 +5025,7 @@ async function runProjectCheck(opts) {
|
|
|
4431
5025
|
}
|
|
4432
5026
|
return true;
|
|
4433
5027
|
}
|
|
4434
|
-
const gitDir =
|
|
5028
|
+
const gitDir = path23.join(projectRoot, ".git");
|
|
4435
5029
|
let hasGit = false;
|
|
4436
5030
|
try {
|
|
4437
5031
|
await fsp2.access(gitDir);
|
|
@@ -4445,8 +5039,12 @@ async function runProjectCheck(opts) {
|
|
|
4445
5039
|
`
|
|
4446
5040
|
);
|
|
4447
5041
|
const answer2 = (await reader.readLine(
|
|
4448
|
-
` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
|
|
5042
|
+
` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N/q]")} `
|
|
4449
5043
|
)).trim().toLowerCase();
|
|
5044
|
+
if (answer2 === "q") {
|
|
5045
|
+
renderer.write(color.dim(" Cancelled.\n"));
|
|
5046
|
+
return false;
|
|
5047
|
+
}
|
|
4450
5048
|
if (answer2 === "y" || answer2 === "yes") {
|
|
4451
5049
|
try {
|
|
4452
5050
|
const { spawn: spawn3 } = await import('child_process');
|
|
@@ -4468,8 +5066,8 @@ async function runProjectCheck(opts) {
|
|
|
4468
5066
|
`
|
|
4469
5067
|
);
|
|
4470
5068
|
}
|
|
4471
|
-
const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
|
|
4472
|
-
if (answer === "n" || answer === "no") {
|
|
5069
|
+
const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n/q]")} `)).trim().toLowerCase();
|
|
5070
|
+
if (answer === "q" || answer === "n" || answer === "no") {
|
|
4473
5071
|
renderer.write(color.dim(" Cancelled.\n"));
|
|
4474
5072
|
return false;
|
|
4475
5073
|
}
|
|
@@ -4483,8 +5081,12 @@ async function runLaunchPrompts(opts) {
|
|
|
4483
5081
|
} else {
|
|
4484
5082
|
const answer = (await reader.readLine(
|
|
4485
5083
|
`
|
|
4486
|
-
${color.amber("?")} Interactive mode: ${color.bold("T")}UI / ${color.bold("R")}EPL ${color.dim("[T/r]")} `
|
|
5084
|
+
${color.amber("?")} Interactive mode: ${color.bold("T")}UI / ${color.bold("R")}EPL ${color.dim("[T/r/q]")} `
|
|
4487
5085
|
)).trim().toLowerCase();
|
|
5086
|
+
if (answer === "q") {
|
|
5087
|
+
renderer.write(color.dim(" Goodbye!\n"));
|
|
5088
|
+
process.exit(0);
|
|
5089
|
+
}
|
|
4488
5090
|
mode = answer === "r" || answer === "repl" ? "repl" : "tui";
|
|
4489
5091
|
}
|
|
4490
5092
|
let yolo;
|
|
@@ -4492,8 +5094,12 @@ async function runLaunchPrompts(opts) {
|
|
|
4492
5094
|
yolo = yoloPinned;
|
|
4493
5095
|
} else {
|
|
4494
5096
|
const answer = (await reader.readLine(
|
|
4495
|
-
` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n]")} `
|
|
5097
|
+
` ${color.amber("?")} YOLO mode ${color.dim("(auto-approve every tool call)")} ${color.dim("[Y/n/q]")} `
|
|
4496
5098
|
)).trim().toLowerCase();
|
|
5099
|
+
if (answer === "q") {
|
|
5100
|
+
renderer.write(color.dim(" Goodbye!\n"));
|
|
5101
|
+
process.exit(0);
|
|
5102
|
+
}
|
|
4497
5103
|
yolo = answer !== "n" && answer !== "no";
|
|
4498
5104
|
}
|
|
4499
5105
|
renderer.write(
|
|
@@ -4732,14 +5338,14 @@ function summarize(value, name) {
|
|
|
4732
5338
|
if (typeof v === "object" && v !== null) {
|
|
4733
5339
|
const o = v;
|
|
4734
5340
|
if (name === "edit") {
|
|
4735
|
-
const
|
|
5341
|
+
const path24 = typeof o["path"] === "string" ? o["path"] : "";
|
|
4736
5342
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
4737
|
-
return `${
|
|
5343
|
+
return `${path24} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
4738
5344
|
}
|
|
4739
5345
|
if (name === "write") {
|
|
4740
|
-
const
|
|
5346
|
+
const path24 = typeof o["path"] === "string" ? o["path"] : "";
|
|
4741
5347
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
4742
|
-
return bytes !== void 0 ? `${
|
|
5348
|
+
return bytes !== void 0 ? `${path24} ${bytes}B` : path24;
|
|
4743
5349
|
}
|
|
4744
5350
|
if (typeof o["count"] === "number") {
|
|
4745
5351
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -4889,10 +5495,12 @@ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.am
|
|
|
4889
5495
|
deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
|
|
4890
5496
|
`);
|
|
4891
5497
|
deps.renderer.write(` ${color.bold("b")} Back
|
|
5498
|
+
`);
|
|
5499
|
+
deps.renderer.write(` ${color.bold("q")} Quit
|
|
4892
5500
|
`);
|
|
4893
5501
|
const raw = (await deps.reader.readLine(`
|
|
4894
5502
|
${color.amber("?")} ${providerId} > `)).trim();
|
|
4895
|
-
if (!raw || raw === "b" || raw === "back") return;
|
|
5503
|
+
if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") return;
|
|
4896
5504
|
const [verb, argRaw] = raw.split(/\s+/, 2);
|
|
4897
5505
|
const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
|
|
4898
5506
|
if (verb === "a" || verb === "add") {
|
|
@@ -4901,8 +5509,9 @@ ${color.amber("?")} ${providerId} > `)).trim();
|
|
|
4901
5509
|
}
|
|
4902
5510
|
if (verb === "x" || verb === "remove") {
|
|
4903
5511
|
const confirm = (await deps.reader.readLine(
|
|
4904
|
-
` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N]")} `
|
|
5512
|
+
` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N/q]")} `
|
|
4905
5513
|
)).trim().toLowerCase();
|
|
5514
|
+
if (confirm === "q") continue;
|
|
4906
5515
|
if (confirm === "y" || confirm === "yes") {
|
|
4907
5516
|
await mutateProviders(deps, (all) => {
|
|
4908
5517
|
delete all[providerId];
|
|
@@ -4940,8 +5549,9 @@ ${color.amber("?")} ${providerId} > `)).trim();
|
|
|
4940
5549
|
}
|
|
4941
5550
|
const target = keys[arg - 1];
|
|
4942
5551
|
const confirm = (await deps.reader.readLine(
|
|
4943
|
-
` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N]")} `
|
|
5552
|
+
` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N/q]")} `
|
|
4944
5553
|
)).trim().toLowerCase();
|
|
5554
|
+
if (confirm === "q") continue;
|
|
4945
5555
|
if (confirm !== "y" && confirm !== "yes") continue;
|
|
4946
5556
|
await mutateProviders(deps, (all) => {
|
|
4947
5557
|
const p = all[providerId];
|
|
@@ -5038,8 +5648,8 @@ async function addForNewProvider(deps) {
|
|
|
5038
5648
|
deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
|
|
5039
5649
|
}
|
|
5040
5650
|
if (catalog.length === 0) {
|
|
5041
|
-
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id: `)).trim();
|
|
5042
|
-
if (!pid) return;
|
|
5651
|
+
const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
|
|
5652
|
+
if (!pid || pid === "q") return;
|
|
5043
5653
|
const fam = (await deps.reader.readLine(
|
|
5044
5654
|
` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
|
|
5045
5655
|
)).trim();
|
|
@@ -5059,8 +5669,9 @@ async function addForNewProvider(deps) {
|
|
|
5059
5669
|
)
|
|
5060
5670
|
);
|
|
5061
5671
|
const filterRaw = (await deps.reader.readLine(
|
|
5062
|
-
` ${color.amber("?")} Filter ${color.dim('(substring
|
|
5672
|
+
` ${color.amber("?")} Filter ${color.dim('(substring, "s" for unsaved-only, q to quit)')}: `
|
|
5063
5673
|
)).trim();
|
|
5674
|
+
if (filterRaw === "q") return;
|
|
5064
5675
|
const filterLc = filterRaw.toLowerCase();
|
|
5065
5676
|
const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
|
|
5066
5677
|
const matches = (p) => {
|
|
@@ -5114,9 +5725,9 @@ async function addForNewProvider(deps) {
|
|
|
5114
5725
|
`);
|
|
5115
5726
|
const answer = (await deps.reader.readLine(
|
|
5116
5727
|
`
|
|
5117
|
-
${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
|
|
5728
|
+
${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
|
|
5118
5729
|
)).trim();
|
|
5119
|
-
if (!answer) return;
|
|
5730
|
+
if (!answer || answer === "q") return;
|
|
5120
5731
|
let chosen;
|
|
5121
5732
|
const num = Number.parseInt(answer, 10);
|
|
5122
5733
|
if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
|
|
@@ -5133,7 +5744,8 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
|
|
|
5133
5744
|
Defaults from models.dev \u2014 press Enter to keep, or type a new value.
|
|
5134
5745
|
`)
|
|
5135
5746
|
);
|
|
5136
|
-
const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)}: `)).trim();
|
|
5747
|
+
const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `)).trim();
|
|
5748
|
+
if (famRaw === "q") return;
|
|
5137
5749
|
let family = chosen.family;
|
|
5138
5750
|
if (famRaw) {
|
|
5139
5751
|
if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
|
|
@@ -5145,8 +5757,9 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
|
|
|
5145
5757
|
family = famRaw;
|
|
5146
5758
|
}
|
|
5147
5759
|
const baseRaw = (await deps.reader.readLine(
|
|
5148
|
-
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)}: `
|
|
5760
|
+
` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
|
|
5149
5761
|
)).trim();
|
|
5762
|
+
if (baseRaw === "q") return;
|
|
5150
5763
|
const baseUrl = baseRaw || chosen.apiBase;
|
|
5151
5764
|
const providersNow = await loadProviders(deps);
|
|
5152
5765
|
let suggestedAlias = chosen.id;
|
|
@@ -5191,17 +5804,18 @@ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies
|
|
|
5191
5804
|
`
|
|
5192
5805
|
);
|
|
5193
5806
|
const type = (await deps.reader.readLine(
|
|
5194
|
-
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy")')}: `
|
|
5807
|
+
` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
|
|
5195
5808
|
)).trim();
|
|
5196
|
-
if (!type) return;
|
|
5809
|
+
if (!type || type === "q") return;
|
|
5197
5810
|
const existing = (await loadProviders(deps))[type];
|
|
5198
5811
|
if (existing) {
|
|
5199
5812
|
deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
|
|
5200
5813
|
return;
|
|
5201
5814
|
}
|
|
5202
5815
|
const familyRaw = (await deps.reader.readLine(
|
|
5203
|
-
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")}: `
|
|
5816
|
+
` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
|
|
5204
5817
|
)).trim();
|
|
5818
|
+
if (familyRaw === "q") return;
|
|
5205
5819
|
if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
|
|
5206
5820
|
deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
|
|
5207
5821
|
return;
|
|
@@ -5336,13 +5950,21 @@ async function loadProviders(deps) {
|
|
|
5336
5950
|
let raw;
|
|
5337
5951
|
try {
|
|
5338
5952
|
raw = await fsp2.readFile(deps.globalConfigPath, "utf8");
|
|
5339
|
-
} catch {
|
|
5953
|
+
} catch (err) {
|
|
5954
|
+
if (err.code !== "ENOENT") {
|
|
5955
|
+
deps.renderer.writeWarning(
|
|
5956
|
+
`Could not read ${deps.globalConfigPath}: ${err.message}. Treating as empty.`
|
|
5957
|
+
);
|
|
5958
|
+
}
|
|
5340
5959
|
return {};
|
|
5341
5960
|
}
|
|
5342
5961
|
let parsed = {};
|
|
5343
5962
|
try {
|
|
5344
5963
|
parsed = JSON.parse(raw);
|
|
5345
|
-
} catch {
|
|
5964
|
+
} catch (err) {
|
|
5965
|
+
deps.renderer.writeWarning(
|
|
5966
|
+
`Config at ${deps.globalConfigPath} is not valid JSON: ${err.message}`
|
|
5967
|
+
);
|
|
5346
5968
|
return {};
|
|
5347
5969
|
}
|
|
5348
5970
|
const decrypted = decryptConfigSecrets(parsed, deps.vault);
|
|
@@ -5350,15 +5972,29 @@ async function loadProviders(deps) {
|
|
|
5350
5972
|
}
|
|
5351
5973
|
async function mutateProviders(deps, mutator) {
|
|
5352
5974
|
let raw;
|
|
5975
|
+
let fileExists = true;
|
|
5353
5976
|
try {
|
|
5354
5977
|
raw = await fsp2.readFile(deps.globalConfigPath, "utf8");
|
|
5355
|
-
} catch {
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
if (err.code !== "ENOENT") {
|
|
5980
|
+
throw new Error(
|
|
5981
|
+
`Refusing to mutate ${deps.globalConfigPath}: ${err.message}`,
|
|
5982
|
+
{ cause: err }
|
|
5983
|
+
);
|
|
5984
|
+
}
|
|
5985
|
+
fileExists = false;
|
|
5356
5986
|
raw = "{}";
|
|
5357
5987
|
}
|
|
5358
5988
|
let parsed;
|
|
5359
5989
|
try {
|
|
5360
5990
|
parsed = JSON.parse(raw);
|
|
5361
|
-
} catch {
|
|
5991
|
+
} catch (err) {
|
|
5992
|
+
if (fileExists) {
|
|
5993
|
+
throw new Error(
|
|
5994
|
+
`Refusing to overwrite corrupt config at ${deps.globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
|
|
5995
|
+
{ cause: err }
|
|
5996
|
+
);
|
|
5997
|
+
}
|
|
5362
5998
|
parsed = {};
|
|
5363
5999
|
}
|
|
5364
6000
|
const decrypted = decryptConfigSecrets(parsed, deps.vault);
|
|
@@ -5562,7 +6198,7 @@ var doctorCmd = async (_args, deps) => {
|
|
|
5562
6198
|
}
|
|
5563
6199
|
try {
|
|
5564
6200
|
await fsp2.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
5565
|
-
const probe =
|
|
6201
|
+
const probe = path23.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
5566
6202
|
await fsp2.writeFile(probe, "");
|
|
5567
6203
|
await fsp2.unlink(probe);
|
|
5568
6204
|
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
@@ -5665,8 +6301,8 @@ var exportCmd = async (args, deps) => {
|
|
|
5665
6301
|
return 1;
|
|
5666
6302
|
}
|
|
5667
6303
|
if (output) {
|
|
5668
|
-
await fsp2.mkdir(
|
|
5669
|
-
await fsp2.writeFile(
|
|
6304
|
+
await fsp2.mkdir(path23.dirname(path23.resolve(deps.cwd, output)), { recursive: true });
|
|
6305
|
+
await fsp2.writeFile(path23.resolve(deps.cwd, output), rendered, "utf8");
|
|
5670
6306
|
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
5671
6307
|
`);
|
|
5672
6308
|
} else {
|
|
@@ -5695,7 +6331,12 @@ var initCmd = async (_args, deps) => {
|
|
|
5695
6331
|
`
|
|
5696
6332
|
);
|
|
5697
6333
|
const defaultId = ranked[0]?.id ?? "anthropic";
|
|
5698
|
-
const
|
|
6334
|
+
const providerAnswer = (await deps.reader.readLine(`Provider [${defaultId}]: `)).trim();
|
|
6335
|
+
if (providerAnswer === "q") {
|
|
6336
|
+
deps.renderer.write(color.dim("Cancelled.\n"));
|
|
6337
|
+
return 0;
|
|
6338
|
+
}
|
|
6339
|
+
const providerId = providerAnswer || defaultId;
|
|
5699
6340
|
const provider = await deps.modelsRegistry.getProvider(providerId);
|
|
5700
6341
|
if (!provider) {
|
|
5701
6342
|
deps.renderer.writeError(`Provider "${providerId}" not found in models.dev catalog.`);
|
|
@@ -5709,7 +6350,12 @@ var initCmd = async (_args, deps) => {
|
|
|
5709
6350
|
}
|
|
5710
6351
|
const suggestedModel = await deps.modelsRegistry.suggestModel(providerId) ?? "";
|
|
5711
6352
|
const modelHint = suggestedModel ? ` [${suggestedModel}]` : "";
|
|
5712
|
-
const
|
|
6353
|
+
const modelAnswer = (await deps.reader.readLine(`Model${modelHint}: `)).trim();
|
|
6354
|
+
if (modelAnswer === "q") {
|
|
6355
|
+
deps.renderer.write(color.dim("Cancelled.\n"));
|
|
6356
|
+
return 0;
|
|
6357
|
+
}
|
|
6358
|
+
const modelId = modelAnswer || suggestedModel;
|
|
5713
6359
|
if (!modelId) {
|
|
5714
6360
|
deps.renderer.writeError("No model selected. Aborting.");
|
|
5715
6361
|
return 1;
|
|
@@ -5726,18 +6372,14 @@ var initCmd = async (_args, deps) => {
|
|
|
5726
6372
|
await fsp2.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
5727
6373
|
const config = { version: 1, provider: providerId, model: modelId };
|
|
5728
6374
|
if (apiKey) config.apiKey = apiKey;
|
|
5729
|
-
const keyFile =
|
|
6375
|
+
const keyFile = path23.join(path23.dirname(deps.paths.globalConfig), ".key");
|
|
5730
6376
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
5731
6377
|
const encrypted = encryptConfigSecrets(config, vault);
|
|
5732
6378
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
|
|
5733
|
-
await fsp2.mkdir(
|
|
5734
|
-
const agentsFile =
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
} catch {
|
|
5738
|
-
const detected2 = await detectProjectFacts(deps.projectRoot);
|
|
5739
|
-
await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
|
|
5740
|
-
}
|
|
6379
|
+
await fsp2.mkdir(path23.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
6380
|
+
const agentsFile = path23.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
6381
|
+
const projectFacts = await detectProjectFacts(deps.projectRoot);
|
|
6382
|
+
await atomicWrite(agentsFile, renderAgentsTemplate(projectFacts));
|
|
5741
6383
|
deps.renderer.writeInfo(`Wrote ${deps.paths.globalConfig}`);
|
|
5742
6384
|
deps.renderer.writeInfo(`Project state lives in ${deps.paths.projectDir}`);
|
|
5743
6385
|
deps.renderer.writeInfo('Try: wstack "<task>" or wstack');
|
|
@@ -5773,7 +6415,7 @@ var mcpCmd = async (args, deps) => {
|
|
|
5773
6415
|
return removeMcpServer(name, deps);
|
|
5774
6416
|
}
|
|
5775
6417
|
if (sub === "restart") {
|
|
5776
|
-
deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
|
|
6418
|
+
deps.renderer.writeWarning("mcp restart is only available in REPL mode. Use /mcp restart instead.");
|
|
5777
6419
|
return 0;
|
|
5778
6420
|
}
|
|
5779
6421
|
deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
|
|
@@ -5948,7 +6590,7 @@ function renderConfiguredPlugins(config) {
|
|
|
5948
6590
|
return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
|
|
5949
6591
|
}).join("\n");
|
|
5950
6592
|
}
|
|
5951
|
-
async function
|
|
6593
|
+
async function readConfig2(file) {
|
|
5952
6594
|
try {
|
|
5953
6595
|
return JSON.parse(await fsp2.readFile(file, "utf8"));
|
|
5954
6596
|
} catch {
|
|
@@ -5967,7 +6609,7 @@ function officialPluginState(config, spec) {
|
|
|
5967
6609
|
return typeof match === "object" && match.enabled === false ? "disabled" : "enabled";
|
|
5968
6610
|
}
|
|
5969
6611
|
async function upsertPlugin(spec, opts, deps, verb) {
|
|
5970
|
-
const existing = await
|
|
6612
|
+
const existing = await readConfig2(deps.configPath);
|
|
5971
6613
|
const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
|
|
5972
6614
|
const idx = plugins.findIndex((p) => pluginName(p) === spec);
|
|
5973
6615
|
const nextEntry = pluginEntry(spec, opts.enabled);
|
|
@@ -5990,7 +6632,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
|
|
|
5990
6632
|
};
|
|
5991
6633
|
}
|
|
5992
6634
|
async function removePlugin(spec, deps) {
|
|
5993
|
-
const existing = await
|
|
6635
|
+
const existing = await readConfig2(deps.configPath);
|
|
5994
6636
|
const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
|
|
5995
6637
|
const next = plugins.filter((p) => pluginName(p) !== spec);
|
|
5996
6638
|
if (next.length === plugins.length) {
|
|
@@ -6041,7 +6683,7 @@ var usageCmd = async (_args, deps) => {
|
|
|
6041
6683
|
return 0;
|
|
6042
6684
|
};
|
|
6043
6685
|
var projectsCmd = async (_args, deps) => {
|
|
6044
|
-
const projectsRoot =
|
|
6686
|
+
const projectsRoot = path23.join(deps.paths.globalRoot, "projects");
|
|
6045
6687
|
try {
|
|
6046
6688
|
const entries = await fsp2.readdir(projectsRoot);
|
|
6047
6689
|
if (entries.length === 0) {
|
|
@@ -6051,7 +6693,7 @@ var projectsCmd = async (_args, deps) => {
|
|
|
6051
6693
|
for (const hash of entries) {
|
|
6052
6694
|
try {
|
|
6053
6695
|
const meta = JSON.parse(
|
|
6054
|
-
await fsp2.readFile(
|
|
6696
|
+
await fsp2.readFile(path23.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
6055
6697
|
);
|
|
6056
6698
|
deps.renderer.write(
|
|
6057
6699
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -6212,7 +6854,7 @@ async function listFleetRuns(deps) {
|
|
|
6212
6854
|
}
|
|
6213
6855
|
const runs = [];
|
|
6214
6856
|
for (const id of entries) {
|
|
6215
|
-
const runDir =
|
|
6857
|
+
const runDir = path23.join(deps.paths.projectSessions, id);
|
|
6216
6858
|
let stat3;
|
|
6217
6859
|
try {
|
|
6218
6860
|
stat3 = await fsp2.stat(runDir);
|
|
@@ -6225,17 +6867,17 @@ async function listFleetRuns(deps) {
|
|
|
6225
6867
|
let subagentCount = 0;
|
|
6226
6868
|
let subagentsDir;
|
|
6227
6869
|
try {
|
|
6228
|
-
await fsp2.access(
|
|
6870
|
+
await fsp2.access(path23.join(runDir, "fleet.json"));
|
|
6229
6871
|
manifest = true;
|
|
6230
6872
|
} catch {
|
|
6231
6873
|
}
|
|
6232
6874
|
try {
|
|
6233
|
-
await fsp2.access(
|
|
6875
|
+
await fsp2.access(path23.join(runDir, "checkpoint.json"));
|
|
6234
6876
|
checkpoint = true;
|
|
6235
6877
|
} catch {
|
|
6236
6878
|
}
|
|
6237
6879
|
try {
|
|
6238
|
-
subagentsDir =
|
|
6880
|
+
subagentsDir = path23.join(runDir, "subagents");
|
|
6239
6881
|
const files = await fsp2.readdir(subagentsDir);
|
|
6240
6882
|
subagentCount = files.filter((f) => f.endsWith(".jsonl")).length;
|
|
6241
6883
|
} catch {
|
|
@@ -6264,7 +6906,7 @@ async function listFleetRuns(deps) {
|
|
|
6264
6906
|
return 0;
|
|
6265
6907
|
}
|
|
6266
6908
|
async function showFleetRun(runId, deps) {
|
|
6267
|
-
const runDir =
|
|
6909
|
+
const runDir = path23.join(deps.paths.projectSessions, runId);
|
|
6268
6910
|
let stat3;
|
|
6269
6911
|
try {
|
|
6270
6912
|
stat3 = await fsp2.stat(runDir);
|
|
@@ -6281,7 +6923,7 @@ async function showFleetRun(runId, deps) {
|
|
|
6281
6923
|
deps.renderer.write(color.bold(`
|
|
6282
6924
|
Fleet Run: ${runId}
|
|
6283
6925
|
`) + "\n");
|
|
6284
|
-
const manifestPath =
|
|
6926
|
+
const manifestPath = path23.join(runDir, "fleet.json");
|
|
6285
6927
|
let manifestData = null;
|
|
6286
6928
|
try {
|
|
6287
6929
|
manifestData = await fsp2.readFile(manifestPath, "utf8");
|
|
@@ -6297,7 +6939,7 @@ Fleet Run: ${runId}
|
|
|
6297
6939
|
deps.renderer.write(` ${color.dim("\u25CB")} fleet.json \u2014 not found
|
|
6298
6940
|
`);
|
|
6299
6941
|
}
|
|
6300
|
-
const checkpointPath =
|
|
6942
|
+
const checkpointPath = path23.join(runDir, "checkpoint.json");
|
|
6301
6943
|
let checkpointData = null;
|
|
6302
6944
|
try {
|
|
6303
6945
|
checkpointData = await fsp2.readFile(checkpointPath, "utf8");
|
|
@@ -6344,7 +6986,7 @@ Fleet Run: ${runId}
|
|
|
6344
6986
|
} catch {
|
|
6345
6987
|
}
|
|
6346
6988
|
}
|
|
6347
|
-
const subagentsDir =
|
|
6989
|
+
const subagentsDir = path23.join(runDir, "subagents");
|
|
6348
6990
|
let subagentFiles = [];
|
|
6349
6991
|
try {
|
|
6350
6992
|
subagentFiles = await fsp2.readdir(subagentsDir);
|
|
@@ -6356,7 +6998,7 @@ Fleet Run: ${runId}
|
|
|
6356
6998
|
Subagent transcripts (${subagentFiles.length}):
|
|
6357
6999
|
`);
|
|
6358
7000
|
for (const f of subagentFiles.sort()) {
|
|
6359
|
-
const filePath =
|
|
7001
|
+
const filePath = path23.join(subagentsDir, f);
|
|
6360
7002
|
let size;
|
|
6361
7003
|
try {
|
|
6362
7004
|
const s = await fsp2.stat(filePath);
|
|
@@ -6373,7 +7015,7 @@ Fleet Run: ${runId}
|
|
|
6373
7015
|
${color.dim("\u25CB")} No subagent transcripts
|
|
6374
7016
|
`);
|
|
6375
7017
|
}
|
|
6376
|
-
const sharedDir =
|
|
7018
|
+
const sharedDir = path23.join(runDir, "shared");
|
|
6377
7019
|
try {
|
|
6378
7020
|
const files = await fsp2.readdir(sharedDir);
|
|
6379
7021
|
deps.renderer.write(`
|
|
@@ -6526,12 +7168,23 @@ function parseRewindFlags(args) {
|
|
|
6526
7168
|
}
|
|
6527
7169
|
return flags;
|
|
6528
7170
|
}
|
|
7171
|
+
function findSessionId(args) {
|
|
7172
|
+
for (let i = 0; i < args.length; i++) {
|
|
7173
|
+
const a = args[i];
|
|
7174
|
+
if (a === "--last" || a === "--to") {
|
|
7175
|
+
i++;
|
|
7176
|
+
continue;
|
|
7177
|
+
}
|
|
7178
|
+
if (!a.startsWith("--")) return a;
|
|
7179
|
+
}
|
|
7180
|
+
return void 0;
|
|
7181
|
+
}
|
|
6529
7182
|
var rewindCmd = async (args, deps) => {
|
|
6530
7183
|
const flags = parseRewindFlags(args);
|
|
6531
7184
|
const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
|
|
6532
|
-
const sessionsDir =
|
|
7185
|
+
const sessionsDir = path23.join(wpaths.globalRoot, "sessions");
|
|
6533
7186
|
const rewind = new DefaultSessionRewinder(sessionsDir);
|
|
6534
|
-
let sessionId = args
|
|
7187
|
+
let sessionId = findSessionId(args);
|
|
6535
7188
|
if (!sessionId) {
|
|
6536
7189
|
if (!deps.sessionStore) {
|
|
6537
7190
|
deps.renderer.writeError("No session store available.");
|
|
@@ -6672,6 +7325,7 @@ var helpCmd = async (_args, deps) => {
|
|
|
6672
7325
|
"",
|
|
6673
7326
|
" wstack Start REPL",
|
|
6674
7327
|
' wstack "<task>" Run task and exit',
|
|
7328
|
+
' wstack --eternal "<mission>" Launch eternal-autonomy loop against a goal \u2014 Ctrl+C to stop',
|
|
6675
7329
|
" wstack resume [<id>] Resume a session",
|
|
6676
7330
|
" wstack sessions List recent sessions",
|
|
6677
7331
|
" wstack init Pick provider + model from models.dev",
|
|
@@ -6739,22 +7393,22 @@ function fmtDuration(ms) {
|
|
|
6739
7393
|
const remMin = m - h * 60;
|
|
6740
7394
|
return `${h}h${remMin}m`;
|
|
6741
7395
|
}
|
|
6742
|
-
function fmtTaskResultLine(r,
|
|
7396
|
+
function fmtTaskResultLine(r, color35) {
|
|
6743
7397
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
6744
7398
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
6745
7399
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
6746
7400
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
6747
|
-
const errKindChip = errKind ?
|
|
6748
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
7401
|
+
const errKindChip = errKind ? color35.dim(` [${errKind}]`) : "";
|
|
7402
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color35.dim(errTail)}` : "";
|
|
6749
7403
|
switch (r.status) {
|
|
6750
7404
|
case "success":
|
|
6751
|
-
return { mark:
|
|
7405
|
+
return { mark: color35.green("\u2713"), stats, tail: "" };
|
|
6752
7406
|
case "timeout":
|
|
6753
|
-
return { mark:
|
|
7407
|
+
return { mark: color35.yellow("\u23F1"), stats: `${color35.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
6754
7408
|
case "stopped":
|
|
6755
|
-
return { mark:
|
|
7409
|
+
return { mark: color35.dim("\u2298"), stats: `${color35.dim("stopped")} ${stats}`, tail: errSnip };
|
|
6756
7410
|
case "failed":
|
|
6757
|
-
return { mark:
|
|
7411
|
+
return { mark: color35.red("\u2717"), stats: `${color35.red("failed")} ${stats}`, tail: errSnip };
|
|
6758
7412
|
}
|
|
6759
7413
|
}
|
|
6760
7414
|
|
|
@@ -6764,7 +7418,7 @@ function resolveBundledSkillsDir() {
|
|
|
6764
7418
|
try {
|
|
6765
7419
|
const req2 = createRequire(import.meta.url);
|
|
6766
7420
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
6767
|
-
return
|
|
7421
|
+
return path23.join(path23.dirname(corePkg), "skills");
|
|
6768
7422
|
} catch {
|
|
6769
7423
|
return void 0;
|
|
6770
7424
|
}
|
|
@@ -6917,6 +7571,7 @@ async function boot(argv) {
|
|
|
6917
7571
|
init_sdd();
|
|
6918
7572
|
async function runRepl(opts) {
|
|
6919
7573
|
if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
|
|
7574
|
+
await renderGoalBanner(opts);
|
|
6920
7575
|
let activeCtrl;
|
|
6921
7576
|
let interrupts = 0;
|
|
6922
7577
|
const onSigint = () => {
|
|
@@ -6925,6 +7580,12 @@ async function runRepl(opts) {
|
|
|
6925
7580
|
opts.renderer.writeWarning("Exiting.");
|
|
6926
7581
|
process.exit(130);
|
|
6927
7582
|
}
|
|
7583
|
+
const engine = opts.getEternalEngine?.();
|
|
7584
|
+
if (engine && opts.getAutonomy?.() === "eternal") {
|
|
7585
|
+
engine.stop();
|
|
7586
|
+
opts.renderer.writeWarning("Eternal mode stop requested. Press Ctrl+C again to exit.");
|
|
7587
|
+
return;
|
|
7588
|
+
}
|
|
6928
7589
|
if (activeCtrl) {
|
|
6929
7590
|
activeCtrl.abort();
|
|
6930
7591
|
opts.renderer.writeWarning("Iteration cancelled. Press Ctrl+C again to exit.");
|
|
@@ -6936,6 +7597,42 @@ async function runRepl(opts) {
|
|
|
6936
7597
|
const builder = new InputBuilder({ store: opts.attachments });
|
|
6937
7598
|
try {
|
|
6938
7599
|
for (; ; ) {
|
|
7600
|
+
if (opts.getAutonomy?.() === "eternal") {
|
|
7601
|
+
const engine = opts.getEternalEngine?.();
|
|
7602
|
+
if (!engine) {
|
|
7603
|
+
opts.renderer.writeWarning("Eternal mode set but no engine wired \u2014 falling back to off.");
|
|
7604
|
+
} else {
|
|
7605
|
+
const beforeGoal = await loadGoalSafe(opts);
|
|
7606
|
+
const beforeIter = beforeGoal?.iterations ?? 0;
|
|
7607
|
+
opts.renderer.write(
|
|
7608
|
+
color.dim(`
|
|
7609
|
+
\u21B3 [eternal #${beforeIter + 1}] running iteration\u2026
|
|
7610
|
+
`)
|
|
7611
|
+
);
|
|
7612
|
+
interrupts = 0;
|
|
7613
|
+
try {
|
|
7614
|
+
const ok = await engine.runOneIteration();
|
|
7615
|
+
const afterGoal = await loadGoalSafe(opts);
|
|
7616
|
+
const last = afterGoal?.journal[afterGoal.journal.length - 1];
|
|
7617
|
+
if (!ok && !last) {
|
|
7618
|
+
opts.renderer.write(color.dim(" \u21B3 [eternal] iteration produced no progress.\n"));
|
|
7619
|
+
} else if (last) {
|
|
7620
|
+
const mark = last.status === "success" ? color.green("\u2713") : last.status === "failure" ? color.red("\u2717") : color.amber("\u2298");
|
|
7621
|
+
const tail = last.note ? color.dim(` \u2014 ${last.note.slice(0, 80)}`) : "";
|
|
7622
|
+
opts.renderer.write(
|
|
7623
|
+
` ${mark} ${color.dim(`#${last.iteration}`)} ${color.dim(`[${last.source}]`)} ${last.task}${tail}
|
|
7624
|
+
`
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
7627
|
+
} catch (err) {
|
|
7628
|
+
opts.renderer.writeError(
|
|
7629
|
+
`[eternal] ${err instanceof Error ? err.message : String(err)}`
|
|
7630
|
+
);
|
|
7631
|
+
}
|
|
7632
|
+
await new Promise((resolve4) => setTimeout(resolve4, 250));
|
|
7633
|
+
continue;
|
|
7634
|
+
}
|
|
7635
|
+
}
|
|
6939
7636
|
let raw;
|
|
6940
7637
|
try {
|
|
6941
7638
|
raw = await readPossiblyMultiline(opts);
|
|
@@ -6948,6 +7645,10 @@ async function runRepl(opts) {
|
|
|
6948
7645
|
continue;
|
|
6949
7646
|
}
|
|
6950
7647
|
interrupts = 0;
|
|
7648
|
+
if (trimmed === "q") {
|
|
7649
|
+
opts.renderer.write(color.dim(" Goodbye!\n"));
|
|
7650
|
+
break;
|
|
7651
|
+
}
|
|
6951
7652
|
if (trimmed === "/image" || trimmed === "/paste-image" || raw === "\x1Bv") {
|
|
6952
7653
|
await pasteClipboardImage(builder, opts);
|
|
6953
7654
|
continue;
|
|
@@ -7219,6 +7920,48 @@ async function pasteClipboardImage(builder, opts) {
|
|
|
7219
7920
|
);
|
|
7220
7921
|
}
|
|
7221
7922
|
}
|
|
7923
|
+
async function loadGoalSafe(opts) {
|
|
7924
|
+
if (!opts.projectRoot) return null;
|
|
7925
|
+
try {
|
|
7926
|
+
return await loadGoal(goalFilePath(opts.projectRoot));
|
|
7927
|
+
} catch {
|
|
7928
|
+
return null;
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
async function renderGoalBanner(opts) {
|
|
7932
|
+
const goal = await loadGoalSafe(opts);
|
|
7933
|
+
if (!goal) return;
|
|
7934
|
+
const summary = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
|
|
7935
|
+
opts.renderer.write(
|
|
7936
|
+
color.dim("Goal: ") + color.bold(summary) + color.dim(` (iter ${goal.iterations})`) + "\n"
|
|
7937
|
+
);
|
|
7938
|
+
if (goal.engineState === "running") {
|
|
7939
|
+
opts.renderer.write(
|
|
7940
|
+
color.amber(" \u21BA Eternal engine was running when last session ended.") + "\n"
|
|
7941
|
+
);
|
|
7942
|
+
try {
|
|
7943
|
+
const answer = (await opts.reader.readLine(color.dim(" Resume eternal mode? [y/N] "))).trim().toLowerCase();
|
|
7944
|
+
if (answer === "y" || answer === "yes") {
|
|
7945
|
+
try {
|
|
7946
|
+
await opts.slashRegistry.dispatch("/autonomy eternal", opts.agent.ctx);
|
|
7947
|
+
} catch (err) {
|
|
7948
|
+
opts.renderer.writeError(
|
|
7949
|
+
`Auto-resume failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7950
|
+
);
|
|
7951
|
+
}
|
|
7952
|
+
} else {
|
|
7953
|
+
opts.renderer.write(
|
|
7954
|
+
color.dim(" Not resuming. Use `/autonomy eternal` later to continue.") + "\n"
|
|
7955
|
+
);
|
|
7956
|
+
}
|
|
7957
|
+
} catch {
|
|
7958
|
+
opts.renderer.write(
|
|
7959
|
+
color.dim(" Use `/autonomy eternal` to resume.") + "\n"
|
|
7960
|
+
);
|
|
7961
|
+
}
|
|
7962
|
+
}
|
|
7963
|
+
opts.renderer.write("\n");
|
|
7964
|
+
}
|
|
7222
7965
|
async function readPossiblyMultiline(opts) {
|
|
7223
7966
|
const firstPrompt = theme2.primary("\u203A ");
|
|
7224
7967
|
const contPrompt = color.dim("\xB7 ");
|
|
@@ -7266,7 +8009,7 @@ function printBanner(renderer, projectName) {
|
|
|
7266
8009
|
if (projectName && projectName.length > 0) {
|
|
7267
8010
|
lines.push(color.dim("Project: ") + theme2.bold(projectName));
|
|
7268
8011
|
}
|
|
7269
|
-
lines.push(color.dim("Type /help for commands, /exit to quit."), "");
|
|
8012
|
+
lines.push(color.dim("Type /help for commands, /exit or q to quit."), "");
|
|
7270
8013
|
renderer.write(`${lines.join("\n")}
|
|
7271
8014
|
`);
|
|
7272
8015
|
}
|
|
@@ -7302,8 +8045,12 @@ async function execute(deps) {
|
|
|
7302
8045
|
director,
|
|
7303
8046
|
fleetRoster,
|
|
7304
8047
|
fleetStreamController,
|
|
8048
|
+
statuslineHiddenItems,
|
|
8049
|
+
setStatuslineHiddenItems,
|
|
7305
8050
|
getYolo,
|
|
7306
8051
|
getAutonomy,
|
|
8052
|
+
getEternalEngine,
|
|
8053
|
+
subscribeEternalIteration,
|
|
7307
8054
|
skillLoader
|
|
7308
8055
|
} = deps;
|
|
7309
8056
|
let code = 0;
|
|
@@ -7411,6 +8158,9 @@ async function execute(deps) {
|
|
|
7411
8158
|
queueStore,
|
|
7412
8159
|
yolo: !!config.yolo,
|
|
7413
8160
|
getYolo,
|
|
8161
|
+
getAutonomy,
|
|
8162
|
+
getEternalEngine,
|
|
8163
|
+
subscribeEternalIteration,
|
|
7414
8164
|
appVersion: CLI_VERSION,
|
|
7415
8165
|
provider: config.provider,
|
|
7416
8166
|
family: banneredFamily,
|
|
@@ -7436,6 +8186,8 @@ async function execute(deps) {
|
|
|
7436
8186
|
dispatch({ type: "resetContextChip" });
|
|
7437
8187
|
},
|
|
7438
8188
|
fleetStreamController,
|
|
8189
|
+
statuslineHiddenItems,
|
|
8190
|
+
setStatuslineHiddenItems,
|
|
7439
8191
|
initialGoal: goalFlag,
|
|
7440
8192
|
initialAsk: askFlag,
|
|
7441
8193
|
getSDDContext: () => {
|
|
@@ -7479,7 +8231,8 @@ async function execute(deps) {
|
|
|
7479
8231
|
session,
|
|
7480
8232
|
port: Number.parseInt(String(flags.port ?? "3457"), 10),
|
|
7481
8233
|
modelsRegistry,
|
|
7482
|
-
globalConfigPath: wpaths.globalConfig
|
|
8234
|
+
globalConfigPath: wpaths.globalConfig,
|
|
8235
|
+
subscribeEternalIteration
|
|
7483
8236
|
});
|
|
7484
8237
|
try {
|
|
7485
8238
|
code = await runRepl({
|
|
@@ -7492,8 +8245,10 @@ async function execute(deps) {
|
|
|
7492
8245
|
supportsVision,
|
|
7493
8246
|
attachments,
|
|
7494
8247
|
effectiveMaxContext,
|
|
7495
|
-
projectName:
|
|
8248
|
+
projectName: path23.basename(projectRoot) || void 0,
|
|
8249
|
+
projectRoot,
|
|
7496
8250
|
getAutonomy,
|
|
8251
|
+
getEternalEngine,
|
|
7497
8252
|
skillLoader
|
|
7498
8253
|
});
|
|
7499
8254
|
} finally {
|
|
@@ -7510,7 +8265,7 @@ async function execute(deps) {
|
|
|
7510
8265
|
supportsVision,
|
|
7511
8266
|
attachments,
|
|
7512
8267
|
effectiveMaxContext,
|
|
7513
|
-
projectName:
|
|
8268
|
+
projectName: path23.basename(projectRoot) || void 0,
|
|
7514
8269
|
getAutonomy,
|
|
7515
8270
|
skillLoader
|
|
7516
8271
|
});
|
|
@@ -7599,9 +8354,9 @@ var MultiAgentHost = class {
|
|
|
7599
8354
|
const coordinatorConfig = {
|
|
7600
8355
|
coordinatorId: randomUUID(),
|
|
7601
8356
|
doneCondition: { type: "all_tasks_done" },
|
|
7602
|
-
maxConcurrent:
|
|
8357
|
+
maxConcurrent: this.opts.maxConcurrent ?? 4
|
|
7603
8358
|
};
|
|
7604
|
-
const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ?
|
|
8359
|
+
const defaultScratchpad = this.opts.sharedScratchpadPath || (this.opts.sessionsRoot && this.opts.directorRunId ? path23.join(this.opts.sessionsRoot, this.opts.directorRunId, "shared") : void 0);
|
|
7605
8360
|
this.director = new Director({
|
|
7606
8361
|
config: coordinatorConfig,
|
|
7607
8362
|
manifestPath: this.opts.manifestPath,
|
|
@@ -7687,6 +8442,16 @@ var MultiAgentHost = class {
|
|
|
7687
8442
|
model: subCfg.model ?? config.model,
|
|
7688
8443
|
tools: this.filterTools(subCfg.tools)
|
|
7689
8444
|
});
|
|
8445
|
+
const toolExecutor = new ToolExecutor(this.subagentToolRegistry(subCfg.tools), {
|
|
8446
|
+
permissionPolicy: new AutoApprovePermissionPolicy(),
|
|
8447
|
+
secretScrubber: this.deps.secretScrubber,
|
|
8448
|
+
renderer: this.deps.renderer,
|
|
8449
|
+
events,
|
|
8450
|
+
confirmAwaiter: void 0,
|
|
8451
|
+
iterationTimeoutMs: config.tools?.iterationTimeoutMs ?? 12e4,
|
|
8452
|
+
perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? 1e5,
|
|
8453
|
+
tracer: void 0
|
|
8454
|
+
});
|
|
7690
8455
|
const agent = new Agent({
|
|
7691
8456
|
container: this.deps.container,
|
|
7692
8457
|
tools: this.subagentToolRegistry(subCfg.tools),
|
|
@@ -7698,7 +8463,8 @@ var MultiAgentHost = class {
|
|
|
7698
8463
|
// run under a director, not the user. Auto-approve everything
|
|
7699
8464
|
// (except tool-level hard denies); the user already authorized
|
|
7700
8465
|
// the work when they invoked the leader.
|
|
7701
|
-
permissionPolicy: new AutoApprovePermissionPolicy()
|
|
8466
|
+
permissionPolicy: new AutoApprovePermissionPolicy(),
|
|
8467
|
+
toolExecutor
|
|
7702
8468
|
});
|
|
7703
8469
|
const hostEvents = this.deps.events;
|
|
7704
8470
|
const offToolBridge = events.on("tool.executed", (e) => {
|
|
@@ -7774,7 +8540,7 @@ var MultiAgentHost = class {
|
|
|
7774
8540
|
model: opts?.model,
|
|
7775
8541
|
tools: opts?.tools
|
|
7776
8542
|
};
|
|
7777
|
-
const transcriptPath = this.sessionFactory ?
|
|
8543
|
+
const transcriptPath = this.sessionFactory ? path23.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
7778
8544
|
const { subagentId, taskId } = await this._spawnAndAssign(subagentConfig);
|
|
7779
8545
|
this.fleetManager?.addPendingTask(taskId, subagentId, description);
|
|
7780
8546
|
this.deps.events.emit("subagent.spawned", {
|
|
@@ -7917,16 +8683,16 @@ var MultiAgentHost = class {
|
|
|
7917
8683
|
if (this.director) return this.director;
|
|
7918
8684
|
this.opts.directorMode = true;
|
|
7919
8685
|
if (this.opts.fleetRoot && !this.opts.manifestPath) {
|
|
7920
|
-
this.opts.manifestPath =
|
|
8686
|
+
this.opts.manifestPath = path23.join(this.opts.fleetRoot, "fleet.json");
|
|
7921
8687
|
}
|
|
7922
8688
|
if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
|
|
7923
|
-
this.opts.sharedScratchpadPath =
|
|
8689
|
+
this.opts.sharedScratchpadPath = path23.join(this.opts.fleetRoot, "shared");
|
|
7924
8690
|
}
|
|
7925
8691
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
7926
|
-
this.opts.sessionsRoot =
|
|
8692
|
+
this.opts.sessionsRoot = path23.join(this.opts.fleetRoot, "subagents");
|
|
7927
8693
|
}
|
|
7928
8694
|
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
7929
|
-
this.opts.stateCheckpointPath =
|
|
8695
|
+
this.opts.stateCheckpointPath = path23.join(this.opts.fleetRoot, "director-state.json");
|
|
7930
8696
|
}
|
|
7931
8697
|
await this.ensureDirector();
|
|
7932
8698
|
return this.director ?? null;
|
|
@@ -7965,6 +8731,37 @@ var MultiAgentHost = class {
|
|
|
7965
8731
|
await this.getCoordinator().stopAll();
|
|
7966
8732
|
}
|
|
7967
8733
|
}
|
|
8734
|
+
/**
|
|
8735
|
+
* Current effective concurrent-subagent ceiling. Reads the live
|
|
8736
|
+
* coordinator config when the director is built; otherwise falls back
|
|
8737
|
+
* to the constructor option (or the default of 4 that buildDirector
|
|
8738
|
+
* will apply on first /spawn).
|
|
8739
|
+
*/
|
|
8740
|
+
getMaxConcurrent() {
|
|
8741
|
+
if (this.director) {
|
|
8742
|
+
return this.getCoordinator().config.maxConcurrent ?? 4;
|
|
8743
|
+
}
|
|
8744
|
+
return this.opts.maxConcurrent ?? 4;
|
|
8745
|
+
}
|
|
8746
|
+
/**
|
|
8747
|
+
* Change the concurrent-subagent ceiling at runtime. Updates the
|
|
8748
|
+
* constructor option (so lazy-built director picks it up) and, if the
|
|
8749
|
+
* coordinator already exists, mutates its live config + triggers a
|
|
8750
|
+
* dispatch pass so newly-allowed slots fill immediately.
|
|
8751
|
+
*
|
|
8752
|
+
* Throws on non-positive values; the caller is expected to validate
|
|
8753
|
+
* user input first.
|
|
8754
|
+
*/
|
|
8755
|
+
setMaxConcurrent(n) {
|
|
8756
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
8757
|
+
throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
|
|
8758
|
+
}
|
|
8759
|
+
const v = Math.floor(n);
|
|
8760
|
+
this.opts.maxConcurrent = v;
|
|
8761
|
+
if (this.director) {
|
|
8762
|
+
this.getCoordinator().setMaxConcurrent(v);
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
7968
8765
|
};
|
|
7969
8766
|
function makePromptDelegate(reader) {
|
|
7970
8767
|
return async (tool, input, suggestedPattern) => {
|
|
@@ -8047,11 +8844,11 @@ var SessionStats = class {
|
|
|
8047
8844
|
if (e.name === "bash") this.bashCommands++;
|
|
8048
8845
|
else if (e.name === "fetch") this.fetches++;
|
|
8049
8846
|
if (!e.ok) return;
|
|
8050
|
-
const
|
|
8051
|
-
if (e.name === "read" &&
|
|
8052
|
-
else if (e.name === "edit" &&
|
|
8053
|
-
else if (e.name === "write" &&
|
|
8054
|
-
this.writtenPaths.add(
|
|
8847
|
+
const path24 = typeof input?.path === "string" ? input.path : void 0;
|
|
8848
|
+
if (e.name === "read" && path24) this.readPaths.add(path24);
|
|
8849
|
+
else if (e.name === "edit" && path24) this.editedPaths.add(path24);
|
|
8850
|
+
else if (e.name === "write" && path24) {
|
|
8851
|
+
this.writtenPaths.add(path24);
|
|
8055
8852
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
8056
8853
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
8057
8854
|
}
|
|
@@ -8277,6 +9074,19 @@ async function setupCompaction(params) {
|
|
|
8277
9074
|
return { effectiveMaxContext, autoCompactor };
|
|
8278
9075
|
}
|
|
8279
9076
|
function createAgent(params) {
|
|
9077
|
+
const secretScrubber = params.container.resolve(TOKENS.SecretScrubber);
|
|
9078
|
+
const renderer = params.container.has(TOKENS.Renderer) ? params.container.resolve(TOKENS.Renderer) : void 0;
|
|
9079
|
+
params.container.resolve(TOKENS.Logger);
|
|
9080
|
+
const toolExecutor = new ToolExecutor(params.tools, {
|
|
9081
|
+
permissionPolicy: params.permissionPolicy ?? params.container.resolve(TOKENS.PermissionPolicy),
|
|
9082
|
+
secretScrubber,
|
|
9083
|
+
renderer,
|
|
9084
|
+
events: params.events,
|
|
9085
|
+
confirmAwaiter: params.confirmAwaiter,
|
|
9086
|
+
iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
|
|
9087
|
+
perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
|
|
9088
|
+
tracer: params.tracer
|
|
9089
|
+
});
|
|
8280
9090
|
return new Agent({
|
|
8281
9091
|
container: params.container,
|
|
8282
9092
|
tools: params.tools,
|
|
@@ -8288,9 +9098,144 @@ function createAgent(params) {
|
|
|
8288
9098
|
iterationTimeoutMs: params.config.tools.iterationTimeoutMs,
|
|
8289
9099
|
executionStrategy: params.config.tools.defaultExecutionStrategy,
|
|
8290
9100
|
perIterationOutputCapBytes: params.config.tools.perIterationOutputCapBytes,
|
|
8291
|
-
confirmAwaiter: params.confirmAwaiter
|
|
9101
|
+
confirmAwaiter: params.confirmAwaiter,
|
|
9102
|
+
toolExecutor,
|
|
9103
|
+
tracer: params.tracer
|
|
8292
9104
|
});
|
|
8293
9105
|
}
|
|
9106
|
+
function setupMetrics(params) {
|
|
9107
|
+
const { flags, wpaths, events, logger, config } = params;
|
|
9108
|
+
let metricsSink;
|
|
9109
|
+
let healthRegistry;
|
|
9110
|
+
let metricsServerHandle;
|
|
9111
|
+
const metricsPortFlag = flags["metrics-port"];
|
|
9112
|
+
const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
|
|
9113
|
+
if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
|
|
9114
|
+
if (!flags.metrics) return { metricsSink, healthRegistry, metricsServerHandle };
|
|
9115
|
+
metricsSink = new InMemoryMetricsSink();
|
|
9116
|
+
wireMetricsToEvents(events, metricsSink);
|
|
9117
|
+
healthRegistry = new DefaultHealthRegistry();
|
|
9118
|
+
healthRegistry.register({
|
|
9119
|
+
name: "session-store",
|
|
9120
|
+
check: async () => {
|
|
9121
|
+
try {
|
|
9122
|
+
await fsp2.access(wpaths.projectSessions);
|
|
9123
|
+
return { status: "healthy" };
|
|
9124
|
+
} catch (e) {
|
|
9125
|
+
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
});
|
|
9129
|
+
healthRegistry.register({
|
|
9130
|
+
name: "provider",
|
|
9131
|
+
check: async () => ({
|
|
9132
|
+
status: "healthy",
|
|
9133
|
+
data: { id: config.provider, model: config.model }
|
|
9134
|
+
})
|
|
9135
|
+
});
|
|
9136
|
+
const dumpMetrics = () => {
|
|
9137
|
+
if (!metricsSink) return;
|
|
9138
|
+
try {
|
|
9139
|
+
const out = path23.join(wpaths.projectSessions, "metrics.json");
|
|
9140
|
+
const snap = metricsSink.snapshot();
|
|
9141
|
+
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
9142
|
+
} catch {
|
|
9143
|
+
}
|
|
9144
|
+
};
|
|
9145
|
+
process.on("exit", dumpMetrics);
|
|
9146
|
+
if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
|
|
9147
|
+
try {
|
|
9148
|
+
metricsServerHandle = startMetricsServer({
|
|
9149
|
+
port: metricsPort,
|
|
9150
|
+
host: process.env["METRICS_HOST"] ?? "127.0.0.1",
|
|
9151
|
+
sink: metricsSink,
|
|
9152
|
+
healthRegistry
|
|
9153
|
+
});
|
|
9154
|
+
logger.info(
|
|
9155
|
+
`metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`
|
|
9156
|
+
);
|
|
9157
|
+
process.on("exit", () => {
|
|
9158
|
+
void metricsServerHandle?.close().catch(() => {
|
|
9159
|
+
});
|
|
9160
|
+
});
|
|
9161
|
+
} catch (err) {
|
|
9162
|
+
logger.warn(
|
|
9163
|
+
`metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
9164
|
+
);
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
return { metricsSink, healthRegistry, metricsServerHandle };
|
|
9168
|
+
}
|
|
9169
|
+
function createApi(ownerName, base) {
|
|
9170
|
+
return new DefaultPluginAPI({ ownerName, ...base });
|
|
9171
|
+
}
|
|
9172
|
+
|
|
9173
|
+
// src/wiring/plugins.ts
|
|
9174
|
+
async function setupPlugins(params) {
|
|
9175
|
+
const {
|
|
9176
|
+
config,
|
|
9177
|
+
container,
|
|
9178
|
+
events,
|
|
9179
|
+
toolRegistry,
|
|
9180
|
+
providerRegistry,
|
|
9181
|
+
slashCommandRegistry,
|
|
9182
|
+
mcpRegistry,
|
|
9183
|
+
log,
|
|
9184
|
+
agent,
|
|
9185
|
+
sessionWriter,
|
|
9186
|
+
metricsSink,
|
|
9187
|
+
configStore,
|
|
9188
|
+
pipelines
|
|
9189
|
+
} = params;
|
|
9190
|
+
if (!config.features.plugins || !config.plugins || config.plugins.length === 0) return;
|
|
9191
|
+
const resolvedPlugins = [];
|
|
9192
|
+
for (const p of config.plugins) {
|
|
9193
|
+
if (typeof p === "object" && p.enabled === false) continue;
|
|
9194
|
+
const spec = typeof p === "string" ? p : p.name;
|
|
9195
|
+
try {
|
|
9196
|
+
const mod = await import(spec);
|
|
9197
|
+
if (mod.default) resolvedPlugins.push(mod.default);
|
|
9198
|
+
} catch (err) {
|
|
9199
|
+
log.warn(`Plugin "${spec}" failed to load`, err);
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
9202
|
+
if (resolvedPlugins.length === 0) return;
|
|
9203
|
+
const pluginOptions = buildPluginOptions(config);
|
|
9204
|
+
const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
|
|
9205
|
+
await loadPlugins(resolvedPlugins, {
|
|
9206
|
+
log,
|
|
9207
|
+
pluginOptions,
|
|
9208
|
+
apiFactory: (plugin) => createApi(plugin.name, {
|
|
9209
|
+
container,
|
|
9210
|
+
events,
|
|
9211
|
+
pipelines,
|
|
9212
|
+
toolRegistry,
|
|
9213
|
+
providerRegistry,
|
|
9214
|
+
slashCommandRegistry,
|
|
9215
|
+
mcpRegistry,
|
|
9216
|
+
config: pluginConfig,
|
|
9217
|
+
log,
|
|
9218
|
+
extensions: agent.extensions,
|
|
9219
|
+
sessionWriter: {
|
|
9220
|
+
transcriptPath: sessionWriter.transcriptPath,
|
|
9221
|
+
append: (e) => sessionWriter.append(e)
|
|
9222
|
+
},
|
|
9223
|
+
metricsSink,
|
|
9224
|
+
configStore
|
|
9225
|
+
})
|
|
9226
|
+
});
|
|
9227
|
+
}
|
|
9228
|
+
function buildPluginOptions(config) {
|
|
9229
|
+
const options = {};
|
|
9230
|
+
for (const entry of config.plugins ?? []) {
|
|
9231
|
+
if (typeof entry !== "object") continue;
|
|
9232
|
+
if (entry.options) options[entry.name] = { ...entry.options };
|
|
9233
|
+
}
|
|
9234
|
+
for (const [name, value] of Object.entries(config.extensions ?? {})) {
|
|
9235
|
+
options[name] = { ...options[name] ?? {}, ...value };
|
|
9236
|
+
}
|
|
9237
|
+
return options;
|
|
9238
|
+
}
|
|
8294
9239
|
async function setupProvider(params) {
|
|
8295
9240
|
const { config, modelsRegistry, logger } = params;
|
|
8296
9241
|
const savedProviderCfg = config.providers?.[config.provider];
|
|
@@ -8382,12 +9327,12 @@ async function setupSession(params) {
|
|
|
8382
9327
|
}
|
|
8383
9328
|
const sessionRef = { current: session };
|
|
8384
9329
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
8385
|
-
const attachments = new DefaultAttachmentStore({ spoolDir:
|
|
8386
|
-
const queueStore = new QueueStore({ dir:
|
|
9330
|
+
const attachments = new DefaultAttachmentStore({ spoolDir: path23.join(wpaths.projectSessions, session.id, "attachments") });
|
|
9331
|
+
const queueStore = new QueueStore({ dir: path23.join(wpaths.projectSessions, session.id) });
|
|
8387
9332
|
const ctxSignal = new AbortController().signal;
|
|
8388
9333
|
const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
|
|
8389
9334
|
if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
|
|
8390
|
-
const todosCheckpointPath =
|
|
9335
|
+
const todosCheckpointPath = path23.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
8391
9336
|
if (resumeId) {
|
|
8392
9337
|
try {
|
|
8393
9338
|
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
@@ -8399,13 +9344,13 @@ async function setupSession(params) {
|
|
|
8399
9344
|
}
|
|
8400
9345
|
}
|
|
8401
9346
|
const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
|
|
8402
|
-
const planPath =
|
|
9347
|
+
const planPath = path23.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
8403
9348
|
context.state.setMeta("plan.path", planPath);
|
|
8404
9349
|
let dirState;
|
|
8405
9350
|
if (resumeId) {
|
|
8406
9351
|
try {
|
|
8407
|
-
const fleetRoot =
|
|
8408
|
-
dirState = await loadDirectorState(
|
|
9352
|
+
const fleetRoot = path23.join(wpaths.projectSessions, session.id);
|
|
9353
|
+
dirState = await loadDirectorState(path23.join(fleetRoot, "director-state.json"));
|
|
8409
9354
|
if (dirState) {
|
|
8410
9355
|
const tCounts = {};
|
|
8411
9356
|
for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
@@ -8432,22 +9377,11 @@ function resolveBundledSkillsDir2() {
|
|
|
8432
9377
|
try {
|
|
8433
9378
|
const req2 = createRequire(import.meta.url);
|
|
8434
9379
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
8435
|
-
return
|
|
9380
|
+
return path23.join(path23.dirname(corePkg), "skills");
|
|
8436
9381
|
} catch {
|
|
8437
9382
|
return void 0;
|
|
8438
9383
|
}
|
|
8439
9384
|
}
|
|
8440
|
-
function buildPluginOptions(config) {
|
|
8441
|
-
const options = {};
|
|
8442
|
-
for (const entry of config.plugins ?? []) {
|
|
8443
|
-
if (typeof entry !== "object") continue;
|
|
8444
|
-
if (entry.options) options[entry.name] = { ...entry.options };
|
|
8445
|
-
}
|
|
8446
|
-
for (const [name, value] of Object.entries(config.extensions ?? {})) {
|
|
8447
|
-
options[name] = { ...options[name] ?? {}, ...value };
|
|
8448
|
-
}
|
|
8449
|
-
return options;
|
|
8450
|
-
}
|
|
8451
9385
|
async function main(argv) {
|
|
8452
9386
|
const ctx = await boot(argv);
|
|
8453
9387
|
if (typeof ctx === "number") return ctx;
|
|
@@ -8530,6 +9464,8 @@ async function main(argv) {
|
|
|
8530
9464
|
const memoryStore = container.resolve(TOKENS.MemoryStore);
|
|
8531
9465
|
const skillLoader = container.resolve(TOKENS.SkillLoader);
|
|
8532
9466
|
const sessionRef = {};
|
|
9467
|
+
const autonomyModeRef = { current: "off" };
|
|
9468
|
+
const goalPathForPrompt = path23.join(projectRoot, ".wrongstack", "goal.json");
|
|
8533
9469
|
container.bind(
|
|
8534
9470
|
TOKENS.SystemPromptBuilder,
|
|
8535
9471
|
() => new DefaultSystemPromptBuilder({
|
|
@@ -8539,7 +9475,17 @@ async function main(argv) {
|
|
|
8539
9475
|
modeId,
|
|
8540
9476
|
modePrompt,
|
|
8541
9477
|
modelCapabilities,
|
|
8542
|
-
planPath: () => sessionRef.current ?
|
|
9478
|
+
planPath: () => sessionRef.current ? path23.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0,
|
|
9479
|
+
contributors: [
|
|
9480
|
+
// Injects the ETERNAL AUTONOMY block when the user has activated
|
|
9481
|
+
// `/autonomy eternal`. Without this, the per-iteration directive
|
|
9482
|
+
// is the only place the model sees the rules — compaction can
|
|
9483
|
+
// drop it and the model forgets it's in autonomy mode.
|
|
9484
|
+
makeAutonomyPromptContributor({
|
|
9485
|
+
goalPath: goalPathForPrompt,
|
|
9486
|
+
enabled: () => autonomyModeRef.current === "eternal"
|
|
9487
|
+
})
|
|
9488
|
+
]
|
|
8543
9489
|
})
|
|
8544
9490
|
);
|
|
8545
9491
|
const toolRegistry = new ToolRegistry();
|
|
@@ -8553,72 +9499,10 @@ async function main(argv) {
|
|
|
8553
9499
|
}
|
|
8554
9500
|
const events = new EventBus();
|
|
8555
9501
|
events.setLogger(logger);
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
|
|
8561
|
-
if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
|
|
8562
|
-
if (flags.metrics) {
|
|
8563
|
-
metricsSink = new InMemoryMetricsSink();
|
|
8564
|
-
wireMetricsToEvents(events, metricsSink);
|
|
8565
|
-
healthRegistry = new DefaultHealthRegistry();
|
|
8566
|
-
healthRegistry.register({
|
|
8567
|
-
name: "session-store",
|
|
8568
|
-
check: async () => {
|
|
8569
|
-
try {
|
|
8570
|
-
await fsp2.access(wpaths.projectSessions);
|
|
8571
|
-
return { status: "healthy" };
|
|
8572
|
-
} catch (e) {
|
|
8573
|
-
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
8574
|
-
}
|
|
8575
|
-
}
|
|
8576
|
-
});
|
|
8577
|
-
healthRegistry.register({
|
|
8578
|
-
name: "provider",
|
|
8579
|
-
check: async () => ({
|
|
8580
|
-
status: "healthy",
|
|
8581
|
-
data: { id: config.provider, model: config.model }
|
|
8582
|
-
})
|
|
8583
|
-
});
|
|
8584
|
-
const dumpMetrics = () => {
|
|
8585
|
-
if (!metricsSink) return;
|
|
8586
|
-
try {
|
|
8587
|
-
const out = path21.join(wpaths.projectSessions, "metrics.json");
|
|
8588
|
-
const snap = metricsSink.snapshot();
|
|
8589
|
-
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
8590
|
-
} catch {
|
|
8591
|
-
}
|
|
8592
|
-
};
|
|
8593
|
-
process.on("exit", dumpMetrics);
|
|
8594
|
-
process.on("SIGINT", () => {
|
|
8595
|
-
dumpMetrics();
|
|
8596
|
-
process.exit(130);
|
|
8597
|
-
});
|
|
8598
|
-
if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
|
|
8599
|
-
try {
|
|
8600
|
-
metricsServerHandle = await startMetricsServer({
|
|
8601
|
-
port: metricsPort,
|
|
8602
|
-
host: process.env.METRICS_HOST ?? "127.0.0.1",
|
|
8603
|
-
sink: metricsSink,
|
|
8604
|
-
// V2-C: mount /healthz on the same listener so k8s probes can
|
|
8605
|
-
// hit one endpoint per pod for both observability and liveness.
|
|
8606
|
-
healthRegistry
|
|
8607
|
-
});
|
|
8608
|
-
logger.info(
|
|
8609
|
-
`metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`
|
|
8610
|
-
);
|
|
8611
|
-
process.on("exit", () => {
|
|
8612
|
-
void metricsServerHandle?.close().catch(() => {
|
|
8613
|
-
});
|
|
8614
|
-
});
|
|
8615
|
-
} catch (err) {
|
|
8616
|
-
logger.warn(
|
|
8617
|
-
`metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
8618
|
-
);
|
|
8619
|
-
}
|
|
8620
|
-
}
|
|
8621
|
-
}
|
|
9502
|
+
const { metricsSink, healthRegistry, metricsServerHandle } = (() => {
|
|
9503
|
+
const ms = setupMetrics({ flags, wpaths, events, logger, config: { provider: config.provider, model: config.model } });
|
|
9504
|
+
return ms;
|
|
9505
|
+
})();
|
|
8622
9506
|
const spinner = new Spinner();
|
|
8623
9507
|
let lastInputTokens = 0;
|
|
8624
9508
|
events.on("provider.response", (e) => {
|
|
@@ -8737,49 +9621,21 @@ async function main(argv) {
|
|
|
8737
9621
|
}
|
|
8738
9622
|
}
|
|
8739
9623
|
const slashRegistry = new SlashCommandRegistry();
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
const pluginConfig = Object.keys(pluginOptions).length > 0 ? patchConfig(config, { extensions: pluginOptions }) : config;
|
|
8756
|
-
await loadPlugins(resolvedPlugins, {
|
|
8757
|
-
log: logger,
|
|
8758
|
-
// Each plugin's `configSchema` is validated against merged
|
|
8759
|
-
// options from `plugins[].options` and `extensions[name]`.
|
|
8760
|
-
// The merged view is also exposed as `api.config.extensions`.
|
|
8761
|
-
pluginOptions,
|
|
8762
|
-
apiFactory: (plugin) => createApi2(plugin.name, {
|
|
8763
|
-
container,
|
|
8764
|
-
events,
|
|
8765
|
-
pipelines,
|
|
8766
|
-
toolRegistry,
|
|
8767
|
-
providerRegistry,
|
|
8768
|
-
slashCommandRegistry: slashRegistry,
|
|
8769
|
-
mcpRegistry,
|
|
8770
|
-
config: pluginConfig,
|
|
8771
|
-
log: logger,
|
|
8772
|
-
extensions: agent.extensions,
|
|
8773
|
-
sessionWriter: {
|
|
8774
|
-
transcriptPath: context.session.transcriptPath,
|
|
8775
|
-
append: (e) => context.session.append(e)
|
|
8776
|
-
},
|
|
8777
|
-
metricsSink,
|
|
8778
|
-
configStore
|
|
8779
|
-
})
|
|
8780
|
-
});
|
|
8781
|
-
}
|
|
8782
|
-
}
|
|
9624
|
+
await setupPlugins({
|
|
9625
|
+
config,
|
|
9626
|
+
container,
|
|
9627
|
+
events,
|
|
9628
|
+
pipelines,
|
|
9629
|
+
toolRegistry,
|
|
9630
|
+
providerRegistry,
|
|
9631
|
+
slashCommandRegistry: slashRegistry,
|
|
9632
|
+
mcpRegistry,
|
|
9633
|
+
log: logger,
|
|
9634
|
+
agent,
|
|
9635
|
+
sessionWriter: context.session,
|
|
9636
|
+
metricsSink,
|
|
9637
|
+
configStore
|
|
9638
|
+
});
|
|
8783
9639
|
const switchProviderAndModel = (providerId, modelId) => {
|
|
8784
9640
|
try {
|
|
8785
9641
|
const savedCfg = config.providers?.[providerId];
|
|
@@ -8802,14 +9658,27 @@ async function main(argv) {
|
|
|
8802
9658
|
}
|
|
8803
9659
|
};
|
|
8804
9660
|
const directorMode = flags["director"] === true || typeof flags["resume"] === "string";
|
|
9661
|
+
const maxConcurrentFromFlag = typeof flags["max-concurrent"] === "string" ? Number.parseInt(flags["max-concurrent"], 10) : void 0;
|
|
9662
|
+
const maxConcurrentFromEnv = typeof process.env["WRONGSTACK_MAX_CONCURRENT"] === "string" ? Number.parseInt(process.env["WRONGSTACK_MAX_CONCURRENT"], 10) : void 0;
|
|
9663
|
+
const maxConcurrent = Number.isFinite(maxConcurrentFromFlag) && maxConcurrentFromFlag > 0 ? maxConcurrentFromFlag : Number.isFinite(maxConcurrentFromEnv) && maxConcurrentFromEnv > 0 ? maxConcurrentFromEnv : void 0;
|
|
8805
9664
|
let director = null;
|
|
8806
9665
|
let autonomyMode = "off";
|
|
8807
|
-
|
|
8808
|
-
const
|
|
8809
|
-
const
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
9666
|
+
let eternalEngine = null;
|
|
9667
|
+
const eternalListeners = /* @__PURE__ */ new Set();
|
|
9668
|
+
const broadcastEternalIteration = (entry) => {
|
|
9669
|
+
for (const fn of eternalListeners) {
|
|
9670
|
+
try {
|
|
9671
|
+
fn(entry);
|
|
9672
|
+
} catch {
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
};
|
|
9676
|
+
const fleetRoot = directorMode ? path23.join(wpaths.projectSessions, session.id) : void 0;
|
|
9677
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path23.join(fleetRoot, "fleet.json") : void 0;
|
|
9678
|
+
const sharedScratchpadPath = directorMode ? path23.join(fleetRoot, "shared") : void 0;
|
|
9679
|
+
const subagentSessionsRoot = directorMode ? path23.join(fleetRoot, "subagents") : void 0;
|
|
9680
|
+
const stateCheckpointPath = directorMode ? path23.join(fleetRoot, "director-state.json") : void 0;
|
|
9681
|
+
const fleetRootForPromotion = path23.join(wpaths.projectSessions, session.id);
|
|
8813
9682
|
const multiAgentHost = new MultiAgentHost(
|
|
8814
9683
|
{
|
|
8815
9684
|
container,
|
|
@@ -8821,7 +9690,8 @@ async function main(argv) {
|
|
|
8821
9690
|
session,
|
|
8822
9691
|
tokenCounter,
|
|
8823
9692
|
projectRoot,
|
|
8824
|
-
cwd
|
|
9693
|
+
cwd,
|
|
9694
|
+
secretScrubber: container.resolve(TOKENS.SecretScrubber)
|
|
8825
9695
|
},
|
|
8826
9696
|
{
|
|
8827
9697
|
directorMode,
|
|
@@ -8831,7 +9701,8 @@ async function main(argv) {
|
|
|
8831
9701
|
directorRunId: session.id,
|
|
8832
9702
|
fleetRoot: fleetRootForPromotion,
|
|
8833
9703
|
stateCheckpointPath,
|
|
8834
|
-
sessionWriter: session
|
|
9704
|
+
sessionWriter: session,
|
|
9705
|
+
maxConcurrent
|
|
8835
9706
|
}
|
|
8836
9707
|
);
|
|
8837
9708
|
toolRegistry.register(
|
|
@@ -8846,6 +9717,13 @@ async function main(argv) {
|
|
|
8846
9717
|
directorRunId: session.id
|
|
8847
9718
|
})
|
|
8848
9719
|
);
|
|
9720
|
+
toolRegistry.register(
|
|
9721
|
+
createMcpControlTool({
|
|
9722
|
+
getConfig: () => configStore.get(),
|
|
9723
|
+
configPath: wpaths.globalConfig,
|
|
9724
|
+
registry: mcpRegistry
|
|
9725
|
+
})
|
|
9726
|
+
);
|
|
8849
9727
|
if (directorMode) {
|
|
8850
9728
|
director = await multiAgentHost.ensureDirector();
|
|
8851
9729
|
if (director) {
|
|
@@ -8868,6 +9746,20 @@ async function main(argv) {
|
|
|
8868
9746
|
this.enabled = enabled;
|
|
8869
9747
|
}
|
|
8870
9748
|
};
|
|
9749
|
+
const statuslineConfigDeps = {
|
|
9750
|
+
get: () => loadStatuslineConfig(),
|
|
9751
|
+
set: (cfg) => saveStatuslineConfig(cfg)
|
|
9752
|
+
};
|
|
9753
|
+
const hiddenItemsFromConfig = await loadStatuslineConfig();
|
|
9754
|
+
const hiddenItemsList = [];
|
|
9755
|
+
const ALL_ITEMS = ["todos", "plan", "fleet", "git", "elapsed", "context", "cost"];
|
|
9756
|
+
for (const k of ALL_ITEMS) {
|
|
9757
|
+
if (!hiddenItemsFromConfig[k]) hiddenItemsList.push(k);
|
|
9758
|
+
}
|
|
9759
|
+
const statuslineHiddenItems = hiddenItemsList;
|
|
9760
|
+
[...statuslineHiddenItems];
|
|
9761
|
+
const setStatuslineHiddenItems = (items) => {
|
|
9762
|
+
};
|
|
8871
9763
|
const slashCmds = buildBuiltinSlashCommands({
|
|
8872
9764
|
registry: slashRegistry,
|
|
8873
9765
|
toolRegistry,
|
|
@@ -8887,6 +9779,7 @@ async function main(argv) {
|
|
|
8887
9779
|
fleetStreamController,
|
|
8888
9780
|
llmProvider: provider,
|
|
8889
9781
|
llmModel: config.model,
|
|
9782
|
+
statuslineConfig: statuslineConfigDeps,
|
|
8890
9783
|
onSpawn: async (description, spawnOpts) => {
|
|
8891
9784
|
const { subagentId, taskId } = await multiAgentHost.spawn(description, spawnOpts);
|
|
8892
9785
|
const tags = [];
|
|
@@ -8990,10 +9883,26 @@ async function main(argv) {
|
|
|
8990
9883
|
}
|
|
8991
9884
|
return `Manifest written \u2192 ${p}`;
|
|
8992
9885
|
}
|
|
9886
|
+
if (action === "concurrency") {
|
|
9887
|
+
const current = multiAgentHost.getMaxConcurrent();
|
|
9888
|
+
if (!target) {
|
|
9889
|
+
return `Concurrent-subagent ceiling: ${current}`;
|
|
9890
|
+
}
|
|
9891
|
+
const n = Number.parseInt(target, 10);
|
|
9892
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
9893
|
+
return `Invalid value "${target}". Concurrency must be an integer >= 1.`;
|
|
9894
|
+
}
|
|
9895
|
+
try {
|
|
9896
|
+
multiAgentHost.setMaxConcurrent(n);
|
|
9897
|
+
} catch (err) {
|
|
9898
|
+
return err instanceof Error ? err.message : String(err);
|
|
9899
|
+
}
|
|
9900
|
+
return `Concurrent-subagent ceiling: ${current} \u2192 ${n}`;
|
|
9901
|
+
}
|
|
8993
9902
|
return `Unknown fleet action: ${action}`;
|
|
8994
9903
|
},
|
|
8995
9904
|
onFleetLog: async (subagentId, mode) => {
|
|
8996
|
-
const subagentsRoot =
|
|
9905
|
+
const subagentsRoot = path23.join(fleetRootForPromotion, "subagents");
|
|
8997
9906
|
let runDirs;
|
|
8998
9907
|
try {
|
|
8999
9908
|
runDirs = await fsp2.readdir(subagentsRoot);
|
|
@@ -9002,7 +9911,7 @@ async function main(argv) {
|
|
|
9002
9911
|
}
|
|
9003
9912
|
const found = [];
|
|
9004
9913
|
for (const runId of runDirs) {
|
|
9005
|
-
const runDir =
|
|
9914
|
+
const runDir = path23.join(subagentsRoot, runId);
|
|
9006
9915
|
let files;
|
|
9007
9916
|
try {
|
|
9008
9917
|
files = await fsp2.readdir(runDir);
|
|
@@ -9011,7 +9920,7 @@ async function main(argv) {
|
|
|
9011
9920
|
}
|
|
9012
9921
|
for (const f of files) {
|
|
9013
9922
|
if (!f.endsWith(".jsonl")) continue;
|
|
9014
|
-
const full =
|
|
9923
|
+
const full = path23.join(runDir, f);
|
|
9015
9924
|
try {
|
|
9016
9925
|
const stat3 = await fsp2.stat(full);
|
|
9017
9926
|
found.push({
|
|
@@ -9108,7 +10017,7 @@ async function main(argv) {
|
|
|
9108
10017
|
}
|
|
9109
10018
|
const dir = await multiAgentHost.ensureDirector();
|
|
9110
10019
|
if (!dir) return "Director is not available.";
|
|
9111
|
-
const dirStatePath =
|
|
10020
|
+
const dirStatePath = path23.join(fleetRootForPromotion, "director-state.json");
|
|
9112
10021
|
const prior = await loadDirectorState(dirStatePath);
|
|
9113
10022
|
if (!prior) {
|
|
9114
10023
|
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
@@ -9179,9 +10088,9 @@ async function main(argv) {
|
|
|
9179
10088
|
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
9180
10089
|
toolRegistry.register(tool);
|
|
9181
10090
|
}
|
|
9182
|
-
const mp =
|
|
9183
|
-
const sp =
|
|
9184
|
-
const ss =
|
|
10091
|
+
const mp = path23.join(fleetRootForPromotion, "fleet.json");
|
|
10092
|
+
const sp = path23.join(fleetRootForPromotion, "shared");
|
|
10093
|
+
const ss = path23.join(fleetRootForPromotion, "subagents");
|
|
9185
10094
|
const lines = [
|
|
9186
10095
|
`${color.green("\u2713")} Promoted to director mode.`,
|
|
9187
10096
|
` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
|
|
@@ -9208,6 +10117,21 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
9208
10117
|
}
|
|
9209
10118
|
return result.message;
|
|
9210
10119
|
},
|
|
10120
|
+
onMcp: async (args) => {
|
|
10121
|
+
const parsed = parseMcpArgs(args);
|
|
10122
|
+
if (!parsed) {
|
|
10123
|
+
return [
|
|
10124
|
+
"Usage: /mcp [list|add <name>|remove <name>|enable <name>|disable <name>|restart <name>]",
|
|
10125
|
+
"Run `/mcp` without args to see available servers."
|
|
10126
|
+
].join("\n");
|
|
10127
|
+
}
|
|
10128
|
+
return runMcpManagementCommand(parsed, {
|
|
10129
|
+
config,
|
|
10130
|
+
configPath: wpaths.globalConfig,
|
|
10131
|
+
mcpRegistry,
|
|
10132
|
+
allServerPresets: allServers$1()
|
|
10133
|
+
});
|
|
10134
|
+
},
|
|
9211
10135
|
onYolo: (setTo) => {
|
|
9212
10136
|
const policy = container.resolve(TOKENS.PermissionPolicy);
|
|
9213
10137
|
if (setTo !== void 0) {
|
|
@@ -9220,10 +10144,31 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
9220
10144
|
onAutonomy: (setTo) => {
|
|
9221
10145
|
if (setTo !== void 0) {
|
|
9222
10146
|
autonomyMode = setTo;
|
|
10147
|
+
autonomyModeRef.current = setTo;
|
|
9223
10148
|
return setTo;
|
|
9224
10149
|
}
|
|
9225
10150
|
return autonomyMode;
|
|
9226
10151
|
},
|
|
10152
|
+
onEternalStart: () => {
|
|
10153
|
+
if (!eternalEngine) {
|
|
10154
|
+
eternalEngine = new EternalAutonomyEngine({
|
|
10155
|
+
agent,
|
|
10156
|
+
projectRoot,
|
|
10157
|
+
// Wire the same compactor the manual /compact command uses so
|
|
10158
|
+
// multi-day eternal loops don't overflow the provider's context.
|
|
10159
|
+
// effectiveMaxContext is set up earlier with a model-specific
|
|
10160
|
+
// value; pass it through so aggressive-mode compact triggers
|
|
10161
|
+
// before the next iteration would actually overflow.
|
|
10162
|
+
compactor: container.resolve(TOKENS.Compactor),
|
|
10163
|
+
maxContextTokens: effectiveMaxContext > 0 ? effectiveMaxContext : void 0,
|
|
10164
|
+
onIteration: broadcastEternalIteration
|
|
10165
|
+
});
|
|
10166
|
+
}
|
|
10167
|
+
void eternalEngine.prime();
|
|
10168
|
+
},
|
|
10169
|
+
onEternalStop: () => {
|
|
10170
|
+
eternalEngine?.stop();
|
|
10171
|
+
},
|
|
9227
10172
|
onExit: () => {
|
|
9228
10173
|
void mcpRegistry.stopAll();
|
|
9229
10174
|
},
|
|
@@ -9281,6 +10226,30 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
9281
10226
|
}
|
|
9282
10227
|
});
|
|
9283
10228
|
for (const cmd of slashCmds) slashRegistry.register(cmd);
|
|
10229
|
+
const eternalFlag = typeof flags["eternal"] === "string" ? flags["eternal"].trim() : "";
|
|
10230
|
+
if (eternalFlag.length > 0) {
|
|
10231
|
+
const { saveGoal: saveGoal2, emptyGoal: emptyGoal2, goalFilePath: goalFilePath4, loadGoal: loadGoal4 } = await import('@wrongstack/core');
|
|
10232
|
+
const goalPath = goalFilePath4(projectRoot);
|
|
10233
|
+
const prior = await loadGoal4(goalPath);
|
|
10234
|
+
const next = prior ? { ...prior, goal: eternalFlag, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal2(eternalFlag);
|
|
10235
|
+
await saveGoal2(goalPath, next);
|
|
10236
|
+
const policy = container.resolve(TOKENS.PermissionPolicy);
|
|
10237
|
+
policy.setYolo(true);
|
|
10238
|
+
config = patchConfig(config, { yolo: true });
|
|
10239
|
+
eternalEngine = new EternalAutonomyEngine({
|
|
10240
|
+
agent,
|
|
10241
|
+
projectRoot,
|
|
10242
|
+
compactor: container.resolve(TOKENS.Compactor),
|
|
10243
|
+
maxContextTokens: effectiveMaxContext > 0 ? effectiveMaxContext : void 0,
|
|
10244
|
+
onIteration: broadcastEternalIteration
|
|
10245
|
+
});
|
|
10246
|
+
await eternalEngine.prime();
|
|
10247
|
+
autonomyMode = "eternal";
|
|
10248
|
+
autonomyModeRef.current = "eternal";
|
|
10249
|
+
renderer.write(
|
|
10250
|
+
color.red("Eternal mode launching from --eternal flag.") + color.dim(` Goal: ${eternalFlag.slice(0, 80)}${eternalFlag.length > 80 ? "\u2026" : ""}`) + "\n"
|
|
10251
|
+
);
|
|
10252
|
+
}
|
|
9284
10253
|
const savedProviderCfg = config.providers?.[config.provider];
|
|
9285
10254
|
return execute({
|
|
9286
10255
|
agent,
|
|
@@ -9311,11 +10280,18 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
9311
10280
|
director: director ?? null,
|
|
9312
10281
|
fleetRoster: FLEET_ROSTER,
|
|
9313
10282
|
fleetStreamController,
|
|
10283
|
+
statuslineHiddenItems,
|
|
10284
|
+
setStatuslineHiddenItems,
|
|
9314
10285
|
getYolo: () => {
|
|
9315
10286
|
const policy = container.resolve(TOKENS.PermissionPolicy);
|
|
9316
10287
|
return policy.getYolo();
|
|
9317
10288
|
},
|
|
9318
10289
|
getAutonomy: () => autonomyMode,
|
|
10290
|
+
getEternalEngine: () => eternalEngine,
|
|
10291
|
+
subscribeEternalIteration: (fn) => {
|
|
10292
|
+
eternalListeners.add(fn);
|
|
10293
|
+
return () => eternalListeners.delete(fn);
|
|
10294
|
+
},
|
|
9319
10295
|
skillLoader: config.features.skills ? skillLoader : void 0
|
|
9320
10296
|
});
|
|
9321
10297
|
}
|