@mtreeai/msapling-cli 2.3.6-beta.46 → 2.3.6-beta.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1422 -402
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2194,7 +2194,7 @@ var init_RunCommandTool = __esm({
2194
2194
  this.activeCommands++;
2195
2195
  return;
2196
2196
  }
2197
- return new Promise((resolve20) => this.queue.push(resolve20));
2197
+ return new Promise((resolve21) => this.queue.push(resolve21));
2198
2198
  }
2199
2199
  static releaseLock() {
2200
2200
  if (this.queue.length > 0) {
@@ -2274,9 +2274,9 @@ var init_RunCommandTool = __esm({
2274
2274
  const chunks = { stdout: [], stderr: [] };
2275
2275
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
2276
2276
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
2277
- const exitCode = await new Promise((resolve20) => {
2278
- proc.on("exit", (code) => resolve20(code ?? 1));
2279
- proc.on("error", () => resolve20(1));
2277
+ const exitCode = await new Promise((resolve21) => {
2278
+ proc.on("exit", (code) => resolve21(code ?? 1));
2279
+ proc.on("error", () => resolve21(1));
2280
2280
  });
2281
2281
  const stdout = Buffer.concat(chunks.stdout).toString("utf-8");
2282
2282
  const stderr = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -2434,7 +2434,7 @@ var init_src2 = __esm({
2434
2434
  const message = `Content-Length: ${Buffer.byteLength(content, "utf8")}\r
2435
2435
  \r
2436
2436
  ${content}`;
2437
- return new Promise((resolve20, reject) => {
2437
+ return new Promise((resolve21, reject) => {
2438
2438
  const timeoutHandle = setTimeout(() => {
2439
2439
  this.pendingRequests.delete(id);
2440
2440
  reject(new Error(`LSP request timeout after ${timeoutMs}ms: ${method}`));
@@ -2442,7 +2442,7 @@ ${content}`;
2442
2442
  this.pendingRequests.set(id, {
2443
2443
  resolve: (response) => {
2444
2444
  clearTimeout(timeoutHandle);
2445
- resolve20(response);
2445
+ resolve21(response);
2446
2446
  },
2447
2447
  reject: (error) => {
2448
2448
  clearTimeout(timeoutHandle);
@@ -2595,9 +2595,9 @@ var init_SubShellTool = __esm({
2595
2595
  }
2596
2596
  throw e;
2597
2597
  }
2598
- await new Promise((resolve20) => {
2599
- proc.on("exit", () => resolve20());
2600
- proc.on("error", () => resolve20());
2598
+ await new Promise((resolve21) => {
2599
+ proc.on("exit", () => resolve21());
2600
+ proc.on("error", () => resolve21());
2601
2601
  });
2602
2602
  return { content: `Successfully launched separate window for ${args2.worker_id}` };
2603
2603
  }
@@ -2676,27 +2676,42 @@ async function* walkFiles(root, current = root) {
2676
2676
  }
2677
2677
  }
2678
2678
  }
2679
+ function rgCandidates() {
2680
+ if (process.platform === "win32") {
2681
+ return [
2682
+ "rg.exe",
2683
+ "rg",
2684
+ "C:\\Program Files\\ripgrep\\rg.exe",
2685
+ "C:\\ProgramData\\chocolatey\\bin\\rg.exe"
2686
+ ];
2687
+ }
2688
+ return ["rg", "/usr/bin/rg", "/usr/local/bin/rg", "/opt/homebrew/bin/rg"];
2689
+ }
2679
2690
  async function findRg() {
2680
- const candidates = ["rg", "C:\\Program Files\\ripgrep\\rg.exe"];
2681
- for (const bin of candidates) {
2691
+ if (_rgCache.resolved) return _rgCache.value;
2692
+ for (const bin of rgCandidates()) {
2682
2693
  try {
2683
- const exited = await new Promise((resolve20) => {
2694
+ const exited = await new Promise((resolve21) => {
2684
2695
  try {
2685
2696
  const p = spawn4(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
2686
- p.on("error", () => resolve20(null));
2687
- p.on("exit", (code) => resolve20(code));
2697
+ p.on("error", () => resolve21(null));
2698
+ p.on("exit", (code) => resolve21(code));
2688
2699
  } catch {
2689
- resolve20(null);
2700
+ resolve21(null);
2690
2701
  }
2691
2702
  });
2692
- if (exited === 0) return bin;
2703
+ if (exited === 0) {
2704
+ _rgCache = { resolved: true, value: bin };
2705
+ return bin;
2706
+ }
2693
2707
  } catch {
2694
2708
  }
2695
2709
  }
2710
+ _rgCache = { resolved: true, value: null };
2696
2711
  return null;
2697
2712
  }
2698
2713
  function runRg(bin, args2) {
2699
- return new Promise((resolve20) => {
2714
+ return new Promise((resolve21) => {
2700
2715
  const p = spawn4(bin, args2, { stdio: ["ignore", "pipe", "pipe"] });
2701
2716
  let stdout = "";
2702
2717
  let stderr = "";
@@ -2707,17 +2722,24 @@ function runRg(bin, args2) {
2707
2722
  stderr += d.toString("utf8");
2708
2723
  });
2709
2724
  p.on("error", (e) => {
2710
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2725
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2711
2726
  });
2712
2727
  p.on("exit", (code) => {
2713
- resolve20({ stdout, stderr, exitCode: code });
2728
+ resolve21({ stdout, stderr, exitCode: code });
2714
2729
  });
2715
2730
  });
2716
2731
  }
2717
- async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive) {
2732
+ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive, include, exclude) {
2718
2733
  const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2719
2734
  const matches2 = [];
2735
+ const relPaths = [];
2720
2736
  for await (const relPath of walkFiles(searchRoot)) {
2737
+ if (include && !include.test(relPath)) continue;
2738
+ if (exclude && exclude.test(relPath)) continue;
2739
+ relPaths.push(relPath);
2740
+ }
2741
+ relPaths.sort();
2742
+ for (const relPath of relPaths) {
2721
2743
  if (matches2.length >= maxMatches) break;
2722
2744
  const fullPath = join6(searchRoot, relPath);
2723
2745
  try {
@@ -2737,7 +2759,7 @@ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive)
2737
2759
  }
2738
2760
  return matches2;
2739
2761
  }
2740
- var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, GrepSearchTool;
2762
+ var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, _rgCache, GrepSearchTool;
2741
2763
  var init_SearchTools = __esm({
2742
2764
  "../core/src/tools/SearchTools.ts"() {
2743
2765
  "use strict";
@@ -2825,9 +2847,13 @@ var init_SearchTools = __esm({
2825
2847
  }
2826
2848
  }
2827
2849
  };
2850
+ _rgCache = {
2851
+ resolved: false,
2852
+ value: null
2853
+ };
2828
2854
  GrepSearchTool = class extends BaseTool {
2829
2855
  name = "grep_search";
2830
- description = "Search file contents for a regex pattern. Returns matching lines with file path and line number. Uses ripgrep (rg) when available, falls back to a built-in scanner otherwise.";
2856
+ description = 'Search file contents for a regex pattern. Returns matching lines with file path and line number, sorted deterministically. Uses ripgrep (rg) when available \u2014 fast on large repos, respects .gitignore \u2014 and falls back to a built-in scanner otherwise. Optional include/exclude glob filters narrow which files are searched (e.g. include "*.ts", exclude "*.test.ts").';
2831
2857
  parameters = {
2832
2858
  type: "object",
2833
2859
  required: ["pattern"],
@@ -2843,6 +2869,14 @@ var init_SearchTools = __esm({
2843
2869
  case_sensitive: {
2844
2870
  type: "boolean",
2845
2871
  description: "Whether the search is case-sensitive (default: false)."
2872
+ },
2873
+ include: {
2874
+ type: "string",
2875
+ description: 'Optional glob of files to include (e.g. "*.ts", "src/**/*.tsx"). Passed to ripgrep as --glob.'
2876
+ },
2877
+ exclude: {
2878
+ type: "string",
2879
+ description: 'Optional glob of files to exclude (e.g. "*.test.ts", "dist/**"). Passed to ripgrep as a negated --glob.'
2846
2880
  }
2847
2881
  }
2848
2882
  };
@@ -2870,6 +2904,8 @@ var init_SearchTools = __esm({
2870
2904
  };
2871
2905
  }
2872
2906
  const caseSensitive = args2?.case_sensitive === true;
2907
+ const include = typeof args2?.include === "string" && args2.include.trim() ? args2.include.trim() : void 0;
2908
+ const exclude = typeof args2?.exclude === "string" && args2.exclude.trim() ? args2.exclude.trim() : void 0;
2873
2909
  const rg = await findRg();
2874
2910
  if (rg) {
2875
2911
  try {
@@ -2877,9 +2913,14 @@ var init_SearchTools = __esm({
2877
2913
  "--line-number",
2878
2914
  `--max-count=${MAX_GREP_MATCHES}`,
2879
2915
  "--no-heading",
2880
- "--color=never"
2916
+ "--color=never",
2917
+ // Deterministic output: sort by file path so repeated queries return
2918
+ // identical ordering (ripgrep parallelises by default → non-stable).
2919
+ "--sort=path"
2881
2920
  ];
2882
2921
  if (!caseSensitive) rgArgs.push("--ignore-case");
2922
+ if (include) rgArgs.push("--glob", include);
2923
+ if (exclude) rgArgs.push("--glob", `!${exclude}`);
2883
2924
  rgArgs.push("--", pattern, searchPath);
2884
2925
  const { stdout, stderr, exitCode } = await runRg(rg, rgArgs);
2885
2926
  if (exitCode !== 0 && exitCode !== 1) {
@@ -2899,15 +2940,28 @@ var init_SearchTools = __esm({
2899
2940
  }
2900
2941
  try {
2901
2942
  const isFile = statSync(searchPath).isFile();
2943
+ const includeRe = include ? globToRegExp(include) : void 0;
2944
+ const excludeRe = exclude ? globToRegExp(exclude) : void 0;
2902
2945
  let matches2;
2903
2946
  if (isFile) {
2904
- const content = await readFile4(searchPath, "utf8");
2905
- const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2906
- const lines = content.split("\n");
2907
2947
  const relPath = args2?.path ?? searchPath;
2908
- matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2948
+ if (includeRe && !includeRe.test(String(relPath)) || excludeRe && excludeRe.test(String(relPath))) {
2949
+ matches2 = [];
2950
+ } else {
2951
+ const content = await readFile4(searchPath, "utf8");
2952
+ const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2953
+ const lines = content.split("\n");
2954
+ matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2955
+ }
2909
2956
  } else {
2910
- matches2 = await nodeGrepFallback(pattern, searchPath, MAX_GREP_MATCHES, caseSensitive);
2957
+ matches2 = await nodeGrepFallback(
2958
+ pattern,
2959
+ searchPath,
2960
+ MAX_GREP_MATCHES,
2961
+ caseSensitive,
2962
+ includeRe,
2963
+ excludeRe
2964
+ );
2911
2965
  }
2912
2966
  if (matches2.length === 0) return { content: "No matches found." };
2913
2967
  const suffix = matches2.length >= MAX_GREP_MATCHES ? `
@@ -3400,13 +3454,164 @@ Pre-edit content backed up to: ${backedUpTo}`;
3400
3454
  }
3401
3455
  });
3402
3456
 
3457
+ // ../core/src/agent/namedAgents.ts
3458
+ import { readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync2 } from "fs";
3459
+ import { join as join9 } from "path";
3460
+ function normalizeToolName(raw) {
3461
+ const trimmed = raw.trim();
3462
+ if (!trimmed) return "";
3463
+ const lower = trimmed.toLowerCase();
3464
+ const key = lower.replace(/[^a-z0-9]/g, "");
3465
+ if (TOOL_NAME_ALIASES[key]) return TOOL_NAME_ALIASES[key];
3466
+ return lower;
3467
+ }
3468
+ function discoverAgentFiles(cwd, homeDir) {
3469
+ const seen = /* @__PURE__ */ new Map();
3470
+ for (const scope of [
3471
+ { dir: join9(homeDir, ".msapling", "agents"), scope: "user" },
3472
+ { dir: join9(cwd, ".msapling", "agents"), scope: "project" }
3473
+ ]) {
3474
+ const entries = safeReaddir(scope.dir);
3475
+ for (const fname of entries) {
3476
+ if (!fname.toLowerCase().endsWith(".md")) continue;
3477
+ const path2 = join9(scope.dir, fname);
3478
+ if (!isRegularFile(path2)) continue;
3479
+ const base = fname.slice(0, -".md".length);
3480
+ seen.set(base, { name: base, path: path2, scope: scope.scope });
3481
+ }
3482
+ }
3483
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
3484
+ }
3485
+ function safeReaddir(dir) {
3486
+ try {
3487
+ return readdirSync2(dir);
3488
+ } catch {
3489
+ return [];
3490
+ }
3491
+ }
3492
+ function isRegularFile(p) {
3493
+ try {
3494
+ return statSync3(p).isFile();
3495
+ } catch {
3496
+ return false;
3497
+ }
3498
+ }
3499
+ function parseToolsValue(raw) {
3500
+ if (raw === void 0) return void 0;
3501
+ let s = raw.trim();
3502
+ if (s === "") return void 0;
3503
+ if (s === "*" || s.toLowerCase() === "all") return void 0;
3504
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
3505
+ const parts = s.split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3506
+ return [...new Set(parts)];
3507
+ }
3508
+ function parseAgentFile(text, fallbackName) {
3509
+ const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
3510
+ if (!fm) {
3511
+ const body = text.trim();
3512
+ const firstLine = body.split(/\r?\n/).find((l) => l.trim()) ?? "";
3513
+ return {
3514
+ name: fallbackName.toLowerCase(),
3515
+ description: firstLine.replace(/^#\s+/, "").trim() || `(agent: ${fallbackName})`,
3516
+ systemPrompt: body
3517
+ };
3518
+ }
3519
+ const meta = fm[1];
3520
+ const systemPrompt = text.slice(fm[0].length).trim();
3521
+ const scalar = (key) => {
3522
+ const m = meta.match(new RegExp(`^${key}[ \\t]*:[ \\t]*(.+)$`, "m"));
3523
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
3524
+ };
3525
+ const name = (scalar("name") ?? fallbackName).toLowerCase();
3526
+ const description = scalar("description") ?? `(agent: ${name})`;
3527
+ const model = scalar("model");
3528
+ let tools;
3529
+ const inlineTools = scalar("tools");
3530
+ const blockMatch = meta.match(/^tools[ \t]*:[ \t]*\n((?:[ \t]*-[ \t]*.+\n?)+)/m);
3531
+ if (blockMatch && (inlineTools === void 0 || inlineTools === "")) {
3532
+ const items = blockMatch[1].split(/\r?\n/).map((l) => l.replace(/^[ \t]*-[ \t]*/, "").trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3533
+ tools = [...new Set(items)];
3534
+ } else {
3535
+ tools = parseToolsValue(inlineTools);
3536
+ }
3537
+ return { name, description, model, tools, systemPrompt };
3538
+ }
3539
+ function loadNamedAgents(cwd, homeDir) {
3540
+ const out = [];
3541
+ for (const file of discoverAgentFiles(cwd, homeDir)) {
3542
+ let text;
3543
+ try {
3544
+ text = readFileSync2(file.path, "utf8");
3545
+ } catch {
3546
+ continue;
3547
+ }
3548
+ const parsed = parseAgentFile(text, file.name);
3549
+ out.push({
3550
+ name: parsed.name,
3551
+ description: parsed.description,
3552
+ model: parsed.model,
3553
+ tools: parsed.tools,
3554
+ systemPrompt: parsed.systemPrompt,
3555
+ path: file.path,
3556
+ scope: file.scope
3557
+ });
3558
+ }
3559
+ const byName = /* @__PURE__ */ new Map();
3560
+ for (const a of out) {
3561
+ const prev = byName.get(a.name);
3562
+ if (!prev || prev.scope === "user" && a.scope === "project") {
3563
+ byName.set(a.name, a);
3564
+ }
3565
+ }
3566
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
3567
+ }
3568
+ function findNamedAgent(name, cwd, homeDir) {
3569
+ const target = name.trim().toLowerCase();
3570
+ return loadNamedAgents(cwd, homeDir).find((a) => a.name === target) ?? null;
3571
+ }
3572
+ var TOOL_NAME_ALIASES;
3573
+ var init_namedAgents = __esm({
3574
+ "../core/src/agent/namedAgents.ts"() {
3575
+ "use strict";
3576
+ init_esm_shims();
3577
+ TOOL_NAME_ALIASES = {
3578
+ read: "read_file",
3579
+ write: "write_file",
3580
+ edit: "edit_file",
3581
+ multiedit: "multi_edit_file",
3582
+ patch: "patch_file",
3583
+ move: "move_file",
3584
+ delete: "delete_file",
3585
+ bash: "bash_command",
3586
+ shell: "bash_command",
3587
+ run: "run_command",
3588
+ glob: "glob_files",
3589
+ grep: "grep_search",
3590
+ ls: "list_directory",
3591
+ list: "list_directory",
3592
+ webfetch: "web_fetch",
3593
+ websearch: "web_search",
3594
+ fetch: "web_fetch",
3595
+ search: "web_search",
3596
+ task: "dispatch_agent",
3597
+ dispatch: "dispatch_agent",
3598
+ todowrite: "todo_write",
3599
+ todoread: "todo_read",
3600
+ notebookread: "notebook_read",
3601
+ notebookedit: "notebook_edit_cell"
3602
+ };
3603
+ }
3604
+ });
3605
+
3403
3606
  // ../core/src/tools/DispatchAgentTool.ts
3607
+ import { homedir as homedir5 } from "os";
3404
3608
  var SUB_AGENT_TIMEOUT_MS, MAX_PROMPT_CHARS, MAX_RESPONSE_CHARS, DEFAULT_SUB_AGENT_MODEL, DispatchAgentTool;
3405
3609
  var init_DispatchAgentTool = __esm({
3406
3610
  "../core/src/tools/DispatchAgentTool.ts"() {
3407
3611
  "use strict";
3408
3612
  init_esm_shims();
3409
3613
  init_BaseTool();
3614
+ init_namedAgents();
3410
3615
  SUB_AGENT_TIMEOUT_MS = 12e4;
3411
3616
  MAX_PROMPT_CHARS = 8e3;
3412
3617
  MAX_RESPONSE_CHARS = 16e3;
@@ -3422,6 +3627,10 @@ var init_DispatchAgentTool = __esm({
3422
3627
  type: "string",
3423
3628
  description: `The full self-contained prompt for the sub-agent. Include all necessary context \u2014 the sub-agent does not have access to prior conversation history. Maximum ${MAX_PROMPT_CHARS} characters.`
3424
3629
  },
3630
+ agent: {
3631
+ type: "string",
3632
+ description: "Optional NAMED subagent to run (defined in .msapling/agents/<name>.md). The named agent supplies its own system prompt, model, and tool allowlist \u2014 unlike the anonymous dispatch path it CAN run tools (gated by the permission system). Run /agents to list available named agents. When omitted, an anonymous read/reasoning sub-agent runs."
3633
+ },
3425
3634
  description: {
3426
3635
  type: "string",
3427
3636
  description: 'Short human-readable label for this sub-task (e.g. "Summarise auth.ts"). Appears in the session log alongside the result.'
@@ -3440,12 +3649,19 @@ var init_DispatchAgentTool = __esm({
3440
3649
  parentChatId;
3441
3650
  projectRoot;
3442
3651
  onSubagentStop;
3652
+ runNamedAgent;
3443
3653
  constructor(options) {
3444
3654
  super();
3445
3655
  this.client = options.client;
3446
3656
  this.parentChatId = options.parentChatId ?? void 0;
3447
3657
  this.projectRoot = options.projectRoot;
3448
3658
  this.onSubagentStop = options.onSubagentStop;
3659
+ this.runNamedAgent = options.runNamedAgent;
3660
+ }
3661
+ /** Resolve the user home dir, preferring env overrides so test isolation
3662
+ * (HOME / USERPROFILE) is honored exactly like outputStyle.ts. */
3663
+ resolveHome() {
3664
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
3449
3665
  }
3450
3666
  /** Fire the subagent-stop callback defensively — it must never throw. */
3451
3667
  signalStop(info) {
@@ -3462,9 +3678,19 @@ var init_DispatchAgentTool = __esm({
3462
3678
  };
3463
3679
  }
3464
3680
  const prompt4 = args2.prompt.trim().slice(0, MAX_PROMPT_CHARS);
3465
- const model = typeof args2.model === "string" && args2.model.trim() ? args2.model.trim() : DEFAULT_SUB_AGENT_MODEL;
3466
3681
  const chatId = typeof args2.chat_id === "string" && args2.chat_id.trim() ? args2.chat_id.trim() : this.parentChatId;
3467
- const description = typeof args2.description === "string" && args2.description.trim() ? args2.description.trim() : "sub-task";
3682
+ let named = null;
3683
+ if (typeof args2.agent === "string" && args2.agent.trim()) {
3684
+ named = findNamedAgent(args2.agent.trim(), this.projectRoot, this.resolveHome());
3685
+ if (!named) {
3686
+ return {
3687
+ content: `dispatch_agent error: no named agent "${args2.agent.trim()}" found in .msapling/agents/. Run /agents to list available agents.`,
3688
+ isError: true
3689
+ };
3690
+ }
3691
+ }
3692
+ const model = typeof args2.model === "string" && args2.model.trim() ? args2.model.trim() : named?.model && named.model.trim() ? named.model.trim() : DEFAULT_SUB_AGENT_MODEL;
3693
+ const description = typeof args2.description === "string" && args2.description.trim() ? args2.description.trim() : named ? `agent:${named.name}` : "sub-task";
3468
3694
  let response = "";
3469
3695
  const timeoutPromise = new Promise(
3470
3696
  (_, reject) => setTimeout(
@@ -3473,10 +3699,28 @@ var init_DispatchAgentTool = __esm({
3473
3699
  )
3474
3700
  );
3475
3701
  const streamPromise = (async () => {
3702
+ if (named && this.runNamedAgent) {
3703
+ await this.runNamedAgent({
3704
+ agent: named,
3705
+ prompt: prompt4,
3706
+ model,
3707
+ chatId,
3708
+ onContent: (chunk) => {
3709
+ response += chunk;
3710
+ }
3711
+ });
3712
+ return;
3713
+ }
3714
+ const effectivePrompt = named ? `[System prompt for agent "${named.name}"]
3715
+ ${named.systemPrompt}
3716
+
3717
+ ---
3718
+
3719
+ ${prompt4}`.slice(0, MAX_PROMPT_CHARS) : prompt4;
3476
3720
  const stream = this.client.streamChat({
3477
- prompt: prompt4,
3721
+ prompt: effectivePrompt,
3478
3722
  model,
3479
- // No tools — sub-agent is read/reasoning only.
3723
+ // No tools — sub-agent is read/reasoning only on this path.
3480
3724
  tools: [],
3481
3725
  ...chatId ? { chat_id: chatId } : {},
3482
3726
  // Forward project root as context so the backend can enrich the prompt
@@ -4051,12 +4295,12 @@ Command: ${command}`,
4051
4295
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
4052
4296
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
4053
4297
  const timeoutPromise = new Promise(
4054
- (resolve20) => setTimeout(() => resolve20("timeout"), timeoutMs)
4298
+ (resolve21) => setTimeout(() => resolve21("timeout"), timeoutMs)
4055
4299
  );
4056
4300
  const processPromise = (async () => {
4057
- const exitCode2 = await new Promise((resolve20) => {
4058
- proc.on("exit", (code) => resolve20(code ?? 1));
4059
- proc.on("error", () => resolve20(1));
4301
+ const exitCode2 = await new Promise((resolve21) => {
4302
+ proc.on("exit", (code) => resolve21(code ?? 1));
4303
+ proc.on("error", () => resolve21(1));
4060
4304
  });
4061
4305
  const stdout2 = Buffer.concat(chunks.stdout).toString("utf-8");
4062
4306
  const stderr2 = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -4090,8 +4334,315 @@ ${stderr}`;
4090
4334
  }
4091
4335
  });
4092
4336
 
4337
+ // ../core/src/tools/BackgroundShellTool.ts
4338
+ import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8 } from "path";
4339
+ import { spawn as spawn6 } from "child_process";
4340
+ function formatBackgroundShells(shells) {
4341
+ if (shells.length === 0) return "No background shells.";
4342
+ return shells.map((s) => {
4343
+ const dur = ((s.endedAt ?? Date.now()) - s.startedAt) / 1e3;
4344
+ const exit = s.exitCode !== void 0 && s.exitCode !== null ? ` exit=${s.exitCode}` : "";
4345
+ const pid = s.pid !== void 0 ? ` pid=${s.pid}` : "";
4346
+ return `[${s.id}] ${s.status.toUpperCase()}${pid}${exit} ${dur.toFixed(1)}s $ ${s.command}`;
4347
+ }).join("\n");
4348
+ }
4349
+ function resolveCwd(projectRoot, rawArg) {
4350
+ let cwd = projectRoot;
4351
+ if (typeof rawArg === "string" && rawArg.trim()) {
4352
+ const raw = rawArg.trim();
4353
+ const resolved = isAbsolute8(raw) ? normalize7(raw) : resolve8(projectRoot, raw);
4354
+ const rel = relative8(projectRoot, resolved);
4355
+ if (rel.startsWith("..") || isAbsolute8(rel)) {
4356
+ return { error: `Security Block: cwd "${raw}" resolves outside the project root.` };
4357
+ }
4358
+ cwd = resolved;
4359
+ }
4360
+ return { cwd };
4361
+ }
4362
+ var MAX_BUFFER_BYTES, MAX_FINISHED_RETAINED, BackgroundShellRegistryImpl, BackgroundShellRegistry, BashBackgroundTool, ListBackgroundShellsTool, ReadBackgroundShellTool, KillBackgroundShellTool;
4363
+ var init_BackgroundShellTool = __esm({
4364
+ "../core/src/tools/BackgroundShellTool.ts"() {
4365
+ "use strict";
4366
+ init_esm_shims();
4367
+ init_BaseTool();
4368
+ init_BashTool();
4369
+ MAX_BUFFER_BYTES = 256 * 1024;
4370
+ MAX_FINISHED_RETAINED = 50;
4371
+ BackgroundShellRegistryImpl = class {
4372
+ shells = /* @__PURE__ */ new Map();
4373
+ counter = 0;
4374
+ /** Monotonic, human-readable id: bg-1, bg-2, … */
4375
+ nextId() {
4376
+ this.counter += 1;
4377
+ return `bg-${this.counter}`;
4378
+ }
4379
+ /**
4380
+ * Register an already-spawned child process and start buffering its output.
4381
+ * Returns the assigned shell id.
4382
+ */
4383
+ register(proc, command, cwd) {
4384
+ const id = this.nextId();
4385
+ const entry = {
4386
+ id,
4387
+ command,
4388
+ cwd,
4389
+ status: "running",
4390
+ pid: proc.pid,
4391
+ startedAt: Date.now(),
4392
+ proc,
4393
+ buffer: "",
4394
+ bufferTruncated: false
4395
+ };
4396
+ this.shells.set(id, entry);
4397
+ const append = (chunk) => {
4398
+ entry.buffer += chunk.toString("utf-8");
4399
+ if (entry.buffer.length > MAX_BUFFER_BYTES) {
4400
+ entry.buffer = entry.buffer.slice(entry.buffer.length - MAX_BUFFER_BYTES);
4401
+ entry.bufferTruncated = true;
4402
+ }
4403
+ };
4404
+ proc.stdout?.on("data", append);
4405
+ proc.stderr?.on("data", append);
4406
+ proc.on("error", (err) => {
4407
+ if (entry.status === "running") {
4408
+ entry.status = "error";
4409
+ entry.endedAt = Date.now();
4410
+ entry.buffer += `
4411
+ [spawn error: ${err.message}]`;
4412
+ }
4413
+ this.reapFinished();
4414
+ });
4415
+ proc.on("exit", (code, signal) => {
4416
+ if (entry.status === "running") {
4417
+ entry.status = "exited";
4418
+ }
4419
+ entry.exitCode = code;
4420
+ entry.endedAt = Date.now();
4421
+ if (signal) entry.buffer += `
4422
+ [terminated by signal ${signal}]`;
4423
+ this.reapFinished();
4424
+ });
4425
+ return id;
4426
+ }
4427
+ /** Evict the oldest finished shells once we exceed the retention cap. */
4428
+ reapFinished() {
4429
+ const finished = [...this.shells.values()].filter((s) => s.status !== "running").sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
4430
+ let excess = finished.length - MAX_FINISHED_RETAINED;
4431
+ for (let i = 0; i < finished.length && excess > 0; i++, excess--) {
4432
+ this.shells.delete(finished[i].id);
4433
+ }
4434
+ }
4435
+ /** Public view of all tracked shells, newest first. */
4436
+ list() {
4437
+ return [...this.shells.values()].sort((a, b) => b.startedAt - a.startedAt).map((s) => this.toInfo(s));
4438
+ }
4439
+ get(id) {
4440
+ const s = this.shells.get(id);
4441
+ return s ? this.toInfo(s) : void 0;
4442
+ }
4443
+ /**
4444
+ * Read the buffered output of a shell. `lines` (optional) tails the last N
4445
+ * lines. Returns null if the id is unknown.
4446
+ */
4447
+ readOutput(id, lines) {
4448
+ const s = this.shells.get(id);
4449
+ if (!s) return null;
4450
+ let output = s.buffer;
4451
+ if (lines && lines > 0) {
4452
+ const all = output.split("\n");
4453
+ output = all.slice(-lines).join("\n");
4454
+ }
4455
+ return { output, truncated: s.bufferTruncated };
4456
+ }
4457
+ /**
4458
+ * Terminate a running shell. Returns:
4459
+ * - 'killed' on success
4460
+ * - 'not-found' if the id is unknown
4461
+ * - 'already' if the shell had already finished
4462
+ */
4463
+ kill(id) {
4464
+ const s = this.shells.get(id);
4465
+ if (!s) return "not-found";
4466
+ if (s.status !== "running") return "already";
4467
+ s.status = "killed";
4468
+ s.endedAt = Date.now();
4469
+ try {
4470
+ s.proc.kill();
4471
+ } catch {
4472
+ }
4473
+ return "killed";
4474
+ }
4475
+ /** Kill every running shell (used on CLI shutdown). */
4476
+ killAll() {
4477
+ for (const s of this.shells.values()) {
4478
+ if (s.status === "running") {
4479
+ s.status = "killed";
4480
+ s.endedAt = Date.now();
4481
+ try {
4482
+ s.proc.kill();
4483
+ } catch {
4484
+ }
4485
+ }
4486
+ }
4487
+ }
4488
+ /** Test-only: drop all tracked shells (does not kill — caller's responsibility). */
4489
+ _reset() {
4490
+ this.shells.clear();
4491
+ this.counter = 0;
4492
+ }
4493
+ toInfo(s) {
4494
+ return {
4495
+ id: s.id,
4496
+ command: s.command,
4497
+ cwd: s.cwd,
4498
+ status: s.status,
4499
+ pid: s.pid,
4500
+ exitCode: s.exitCode,
4501
+ startedAt: s.startedAt,
4502
+ endedAt: s.endedAt
4503
+ };
4504
+ }
4505
+ };
4506
+ BackgroundShellRegistry = new BackgroundShellRegistryImpl();
4507
+ BashBackgroundTool = class extends BaseTool {
4508
+ name = "bash_background";
4509
+ description = "Launch a shell command in the BACKGROUND and return immediately with a shell id. Use for long-running processes (dev servers, watchers, `tail -f`) that should keep running while you continue working. Inspect output with read_background_shell, list with list_background_shells, and stop with kill_background_shell. Runs via bash (Unix) / cmd (Windows) so shell operators work. For commands that must finish before you proceed, use bash_command instead.";
4510
+ parameters = {
4511
+ type: "object",
4512
+ required: ["command"],
4513
+ properties: {
4514
+ command: {
4515
+ type: "string",
4516
+ description: "The shell command to run in the background. May contain pipes, redirects, and other shell operators."
4517
+ },
4518
+ cwd: {
4519
+ type: "string",
4520
+ description: "Optional working directory (relative to project root). Defaults to the project root."
4521
+ }
4522
+ }
4523
+ };
4524
+ async execute(args2, projectRoot) {
4525
+ if (!args2?.command || typeof args2.command !== "string" || args2.command.trim() === "") {
4526
+ return {
4527
+ content: 'Error: bash_background requires a non-empty "command" argument.',
4528
+ isError: true
4529
+ };
4530
+ }
4531
+ const command = args2.command;
4532
+ const block = BashTool.checkBlocklist(command);
4533
+ if (block.blocked) {
4534
+ return {
4535
+ content: `Security Block: command rejected \u2014 ${block.reason}.
4536
+ Command: ${command}`,
4537
+ isError: true
4538
+ };
4539
+ }
4540
+ const cwdResult = resolveCwd(projectRoot, args2.cwd);
4541
+ if ("error" in cwdResult) {
4542
+ return { content: cwdResult.error, isError: true };
4543
+ }
4544
+ const cwd = cwdResult.cwd;
4545
+ const isWindows = process.platform === "win32";
4546
+ const shellArgv = isWindows ? ["cmd", "/c", command] : ["bash", "-c", command];
4547
+ let proc;
4548
+ try {
4549
+ proc = spawn6(shellArgv[0], shellArgv.slice(1), {
4550
+ cwd,
4551
+ stdio: ["ignore", "pipe", "pipe"],
4552
+ shell: false
4553
+ });
4554
+ } catch (e) {
4555
+ return { content: `Error: failed to spawn background shell: ${e.message}`, isError: true };
4556
+ }
4557
+ const id = BackgroundShellRegistry.register(proc, command, cwd);
4558
+ return {
4559
+ content: `Started background shell ${id} (pid ${proc.pid ?? "?"}).
4560
+ Command: ${command}
4561
+ Use read_background_shell with shell_id="${id}" to read its output, or kill_background_shell to stop it.`
4562
+ };
4563
+ }
4564
+ };
4565
+ ListBackgroundShellsTool = class extends BaseTool {
4566
+ name = "list_background_shells";
4567
+ description = "List all background shells started in this session (running and finished), with their id, status, pid, exit code, and command.";
4568
+ parameters = {
4569
+ type: "object",
4570
+ properties: {}
4571
+ };
4572
+ async execute() {
4573
+ return { content: formatBackgroundShells(BackgroundShellRegistry.list()) };
4574
+ }
4575
+ };
4576
+ ReadBackgroundShellTool = class extends BaseTool {
4577
+ name = "read_background_shell";
4578
+ description = "Read the buffered output of a background shell by its id. Optionally tail only the last N lines.";
4579
+ parameters = {
4580
+ type: "object",
4581
+ required: ["shell_id"],
4582
+ properties: {
4583
+ shell_id: {
4584
+ type: "string",
4585
+ description: 'The background shell id (e.g. "bg-1") returned by bash_background.'
4586
+ },
4587
+ lines: {
4588
+ type: "number",
4589
+ description: "Optional: return only the last N lines of buffered output."
4590
+ }
4591
+ }
4592
+ };
4593
+ async execute(args2) {
4594
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4595
+ if (!id) {
4596
+ return { content: 'Error: read_background_shell requires a "shell_id" argument.', isError: true };
4597
+ }
4598
+ const lines = typeof args2?.lines === "number" && args2.lines > 0 ? Math.floor(args2.lines) : void 0;
4599
+ const read = BackgroundShellRegistry.readOutput(id, lines);
4600
+ if (!read) {
4601
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4602
+ }
4603
+ const info = BackgroundShellRegistry.get(id);
4604
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
4605
+ `;
4606
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
4607
+ const truncNote = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
4608
+ return { content: header + body + truncNote };
4609
+ }
4610
+ };
4611
+ KillBackgroundShellTool = class extends BaseTool {
4612
+ name = "kill_background_shell";
4613
+ description = "Terminate a running background shell by its id.";
4614
+ parameters = {
4615
+ type: "object",
4616
+ required: ["shell_id"],
4617
+ properties: {
4618
+ shell_id: {
4619
+ type: "string",
4620
+ description: 'The background shell id (e.g. "bg-1") to terminate.'
4621
+ }
4622
+ }
4623
+ };
4624
+ async execute(args2) {
4625
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4626
+ if (!id) {
4627
+ return { content: 'Error: kill_background_shell requires a "shell_id" argument.', isError: true };
4628
+ }
4629
+ const result = BackgroundShellRegistry.kill(id);
4630
+ switch (result) {
4631
+ case "killed":
4632
+ return { content: `Killed background shell ${id}.` };
4633
+ case "already":
4634
+ return { content: `Background shell ${id} had already finished.` };
4635
+ case "not-found":
4636
+ default:
4637
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4638
+ }
4639
+ }
4640
+ };
4641
+ }
4642
+ });
4643
+
4093
4644
  // ../core/src/tools/NotebookReadTool.ts
4094
- import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8, extname } from "path";
4645
+ import { resolve as resolve9, normalize as normalize8, relative as relative9, isAbsolute as isAbsolute9, extname } from "path";
4095
4646
  import { readFile as readFile6 } from "fs/promises";
4096
4647
  import { existsSync as existsSync7 } from "fs";
4097
4648
  function joinSource(source) {
@@ -4246,9 +4797,9 @@ var init_NotebookReadTool = __esm({
4246
4797
  isError: true
4247
4798
  };
4248
4799
  }
4249
- const absPath = isAbsolute8(rawPath) ? normalize7(rawPath) : resolve8(projectRoot, rawPath);
4250
- const rel = relative8(projectRoot, absPath);
4251
- if (rel.startsWith("..") || isAbsolute8(rel)) {
4800
+ const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4801
+ const rel = relative9(projectRoot, absPath);
4802
+ if (rel.startsWith("..") || isAbsolute9(rel)) {
4252
4803
  return {
4253
4804
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4254
4805
  isError: true
@@ -4316,10 +4867,10 @@ _(Note: notebook has ${nb.cells.length} cells; only first ${MAX_CELLS} shown.)_`
4316
4867
  });
4317
4868
 
4318
4869
  // ../core/src/tools/NotebookEditTool.ts
4319
- import { resolve as resolve9, normalize as normalize8, relative as relative9, isAbsolute as isAbsolute9, extname as extname2, join as join10 } from "path";
4870
+ import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, extname as extname2, join as join11 } from "path";
4320
4871
  import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
4321
4872
  import { existsSync as existsSync8 } from "fs";
4322
- import { homedir as homedir5 } from "os";
4873
+ import { homedir as homedir6 } from "os";
4323
4874
  import { randomBytes as randomBytes5 } from "crypto";
4324
4875
  function normaliseSource(source) {
4325
4876
  if (source === "") return [];
@@ -4440,9 +4991,9 @@ var init_NotebookEditTool = __esm({
4440
4991
  isError: true
4441
4992
  };
4442
4993
  }
4443
- const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4444
- const rel = relative9(projectRoot, absPath);
4445
- if (rel.startsWith("..") || isAbsolute9(rel)) {
4994
+ const absPath = isAbsolute10(rawPath) ? normalize9(rawPath) : resolve10(projectRoot, rawPath);
4995
+ const rel = relative10(projectRoot, absPath);
4996
+ if (rel.startsWith("..") || isAbsolute10(rel)) {
4446
4997
  return {
4447
4998
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4448
4999
  isError: true
@@ -4499,13 +5050,13 @@ var init_NotebookEditTool = __esm({
4499
5050
  const filename = absPath.split(/[\\/]/).pop() ?? "notebook.ipynb";
4500
5051
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4501
5052
  const suffix = randomBytes5(4).toString("hex");
4502
- const backupPath = join10(
4503
- homedir5(),
5053
+ const backupPath = join11(
5054
+ homedir6(),
4504
5055
  ".msapling",
4505
5056
  "backups",
4506
5057
  `${filename}.backup-${stamp}-${suffix}.bak`
4507
5058
  );
4508
- await mkdir3(join10(homedir5(), ".msapling", "backups"), { recursive: true });
5059
+ await mkdir3(join11(homedir6(), ".msapling", "backups"), { recursive: true });
4509
5060
  await writeFile3(backupPath, rawJson, "utf8");
4510
5061
  backedUpTo = backupPath;
4511
5062
  } catch {
@@ -4575,10 +5126,10 @@ Backup: ${backedUpTo}`;
4575
5126
  });
4576
5127
 
4577
5128
  // ../core/src/tools/MultiEditFileTool.ts
4578
- import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, join as join11 } from "path";
5129
+ import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, join as join12 } from "path";
4579
5130
  import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
4580
5131
  import { existsSync as existsSync9 } from "fs";
4581
- import { homedir as homedir6 } from "os";
5132
+ import { homedir as homedir7 } from "os";
4582
5133
  import { randomBytes as randomBytes6 } from "crypto";
4583
5134
  var MAX_EDITS, MultiEditFileTool;
4584
5135
  var init_MultiEditFileTool = __esm({
@@ -4672,9 +5223,9 @@ var init_MultiEditFileTool = __esm({
4672
5223
  };
4673
5224
  }
4674
5225
  }
4675
- const normalizedTarget = isAbsolute10(args2.path) ? normalize9(args2.path) : resolve10(projectRoot, args2.path.trim());
4676
- const rel = relative10(projectRoot, normalizedTarget);
4677
- if (rel.startsWith("..") || isAbsolute10(rel)) {
5226
+ const normalizedTarget = isAbsolute11(args2.path) ? normalize10(args2.path) : resolve11(projectRoot, args2.path.trim());
5227
+ const rel = relative11(projectRoot, normalizedTarget);
5228
+ if (rel.startsWith("..") || isAbsolute11(rel)) {
4678
5229
  return {
4679
5230
  content: `Security Block: path "${args2.path}" resolves outside the project root.`,
4680
5231
  isError: true
@@ -4742,13 +5293,13 @@ No changes were written (atomic: all-or-nothing).`,
4742
5293
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
4743
5294
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4744
5295
  const suffix = randomBytes6(4).toString("hex");
4745
- const backupPath = join11(
4746
- homedir6(),
5296
+ const backupPath = join12(
5297
+ homedir7(),
4747
5298
  ".msapling",
4748
5299
  "backups",
4749
5300
  `${filename}.backup-${stamp}-${suffix}.bak`
4750
5301
  );
4751
- await mkdir4(join11(homedir6(), ".msapling", "backups"), { recursive: true });
5302
+ await mkdir4(join12(homedir7(), ".msapling", "backups"), { recursive: true });
4752
5303
  await writeFile4(backupPath, originalContent, "utf8");
4753
5304
  backedUpTo = backupPath;
4754
5305
  } catch {
@@ -4776,14 +5327,14 @@ Pre-edit content backed up to: ${backedUpTo}`;
4776
5327
  });
4777
5328
 
4778
5329
  // ../core/src/tools/MoveFileTool.ts
4779
- import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, dirname as dirname2 } from "path";
5330
+ import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, dirname as dirname2 } from "path";
4780
5331
  import { rename, mkdir as mkdir5, copyFile, rm, stat as stat2, readdir as readdir2 } from "fs/promises";
4781
- import { existsSync as existsSync10, statSync as statSync3 } from "fs";
5332
+ import { existsSync as existsSync10, statSync as statSync4 } from "fs";
4782
5333
  import { randomBytes as randomBytes7 } from "crypto";
4783
5334
  function containedPath(p, root) {
4784
- const abs = isAbsolute11(p) ? normalize10(p) : resolve11(root, p.trim());
4785
- const rel = relative11(root, abs);
4786
- if (rel.startsWith("..") || isAbsolute11(rel)) {
5335
+ const abs = isAbsolute12(p) ? normalize11(p) : resolve12(root, p.trim());
5336
+ const rel = relative12(root, abs);
5337
+ if (rel.startsWith("..") || isAbsolute12(rel)) {
4787
5338
  return { ok: false, reason: `path "${p}" resolves outside the project root` };
4788
5339
  }
4789
5340
  return { ok: true, abs };
@@ -4792,8 +5343,8 @@ async function copyDir(src, dst) {
4792
5343
  await mkdir5(dst, { recursive: true });
4793
5344
  const entries = await readdir2(src, { withFileTypes: true });
4794
5345
  for (const entry of entries) {
4795
- const srcPath = resolve11(src, entry.name);
4796
- const dstPath = resolve11(dst, entry.name);
5346
+ const srcPath = resolve12(src, entry.name);
5347
+ const dstPath = resolve12(dst, entry.name);
4797
5348
  if (entry.isDirectory()) {
4798
5349
  await copyDir(srcPath, dstPath);
4799
5350
  } else {
@@ -4804,18 +5355,18 @@ async function copyDir(src, dst) {
4804
5355
  async function backupFile(absPath) {
4805
5356
  try {
4806
5357
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
4807
- const { homedir: homedir22 } = await import("os");
4808
- const { join: join39 } = await import("path");
5358
+ const { homedir: homedir25 } = await import("os");
5359
+ const { join: join41 } = await import("path");
4809
5360
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
4810
5361
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4811
5362
  const suffix = randomBytes7(4).toString("hex");
4812
- const backupPath = join39(
4813
- homedir22(),
5363
+ const backupPath = join41(
5364
+ homedir25(),
4814
5365
  ".msapling",
4815
5366
  "backups",
4816
5367
  `${filename}.backup-${stamp}-${suffix}.bak`
4817
5368
  );
4818
- await mkdir10(join39(homedir22(), ".msapling", "backups"), { recursive: true });
5369
+ await mkdir10(join41(homedir25(), ".msapling", "backups"), { recursive: true });
4819
5370
  const content = await readFile30(absPath, "utf8");
4820
5371
  await writeFile19(backupPath, content, "utf8");
4821
5372
  return backupPath;
@@ -4875,7 +5426,7 @@ var init_MoveFileTool = __esm({
4875
5426
  isError: true
4876
5427
  };
4877
5428
  }
4878
- const srcStat = statSync3(absSrc);
5429
+ const srcStat = statSync4(absSrc);
4879
5430
  const srcIsDir = srcStat.isDirectory();
4880
5431
  let rawDst = args2.destination.trim();
4881
5432
  const dstPrelimCheck = containedPath(rawDst, projectRoot);
@@ -4883,11 +5434,11 @@ var init_MoveFileTool = __esm({
4883
5434
  return { content: `Security Block: ${dstPrelimCheck.reason}.`, isError: true };
4884
5435
  }
4885
5436
  let absDst = dstPrelimCheck.abs;
4886
- if (existsSync10(absDst) && statSync3(absDst).isDirectory() && !srcIsDir) {
5437
+ if (existsSync10(absDst) && statSync4(absDst).isDirectory() && !srcIsDir) {
4887
5438
  const srcName = absSrc.split(/[\\/]/).pop();
4888
- absDst = resolve11(absDst, srcName);
4889
- const rel2 = relative11(projectRoot, absDst);
4890
- if (rel2.startsWith("..") || isAbsolute11(rel2)) {
5439
+ absDst = resolve12(absDst, srcName);
5440
+ const rel2 = relative12(projectRoot, absDst);
5441
+ if (rel2.startsWith("..") || isAbsolute12(rel2)) {
4891
5442
  return {
4892
5443
  content: `Security Block: adjusted destination "${absDst}" resolves outside the project root.`,
4893
5444
  isError: true
@@ -4908,7 +5459,7 @@ var init_MoveFileTool = __esm({
4908
5459
  isError: true
4909
5460
  };
4910
5461
  }
4911
- if (!statSync3(absDst).isDirectory()) {
5462
+ if (!statSync4(absDst).isDirectory()) {
4912
5463
  backedUpTo = await backupFile(absDst);
4913
5464
  }
4914
5465
  }
@@ -4967,7 +5518,7 @@ Overwritten destination backed up to: ${backedUpTo}`;
4967
5518
  });
4968
5519
 
4969
5520
  // ../core/src/tools/DeleteFileTool.ts
4970
- import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, join as join13 } from "path";
5521
+ import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13, join as join14 } from "path";
4971
5522
  import { rm as rm2, stat as stat3 } from "fs/promises";
4972
5523
  import { randomBytes as randomBytes8 } from "crypto";
4973
5524
  var DeleteFileTool;
@@ -5002,15 +5553,15 @@ var init_DeleteFileTool = __esm({
5002
5553
  }
5003
5554
  const rawPath = args2.path.trim();
5004
5555
  const recursive = args2.recursive === true;
5005
- const abs = isAbsolute12(rawPath) ? normalize11(rawPath) : resolve12(projectRoot, rawPath);
5006
- const rel = relative12(projectRoot, abs);
5007
- if (rel.startsWith("..") || isAbsolute12(rel)) {
5556
+ const abs = isAbsolute13(rawPath) ? normalize12(rawPath) : resolve13(projectRoot, rawPath);
5557
+ const rel = relative13(projectRoot, abs);
5558
+ if (rel.startsWith("..") || isAbsolute13(rel)) {
5008
5559
  return {
5009
5560
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
5010
5561
  isError: true
5011
5562
  };
5012
5563
  }
5013
- if (rel === "" || abs === normalize11(projectRoot)) {
5564
+ if (rel === "" || abs === normalize12(projectRoot)) {
5014
5565
  return {
5015
5566
  content: "Security Block: refusing to delete the project root directory.",
5016
5567
  isError: true
@@ -5043,13 +5594,13 @@ var init_DeleteFileTool = __esm({
5043
5594
  if (isFile) {
5044
5595
  try {
5045
5596
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
5046
- const { homedir: homedir22 } = await import("os");
5597
+ const { homedir: homedir25 } = await import("os");
5047
5598
  const existingContent = await readFile30(abs, "utf8");
5048
5599
  const filename = abs.split(/[\\/]/).pop() ?? "file";
5049
5600
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5050
5601
  const suffix = randomBytes8(4).toString("hex");
5051
- const backupPath = join13(homedir22(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
5052
- await mkdir10(join13(homedir22(), ".msapling", "backups"), { recursive: true });
5602
+ const backupPath = join14(homedir25(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
5603
+ await mkdir10(join14(homedir25(), ".msapling", "backups"), { recursive: true });
5053
5604
  await writeFile19(backupPath, existingContent, "utf8");
5054
5605
  backedUpTo = backupPath;
5055
5606
  } catch {
@@ -5191,7 +5742,7 @@ var init_MDrive = __esm({
5191
5742
  });
5192
5743
 
5193
5744
  // ../core/src/Sandbox.ts
5194
- import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13 } from "path";
5745
+ import { resolve as resolve14, normalize as normalize13, relative as relative14, isAbsolute as isAbsolute14 } from "path";
5195
5746
  import { realpathSync } from "fs";
5196
5747
  import { realpath as realpath2 } from "fs/promises";
5197
5748
  import { createHash as createHash3 } from "crypto";
@@ -5250,7 +5801,7 @@ var init_Sandbox = __esm({
5250
5801
  "sc"
5251
5802
  ]);
5252
5803
  constructor(projectRoot, opts) {
5253
- this.projectRoot = resolve13(projectRoot);
5804
+ this.projectRoot = resolve14(projectRoot);
5254
5805
  this.realpathSyncFn = opts?.realpathSync ?? realpathSync;
5255
5806
  }
5256
5807
  setPermissions(state) {
@@ -5265,15 +5816,15 @@ var init_Sandbox = __esm({
5265
5816
  return hasher.digest("hex");
5266
5817
  }
5267
5818
  isPathSafe(targetPath) {
5268
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5819
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5269
5820
  let resolvedTarget = normalizedTarget;
5270
5821
  try {
5271
5822
  resolvedTarget = this.realpathSyncFn(normalizedTarget);
5272
5823
  } catch {
5273
5824
  resolvedTarget = normalizedTarget;
5274
5825
  }
5275
- const rel = relative13(this.projectRoot, resolvedTarget);
5276
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5826
+ const rel = relative14(this.projectRoot, resolvedTarget);
5827
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5277
5828
  if (isOutside) {
5278
5829
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5279
5830
  return { safe: true, absolutePath: resolvedTarget };
@@ -5291,15 +5842,15 @@ var init_Sandbox = __esm({
5291
5842
  * isPathSafe (sync) is kept for cold/boot-time paths and existing tests.
5292
5843
  */
5293
5844
  async isPathSafeAsync(targetPath) {
5294
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5845
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5295
5846
  let resolvedTarget = normalizedTarget;
5296
5847
  try {
5297
5848
  resolvedTarget = await realpath2(normalizedTarget);
5298
5849
  } catch {
5299
5850
  resolvedTarget = normalizedTarget;
5300
5851
  }
5301
- const rel = relative13(this.projectRoot, resolvedTarget);
5302
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5852
+ const rel = relative14(this.projectRoot, resolvedTarget);
5853
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5303
5854
  if (isOutside) {
5304
5855
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5305
5856
  return { safe: true, absolutePath: resolvedTarget };
@@ -5331,7 +5882,7 @@ var init_Sandbox = __esm({
5331
5882
  }
5332
5883
  const parsed = parse(commandLine);
5333
5884
  const tokens = parsed.filter((t) => typeof t === "string");
5334
- const normalize13 = (raw) => {
5885
+ const normalize14 = (raw) => {
5335
5886
  let b = raw.toLowerCase();
5336
5887
  const slash = Math.max(b.lastIndexOf("/"), b.lastIndexOf("\\"));
5337
5888
  if (slash >= 0) b = b.slice(slash + 1);
@@ -5339,7 +5890,7 @@ var init_Sandbox = __esm({
5339
5890
  return b;
5340
5891
  };
5341
5892
  for (const tok of tokens) {
5342
- const b = normalize13(tok);
5893
+ const b = normalize14(tok);
5343
5894
  if (_Sandbox.DANGEROUS_BINARIES.has(b)) {
5344
5895
  return { status: "blocked", reason: `Binary '${b}' is explicitly forbidden.`, hash };
5345
5896
  }
@@ -5347,7 +5898,7 @@ var init_Sandbox = __esm({
5347
5898
  if (tokens.length === 0) {
5348
5899
  return { status: "safe", hash };
5349
5900
  }
5350
- const binary = normalize13(tokens[0]);
5901
+ const binary = normalize14(tokens[0]);
5351
5902
  const sub = tokens.length > 1 ? tokens[1].toLowerCase() : "";
5352
5903
  if (binary === "git") {
5353
5904
  const destructiveGit = /* @__PURE__ */ new Set(["push", "reset", "clean", "rebase", "merge", "force-push"]);
@@ -5446,7 +5997,7 @@ var init_Sandbox = __esm({
5446
5997
  });
5447
5998
 
5448
5999
  // ../core/src/Voice.ts
5449
- import { spawn as spawn6 } from "child_process";
6000
+ import { spawn as spawn7 } from "child_process";
5450
6001
  function buildTtsInvocation(text, rate) {
5451
6002
  const psCommand = `
5452
6003
  Add-Type -AssemblyName System.Speech;
@@ -5486,10 +6037,10 @@ var init_Voice = __esm({
5486
6037
  const { command, args: args2, env } = buildTtsInvocation(text, rate);
5487
6038
  try {
5488
6039
  if (process.platform === "win32") {
5489
- await new Promise((resolve20, reject) => {
6040
+ await new Promise((resolve21, reject) => {
5490
6041
  try {
5491
- const proc = spawn6(command, args2, { env });
5492
- proc.on("exit", () => resolve20());
6042
+ const proc = spawn7(command, args2, { env });
6043
+ proc.on("exit", () => resolve21());
5493
6044
  proc.on("error", reject);
5494
6045
  } catch (e) {
5495
6046
  reject(e);
@@ -5586,7 +6137,7 @@ var init_ShadowService = __esm({
5586
6137
  });
5587
6138
 
5588
6139
  // ../core/src/Hooks.ts
5589
- import { spawn as spawn7 } from "child_process";
6140
+ import { exec } from "child_process";
5590
6141
  function matches(entry, ctx) {
5591
6142
  if (!entry.matcher) return true;
5592
6143
  let re;
@@ -5603,55 +6154,34 @@ function matches(entry, ctx) {
5603
6154
  async function runOne(entry, ctx) {
5604
6155
  const timeoutMs = entry.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
5605
6156
  const command = entry.command;
5606
- return new Promise((resolve20) => {
5607
- const isWindows = process.platform === "win32";
5608
- const child = spawn7(isWindows ? "cmd.exe" : "sh", isWindows ? ["/c", command] : ["-c", command], {
5609
- cwd: ctx.cwd ?? process.cwd(),
5610
- stdio: ["pipe", "pipe", "pipe"]
5611
- });
5612
- let stdout = "";
5613
- let stderr = "";
6157
+ const env = { ...process.env, MSAPLING_HOOK_PAYLOAD: JSON.stringify({ ...ctx }) };
6158
+ return new Promise((resolve21) => {
5614
6159
  let timedOut = false;
5615
6160
  let settled = false;
5616
- const finish = (exitCode) => {
6161
+ const finish = (exitCode, stdout, stderr) => {
5617
6162
  if (settled) return;
5618
6163
  settled = true;
5619
6164
  clearTimeout(killer);
5620
6165
  const blocked = !NON_BLOCKING_EVENTS.has(ctx.event) && !!entry.blocking && (exitCode === null || exitCode !== 0);
5621
- resolve20({ command, exitCode, stdout, stderr, timedOut, blocked });
6166
+ resolve21({ command, exitCode, stdout, stderr, timedOut, blocked });
5622
6167
  };
6168
+ const child = exec(command, {
6169
+ cwd: ctx.cwd ?? process.cwd(),
6170
+ env,
6171
+ maxBuffer: MAX_OUTPUT_BYTES3
6172
+ }, (err, stdout, stderr) => {
6173
+ const rawCode = err ? err.code : 0;
6174
+ const exitCode = typeof rawCode === "number" ? rawCode : null;
6175
+ finish(exitCode, stdout.slice(0, MAX_OUTPUT_BYTES3), stderr.slice(0, MAX_OUTPUT_BYTES3));
6176
+ });
5623
6177
  const killer = setTimeout(() => {
5624
6178
  timedOut = true;
5625
6179
  try {
5626
6180
  child.kill("SIGKILL");
5627
6181
  } catch {
5628
6182
  }
5629
- finish(null);
6183
+ finish(null, "", "");
5630
6184
  }, timeoutMs);
5631
- child.stdout.on("data", (b) => {
5632
- if (stdout.length < MAX_OUTPUT_BYTES3) stdout += b.toString("utf8");
5633
- });
5634
- child.stderr.on("data", (b) => {
5635
- if (stderr.length < MAX_OUTPUT_BYTES3) stderr += b.toString("utf8");
5636
- });
5637
- child.on("error", (e) => {
5638
- stderr += `
5639
- [hook spawn error] ${e.message}`;
5640
- finish(null);
5641
- });
5642
- child.on("close", (code) => finish(code));
5643
- try {
5644
- child.stdin.write(JSON.stringify({ ...ctx }) + "\n");
5645
- child.stdin.end();
5646
- } catch (e) {
5647
- stderr += `
5648
- [hook stdin error] ${e?.message}`;
5649
- try {
5650
- child.kill();
5651
- } catch {
5652
- }
5653
- finish(null);
5654
- }
5655
6185
  });
5656
6186
  }
5657
6187
  var NON_BLOCKING_EVENTS, DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
@@ -5869,9 +6399,9 @@ var init_specs = __esm({
5869
6399
 
5870
6400
  // ../core/src/governor/ResourceGovernor.ts
5871
6401
  import { freemem as freemem2 } from "os";
5872
- import { homedir as homedir7 } from "os";
6402
+ import { homedir as homedir8 } from "os";
5873
6403
  import { readFile as readFile9 } from "fs/promises";
5874
- import { join as join14 } from "path";
6404
+ import { join as join15 } from "path";
5875
6405
  function determineTier(specs) {
5876
6406
  const memGB = specs.memory.totalGB;
5877
6407
  const cores = specs.cpu.cores;
@@ -5886,7 +6416,7 @@ function recommendLimits(specs) {
5886
6416
  }
5887
6417
  async function readConfigOverrides() {
5888
6418
  try {
5889
- const configPath = join14(homedir7(), ".msapling", "config.json");
6419
+ const configPath = join15(homedir8(), ".msapling", "config.json");
5890
6420
  const content = await readFile9(configPath, "utf-8");
5891
6421
  const config = JSON.parse(content);
5892
6422
  return config.limits ?? null;
@@ -5967,7 +6497,7 @@ var init_ResourceGovernor = __esm({
5967
6497
  this.activeAgents++;
5968
6498
  return;
5969
6499
  }
5970
- await new Promise((resolve20) => this.agentWaiters.push(resolve20));
6500
+ await new Promise((resolve21) => this.agentWaiters.push(resolve21));
5971
6501
  this.activeAgents++;
5972
6502
  }
5973
6503
  /**
@@ -5986,7 +6516,7 @@ var init_ResourceGovernor = __esm({
5986
6516
  this.activeTool++;
5987
6517
  return;
5988
6518
  }
5989
- await new Promise((resolve20) => this.toolWaiters.push(resolve20));
6519
+ await new Promise((resolve21) => this.toolWaiters.push(resolve21));
5990
6520
  this.activeTool++;
5991
6521
  }
5992
6522
  /**
@@ -6044,6 +6574,7 @@ var init_ToolExecutor = __esm({
6044
6574
  init_WebFetchTool();
6045
6575
  init_WebSearchTool();
6046
6576
  init_BashTool();
6577
+ init_BackgroundShellTool();
6047
6578
  init_NotebookReadTool();
6048
6579
  init_NotebookEditTool();
6049
6580
  init_MultiEditFileTool();
@@ -6056,9 +6587,14 @@ var init_ToolExecutor = __esm({
6056
6587
  init_ShadowService();
6057
6588
  init_Hooks();
6058
6589
  init_ResourceGovernor();
6059
- APPROVAL_GATED = /* @__PURE__ */ new Set(["run_command", "bash_command", "edit_file", "write_file", "patch_file", "multi_edit_file", "notebook_edit_cell", "move_file", "delete_file", "open_sub_shell"]);
6590
+ APPROVAL_GATED = /* @__PURE__ */ new Set(["run_command", "bash_command", "bash_background", "edit_file", "write_file", "patch_file", "multi_edit_file", "notebook_edit_cell", "move_file", "delete_file", "open_sub_shell"]);
6060
6591
  ToolExecutor = class {
6061
6592
  tools = /* @__PURE__ */ new Map();
6593
+ /** Backend client — retained so named-subagent dispatch can open its own
6594
+ * scoped stream (CLI-PARITY-P1-6). */
6595
+ client;
6596
+ /** Project root — retained for named-subagent dispatch context. */
6597
+ projectRoot;
6062
6598
  sandbox;
6063
6599
  mdrive;
6064
6600
  voice;
@@ -6093,6 +6629,8 @@ var init_ToolExecutor = __esm({
6093
6629
  */
6094
6630
  onModeChange = null;
6095
6631
  constructor(client, projectRoot) {
6632
+ this.client = client;
6633
+ this.projectRoot = projectRoot;
6096
6634
  this.sandbox = new Sandbox(projectRoot);
6097
6635
  this.mdrive = new MDriveService(client);
6098
6636
  this.voice = new VoiceService();
@@ -6121,11 +6659,20 @@ var init_ToolExecutor = __esm({
6121
6659
  payload: info,
6122
6660
  cwd: projectRoot
6123
6661
  });
6124
- }
6662
+ },
6663
+ // CLI-PARITY-P1-6: named-subagent runner. Runs a named agent with its own
6664
+ // system prompt + model + TOOL ALLOWLIST. Tools execute back through THIS
6665
+ // executor (so the existing permission/approval system gates writes), but
6666
+ // only the agent's allowlisted tools are advertised + permitted.
6667
+ runNamedAgent: (req) => this.runNamedSubagent(req)
6125
6668
  }));
6126
6669
  this.registerTool(new WebFetchTool());
6127
6670
  this.registerTool(new WebSearchTool({ client }));
6128
6671
  this.registerTool(new BashTool());
6672
+ this.registerTool(new BashBackgroundTool());
6673
+ this.registerTool(new ListBackgroundShellsTool());
6674
+ this.registerTool(new ReadBackgroundShellTool());
6675
+ this.registerTool(new KillBackgroundShellTool());
6129
6676
  this.registerTool(new NotebookReadTool());
6130
6677
  this.registerTool(new NotebookEditTool());
6131
6678
  this.registerTool(new MultiEditFileTool());
@@ -6317,13 +6864,13 @@ ${blocker.stderr || "(empty)"}`,
6317
6864
  return { content: `Security Block: ${check2.reason}`, isError: true };
6318
6865
  }
6319
6866
  }
6320
- if (toolName === "run_command" || toolName === "bash_command") {
6867
+ if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background") {
6321
6868
  if (!args2.command) {
6322
6869
  return { content: `Error: ${toolName} requires a command argument`, isError: true };
6323
6870
  }
6324
6871
  const analysis = this.sandbox.analyzeCommand(args2.command);
6325
6872
  if (analysis.status === "blocked") {
6326
- const staleCmdKey = toolName === "bash_command" ? `bash_command:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}` : `run_command:${(args2.command || "").trim()}`;
6873
+ const staleCmdKey = toolName === "run_command" ? `run_command:${(args2.command || "").trim()}` : `${toolName}:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}`;
6327
6874
  if (this.trustStore?.has(staleCmdKey)) {
6328
6875
  this.trustStore.delete(staleCmdKey).catch(() => {
6329
6876
  });
@@ -6338,10 +6885,10 @@ ${blocker.stderr || "(empty)"}`,
6338
6885
  let cmdKey = "";
6339
6886
  if (toolName === "run_command") {
6340
6887
  cmdKey = `run_command:${(args2.command || "").trim()}`;
6341
- } else if (toolName === "bash_command") {
6888
+ } else if (toolName === "bash_command" || toolName === "bash_background") {
6342
6889
  const normalizedCmd = (args2.command || "").trim().replace(/\s+/g, " ");
6343
6890
  const cwdSuffix = args2.cwd ? `:cwd=${args2.cwd}` : "";
6344
- cmdKey = `bash_command:${normalizedCmd}${cwdSuffix}`;
6891
+ cmdKey = `${toolName}:${normalizedCmd}${cwdSuffix}`;
6345
6892
  } else {
6346
6893
  cmdKey = `${toolName}:${(args2.path || args2.instruction || "").trim()}`;
6347
6894
  }
@@ -6365,7 +6912,7 @@ ${blocker.stderr || "(empty)"}`,
6365
6912
  }
6366
6913
  }
6367
6914
  }
6368
- if (toolName === "run_command" || toolName === "bash_command" || toolName === "edit_file" || toolName === "write_file" || toolName === "patch_file" || toolName === "multi_edit_file" || toolName === "notebook_edit_cell" || toolName === "move_file" || toolName === "delete_file") {
6915
+ if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background" || toolName === "edit_file" || toolName === "write_file" || toolName === "patch_file" || toolName === "multi_edit_file" || toolName === "notebook_edit_cell" || toolName === "move_file" || toolName === "delete_file") {
6369
6916
  const audit = await this.shadow.verifyAction(
6370
6917
  JSON.stringify({ tool: toolName, args: args2 }),
6371
6918
  `Execution context: ${projectRoot}`
@@ -6426,6 +6973,80 @@ Please approve the diff in the UI to sync this change locally.`
6426
6973
  }));
6427
6974
  return [...builtin, ...mcp];
6428
6975
  }
6976
+ /**
6977
+ * CLI-PARITY-P1-6: tool schemas filtered to a named agent's allowlist.
6978
+ *
6979
+ * - `undefined` allowlist → inherit the FULL builtin+MCP set (getToolSchemas).
6980
+ * - `[]` → no tools (read/reason-only agent).
6981
+ * - `['read_file', …]` → ONLY those tools (silently drops names that
6982
+ * don't resolve to a registered tool).
6983
+ *
6984
+ * `dispatch_agent` is always excluded from a subagent's view so a named agent
6985
+ * cannot recursively spawn further named agents (avoids unbounded fan-out).
6986
+ */
6987
+ getToolSchemasForAllowlist(allow) {
6988
+ const all = this.getToolSchemas();
6989
+ if (allow === void 0) {
6990
+ return all.filter((s) => s.name !== "dispatch_agent");
6991
+ }
6992
+ const allowSet = new Set(allow);
6993
+ return all.filter((s) => s.name !== "dispatch_agent" && allowSet.has(s.name));
6994
+ }
6995
+ /**
6996
+ * CLI-PARITY-P1-6: run a NAMED subagent to completion.
6997
+ *
6998
+ * Drives a bounded agentic loop against the backend using the agent's system
6999
+ * prompt + model, advertising ONLY the agent's allowlisted tools. Tool calls
7000
+ * execute back through `this.execute(...)`, so the SAME permission/approval +
7001
+ * shadow-audit gates apply — the allowlist narrows WHICH tools the agent may
7002
+ * request; the permission system still governs WHETHER a given write runs.
7003
+ *
7004
+ * This is the mechanism that closes the "subagents are read-only" gap: a named
7005
+ * agent whose frontmatter grants `write_file`/`edit_file`/`bash_command` can
7006
+ * use them (subject to approval), whereas the anonymous dispatch path cannot.
7007
+ */
7008
+ async runNamedSubagent(req) {
7009
+ const { agent, prompt: prompt4, model, chatId, onContent } = req;
7010
+ const schemas = this.getToolSchemasForAllowlist(agent.tools);
7011
+ const framedPrompt = `[You are the "${agent.name}" subagent. Operate strictly within this role.]
7012
+ ${agent.systemPrompt}
7013
+
7014
+ ---
7015
+
7016
+ ${prompt4}`;
7017
+ const MAX_SUBAGENT_ROUNDS = 12;
7018
+ const queue = [framedPrompt];
7019
+ let rounds = 0;
7020
+ while (queue.length > 0 && rounds < MAX_SUBAGENT_ROUNDS) {
7021
+ const current = queue.shift();
7022
+ rounds++;
7023
+ const stream = this.client.streamChat({
7024
+ ...chatId ? { chat_id: chatId } : {},
7025
+ prompt: current,
7026
+ model,
7027
+ tools: schemas,
7028
+ project_root: this.projectRoot,
7029
+ mode: this.mode
7030
+ });
7031
+ for await (const chunk of stream) {
7032
+ if (chunk.content) onContent(chunk.content);
7033
+ if (chunk.tool_use) {
7034
+ const name = chunk.tool_use.name;
7035
+ if (agent.tools !== void 0 && !agent.tools.includes(name)) {
7036
+ queue.push(
7037
+ `[TOOL_RESULT]: ${JSON.stringify({
7038
+ content: `Tool "${name}" is not in the "${agent.name}" agent's allowlist; refused.`,
7039
+ isError: true
7040
+ })}`
7041
+ );
7042
+ continue;
7043
+ }
7044
+ const result = await this.execute(name, chunk.tool_use.args, this.projectRoot);
7045
+ queue.push(`[TOOL_RESULT]: ${JSON.stringify(result)}`);
7046
+ }
7047
+ }
7048
+ }
7049
+ }
6429
7050
  };
6430
7051
  }
6431
7052
  });
@@ -6471,8 +7092,8 @@ var init_Safety = __esm({
6471
7092
  });
6472
7093
 
6473
7094
  // ../core/src/ProjectConfig.ts
6474
- import { homedir as homedir8 } from "os";
6475
- import { join as join15, dirname as dirname3, parse as parsePath } from "path";
7095
+ import { homedir as homedir9 } from "os";
7096
+ import { join as join16, dirname as dirname3, parse as parsePath } from "path";
6476
7097
  import { existsSync as existsSync11 } from "fs";
6477
7098
  import { readFile as readFile11 } from "fs/promises";
6478
7099
  async function readIfExists(path2) {
@@ -6486,7 +7107,7 @@ async function readIfExists(path2) {
6486
7107
  }
6487
7108
  async function findInDir(dir) {
6488
7109
  for (const filename of FILENAMES) {
6489
- const path2 = join15(dir, filename);
7110
+ const path2 = join16(dir, filename);
6490
7111
  const content = await readIfExists(path2);
6491
7112
  if (content !== null) {
6492
7113
  return { path: path2, filename, content };
@@ -6509,9 +7130,9 @@ async function findProjectConfig(start) {
6509
7130
  return null;
6510
7131
  }
6511
7132
  async function findUserConfig() {
6512
- const home = homedir8();
7133
+ const home = homedir9();
6513
7134
  if (!home) return null;
6514
- const userDir = join15(home, ".msapling");
7135
+ const userDir = join16(home, ".msapling");
6515
7136
  return findInDir(userDir);
6516
7137
  }
6517
7138
  function buildCombined(user, project) {
@@ -7119,8 +7740,8 @@ var init_Mutex = __esm({
7119
7740
  */
7120
7741
  acquire() {
7121
7742
  let release3;
7122
- const next = new Promise((resolve20) => {
7123
- release3 = resolve20;
7743
+ const next = new Promise((resolve21) => {
7744
+ release3 = resolve21;
7124
7745
  });
7125
7746
  const entry = this._queue.then(() => release3);
7126
7747
  this._queue = this._queue.then(() => next);
@@ -7158,8 +7779,8 @@ var init_lockfile = __esm({
7158
7779
  });
7159
7780
 
7160
7781
  // ../core/src/TrustStore.ts
7161
- import { join as join16, dirname as dirname4 } from "path";
7162
- import { homedir as homedir9, platform as platform3 } from "os";
7782
+ import { join as join17, dirname as dirname4 } from "path";
7783
+ import { homedir as homedir10, platform as platform3 } from "os";
7163
7784
  import { existsSync as existsSync12, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
7164
7785
  import { readFile as readFile12, writeFile as writeFile5, rename as rename2, chmod } from "fs/promises";
7165
7786
  import { randomBytes as randomBytes9 } from "crypto";
@@ -7170,7 +7791,7 @@ var init_TrustStore = __esm({
7170
7791
  init_esm_shims();
7171
7792
  init_Mutex();
7172
7793
  init_lockfile();
7173
- USER_SETTINGS_PATH = join16(homedir9(), ".msapling", "settings.json");
7794
+ USER_SETTINGS_PATH = join17(homedir10(), ".msapling", "settings.json");
7174
7795
  TrustStore = class {
7175
7796
  /** Current in-memory set of trusted `tool:command` keys. */
7176
7797
  trusted = /* @__PURE__ */ new Set();
@@ -7327,9 +7948,118 @@ var init_TrustStore = __esm({
7327
7948
  }
7328
7949
  });
7329
7950
 
7951
+ // ../core/src/BackupIndex.ts
7952
+ import { join as join18 } from "path";
7953
+ import { homedir as homedir11 } from "os";
7954
+ import {
7955
+ readFileSync as readFileSync3,
7956
+ appendFileSync as appendFileSync2,
7957
+ existsSync as existsSync13,
7958
+ mkdirSync as mkdirSync3,
7959
+ copyFileSync
7960
+ } from "fs";
7961
+ import { randomBytes as randomBytes10 } from "crypto";
7962
+ function _setBackupDirOverride(dir) {
7963
+ _backupDirOverride = dir;
7964
+ }
7965
+ function getBackupDir() {
7966
+ return _backupDirOverride ?? join18(homedir11(), ".msapling", "backups");
7967
+ }
7968
+ function getManifestPath() {
7969
+ return join18(getBackupDir(), "manifest.jsonl");
7970
+ }
7971
+ function ensureBackupDir() {
7972
+ const dir = getBackupDir();
7973
+ if (!existsSync13(dir)) {
7974
+ mkdirSync3(dir, { recursive: true });
7975
+ }
7976
+ }
7977
+ function openTurn(label) {
7978
+ ensureBackupDir();
7979
+ _turnCounter += 1;
7980
+ const turnId = `turn-${Date.now()}-${randomBytes10(4).toString("hex")}`;
7981
+ const entry = {
7982
+ turnId,
7983
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7984
+ label: label ?? `Turn ${_turnCounter}`,
7985
+ backups: []
7986
+ };
7987
+ _openTurns.set(turnId, entry);
7988
+ return turnId;
7989
+ }
7990
+ function recordBackup(turnId, originalPath, backupPath) {
7991
+ const entry = _openTurns.get(turnId);
7992
+ if (!entry) return;
7993
+ entry.backups.push({ originalPath, backupPath });
7994
+ }
7995
+ function closeTurn(turnId) {
7996
+ const entry = _openTurns.get(turnId);
7997
+ if (!entry) return;
7998
+ _openTurns.delete(turnId);
7999
+ entry.closedAt = (/* @__PURE__ */ new Date()).toISOString();
8000
+ try {
8001
+ ensureBackupDir();
8002
+ appendFileSync2(getManifestPath(), JSON.stringify(entry) + "\n", "utf8");
8003
+ } catch {
8004
+ }
8005
+ }
8006
+ function listCheckpoints() {
8007
+ const mp = getManifestPath();
8008
+ if (!existsSync13(mp)) return [];
8009
+ let raw;
8010
+ try {
8011
+ raw = readFileSync3(mp, "utf8");
8012
+ } catch {
8013
+ return [];
8014
+ }
8015
+ const entries = [];
8016
+ for (const line of raw.split("\n")) {
8017
+ const trimmed = line.trim();
8018
+ if (!trimmed) continue;
8019
+ try {
8020
+ entries.push(JSON.parse(trimmed));
8021
+ } catch {
8022
+ }
8023
+ }
8024
+ return entries.reverse().slice(0, MAX_CHECKPOINTS);
8025
+ }
8026
+ function restoreCheckpoint(entry) {
8027
+ const results = [];
8028
+ for (const rec of entry.backups) {
8029
+ if (!existsSync13(rec.backupPath)) {
8030
+ results.push(`Skip (backup file missing): ${rec.originalPath}`);
8031
+ continue;
8032
+ }
8033
+ try {
8034
+ copyFileSync(rec.backupPath, rec.originalPath);
8035
+ results.push(`Restored: ${rec.originalPath}`);
8036
+ } catch (e) {
8037
+ results.push(`Error restoring ${rec.originalPath}: ${e.message}`);
8038
+ }
8039
+ }
8040
+ return results;
8041
+ }
8042
+ function manifestPath() {
8043
+ return getManifestPath();
8044
+ }
8045
+ function backupDir() {
8046
+ return getBackupDir();
8047
+ }
8048
+ var MAX_CHECKPOINTS, _backupDirOverride, _openTurns, _turnCounter;
8049
+ var init_BackupIndex = __esm({
8050
+ "../core/src/BackupIndex.ts"() {
8051
+ "use strict";
8052
+ init_esm_shims();
8053
+ MAX_CHECKPOINTS = 50;
8054
+ _backupDirOverride = null;
8055
+ _openTurns = /* @__PURE__ */ new Map();
8056
+ _turnCounter = 0;
8057
+ }
8058
+ });
8059
+
7330
8060
  // ../core/src/Storage/vault.ts
7331
- import { join as join17 } from "path";
7332
- import { chmodSync, existsSync as existsSync13, renameSync as renameSync2, unlinkSync } from "fs";
8061
+ import { join as join19 } from "path";
8062
+ import { chmodSync, existsSync as existsSync14, renameSync as renameSync2, unlinkSync } from "fs";
7333
8063
  import { writeFile as writeFile6, readFile as readFile13 } from "fs/promises";
7334
8064
  import { createHash as createHash4 } from "crypto";
7335
8065
  async function getKeytar() {
@@ -7343,7 +8073,7 @@ async function getKeytar() {
7343
8073
  return _keytar;
7344
8074
  }
7345
8075
  async function saveToken(baseDir, token) {
7346
- const filePath = join17(baseDir, "vault", "token");
8076
+ const filePath = join19(baseDir, "vault", "token");
7347
8077
  try {
7348
8078
  const kt = await getKeytar();
7349
8079
  if (!kt) throw new Error("keyring not loadable");
@@ -7352,12 +8082,12 @@ async function saveToken(baseDir, token) {
7352
8082
  throw new KeychainUnavailableError(e instanceof Error ? e.message : String(e));
7353
8083
  }
7354
8084
  try {
7355
- if (existsSync13(filePath)) unlinkSync(filePath);
8085
+ if (existsSync14(filePath)) unlinkSync(filePath);
7356
8086
  } catch {
7357
8087
  }
7358
8088
  }
7359
8089
  async function loadToken(baseDir) {
7360
- const filePath = join17(baseDir, "vault", "token");
8090
+ const filePath = join19(baseDir, "vault", "token");
7361
8091
  let kt = null;
7362
8092
  try {
7363
8093
  kt = await getKeytar();
@@ -7368,7 +8098,7 @@ async function loadToken(baseDir) {
7368
8098
  kt = null;
7369
8099
  console.debug(`Keychain unavailable (${e instanceof Error ? e.message : String(e)})`);
7370
8100
  }
7371
- if (existsSync13(filePath)) {
8101
+ if (existsSync14(filePath)) {
7372
8102
  const legacyToken = (await readFile13(filePath, "utf8")).trim();
7373
8103
  if (!legacyToken) return null;
7374
8104
  if (kt) {
@@ -7387,7 +8117,7 @@ async function loadToken(baseDir) {
7387
8117
  return null;
7388
8118
  }
7389
8119
  async function clearToken(baseDir) {
7390
- const filePath = join17(baseDir, "vault", "token");
8120
+ const filePath = join19(baseDir, "vault", "token");
7391
8121
  try {
7392
8122
  const kt = await getKeytar();
7393
8123
  if (kt) await kt.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
@@ -7395,7 +8125,7 @@ async function clearToken(baseDir) {
7395
8125
  console.debug(`Keychain unavailable for deletion (${e instanceof Error ? e.message : String(e)})`);
7396
8126
  }
7397
8127
  try {
7398
- if (existsSync13(filePath)) {
8128
+ if (existsSync14(filePath)) {
7399
8129
  const fs3 = await import("fs/promises");
7400
8130
  await fs3.unlink(filePath);
7401
8131
  }
@@ -7417,8 +8147,8 @@ async function getOrCreateJournalKey() {
7417
8147
  const buf = Buffer.from(existing, "base64");
7418
8148
  if (buf.length === 32) return buf;
7419
8149
  }
7420
- const { randomBytes: randomBytes15 } = await import("crypto");
7421
- const key = randomBytes15(32);
8150
+ const { randomBytes: randomBytes16 } = await import("crypto");
8151
+ const key = randomBytes16(32);
7422
8152
  await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_JOURNAL_ACCOUNT, key.toString("base64"));
7423
8153
  return key;
7424
8154
  } catch (e) {
@@ -7428,8 +8158,8 @@ async function getOrCreateJournalKey() {
7428
8158
  }
7429
8159
  async function writeVaultRef(baseDir, label, value) {
7430
8160
  const hash = createHash4("sha256").update(value, "utf8").digest("hex");
7431
- const objectPath = join17(baseDir, "vault", "objects", hash);
7432
- const refPath = join17(baseDir, "vault", "refs", label);
8161
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8162
+ const refPath = join19(baseDir, "vault", "refs", label);
7433
8163
  const refTmp = `${refPath}.tmp`;
7434
8164
  await writeFile6(objectPath, value, "utf8");
7435
8165
  if (process.platform !== "win32") {
@@ -7440,11 +8170,11 @@ async function writeVaultRef(baseDir, label, value) {
7440
8170
  return hash;
7441
8171
  }
7442
8172
  async function readVaultRef(baseDir, label) {
7443
- const refPath = join17(baseDir, "vault", "refs", label);
7444
- if (!existsSync13(refPath)) return null;
8173
+ const refPath = join19(baseDir, "vault", "refs", label);
8174
+ if (!existsSync14(refPath)) return null;
7445
8175
  const hash = (await readFile13(refPath, "utf8")).trim();
7446
- const objectPath = join17(baseDir, "vault", "objects", hash);
7447
- if (!existsSync13(objectPath)) return null;
8176
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8177
+ if (!existsSync14(objectPath)) return null;
7448
8178
  return readFile13(objectPath, "utf8");
7449
8179
  }
7450
8180
  var _keytar, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, KeychainUnavailableError, KEYCHAIN_JOURNAL_ACCOUNT;
@@ -7468,20 +8198,20 @@ var init_vault = __esm({
7468
8198
  });
7469
8199
 
7470
8200
  // ../core/src/Storage/recipes.ts
7471
- import { join as join18 } from "path";
7472
- import { existsSync as existsSync14, renameSync as renameSync3 } from "fs";
8201
+ import { join as join20 } from "path";
8202
+ import { existsSync as existsSync15, renameSync as renameSync3 } from "fs";
7473
8203
  import { writeFile as writeFile7, readFile as readFile14 } from "fs/promises";
7474
8204
  import { createHash as createHash5 } from "crypto";
7475
8205
  async function registerRecipe(baseDir, name, content) {
7476
8206
  const hash = createHash5("sha256").update(content, "utf8").digest("hex");
7477
- const objectPath = join18(baseDir, "cache", "recipes", "objects", hash);
7478
- const indexPath = join18(baseDir, "cache", "recipes", "index.json");
8207
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8208
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7479
8209
  const indexTmp = `${indexPath}.tmp`;
7480
- if (!existsSync14(objectPath)) {
8210
+ if (!existsSync15(objectPath)) {
7481
8211
  await writeFile7(objectPath, content, "utf8");
7482
8212
  }
7483
8213
  let index = {};
7484
- if (existsSync14(indexPath)) {
8214
+ if (existsSync15(indexPath)) {
7485
8215
  try {
7486
8216
  index = JSON.parse(await readFile14(indexPath, "utf8"));
7487
8217
  } catch {
@@ -7494,15 +8224,15 @@ async function registerRecipe(baseDir, name, content) {
7494
8224
  return hash;
7495
8225
  }
7496
8226
  async function resolveRecipe(baseDir, nameOrRef) {
7497
- const indexPath = join18(baseDir, "cache", "recipes", "index.json");
8227
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7498
8228
  const atIdx = nameOrRef.indexOf("@");
7499
8229
  if (atIdx !== -1) {
7500
8230
  const hash2 = nameOrRef.slice(atIdx + 1);
7501
- const objectPath2 = join18(baseDir, "cache", "recipes", "objects", hash2);
7502
- if (!existsSync14(objectPath2)) return null;
8231
+ const objectPath2 = join20(baseDir, "cache", "recipes", "objects", hash2);
8232
+ if (!existsSync15(objectPath2)) return null;
7503
8233
  return { hash: hash2, content: await readFile14(objectPath2, "utf8") };
7504
8234
  }
7505
- if (!existsSync14(indexPath)) return null;
8235
+ if (!existsSync15(indexPath)) return null;
7506
8236
  let index;
7507
8237
  try {
7508
8238
  index = JSON.parse(await readFile14(indexPath, "utf8"));
@@ -7511,8 +8241,8 @@ async function resolveRecipe(baseDir, nameOrRef) {
7511
8241
  }
7512
8242
  const hash = index[nameOrRef];
7513
8243
  if (!hash) return null;
7514
- const objectPath = join18(baseDir, "cache", "recipes", "objects", hash);
7515
- if (!existsSync14(objectPath)) return null;
8244
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8245
+ if (!existsSync15(objectPath)) return null;
7516
8246
  return { hash, content: await readFile14(objectPath, "utf8") };
7517
8247
  }
7518
8248
  var init_recipes = __esm({
@@ -7523,25 +8253,25 @@ var init_recipes = __esm({
7523
8253
  });
7524
8254
 
7525
8255
  // ../core/src/Storage/history.ts
7526
- import { join as join19, basename } from "path";
7527
- import { existsSync as existsSync15, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "fs";
8256
+ import { join as join21, basename } from "path";
8257
+ import { existsSync as existsSync16, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "fs";
7528
8258
  import { writeFile as writeFile8, readFile as readFile15, appendFile } from "fs/promises";
7529
- import { createHash as createHash6, randomBytes as randomBytes10 } from "crypto";
8259
+ import { createHash as createHash6, randomBytes as randomBytes11 } from "crypto";
7530
8260
  function hashLine(line) {
7531
8261
  return createHash6("sha256").update(line, "utf8").digest("hex");
7532
8262
  }
7533
8263
  async function appendHistoryEntry(baseDir, historyMutex, content) {
7534
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
8264
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
7535
8265
  return historyMutex.run(async () => {
7536
8266
  let release3 = null;
7537
8267
  try {
7538
- if (!existsSync15(path2)) {
8268
+ if (!existsSync16(path2)) {
7539
8269
  await writeFile8(path2, "", "utf8");
7540
8270
  }
7541
8271
  release3 = await lock(path2, { retries: 5, retryWait: 50 });
7542
8272
  let prevHash = null;
7543
8273
  let seq = 1;
7544
- if (existsSync15(path2)) {
8274
+ if (existsSync16(path2)) {
7545
8275
  const raw = (await readFile15(path2, "utf8")).trimEnd();
7546
8276
  if (raw.length > 0) {
7547
8277
  const lines = raw.split("\n");
@@ -7574,8 +8304,8 @@ async function appendHistoryEntry(baseDir, historyMutex, content) {
7574
8304
  });
7575
8305
  }
7576
8306
  async function loadHistoryEntries(baseDir) {
7577
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
7578
- if (!existsSync15(path2)) return [];
8307
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
8308
+ if (!existsSync16(path2)) return [];
7579
8309
  const raw = await readFile15(path2, "utf8");
7580
8310
  const entries = [];
7581
8311
  for (const line of raw.split("\n")) {
@@ -7607,10 +8337,10 @@ async function verifyHistory(baseDir) {
7607
8337
  return breaks.length === 0 ? { ok: true } : { ok: false, breaks };
7608
8338
  }
7609
8339
  async function saveHistory(baseDir, historyMutex, history) {
7610
- const path2 = join19(baseDir, "history", "shell_history.json");
8340
+ const path2 = join21(baseDir, "history", "shell_history.json");
7611
8341
  let release3;
7612
8342
  try {
7613
- if (!existsSync15(path2)) writeFileSync3(path2, "[]", "utf8");
8343
+ if (!existsSync16(path2)) writeFileSync3(path2, "[]", "utf8");
7614
8344
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7615
8345
  await historyMutex.run(async () => {
7616
8346
  const tmpPath = `${path2}.tmp`;
@@ -7620,7 +8350,7 @@ async function saveHistory(baseDir, historyMutex, history) {
7620
8350
  renameSync4(tmpPath, path2);
7621
8351
  } catch (e) {
7622
8352
  try {
7623
- if (existsSync15(tmpPath)) {
8353
+ if (existsSync16(tmpPath)) {
7624
8354
  const fs3 = await import("fs/promises");
7625
8355
  await fs3.unlink(tmpPath);
7626
8356
  }
@@ -7640,21 +8370,21 @@ async function saveHistory(baseDir, historyMutex, history) {
7640
8370
  }
7641
8371
  }
7642
8372
  async function loadHistory(baseDir, historyMutex) {
7643
- const path2 = join19(baseDir, "history", "shell_history.json");
7644
- if (!existsSync15(path2)) return [];
8373
+ const path2 = join21(baseDir, "history", "shell_history.json");
8374
+ if (!existsSync16(path2)) return [];
7645
8375
  let release3;
7646
8376
  try {
7647
8377
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7648
8378
  return historyMutex.run(async () => {
7649
- if (existsSync15(path2)) {
8379
+ if (existsSync16(path2)) {
7650
8380
  const text = await readFile15(path2, "utf8");
7651
8381
  try {
7652
8382
  return JSON.parse(text);
7653
8383
  } catch (parseErr) {
7654
8384
  const filename = basename(path2) || "shell_history.json";
7655
8385
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7656
- const suffix = randomBytes10(4).toString("hex");
7657
- const corruptBackupPath = join19(
8386
+ const suffix = randomBytes11(4).toString("hex");
8387
+ const corruptBackupPath = join21(
7658
8388
  baseDir,
7659
8389
  "history",
7660
8390
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7689,15 +8419,15 @@ var init_history = __esm({
7689
8419
  });
7690
8420
 
7691
8421
  // ../core/src/Storage/permissions.ts
7692
- import { join as join20 } from "path";
7693
- import { existsSync as existsSync16, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
8422
+ import { join as join22 } from "path";
8423
+ import { existsSync as existsSync17, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
7694
8424
  import { writeFile as writeFile9, readFile as readFile16 } from "fs/promises";
7695
- import { randomBytes as randomBytes11 } from "crypto";
8425
+ import { randomBytes as randomBytes12 } from "crypto";
7696
8426
  async function savePermissions(baseDir, permissionsMutex, permissions) {
7697
- const path2 = join20(baseDir, "vault", "permissions.json");
8427
+ const path2 = join22(baseDir, "vault", "permissions.json");
7698
8428
  let release3;
7699
8429
  try {
7700
- if (!existsSync16(path2)) writeFileSync4(path2, "{}", "utf8");
8430
+ if (!existsSync17(path2)) writeFileSync4(path2, "{}", "utf8");
7701
8431
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7702
8432
  await permissionsMutex.run(async () => {
7703
8433
  const tmpPath = `${path2}.tmp`;
@@ -7707,7 +8437,7 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7707
8437
  renameSync5(tmpPath, path2);
7708
8438
  } catch (e) {
7709
8439
  try {
7710
- if (existsSync16(tmpPath)) {
8440
+ if (existsSync17(tmpPath)) {
7711
8441
  const fs3 = await import("fs/promises");
7712
8442
  await fs3.unlink(tmpPath);
7713
8443
  }
@@ -7727,21 +8457,21 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7727
8457
  }
7728
8458
  }
7729
8459
  async function loadPermissions(baseDir, permissionsMutex) {
7730
- const path2 = join20(baseDir, "vault", "permissions.json");
7731
- if (!existsSync16(path2)) return { trustedCommands: [], trustedPaths: [] };
8460
+ const path2 = join22(baseDir, "vault", "permissions.json");
8461
+ if (!existsSync17(path2)) return { trustedCommands: [], trustedPaths: [] };
7732
8462
  let release3;
7733
8463
  try {
7734
8464
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7735
8465
  return permissionsMutex.run(async () => {
7736
- if (existsSync16(path2)) {
8466
+ if (existsSync17(path2)) {
7737
8467
  const text = await readFile16(path2, "utf8");
7738
8468
  try {
7739
8469
  return JSON.parse(text);
7740
8470
  } catch (parseErr) {
7741
8471
  const filename = path2.split("/").pop() || "permissions.json";
7742
8472
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7743
- const suffix = randomBytes11(4).toString("hex");
7744
- const corruptBackupPath = join20(
8473
+ const suffix = randomBytes12(4).toString("hex");
8474
+ const corruptBackupPath = join22(
7745
8475
  baseDir,
7746
8476
  "vault",
7747
8477
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7776,11 +8506,11 @@ var init_permissions = __esm({
7776
8506
  });
7777
8507
 
7778
8508
  // ../core/src/Storage.ts
7779
- import { join as join21 } from "path";
7780
- import { homedir as homedir10 } from "os";
8509
+ import { join as join23 } from "path";
8510
+ import { homedir as homedir12 } from "os";
7781
8511
  import { chmodSync as chmodSync2 } from "fs";
7782
8512
  import { mkdir as mkdir6, writeFile as writeFile10 } from "fs/promises";
7783
- import { randomBytes as randomBytes12 } from "crypto";
8513
+ import { randomBytes as randomBytes13 } from "crypto";
7784
8514
  var StorageManager;
7785
8515
  var init_Storage = __esm({
7786
8516
  "../core/src/Storage.ts"() {
@@ -7807,7 +8537,7 @@ var init_Storage = __esm({
7807
8537
  */
7808
8538
  _ready;
7809
8539
  constructor() {
7810
- this.baseDir = join21(homedir10(), ".msapling");
8540
+ this.baseDir = join23(homedir12(), ".msapling");
7811
8541
  this._ready = this.ensureDirs();
7812
8542
  }
7813
8543
  async ensureDirs() {
@@ -7824,7 +8554,7 @@ var init_Storage = __esm({
7824
8554
  "cache/recipes/objects"
7825
8555
  ];
7826
8556
  for (const sub of subdirs) {
7827
- await mkdir6(join21(this.baseDir, sub), { recursive: true });
8557
+ await mkdir6(join23(this.baseDir, sub), { recursive: true });
7828
8558
  }
7829
8559
  if (process.platform !== "win32") {
7830
8560
  chmodSync2(this.baseDir, 448);
@@ -7901,8 +8631,8 @@ var init_Storage = __esm({
7901
8631
  await this._ready;
7902
8632
  const filename = filePath.split("/").pop() || "file";
7903
8633
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7904
- const suffix = randomBytes12(4).toString("hex");
7905
- const backupPath = join21(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
8634
+ const suffix = randomBytes13(4).toString("hex");
8635
+ const backupPath = join23(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
7906
8636
  await writeFile10(backupPath, content, "utf8");
7907
8637
  if (process.platform !== "win32") {
7908
8638
  chmodSync2(backupPath, 384);
@@ -7933,15 +8663,15 @@ var init_journalCrypto = __esm({
7933
8663
  });
7934
8664
 
7935
8665
  // ../core/src/Settings.ts
7936
- import { homedir as homedir11 } from "os";
7937
- import { join as join22 } from "path";
7938
- import { existsSync as existsSync17 } from "fs";
8666
+ import { homedir as homedir13 } from "os";
8667
+ import { join as join24 } from "path";
8668
+ import { existsSync as existsSync18 } from "fs";
7939
8669
  import * as fs from "fs";
7940
8670
  import { readFile as readFile17 } from "fs/promises";
7941
- import { randomBytes as randomBytes13 } from "crypto";
8671
+ import { randomBytes as randomBytes14 } from "crypto";
7942
8672
  function backupStaleFile(p) {
7943
8673
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7944
- const suffix = randomBytes13(4).toString("hex");
8674
+ const suffix = randomBytes14(4).toString("hex");
7945
8675
  fs.renameSync(p, `${p}.broken-${stamp}-${suffix}`);
7946
8676
  }
7947
8677
  function ensureConfigDir(p) {
@@ -7964,7 +8694,7 @@ function ensureConfigDir(p) {
7964
8694
  }
7965
8695
  async function readJson(path2) {
7966
8696
  try {
7967
- if (!existsSync17(path2)) return null;
8697
+ if (!existsSync18(path2)) return null;
7968
8698
  const text = await readFile17(path2, "utf8");
7969
8699
  if (!text.trim()) return null;
7970
8700
  return JSON.parse(text);
@@ -8019,9 +8749,9 @@ function mergeSettings(base, override) {
8019
8749
  }
8020
8750
  async function loadSettings(cwd = process.cwd(), env = process.env, warn) {
8021
8751
  const sources = [];
8022
- const home = env.HOME || env.USERPROFILE || process.env.HOME || process.env.USERPROFILE || homedir11() || ".";
8023
- const userPath = join22(home, ".msapling", "settings.json");
8024
- const projectPath = join22(cwd, ".msapling", "settings.json");
8752
+ const home = env.HOME || env.USERPROFILE || process.env.HOME || process.env.USERPROFILE || homedir13() || ".";
8753
+ const userPath = join24(home, ".msapling", "settings.json");
8754
+ const projectPath = join24(cwd, ".msapling", "settings.json");
8025
8755
  const [user, project] = await Promise.all([readJson(userPath), readJson(projectPath)]);
8026
8756
  if (user) sources.push(userPath);
8027
8757
  if (project) sources.push(projectPath);
@@ -8074,7 +8804,7 @@ var init_Settings = __esm({
8074
8804
  });
8075
8805
 
8076
8806
  // ../core/src/mcp/client.ts
8077
- import { spawn as spawn8 } from "child_process";
8807
+ import { spawn as spawn9 } from "child_process";
8078
8808
  var PROTOCOL_VERSION, CLIENT_INFO, MCPClientError, MCPClient, MCPRegistry;
8079
8809
  var init_client = __esm({
8080
8810
  "../core/src/mcp/client.ts"() {
@@ -8110,7 +8840,7 @@ var init_client = __esm({
8110
8840
  if (this.proc) return;
8111
8841
  const env = { ...process.env, ...this.config.env ?? {} };
8112
8842
  try {
8113
- this.proc = spawn8(this.config.command, this.config.args ?? [], {
8843
+ this.proc = spawn9(this.config.command, this.config.args ?? [], {
8114
8844
  stdio: ["pipe", "pipe", "pipe"],
8115
8845
  env
8116
8846
  });
@@ -8184,7 +8914,7 @@ var init_client = __esm({
8184
8914
  if (!this.proc) throw new MCPClientError(`MCP server "${this.name}" not started`);
8185
8915
  const id = this.nextId++;
8186
8916
  const frame = { jsonrpc: "2.0", id, method, params };
8187
- return new Promise((resolve20, reject) => {
8917
+ return new Promise((resolve21, reject) => {
8188
8918
  const timer = setTimeout(() => {
8189
8919
  this.pending.delete(id);
8190
8920
  reject(new MCPClientError(`MCP request ${method} timed out after ${timeoutMs}ms`));
@@ -8192,7 +8922,7 @@ var init_client = __esm({
8192
8922
  this.pending.set(id, {
8193
8923
  resolve: (v) => {
8194
8924
  clearTimeout(timer);
8195
- resolve20(v);
8925
+ resolve21(v);
8196
8926
  },
8197
8927
  reject: (e) => {
8198
8928
  clearTimeout(timer);
@@ -8219,7 +8949,7 @@ var init_client = __esm({
8219
8949
  if (!this.proc?.stdout) return;
8220
8950
  const stdout = this.proc.stdout;
8221
8951
  const decoder = new TextDecoder();
8222
- return new Promise((resolve20) => {
8952
+ return new Promise((resolve21) => {
8223
8953
  stdout.on("data", (chunk) => {
8224
8954
  this.buffer += decoder.decode(chunk, { stream: true });
8225
8955
  let idx;
@@ -8230,8 +8960,8 @@ var init_client = __esm({
8230
8960
  this.handleFrame(line);
8231
8961
  }
8232
8962
  });
8233
- stdout.on("end", () => resolve20());
8234
- stdout.on("error", () => resolve20());
8963
+ stdout.on("end", () => resolve21());
8964
+ stdout.on("error", () => resolve21());
8235
8965
  });
8236
8966
  }
8237
8967
  handleFrame(line) {
@@ -8371,6 +9101,8 @@ __export(src_exports2, {
8371
9101
  APPROVAL_GATED: () => APPROVAL_GATED,
8372
9102
  Agent: () => Agent,
8373
9103
  AsyncMutex: () => AsyncMutex,
9104
+ BackgroundShellRegistry: () => BackgroundShellRegistry,
9105
+ BashBackgroundTool: () => BashBackgroundTool,
8374
9106
  BashTool: () => BashTool,
8375
9107
  ContextBudget: () => ContextBudget,
8376
9108
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
@@ -8382,6 +9114,8 @@ __export(src_exports2, {
8382
9114
  GrepSearchTool: () => GrepSearchTool,
8383
9115
  HookRunner: () => HookRunner,
8384
9116
  KeychainUnavailableError: () => KeychainUnavailableError,
9117
+ KillBackgroundShellTool: () => KillBackgroundShellTool,
9118
+ ListBackgroundShellsTool: () => ListBackgroundShellsTool,
8385
9119
  ListDirectoryTool: () => ListDirectoryTool,
8386
9120
  MAX_PLAN_CHARS: () => MAX_PLAN_CHARS,
8387
9121
  MCPClient: () => MCPClient,
@@ -8392,8 +9126,10 @@ __export(src_exports2, {
8392
9126
  NotebookEditTool: () => NotebookEditTool,
8393
9127
  NotebookReadTool: () => NotebookReadTool,
8394
9128
  PatchFileTool: () => PatchFileTool,
9129
+ ReadBackgroundShellTool: () => ReadBackgroundShellTool,
8395
9130
  StorageManager: () => StorageManager,
8396
9131
  SwarmManager: () => SwarmManager,
9132
+ TOOL_NAME_ALIASES: () => TOOL_NAME_ALIASES,
8397
9133
  TodoReadTool: () => TodoReadTool,
8398
9134
  TodoStore: () => TodoStore,
8399
9135
  TodoWriteTool: () => TodoWriteTool,
@@ -8402,16 +9138,31 @@ __export(src_exports2, {
8402
9138
  WebFetchTool: () => WebFetchTool,
8403
9139
  WebSearchTool: () => WebSearchTool,
8404
9140
  WriteFileTool: () => WriteFileTool,
9141
+ _setBackupDirOverride: () => _setBackupDirOverride,
9142
+ backupDir: () => backupDir,
8405
9143
  buildCompactionPrompt: () => buildCompactionPrompt,
8406
9144
  buildHwContext: () => buildHwContext,
9145
+ closeTurn: () => closeTurn,
9146
+ discoverAgentFiles: () => discoverAgentFiles,
8407
9147
  ensureConfigDir: () => ensureConfigDir,
9148
+ findNamedAgent: () => findNamedAgent,
9149
+ formatBackgroundShells: () => formatBackgroundShells,
8408
9150
  formatCell: () => formatCell,
8409
9151
  formatNotebookHeader: () => formatNotebookHeader,
8410
9152
  formatTodos: () => formatTodos,
8411
9153
  getOrCreateJournalKey: () => getOrCreateJournalKey,
8412
9154
  initJournalEncryption: () => initJournalEncryption,
9155
+ listCheckpoints: () => listCheckpoints,
9156
+ loadNamedAgents: () => loadNamedAgents,
8413
9157
  loadProjectConfig: () => loadProjectConfig,
8414
9158
  loadSettings: () => loadSettings,
9159
+ manifestPath: () => manifestPath,
9160
+ normalizeToolName: () => normalizeToolName,
9161
+ openTurn: () => openTurn,
9162
+ parseAgentFile: () => parseAgentFile,
9163
+ parseToolsValue: () => parseToolsValue,
9164
+ recordBackup: () => recordBackup,
9165
+ restoreCheckpoint: () => restoreCheckpoint,
8415
9166
  takeSnapshot: () => takeSnapshot
8416
9167
  });
8417
9168
  var init_src3 = __esm({
@@ -8420,9 +9171,11 @@ var init_src3 = __esm({
8420
9171
  init_esm_shims();
8421
9172
  init_ToolExecutor();
8422
9173
  init_Agent();
9174
+ init_namedAgents();
8423
9175
  init_HardwareMonitor();
8424
9176
  init_TrustStore();
8425
9177
  init_ContextBudget();
9178
+ init_BackupIndex();
8426
9179
  init_Storage();
8427
9180
  init_Storage();
8428
9181
  init_journalCrypto();
@@ -8442,6 +9195,7 @@ var init_src3 = __esm({
8442
9195
  init_WebSearchTool();
8443
9196
  init_PlanModeTools();
8444
9197
  init_BashTool();
9198
+ init_BackgroundShellTool();
8445
9199
  init_NotebookReadTool();
8446
9200
  init_NotebookEditTool();
8447
9201
  init_MultiEditFileTool();
@@ -8466,7 +9220,7 @@ function setRawModeGuarded(stdin, mode) {
8466
9220
  }
8467
9221
  }
8468
9222
  async function promptPassword(prompt4) {
8469
- return new Promise((resolve20) => {
9223
+ return new Promise((resolve21) => {
8470
9224
  const stdin = process.stdin;
8471
9225
  const stdout = process.stdout;
8472
9226
  stdout.write(prompt4);
@@ -8480,7 +9234,7 @@ async function promptPassword(prompt4) {
8480
9234
  setRawModeGuarded(stdin, wasRaw);
8481
9235
  stdin.removeListener("data", onData);
8482
9236
  stdout.write("\n");
8483
- resolve20(value);
9237
+ resolve21(value);
8484
9238
  };
8485
9239
  const onData = (chunk) => {
8486
9240
  try {
@@ -8582,7 +9336,7 @@ async function loginWithGithubDevice(context) {
8582
9336
  const deadline = Date.now() + expires_in * 1e3;
8583
9337
  let githubToken = null;
8584
9338
  while (Date.now() < deadline) {
8585
- await new Promise((resolve20) => setTimeout(resolve20, pollMs));
9339
+ await new Promise((resolve21) => setTimeout(resolve21, pollMs));
8586
9340
  let tokenResp;
8587
9341
  try {
8588
9342
  tokenResp = await fetch(GITHUB_TOKEN_URL, {
@@ -8606,7 +9360,7 @@ async function loginWithGithubDevice(context) {
8606
9360
  if (tokenData.error === "authorization_pending") continue;
8607
9361
  if (tokenData.error === "slow_down") {
8608
9362
  pollMs += 5e3;
8609
- await new Promise((resolve20) => setTimeout(resolve20, 5e3));
9363
+ await new Promise((resolve21) => setTimeout(resolve21, 5e3));
8610
9364
  continue;
8611
9365
  }
8612
9366
  context.addMessage("system", `GitHub auth error: ${tokenData.error_description || tokenData.error}`);
@@ -8951,9 +9705,9 @@ var init_unlock = __esm({
8951
9705
  });
8952
9706
 
8953
9707
  // src/commands/doctor.ts
8954
- import { homedir as homedir12 } from "os";
8955
- import { join as join23 } from "path";
8956
- import { existsSync as existsSync19 } from "fs";
9708
+ import { homedir as homedir14 } from "os";
9709
+ import { join as join25 } from "path";
9710
+ import { existsSync as existsSync20 } from "fs";
8957
9711
  import { readFile as readFile18 } from "fs/promises";
8958
9712
  async function checkApiHealth(client) {
8959
9713
  try {
@@ -8979,9 +9733,9 @@ async function checkAuthStatus(client) {
8979
9733
  }
8980
9734
  }
8981
9735
  async function checkSettingsFile() {
8982
- const settingsPath = join23(homedir12(), ".msapling", "settings.json");
9736
+ const settingsPath = join25(homedir14(), ".msapling", "settings.json");
8983
9737
  try {
8984
- if (!existsSync19(settingsPath)) {
9738
+ if (!existsSync20(settingsPath)) {
8985
9739
  return { ok: false, message: `Not found: ${settingsPath}` };
8986
9740
  }
8987
9741
  const text = await readFile18(settingsPath, "utf8");
@@ -9656,7 +10410,7 @@ function setRawModeGuarded2(stdin, mode) {
9656
10410
  }
9657
10411
  }
9658
10412
  async function promptSecret(prompt4) {
9659
- return new Promise((resolve20) => {
10413
+ return new Promise((resolve21) => {
9660
10414
  const stdin = process.stdin;
9661
10415
  const stdout = process.stdout;
9662
10416
  stdout.write(prompt4);
@@ -9669,12 +10423,12 @@ async function promptSecret(prompt4) {
9669
10423
  setRawModeGuarded2(stdin, wasRaw);
9670
10424
  stdin.removeListener("data", onData);
9671
10425
  stdout.write("\n");
9672
- resolve20(secret);
10426
+ resolve21(secret);
9673
10427
  } else if (char === "") {
9674
10428
  setRawModeGuarded2(stdin, wasRaw);
9675
10429
  stdin.removeListener("data", onData);
9676
10430
  stdout.write("\n");
9677
- resolve20("");
10431
+ resolve21("");
9678
10432
  } else if (char === "\x7F" || char === "\b") {
9679
10433
  secret = secret.slice(0, -1);
9680
10434
  } else if (char >= " " && char <= "~") {
@@ -9834,8 +10588,8 @@ var init_memories = __esm({
9834
10588
 
9835
10589
  // src/commands/mdrive.ts
9836
10590
  import { readFile as readFile19, writeFile as writeFile11 } from "fs/promises";
9837
- import { existsSync as existsSync20 } from "fs";
9838
- import { basename as basename2, resolve as resolve14, relative as relative14, isAbsolute as isAbsolute14, sep as sep3 } from "path";
10591
+ import { existsSync as existsSync21 } from "fs";
10592
+ import { basename as basename2, resolve as resolve15, relative as relative15, isAbsolute as isAbsolute15, sep as sep3 } from "path";
9839
10593
  function formatBytes(b) {
9840
10594
  if (!b) return "0";
9841
10595
  if (b < 1024) return `${b}`;
@@ -9843,10 +10597,10 @@ function formatBytes(b) {
9843
10597
  return `${(b / 1024 / 1024).toFixed(1)}M`;
9844
10598
  }
9845
10599
  function containLocalPath(targetPath, root = process.cwd()) {
9846
- const resolvedRoot = resolve14(root);
9847
- const resolved = isAbsolute14(targetPath) ? resolve14(targetPath) : resolve14(resolvedRoot, targetPath);
9848
- const rel = relative14(resolvedRoot, resolved);
9849
- if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") || isAbsolute14(rel)) {
10600
+ const resolvedRoot = resolve15(root);
10601
+ const resolved = isAbsolute15(targetPath) ? resolve15(targetPath) : resolve15(resolvedRoot, targetPath);
10602
+ const rel = relative15(resolvedRoot, resolved);
10603
+ if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") || isAbsolute15(rel)) {
9850
10604
  return null;
9851
10605
  }
9852
10606
  return resolved;
@@ -9927,7 +10681,7 @@ var init_mdrive = __esm({
9927
10681
  context.addMessage("error", `Refusing to read local file outside the working directory: ${local}`);
9928
10682
  return;
9929
10683
  }
9930
- if (!existsSync20(absLocal)) {
10684
+ if (!existsSync21(absLocal)) {
9931
10685
  context.addMessage("error", `Local file not found: ${absLocal}`);
9932
10686
  return;
9933
10687
  }
@@ -10048,14 +10802,14 @@ var init_clear = __esm({
10048
10802
  });
10049
10803
 
10050
10804
  // src/commands/mode.ts
10051
- import { homedir as homedir13 } from "os";
10052
- import { join as join24 } from "path";
10053
- import { existsSync as existsSync21 } from "fs";
10805
+ import { homedir as homedir15 } from "os";
10806
+ import { join as join26 } from "path";
10807
+ import { existsSync as existsSync22 } from "fs";
10054
10808
  import { readFile as readFile20, writeFile as writeFile12, mkdir as mkdir7 } from "fs/promises";
10055
10809
  async function persistApprovalMode(mode, ttlMs) {
10056
10810
  try {
10057
10811
  let existing = {};
10058
- if (existsSync21(SETTINGS_PATH)) {
10812
+ if (existsSync22(SETTINGS_PATH)) {
10059
10813
  const text = await readFile20(SETTINGS_PATH, "utf8");
10060
10814
  if (text.trim()) {
10061
10815
  existing = JSON.parse(text);
@@ -10067,8 +10821,8 @@ async function persistApprovalMode(mode, ttlMs) {
10067
10821
  ...ttlMs && { ttlMs }
10068
10822
  };
10069
10823
  existing.approvalMode = entry;
10070
- const settingsDir = join24(homedir13(), ".msapling");
10071
- if (!existsSync21(settingsDir)) {
10824
+ const settingsDir = join26(homedir15(), ".msapling");
10825
+ if (!existsSync22(settingsDir)) {
10072
10826
  await mkdir7(settingsDir, { recursive: true });
10073
10827
  }
10074
10828
  await writeFile12(SETTINGS_PATH, JSON.stringify(existing, null, 2), "utf8");
@@ -10080,7 +10834,7 @@ var init_mode = __esm({
10080
10834
  "src/commands/mode.ts"() {
10081
10835
  "use strict";
10082
10836
  init_esm_shims();
10083
- SETTINGS_PATH = join24(homedir13(), ".msapling", "settings.json");
10837
+ SETTINGS_PATH = join26(homedir15(), ".msapling", "settings.json");
10084
10838
  modeCommand = {
10085
10839
  name: "mode",
10086
10840
  args: "[default|plan|acceptEdits|bypassPermissions] [...options]",
@@ -10502,8 +11256,8 @@ var init_compact = __esm({
10502
11256
  });
10503
11257
 
10504
11258
  // src/commands/init.ts
10505
- import { join as join25 } from "path";
10506
- import { existsSync as existsSync22 } from "fs";
11259
+ import { join as join27 } from "path";
11260
+ import { existsSync as existsSync23 } from "fs";
10507
11261
  import { writeFile as writeFile13 } from "fs/promises";
10508
11262
  var initCommand;
10509
11263
  var init_init = __esm({
@@ -10517,8 +11271,8 @@ var init_init = __esm({
10517
11271
  handler: async (args2, context) => {
10518
11272
  try {
10519
11273
  const cwd = process.cwd();
10520
- const path2 = join25(cwd, "MSAPLING.md");
10521
- if (existsSync22(path2)) {
11274
+ const path2 = join27(cwd, "MSAPLING.md");
11275
+ if (existsSync23(path2)) {
10522
11276
  context.addMessage("error", "MSAPLING.md already exists in current directory.");
10523
11277
  return;
10524
11278
  }
@@ -10544,7 +11298,7 @@ var init_init = __esm({
10544
11298
  });
10545
11299
 
10546
11300
  // src/commands/review.ts
10547
- import { existsSync as existsSync23 } from "fs";
11301
+ import { existsSync as existsSync24 } from "fs";
10548
11302
  import { readFile as readFile21 } from "fs/promises";
10549
11303
  var reviewCommand;
10550
11304
  var init_review = __esm({
@@ -10564,7 +11318,7 @@ var init_review = __esm({
10564
11318
  }
10565
11319
  let content = "";
10566
11320
  try {
10567
- if (existsSync23(target)) {
11321
+ if (existsSync24(target)) {
10568
11322
  content = await readFile21(target, "utf8");
10569
11323
  } else {
10570
11324
  content = `Review target: ${target}`;
@@ -10658,15 +11412,15 @@ var init_swarm = __esm({
10658
11412
 
10659
11413
  // src/commands/recipe.ts
10660
11414
  import { parse as parseYaml } from "yaml";
10661
- import { existsSync as existsSync24 } from "fs";
11415
+ import { existsSync as existsSync25 } from "fs";
10662
11416
  import { readFile as readFile22 } from "fs/promises";
10663
- import { join as join26 } from "path";
11417
+ import { join as join28 } from "path";
10664
11418
  function findRecipe(name, cwd) {
10665
11419
  for (const dir of RECIPE_DIRS) {
10666
11420
  for (const suffix of NAME_SUFFIXES) {
10667
11421
  for (const ext of FILE_EXTS) {
10668
- const p = join26(cwd, dir, `${name}${suffix}${ext}`);
10669
- if (existsSync24(p)) return p;
11422
+ const p = join28(cwd, dir, `${name}${suffix}${ext}`);
11423
+ if (existsSync25(p)) return p;
10670
11424
  }
10671
11425
  }
10672
11426
  }
@@ -10779,13 +11533,13 @@ ${rendered}` : rendered;
10779
11533
  });
10780
11534
 
10781
11535
  // src/commands/skill.ts
10782
- import { existsSync as existsSync25, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
11536
+ import { existsSync as existsSync26, readdirSync as readdirSync3, statSync as statSync7 } from "fs";
10783
11537
  import { readFile as readFile23 } from "fs/promises";
10784
- import { join as join27, resolve as resolve15 } from "path";
11538
+ import { join as join29, resolve as resolve16 } from "path";
10785
11539
  function findSkillsRoot(cwd) {
10786
11540
  for (const candidate of SKILLS_DIRS) {
10787
- const full = resolve15(cwd, candidate);
10788
- if (existsSync25(full) && statSync5(full).isDirectory()) return full;
11541
+ const full = resolve16(cwd, candidate);
11542
+ if (existsSync26(full) && statSync7(full).isDirectory()) return full;
10789
11543
  }
10790
11544
  return null;
10791
11545
  }
@@ -10793,28 +11547,28 @@ function listAllSkills(root) {
10793
11547
  const out = [];
10794
11548
  let domains;
10795
11549
  try {
10796
- domains = readdirSync2(root);
11550
+ domains = readdirSync3(root);
10797
11551
  } catch {
10798
11552
  return out;
10799
11553
  }
10800
11554
  for (const domain of domains) {
10801
- const dir = join27(root, domain);
11555
+ const dir = join29(root, domain);
10802
11556
  let s;
10803
11557
  try {
10804
- s = statSync5(dir);
11558
+ s = statSync7(dir);
10805
11559
  } catch {
10806
11560
  continue;
10807
11561
  }
10808
11562
  if (!s.isDirectory()) continue;
10809
11563
  let files;
10810
11564
  try {
10811
- files = readdirSync2(dir);
11565
+ files = readdirSync3(dir);
10812
11566
  } catch {
10813
11567
  continue;
10814
11568
  }
10815
11569
  for (const f of files) {
10816
11570
  if (!f.endsWith(".md")) continue;
10817
- out.push({ domain, name: f.slice(0, -3), path: join27(dir, f) });
11571
+ out.push({ domain, name: f.slice(0, -3), path: join29(dir, f) });
10818
11572
  }
10819
11573
  }
10820
11574
  return out.sort(
@@ -10903,9 +11657,9 @@ ${prompt4}`;
10903
11657
  });
10904
11658
 
10905
11659
  // src/commands/benchmark.ts
10906
- import { homedir as homedir14 } from "os";
10907
- import { join as join28 } from "path";
10908
- import { mkdirSync as mkdirSync4 } from "fs";
11660
+ import { homedir as homedir16 } from "os";
11661
+ import { join as join30 } from "path";
11662
+ import { mkdirSync as mkdirSync5 } from "fs";
10909
11663
  import * as fs2 from "fs";
10910
11664
  function parseArgs(args2) {
10911
11665
  let models = null;
@@ -11024,10 +11778,10 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
11024
11778
  `[HW at run time: CPU ${hwAtEnd.cpuPct}% / RAM ${hwAtEnd.ramPct}% | ${hw.ramGiB} GiB RAM, ${hw.cores} cores]`
11025
11779
  );
11026
11780
  try {
11027
- const dir = join28(homedir14(), ".msapling", "benchmarks");
11028
- mkdirSync4(dir, { recursive: true });
11781
+ const dir = join30(homedir16(), ".msapling", "benchmarks");
11782
+ mkdirSync5(dir, { recursive: true });
11029
11783
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
11030
- const file = join28(dir, `${ts}.json`);
11784
+ const file = join30(dir, `${ts}.json`);
11031
11785
  const run = {
11032
11786
  ts: (/* @__PURE__ */ new Date()).toISOString(),
11033
11787
  rounds,
@@ -11273,22 +12027,22 @@ var init_theme = __esm({
11273
12027
  });
11274
12028
 
11275
12029
  // src/commands/theme.ts
11276
- import { join as join29 } from "path";
11277
- import { homedir as homedir15 } from "os";
11278
- import { existsSync as existsSync26 } from "fs";
12030
+ import { join as join31 } from "path";
12031
+ import { homedir as homedir17 } from "os";
12032
+ import { existsSync as existsSync27 } from "fs";
11279
12033
  import { readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
11280
12034
  async function persistTheme(storage, themeName) {
11281
- const settingsPath = join29(homedir15(), ".msapling", "settings.json");
12035
+ const settingsPath = join31(homedir17(), ".msapling", "settings.json");
11282
12036
  let existing = {};
11283
12037
  try {
11284
- if (existsSync26(settingsPath)) {
12038
+ if (existsSync27(settingsPath)) {
11285
12039
  const text = await readFile24(settingsPath, "utf8");
11286
12040
  if (text.trim()) existing = JSON.parse(text);
11287
12041
  }
11288
12042
  } catch {
11289
12043
  }
11290
12044
  existing["theme"] = themeName;
11291
- ensureConfigDir(join29(homedir15(), ".msapling"));
12045
+ ensureConfigDir(join31(homedir17(), ".msapling"));
11292
12046
  await writeFile14(settingsPath, JSON.stringify(existing, null, 2), "utf8");
11293
12047
  }
11294
12048
  var VALID_THEMES, themeCommand;
@@ -11360,7 +12114,7 @@ var init_version = __esm({
11360
12114
  description: "Show version information for CLI and core packages",
11361
12115
  category: "debug",
11362
12116
  handler: async (_args, context) => {
11363
- const cliVersion = true ? "2.3.6-beta.46" : "(dev)";
12117
+ const cliVersion = true ? "2.3.6-beta.47" : "(dev)";
11364
12118
  const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
11365
12119
  const runtime = process.version;
11366
12120
  context.addMessage("system", "MSapling Version Info");
@@ -11369,7 +12123,7 @@ var init_version = __esm({
11369
12123
  context.addMessage("system", row2("Core (@msapling/core)", coreVersion));
11370
12124
  context.addMessage("system", row2("Runtime (Node/Bun)", runtime));
11371
12125
  try {
11372
- const ts = "2026-06-20T07:22:28.613Z";
12126
+ const ts = "2026-06-20T08:11:43.445Z";
11373
12127
  if (ts && ts !== "__BUILD_TIMESTAMP__") {
11374
12128
  context.addMessage("system", row2("Build Timestamp", ts));
11375
12129
  }
@@ -11382,14 +12136,14 @@ var init_version = __esm({
11382
12136
  });
11383
12137
 
11384
12138
  // src/commands/feedback.ts
11385
- import { join as join30 } from "path";
11386
- import { existsSync as existsSync27 } from "fs";
12139
+ import { join as join32 } from "path";
12140
+ import { existsSync as existsSync28 } from "fs";
11387
12141
  import { readFile as readFile25 } from "fs/promises";
11388
12142
  async function readCliVersion() {
11389
12143
  try {
11390
12144
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
11391
- const pkgPath = join30(baseDir, "..", "..", "package.json");
11392
- if (!existsSync27(pkgPath)) return "unknown";
12145
+ const pkgPath = join32(baseDir, "..", "..", "package.json");
12146
+ if (!existsSync28(pkgPath)) return "unknown";
11393
12147
  const text = await readFile25(pkgPath, "utf8");
11394
12148
  const json = JSON.parse(text);
11395
12149
  return json.version ?? "unknown";
@@ -11430,8 +12184,8 @@ var init_feedback = __esm({
11430
12184
  });
11431
12185
 
11432
12186
  // src/commands/export.ts
11433
- import { homedir as homedir16 } from "os";
11434
- import { join as join31 } from "path";
12187
+ import { homedir as homedir18 } from "os";
12188
+ import { join as join33 } from "path";
11435
12189
  import { writeFile as writeFile15, mkdir as mkdir8 } from "fs/promises";
11436
12190
  function formatTimestamp(date) {
11437
12191
  return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
@@ -11481,10 +12235,10 @@ var init_export = __esm({
11481
12235
  let outputPath;
11482
12236
  let content;
11483
12237
  if (arg === "" || arg === "json") {
11484
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.json`);
12238
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.json`);
11485
12239
  content = buildJsonExport(history);
11486
12240
  } else if (arg === "markdown" || arg === "md") {
11487
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.md`);
12241
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.md`);
11488
12242
  content = buildMarkdownExport(history);
11489
12243
  } else {
11490
12244
  outputPath = arg;
@@ -11496,7 +12250,7 @@ var init_export = __esm({
11496
12250
  }
11497
12251
  }
11498
12252
  try {
11499
- const dir = join31(outputPath, "..");
12253
+ const dir = join33(outputPath, "..");
11500
12254
  await mkdir8(dir, { recursive: true });
11501
12255
  await writeFile15(outputPath, content, "utf8");
11502
12256
  context.addMessage("system", `Exported to: ${outputPath}`);
@@ -11691,16 +12445,16 @@ var init_plan = __esm({
11691
12445
  });
11692
12446
 
11693
12447
  // src/commands/note.ts
11694
- import { homedir as homedir17 } from "os";
11695
- import { join as join32 } from "path";
11696
- import { existsSync as existsSync28 } from "fs";
12448
+ import { homedir as homedir19 } from "os";
12449
+ import { join as join34 } from "path";
12450
+ import { existsSync as existsSync29 } from "fs";
11697
12451
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
11698
12452
  function getNotesFilePath() {
11699
- return join32(homedir17(), ".msapling", "notes.json");
12453
+ return join34(homedir19(), ".msapling", "notes.json");
11700
12454
  }
11701
12455
  async function readNotes(filePath = getNotesFilePath()) {
11702
12456
  try {
11703
- if (!existsSync28(filePath)) return [];
12457
+ if (!existsSync29(filePath)) return [];
11704
12458
  const raw = await readFile26(filePath, "utf8");
11705
12459
  const parsed = JSON.parse(raw);
11706
12460
  if (!Array.isArray(parsed)) return [];
@@ -11710,7 +12464,7 @@ async function readNotes(filePath = getNotesFilePath()) {
11710
12464
  }
11711
12465
  }
11712
12466
  async function writeNotes(notes, filePath = getNotesFilePath()) {
11713
- const dir = join32(homedir17(), ".msapling");
12467
+ const dir = join34(homedir19(), ".msapling");
11714
12468
  ensureConfigDir(dir);
11715
12469
  await writeFile16(filePath, JSON.stringify(notes, null, 2), "utf8");
11716
12470
  }
@@ -11856,17 +12610,17 @@ var init_todo = __esm({
11856
12610
  });
11857
12611
 
11858
12612
  // src/commands/outputStyle.ts
11859
- import { homedir as homedir18 } from "os";
11860
- import { join as join33, basename as basename3, extname as extname3 } from "path";
11861
- import { existsSync as existsSync29, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
12613
+ import { homedir as homedir20 } from "os";
12614
+ import { join as join35, basename as basename3, extname as extname3 } from "path";
12615
+ import { existsSync as existsSync30, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
11862
12616
  function resolveHome() {
11863
- return process.env.HOME || process.env.USERPROFILE || homedir18();
12617
+ return process.env.HOME || process.env.USERPROFILE || homedir20();
11864
12618
  }
11865
12619
  function stylesDir() {
11866
- return join33(resolveHome(), ".msapling", "output-styles");
12620
+ return join35(resolveHome(), ".msapling", "output-styles");
11867
12621
  }
11868
12622
  function activeFile() {
11869
- return join33(stylesDir(), ".active");
12623
+ return join35(stylesDir(), ".active");
11870
12624
  }
11871
12625
  function parseStyleFile(text) {
11872
12626
  const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
@@ -11887,13 +12641,13 @@ function parseStyleFile(text) {
11887
12641
  }
11888
12642
  function listUserStyles() {
11889
12643
  const dir = stylesDir();
11890
- if (!existsSync29(dir)) return [];
12644
+ if (!existsSync30(dir)) return [];
11891
12645
  const out = [];
11892
- for (const entry of readdirSync3(dir)) {
12646
+ for (const entry of readdirSync4(dir)) {
11893
12647
  if (extname3(entry).toLowerCase() !== ".md") continue;
11894
- const full = join33(dir, entry);
12648
+ const full = join35(dir, entry);
11895
12649
  try {
11896
- const text = readFileSync2(full, "utf8");
12650
+ const text = readFileSync4(full, "utf8");
11897
12651
  const { description, body } = parseStyleFile(text);
11898
12652
  out.push({
11899
12653
  name: basename3(entry, ".md"),
@@ -11919,15 +12673,15 @@ function findStyle(name) {
11919
12673
  function getActiveStyleName() {
11920
12674
  try {
11921
12675
  const f = activeFile();
11922
- if (!existsSync29(f)) return "default";
11923
- return readFileSync2(f, "utf8").trim() || "default";
12676
+ if (!existsSync30(f)) return "default";
12677
+ return readFileSync4(f, "utf8").trim() || "default";
11924
12678
  } catch {
11925
12679
  return "default";
11926
12680
  }
11927
12681
  }
11928
12682
  function setActiveStyleName(name) {
11929
12683
  const dir = stylesDir();
11930
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
12684
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
11931
12685
  writeFileSync5(activeFile(), `${name}
11932
12686
  `, "utf8");
11933
12687
  }
@@ -11940,8 +12694,8 @@ function createUserStyle(name, description, body) {
11940
12694
  throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
11941
12695
  }
11942
12696
  const dir = stylesDir();
11943
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
11944
- const target = join33(dir, `${name}.md`);
12697
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
12698
+ const target = join35(dir, `${name}.md`);
11945
12699
  const frontmatter = `---
11946
12700
  description: ${description.replace(/\n/g, " ")}
11947
12701
  ---
@@ -13105,6 +13859,259 @@ Note: server may return a [server notice] caveat for v1-scaffold features.`
13105
13859
  }
13106
13860
  });
13107
13861
 
13862
+ // src/commands/rewind.ts
13863
+ var rewindCommand;
13864
+ var init_rewind = __esm({
13865
+ "src/commands/rewind.ts"() {
13866
+ "use strict";
13867
+ init_esm_shims();
13868
+ init_src3();
13869
+ rewindCommand = {
13870
+ name: "rewind",
13871
+ aliases: ["checkpoint", "restore"],
13872
+ args: "[n] [--confirm]",
13873
+ description: "List file-edit checkpoints or restore files to a prior snapshot. Usage: /rewind (list) \xB7 /rewind <n> (preview) \xB7 /rewind <n> --confirm (restore).",
13874
+ category: "chat",
13875
+ handler(args2, context) {
13876
+ const checkpoints = listCheckpoints();
13877
+ if (args2.length === 0) {
13878
+ if (checkpoints.length === 0) {
13879
+ context.addMessage(
13880
+ "system",
13881
+ "No checkpoints found. File edits made by the agent will be listed here."
13882
+ );
13883
+ return;
13884
+ }
13885
+ const lines = [
13886
+ `${checkpoints.length} checkpoint(s) \u2014 most recent first:`,
13887
+ ""
13888
+ ];
13889
+ checkpoints.forEach((cp, idx) => {
13890
+ const num = idx + 1;
13891
+ const fileCount = cp.backups.length;
13892
+ const when = new Date(cp.timestamp).toLocaleString();
13893
+ lines.push(
13894
+ ` ${num}. [${when}] ${cp.label} (${fileCount} file${fileCount !== 1 ? "s" : ""})`
13895
+ );
13896
+ });
13897
+ lines.push("");
13898
+ lines.push("To preview a restore: /rewind <n>");
13899
+ lines.push("To restore: /rewind <n> --confirm");
13900
+ context.addMessage("system", lines.join("\n"));
13901
+ return;
13902
+ }
13903
+ const positional = args2.filter((a) => !a.startsWith("-"));
13904
+ const hasConfirm = args2.includes("--confirm") || args2.includes("-y") || args2.includes("--yes");
13905
+ if (positional.length === 0) {
13906
+ context.addMessage("system", "Usage: /rewind <n> [--confirm]");
13907
+ return;
13908
+ }
13909
+ const rawN = positional[0];
13910
+ const n = parseInt(rawN, 10);
13911
+ if (isNaN(n) || n < 1) {
13912
+ context.addMessage(
13913
+ "error",
13914
+ `Invalid checkpoint number: "${rawN}". Run /rewind to list available checkpoints.`
13915
+ );
13916
+ return;
13917
+ }
13918
+ if (checkpoints.length === 0) {
13919
+ context.addMessage("system", "No checkpoints available to restore.");
13920
+ return;
13921
+ }
13922
+ if (n > checkpoints.length) {
13923
+ context.addMessage(
13924
+ "error",
13925
+ `Checkpoint #${n} does not exist. ${checkpoints.length} checkpoint(s) available. Run /rewind to list them.`
13926
+ );
13927
+ return;
13928
+ }
13929
+ const target = checkpoints[n - 1];
13930
+ if (!hasConfirm) {
13931
+ const when = new Date(target.timestamp).toLocaleString();
13932
+ const lines = [
13933
+ `Checkpoint #${n}: ${target.label} [${when}]`,
13934
+ ""
13935
+ ];
13936
+ if (target.backups.length === 0) {
13937
+ lines.push(" (no file edits recorded in this checkpoint)");
13938
+ } else {
13939
+ lines.push("Files that will be reverted:");
13940
+ for (const rec of target.backups) {
13941
+ lines.push(` - ${rec.originalPath}`);
13942
+ }
13943
+ }
13944
+ lines.push("");
13945
+ lines.push(`To restore these files, run: /rewind ${n} --confirm`);
13946
+ lines.push(
13947
+ "Note: only file contents are restored (chat history is NOT truncated)."
13948
+ );
13949
+ context.addMessage("system", lines.join("\n"));
13950
+ return;
13951
+ }
13952
+ if (target.backups.length === 0) {
13953
+ context.addMessage(
13954
+ "system",
13955
+ `Checkpoint #${n} has no file edits \u2014 nothing to restore.`
13956
+ );
13957
+ return;
13958
+ }
13959
+ const results = restoreCheckpoint(target);
13960
+ const restored = results.filter((r) => r.startsWith("Restored:"));
13961
+ const skipped = results.filter((r) => r.startsWith("Skip"));
13962
+ const errors = results.filter((r) => r.startsWith("Error"));
13963
+ const summary = [
13964
+ `Rewind to checkpoint #${n} complete.`,
13965
+ "",
13966
+ ...results
13967
+ ];
13968
+ if (errors.length > 0) {
13969
+ summary.push("");
13970
+ summary.push(
13971
+ `${errors.length} error(s) occurred. Check that the backup files in ~/.msapling/backups/ are accessible.`
13972
+ );
13973
+ }
13974
+ if (restored.length > 0 && errors.length === 0) {
13975
+ summary.push("");
13976
+ summary.push(
13977
+ `${restored.length} file(s) restored successfully. Note: chat history was NOT truncated (follow-up: REWIND-FOLLOW-UP-01).`
13978
+ );
13979
+ }
13980
+ context.addMessage("system", summary.join("\n"));
13981
+ }
13982
+ };
13983
+ }
13984
+ });
13985
+
13986
+ // src/commands/bashes.ts
13987
+ var bashesCommand;
13988
+ var init_bashes = __esm({
13989
+ "src/commands/bashes.ts"() {
13990
+ "use strict";
13991
+ init_esm_shims();
13992
+ init_src3();
13993
+ bashesCommand = {
13994
+ name: "bashes",
13995
+ aliases: ["bg", "background"],
13996
+ args: "[read <id> [n] | kill <id|all> | <id>]",
13997
+ description: "List, inspect, or kill background shells",
13998
+ category: "debug",
13999
+ handler(args2, context) {
14000
+ const sub = (args2[0] ?? "").toLowerCase();
14001
+ if (sub === "kill") {
14002
+ const target = args2[1] ?? "";
14003
+ if (!target) {
14004
+ context.addMessage("error", "Usage: /bashes kill <id|all>");
14005
+ return;
14006
+ }
14007
+ if (target.toLowerCase() === "all") {
14008
+ const running = BackgroundShellRegistry.list().filter((s) => s.status === "running");
14009
+ BackgroundShellRegistry.killAll();
14010
+ context.addMessage("system", `Killed ${running.length} running background shell(s).`);
14011
+ return;
14012
+ }
14013
+ const result = BackgroundShellRegistry.kill(target);
14014
+ if (result === "killed") context.addMessage("system", `Killed background shell ${target}.`);
14015
+ else if (result === "already")
14016
+ context.addMessage("system", `Background shell ${target} had already finished.`);
14017
+ else context.addMessage("error", `No background shell with id "${target}".`);
14018
+ return;
14019
+ }
14020
+ const explicitRead = sub === "read";
14021
+ const id = explicitRead ? args2[1] : args2[0];
14022
+ if (id) {
14023
+ const lineArg = explicitRead ? args2[2] : args2[1];
14024
+ const lines = lineArg && /^\d+$/.test(lineArg) ? parseInt(lineArg, 10) : void 0;
14025
+ const read = BackgroundShellRegistry.readOutput(id, lines);
14026
+ const info = BackgroundShellRegistry.get(id);
14027
+ if (!read || !info) {
14028
+ context.addMessage("error", `No background shell with id "${id}".`);
14029
+ return;
14030
+ }
14031
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
14032
+ `;
14033
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
14034
+ const trunc = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
14035
+ context.addMessage("system", header + body + trunc);
14036
+ return;
14037
+ }
14038
+ context.addMessage("system", formatBackgroundShells(BackgroundShellRegistry.list()));
14039
+ }
14040
+ };
14041
+ }
14042
+ });
14043
+
14044
+ // src/commands/agents.ts
14045
+ import { homedir as homedir21 } from "os";
14046
+ function resolveHome2() {
14047
+ return process.env.HOME || process.env.USERPROFILE || homedir21();
14048
+ }
14049
+ function describeTools(agent) {
14050
+ if (agent.tools === void 0) return "all (inherits full tool set)";
14051
+ if (agent.tools.length === 0) return "none (read/reason only)";
14052
+ return agent.tools.join(", ");
14053
+ }
14054
+ var agentsCommand;
14055
+ var init_agents = __esm({
14056
+ "src/commands/agents.ts"() {
14057
+ "use strict";
14058
+ init_esm_shims();
14059
+ init_src3();
14060
+ agentsCommand = {
14061
+ name: "agents",
14062
+ aliases: ["agent"],
14063
+ args: "[name]",
14064
+ description: "List named subagents (.msapling/agents/*.md), or show one. Named agents have their own system prompt, model, and tool allowlist.",
14065
+ category: "swarm",
14066
+ handler: (args2, context) => {
14067
+ const cwd = process.cwd();
14068
+ const home = resolveHome2();
14069
+ if (args2.length >= 1 && args2[0] !== "list" && args2[0] !== "ls") {
14070
+ const ref = args2[0];
14071
+ const agent = findNamedAgent(ref, cwd, home);
14072
+ if (!agent) {
14073
+ context.addMessage(
14074
+ "error",
14075
+ `No named agent "${ref}". Run /agents (no args) to list available agents.`
14076
+ );
14077
+ return;
14078
+ }
14079
+ context.addMessage("system", `Agent: ${agent.name} [${agent.scope}]`);
14080
+ context.addMessage("system", ` description: ${agent.description}`);
14081
+ context.addMessage("system", ` model: ${agent.model ?? "(inherit session model)"}`);
14082
+ context.addMessage("system", ` tools: ${describeTools(agent)}`);
14083
+ context.addMessage("system", ` source: ${agent.path}`);
14084
+ context.addMessage("system", " \u2500\u2500\u2500 system prompt \u2500\u2500\u2500");
14085
+ context.addMessage("system", agent.systemPrompt || "(empty)");
14086
+ context.addMessage(
14087
+ "system",
14088
+ `Run it: the model can call dispatch_agent with agent="${agent.name}".`
14089
+ );
14090
+ return;
14091
+ }
14092
+ const agents = loadNamedAgents(cwd, home);
14093
+ if (agents.length === 0) {
14094
+ context.addMessage(
14095
+ "system",
14096
+ "No named agents found. Create one at .msapling/agents/<name>.md (project) or ~/.msapling/agents/<name>.md (global). Frontmatter: name, description, model (optional), tools (optional allowlist); body = the system prompt."
14097
+ );
14098
+ return;
14099
+ }
14100
+ context.addMessage("system", `Named subagents (${agents.length}):`);
14101
+ for (const a of agents) {
14102
+ const model = a.model ? ` model=${a.model}` : "";
14103
+ const toolCount = a.tools === void 0 ? "tools=all" : `tools=${a.tools.length}`;
14104
+ context.addMessage(
14105
+ "system",
14106
+ ` ${a.name.padEnd(20)} [${a.scope}] ${toolCount}${model} \u2014 ${a.description}`
14107
+ );
14108
+ }
14109
+ context.addMessage("system", "Run /agents <name> to see an agent in full.");
14110
+ }
14111
+ };
14112
+ }
14113
+ });
14114
+
13108
14115
  // src/commands/index.ts
13109
14116
  var commands_exports = {};
13110
14117
  __export(commands_exports, {
@@ -13176,6 +14183,9 @@ var init_commands = __esm({
13176
14183
  init_diag();
13177
14184
  init_parallel();
13178
14185
  init_remoteAgent();
14186
+ init_rewind();
14187
+ init_bashes();
14188
+ init_agents();
13179
14189
  commands = [
13180
14190
  loginCommand,
13181
14191
  logoutCommand,
@@ -13239,7 +14249,10 @@ var init_commands = __esm({
13239
14249
  syncCommand,
13240
14250
  diagCommand,
13241
14251
  parallelCommand,
13242
- remoteAgentCommand
14252
+ remoteAgentCommand,
14253
+ rewindCommand,
14254
+ bashesCommand,
14255
+ agentsCommand
13243
14256
  ];
13244
14257
  }
13245
14258
  });
@@ -13309,15 +14322,15 @@ var exec_exports = {};
13309
14322
  __export(exec_exports, {
13310
14323
  runExec: () => runExec
13311
14324
  });
13312
- import { existsSync as existsSync32 } from "fs";
14325
+ import { existsSync as existsSync33 } from "fs";
13313
14326
  import { readFile as readFile29 } from "fs/promises";
13314
- import { homedir as homedir20 } from "os";
13315
- import { join as join35 } from "path";
14327
+ import { homedir as homedir23 } from "os";
14328
+ import { join as join37 } from "path";
13316
14329
  async function loadPersistedSettings() {
13317
14330
  const out = { mode: "default", theme: null };
13318
14331
  try {
13319
- const p = join35(homedir20(), ".msapling", "settings.json");
13320
- if (!existsSync32(p)) return out;
14332
+ const p = join37(homedir23(), ".msapling", "settings.json");
14333
+ if (!existsSync33(p)) return out;
13321
14334
  const raw = JSON.parse(await readFile29(p, "utf8"));
13322
14335
  const parsed = parseApprovalMode(raw, Date.now());
13323
14336
  if (parsed.kind === "ok") out.mode = parsed.mode;
@@ -13553,7 +14566,7 @@ var init_format = __esm({
13553
14566
  });
13554
14567
 
13555
14568
  // src/commands/billing/open-browser.ts
13556
- import { spawn as spawn10 } from "child_process";
14569
+ import { spawn as spawn11 } from "child_process";
13557
14570
  async function openBrowser(url) {
13558
14571
  const platform5 = process.platform;
13559
14572
  let cmd;
@@ -13568,13 +14581,13 @@ async function openBrowser(url) {
13568
14581
  cmd = "xdg-open";
13569
14582
  args2 = [url];
13570
14583
  }
13571
- return new Promise((resolve20) => {
14584
+ return new Promise((resolve21) => {
13572
14585
  try {
13573
- const child = spawn10(cmd, args2, { stdio: "ignore", detached: true });
14586
+ const child = spawn11(cmd, args2, { stdio: "ignore", detached: true });
13574
14587
  child.unref();
13575
14588
  } catch {
13576
14589
  }
13577
- resolve20();
14590
+ resolve21();
13578
14591
  });
13579
14592
  }
13580
14593
  var init_open_browser = __esm({
@@ -13664,7 +14677,7 @@ var init_checkout = __esm({
13664
14677
  // src/commands/billing/sub.ts
13665
14678
  import * as readline from "readline";
13666
14679
  function prompt(rl, question) {
13667
- return new Promise((resolve20) => rl.question(question, resolve20));
14680
+ return new Promise((resolve21) => rl.question(question, resolve21));
13668
14681
  }
13669
14682
  async function runSub(argv) {
13670
14683
  const subCmd = argv[0] ?? "";
@@ -13790,11 +14803,11 @@ var init_sub = __esm({
13790
14803
  // src/commands/billing/topup.ts
13791
14804
  import * as readline2 from "readline";
13792
14805
  function prompt2(rl, question) {
13793
- return new Promise((resolve20) => rl.question(question, resolve20));
14806
+ return new Promise((resolve21) => rl.question(question, resolve21));
13794
14807
  }
13795
14808
  function promptDefault(rl, question, defaultVal) {
13796
14809
  return new Promise(
13797
- (resolve20) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve20(ans.trim() || defaultVal))
14810
+ (resolve21) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve21(ans.trim() || defaultVal))
13798
14811
  );
13799
14812
  }
13800
14813
  async function runTopup(argv) {
@@ -13929,7 +14942,7 @@ var init_redeem = __esm({
13929
14942
  // src/commands/billing/gift.ts
13930
14943
  import * as readline3 from "readline";
13931
14944
  function prompt3(rl, question) {
13932
- return new Promise((resolve20) => rl.question(question, resolve20));
14945
+ return new Promise((resolve21) => rl.question(question, resolve21));
13933
14946
  }
13934
14947
  async function runGift(argv) {
13935
14948
  const subCmd = argv[0] ?? "";
@@ -14175,11 +15188,11 @@ var doctor_exports = {};
14175
15188
  __export(doctor_exports, {
14176
15189
  runDoctor: () => runDoctor
14177
15190
  });
14178
- import { homedir as homedir21, platform as platform4, tmpdir } from "os";
14179
- import { join as join36 } from "path";
14180
- import { existsSync as existsSync33, statSync as statSync6 } from "fs";
15191
+ import { homedir as homedir24, platform as platform4, tmpdir } from "os";
15192
+ import { join as join38 } from "path";
15193
+ import { existsSync as existsSync34, statSync as statSync8 } from "fs";
14181
15194
  import { readdir as readdir3, mkdir as mkdir9, rm as rm3 } from "fs/promises";
14182
- import { exec } from "child_process";
15195
+ import { exec as exec2 } from "child_process";
14183
15196
  import { promisify } from "util";
14184
15197
  async function checkNodeVersion() {
14185
15198
  const version = process.version;
@@ -14200,8 +15213,8 @@ async function checkNodeVersion() {
14200
15213
  };
14201
15214
  }
14202
15215
  async function checkConfigDir() {
14203
- const configDir = join36(homedir21(), ".msapling");
14204
- if (!existsSync33(configDir)) {
15216
+ const configDir = join38(homedir24(), ".msapling");
15217
+ if (!existsSync34(configDir)) {
14205
15218
  return {
14206
15219
  name: "Config directory",
14207
15220
  status: "WARN",
@@ -14209,7 +15222,7 @@ async function checkConfigDir() {
14209
15222
  remediation: `mkdir -p "${configDir}" && chmod 700 "${configDir}"`
14210
15223
  };
14211
15224
  }
14212
- const stats = statSync6(configDir);
15225
+ const stats = statSync8(configDir);
14213
15226
  if (!stats.isDirectory()) {
14214
15227
  return {
14215
15228
  name: "Config directory",
@@ -14270,7 +15283,7 @@ async function checkPathConflicts() {
14270
15283
  const timedOutDirs = [];
14271
15284
  const DIR_TIMEOUT_MS = 1500;
14272
15285
  for (const dir of paths) {
14273
- if (!dir || !existsSync33(dir)) continue;
15286
+ if (!dir || !existsSync34(dir)) continue;
14274
15287
  try {
14275
15288
  const files = await Promise.race([
14276
15289
  readdir3(dir),
@@ -14283,7 +15296,7 @@ async function checkPathConflicts() {
14283
15296
  ]);
14284
15297
  for (const file of files) {
14285
15298
  if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
14286
- const fullPath = join36(dir, file);
15299
+ const fullPath = join38(dir, file);
14287
15300
  conflicts.push(fullPath);
14288
15301
  }
14289
15302
  }
@@ -14401,9 +15414,9 @@ async function checkTokenValidity() {
14401
15414
  }
14402
15415
  async function checkOsSpecific() {
14403
15416
  if (platform4() === "win32") {
14404
- const testDir = join36(tmpdir(), `msapling-longpath-test-${Date.now()}`);
15417
+ const testDir = join38(tmpdir(), `msapling-longpath-test-${Date.now()}`);
14405
15418
  const longDirName = "A".repeat(260);
14406
- const testPath = join36(testDir, longDirName);
15419
+ const testPath = join38(testDir, longDirName);
14407
15420
  try {
14408
15421
  await mkdir9(testDir, { recursive: true });
14409
15422
  try {
@@ -14557,7 +15570,7 @@ var init_doctor2 = __esm({
14557
15570
  "use strict";
14558
15571
  init_esm_shims();
14559
15572
  init_doctorRedact();
14560
- execAsync = promisify(exec);
15573
+ execAsync = promisify(exec2);
14561
15574
  }
14562
15575
  });
14563
15576
 
@@ -14603,21 +15616,21 @@ var init_registry_merger = __esm({
14603
15616
  });
14604
15617
 
14605
15618
  // ../core/src/mcp/local_tools.ts
14606
- import { spawn as spawn11 } from "child_process";
15619
+ import { spawn as spawn12 } from "child_process";
14607
15620
  import { readdir as readdir4, stat as stat4, realpath as realpath3 } from "fs/promises";
14608
- import { resolve as resolve17 } from "path";
15621
+ import { resolve as resolve18 } from "path";
14609
15622
  function asResult(text, isError = false) {
14610
15623
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
14611
15624
  }
14612
15625
  async function runCommand(command, cwd) {
14613
- return new Promise((resolve20) => {
15626
+ return new Promise((resolve21) => {
14614
15627
  let p;
14615
15628
  const timeout = setTimeout(() => {
14616
15629
  if (p) p.kill();
14617
- resolve20({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
15630
+ resolve21({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
14618
15631
  }, 3e4);
14619
15632
  try {
14620
- p = spawn11("sh", ["-c", command], {
15633
+ p = spawn12("sh", ["-c", command], {
14621
15634
  cwd: cwd || process.cwd(),
14622
15635
  stdio: ["ignore", "pipe", "pipe"],
14623
15636
  timeout: 3e4
@@ -14632,15 +15645,15 @@ async function runCommand(command, cwd) {
14632
15645
  });
14633
15646
  p.on("error", (e) => {
14634
15647
  clearTimeout(timeout);
14635
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
15648
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
14636
15649
  });
14637
15650
  p.on("exit", (code) => {
14638
15651
  clearTimeout(timeout);
14639
- resolve20({ stdout, stderr, exit_code: code });
15652
+ resolve21({ stdout, stderr, exit_code: code });
14640
15653
  });
14641
15654
  } catch (e) {
14642
15655
  clearTimeout(timeout);
14643
- resolve20({
15656
+ resolve21({
14644
15657
  stdout: "",
14645
15658
  stderr: e?.message ?? "Failed to spawn process",
14646
15659
  exit_code: -1
@@ -14707,7 +15720,7 @@ async function callLocalTool(name, args2, projectRoot) {
14707
15720
  const command = String(args2.command ?? "");
14708
15721
  let cwd = projectRoot;
14709
15722
  if (args2.cwd) {
14710
- cwd = resolve17(projectRoot, String(args2.cwd));
15723
+ cwd = resolve18(projectRoot, String(args2.cwd));
14711
15724
  try {
14712
15725
  const resolvedCwd = await realpath3(cwd);
14713
15726
  const resolvedRoot = await realpath3(projectRoot);
@@ -14744,7 +15757,7 @@ ${res.stderr}`
14744
15757
  return asResult("path is required", true);
14745
15758
  }
14746
15759
  try {
14747
- const resolvedPath = await realpath3(resolve17(projectRoot, pathArg));
15760
+ const resolvedPath = await realpath3(resolve18(projectRoot, pathArg));
14748
15761
  const resolvedRoot = await realpath3(projectRoot);
14749
15762
  if (!resolvedPath.startsWith(resolvedRoot)) {
14750
15763
  return asResult("Error: path attempts to escape project root", true);
@@ -14763,7 +15776,7 @@ ${res.stderr}`
14763
15776
  }
14764
15777
  case "local_glob": {
14765
15778
  const pattern = String(args2.pattern ?? "");
14766
- let cwd = args2.cwd ? resolve17(projectRoot, String(args2.cwd)) : projectRoot;
15779
+ let cwd = args2.cwd ? resolve18(projectRoot, String(args2.cwd)) : projectRoot;
14767
15780
  if (!pattern) {
14768
15781
  return asResult("pattern is required", true);
14769
15782
  }
@@ -14792,7 +15805,7 @@ ${res.stderr}`
14792
15805
  }
14793
15806
  if (path2) {
14794
15807
  try {
14795
- const resolvedPath = await realpath3(resolve17(projectRoot, path2));
15808
+ const resolvedPath = await realpath3(resolve18(projectRoot, path2));
14796
15809
  const resolvedRoot = await realpath3(projectRoot);
14797
15810
  if (!resolvedPath.startsWith(resolvedRoot)) {
14798
15811
  return asResult("Error: path attempts to escape project root", true);
@@ -14812,7 +15825,7 @@ ${res.stderr}`
14812
15825
  return asResult("cwd is required", true);
14813
15826
  }
14814
15827
  try {
14815
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15828
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14816
15829
  const resolvedRoot = await realpath3(projectRoot);
14817
15830
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14818
15831
  return asResult("Error: cwd attempts to escape project root", true);
@@ -14838,7 +15851,7 @@ ${status2.porcelain || "(clean)"}`
14838
15851
  return asResult("cwd is required", true);
14839
15852
  }
14840
15853
  try {
14841
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15854
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14842
15855
  const resolvedRoot = await realpath3(projectRoot);
14843
15856
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14844
15857
  return asResult("Error: cwd attempts to escape project root", true);
@@ -15032,13 +16045,13 @@ var init_base = __esm({
15032
16045
  editLength++;
15033
16046
  };
15034
16047
  if (callback) {
15035
- (function exec2() {
16048
+ (function exec3() {
15036
16049
  setTimeout(function() {
15037
16050
  if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
15038
16051
  return callback(void 0);
15039
16052
  }
15040
16053
  if (!execEditLength()) {
15041
- exec2();
16054
+ exec3();
15042
16055
  }
15043
16056
  }, 0);
15044
16057
  })();
@@ -16080,8 +17093,8 @@ var init_libesm = __esm({
16080
17093
  });
16081
17094
 
16082
17095
  // ../core/src/mcp/catalog.ts
16083
- import { readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync7 } from "fs";
16084
- import { join as join37, relative as relative15 } from "path";
17096
+ import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync9 } from "fs";
17097
+ import { join as join39, relative as relative16 } from "path";
16085
17098
  function buildFileTree(root, maxFiles) {
16086
17099
  const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "build", "dist", ".venv", "venv", ".next", "__pycache__", ".dart_tool", ".bun", "target"]);
16087
17100
  const SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".kt", ".swift", ".dart", ".rb", ".cs", ".cpp", ".c", ".h", ".md", ".json", ".yaml", ".yml", ".toml", ".sh", ".sql"]);
@@ -16091,17 +17104,17 @@ function buildFileTree(root, maxFiles) {
16091
17104
  const dir = queue.shift();
16092
17105
  let entries;
16093
17106
  try {
16094
- entries = readdirSync4(dir);
17107
+ entries = readdirSync5(dir);
16095
17108
  } catch {
16096
17109
  continue;
16097
17110
  }
16098
17111
  for (const name of entries) {
16099
17112
  if (out.length >= maxFiles) break;
16100
17113
  if (SKIP_DIRS2.has(name)) continue;
16101
- const full = join37(dir, name);
17114
+ const full = join39(dir, name);
16102
17115
  let s;
16103
17116
  try {
16104
- s = statSync7(full);
17117
+ s = statSync9(full);
16105
17118
  } catch {
16106
17119
  continue;
16107
17120
  }
@@ -16122,12 +17135,12 @@ function readFilesAsContext(root, files, maxKB) {
16122
17135
  for (const f of files) {
16123
17136
  let body;
16124
17137
  try {
16125
- body = readFileSync3(f, "utf8");
17138
+ body = readFileSync5(f, "utf8");
16126
17139
  } catch {
16127
17140
  continue;
16128
17141
  }
16129
17142
  if (body.length > cap) body = body.slice(0, cap) + "\n[...truncated]";
16130
- const rel = relative15(root, f).replace(/\\/g, "/");
17143
+ const rel = relative16(root, f).replace(/\\/g, "/");
16131
17144
  parts.push(`### ${rel}
16132
17145
 
16133
17146
  \`\`\`
@@ -16329,7 +17342,7 @@ var init_types = __esm({
16329
17342
  });
16330
17343
 
16331
17344
  // ../core/src/mcp/handlers.ts
16332
- import { resolve as resolve19 } from "path";
17345
+ import { resolve as resolve20 } from "path";
16333
17346
  async function callTool(name, args2, client, getIsProCached) {
16334
17347
  switch (name) {
16335
17348
  case "msapling_chat": {
@@ -16427,7 +17440,7 @@ ${r.response ?? ""}`;
16427
17440
  return asResult2(JSON.stringify(result));
16428
17441
  }
16429
17442
  case "msapling_project_context": {
16430
- const root = resolve19(String(args2.path ?? "."));
17443
+ const root = resolve20(String(args2.path ?? "."));
16431
17444
  const maxFiles = Number.isFinite(args2.max_files) ? Number(args2.max_files) : 30;
16432
17445
  const maxKB = Number.isFinite(args2.max_file_size_kb) ? Number(args2.max_file_size_kb) : 50;
16433
17446
  const files = buildFileTree(root, maxFiles);
@@ -16682,13 +17695,13 @@ var init_server = __esm({
16682
17695
  if (inflight === 0) {
16683
17696
  return [];
16684
17697
  }
16685
- const forcedResponses = await new Promise((resolve20) => {
16686
- this._drainResolve = () => resolve20([]);
17698
+ const forcedResponses = await new Promise((resolve21) => {
17699
+ this._drainResolve = () => resolve21([]);
16687
17700
  setTimeout(() => {
16688
17701
  this._drainResolve = null;
16689
17702
  const remaining = Array.from(this._inflightCalls.values());
16690
17703
  if (remaining.length === 0) {
16691
- resolve20([]);
17704
+ resolve21([]);
16692
17705
  return;
16693
17706
  }
16694
17707
  process.stderr.write(
@@ -16704,7 +17717,7 @@ var init_server = __esm({
16704
17717
  }
16705
17718
  }));
16706
17719
  this._inflightCalls.clear();
16707
- resolve20(errorResponses);
17720
+ resolve21(errorResponses);
16708
17721
  }, DRAIN_TIMEOUT_MS);
16709
17722
  });
16710
17723
  return forcedResponses;
@@ -16904,7 +17917,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
16904
17917
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
16905
17918
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
16906
17919
  "\u25CF MSapling CLI v",
16907
- "2.3.6-beta.46"
17920
+ "2.3.6-beta.47"
16908
17921
  ] }),
16909
17922
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
16910
17923
  ] });
@@ -17309,19 +18322,19 @@ function createIdleAwarePoll(pollingIntervalRef, lastActivityRef, client, setUse
17309
18322
  init_esm_shims();
17310
18323
  init_commands();
17311
18324
  init_plan();
17312
- import { spawn as spawn9 } from "child_process";
18325
+ import { spawn as spawn10 } from "child_process";
17313
18326
 
17314
18327
  // src/state/persistentState.ts
17315
18328
  init_esm_shims();
17316
- import { homedir as homedir19 } from "os";
17317
- import { join as join34, dirname as dirname5 } from "path";
17318
- import { existsSync as existsSync30, mkdirSync as mkdirSync6 } from "fs";
18329
+ import { homedir as homedir22 } from "os";
18330
+ import { join as join36, dirname as dirname5 } from "path";
18331
+ import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "fs";
17319
18332
  import { readFile as readFile27, writeFile as writeFile17, rename as rename3 } from "fs/promises";
17320
- import { randomBytes as randomBytes14 } from "crypto";
17321
- var STATE_PATH = join34(homedir19(), ".msapling", "state.json");
18333
+ import { randomBytes as randomBytes15 } from "crypto";
18334
+ var STATE_PATH = join36(homedir22(), ".msapling", "state.json");
17322
18335
  async function loadPersistentState(statePath = STATE_PATH) {
17323
18336
  try {
17324
- if (!existsSync30(statePath)) return { version: 1 };
18337
+ if (!existsSync31(statePath)) return { version: 1 };
17325
18338
  const text = await readFile27(statePath, "utf8");
17326
18339
  const parsed = JSON.parse(text);
17327
18340
  if (parsed.version !== 1) return { version: 1 };
@@ -17337,7 +18350,7 @@ async function loadPersistentState(statePath = STATE_PATH) {
17337
18350
  async function savePersistentState(state, statePath = STATE_PATH) {
17338
18351
  try {
17339
18352
  const dir = dirname5(statePath);
17340
- if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
18353
+ if (!existsSync31(dir)) mkdirSync7(dir, { recursive: true });
17341
18354
  const existing = await loadPersistentState(statePath);
17342
18355
  const merged = {
17343
18356
  version: 1,
@@ -17345,7 +18358,7 @@ async function savePersistentState(state, statePath = STATE_PATH) {
17345
18358
  lastChatId: state.lastChatId ?? existing.lastChatId
17346
18359
  };
17347
18360
  const pid = process.pid;
17348
- const rand = randomBytes14(4).toString("hex");
18361
+ const rand = randomBytes15(4).toString("hex");
17349
18362
  const tmp = `${statePath}.tmp.${pid}.${rand}`;
17350
18363
  await writeFile17(tmp, JSON.stringify(merged, null, 2), "utf8");
17351
18364
  await rename3(tmp, statePath);
@@ -17432,7 +18445,7 @@ ${prompt4}` : prompt4;
17432
18445
  ctx.addMessage("system", "\u26A0 Local shell command executed without MCP/tool-level safety controls. Ensure command is trusted.");
17433
18446
  try {
17434
18447
  const shellArgv = process.platform === "win32" ? ["cmd.exe", "/c", execCmd] : ["sh", "-c", execCmd];
17435
- const proc = spawn9(shellArgv[0], shellArgv.slice(1), {
18448
+ const proc = spawn10(shellArgv[0], shellArgv.slice(1), {
17436
18449
  stdio: ["inherit", "pipe", "pipe"]
17437
18450
  });
17438
18451
  let stdout = "";
@@ -17443,9 +18456,9 @@ ${prompt4}` : prompt4;
17443
18456
  if (proc.stderr) proc.stderr.on("data", (chunk) => {
17444
18457
  stderr += chunk.toString();
17445
18458
  });
17446
- await new Promise((resolve20, reject) => {
18459
+ await new Promise((resolve21, reject) => {
17447
18460
  proc.on("close", (code) => {
17448
- if (code === 0 || code === null) resolve20();
18461
+ if (code === 0 || code === null) resolve21();
17449
18462
  else reject(new Error(`Process exited with code ${code}`));
17450
18463
  });
17451
18464
  proc.on("error", reject);
@@ -17484,9 +18497,9 @@ ${prompt4}` : prompt4;
17484
18497
  for (const mention of fileMentions) {
17485
18498
  const filePath = mention.slice(1);
17486
18499
  try {
17487
- const { existsSync: existsSync34 } = await import("fs");
18500
+ const { existsSync: existsSync35 } = await import("fs");
17488
18501
  const { readFile: readFile30 } = await import("fs/promises");
17489
- if (existsSync34(filePath)) {
18502
+ if (existsSync35(filePath)) {
17490
18503
  const content = await readFile30(filePath, "utf8");
17491
18504
  const MAX_LEN = 32768;
17492
18505
  const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
@@ -17548,7 +18561,7 @@ init_src3();
17548
18561
  init_src();
17549
18562
  init_parseApprovalMode();
17550
18563
  import { readFile as readFile28 } from "fs/promises";
17551
- import { existsSync as existsSync31 } from "fs";
18564
+ import { existsSync as existsSync32 } from "fs";
17552
18565
  async function initSession(ctx) {
17553
18566
  try {
17554
18567
  const journalEncrypted = await initJournalEncryption();
@@ -17573,10 +18586,10 @@ async function initSession(ctx) {
17573
18586
  ctx.setShellEscapeEnabled(settings.shellEscapeEnabled !== false);
17574
18587
  }
17575
18588
  try {
17576
- const { homedir: homedir22 } = await import("os");
17577
- const { join: join39 } = await import("path");
17578
- const userSettingsPath = join39(homedir22(), ".msapling", "settings.json");
17579
- if (existsSync31(userSettingsPath)) {
18589
+ const { homedir: homedir25 } = await import("os");
18590
+ const { join: join41 } = await import("path");
18591
+ const userSettingsPath = join41(homedir25(), ".msapling", "settings.json");
18592
+ if (existsSync32(userSettingsPath)) {
17580
18593
  const userText = await readFile28(userSettingsPath, "utf8");
17581
18594
  let parsed;
17582
18595
  try {
@@ -17734,8 +18747,8 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
17734
18747
  { kind: "approval-request", tool: request.tool, command: request.command, reason: request.reason },
17735
18748
  request.tool
17736
18749
  );
17737
- return new Promise((resolve20) => {
17738
- setPendingApproval({ request, resolve: resolve20 });
18750
+ return new Promise((resolve21) => {
18751
+ setPendingApproval({ request, resolve: resolve21 });
17739
18752
  });
17740
18753
  }, []);
17741
18754
  const agent = useRef(new Agent(client, process.cwd(), requestApproval)).current;
@@ -17993,14 +19006,14 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
17993
19006
 
17994
19007
  // src/runtime/bootstrap.ts
17995
19008
  init_esm_shims();
17996
- import { readFileSync as readFileSync4 } from "fs";
19009
+ import { readFileSync as readFileSync6 } from "fs";
17997
19010
  import { fileURLToPath as fileURLToPath2 } from "url";
17998
- import { dirname as dirname6, join as join38 } from "path";
19011
+ import { dirname as dirname6, join as join40 } from "path";
17999
19012
  function readCliVersion2() {
18000
19013
  const here = dirname6(fileURLToPath2(import.meta.url));
18001
19014
  for (const rel of ["../package.json", "../../package.json"]) {
18002
19015
  try {
18003
- const pkg = JSON.parse(readFileSync4(join38(here, rel), "utf8"));
19016
+ const pkg = JSON.parse(readFileSync6(join40(here, rel), "utf8"));
18004
19017
  if (pkg.name && pkg.version) {
18005
19018
  return { name: pkg.name, version: pkg.version };
18006
19019
  }
@@ -18106,6 +19119,7 @@ function handleCliArgs(args2) {
18106
19119
  }
18107
19120
 
18108
19121
  // src/index.tsx
19122
+ init_src3();
18109
19123
  import { jsx as jsx9 } from "react/jsx-runtime";
18110
19124
  var index_default = App;
18111
19125
  function restoreTerminalMode() {
@@ -18131,6 +19145,12 @@ msapling: fatal ${label}: ${msg}`);
18131
19145
  if (!process.env.NODE_ENV?.includes("test")) {
18132
19146
  process.on("uncaughtException", (err) => handleFatal("uncaught exception", err));
18133
19147
  process.on("unhandledRejection", (reason) => handleFatal("unhandled rejection", reason));
19148
+ process.on("exit", () => {
19149
+ try {
19150
+ BackgroundShellRegistry.killAll();
19151
+ } catch {
19152
+ }
19153
+ });
18134
19154
  }
18135
19155
  var args = process.argv.slice(2);
18136
19156
  var compact = args.includes("--compact");