@wrongstack/cli 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +480 -124
- 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
|
|
3
|
-
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
2
|
+
import * as path18 from 'path';
|
|
3
|
+
import { color, allServers, DefaultPathResolver, TOKENS, DefaultSystemPromptBuilder, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, SlashCommandRegistry, loadPlugins, createDelegateTool, FLEET_ROSTER, DefaultLogger, DefaultModelsRegistry, ProviderRegistry, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, AutoCompactionMiddleware, estimateRequestTokens, Agent, makeDirectorSessionFactory, Director, DefaultMultiAgentCoordinator, makeAgentSubagentRunner, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, atomicWrite, AutoApprovePermissionPolicy, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, resolveContextWindowPolicy, formatTodosList, emptyPlan, clearPlan, savePlan, removePlanItem, formatPlan, setPlanItemStatus, addPlanItem, SpecStore, TaskGraphStore, SpecVersioning, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, InputBuilder, projectHash, decryptConfigSecrets, encryptConfigSecrets as encryptConfigSecrets$1, DefaultPluginAPI } from '@wrongstack/core';
|
|
4
4
|
import * as crypto from 'crypto';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
|
-
import * as
|
|
6
|
+
import * as fs5 from 'fs/promises';
|
|
7
7
|
import { DefaultSecretVault as DefaultSecretVault$1, encryptConfigSecrets, decryptConfigSecrets as decryptConfigSecrets$1 } from '@wrongstack/core/security';
|
|
8
8
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
9
|
import { writeFileSync } from 'fs';
|
|
@@ -14,6 +14,7 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
|
|
|
14
14
|
import { builtinToolsPack, rememberTool, forgetTool } from '@wrongstack/tools';
|
|
15
15
|
import * as os4 from 'os';
|
|
16
16
|
import * as readline from 'readline';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
17
18
|
import { SkillInstaller } from '@wrongstack/core/skills';
|
|
18
19
|
import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
|
|
19
20
|
|
|
@@ -236,8 +237,8 @@ function buildSddCommand(opts) {
|
|
|
236
237
|
async run(args) {
|
|
237
238
|
const ctx = opts.context;
|
|
238
239
|
const projectRoot = ctx?.projectRoot ?? process.cwd();
|
|
239
|
-
const specsDir =
|
|
240
|
-
const graphsDir =
|
|
240
|
+
const specsDir = path18.join(projectRoot, ".wrongstack", "specs");
|
|
241
|
+
const graphsDir = path18.join(projectRoot, ".wrongstack", "task-graphs");
|
|
241
242
|
const specStore = new SpecStore({ baseDir: specsDir });
|
|
242
243
|
new TaskGraphStore({ baseDir: graphsDir });
|
|
243
244
|
const versioning = new SpecVersioning();
|
|
@@ -253,7 +254,7 @@ function buildSddCommand(opts) {
|
|
|
253
254
|
const forceFlag = rest.includes("--force") || rest.includes("-f");
|
|
254
255
|
const title = rest.filter((a) => !a.startsWith("-")).join(" ").trim() || "Untitled Feature";
|
|
255
256
|
if (!activeBuilder && !forceFlag) {
|
|
256
|
-
const sessionPath =
|
|
257
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
257
258
|
try {
|
|
258
259
|
const fsp = await import('fs/promises');
|
|
259
260
|
await fsp.access(sessionPath);
|
|
@@ -291,7 +292,7 @@ function buildSddCommand(opts) {
|
|
|
291
292
|
projectContext,
|
|
292
293
|
minQuestions: 2,
|
|
293
294
|
maxQuestions: 10,
|
|
294
|
-
sessionPath:
|
|
295
|
+
sessionPath: path18.join(projectRoot, ".wrongstack", "sdd-session.json")
|
|
295
296
|
});
|
|
296
297
|
activeBuilder.startSession(title);
|
|
297
298
|
const aiPrompt = activeBuilder.getAIPrompt();
|
|
@@ -551,7 +552,7 @@ Start executing the tasks one by one.`
|
|
|
551
552
|
};
|
|
552
553
|
}
|
|
553
554
|
case "cancel": {
|
|
554
|
-
const sessionPath =
|
|
555
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
555
556
|
let deletedFromDisk = false;
|
|
556
557
|
try {
|
|
557
558
|
const fsp = await import('fs/promises');
|
|
@@ -577,7 +578,7 @@ Start executing the tasks one by one.`
|
|
|
577
578
|
if (activeBuilder) {
|
|
578
579
|
return { message: "An SDD session is already active. Use /sdd cancel first." };
|
|
579
580
|
}
|
|
580
|
-
const sessionPath =
|
|
581
|
+
const sessionPath = path18.join(projectRoot, ".wrongstack", "sdd-session.json");
|
|
581
582
|
const projectContext = await gatherProjectContext(projectRoot);
|
|
582
583
|
activeBuilder = new AISpecBuilder({
|
|
583
584
|
store: specStore,
|
|
@@ -803,7 +804,7 @@ async function gatherProjectContext(projectRoot) {
|
|
|
803
804
|
const parts = [];
|
|
804
805
|
try {
|
|
805
806
|
const fsp = await import('fs/promises');
|
|
806
|
-
const pkgPath =
|
|
807
|
+
const pkgPath = path18.join(projectRoot, "package.json");
|
|
807
808
|
const pkgRaw = await fsp.readFile(pkgPath, "utf8");
|
|
808
809
|
const pkg = JSON.parse(pkgRaw);
|
|
809
810
|
parts.push(`Project: ${String(pkg.name ?? "unknown")}`);
|
|
@@ -820,14 +821,14 @@ async function gatherProjectContext(projectRoot) {
|
|
|
820
821
|
}
|
|
821
822
|
try {
|
|
822
823
|
const fsp = await import('fs/promises');
|
|
823
|
-
const tsconfigPath =
|
|
824
|
+
const tsconfigPath = path18.join(projectRoot, "tsconfig.json");
|
|
824
825
|
await fsp.access(tsconfigPath);
|
|
825
826
|
parts.push("Language: TypeScript");
|
|
826
827
|
} catch {
|
|
827
828
|
}
|
|
828
829
|
try {
|
|
829
830
|
const fsp = await import('fs/promises');
|
|
830
|
-
const srcDir =
|
|
831
|
+
const srcDir = path18.join(projectRoot, "src");
|
|
831
832
|
const entries = await fsp.readdir(srcDir, { withFileTypes: true });
|
|
832
833
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
833
834
|
if (dirs.length > 0) {
|
|
@@ -1381,7 +1382,7 @@ async function runWebUI(opts) {
|
|
|
1381
1382
|
if (!opts.globalConfigPath) return {};
|
|
1382
1383
|
let raw;
|
|
1383
1384
|
try {
|
|
1384
|
-
raw = await
|
|
1385
|
+
raw = await fs5.readFile(opts.globalConfigPath, "utf8");
|
|
1385
1386
|
} catch {
|
|
1386
1387
|
return {};
|
|
1387
1388
|
}
|
|
@@ -1392,7 +1393,7 @@ async function runWebUI(opts) {
|
|
|
1392
1393
|
return {};
|
|
1393
1394
|
}
|
|
1394
1395
|
if (!parsed.providers) return {};
|
|
1395
|
-
const keyFile =
|
|
1396
|
+
const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
|
|
1396
1397
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1397
1398
|
return decryptConfigSecrets$1(parsed.providers, vault);
|
|
1398
1399
|
}
|
|
@@ -1400,7 +1401,7 @@ async function runWebUI(opts) {
|
|
|
1400
1401
|
if (!opts.globalConfigPath) return;
|
|
1401
1402
|
let raw;
|
|
1402
1403
|
try {
|
|
1403
|
-
raw = await
|
|
1404
|
+
raw = await fs5.readFile(opts.globalConfigPath, "utf8");
|
|
1404
1405
|
} catch {
|
|
1405
1406
|
raw = "{}";
|
|
1406
1407
|
}
|
|
@@ -1411,7 +1412,7 @@ async function runWebUI(opts) {
|
|
|
1411
1412
|
parsed = {};
|
|
1412
1413
|
}
|
|
1413
1414
|
parsed.providers = providers;
|
|
1414
|
-
const keyFile =
|
|
1415
|
+
const keyFile = path18.join(path18.dirname(opts.globalConfigPath), ".key");
|
|
1415
1416
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
1416
1417
|
const encrypted = encryptConfigSecrets(parsed, vault);
|
|
1417
1418
|
await atomicWrite(opts.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
@@ -1563,7 +1564,7 @@ function parseSpawnFlags(input) {
|
|
|
1563
1564
|
return { description: rest.trim(), opts };
|
|
1564
1565
|
}
|
|
1565
1566
|
async function bootConfig(flags) {
|
|
1566
|
-
const cwd = typeof flags["cwd"] === "string" ?
|
|
1567
|
+
const cwd = typeof flags["cwd"] === "string" ? path18.resolve(flags["cwd"]) : process.cwd();
|
|
1567
1568
|
const pathResolver = new DefaultPathResolver(cwd);
|
|
1568
1569
|
const projectRoot = pathResolver.projectRoot;
|
|
1569
1570
|
const userHome = os4.homedir();
|
|
@@ -1614,13 +1615,13 @@ function flagsToConfigPatch(flags) {
|
|
|
1614
1615
|
}
|
|
1615
1616
|
async function ensureProjectMeta(paths, projectRoot) {
|
|
1616
1617
|
try {
|
|
1617
|
-
await
|
|
1618
|
+
await fs5.mkdir(paths.projectDir, { recursive: true });
|
|
1618
1619
|
const meta = {
|
|
1619
1620
|
hash: paths.projectHash,
|
|
1620
1621
|
root: projectRoot,
|
|
1621
1622
|
lastSeen: (/* @__PURE__ */ new Date()).toISOString()
|
|
1622
1623
|
};
|
|
1623
|
-
await
|
|
1624
|
+
await fs5.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
|
|
1624
1625
|
} catch {
|
|
1625
1626
|
}
|
|
1626
1627
|
}
|
|
@@ -1630,11 +1631,11 @@ var ReadlineInputReader = class {
|
|
|
1630
1631
|
history = [];
|
|
1631
1632
|
pending = false;
|
|
1632
1633
|
constructor(opts = {}) {
|
|
1633
|
-
this.historyFile = opts.historyFile ??
|
|
1634
|
+
this.historyFile = opts.historyFile ?? path18.join(os4.homedir(), ".wrongstack", "history");
|
|
1634
1635
|
}
|
|
1635
1636
|
async loadHistory() {
|
|
1636
1637
|
try {
|
|
1637
|
-
const raw = await
|
|
1638
|
+
const raw = await fs5.readFile(this.historyFile, "utf8");
|
|
1638
1639
|
this.history = raw.split("\n").filter(Boolean).slice(-1e3);
|
|
1639
1640
|
} catch {
|
|
1640
1641
|
this.history = [];
|
|
@@ -1642,8 +1643,8 @@ var ReadlineInputReader = class {
|
|
|
1642
1643
|
}
|
|
1643
1644
|
async saveHistory() {
|
|
1644
1645
|
try {
|
|
1645
|
-
await
|
|
1646
|
-
await
|
|
1646
|
+
await fs5.mkdir(path18.dirname(this.historyFile), { recursive: true });
|
|
1647
|
+
await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
|
|
1647
1648
|
} catch {
|
|
1648
1649
|
}
|
|
1649
1650
|
}
|
|
@@ -2113,7 +2114,7 @@ async function saveToGlobalConfig(configPath, provider, model) {
|
|
|
2113
2114
|
}
|
|
2114
2115
|
async function pathExists(file) {
|
|
2115
2116
|
try {
|
|
2116
|
-
await
|
|
2117
|
+
await fs5.access(file);
|
|
2117
2118
|
return true;
|
|
2118
2119
|
} catch {
|
|
2119
2120
|
return false;
|
|
@@ -2124,10 +2125,10 @@ async function detectPackageManager(root, declared) {
|
|
|
2124
2125
|
const name = declared.split("@")[0];
|
|
2125
2126
|
if (name) return name;
|
|
2126
2127
|
}
|
|
2127
|
-
if (await pathExists(
|
|
2128
|
-
if (await pathExists(
|
|
2129
|
-
if (await pathExists(
|
|
2130
|
-
if (await pathExists(
|
|
2128
|
+
if (await pathExists(path18.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
2129
|
+
if (await pathExists(path18.join(root, "bun.lockb"))) return "bun";
|
|
2130
|
+
if (await pathExists(path18.join(root, "bun.lock"))) return "bun";
|
|
2131
|
+
if (await pathExists(path18.join(root, "yarn.lock"))) return "yarn";
|
|
2131
2132
|
return "npm";
|
|
2132
2133
|
}
|
|
2133
2134
|
function hasUsableScript(scripts, name) {
|
|
@@ -2148,7 +2149,7 @@ function parseMakeTargets(makefile) {
|
|
|
2148
2149
|
async function detectProjectFacts(root) {
|
|
2149
2150
|
const facts = { hints: [] };
|
|
2150
2151
|
try {
|
|
2151
|
-
const pkg = JSON.parse(await
|
|
2152
|
+
const pkg = JSON.parse(await fs5.readFile(path18.join(root, "package.json"), "utf8"));
|
|
2152
2153
|
const scripts = pkg.scripts ?? {};
|
|
2153
2154
|
const pm = await detectPackageManager(root, pkg.packageManager);
|
|
2154
2155
|
if (hasUsableScript(scripts, "build")) facts.build = `${pm} run build`;
|
|
@@ -2162,14 +2163,14 @@ async function detectProjectFacts(root) {
|
|
|
2162
2163
|
} catch {
|
|
2163
2164
|
}
|
|
2164
2165
|
try {
|
|
2165
|
-
if (!await pathExists(
|
|
2166
|
+
if (!await pathExists(path18.join(root, "pyproject.toml"))) throw new Error("not python");
|
|
2166
2167
|
facts.test ??= "pytest";
|
|
2167
2168
|
facts.lint ??= "ruff check .";
|
|
2168
2169
|
facts.hints.push("pyproject.toml");
|
|
2169
2170
|
} catch {
|
|
2170
2171
|
}
|
|
2171
2172
|
try {
|
|
2172
|
-
if (!await pathExists(
|
|
2173
|
+
if (!await pathExists(path18.join(root, "go.mod"))) throw new Error("not go");
|
|
2173
2174
|
facts.build ??= "go build ./...";
|
|
2174
2175
|
facts.test ??= "go test ./...";
|
|
2175
2176
|
facts.run ??= "go run .";
|
|
@@ -2177,7 +2178,7 @@ async function detectProjectFacts(root) {
|
|
|
2177
2178
|
} catch {
|
|
2178
2179
|
}
|
|
2179
2180
|
try {
|
|
2180
|
-
if (!await pathExists(
|
|
2181
|
+
if (!await pathExists(path18.join(root, "Cargo.toml"))) throw new Error("not rust");
|
|
2181
2182
|
facts.build ??= "cargo build";
|
|
2182
2183
|
facts.test ??= "cargo test";
|
|
2183
2184
|
facts.lint ??= "cargo clippy";
|
|
@@ -2186,7 +2187,7 @@ async function detectProjectFacts(root) {
|
|
|
2186
2187
|
} catch {
|
|
2187
2188
|
}
|
|
2188
2189
|
try {
|
|
2189
|
-
const makefile = await
|
|
2190
|
+
const makefile = await fs5.readFile(path18.join(root, "Makefile"), "utf8");
|
|
2190
2191
|
const targets = parseMakeTargets(makefile);
|
|
2191
2192
|
facts.build ??= targets.has("build") ? "make build" : "make";
|
|
2192
2193
|
if (targets.has("test")) facts.test ??= "make test";
|
|
@@ -2320,6 +2321,175 @@ function buildClearCommand(opts) {
|
|
|
2320
2321
|
}
|
|
2321
2322
|
};
|
|
2322
2323
|
}
|
|
2324
|
+
async function runGit(args, cwd) {
|
|
2325
|
+
return new Promise((resolve3) => {
|
|
2326
|
+
const child = spawn("git", args, {
|
|
2327
|
+
cwd,
|
|
2328
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2329
|
+
});
|
|
2330
|
+
let stdout = "";
|
|
2331
|
+
let stderr = "";
|
|
2332
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
2333
|
+
child.stderr?.on("data", (d) => stderr += d);
|
|
2334
|
+
child.on("close", (code) => resolve3({ stdout, stderr, code: code ?? 0 }));
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
function detectCommitType(stats) {
|
|
2338
|
+
const lines = stats.split("\n");
|
|
2339
|
+
const hasTestFiles = lines.some(
|
|
2340
|
+
(l) => l.includes("_test.") || l.includes(".test.") || l.includes(".spec.")
|
|
2341
|
+
);
|
|
2342
|
+
const hasDocs = lines.some(
|
|
2343
|
+
(l) => l.includes("README") || l.includes("CHANGELOG") || l.includes("docs/") || l.includes(".md")
|
|
2344
|
+
);
|
|
2345
|
+
const hasConfig = lines.some(
|
|
2346
|
+
(l) => l.includes("config") || l.includes("tsconfig") || l.includes(".json")
|
|
2347
|
+
);
|
|
2348
|
+
if (hasTestFiles) return "test";
|
|
2349
|
+
if (hasDocs) return "docs";
|
|
2350
|
+
if (hasConfig) return "chore";
|
|
2351
|
+
return "feat";
|
|
2352
|
+
}
|
|
2353
|
+
async function generateCommitMessage(cwd) {
|
|
2354
|
+
const statsResult = await runGit(["diff", "--stat"], cwd);
|
|
2355
|
+
if (statsResult.code !== 0) return "chore: update";
|
|
2356
|
+
const nameResult = await runGit(["diff", "--name-only"], cwd);
|
|
2357
|
+
const files = nameResult.stdout.split("\n").filter(Boolean);
|
|
2358
|
+
const commitType = detectCommitType(statsResult.stdout);
|
|
2359
|
+
let scope = "";
|
|
2360
|
+
if (files.length > 0) {
|
|
2361
|
+
const primary = files[0].split("/")[0];
|
|
2362
|
+
if (primary && primary !== "packages" && primary !== "apps" && primary !== "node_modules") {
|
|
2363
|
+
scope = `(${primary})`;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (files.length === 0) {
|
|
2367
|
+
return `${commitType}${scope}: update`;
|
|
2368
|
+
}
|
|
2369
|
+
if (files.length <= 3) {
|
|
2370
|
+
const summary2 = files.map((f) => f.split("/").pop()).join(", ");
|
|
2371
|
+
return `${commitType}${scope}: ${summary2}`;
|
|
2372
|
+
}
|
|
2373
|
+
const summary = files.slice(0, 3).map((f) => f.split("/").pop()).join(", ") + ` and ${files.length - 3} more`;
|
|
2374
|
+
return `${commitType}${scope}: ${summary}`;
|
|
2375
|
+
}
|
|
2376
|
+
async function hasUncommittedChanges(cwd) {
|
|
2377
|
+
const result = await runGit(["status", "--porcelain"], cwd);
|
|
2378
|
+
return result.stdout.trim().length > 0;
|
|
2379
|
+
}
|
|
2380
|
+
async function isGitRepo(cwd) {
|
|
2381
|
+
const result = await runGit(["rev-parse", "--git-dir"], cwd);
|
|
2382
|
+
return result.code === 0;
|
|
2383
|
+
}
|
|
2384
|
+
function buildCommitCommand(_opts) {
|
|
2385
|
+
return {
|
|
2386
|
+
name: "commit",
|
|
2387
|
+
description: "Stage all changes and commit with auto-generated message.",
|
|
2388
|
+
aliases: ["gc"],
|
|
2389
|
+
async run(args, ctx) {
|
|
2390
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2391
|
+
if (!await isGitRepo(cwd)) {
|
|
2392
|
+
return { message: "Not a git repository." };
|
|
2393
|
+
}
|
|
2394
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2395
|
+
return { message: "Nothing to commit (working tree clean)." };
|
|
2396
|
+
}
|
|
2397
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2398
|
+
const message = await generateCommitMessage(cwd);
|
|
2399
|
+
if (dryRun) {
|
|
2400
|
+
return {
|
|
2401
|
+
message: `Would commit:
|
|
2402
|
+
|
|
2403
|
+
${color.green(message)}
|
|
2404
|
+
|
|
2405
|
+
${color.dim("(dry-run \u2014 no actual commit)")}`
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const stageResult = await runGit(["add", "."], cwd);
|
|
2409
|
+
if (stageResult.code !== 0) {
|
|
2410
|
+
return { message: `Stage failed: ${stageResult.stderr}` };
|
|
2411
|
+
}
|
|
2412
|
+
const commitResult = await runGit(["commit", "-m", message], cwd);
|
|
2413
|
+
if (commitResult.code !== 0) {
|
|
2414
|
+
return { message: `Commit failed: ${commitResult.stderr}` };
|
|
2415
|
+
}
|
|
2416
|
+
const hashResult = await runGit(["rev-parse", "--short", "HEAD"], cwd);
|
|
2417
|
+
const hash = hashResult.stdout.trim();
|
|
2418
|
+
const pushResult = await runGit(["remote"], cwd);
|
|
2419
|
+
const hasRemote = pushResult.stdout.trim().length > 0;
|
|
2420
|
+
let pushMsg = "";
|
|
2421
|
+
if (hasRemote) {
|
|
2422
|
+
pushMsg = `
|
|
2423
|
+
|
|
2424
|
+
${color.dim("Tip: Run /push to push to remote")}`;
|
|
2425
|
+
}
|
|
2426
|
+
return {
|
|
2427
|
+
message: `${color.green("\u2713")} Committed: ${color.bold(message)}
|
|
2428
|
+
${color.dim(hash)}${pushMsg}`
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function buildGitcheckCommand(_opts) {
|
|
2434
|
+
return {
|
|
2435
|
+
name: "gitcheck",
|
|
2436
|
+
description: "Check for uncommitted changes (for system prompt integration).",
|
|
2437
|
+
aliases: ["gcstatus"],
|
|
2438
|
+
async run(_args, ctx) {
|
|
2439
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2440
|
+
if (!await isGitRepo(cwd)) {
|
|
2441
|
+
return { message: "" };
|
|
2442
|
+
}
|
|
2443
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
2444
|
+
return { message: "" };
|
|
2445
|
+
}
|
|
2446
|
+
const statusResult = await runGit(["status", "--porcelain"], cwd);
|
|
2447
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
2448
|
+
const count = lines.length;
|
|
2449
|
+
if (count === 0) return { message: "" };
|
|
2450
|
+
return {
|
|
2451
|
+
message: `\u26A0 ${color.yellow(`${count} uncommitted change${count > 1 ? "s" : ""}`)} \u2014 consider /commit`
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
function buildPushCommand(_opts) {
|
|
2457
|
+
return {
|
|
2458
|
+
name: "push",
|
|
2459
|
+
description: "Push to remote after commit.",
|
|
2460
|
+
async run(args, ctx) {
|
|
2461
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
2462
|
+
if (!await isGitRepo(cwd)) {
|
|
2463
|
+
return { message: "Not a git repository." };
|
|
2464
|
+
}
|
|
2465
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2466
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
2467
|
+
const remoteResult = await runGit(["remote"], cwd);
|
|
2468
|
+
const remotes = remoteResult.stdout.split("\n").filter(Boolean);
|
|
2469
|
+
if (remotes.length === 0) {
|
|
2470
|
+
return { message: "No remote configured. Add one with: git remote add origin <url>" };
|
|
2471
|
+
}
|
|
2472
|
+
if (dryRun) {
|
|
2473
|
+
return {
|
|
2474
|
+
message: `Would push to ${remotes.join(", ")}${force ? " (force)" : ""}
|
|
2475
|
+
${color.dim("(dry-run)")}`
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
const branchResult = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
2479
|
+
const branch = branchResult.stdout.trim() || "main";
|
|
2480
|
+
const pushArgs = ["push"];
|
|
2481
|
+
if (force) pushArgs.push("--force");
|
|
2482
|
+
pushArgs.push(...remotes, branch);
|
|
2483
|
+
const pushResult = await runGit(pushArgs, cwd);
|
|
2484
|
+
if (pushResult.code !== 0) {
|
|
2485
|
+
return { message: `Push failed: ${pushResult.stderr}` };
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
message: `${color.green("\u2713")} Pushed to ${remotes.join(", ")} (${branch})`
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2323
2493
|
|
|
2324
2494
|
// src/slash-commands/compact.ts
|
|
2325
2495
|
function buildCompactCommand(opts) {
|
|
@@ -2649,10 +2819,10 @@ function buildInitCommand(opts) {
|
|
|
2649
2819
|
description: "Create .wrongstack/AGENTS.md project context for the system prompt.",
|
|
2650
2820
|
async run(args, ctx) {
|
|
2651
2821
|
const force = args.trim() === "--force";
|
|
2652
|
-
const dir =
|
|
2653
|
-
const file =
|
|
2822
|
+
const dir = path18.join(ctx.projectRoot, ".wrongstack");
|
|
2823
|
+
const file = path18.join(dir, "AGENTS.md");
|
|
2654
2824
|
try {
|
|
2655
|
-
await
|
|
2825
|
+
await fs5.access(file);
|
|
2656
2826
|
if (!force) {
|
|
2657
2827
|
const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
|
|
2658
2828
|
opts.renderer.writeWarning(msg2);
|
|
@@ -2662,8 +2832,8 @@ function buildInitCommand(opts) {
|
|
|
2662
2832
|
}
|
|
2663
2833
|
const detected = await detectProjectFacts(ctx.projectRoot);
|
|
2664
2834
|
const body = renderAgentsTemplate(detected);
|
|
2665
|
-
await
|
|
2666
|
-
await
|
|
2835
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
2836
|
+
await fs5.writeFile(file, body, "utf8");
|
|
2667
2837
|
if (detected.hints.length > 0) {
|
|
2668
2838
|
const msg2 = `Wrote ${file}
|
|
2669
2839
|
Pre-filled: ${detected.hints.join(", ")}. Edit the file with project context and instructions the system prompt should carry.`;
|
|
@@ -2892,6 +3062,13 @@ function buildExitCommand(opts) {
|
|
|
2892
3062
|
aliases: ["quit", "q"],
|
|
2893
3063
|
description: "Exit the REPL.",
|
|
2894
3064
|
async run() {
|
|
3065
|
+
if (opts.onBeforeExit) {
|
|
3066
|
+
const result = await opts.onBeforeExit();
|
|
3067
|
+
if (result?.abort) {
|
|
3068
|
+
opts.onExit?.();
|
|
3069
|
+
return { message: result.message ?? "", exit: true };
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
2895
3072
|
opts.onExit?.();
|
|
2896
3073
|
return { exit: true };
|
|
2897
3074
|
}
|
|
@@ -3264,11 +3441,11 @@ ${lines.join("\n\n")}
|
|
|
3264
3441
|
};
|
|
3265
3442
|
}
|
|
3266
3443
|
function makeInstaller(opts, projectRoot, global) {
|
|
3267
|
-
const globalRoot =
|
|
3444
|
+
const globalRoot = path18.join(os4.homedir(), ".wrongstack");
|
|
3268
3445
|
return new SkillInstaller({
|
|
3269
|
-
manifestPath:
|
|
3270
|
-
projectSkillsDir:
|
|
3271
|
-
globalSkillsDir:
|
|
3446
|
+
manifestPath: path18.join(globalRoot, "installed-skills.json"),
|
|
3447
|
+
projectSkillsDir: path18.join(projectRoot, ".wrongstack", "skills"),
|
|
3448
|
+
globalSkillsDir: path18.join(globalRoot, "skills"),
|
|
3272
3449
|
projectHash: projectHash(projectRoot),
|
|
3273
3450
|
skillLoader: opts.skillLoader
|
|
3274
3451
|
});
|
|
@@ -3457,7 +3634,10 @@ function buildBuiltinSlashCommands(opts) {
|
|
|
3457
3634
|
buildYoloCommand(opts),
|
|
3458
3635
|
buildAutonomyCommand(opts),
|
|
3459
3636
|
buildModeCommand(opts),
|
|
3460
|
-
buildExitCommand(opts)
|
|
3637
|
+
buildExitCommand(opts),
|
|
3638
|
+
buildCommitCommand(),
|
|
3639
|
+
buildGitcheckCommand(),
|
|
3640
|
+
buildPushCommand()
|
|
3461
3641
|
];
|
|
3462
3642
|
}
|
|
3463
3643
|
|
|
@@ -3476,13 +3656,13 @@ var MANIFESTS = [
|
|
|
3476
3656
|
];
|
|
3477
3657
|
async function detectProjectKind(projectRoot) {
|
|
3478
3658
|
try {
|
|
3479
|
-
await
|
|
3659
|
+
await fs5.access(path18.join(projectRoot, ".wrongstack", "AGENTS.md"));
|
|
3480
3660
|
return "initialized";
|
|
3481
3661
|
} catch {
|
|
3482
3662
|
}
|
|
3483
3663
|
for (const m of MANIFESTS) {
|
|
3484
3664
|
try {
|
|
3485
|
-
await
|
|
3665
|
+
await fs5.access(path18.join(projectRoot, m));
|
|
3486
3666
|
return "project";
|
|
3487
3667
|
} catch {
|
|
3488
3668
|
}
|
|
@@ -3490,12 +3670,12 @@ async function detectProjectKind(projectRoot) {
|
|
|
3490
3670
|
return "empty";
|
|
3491
3671
|
}
|
|
3492
3672
|
async function scaffoldAgentsMd(projectRoot) {
|
|
3493
|
-
const dir =
|
|
3494
|
-
const file =
|
|
3673
|
+
const dir = path18.join(projectRoot, ".wrongstack");
|
|
3674
|
+
const file = path18.join(dir, "AGENTS.md");
|
|
3495
3675
|
const facts = await detectProjectFacts(projectRoot);
|
|
3496
3676
|
const body = renderAgentsTemplate(facts);
|
|
3497
|
-
await
|
|
3498
|
-
await
|
|
3677
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
3678
|
+
await fs5.writeFile(file, body, "utf8");
|
|
3499
3679
|
return file;
|
|
3500
3680
|
}
|
|
3501
3681
|
async function runProjectCheck(opts) {
|
|
@@ -3504,7 +3684,7 @@ async function runProjectCheck(opts) {
|
|
|
3504
3684
|
if (kind === "initialized") {
|
|
3505
3685
|
renderer.write(
|
|
3506
3686
|
`
|
|
3507
|
-
${color.green("\u2713")} Project initialized ${color.dim(`(${
|
|
3687
|
+
${color.green("\u2713")} Project initialized ${color.dim(`(${path18.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
|
|
3508
3688
|
`
|
|
3509
3689
|
);
|
|
3510
3690
|
return true;
|
|
@@ -3531,11 +3711,43 @@ async function runProjectCheck(opts) {
|
|
|
3531
3711
|
}
|
|
3532
3712
|
return true;
|
|
3533
3713
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3714
|
+
const gitDir = path18.join(projectRoot, ".git");
|
|
3715
|
+
let hasGit = false;
|
|
3716
|
+
try {
|
|
3717
|
+
await fs5.access(gitDir);
|
|
3718
|
+
hasGit = true;
|
|
3719
|
+
} catch {
|
|
3720
|
+
}
|
|
3721
|
+
if (!hasGit) {
|
|
3722
|
+
renderer.write(
|
|
3723
|
+
`
|
|
3536
3724
|
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
3537
3725
|
`
|
|
3538
|
-
|
|
3726
|
+
);
|
|
3727
|
+
const answer2 = (await reader.readLine(
|
|
3728
|
+
` ${color.amber("?")} No git repo found. ${color.bold("Initialize git?")} ${color.dim("[y/N]")} `
|
|
3729
|
+
)).trim().toLowerCase();
|
|
3730
|
+
if (answer2 === "y" || answer2 === "yes") {
|
|
3731
|
+
try {
|
|
3732
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
3733
|
+
await new Promise((resolve3, reject) => {
|
|
3734
|
+
const child = spawn2("git", ["init"], { cwd: projectRoot });
|
|
3735
|
+
child.on("close", (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`)));
|
|
3736
|
+
});
|
|
3737
|
+
renderer.write(` ${color.green("\u2713")} Git repository initialized
|
|
3738
|
+
`);
|
|
3739
|
+
} catch (err) {
|
|
3740
|
+
renderer.writeError(`git init failed: ${err instanceof Error ? err.message : String(err)}
|
|
3741
|
+
`);
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
} else {
|
|
3745
|
+
renderer.write(
|
|
3746
|
+
`
|
|
3747
|
+
${color.dim("\u25CB")} ${color.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
|
|
3748
|
+
`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3539
3751
|
const answer = (await reader.readLine(` ${color.amber("?")} Continue anyway? ${color.dim("[Y/n]")} `)).trim().toLowerCase();
|
|
3540
3752
|
if (answer === "n" || answer === "no") {
|
|
3541
3753
|
renderer.write(color.dim(" Cancelled.\n"));
|
|
@@ -3800,14 +4012,14 @@ function summarize(value, name) {
|
|
|
3800
4012
|
if (typeof v === "object" && v !== null) {
|
|
3801
4013
|
const o = v;
|
|
3802
4014
|
if (name === "edit") {
|
|
3803
|
-
const
|
|
4015
|
+
const path19 = typeof o["path"] === "string" ? o["path"] : "";
|
|
3804
4016
|
const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
|
|
3805
|
-
return `${
|
|
4017
|
+
return `${path19} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
|
|
3806
4018
|
}
|
|
3807
4019
|
if (name === "write") {
|
|
3808
|
-
const
|
|
4020
|
+
const path19 = typeof o["path"] === "string" ? o["path"] : "";
|
|
3809
4021
|
const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
|
|
3810
|
-
return bytes !== void 0 ? `${
|
|
4022
|
+
return bytes !== void 0 ? `${path19} ${bytes}B` : path19;
|
|
3811
4023
|
}
|
|
3812
4024
|
if (typeof o["count"] === "number") {
|
|
3813
4025
|
return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
|
|
@@ -4403,7 +4615,7 @@ async function readKeyInput(deps, intent) {
|
|
|
4403
4615
|
async function loadProviders(deps) {
|
|
4404
4616
|
let raw;
|
|
4405
4617
|
try {
|
|
4406
|
-
raw = await
|
|
4618
|
+
raw = await fs5.readFile(deps.globalConfigPath, "utf8");
|
|
4407
4619
|
} catch {
|
|
4408
4620
|
return {};
|
|
4409
4621
|
}
|
|
@@ -4419,7 +4631,7 @@ async function loadProviders(deps) {
|
|
|
4419
4631
|
async function mutateProviders(deps, mutator) {
|
|
4420
4632
|
let raw;
|
|
4421
4633
|
try {
|
|
4422
|
-
raw = await
|
|
4634
|
+
raw = await fs5.readFile(deps.globalConfigPath, "utf8");
|
|
4423
4635
|
} catch {
|
|
4424
4636
|
raw = "{}";
|
|
4425
4637
|
}
|
|
@@ -4559,7 +4771,7 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4559
4771
|
});
|
|
4560
4772
|
}
|
|
4561
4773
|
try {
|
|
4562
|
-
await
|
|
4774
|
+
await fs5.access(deps.paths.secretsKey);
|
|
4563
4775
|
checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
|
|
4564
4776
|
} catch {
|
|
4565
4777
|
checks.push({
|
|
@@ -4569,10 +4781,10 @@ var doctorCmd = async (_args, deps) => {
|
|
|
4569
4781
|
});
|
|
4570
4782
|
}
|
|
4571
4783
|
try {
|
|
4572
|
-
await
|
|
4573
|
-
const probe =
|
|
4574
|
-
await
|
|
4575
|
-
await
|
|
4784
|
+
await fs5.mkdir(deps.paths.projectSessions, { recursive: true });
|
|
4785
|
+
const probe = path18.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
|
|
4786
|
+
await fs5.writeFile(probe, "");
|
|
4787
|
+
await fs5.unlink(probe);
|
|
4576
4788
|
checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
|
|
4577
4789
|
} catch (err) {
|
|
4578
4790
|
checks.push({
|
|
@@ -4673,8 +4885,8 @@ var exportCmd = async (args, deps) => {
|
|
|
4673
4885
|
return 1;
|
|
4674
4886
|
}
|
|
4675
4887
|
if (output) {
|
|
4676
|
-
await
|
|
4677
|
-
await
|
|
4888
|
+
await fs5.mkdir(path18.dirname(path18.resolve(deps.cwd, output)), { recursive: true });
|
|
4889
|
+
await fs5.writeFile(path18.resolve(deps.cwd, output), rendered, "utf8");
|
|
4678
4890
|
deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
|
|
4679
4891
|
`);
|
|
4680
4892
|
} else {
|
|
@@ -4731,17 +4943,17 @@ var initCmd = async (_args, deps) => {
|
|
|
4731
4943
|
} else {
|
|
4732
4944
|
deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
|
|
4733
4945
|
}
|
|
4734
|
-
await
|
|
4946
|
+
await fs5.mkdir(deps.paths.globalRoot, { recursive: true });
|
|
4735
4947
|
const config = { version: 1, provider: providerId, model: modelId };
|
|
4736
4948
|
if (apiKey) config.apiKey = apiKey;
|
|
4737
|
-
const keyFile =
|
|
4949
|
+
const keyFile = path18.join(path18.dirname(deps.paths.globalConfig), ".key");
|
|
4738
4950
|
const vault = new DefaultSecretVault$1({ keyFile });
|
|
4739
4951
|
const encrypted = encryptConfigSecrets(config, vault);
|
|
4740
4952
|
await atomicWrite(deps.paths.globalConfig, JSON.stringify(encrypted, null, 2));
|
|
4741
|
-
await
|
|
4742
|
-
const agentsFile =
|
|
4953
|
+
await fs5.mkdir(path18.join(deps.projectRoot, ".wrongstack"), { recursive: true });
|
|
4954
|
+
const agentsFile = path18.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
|
|
4743
4955
|
try {
|
|
4744
|
-
await
|
|
4956
|
+
await fs5.access(agentsFile);
|
|
4745
4957
|
} catch {
|
|
4746
4958
|
const detected2 = await detectProjectFacts(deps.projectRoot);
|
|
4747
4959
|
await atomicWrite(agentsFile, renderAgentsTemplate(detected2));
|
|
@@ -4817,7 +5029,7 @@ async function addMcpServer(args, deps) {
|
|
|
4817
5029
|
serverCfg.enabled = enable;
|
|
4818
5030
|
let existing = {};
|
|
4819
5031
|
try {
|
|
4820
|
-
existing = JSON.parse(await
|
|
5032
|
+
existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
|
|
4821
5033
|
} catch {
|
|
4822
5034
|
}
|
|
4823
5035
|
const mcpServers = existing.mcpServers ?? {};
|
|
@@ -4837,7 +5049,7 @@ async function addMcpServer(args, deps) {
|
|
|
4837
5049
|
async function removeMcpServer(name, deps) {
|
|
4838
5050
|
let existing = {};
|
|
4839
5051
|
try {
|
|
4840
|
-
existing = JSON.parse(await
|
|
5052
|
+
existing = JSON.parse(await fs5.readFile(deps.paths.globalConfig, "utf8"));
|
|
4841
5053
|
} catch {
|
|
4842
5054
|
deps.renderer.writeError("No config file found.\n");
|
|
4843
5055
|
return 1;
|
|
@@ -4958,7 +5170,7 @@ function renderConfiguredPlugins(config) {
|
|
|
4958
5170
|
}
|
|
4959
5171
|
async function readConfig(file) {
|
|
4960
5172
|
try {
|
|
4961
|
-
return JSON.parse(await
|
|
5173
|
+
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
4962
5174
|
} catch {
|
|
4963
5175
|
return {};
|
|
4964
5176
|
}
|
|
@@ -5049,9 +5261,9 @@ var usageCmd = async (_args, deps) => {
|
|
|
5049
5261
|
return 0;
|
|
5050
5262
|
};
|
|
5051
5263
|
var projectsCmd = async (_args, deps) => {
|
|
5052
|
-
const projectsRoot =
|
|
5264
|
+
const projectsRoot = path18.join(deps.paths.globalRoot, "projects");
|
|
5053
5265
|
try {
|
|
5054
|
-
const entries = await
|
|
5266
|
+
const entries = await fs5.readdir(projectsRoot);
|
|
5055
5267
|
if (entries.length === 0) {
|
|
5056
5268
|
deps.renderer.write("No projects tracked.\n");
|
|
5057
5269
|
return 0;
|
|
@@ -5059,7 +5271,7 @@ var projectsCmd = async (_args, deps) => {
|
|
|
5059
5271
|
for (const hash of entries) {
|
|
5060
5272
|
try {
|
|
5061
5273
|
const meta = JSON.parse(
|
|
5062
|
-
await
|
|
5274
|
+
await fs5.readFile(path18.join(projectsRoot, hash, "meta.json"), "utf8")
|
|
5063
5275
|
);
|
|
5064
5276
|
deps.renderer.write(
|
|
5065
5277
|
` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
|
|
@@ -5236,6 +5448,131 @@ var configCmd = async (args, deps) => {
|
|
|
5236
5448
|
deps.renderer.writeError(`Unknown config subcommand: ${sub}`);
|
|
5237
5449
|
return 1;
|
|
5238
5450
|
};
|
|
5451
|
+
function parseRewindFlags(args) {
|
|
5452
|
+
const flags = {};
|
|
5453
|
+
for (let i = 0; i < args.length; i++) {
|
|
5454
|
+
const a = args[i];
|
|
5455
|
+
if (a === "--all") flags.all = true;
|
|
5456
|
+
else if (a === "--last") flags.last = args[++i] ?? "1";
|
|
5457
|
+
else if (a === "--to") flags.to = args[++i] ?? "";
|
|
5458
|
+
else if (a === "--list") flags.list = true;
|
|
5459
|
+
else if (a === "--resume") flags.resume = true;
|
|
5460
|
+
}
|
|
5461
|
+
return flags;
|
|
5462
|
+
}
|
|
5463
|
+
var rewindCmd = async (args, deps) => {
|
|
5464
|
+
const flags = parseRewindFlags(args);
|
|
5465
|
+
const wpaths = resolveWstackPaths({ projectRoot: deps.projectRoot });
|
|
5466
|
+
const sessionsDir = path18.join(wpaths.globalRoot, "sessions");
|
|
5467
|
+
const rewind = new DefaultSessionRewinder(sessionsDir);
|
|
5468
|
+
let sessionId = args.find((a) => !a.startsWith("--"));
|
|
5469
|
+
if (!sessionId) {
|
|
5470
|
+
if (!deps.sessionStore) {
|
|
5471
|
+
deps.renderer.writeError("No session store available.");
|
|
5472
|
+
return 1;
|
|
5473
|
+
}
|
|
5474
|
+
const sessions = await deps.sessionStore.list(1);
|
|
5475
|
+
if (sessions.length === 0) {
|
|
5476
|
+
deps.renderer.writeError("No sessions found.");
|
|
5477
|
+
return 1;
|
|
5478
|
+
}
|
|
5479
|
+
sessionId = sessions[0].id;
|
|
5480
|
+
}
|
|
5481
|
+
if (flags.list) {
|
|
5482
|
+
deps.renderer.write(`Session: ${color.bold(sessionId)}
|
|
5483
|
+
|
|
5484
|
+
`);
|
|
5485
|
+
const checkpoints = await rewind.listCheckpoints(sessionId);
|
|
5486
|
+
if (checkpoints.length === 0) {
|
|
5487
|
+
deps.renderer.write("No checkpoints in this session.\n");
|
|
5488
|
+
return 0;
|
|
5489
|
+
}
|
|
5490
|
+
for (const cp of checkpoints) {
|
|
5491
|
+
deps.renderer.write(
|
|
5492
|
+
` [${cp.promptIndex}] ${color.dim(cp.ts)} ${cp.promptPreview}${cp.fileCount > 0 ? color.dim(` (${cp.fileCount} file${cp.fileCount === 1 ? "" : "s"})`) : ""}
|
|
5493
|
+
`
|
|
5494
|
+
);
|
|
5495
|
+
}
|
|
5496
|
+
return 0;
|
|
5497
|
+
}
|
|
5498
|
+
try {
|
|
5499
|
+
let result;
|
|
5500
|
+
if (flags.all) {
|
|
5501
|
+
deps.renderer.write("Rewinding to session start...\n");
|
|
5502
|
+
result = await rewind.rewindToStart(sessionId);
|
|
5503
|
+
} else if (flags.last) {
|
|
5504
|
+
const n = parseInt(flags.last, 10);
|
|
5505
|
+
if (isNaN(n) || n < 1) {
|
|
5506
|
+
deps.renderer.writeError("--last requires a positive number");
|
|
5507
|
+
return 1;
|
|
5508
|
+
}
|
|
5509
|
+
deps.renderer.write(`Rewinding last ${n} prompt(s)...
|
|
5510
|
+
`);
|
|
5511
|
+
result = await rewind.rewindLastN(sessionId, n);
|
|
5512
|
+
} else if (flags.to) {
|
|
5513
|
+
const idx = parseInt(flags.to, 10);
|
|
5514
|
+
if (isNaN(idx) || idx < 0) {
|
|
5515
|
+
deps.renderer.writeError("--to requires a non-negative number");
|
|
5516
|
+
return 1;
|
|
5517
|
+
}
|
|
5518
|
+
deps.renderer.write(`Rewinding to checkpoint ${idx}...
|
|
5519
|
+
`);
|
|
5520
|
+
result = await rewind.rewindToCheckpoint(sessionId, idx);
|
|
5521
|
+
} else {
|
|
5522
|
+
deps.renderer.write("Usage: ws rewind --all | --last N | --to <index> [--list] [--resume]\n");
|
|
5523
|
+
deps.renderer.write(" --all Rewind to session start\n");
|
|
5524
|
+
deps.renderer.write(" --last N Rewind last N prompts\n");
|
|
5525
|
+
deps.renderer.write(" --to N Rewind to checkpoint N\n");
|
|
5526
|
+
deps.renderer.write(" --list List checkpoints\n");
|
|
5527
|
+
deps.renderer.write(" --resume After rewind, truncate session history at checkpoint\n");
|
|
5528
|
+
return 1;
|
|
5529
|
+
}
|
|
5530
|
+
if (result.revertedFiles.length === 0) {
|
|
5531
|
+
deps.renderer.write("No files to revert.\n");
|
|
5532
|
+
if (flags.resume) {
|
|
5533
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
5534
|
+
const resumed = await store.resume(sessionId);
|
|
5535
|
+
const toIdx = result.toPromptIndex;
|
|
5536
|
+
await resumed.writer.truncateToCheckpoint(toIdx);
|
|
5537
|
+
await resumed.writer.close();
|
|
5538
|
+
deps.renderer.write(` ${color.green("\u2713")} Session truncated at checkpoint ${toIdx}
|
|
5539
|
+
`);
|
|
5540
|
+
}
|
|
5541
|
+
return 0;
|
|
5542
|
+
}
|
|
5543
|
+
deps.renderer.write(`
|
|
5544
|
+
Reverted ${result.revertedFiles.length} file(s):
|
|
5545
|
+
`);
|
|
5546
|
+
for (const f of result.revertedFiles) {
|
|
5547
|
+
deps.renderer.write(` ${color.green("\u2713")} ${f}
|
|
5548
|
+
`);
|
|
5549
|
+
}
|
|
5550
|
+
if (flags.resume) {
|
|
5551
|
+
const store = new DefaultSessionStore({ dir: sessionsDir });
|
|
5552
|
+
const resumed = await store.resume(sessionId);
|
|
5553
|
+
const toIdx = result.toPromptIndex;
|
|
5554
|
+
const removed = await resumed.writer.truncateToCheckpoint(toIdx);
|
|
5555
|
+
await resumed.writer.close();
|
|
5556
|
+
deps.renderer.write(`
|
|
5557
|
+
${color.green("\u2713")} Session truncated \u2014 ${removed} event(s) removed
|
|
5558
|
+
`);
|
|
5559
|
+
}
|
|
5560
|
+
if (result.errors.length > 0) {
|
|
5561
|
+
deps.renderer.write(`
|
|
5562
|
+
${result.errors.length} error(s):
|
|
5563
|
+
`);
|
|
5564
|
+
for (const e of result.errors) {
|
|
5565
|
+
deps.renderer.write(` ${color.red("\u2717")} ${e}
|
|
5566
|
+
`);
|
|
5567
|
+
}
|
|
5568
|
+
return 1;
|
|
5569
|
+
}
|
|
5570
|
+
return 0;
|
|
5571
|
+
} catch (err) {
|
|
5572
|
+
deps.renderer.writeError(err instanceof Error ? err.message : String(err));
|
|
5573
|
+
return 1;
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
5239
5576
|
var toolsCmd = async (_args, deps) => {
|
|
5240
5577
|
const reg = deps.toolRegistry;
|
|
5241
5578
|
if (!reg) return 0;
|
|
@@ -5298,6 +5635,7 @@ var subcommands = {
|
|
|
5298
5635
|
auth: authCmd,
|
|
5299
5636
|
sessions: sessionsCmd,
|
|
5300
5637
|
config: configCmd,
|
|
5638
|
+
rewind: rewindCmd,
|
|
5301
5639
|
tools: toolsCmd,
|
|
5302
5640
|
skills: skillsCmd,
|
|
5303
5641
|
providers: providersCmd,
|
|
@@ -5334,29 +5672,29 @@ function fmtDuration(ms) {
|
|
|
5334
5672
|
const remMin = m - h * 60;
|
|
5335
5673
|
return `${h}h${remMin}m`;
|
|
5336
5674
|
}
|
|
5337
|
-
function fmtTaskResultLine(r,
|
|
5675
|
+
function fmtTaskResultLine(r, color32) {
|
|
5338
5676
|
const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
|
|
5339
5677
|
const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
|
|
5340
5678
|
const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
|
|
5341
5679
|
const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
|
|
5342
|
-
const errKindChip = errKind ?
|
|
5343
|
-
const errSnip = errMsg || errKind ? `${errKindChip}${
|
|
5680
|
+
const errKindChip = errKind ? color32.dim(` [${errKind}]`) : "";
|
|
5681
|
+
const errSnip = errMsg || errKind ? `${errKindChip}${color32.dim(errTail)}` : "";
|
|
5344
5682
|
switch (r.status) {
|
|
5345
5683
|
case "success":
|
|
5346
|
-
return { mark:
|
|
5684
|
+
return { mark: color32.green("\u2713"), stats, tail: "" };
|
|
5347
5685
|
case "timeout":
|
|
5348
|
-
return { mark:
|
|
5686
|
+
return { mark: color32.yellow("\u23F1"), stats: `${color32.yellow("timeout")} ${stats}`, tail: errSnip };
|
|
5349
5687
|
case "stopped":
|
|
5350
|
-
return { mark:
|
|
5688
|
+
return { mark: color32.dim("\u2298"), stats: `${color32.dim("stopped")} ${stats}`, tail: errSnip };
|
|
5351
5689
|
case "failed":
|
|
5352
|
-
return { mark:
|
|
5690
|
+
return { mark: color32.red("\u2717"), stats: `${color32.red("failed")} ${stats}`, tail: errSnip };
|
|
5353
5691
|
}
|
|
5354
5692
|
}
|
|
5355
5693
|
function resolveBundledSkillsDir() {
|
|
5356
5694
|
try {
|
|
5357
5695
|
const req2 = createRequire(import.meta.url);
|
|
5358
5696
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
5359
|
-
return
|
|
5697
|
+
return path18.join(path18.dirname(corePkg), "skills");
|
|
5360
5698
|
} catch {
|
|
5361
5699
|
return void 0;
|
|
5362
5700
|
}
|
|
@@ -6074,7 +6412,7 @@ async function execute(deps) {
|
|
|
6074
6412
|
supportsVision,
|
|
6075
6413
|
attachments,
|
|
6076
6414
|
effectiveMaxContext,
|
|
6077
|
-
projectName:
|
|
6415
|
+
projectName: path18.basename(projectRoot) || void 0,
|
|
6078
6416
|
getAutonomy,
|
|
6079
6417
|
skillLoader
|
|
6080
6418
|
});
|
|
@@ -6092,7 +6430,7 @@ async function execute(deps) {
|
|
|
6092
6430
|
supportsVision,
|
|
6093
6431
|
attachments,
|
|
6094
6432
|
effectiveMaxContext,
|
|
6095
|
-
projectName:
|
|
6433
|
+
projectName: path18.basename(projectRoot) || void 0,
|
|
6096
6434
|
getAutonomy,
|
|
6097
6435
|
skillLoader
|
|
6098
6436
|
});
|
|
@@ -6364,7 +6702,7 @@ var MultiAgentHost = class {
|
|
|
6364
6702
|
model: opts?.model,
|
|
6365
6703
|
tools: opts?.tools
|
|
6366
6704
|
};
|
|
6367
|
-
const transcriptPath = this.sessionFactory ?
|
|
6705
|
+
const transcriptPath = this.sessionFactory ? path18.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
|
|
6368
6706
|
if (this.director) {
|
|
6369
6707
|
const subagentId = await this.director.spawn(subagentConfig);
|
|
6370
6708
|
const taskId2 = randomUUID();
|
|
@@ -6524,16 +6862,16 @@ var MultiAgentHost = class {
|
|
|
6524
6862
|
}
|
|
6525
6863
|
this.opts.directorMode = true;
|
|
6526
6864
|
if (this.opts.fleetRoot && !this.opts.manifestPath) {
|
|
6527
|
-
this.opts.manifestPath =
|
|
6865
|
+
this.opts.manifestPath = path18.join(this.opts.fleetRoot, "fleet.json");
|
|
6528
6866
|
}
|
|
6529
6867
|
if (this.opts.fleetRoot && !this.opts.sharedScratchpadPath) {
|
|
6530
|
-
this.opts.sharedScratchpadPath =
|
|
6868
|
+
this.opts.sharedScratchpadPath = path18.join(this.opts.fleetRoot, "shared");
|
|
6531
6869
|
}
|
|
6532
6870
|
if (this.opts.fleetRoot && !this.opts.sessionsRoot) {
|
|
6533
|
-
this.opts.sessionsRoot =
|
|
6871
|
+
this.opts.sessionsRoot = path18.join(this.opts.fleetRoot, "subagents");
|
|
6534
6872
|
}
|
|
6535
6873
|
if (this.opts.fleetRoot && !this.opts.stateCheckpointPath) {
|
|
6536
|
-
this.opts.stateCheckpointPath =
|
|
6874
|
+
this.opts.stateCheckpointPath = path18.join(this.opts.fleetRoot, "director-state.json");
|
|
6537
6875
|
}
|
|
6538
6876
|
await this.ensureDirector();
|
|
6539
6877
|
return this.director ?? null;
|
|
@@ -6654,11 +6992,11 @@ var SessionStats = class {
|
|
|
6654
6992
|
if (e.name === "bash") this.bashCommands++;
|
|
6655
6993
|
else if (e.name === "fetch") this.fetches++;
|
|
6656
6994
|
if (!e.ok) return;
|
|
6657
|
-
const
|
|
6658
|
-
if (e.name === "read" &&
|
|
6659
|
-
else if (e.name === "edit" &&
|
|
6660
|
-
else if (e.name === "write" &&
|
|
6661
|
-
this.writtenPaths.add(
|
|
6995
|
+
const path19 = typeof input?.path === "string" ? input.path : void 0;
|
|
6996
|
+
if (e.name === "read" && path19) this.readPaths.add(path19);
|
|
6997
|
+
else if (e.name === "edit" && path19) this.editedPaths.add(path19);
|
|
6998
|
+
else if (e.name === "write" && path19) {
|
|
6999
|
+
this.writtenPaths.add(path19);
|
|
6662
7000
|
const content = typeof input?.content === "string" ? input.content : "";
|
|
6663
7001
|
this.bytesWritten += Buffer.byteLength(content, "utf8");
|
|
6664
7002
|
}
|
|
@@ -6989,12 +7327,12 @@ async function setupSession(params) {
|
|
|
6989
7327
|
}
|
|
6990
7328
|
const sessionRef = { current: session };
|
|
6991
7329
|
await recoveryLock.write(session.id).catch(() => void 0);
|
|
6992
|
-
const attachments = new DefaultAttachmentStore({ spoolDir:
|
|
6993
|
-
const queueStore = new QueueStore({ dir:
|
|
7330
|
+
const attachments = new DefaultAttachmentStore({ spoolDir: path18.join(wpaths.projectSessions, session.id, "attachments") });
|
|
7331
|
+
const queueStore = new QueueStore({ dir: path18.join(wpaths.projectSessions, session.id) });
|
|
6994
7332
|
const ctxSignal = new AbortController().signal;
|
|
6995
7333
|
const context = new Context({ systemPrompt, provider, session, signal: ctxSignal, tokenCounter, cwd, projectRoot, model: config.model });
|
|
6996
7334
|
if (restoredMessages.length > 0) context.state.replaceMessages(restoredMessages);
|
|
6997
|
-
const todosCheckpointPath =
|
|
7335
|
+
const todosCheckpointPath = path18.join(wpaths.projectSessions, `${session.id}.todos.json`);
|
|
6998
7336
|
if (resumeId) {
|
|
6999
7337
|
try {
|
|
7000
7338
|
const restoredTodos = await loadTodosCheckpoint(todosCheckpointPath);
|
|
@@ -7006,12 +7344,12 @@ async function setupSession(params) {
|
|
|
7006
7344
|
}
|
|
7007
7345
|
}
|
|
7008
7346
|
const detachTodosCheckpoint = attachTodosCheckpoint(context.state, todosCheckpointPath, session.id);
|
|
7009
|
-
const planPath =
|
|
7347
|
+
const planPath = path18.join(wpaths.projectSessions, `${session.id}.plan.json`);
|
|
7010
7348
|
context.state.setMeta("plan.path", planPath);
|
|
7011
7349
|
if (resumeId) {
|
|
7012
7350
|
try {
|
|
7013
|
-
const fleetRoot =
|
|
7014
|
-
const dirState = await loadDirectorState(
|
|
7351
|
+
const fleetRoot = path18.join(wpaths.projectSessions, session.id);
|
|
7352
|
+
const dirState = await loadDirectorState(path18.join(fleetRoot, "director-state.json"));
|
|
7015
7353
|
if (dirState) {
|
|
7016
7354
|
const tCounts = {};
|
|
7017
7355
|
for (const t of dirState.tasks) tCounts[t.status] = (tCounts[t.status] ?? 0) + 1;
|
|
@@ -7038,7 +7376,7 @@ function resolveBundledSkillsDir2() {
|
|
|
7038
7376
|
try {
|
|
7039
7377
|
const req2 = createRequire(import.meta.url);
|
|
7040
7378
|
const corePkg = req2.resolve("@wrongstack/core/package.json");
|
|
7041
|
-
return
|
|
7379
|
+
return path18.join(path18.dirname(corePkg), "skills");
|
|
7042
7380
|
} catch {
|
|
7043
7381
|
return void 0;
|
|
7044
7382
|
}
|
|
@@ -7125,7 +7463,7 @@ async function main(argv) {
|
|
|
7125
7463
|
modeId,
|
|
7126
7464
|
modePrompt,
|
|
7127
7465
|
modelCapabilities,
|
|
7128
|
-
planPath: () => sessionRef.current ?
|
|
7466
|
+
planPath: () => sessionRef.current ? path18.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
|
|
7129
7467
|
})
|
|
7130
7468
|
);
|
|
7131
7469
|
const toolRegistry = new ToolRegistry();
|
|
@@ -7153,7 +7491,7 @@ async function main(argv) {
|
|
|
7153
7491
|
name: "session-store",
|
|
7154
7492
|
check: async () => {
|
|
7155
7493
|
try {
|
|
7156
|
-
await
|
|
7494
|
+
await fs5.access(wpaths.projectSessions);
|
|
7157
7495
|
return { status: "healthy" };
|
|
7158
7496
|
} catch (e) {
|
|
7159
7497
|
return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
|
|
@@ -7170,7 +7508,7 @@ async function main(argv) {
|
|
|
7170
7508
|
const dumpMetrics = () => {
|
|
7171
7509
|
if (!metricsSink) return;
|
|
7172
7510
|
try {
|
|
7173
|
-
const out =
|
|
7511
|
+
const out = path18.join(wpaths.projectSessions, "metrics.json");
|
|
7174
7512
|
const snap = metricsSink.snapshot();
|
|
7175
7513
|
writeFileSync(out, JSON.stringify(snap, null, 2));
|
|
7176
7514
|
} catch {
|
|
@@ -7389,12 +7727,12 @@ async function main(argv) {
|
|
|
7389
7727
|
const directorMode = flags["director"] === true;
|
|
7390
7728
|
let director = null;
|
|
7391
7729
|
let autonomyMode = "off";
|
|
7392
|
-
const fleetRoot = directorMode ?
|
|
7393
|
-
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] :
|
|
7394
|
-
const sharedScratchpadPath = directorMode ?
|
|
7395
|
-
const subagentSessionsRoot = directorMode ?
|
|
7396
|
-
const stateCheckpointPath = directorMode ?
|
|
7397
|
-
const fleetRootForPromotion =
|
|
7730
|
+
const fleetRoot = directorMode ? path18.join(wpaths.projectSessions, session.id) : void 0;
|
|
7731
|
+
const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path18.join(fleetRoot, "fleet.json") : void 0;
|
|
7732
|
+
const sharedScratchpadPath = directorMode ? path18.join(fleetRoot, "shared") : void 0;
|
|
7733
|
+
const subagentSessionsRoot = directorMode ? path18.join(fleetRoot, "subagents") : void 0;
|
|
7734
|
+
const stateCheckpointPath = directorMode ? path18.join(fleetRoot, "director-state.json") : void 0;
|
|
7735
|
+
const fleetRootForPromotion = path18.join(wpaths.projectSessions, session.id);
|
|
7398
7736
|
const multiAgentHost = new MultiAgentHost(
|
|
7399
7737
|
{
|
|
7400
7738
|
container,
|
|
@@ -7573,27 +7911,27 @@ async function main(argv) {
|
|
|
7573
7911
|
return `Unknown fleet action: ${action}`;
|
|
7574
7912
|
},
|
|
7575
7913
|
onFleetLog: async (subagentId, mode) => {
|
|
7576
|
-
const subagentsRoot =
|
|
7914
|
+
const subagentsRoot = path18.join(fleetRootForPromotion, "subagents");
|
|
7577
7915
|
let runDirs;
|
|
7578
7916
|
try {
|
|
7579
|
-
runDirs = await
|
|
7917
|
+
runDirs = await fs5.readdir(subagentsRoot);
|
|
7580
7918
|
} catch {
|
|
7581
7919
|
return "No fleet transcripts on disk \u2014 no subagents have been spawned for this session.";
|
|
7582
7920
|
}
|
|
7583
7921
|
const found = [];
|
|
7584
7922
|
for (const runId of runDirs) {
|
|
7585
|
-
const runDir =
|
|
7923
|
+
const runDir = path18.join(subagentsRoot, runId);
|
|
7586
7924
|
let files;
|
|
7587
7925
|
try {
|
|
7588
|
-
files = await
|
|
7926
|
+
files = await fs5.readdir(runDir);
|
|
7589
7927
|
} catch {
|
|
7590
7928
|
continue;
|
|
7591
7929
|
}
|
|
7592
7930
|
for (const f of files) {
|
|
7593
7931
|
if (!f.endsWith(".jsonl")) continue;
|
|
7594
|
-
const full =
|
|
7932
|
+
const full = path18.join(runDir, f);
|
|
7595
7933
|
try {
|
|
7596
|
-
const stat2 = await
|
|
7934
|
+
const stat2 = await fs5.stat(full);
|
|
7597
7935
|
found.push({
|
|
7598
7936
|
runId,
|
|
7599
7937
|
subagentId: f.replace(/\.jsonl$/, ""),
|
|
@@ -7632,7 +7970,7 @@ async function main(argv) {
|
|
|
7632
7970
|
].join("\n");
|
|
7633
7971
|
}
|
|
7634
7972
|
const t = matches[0];
|
|
7635
|
-
const raw = await
|
|
7973
|
+
const raw = await fs5.readFile(t.file, "utf8");
|
|
7636
7974
|
if (mode === "raw") return raw;
|
|
7637
7975
|
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7638
7976
|
const counts = {};
|
|
@@ -7688,7 +8026,7 @@ async function main(argv) {
|
|
|
7688
8026
|
}
|
|
7689
8027
|
const dir = await multiAgentHost.ensureDirector();
|
|
7690
8028
|
if (!dir) return "Director is not available.";
|
|
7691
|
-
const dirStatePath =
|
|
8029
|
+
const dirStatePath = path18.join(fleetRootForPromotion, "director-state.json");
|
|
7692
8030
|
const prior = await loadDirectorState(dirStatePath);
|
|
7693
8031
|
if (!prior) {
|
|
7694
8032
|
return "No prior director-state.json found \u2014 nothing to retry.";
|
|
@@ -7759,9 +8097,9 @@ async function main(argv) {
|
|
|
7759
8097
|
for (const tool of director2.tools(FLEET_ROSTER)) {
|
|
7760
8098
|
toolRegistry.register(tool);
|
|
7761
8099
|
}
|
|
7762
|
-
const mp =
|
|
7763
|
-
const sp =
|
|
7764
|
-
const ss =
|
|
8100
|
+
const mp = path18.join(fleetRootForPromotion, "fleet.json");
|
|
8101
|
+
const sp = path18.join(fleetRootForPromotion, "shared");
|
|
8102
|
+
const ss = path18.join(fleetRootForPromotion, "subagents");
|
|
7765
8103
|
const lines = [
|
|
7766
8104
|
`${color.green("\u2713")} Promoted to director mode.`,
|
|
7767
8105
|
` Roster: ${Object.keys(FLEET_ROSTER).join(", ")}`,
|
|
@@ -7807,6 +8145,24 @@ Restart WrongStack to load or unload plugin code in this session.`;
|
|
|
7807
8145
|
onExit: () => {
|
|
7808
8146
|
void mcpRegistry.stopAll();
|
|
7809
8147
|
},
|
|
8148
|
+
onBeforeExit: async () => {
|
|
8149
|
+
const { spawn: spawn2 } = await import('child_process');
|
|
8150
|
+
const cwd2 = projectRoot;
|
|
8151
|
+
const statusResult = await new Promise((resolve3) => {
|
|
8152
|
+
const child = spawn2("git", ["status", "--porcelain"], { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"] });
|
|
8153
|
+
let stdout = "";
|
|
8154
|
+
child.stdout?.on("data", (d) => stdout += d);
|
|
8155
|
+
child.on("close", (code) => resolve3({ stdout, code: code ?? 0 }));
|
|
8156
|
+
});
|
|
8157
|
+
if (statusResult.stdout.trim().length > 0) {
|
|
8158
|
+
const lines = statusResult.stdout.split("\n").filter(Boolean);
|
|
8159
|
+
return {
|
|
8160
|
+
abort: true,
|
|
8161
|
+
// signals there are uncommitted changes (used only for the message)
|
|
8162
|
+
message: `\u26A0 ${color.yellow(`${lines.length} uncommitted change${lines.length > 1 ? "s" : ""}`)} \u2014 session ended without commit`
|
|
8163
|
+
};
|
|
8164
|
+
}
|
|
8165
|
+
},
|
|
7810
8166
|
onClear: () => {
|
|
7811
8167
|
if (flags.tui && !flags["no-tui"]) return;
|
|
7812
8168
|
try {
|