@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 CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import * as path17 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, 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 fs14 from 'fs/promises';
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 = path17.join(projectRoot, ".wrongstack", "specs");
240
- const graphsDir = path17.join(projectRoot, ".wrongstack", "task-graphs");
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 = path17.join(projectRoot, ".wrongstack", "sdd-session.json");
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: path17.join(projectRoot, ".wrongstack", "sdd-session.json")
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 = path17.join(projectRoot, ".wrongstack", "sdd-session.json");
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 = path17.join(projectRoot, ".wrongstack", "sdd-session.json");
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 = path17.join(projectRoot, "package.json");
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 = path17.join(projectRoot, "tsconfig.json");
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 = path17.join(projectRoot, "src");
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 fs14.readFile(opts.globalConfigPath, "utf8");
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 = path17.join(path17.dirname(opts.globalConfigPath), ".key");
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 fs14.readFile(opts.globalConfigPath, "utf8");
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 = path17.join(path17.dirname(opts.globalConfigPath), ".key");
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" ? path17.resolve(flags["cwd"]) : process.cwd();
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 fs14.mkdir(paths.projectDir, { recursive: true });
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 fs14.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
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 ?? path17.join(os4.homedir(), ".wrongstack", "history");
1634
+ this.historyFile = opts.historyFile ?? path18.join(os4.homedir(), ".wrongstack", "history");
1634
1635
  }
1635
1636
  async loadHistory() {
1636
1637
  try {
1637
- const raw = await fs14.readFile(this.historyFile, "utf8");
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 fs14.mkdir(path17.dirname(this.historyFile), { recursive: true });
1646
- await fs14.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
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 fs14.access(file);
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(path17.join(root, "pnpm-lock.yaml"))) return "pnpm";
2128
- if (await pathExists(path17.join(root, "bun.lockb"))) return "bun";
2129
- if (await pathExists(path17.join(root, "bun.lock"))) return "bun";
2130
- if (await pathExists(path17.join(root, "yarn.lock"))) return "yarn";
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 fs14.readFile(path17.join(root, "package.json"), "utf8"));
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(path17.join(root, "pyproject.toml"))) throw new Error("not python");
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(path17.join(root, "go.mod"))) throw new Error("not go");
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(path17.join(root, "Cargo.toml"))) throw new Error("not rust");
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 fs14.readFile(path17.join(root, "Makefile"), "utf8");
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 = path17.join(ctx.projectRoot, ".wrongstack");
2653
- const file = path17.join(dir, "AGENTS.md");
2822
+ const dir = path18.join(ctx.projectRoot, ".wrongstack");
2823
+ const file = path18.join(dir, "AGENTS.md");
2654
2824
  try {
2655
- await fs14.access(file);
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 fs14.mkdir(dir, { recursive: true });
2666
- await fs14.writeFile(file, body, "utf8");
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 = path17.join(os4.homedir(), ".wrongstack");
3444
+ const globalRoot = path18.join(os4.homedir(), ".wrongstack");
3268
3445
  return new SkillInstaller({
3269
- manifestPath: path17.join(globalRoot, "installed-skills.json"),
3270
- projectSkillsDir: path17.join(projectRoot, ".wrongstack", "skills"),
3271
- globalSkillsDir: path17.join(globalRoot, "skills"),
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 fs14.access(path17.join(projectRoot, ".wrongstack", "AGENTS.md"));
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 fs14.access(path17.join(projectRoot, m));
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 = path17.join(projectRoot, ".wrongstack");
3494
- const file = path17.join(dir, "AGENTS.md");
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 fs14.mkdir(dir, { recursive: true });
3498
- await fs14.writeFile(file, body, "utf8");
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(`(${path17.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
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
- renderer.write(
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 path18 = typeof o["path"] === "string" ? o["path"] : "";
4015
+ const path19 = typeof o["path"] === "string" ? o["path"] : "";
3804
4016
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
3805
- return `${path18} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
4017
+ return `${path19} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
3806
4018
  }
3807
4019
  if (name === "write") {
3808
- const path18 = typeof o["path"] === "string" ? o["path"] : "";
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 ? `${path18} ${bytes}B` : path18;
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 fs14.readFile(deps.globalConfigPath, "utf8");
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 fs14.readFile(deps.globalConfigPath, "utf8");
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 fs14.access(deps.paths.secretsKey);
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 fs14.mkdir(deps.paths.projectSessions, { recursive: true });
4573
- const probe = path17.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
4574
- await fs14.writeFile(probe, "");
4575
- await fs14.unlink(probe);
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 fs14.mkdir(path17.dirname(path17.resolve(deps.cwd, output)), { recursive: true });
4677
- await fs14.writeFile(path17.resolve(deps.cwd, output), rendered, "utf8");
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 fs14.mkdir(deps.paths.globalRoot, { recursive: true });
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 = path17.join(path17.dirname(deps.paths.globalConfig), ".key");
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 fs14.mkdir(path17.join(deps.projectRoot, ".wrongstack"), { recursive: true });
4742
- const agentsFile = path17.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
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 fs14.access(agentsFile);
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 fs14.readFile(deps.paths.globalConfig, "utf8"));
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 fs14.readFile(deps.paths.globalConfig, "utf8"));
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 fs14.readFile(file, "utf8"));
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 = path17.join(deps.paths.globalRoot, "projects");
5264
+ const projectsRoot = path18.join(deps.paths.globalRoot, "projects");
5053
5265
  try {
5054
- const entries = await fs14.readdir(projectsRoot);
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 fs14.readFile(path17.join(projectsRoot, hash, "meta.json"), "utf8")
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, color30) {
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 ? color30.dim(` [${errKind}]`) : "";
5343
- const errSnip = errMsg || errKind ? `${errKindChip}${color30.dim(errTail)}` : "";
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: color30.green("\u2713"), stats, tail: "" };
5684
+ return { mark: color32.green("\u2713"), stats, tail: "" };
5347
5685
  case "timeout":
5348
- return { mark: color30.yellow("\u23F1"), stats: `${color30.yellow("timeout")} ${stats}`, tail: errSnip };
5686
+ return { mark: color32.yellow("\u23F1"), stats: `${color32.yellow("timeout")} ${stats}`, tail: errSnip };
5349
5687
  case "stopped":
5350
- return { mark: color30.dim("\u2298"), stats: `${color30.dim("stopped")} ${stats}`, tail: errSnip };
5688
+ return { mark: color32.dim("\u2298"), stats: `${color32.dim("stopped")} ${stats}`, tail: errSnip };
5351
5689
  case "failed":
5352
- return { mark: color30.red("\u2717"), stats: `${color30.red("failed")} ${stats}`, tail: errSnip };
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 path17.join(path17.dirname(corePkg), "skills");
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: path17.basename(projectRoot) || void 0,
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: path17.basename(projectRoot) || void 0,
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 ? path17.join(this.sessionFactory.dir, `${subagentConfig.name}.jsonl`) : void 0;
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 = path17.join(this.opts.fleetRoot, "fleet.json");
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 = path17.join(this.opts.fleetRoot, "shared");
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 = path17.join(this.opts.fleetRoot, "subagents");
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 = path17.join(this.opts.fleetRoot, "director-state.json");
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 path18 = typeof input?.path === "string" ? input.path : void 0;
6658
- if (e.name === "read" && path18) this.readPaths.add(path18);
6659
- else if (e.name === "edit" && path18) this.editedPaths.add(path18);
6660
- else if (e.name === "write" && path18) {
6661
- this.writtenPaths.add(path18);
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: path17.join(wpaths.projectSessions, session.id, "attachments") });
6993
- const queueStore = new QueueStore({ dir: path17.join(wpaths.projectSessions, session.id) });
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 = path17.join(wpaths.projectSessions, `${session.id}.todos.json`);
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 = path17.join(wpaths.projectSessions, `${session.id}.plan.json`);
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 = path17.join(wpaths.projectSessions, session.id);
7014
- const dirState = await loadDirectorState(path17.join(fleetRoot, "director-state.json"));
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 path17.join(path17.dirname(corePkg), "skills");
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 ? path17.join(wpaths.projectSessions, `${sessionRef.current.id}.plan.json`) : void 0
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 fs14.access(wpaths.projectSessions);
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 = path17.join(wpaths.projectSessions, "metrics.json");
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 ? path17.join(wpaths.projectSessions, session.id) : void 0;
7393
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path17.join(fleetRoot, "fleet.json") : void 0;
7394
- const sharedScratchpadPath = directorMode ? path17.join(fleetRoot, "shared") : void 0;
7395
- const subagentSessionsRoot = directorMode ? path17.join(fleetRoot, "subagents") : void 0;
7396
- const stateCheckpointPath = directorMode ? path17.join(fleetRoot, "director-state.json") : void 0;
7397
- const fleetRootForPromotion = path17.join(wpaths.projectSessions, session.id);
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 = path17.join(fleetRootForPromotion, "subagents");
7914
+ const subagentsRoot = path18.join(fleetRootForPromotion, "subagents");
7577
7915
  let runDirs;
7578
7916
  try {
7579
- runDirs = await fs14.readdir(subagentsRoot);
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 = path17.join(subagentsRoot, runId);
7923
+ const runDir = path18.join(subagentsRoot, runId);
7586
7924
  let files;
7587
7925
  try {
7588
- files = await fs14.readdir(runDir);
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 = path17.join(runDir, f);
7932
+ const full = path18.join(runDir, f);
7595
7933
  try {
7596
- const stat2 = await fs14.stat(full);
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 fs14.readFile(t.file, "utf8");
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 = path17.join(fleetRootForPromotion, "director-state.json");
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 = path17.join(fleetRootForPromotion, "fleet.json");
7763
- const sp = path17.join(fleetRootForPromotion, "shared");
7764
- const ss = path17.join(fleetRootForPromotion, "subagents");
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 {