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

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 +1521 -409
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1032,6 +1032,47 @@ var init_src = __esm({
1032
1032
  body: JSON.stringify({ query, max_results: maxResults })
1033
1033
  });
1034
1034
  }
1035
+ /**
1036
+ * CLI-LOOP-01: cached backend capability map (from GET /chat/health). `null`
1037
+ * until first probed. Cached for the client's lifetime — capabilities are a
1038
+ * deploy-time property of the backend, so a single probe per session suffices.
1039
+ */
1040
+ _capabilities = null;
1041
+ _capabilitiesProbe = null;
1042
+ /**
1043
+ * CLI-LOOP-01: fetch (and cache) the backend capability map from
1044
+ * GET /chat/health. Returns an empty object when the backend is older / the
1045
+ * probe fails, so callers degrade to legacy behavior rather than throwing.
1046
+ *
1047
+ * Concurrent callers share a single in-flight probe.
1048
+ */
1049
+ async getCapabilities(force = false) {
1050
+ if (this._capabilities && !force) return this._capabilities;
1051
+ if (this._capabilitiesProbe && !force) return this._capabilitiesProbe;
1052
+ this._capabilitiesProbe = (async () => {
1053
+ try {
1054
+ const data = await this.request("/api/chat/health");
1055
+ const caps = data && typeof data === "object" && data.capabilities && typeof data.capabilities === "object" ? data.capabilities : {};
1056
+ this._capabilities = caps;
1057
+ return caps;
1058
+ } catch {
1059
+ this._capabilities = {};
1060
+ return this._capabilities;
1061
+ } finally {
1062
+ this._capabilitiesProbe = null;
1063
+ }
1064
+ })();
1065
+ return this._capabilitiesProbe;
1066
+ }
1067
+ /**
1068
+ * CLI-LOOP-01: convenience — does the backend accept the additive structured
1069
+ * `tool_results` array (native parallel multi-tool-use)? When false the Agent
1070
+ * loop uses the legacy `[TOOL_RESULT]`-per-prompt path.
1071
+ */
1072
+ async supportsStructuredToolResults() {
1073
+ const caps = await this.getCapabilities();
1074
+ return caps.structured_tool_results === true;
1075
+ }
1035
1076
  async getHistory(chatId) {
1036
1077
  const data = await this.request(`/api/projects/chat/${chatId}/history`);
1037
1078
  return data.messages.map((m) => ({
@@ -2194,7 +2235,7 @@ var init_RunCommandTool = __esm({
2194
2235
  this.activeCommands++;
2195
2236
  return;
2196
2237
  }
2197
- return new Promise((resolve20) => this.queue.push(resolve20));
2238
+ return new Promise((resolve21) => this.queue.push(resolve21));
2198
2239
  }
2199
2240
  static releaseLock() {
2200
2241
  if (this.queue.length > 0) {
@@ -2274,9 +2315,9 @@ var init_RunCommandTool = __esm({
2274
2315
  const chunks = { stdout: [], stderr: [] };
2275
2316
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
2276
2317
  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));
2318
+ const exitCode = await new Promise((resolve21) => {
2319
+ proc.on("exit", (code) => resolve21(code ?? 1));
2320
+ proc.on("error", () => resolve21(1));
2280
2321
  });
2281
2322
  const stdout = Buffer.concat(chunks.stdout).toString("utf-8");
2282
2323
  const stderr = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -2434,7 +2475,7 @@ var init_src2 = __esm({
2434
2475
  const message = `Content-Length: ${Buffer.byteLength(content, "utf8")}\r
2435
2476
  \r
2436
2477
  ${content}`;
2437
- return new Promise((resolve20, reject) => {
2478
+ return new Promise((resolve21, reject) => {
2438
2479
  const timeoutHandle = setTimeout(() => {
2439
2480
  this.pendingRequests.delete(id);
2440
2481
  reject(new Error(`LSP request timeout after ${timeoutMs}ms: ${method}`));
@@ -2442,7 +2483,7 @@ ${content}`;
2442
2483
  this.pendingRequests.set(id, {
2443
2484
  resolve: (response) => {
2444
2485
  clearTimeout(timeoutHandle);
2445
- resolve20(response);
2486
+ resolve21(response);
2446
2487
  },
2447
2488
  reject: (error) => {
2448
2489
  clearTimeout(timeoutHandle);
@@ -2595,9 +2636,9 @@ var init_SubShellTool = __esm({
2595
2636
  }
2596
2637
  throw e;
2597
2638
  }
2598
- await new Promise((resolve20) => {
2599
- proc.on("exit", () => resolve20());
2600
- proc.on("error", () => resolve20());
2639
+ await new Promise((resolve21) => {
2640
+ proc.on("exit", () => resolve21());
2641
+ proc.on("error", () => resolve21());
2601
2642
  });
2602
2643
  return { content: `Successfully launched separate window for ${args2.worker_id}` };
2603
2644
  }
@@ -2676,27 +2717,42 @@ async function* walkFiles(root, current = root) {
2676
2717
  }
2677
2718
  }
2678
2719
  }
2720
+ function rgCandidates() {
2721
+ if (process.platform === "win32") {
2722
+ return [
2723
+ "rg.exe",
2724
+ "rg",
2725
+ "C:\\Program Files\\ripgrep\\rg.exe",
2726
+ "C:\\ProgramData\\chocolatey\\bin\\rg.exe"
2727
+ ];
2728
+ }
2729
+ return ["rg", "/usr/bin/rg", "/usr/local/bin/rg", "/opt/homebrew/bin/rg"];
2730
+ }
2679
2731
  async function findRg() {
2680
- const candidates = ["rg", "C:\\Program Files\\ripgrep\\rg.exe"];
2681
- for (const bin of candidates) {
2732
+ if (_rgCache.resolved) return _rgCache.value;
2733
+ for (const bin of rgCandidates()) {
2682
2734
  try {
2683
- const exited = await new Promise((resolve20) => {
2735
+ const exited = await new Promise((resolve21) => {
2684
2736
  try {
2685
2737
  const p = spawn4(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
2686
- p.on("error", () => resolve20(null));
2687
- p.on("exit", (code) => resolve20(code));
2738
+ p.on("error", () => resolve21(null));
2739
+ p.on("exit", (code) => resolve21(code));
2688
2740
  } catch {
2689
- resolve20(null);
2741
+ resolve21(null);
2690
2742
  }
2691
2743
  });
2692
- if (exited === 0) return bin;
2744
+ if (exited === 0) {
2745
+ _rgCache = { resolved: true, value: bin };
2746
+ return bin;
2747
+ }
2693
2748
  } catch {
2694
2749
  }
2695
2750
  }
2751
+ _rgCache = { resolved: true, value: null };
2696
2752
  return null;
2697
2753
  }
2698
2754
  function runRg(bin, args2) {
2699
- return new Promise((resolve20) => {
2755
+ return new Promise((resolve21) => {
2700
2756
  const p = spawn4(bin, args2, { stdio: ["ignore", "pipe", "pipe"] });
2701
2757
  let stdout = "";
2702
2758
  let stderr = "";
@@ -2707,17 +2763,24 @@ function runRg(bin, args2) {
2707
2763
  stderr += d.toString("utf8");
2708
2764
  });
2709
2765
  p.on("error", (e) => {
2710
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2766
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2711
2767
  });
2712
2768
  p.on("exit", (code) => {
2713
- resolve20({ stdout, stderr, exitCode: code });
2769
+ resolve21({ stdout, stderr, exitCode: code });
2714
2770
  });
2715
2771
  });
2716
2772
  }
2717
- async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive) {
2773
+ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive, include, exclude) {
2718
2774
  const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2719
2775
  const matches2 = [];
2776
+ const relPaths = [];
2720
2777
  for await (const relPath of walkFiles(searchRoot)) {
2778
+ if (include && !include.test(relPath)) continue;
2779
+ if (exclude && exclude.test(relPath)) continue;
2780
+ relPaths.push(relPath);
2781
+ }
2782
+ relPaths.sort();
2783
+ for (const relPath of relPaths) {
2721
2784
  if (matches2.length >= maxMatches) break;
2722
2785
  const fullPath = join6(searchRoot, relPath);
2723
2786
  try {
@@ -2737,7 +2800,7 @@ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive)
2737
2800
  }
2738
2801
  return matches2;
2739
2802
  }
2740
- var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, GrepSearchTool;
2803
+ var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, _rgCache, GrepSearchTool;
2741
2804
  var init_SearchTools = __esm({
2742
2805
  "../core/src/tools/SearchTools.ts"() {
2743
2806
  "use strict";
@@ -2825,9 +2888,13 @@ var init_SearchTools = __esm({
2825
2888
  }
2826
2889
  }
2827
2890
  };
2891
+ _rgCache = {
2892
+ resolved: false,
2893
+ value: null
2894
+ };
2828
2895
  GrepSearchTool = class extends BaseTool {
2829
2896
  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.";
2897
+ 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
2898
  parameters = {
2832
2899
  type: "object",
2833
2900
  required: ["pattern"],
@@ -2843,6 +2910,14 @@ var init_SearchTools = __esm({
2843
2910
  case_sensitive: {
2844
2911
  type: "boolean",
2845
2912
  description: "Whether the search is case-sensitive (default: false)."
2913
+ },
2914
+ include: {
2915
+ type: "string",
2916
+ description: 'Optional glob of files to include (e.g. "*.ts", "src/**/*.tsx"). Passed to ripgrep as --glob.'
2917
+ },
2918
+ exclude: {
2919
+ type: "string",
2920
+ description: 'Optional glob of files to exclude (e.g. "*.test.ts", "dist/**"). Passed to ripgrep as a negated --glob.'
2846
2921
  }
2847
2922
  }
2848
2923
  };
@@ -2870,6 +2945,8 @@ var init_SearchTools = __esm({
2870
2945
  };
2871
2946
  }
2872
2947
  const caseSensitive = args2?.case_sensitive === true;
2948
+ const include = typeof args2?.include === "string" && args2.include.trim() ? args2.include.trim() : void 0;
2949
+ const exclude = typeof args2?.exclude === "string" && args2.exclude.trim() ? args2.exclude.trim() : void 0;
2873
2950
  const rg = await findRg();
2874
2951
  if (rg) {
2875
2952
  try {
@@ -2877,9 +2954,14 @@ var init_SearchTools = __esm({
2877
2954
  "--line-number",
2878
2955
  `--max-count=${MAX_GREP_MATCHES}`,
2879
2956
  "--no-heading",
2880
- "--color=never"
2957
+ "--color=never",
2958
+ // Deterministic output: sort by file path so repeated queries return
2959
+ // identical ordering (ripgrep parallelises by default → non-stable).
2960
+ "--sort=path"
2881
2961
  ];
2882
2962
  if (!caseSensitive) rgArgs.push("--ignore-case");
2963
+ if (include) rgArgs.push("--glob", include);
2964
+ if (exclude) rgArgs.push("--glob", `!${exclude}`);
2883
2965
  rgArgs.push("--", pattern, searchPath);
2884
2966
  const { stdout, stderr, exitCode } = await runRg(rg, rgArgs);
2885
2967
  if (exitCode !== 0 && exitCode !== 1) {
@@ -2899,15 +2981,28 @@ var init_SearchTools = __esm({
2899
2981
  }
2900
2982
  try {
2901
2983
  const isFile = statSync(searchPath).isFile();
2984
+ const includeRe = include ? globToRegExp(include) : void 0;
2985
+ const excludeRe = exclude ? globToRegExp(exclude) : void 0;
2902
2986
  let matches2;
2903
2987
  if (isFile) {
2904
- const content = await readFile4(searchPath, "utf8");
2905
- const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2906
- const lines = content.split("\n");
2907
2988
  const relPath = args2?.path ?? searchPath;
2908
- matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2989
+ if (includeRe && !includeRe.test(String(relPath)) || excludeRe && excludeRe.test(String(relPath))) {
2990
+ matches2 = [];
2991
+ } else {
2992
+ const content = await readFile4(searchPath, "utf8");
2993
+ const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2994
+ const lines = content.split("\n");
2995
+ matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2996
+ }
2909
2997
  } else {
2910
- matches2 = await nodeGrepFallback(pattern, searchPath, MAX_GREP_MATCHES, caseSensitive);
2998
+ matches2 = await nodeGrepFallback(
2999
+ pattern,
3000
+ searchPath,
3001
+ MAX_GREP_MATCHES,
3002
+ caseSensitive,
3003
+ includeRe,
3004
+ excludeRe
3005
+ );
2911
3006
  }
2912
3007
  if (matches2.length === 0) return { content: "No matches found." };
2913
3008
  const suffix = matches2.length >= MAX_GREP_MATCHES ? `
@@ -3400,13 +3495,164 @@ Pre-edit content backed up to: ${backedUpTo}`;
3400
3495
  }
3401
3496
  });
3402
3497
 
3498
+ // ../core/src/agent/namedAgents.ts
3499
+ import { readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync2 } from "fs";
3500
+ import { join as join9 } from "path";
3501
+ function normalizeToolName(raw) {
3502
+ const trimmed = raw.trim();
3503
+ if (!trimmed) return "";
3504
+ const lower = trimmed.toLowerCase();
3505
+ const key = lower.replace(/[^a-z0-9]/g, "");
3506
+ if (TOOL_NAME_ALIASES[key]) return TOOL_NAME_ALIASES[key];
3507
+ return lower;
3508
+ }
3509
+ function discoverAgentFiles(cwd, homeDir) {
3510
+ const seen = /* @__PURE__ */ new Map();
3511
+ for (const scope of [
3512
+ { dir: join9(homeDir, ".msapling", "agents"), scope: "user" },
3513
+ { dir: join9(cwd, ".msapling", "agents"), scope: "project" }
3514
+ ]) {
3515
+ const entries = safeReaddir(scope.dir);
3516
+ for (const fname of entries) {
3517
+ if (!fname.toLowerCase().endsWith(".md")) continue;
3518
+ const path2 = join9(scope.dir, fname);
3519
+ if (!isRegularFile(path2)) continue;
3520
+ const base = fname.slice(0, -".md".length);
3521
+ seen.set(base, { name: base, path: path2, scope: scope.scope });
3522
+ }
3523
+ }
3524
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
3525
+ }
3526
+ function safeReaddir(dir) {
3527
+ try {
3528
+ return readdirSync2(dir);
3529
+ } catch {
3530
+ return [];
3531
+ }
3532
+ }
3533
+ function isRegularFile(p) {
3534
+ try {
3535
+ return statSync3(p).isFile();
3536
+ } catch {
3537
+ return false;
3538
+ }
3539
+ }
3540
+ function parseToolsValue(raw) {
3541
+ if (raw === void 0) return void 0;
3542
+ let s = raw.trim();
3543
+ if (s === "") return void 0;
3544
+ if (s === "*" || s.toLowerCase() === "all") return void 0;
3545
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
3546
+ const parts = s.split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3547
+ return [...new Set(parts)];
3548
+ }
3549
+ function parseAgentFile(text, fallbackName) {
3550
+ const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
3551
+ if (!fm) {
3552
+ const body = text.trim();
3553
+ const firstLine = body.split(/\r?\n/).find((l) => l.trim()) ?? "";
3554
+ return {
3555
+ name: fallbackName.toLowerCase(),
3556
+ description: firstLine.replace(/^#\s+/, "").trim() || `(agent: ${fallbackName})`,
3557
+ systemPrompt: body
3558
+ };
3559
+ }
3560
+ const meta = fm[1];
3561
+ const systemPrompt = text.slice(fm[0].length).trim();
3562
+ const scalar = (key) => {
3563
+ const m = meta.match(new RegExp(`^${key}[ \\t]*:[ \\t]*(.+)$`, "m"));
3564
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
3565
+ };
3566
+ const name = (scalar("name") ?? fallbackName).toLowerCase();
3567
+ const description = scalar("description") ?? `(agent: ${name})`;
3568
+ const model = scalar("model");
3569
+ let tools;
3570
+ const inlineTools = scalar("tools");
3571
+ const blockMatch = meta.match(/^tools[ \t]*:[ \t]*\n((?:[ \t]*-[ \t]*.+\n?)+)/m);
3572
+ if (blockMatch && (inlineTools === void 0 || inlineTools === "")) {
3573
+ const items = blockMatch[1].split(/\r?\n/).map((l) => l.replace(/^[ \t]*-[ \t]*/, "").trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3574
+ tools = [...new Set(items)];
3575
+ } else {
3576
+ tools = parseToolsValue(inlineTools);
3577
+ }
3578
+ return { name, description, model, tools, systemPrompt };
3579
+ }
3580
+ function loadNamedAgents(cwd, homeDir) {
3581
+ const out = [];
3582
+ for (const file of discoverAgentFiles(cwd, homeDir)) {
3583
+ let text;
3584
+ try {
3585
+ text = readFileSync2(file.path, "utf8");
3586
+ } catch {
3587
+ continue;
3588
+ }
3589
+ const parsed = parseAgentFile(text, file.name);
3590
+ out.push({
3591
+ name: parsed.name,
3592
+ description: parsed.description,
3593
+ model: parsed.model,
3594
+ tools: parsed.tools,
3595
+ systemPrompt: parsed.systemPrompt,
3596
+ path: file.path,
3597
+ scope: file.scope
3598
+ });
3599
+ }
3600
+ const byName = /* @__PURE__ */ new Map();
3601
+ for (const a of out) {
3602
+ const prev = byName.get(a.name);
3603
+ if (!prev || prev.scope === "user" && a.scope === "project") {
3604
+ byName.set(a.name, a);
3605
+ }
3606
+ }
3607
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
3608
+ }
3609
+ function findNamedAgent(name, cwd, homeDir) {
3610
+ const target = name.trim().toLowerCase();
3611
+ return loadNamedAgents(cwd, homeDir).find((a) => a.name === target) ?? null;
3612
+ }
3613
+ var TOOL_NAME_ALIASES;
3614
+ var init_namedAgents = __esm({
3615
+ "../core/src/agent/namedAgents.ts"() {
3616
+ "use strict";
3617
+ init_esm_shims();
3618
+ TOOL_NAME_ALIASES = {
3619
+ read: "read_file",
3620
+ write: "write_file",
3621
+ edit: "edit_file",
3622
+ multiedit: "multi_edit_file",
3623
+ patch: "patch_file",
3624
+ move: "move_file",
3625
+ delete: "delete_file",
3626
+ bash: "bash_command",
3627
+ shell: "bash_command",
3628
+ run: "run_command",
3629
+ glob: "glob_files",
3630
+ grep: "grep_search",
3631
+ ls: "list_directory",
3632
+ list: "list_directory",
3633
+ webfetch: "web_fetch",
3634
+ websearch: "web_search",
3635
+ fetch: "web_fetch",
3636
+ search: "web_search",
3637
+ task: "dispatch_agent",
3638
+ dispatch: "dispatch_agent",
3639
+ todowrite: "todo_write",
3640
+ todoread: "todo_read",
3641
+ notebookread: "notebook_read",
3642
+ notebookedit: "notebook_edit_cell"
3643
+ };
3644
+ }
3645
+ });
3646
+
3403
3647
  // ../core/src/tools/DispatchAgentTool.ts
3648
+ import { homedir as homedir5 } from "os";
3404
3649
  var SUB_AGENT_TIMEOUT_MS, MAX_PROMPT_CHARS, MAX_RESPONSE_CHARS, DEFAULT_SUB_AGENT_MODEL, DispatchAgentTool;
3405
3650
  var init_DispatchAgentTool = __esm({
3406
3651
  "../core/src/tools/DispatchAgentTool.ts"() {
3407
3652
  "use strict";
3408
3653
  init_esm_shims();
3409
3654
  init_BaseTool();
3655
+ init_namedAgents();
3410
3656
  SUB_AGENT_TIMEOUT_MS = 12e4;
3411
3657
  MAX_PROMPT_CHARS = 8e3;
3412
3658
  MAX_RESPONSE_CHARS = 16e3;
@@ -3422,6 +3668,10 @@ var init_DispatchAgentTool = __esm({
3422
3668
  type: "string",
3423
3669
  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
3670
  },
3671
+ agent: {
3672
+ type: "string",
3673
+ 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."
3674
+ },
3425
3675
  description: {
3426
3676
  type: "string",
3427
3677
  description: 'Short human-readable label for this sub-task (e.g. "Summarise auth.ts"). Appears in the session log alongside the result.'
@@ -3440,12 +3690,19 @@ var init_DispatchAgentTool = __esm({
3440
3690
  parentChatId;
3441
3691
  projectRoot;
3442
3692
  onSubagentStop;
3693
+ runNamedAgent;
3443
3694
  constructor(options) {
3444
3695
  super();
3445
3696
  this.client = options.client;
3446
3697
  this.parentChatId = options.parentChatId ?? void 0;
3447
3698
  this.projectRoot = options.projectRoot;
3448
3699
  this.onSubagentStop = options.onSubagentStop;
3700
+ this.runNamedAgent = options.runNamedAgent;
3701
+ }
3702
+ /** Resolve the user home dir, preferring env overrides so test isolation
3703
+ * (HOME / USERPROFILE) is honored exactly like outputStyle.ts. */
3704
+ resolveHome() {
3705
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
3449
3706
  }
3450
3707
  /** Fire the subagent-stop callback defensively — it must never throw. */
3451
3708
  signalStop(info) {
@@ -3462,9 +3719,19 @@ var init_DispatchAgentTool = __esm({
3462
3719
  };
3463
3720
  }
3464
3721
  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
3722
  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";
3723
+ let named = null;
3724
+ if (typeof args2.agent === "string" && args2.agent.trim()) {
3725
+ named = findNamedAgent(args2.agent.trim(), this.projectRoot, this.resolveHome());
3726
+ if (!named) {
3727
+ return {
3728
+ content: `dispatch_agent error: no named agent "${args2.agent.trim()}" found in .msapling/agents/. Run /agents to list available agents.`,
3729
+ isError: true
3730
+ };
3731
+ }
3732
+ }
3733
+ const model = typeof args2.model === "string" && args2.model.trim() ? args2.model.trim() : named?.model && named.model.trim() ? named.model.trim() : DEFAULT_SUB_AGENT_MODEL;
3734
+ const description = typeof args2.description === "string" && args2.description.trim() ? args2.description.trim() : named ? `agent:${named.name}` : "sub-task";
3468
3735
  let response = "";
3469
3736
  const timeoutPromise = new Promise(
3470
3737
  (_, reject) => setTimeout(
@@ -3473,10 +3740,28 @@ var init_DispatchAgentTool = __esm({
3473
3740
  )
3474
3741
  );
3475
3742
  const streamPromise = (async () => {
3743
+ if (named && this.runNamedAgent) {
3744
+ await this.runNamedAgent({
3745
+ agent: named,
3746
+ prompt: prompt4,
3747
+ model,
3748
+ chatId,
3749
+ onContent: (chunk) => {
3750
+ response += chunk;
3751
+ }
3752
+ });
3753
+ return;
3754
+ }
3755
+ const effectivePrompt = named ? `[System prompt for agent "${named.name}"]
3756
+ ${named.systemPrompt}
3757
+
3758
+ ---
3759
+
3760
+ ${prompt4}`.slice(0, MAX_PROMPT_CHARS) : prompt4;
3476
3761
  const stream = this.client.streamChat({
3477
- prompt: prompt4,
3762
+ prompt: effectivePrompt,
3478
3763
  model,
3479
- // No tools — sub-agent is read/reasoning only.
3764
+ // No tools — sub-agent is read/reasoning only on this path.
3480
3765
  tools: [],
3481
3766
  ...chatId ? { chat_id: chatId } : {},
3482
3767
  // Forward project root as context so the backend can enrich the prompt
@@ -4051,12 +4336,12 @@ Command: ${command}`,
4051
4336
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
4052
4337
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
4053
4338
  const timeoutPromise = new Promise(
4054
- (resolve20) => setTimeout(() => resolve20("timeout"), timeoutMs)
4339
+ (resolve21) => setTimeout(() => resolve21("timeout"), timeoutMs)
4055
4340
  );
4056
4341
  const processPromise = (async () => {
4057
- const exitCode2 = await new Promise((resolve20) => {
4058
- proc.on("exit", (code) => resolve20(code ?? 1));
4059
- proc.on("error", () => resolve20(1));
4342
+ const exitCode2 = await new Promise((resolve21) => {
4343
+ proc.on("exit", (code) => resolve21(code ?? 1));
4344
+ proc.on("error", () => resolve21(1));
4060
4345
  });
4061
4346
  const stdout2 = Buffer.concat(chunks.stdout).toString("utf-8");
4062
4347
  const stderr2 = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -4090,8 +4375,315 @@ ${stderr}`;
4090
4375
  }
4091
4376
  });
4092
4377
 
4378
+ // ../core/src/tools/BackgroundShellTool.ts
4379
+ import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8 } from "path";
4380
+ import { spawn as spawn6 } from "child_process";
4381
+ function formatBackgroundShells(shells) {
4382
+ if (shells.length === 0) return "No background shells.";
4383
+ return shells.map((s) => {
4384
+ const dur = ((s.endedAt ?? Date.now()) - s.startedAt) / 1e3;
4385
+ const exit = s.exitCode !== void 0 && s.exitCode !== null ? ` exit=${s.exitCode}` : "";
4386
+ const pid = s.pid !== void 0 ? ` pid=${s.pid}` : "";
4387
+ return `[${s.id}] ${s.status.toUpperCase()}${pid}${exit} ${dur.toFixed(1)}s $ ${s.command}`;
4388
+ }).join("\n");
4389
+ }
4390
+ function resolveCwd(projectRoot, rawArg) {
4391
+ let cwd = projectRoot;
4392
+ if (typeof rawArg === "string" && rawArg.trim()) {
4393
+ const raw = rawArg.trim();
4394
+ const resolved = isAbsolute8(raw) ? normalize7(raw) : resolve8(projectRoot, raw);
4395
+ const rel = relative8(projectRoot, resolved);
4396
+ if (rel.startsWith("..") || isAbsolute8(rel)) {
4397
+ return { error: `Security Block: cwd "${raw}" resolves outside the project root.` };
4398
+ }
4399
+ cwd = resolved;
4400
+ }
4401
+ return { cwd };
4402
+ }
4403
+ var MAX_BUFFER_BYTES, MAX_FINISHED_RETAINED, BackgroundShellRegistryImpl, BackgroundShellRegistry, BashBackgroundTool, ListBackgroundShellsTool, ReadBackgroundShellTool, KillBackgroundShellTool;
4404
+ var init_BackgroundShellTool = __esm({
4405
+ "../core/src/tools/BackgroundShellTool.ts"() {
4406
+ "use strict";
4407
+ init_esm_shims();
4408
+ init_BaseTool();
4409
+ init_BashTool();
4410
+ MAX_BUFFER_BYTES = 256 * 1024;
4411
+ MAX_FINISHED_RETAINED = 50;
4412
+ BackgroundShellRegistryImpl = class {
4413
+ shells = /* @__PURE__ */ new Map();
4414
+ counter = 0;
4415
+ /** Monotonic, human-readable id: bg-1, bg-2, … */
4416
+ nextId() {
4417
+ this.counter += 1;
4418
+ return `bg-${this.counter}`;
4419
+ }
4420
+ /**
4421
+ * Register an already-spawned child process and start buffering its output.
4422
+ * Returns the assigned shell id.
4423
+ */
4424
+ register(proc, command, cwd) {
4425
+ const id = this.nextId();
4426
+ const entry = {
4427
+ id,
4428
+ command,
4429
+ cwd,
4430
+ status: "running",
4431
+ pid: proc.pid,
4432
+ startedAt: Date.now(),
4433
+ proc,
4434
+ buffer: "",
4435
+ bufferTruncated: false
4436
+ };
4437
+ this.shells.set(id, entry);
4438
+ const append = (chunk) => {
4439
+ entry.buffer += chunk.toString("utf-8");
4440
+ if (entry.buffer.length > MAX_BUFFER_BYTES) {
4441
+ entry.buffer = entry.buffer.slice(entry.buffer.length - MAX_BUFFER_BYTES);
4442
+ entry.bufferTruncated = true;
4443
+ }
4444
+ };
4445
+ proc.stdout?.on("data", append);
4446
+ proc.stderr?.on("data", append);
4447
+ proc.on("error", (err) => {
4448
+ if (entry.status === "running") {
4449
+ entry.status = "error";
4450
+ entry.endedAt = Date.now();
4451
+ entry.buffer += `
4452
+ [spawn error: ${err.message}]`;
4453
+ }
4454
+ this.reapFinished();
4455
+ });
4456
+ proc.on("exit", (code, signal) => {
4457
+ if (entry.status === "running") {
4458
+ entry.status = "exited";
4459
+ }
4460
+ entry.exitCode = code;
4461
+ entry.endedAt = Date.now();
4462
+ if (signal) entry.buffer += `
4463
+ [terminated by signal ${signal}]`;
4464
+ this.reapFinished();
4465
+ });
4466
+ return id;
4467
+ }
4468
+ /** Evict the oldest finished shells once we exceed the retention cap. */
4469
+ reapFinished() {
4470
+ const finished = [...this.shells.values()].filter((s) => s.status !== "running").sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
4471
+ let excess = finished.length - MAX_FINISHED_RETAINED;
4472
+ for (let i = 0; i < finished.length && excess > 0; i++, excess--) {
4473
+ this.shells.delete(finished[i].id);
4474
+ }
4475
+ }
4476
+ /** Public view of all tracked shells, newest first. */
4477
+ list() {
4478
+ return [...this.shells.values()].sort((a, b) => b.startedAt - a.startedAt).map((s) => this.toInfo(s));
4479
+ }
4480
+ get(id) {
4481
+ const s = this.shells.get(id);
4482
+ return s ? this.toInfo(s) : void 0;
4483
+ }
4484
+ /**
4485
+ * Read the buffered output of a shell. `lines` (optional) tails the last N
4486
+ * lines. Returns null if the id is unknown.
4487
+ */
4488
+ readOutput(id, lines) {
4489
+ const s = this.shells.get(id);
4490
+ if (!s) return null;
4491
+ let output = s.buffer;
4492
+ if (lines && lines > 0) {
4493
+ const all = output.split("\n");
4494
+ output = all.slice(-lines).join("\n");
4495
+ }
4496
+ return { output, truncated: s.bufferTruncated };
4497
+ }
4498
+ /**
4499
+ * Terminate a running shell. Returns:
4500
+ * - 'killed' on success
4501
+ * - 'not-found' if the id is unknown
4502
+ * - 'already' if the shell had already finished
4503
+ */
4504
+ kill(id) {
4505
+ const s = this.shells.get(id);
4506
+ if (!s) return "not-found";
4507
+ if (s.status !== "running") return "already";
4508
+ s.status = "killed";
4509
+ s.endedAt = Date.now();
4510
+ try {
4511
+ s.proc.kill();
4512
+ } catch {
4513
+ }
4514
+ return "killed";
4515
+ }
4516
+ /** Kill every running shell (used on CLI shutdown). */
4517
+ killAll() {
4518
+ for (const s of this.shells.values()) {
4519
+ if (s.status === "running") {
4520
+ s.status = "killed";
4521
+ s.endedAt = Date.now();
4522
+ try {
4523
+ s.proc.kill();
4524
+ } catch {
4525
+ }
4526
+ }
4527
+ }
4528
+ }
4529
+ /** Test-only: drop all tracked shells (does not kill — caller's responsibility). */
4530
+ _reset() {
4531
+ this.shells.clear();
4532
+ this.counter = 0;
4533
+ }
4534
+ toInfo(s) {
4535
+ return {
4536
+ id: s.id,
4537
+ command: s.command,
4538
+ cwd: s.cwd,
4539
+ status: s.status,
4540
+ pid: s.pid,
4541
+ exitCode: s.exitCode,
4542
+ startedAt: s.startedAt,
4543
+ endedAt: s.endedAt
4544
+ };
4545
+ }
4546
+ };
4547
+ BackgroundShellRegistry = new BackgroundShellRegistryImpl();
4548
+ BashBackgroundTool = class extends BaseTool {
4549
+ name = "bash_background";
4550
+ 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.";
4551
+ parameters = {
4552
+ type: "object",
4553
+ required: ["command"],
4554
+ properties: {
4555
+ command: {
4556
+ type: "string",
4557
+ description: "The shell command to run in the background. May contain pipes, redirects, and other shell operators."
4558
+ },
4559
+ cwd: {
4560
+ type: "string",
4561
+ description: "Optional working directory (relative to project root). Defaults to the project root."
4562
+ }
4563
+ }
4564
+ };
4565
+ async execute(args2, projectRoot) {
4566
+ if (!args2?.command || typeof args2.command !== "string" || args2.command.trim() === "") {
4567
+ return {
4568
+ content: 'Error: bash_background requires a non-empty "command" argument.',
4569
+ isError: true
4570
+ };
4571
+ }
4572
+ const command = args2.command;
4573
+ const block = BashTool.checkBlocklist(command);
4574
+ if (block.blocked) {
4575
+ return {
4576
+ content: `Security Block: command rejected \u2014 ${block.reason}.
4577
+ Command: ${command}`,
4578
+ isError: true
4579
+ };
4580
+ }
4581
+ const cwdResult = resolveCwd(projectRoot, args2.cwd);
4582
+ if ("error" in cwdResult) {
4583
+ return { content: cwdResult.error, isError: true };
4584
+ }
4585
+ const cwd = cwdResult.cwd;
4586
+ const isWindows = process.platform === "win32";
4587
+ const shellArgv = isWindows ? ["cmd", "/c", command] : ["bash", "-c", command];
4588
+ let proc;
4589
+ try {
4590
+ proc = spawn6(shellArgv[0], shellArgv.slice(1), {
4591
+ cwd,
4592
+ stdio: ["ignore", "pipe", "pipe"],
4593
+ shell: false
4594
+ });
4595
+ } catch (e) {
4596
+ return { content: `Error: failed to spawn background shell: ${e.message}`, isError: true };
4597
+ }
4598
+ const id = BackgroundShellRegistry.register(proc, command, cwd);
4599
+ return {
4600
+ content: `Started background shell ${id} (pid ${proc.pid ?? "?"}).
4601
+ Command: ${command}
4602
+ Use read_background_shell with shell_id="${id}" to read its output, or kill_background_shell to stop it.`
4603
+ };
4604
+ }
4605
+ };
4606
+ ListBackgroundShellsTool = class extends BaseTool {
4607
+ name = "list_background_shells";
4608
+ description = "List all background shells started in this session (running and finished), with their id, status, pid, exit code, and command.";
4609
+ parameters = {
4610
+ type: "object",
4611
+ properties: {}
4612
+ };
4613
+ async execute() {
4614
+ return { content: formatBackgroundShells(BackgroundShellRegistry.list()) };
4615
+ }
4616
+ };
4617
+ ReadBackgroundShellTool = class extends BaseTool {
4618
+ name = "read_background_shell";
4619
+ description = "Read the buffered output of a background shell by its id. Optionally tail only the last N lines.";
4620
+ parameters = {
4621
+ type: "object",
4622
+ required: ["shell_id"],
4623
+ properties: {
4624
+ shell_id: {
4625
+ type: "string",
4626
+ description: 'The background shell id (e.g. "bg-1") returned by bash_background.'
4627
+ },
4628
+ lines: {
4629
+ type: "number",
4630
+ description: "Optional: return only the last N lines of buffered output."
4631
+ }
4632
+ }
4633
+ };
4634
+ async execute(args2) {
4635
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4636
+ if (!id) {
4637
+ return { content: 'Error: read_background_shell requires a "shell_id" argument.', isError: true };
4638
+ }
4639
+ const lines = typeof args2?.lines === "number" && args2.lines > 0 ? Math.floor(args2.lines) : void 0;
4640
+ const read = BackgroundShellRegistry.readOutput(id, lines);
4641
+ if (!read) {
4642
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4643
+ }
4644
+ const info = BackgroundShellRegistry.get(id);
4645
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
4646
+ `;
4647
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
4648
+ const truncNote = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
4649
+ return { content: header + body + truncNote };
4650
+ }
4651
+ };
4652
+ KillBackgroundShellTool = class extends BaseTool {
4653
+ name = "kill_background_shell";
4654
+ description = "Terminate a running background shell by its id.";
4655
+ parameters = {
4656
+ type: "object",
4657
+ required: ["shell_id"],
4658
+ properties: {
4659
+ shell_id: {
4660
+ type: "string",
4661
+ description: 'The background shell id (e.g. "bg-1") to terminate.'
4662
+ }
4663
+ }
4664
+ };
4665
+ async execute(args2) {
4666
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4667
+ if (!id) {
4668
+ return { content: 'Error: kill_background_shell requires a "shell_id" argument.', isError: true };
4669
+ }
4670
+ const result = BackgroundShellRegistry.kill(id);
4671
+ switch (result) {
4672
+ case "killed":
4673
+ return { content: `Killed background shell ${id}.` };
4674
+ case "already":
4675
+ return { content: `Background shell ${id} had already finished.` };
4676
+ case "not-found":
4677
+ default:
4678
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4679
+ }
4680
+ }
4681
+ };
4682
+ }
4683
+ });
4684
+
4093
4685
  // ../core/src/tools/NotebookReadTool.ts
4094
- import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8, extname } from "path";
4686
+ import { resolve as resolve9, normalize as normalize8, relative as relative9, isAbsolute as isAbsolute9, extname } from "path";
4095
4687
  import { readFile as readFile6 } from "fs/promises";
4096
4688
  import { existsSync as existsSync7 } from "fs";
4097
4689
  function joinSource(source) {
@@ -4246,9 +4838,9 @@ var init_NotebookReadTool = __esm({
4246
4838
  isError: true
4247
4839
  };
4248
4840
  }
4249
- const absPath = isAbsolute8(rawPath) ? normalize7(rawPath) : resolve8(projectRoot, rawPath);
4250
- const rel = relative8(projectRoot, absPath);
4251
- if (rel.startsWith("..") || isAbsolute8(rel)) {
4841
+ const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4842
+ const rel = relative9(projectRoot, absPath);
4843
+ if (rel.startsWith("..") || isAbsolute9(rel)) {
4252
4844
  return {
4253
4845
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4254
4846
  isError: true
@@ -4316,10 +4908,10 @@ _(Note: notebook has ${nb.cells.length} cells; only first ${MAX_CELLS} shown.)_`
4316
4908
  });
4317
4909
 
4318
4910
  // ../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";
4911
+ import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, extname as extname2, join as join11 } from "path";
4320
4912
  import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
4321
4913
  import { existsSync as existsSync8 } from "fs";
4322
- import { homedir as homedir5 } from "os";
4914
+ import { homedir as homedir6 } from "os";
4323
4915
  import { randomBytes as randomBytes5 } from "crypto";
4324
4916
  function normaliseSource(source) {
4325
4917
  if (source === "") return [];
@@ -4440,9 +5032,9 @@ var init_NotebookEditTool = __esm({
4440
5032
  isError: true
4441
5033
  };
4442
5034
  }
4443
- const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4444
- const rel = relative9(projectRoot, absPath);
4445
- if (rel.startsWith("..") || isAbsolute9(rel)) {
5035
+ const absPath = isAbsolute10(rawPath) ? normalize9(rawPath) : resolve10(projectRoot, rawPath);
5036
+ const rel = relative10(projectRoot, absPath);
5037
+ if (rel.startsWith("..") || isAbsolute10(rel)) {
4446
5038
  return {
4447
5039
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4448
5040
  isError: true
@@ -4499,13 +5091,13 @@ var init_NotebookEditTool = __esm({
4499
5091
  const filename = absPath.split(/[\\/]/).pop() ?? "notebook.ipynb";
4500
5092
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4501
5093
  const suffix = randomBytes5(4).toString("hex");
4502
- const backupPath = join10(
4503
- homedir5(),
5094
+ const backupPath = join11(
5095
+ homedir6(),
4504
5096
  ".msapling",
4505
5097
  "backups",
4506
5098
  `${filename}.backup-${stamp}-${suffix}.bak`
4507
5099
  );
4508
- await mkdir3(join10(homedir5(), ".msapling", "backups"), { recursive: true });
5100
+ await mkdir3(join11(homedir6(), ".msapling", "backups"), { recursive: true });
4509
5101
  await writeFile3(backupPath, rawJson, "utf8");
4510
5102
  backedUpTo = backupPath;
4511
5103
  } catch {
@@ -4575,10 +5167,10 @@ Backup: ${backedUpTo}`;
4575
5167
  });
4576
5168
 
4577
5169
  // ../core/src/tools/MultiEditFileTool.ts
4578
- import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, join as join11 } from "path";
5170
+ import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, join as join12 } from "path";
4579
5171
  import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
4580
5172
  import { existsSync as existsSync9 } from "fs";
4581
- import { homedir as homedir6 } from "os";
5173
+ import { homedir as homedir7 } from "os";
4582
5174
  import { randomBytes as randomBytes6 } from "crypto";
4583
5175
  var MAX_EDITS, MultiEditFileTool;
4584
5176
  var init_MultiEditFileTool = __esm({
@@ -4672,9 +5264,9 @@ var init_MultiEditFileTool = __esm({
4672
5264
  };
4673
5265
  }
4674
5266
  }
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)) {
5267
+ const normalizedTarget = isAbsolute11(args2.path) ? normalize10(args2.path) : resolve11(projectRoot, args2.path.trim());
5268
+ const rel = relative11(projectRoot, normalizedTarget);
5269
+ if (rel.startsWith("..") || isAbsolute11(rel)) {
4678
5270
  return {
4679
5271
  content: `Security Block: path "${args2.path}" resolves outside the project root.`,
4680
5272
  isError: true
@@ -4742,13 +5334,13 @@ No changes were written (atomic: all-or-nothing).`,
4742
5334
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
4743
5335
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4744
5336
  const suffix = randomBytes6(4).toString("hex");
4745
- const backupPath = join11(
4746
- homedir6(),
5337
+ const backupPath = join12(
5338
+ homedir7(),
4747
5339
  ".msapling",
4748
5340
  "backups",
4749
5341
  `${filename}.backup-${stamp}-${suffix}.bak`
4750
5342
  );
4751
- await mkdir4(join11(homedir6(), ".msapling", "backups"), { recursive: true });
5343
+ await mkdir4(join12(homedir7(), ".msapling", "backups"), { recursive: true });
4752
5344
  await writeFile4(backupPath, originalContent, "utf8");
4753
5345
  backedUpTo = backupPath;
4754
5346
  } catch {
@@ -4776,14 +5368,14 @@ Pre-edit content backed up to: ${backedUpTo}`;
4776
5368
  });
4777
5369
 
4778
5370
  // ../core/src/tools/MoveFileTool.ts
4779
- import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, dirname as dirname2 } from "path";
5371
+ import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, dirname as dirname2 } from "path";
4780
5372
  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";
5373
+ import { existsSync as existsSync10, statSync as statSync4 } from "fs";
4782
5374
  import { randomBytes as randomBytes7 } from "crypto";
4783
5375
  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)) {
5376
+ const abs = isAbsolute12(p) ? normalize11(p) : resolve12(root, p.trim());
5377
+ const rel = relative12(root, abs);
5378
+ if (rel.startsWith("..") || isAbsolute12(rel)) {
4787
5379
  return { ok: false, reason: `path "${p}" resolves outside the project root` };
4788
5380
  }
4789
5381
  return { ok: true, abs };
@@ -4792,8 +5384,8 @@ async function copyDir(src, dst) {
4792
5384
  await mkdir5(dst, { recursive: true });
4793
5385
  const entries = await readdir2(src, { withFileTypes: true });
4794
5386
  for (const entry of entries) {
4795
- const srcPath = resolve11(src, entry.name);
4796
- const dstPath = resolve11(dst, entry.name);
5387
+ const srcPath = resolve12(src, entry.name);
5388
+ const dstPath = resolve12(dst, entry.name);
4797
5389
  if (entry.isDirectory()) {
4798
5390
  await copyDir(srcPath, dstPath);
4799
5391
  } else {
@@ -4804,18 +5396,18 @@ async function copyDir(src, dst) {
4804
5396
  async function backupFile(absPath) {
4805
5397
  try {
4806
5398
  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");
5399
+ const { homedir: homedir25 } = await import("os");
5400
+ const { join: join41 } = await import("path");
4809
5401
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
4810
5402
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4811
5403
  const suffix = randomBytes7(4).toString("hex");
4812
- const backupPath = join39(
4813
- homedir22(),
5404
+ const backupPath = join41(
5405
+ homedir25(),
4814
5406
  ".msapling",
4815
5407
  "backups",
4816
5408
  `${filename}.backup-${stamp}-${suffix}.bak`
4817
5409
  );
4818
- await mkdir10(join39(homedir22(), ".msapling", "backups"), { recursive: true });
5410
+ await mkdir10(join41(homedir25(), ".msapling", "backups"), { recursive: true });
4819
5411
  const content = await readFile30(absPath, "utf8");
4820
5412
  await writeFile19(backupPath, content, "utf8");
4821
5413
  return backupPath;
@@ -4875,7 +5467,7 @@ var init_MoveFileTool = __esm({
4875
5467
  isError: true
4876
5468
  };
4877
5469
  }
4878
- const srcStat = statSync3(absSrc);
5470
+ const srcStat = statSync4(absSrc);
4879
5471
  const srcIsDir = srcStat.isDirectory();
4880
5472
  let rawDst = args2.destination.trim();
4881
5473
  const dstPrelimCheck = containedPath(rawDst, projectRoot);
@@ -4883,11 +5475,11 @@ var init_MoveFileTool = __esm({
4883
5475
  return { content: `Security Block: ${dstPrelimCheck.reason}.`, isError: true };
4884
5476
  }
4885
5477
  let absDst = dstPrelimCheck.abs;
4886
- if (existsSync10(absDst) && statSync3(absDst).isDirectory() && !srcIsDir) {
5478
+ if (existsSync10(absDst) && statSync4(absDst).isDirectory() && !srcIsDir) {
4887
5479
  const srcName = absSrc.split(/[\\/]/).pop();
4888
- absDst = resolve11(absDst, srcName);
4889
- const rel2 = relative11(projectRoot, absDst);
4890
- if (rel2.startsWith("..") || isAbsolute11(rel2)) {
5480
+ absDst = resolve12(absDst, srcName);
5481
+ const rel2 = relative12(projectRoot, absDst);
5482
+ if (rel2.startsWith("..") || isAbsolute12(rel2)) {
4891
5483
  return {
4892
5484
  content: `Security Block: adjusted destination "${absDst}" resolves outside the project root.`,
4893
5485
  isError: true
@@ -4908,7 +5500,7 @@ var init_MoveFileTool = __esm({
4908
5500
  isError: true
4909
5501
  };
4910
5502
  }
4911
- if (!statSync3(absDst).isDirectory()) {
5503
+ if (!statSync4(absDst).isDirectory()) {
4912
5504
  backedUpTo = await backupFile(absDst);
4913
5505
  }
4914
5506
  }
@@ -4967,7 +5559,7 @@ Overwritten destination backed up to: ${backedUpTo}`;
4967
5559
  });
4968
5560
 
4969
5561
  // ../core/src/tools/DeleteFileTool.ts
4970
- import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, join as join13 } from "path";
5562
+ import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13, join as join14 } from "path";
4971
5563
  import { rm as rm2, stat as stat3 } from "fs/promises";
4972
5564
  import { randomBytes as randomBytes8 } from "crypto";
4973
5565
  var DeleteFileTool;
@@ -5002,15 +5594,15 @@ var init_DeleteFileTool = __esm({
5002
5594
  }
5003
5595
  const rawPath = args2.path.trim();
5004
5596
  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)) {
5597
+ const abs = isAbsolute13(rawPath) ? normalize12(rawPath) : resolve13(projectRoot, rawPath);
5598
+ const rel = relative13(projectRoot, abs);
5599
+ if (rel.startsWith("..") || isAbsolute13(rel)) {
5008
5600
  return {
5009
5601
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
5010
5602
  isError: true
5011
5603
  };
5012
5604
  }
5013
- if (rel === "" || abs === normalize11(projectRoot)) {
5605
+ if (rel === "" || abs === normalize12(projectRoot)) {
5014
5606
  return {
5015
5607
  content: "Security Block: refusing to delete the project root directory.",
5016
5608
  isError: true
@@ -5043,13 +5635,13 @@ var init_DeleteFileTool = __esm({
5043
5635
  if (isFile) {
5044
5636
  try {
5045
5637
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
5046
- const { homedir: homedir22 } = await import("os");
5638
+ const { homedir: homedir25 } = await import("os");
5047
5639
  const existingContent = await readFile30(abs, "utf8");
5048
5640
  const filename = abs.split(/[\\/]/).pop() ?? "file";
5049
5641
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5050
5642
  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 });
5643
+ const backupPath = join14(homedir25(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
5644
+ await mkdir10(join14(homedir25(), ".msapling", "backups"), { recursive: true });
5053
5645
  await writeFile19(backupPath, existingContent, "utf8");
5054
5646
  backedUpTo = backupPath;
5055
5647
  } catch {
@@ -5191,7 +5783,7 @@ var init_MDrive = __esm({
5191
5783
  });
5192
5784
 
5193
5785
  // ../core/src/Sandbox.ts
5194
- import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13 } from "path";
5786
+ import { resolve as resolve14, normalize as normalize13, relative as relative14, isAbsolute as isAbsolute14 } from "path";
5195
5787
  import { realpathSync } from "fs";
5196
5788
  import { realpath as realpath2 } from "fs/promises";
5197
5789
  import { createHash as createHash3 } from "crypto";
@@ -5250,7 +5842,7 @@ var init_Sandbox = __esm({
5250
5842
  "sc"
5251
5843
  ]);
5252
5844
  constructor(projectRoot, opts) {
5253
- this.projectRoot = resolve13(projectRoot);
5845
+ this.projectRoot = resolve14(projectRoot);
5254
5846
  this.realpathSyncFn = opts?.realpathSync ?? realpathSync;
5255
5847
  }
5256
5848
  setPermissions(state) {
@@ -5265,15 +5857,15 @@ var init_Sandbox = __esm({
5265
5857
  return hasher.digest("hex");
5266
5858
  }
5267
5859
  isPathSafe(targetPath) {
5268
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5860
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5269
5861
  let resolvedTarget = normalizedTarget;
5270
5862
  try {
5271
5863
  resolvedTarget = this.realpathSyncFn(normalizedTarget);
5272
5864
  } catch {
5273
5865
  resolvedTarget = normalizedTarget;
5274
5866
  }
5275
- const rel = relative13(this.projectRoot, resolvedTarget);
5276
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5867
+ const rel = relative14(this.projectRoot, resolvedTarget);
5868
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5277
5869
  if (isOutside) {
5278
5870
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5279
5871
  return { safe: true, absolutePath: resolvedTarget };
@@ -5291,15 +5883,15 @@ var init_Sandbox = __esm({
5291
5883
  * isPathSafe (sync) is kept for cold/boot-time paths and existing tests.
5292
5884
  */
5293
5885
  async isPathSafeAsync(targetPath) {
5294
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5886
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5295
5887
  let resolvedTarget = normalizedTarget;
5296
5888
  try {
5297
5889
  resolvedTarget = await realpath2(normalizedTarget);
5298
5890
  } catch {
5299
5891
  resolvedTarget = normalizedTarget;
5300
5892
  }
5301
- const rel = relative13(this.projectRoot, resolvedTarget);
5302
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5893
+ const rel = relative14(this.projectRoot, resolvedTarget);
5894
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5303
5895
  if (isOutside) {
5304
5896
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5305
5897
  return { safe: true, absolutePath: resolvedTarget };
@@ -5331,7 +5923,7 @@ var init_Sandbox = __esm({
5331
5923
  }
5332
5924
  const parsed = parse(commandLine);
5333
5925
  const tokens = parsed.filter((t) => typeof t === "string");
5334
- const normalize13 = (raw) => {
5926
+ const normalize14 = (raw) => {
5335
5927
  let b = raw.toLowerCase();
5336
5928
  const slash = Math.max(b.lastIndexOf("/"), b.lastIndexOf("\\"));
5337
5929
  if (slash >= 0) b = b.slice(slash + 1);
@@ -5339,7 +5931,7 @@ var init_Sandbox = __esm({
5339
5931
  return b;
5340
5932
  };
5341
5933
  for (const tok of tokens) {
5342
- const b = normalize13(tok);
5934
+ const b = normalize14(tok);
5343
5935
  if (_Sandbox.DANGEROUS_BINARIES.has(b)) {
5344
5936
  return { status: "blocked", reason: `Binary '${b}' is explicitly forbidden.`, hash };
5345
5937
  }
@@ -5347,7 +5939,7 @@ var init_Sandbox = __esm({
5347
5939
  if (tokens.length === 0) {
5348
5940
  return { status: "safe", hash };
5349
5941
  }
5350
- const binary = normalize13(tokens[0]);
5942
+ const binary = normalize14(tokens[0]);
5351
5943
  const sub = tokens.length > 1 ? tokens[1].toLowerCase() : "";
5352
5944
  if (binary === "git") {
5353
5945
  const destructiveGit = /* @__PURE__ */ new Set(["push", "reset", "clean", "rebase", "merge", "force-push"]);
@@ -5446,7 +6038,7 @@ var init_Sandbox = __esm({
5446
6038
  });
5447
6039
 
5448
6040
  // ../core/src/Voice.ts
5449
- import { spawn as spawn6 } from "child_process";
6041
+ import { spawn as spawn7 } from "child_process";
5450
6042
  function buildTtsInvocation(text, rate) {
5451
6043
  const psCommand = `
5452
6044
  Add-Type -AssemblyName System.Speech;
@@ -5486,10 +6078,10 @@ var init_Voice = __esm({
5486
6078
  const { command, args: args2, env } = buildTtsInvocation(text, rate);
5487
6079
  try {
5488
6080
  if (process.platform === "win32") {
5489
- await new Promise((resolve20, reject) => {
6081
+ await new Promise((resolve21, reject) => {
5490
6082
  try {
5491
- const proc = spawn6(command, args2, { env });
5492
- proc.on("exit", () => resolve20());
6083
+ const proc = spawn7(command, args2, { env });
6084
+ proc.on("exit", () => resolve21());
5493
6085
  proc.on("error", reject);
5494
6086
  } catch (e) {
5495
6087
  reject(e);
@@ -5586,7 +6178,7 @@ var init_ShadowService = __esm({
5586
6178
  });
5587
6179
 
5588
6180
  // ../core/src/Hooks.ts
5589
- import { spawn as spawn7 } from "child_process";
6181
+ import { exec } from "child_process";
5590
6182
  function matches(entry, ctx) {
5591
6183
  if (!entry.matcher) return true;
5592
6184
  let re;
@@ -5603,55 +6195,34 @@ function matches(entry, ctx) {
5603
6195
  async function runOne(entry, ctx) {
5604
6196
  const timeoutMs = entry.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
5605
6197
  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 = "";
6198
+ const env = { ...process.env, MSAPLING_HOOK_PAYLOAD: JSON.stringify({ ...ctx }) };
6199
+ return new Promise((resolve21) => {
5614
6200
  let timedOut = false;
5615
6201
  let settled = false;
5616
- const finish = (exitCode) => {
6202
+ const finish = (exitCode, stdout, stderr) => {
5617
6203
  if (settled) return;
5618
6204
  settled = true;
5619
6205
  clearTimeout(killer);
5620
6206
  const blocked = !NON_BLOCKING_EVENTS.has(ctx.event) && !!entry.blocking && (exitCode === null || exitCode !== 0);
5621
- resolve20({ command, exitCode, stdout, stderr, timedOut, blocked });
6207
+ resolve21({ command, exitCode, stdout, stderr, timedOut, blocked });
5622
6208
  };
6209
+ const child = exec(command, {
6210
+ cwd: ctx.cwd ?? process.cwd(),
6211
+ env,
6212
+ maxBuffer: MAX_OUTPUT_BYTES3
6213
+ }, (err, stdout, stderr) => {
6214
+ const rawCode = err ? err.code : 0;
6215
+ const exitCode = typeof rawCode === "number" ? rawCode : null;
6216
+ finish(exitCode, stdout.slice(0, MAX_OUTPUT_BYTES3), stderr.slice(0, MAX_OUTPUT_BYTES3));
6217
+ });
5623
6218
  const killer = setTimeout(() => {
5624
6219
  timedOut = true;
5625
6220
  try {
5626
6221
  child.kill("SIGKILL");
5627
6222
  } catch {
5628
6223
  }
5629
- finish(null);
6224
+ finish(null, "", "");
5630
6225
  }, 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
6226
  });
5656
6227
  }
5657
6228
  var NON_BLOCKING_EVENTS, DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
@@ -5869,9 +6440,9 @@ var init_specs = __esm({
5869
6440
 
5870
6441
  // ../core/src/governor/ResourceGovernor.ts
5871
6442
  import { freemem as freemem2 } from "os";
5872
- import { homedir as homedir7 } from "os";
6443
+ import { homedir as homedir8 } from "os";
5873
6444
  import { readFile as readFile9 } from "fs/promises";
5874
- import { join as join14 } from "path";
6445
+ import { join as join15 } from "path";
5875
6446
  function determineTier(specs) {
5876
6447
  const memGB = specs.memory.totalGB;
5877
6448
  const cores = specs.cpu.cores;
@@ -5886,7 +6457,7 @@ function recommendLimits(specs) {
5886
6457
  }
5887
6458
  async function readConfigOverrides() {
5888
6459
  try {
5889
- const configPath = join14(homedir7(), ".msapling", "config.json");
6460
+ const configPath = join15(homedir8(), ".msapling", "config.json");
5890
6461
  const content = await readFile9(configPath, "utf-8");
5891
6462
  const config = JSON.parse(content);
5892
6463
  return config.limits ?? null;
@@ -5967,7 +6538,7 @@ var init_ResourceGovernor = __esm({
5967
6538
  this.activeAgents++;
5968
6539
  return;
5969
6540
  }
5970
- await new Promise((resolve20) => this.agentWaiters.push(resolve20));
6541
+ await new Promise((resolve21) => this.agentWaiters.push(resolve21));
5971
6542
  this.activeAgents++;
5972
6543
  }
5973
6544
  /**
@@ -5986,7 +6557,7 @@ var init_ResourceGovernor = __esm({
5986
6557
  this.activeTool++;
5987
6558
  return;
5988
6559
  }
5989
- await new Promise((resolve20) => this.toolWaiters.push(resolve20));
6560
+ await new Promise((resolve21) => this.toolWaiters.push(resolve21));
5990
6561
  this.activeTool++;
5991
6562
  }
5992
6563
  /**
@@ -6044,6 +6615,7 @@ var init_ToolExecutor = __esm({
6044
6615
  init_WebFetchTool();
6045
6616
  init_WebSearchTool();
6046
6617
  init_BashTool();
6618
+ init_BackgroundShellTool();
6047
6619
  init_NotebookReadTool();
6048
6620
  init_NotebookEditTool();
6049
6621
  init_MultiEditFileTool();
@@ -6056,9 +6628,14 @@ var init_ToolExecutor = __esm({
6056
6628
  init_ShadowService();
6057
6629
  init_Hooks();
6058
6630
  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"]);
6631
+ 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
6632
  ToolExecutor = class {
6061
6633
  tools = /* @__PURE__ */ new Map();
6634
+ /** Backend client — retained so named-subagent dispatch can open its own
6635
+ * scoped stream (CLI-PARITY-P1-6). */
6636
+ client;
6637
+ /** Project root — retained for named-subagent dispatch context. */
6638
+ projectRoot;
6062
6639
  sandbox;
6063
6640
  mdrive;
6064
6641
  voice;
@@ -6093,6 +6670,8 @@ var init_ToolExecutor = __esm({
6093
6670
  */
6094
6671
  onModeChange = null;
6095
6672
  constructor(client, projectRoot) {
6673
+ this.client = client;
6674
+ this.projectRoot = projectRoot;
6096
6675
  this.sandbox = new Sandbox(projectRoot);
6097
6676
  this.mdrive = new MDriveService(client);
6098
6677
  this.voice = new VoiceService();
@@ -6121,11 +6700,20 @@ var init_ToolExecutor = __esm({
6121
6700
  payload: info,
6122
6701
  cwd: projectRoot
6123
6702
  });
6124
- }
6703
+ },
6704
+ // CLI-PARITY-P1-6: named-subagent runner. Runs a named agent with its own
6705
+ // system prompt + model + TOOL ALLOWLIST. Tools execute back through THIS
6706
+ // executor (so the existing permission/approval system gates writes), but
6707
+ // only the agent's allowlisted tools are advertised + permitted.
6708
+ runNamedAgent: (req) => this.runNamedSubagent(req)
6125
6709
  }));
6126
6710
  this.registerTool(new WebFetchTool());
6127
6711
  this.registerTool(new WebSearchTool({ client }));
6128
6712
  this.registerTool(new BashTool());
6713
+ this.registerTool(new BashBackgroundTool());
6714
+ this.registerTool(new ListBackgroundShellsTool());
6715
+ this.registerTool(new ReadBackgroundShellTool());
6716
+ this.registerTool(new KillBackgroundShellTool());
6129
6717
  this.registerTool(new NotebookReadTool());
6130
6718
  this.registerTool(new NotebookEditTool());
6131
6719
  this.registerTool(new MultiEditFileTool());
@@ -6317,13 +6905,13 @@ ${blocker.stderr || "(empty)"}`,
6317
6905
  return { content: `Security Block: ${check2.reason}`, isError: true };
6318
6906
  }
6319
6907
  }
6320
- if (toolName === "run_command" || toolName === "bash_command") {
6908
+ if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background") {
6321
6909
  if (!args2.command) {
6322
6910
  return { content: `Error: ${toolName} requires a command argument`, isError: true };
6323
6911
  }
6324
6912
  const analysis = this.sandbox.analyzeCommand(args2.command);
6325
6913
  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()}`;
6914
+ const staleCmdKey = toolName === "run_command" ? `run_command:${(args2.command || "").trim()}` : `${toolName}:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}`;
6327
6915
  if (this.trustStore?.has(staleCmdKey)) {
6328
6916
  this.trustStore.delete(staleCmdKey).catch(() => {
6329
6917
  });
@@ -6338,10 +6926,10 @@ ${blocker.stderr || "(empty)"}`,
6338
6926
  let cmdKey = "";
6339
6927
  if (toolName === "run_command") {
6340
6928
  cmdKey = `run_command:${(args2.command || "").trim()}`;
6341
- } else if (toolName === "bash_command") {
6929
+ } else if (toolName === "bash_command" || toolName === "bash_background") {
6342
6930
  const normalizedCmd = (args2.command || "").trim().replace(/\s+/g, " ");
6343
6931
  const cwdSuffix = args2.cwd ? `:cwd=${args2.cwd}` : "";
6344
- cmdKey = `bash_command:${normalizedCmd}${cwdSuffix}`;
6932
+ cmdKey = `${toolName}:${normalizedCmd}${cwdSuffix}`;
6345
6933
  } else {
6346
6934
  cmdKey = `${toolName}:${(args2.path || args2.instruction || "").trim()}`;
6347
6935
  }
@@ -6365,7 +6953,7 @@ ${blocker.stderr || "(empty)"}`,
6365
6953
  }
6366
6954
  }
6367
6955
  }
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") {
6956
+ 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
6957
  const audit = await this.shadow.verifyAction(
6370
6958
  JSON.stringify({ tool: toolName, args: args2 }),
6371
6959
  `Execution context: ${projectRoot}`
@@ -6426,6 +7014,80 @@ Please approve the diff in the UI to sync this change locally.`
6426
7014
  }));
6427
7015
  return [...builtin, ...mcp];
6428
7016
  }
7017
+ /**
7018
+ * CLI-PARITY-P1-6: tool schemas filtered to a named agent's allowlist.
7019
+ *
7020
+ * - `undefined` allowlist → inherit the FULL builtin+MCP set (getToolSchemas).
7021
+ * - `[]` → no tools (read/reason-only agent).
7022
+ * - `['read_file', …]` → ONLY those tools (silently drops names that
7023
+ * don't resolve to a registered tool).
7024
+ *
7025
+ * `dispatch_agent` is always excluded from a subagent's view so a named agent
7026
+ * cannot recursively spawn further named agents (avoids unbounded fan-out).
7027
+ */
7028
+ getToolSchemasForAllowlist(allow) {
7029
+ const all = this.getToolSchemas();
7030
+ if (allow === void 0) {
7031
+ return all.filter((s) => s.name !== "dispatch_agent");
7032
+ }
7033
+ const allowSet = new Set(allow);
7034
+ return all.filter((s) => s.name !== "dispatch_agent" && allowSet.has(s.name));
7035
+ }
7036
+ /**
7037
+ * CLI-PARITY-P1-6: run a NAMED subagent to completion.
7038
+ *
7039
+ * Drives a bounded agentic loop against the backend using the agent's system
7040
+ * prompt + model, advertising ONLY the agent's allowlisted tools. Tool calls
7041
+ * execute back through `this.execute(...)`, so the SAME permission/approval +
7042
+ * shadow-audit gates apply — the allowlist narrows WHICH tools the agent may
7043
+ * request; the permission system still governs WHETHER a given write runs.
7044
+ *
7045
+ * This is the mechanism that closes the "subagents are read-only" gap: a named
7046
+ * agent whose frontmatter grants `write_file`/`edit_file`/`bash_command` can
7047
+ * use them (subject to approval), whereas the anonymous dispatch path cannot.
7048
+ */
7049
+ async runNamedSubagent(req) {
7050
+ const { agent, prompt: prompt4, model, chatId, onContent } = req;
7051
+ const schemas = this.getToolSchemasForAllowlist(agent.tools);
7052
+ const framedPrompt = `[You are the "${agent.name}" subagent. Operate strictly within this role.]
7053
+ ${agent.systemPrompt}
7054
+
7055
+ ---
7056
+
7057
+ ${prompt4}`;
7058
+ const MAX_SUBAGENT_ROUNDS = 12;
7059
+ const queue = [framedPrompt];
7060
+ let rounds = 0;
7061
+ while (queue.length > 0 && rounds < MAX_SUBAGENT_ROUNDS) {
7062
+ const current = queue.shift();
7063
+ rounds++;
7064
+ const stream = this.client.streamChat({
7065
+ ...chatId ? { chat_id: chatId } : {},
7066
+ prompt: current,
7067
+ model,
7068
+ tools: schemas,
7069
+ project_root: this.projectRoot,
7070
+ mode: this.mode
7071
+ });
7072
+ for await (const chunk of stream) {
7073
+ if (chunk.content) onContent(chunk.content);
7074
+ if (chunk.tool_use) {
7075
+ const name = chunk.tool_use.name;
7076
+ if (agent.tools !== void 0 && !agent.tools.includes(name)) {
7077
+ queue.push(
7078
+ `[TOOL_RESULT]: ${JSON.stringify({
7079
+ content: `Tool "${name}" is not in the "${agent.name}" agent's allowlist; refused.`,
7080
+ isError: true
7081
+ })}`
7082
+ );
7083
+ continue;
7084
+ }
7085
+ const result = await this.execute(name, chunk.tool_use.args, this.projectRoot);
7086
+ queue.push(`[TOOL_RESULT]: ${JSON.stringify(result)}`);
7087
+ }
7088
+ }
7089
+ }
7090
+ }
6429
7091
  };
6430
7092
  }
6431
7093
  });
@@ -6471,8 +7133,8 @@ var init_Safety = __esm({
6471
7133
  });
6472
7134
 
6473
7135
  // ../core/src/ProjectConfig.ts
6474
- import { homedir as homedir8 } from "os";
6475
- import { join as join15, dirname as dirname3, parse as parsePath } from "path";
7136
+ import { homedir as homedir9 } from "os";
7137
+ import { join as join16, dirname as dirname3, parse as parsePath } from "path";
6476
7138
  import { existsSync as existsSync11 } from "fs";
6477
7139
  import { readFile as readFile11 } from "fs/promises";
6478
7140
  async function readIfExists(path2) {
@@ -6486,7 +7148,7 @@ async function readIfExists(path2) {
6486
7148
  }
6487
7149
  async function findInDir(dir) {
6488
7150
  for (const filename of FILENAMES) {
6489
- const path2 = join15(dir, filename);
7151
+ const path2 = join16(dir, filename);
6490
7152
  const content = await readIfExists(path2);
6491
7153
  if (content !== null) {
6492
7154
  return { path: path2, filename, content };
@@ -6509,9 +7171,9 @@ async function findProjectConfig(start) {
6509
7171
  return null;
6510
7172
  }
6511
7173
  async function findUserConfig() {
6512
- const home = homedir8();
7174
+ const home = homedir9();
6513
7175
  if (!home) return null;
6514
- const userDir = join15(home, ".msapling");
7176
+ const userDir = join16(home, ".msapling");
6515
7177
  return findInDir(userDir);
6516
7178
  }
6517
7179
  function buildCombined(user, project) {
@@ -6786,8 +7448,15 @@ var init_Agent = __esm({
6786
7448
  }
6787
7449
  const config = await this.getProjectConfig();
6788
7450
  const MAX_WORKER_TURN_DEPTH = 25;
7451
+ let structuredToolResults = false;
7452
+ try {
7453
+ structuredToolResults = await this.client.supportsStructuredToolResults();
7454
+ } catch {
7455
+ structuredToolResults = false;
7456
+ }
6789
7457
  const queue = [prompt4];
6790
7458
  let rounds = 0;
7459
+ let toolCallSeq = 0;
6791
7460
  let streamUsage = null;
6792
7461
  while (queue.length > 0 && rounds < MAX_WORKER_TURN_DEPTH) {
6793
7462
  if (this.contextBudget.needsCompaction() && queue.length > 0) {
@@ -6831,14 +7500,25 @@ var init_Agent = __esm({
6831
7500
  this.contextBudget.reset();
6832
7501
  if (compactionSummary) {
6833
7502
  const next = queue[0];
6834
- queue[0] = `[CONTEXT_SUMMARY]: ${compactionSummary}
7503
+ if (typeof next === "string") {
7504
+ queue[0] = `[CONTEXT_SUMMARY]: ${compactionSummary}
6835
7505
 
6836
7506
  ${next}`;
7507
+ } else {
7508
+ queue[0] = {
7509
+ ...next,
7510
+ prompt: `[CONTEXT_SUMMARY]: ${compactionSummary}
7511
+
7512
+ ${next.prompt}`
7513
+ };
7514
+ }
6837
7515
  }
6838
7516
  continue;
6839
7517
  }
6840
- const currentPrompt = queue.shift();
7518
+ const currentItem = queue.shift();
6841
7519
  rounds++;
7520
+ const currentPrompt = typeof currentItem === "string" ? currentItem : currentItem.prompt;
7521
+ const currentToolResults = typeof currentItem === "string" ? void 0 : currentItem.toolResults;
6842
7522
  const stream = this.chatWithFallback(
6843
7523
  {
6844
7524
  chat_id: chatId,
@@ -6847,11 +7527,16 @@ ${next}`;
6847
7527
  tools: this.executor.getToolSchemas(),
6848
7528
  project_root: this.projectRoot,
6849
7529
  mode: this.executor.getMode(),
7530
+ // CLI-LOOP-01: carry the concurrently-executed tool_results from the
7531
+ // previous turn as a structured batch. Only set in the capability-
7532
+ // enabled path; absent => backend sees the legacy single-prompt turn.
7533
+ ...currentToolResults && currentToolResults.length > 0 ? { tool_results: currentToolResults } : {},
6850
7534
  ...config.combined ? { project_context: config.combined } : {}
6851
7535
  },
6852
7536
  chatId,
6853
7537
  currentPrompt
6854
7538
  );
7539
+ const pendingToolCalls = [];
6855
7540
  for await (const chunk of stream) {
6856
7541
  if (chunk.content) {
6857
7542
  onContent(chunk.content);
@@ -6868,11 +7553,39 @@ ${next}`;
6868
7553
  });
6869
7554
  }
6870
7555
  if (chunk.tool_use) {
6871
- const result = await this.executor.execute(
6872
- chunk.tool_use.name,
6873
- chunk.tool_use.args,
6874
- this.projectRoot
6875
- );
7556
+ const id = chunk.tool_use.id ?? `call_${rounds}_${toolCallSeq++}`;
7557
+ pendingToolCalls.push({ id, name: chunk.tool_use.name, args: chunk.tool_use.args });
7558
+ }
7559
+ }
7560
+ if (pendingToolCalls.length === 0) {
7561
+ continue;
7562
+ }
7563
+ if (structuredToolResults) {
7564
+ const toolResults = await Promise.all(
7565
+ pendingToolCalls.map(async (call) => {
7566
+ try {
7567
+ const result = await this.executor.execute(call.name, call.args, this.projectRoot);
7568
+ const redactedContent = SafetyGuard.redact(result.content);
7569
+ return {
7570
+ tool_use_id: call.id,
7571
+ name: call.name,
7572
+ content: redactedContent,
7573
+ is_error: !!result.isError
7574
+ };
7575
+ } catch (e) {
7576
+ return {
7577
+ tool_use_id: call.id,
7578
+ name: call.name,
7579
+ content: SafetyGuard.redact(`Tool execution failed: ${e?.message ?? String(e)}`),
7580
+ is_error: true
7581
+ };
7582
+ }
7583
+ })
7584
+ );
7585
+ queue.push({ prompt: "", toolResults });
7586
+ } else {
7587
+ for (const call of pendingToolCalls) {
7588
+ const result = await this.executor.execute(call.name, call.args, this.projectRoot);
6876
7589
  const redactedContent = SafetyGuard.redact(result.content);
6877
7590
  const redactedResult = { ...result, content: redactedContent };
6878
7591
  queue.push(`[TOOL_RESULT]: ${JSON.stringify(redactedResult)}`);
@@ -7119,8 +7832,8 @@ var init_Mutex = __esm({
7119
7832
  */
7120
7833
  acquire() {
7121
7834
  let release3;
7122
- const next = new Promise((resolve20) => {
7123
- release3 = resolve20;
7835
+ const next = new Promise((resolve21) => {
7836
+ release3 = resolve21;
7124
7837
  });
7125
7838
  const entry = this._queue.then(() => release3);
7126
7839
  this._queue = this._queue.then(() => next);
@@ -7158,8 +7871,8 @@ var init_lockfile = __esm({
7158
7871
  });
7159
7872
 
7160
7873
  // ../core/src/TrustStore.ts
7161
- import { join as join16, dirname as dirname4 } from "path";
7162
- import { homedir as homedir9, platform as platform3 } from "os";
7874
+ import { join as join17, dirname as dirname4 } from "path";
7875
+ import { homedir as homedir10, platform as platform3 } from "os";
7163
7876
  import { existsSync as existsSync12, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
7164
7877
  import { readFile as readFile12, writeFile as writeFile5, rename as rename2, chmod } from "fs/promises";
7165
7878
  import { randomBytes as randomBytes9 } from "crypto";
@@ -7170,7 +7883,7 @@ var init_TrustStore = __esm({
7170
7883
  init_esm_shims();
7171
7884
  init_Mutex();
7172
7885
  init_lockfile();
7173
- USER_SETTINGS_PATH = join16(homedir9(), ".msapling", "settings.json");
7886
+ USER_SETTINGS_PATH = join17(homedir10(), ".msapling", "settings.json");
7174
7887
  TrustStore = class {
7175
7888
  /** Current in-memory set of trusted `tool:command` keys. */
7176
7889
  trusted = /* @__PURE__ */ new Set();
@@ -7327,9 +8040,118 @@ var init_TrustStore = __esm({
7327
8040
  }
7328
8041
  });
7329
8042
 
8043
+ // ../core/src/BackupIndex.ts
8044
+ import { join as join18 } from "path";
8045
+ import { homedir as homedir11 } from "os";
8046
+ import {
8047
+ readFileSync as readFileSync3,
8048
+ appendFileSync as appendFileSync2,
8049
+ existsSync as existsSync13,
8050
+ mkdirSync as mkdirSync3,
8051
+ copyFileSync
8052
+ } from "fs";
8053
+ import { randomBytes as randomBytes10 } from "crypto";
8054
+ function _setBackupDirOverride(dir) {
8055
+ _backupDirOverride = dir;
8056
+ }
8057
+ function getBackupDir() {
8058
+ return _backupDirOverride ?? join18(homedir11(), ".msapling", "backups");
8059
+ }
8060
+ function getManifestPath() {
8061
+ return join18(getBackupDir(), "manifest.jsonl");
8062
+ }
8063
+ function ensureBackupDir() {
8064
+ const dir = getBackupDir();
8065
+ if (!existsSync13(dir)) {
8066
+ mkdirSync3(dir, { recursive: true });
8067
+ }
8068
+ }
8069
+ function openTurn(label) {
8070
+ ensureBackupDir();
8071
+ _turnCounter += 1;
8072
+ const turnId = `turn-${Date.now()}-${randomBytes10(4).toString("hex")}`;
8073
+ const entry = {
8074
+ turnId,
8075
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8076
+ label: label ?? `Turn ${_turnCounter}`,
8077
+ backups: []
8078
+ };
8079
+ _openTurns.set(turnId, entry);
8080
+ return turnId;
8081
+ }
8082
+ function recordBackup(turnId, originalPath, backupPath) {
8083
+ const entry = _openTurns.get(turnId);
8084
+ if (!entry) return;
8085
+ entry.backups.push({ originalPath, backupPath });
8086
+ }
8087
+ function closeTurn(turnId) {
8088
+ const entry = _openTurns.get(turnId);
8089
+ if (!entry) return;
8090
+ _openTurns.delete(turnId);
8091
+ entry.closedAt = (/* @__PURE__ */ new Date()).toISOString();
8092
+ try {
8093
+ ensureBackupDir();
8094
+ appendFileSync2(getManifestPath(), JSON.stringify(entry) + "\n", "utf8");
8095
+ } catch {
8096
+ }
8097
+ }
8098
+ function listCheckpoints() {
8099
+ const mp = getManifestPath();
8100
+ if (!existsSync13(mp)) return [];
8101
+ let raw;
8102
+ try {
8103
+ raw = readFileSync3(mp, "utf8");
8104
+ } catch {
8105
+ return [];
8106
+ }
8107
+ const entries = [];
8108
+ for (const line of raw.split("\n")) {
8109
+ const trimmed = line.trim();
8110
+ if (!trimmed) continue;
8111
+ try {
8112
+ entries.push(JSON.parse(trimmed));
8113
+ } catch {
8114
+ }
8115
+ }
8116
+ return entries.reverse().slice(0, MAX_CHECKPOINTS);
8117
+ }
8118
+ function restoreCheckpoint(entry) {
8119
+ const results = [];
8120
+ for (const rec of entry.backups) {
8121
+ if (!existsSync13(rec.backupPath)) {
8122
+ results.push(`Skip (backup file missing): ${rec.originalPath}`);
8123
+ continue;
8124
+ }
8125
+ try {
8126
+ copyFileSync(rec.backupPath, rec.originalPath);
8127
+ results.push(`Restored: ${rec.originalPath}`);
8128
+ } catch (e) {
8129
+ results.push(`Error restoring ${rec.originalPath}: ${e.message}`);
8130
+ }
8131
+ }
8132
+ return results;
8133
+ }
8134
+ function manifestPath() {
8135
+ return getManifestPath();
8136
+ }
8137
+ function backupDir() {
8138
+ return getBackupDir();
8139
+ }
8140
+ var MAX_CHECKPOINTS, _backupDirOverride, _openTurns, _turnCounter;
8141
+ var init_BackupIndex = __esm({
8142
+ "../core/src/BackupIndex.ts"() {
8143
+ "use strict";
8144
+ init_esm_shims();
8145
+ MAX_CHECKPOINTS = 50;
8146
+ _backupDirOverride = null;
8147
+ _openTurns = /* @__PURE__ */ new Map();
8148
+ _turnCounter = 0;
8149
+ }
8150
+ });
8151
+
7330
8152
  // ../core/src/Storage/vault.ts
7331
- import { join as join17 } from "path";
7332
- import { chmodSync, existsSync as existsSync13, renameSync as renameSync2, unlinkSync } from "fs";
8153
+ import { join as join19 } from "path";
8154
+ import { chmodSync, existsSync as existsSync14, renameSync as renameSync2, unlinkSync } from "fs";
7333
8155
  import { writeFile as writeFile6, readFile as readFile13 } from "fs/promises";
7334
8156
  import { createHash as createHash4 } from "crypto";
7335
8157
  async function getKeytar() {
@@ -7343,7 +8165,7 @@ async function getKeytar() {
7343
8165
  return _keytar;
7344
8166
  }
7345
8167
  async function saveToken(baseDir, token) {
7346
- const filePath = join17(baseDir, "vault", "token");
8168
+ const filePath = join19(baseDir, "vault", "token");
7347
8169
  try {
7348
8170
  const kt = await getKeytar();
7349
8171
  if (!kt) throw new Error("keyring not loadable");
@@ -7352,12 +8174,12 @@ async function saveToken(baseDir, token) {
7352
8174
  throw new KeychainUnavailableError(e instanceof Error ? e.message : String(e));
7353
8175
  }
7354
8176
  try {
7355
- if (existsSync13(filePath)) unlinkSync(filePath);
8177
+ if (existsSync14(filePath)) unlinkSync(filePath);
7356
8178
  } catch {
7357
8179
  }
7358
8180
  }
7359
8181
  async function loadToken(baseDir) {
7360
- const filePath = join17(baseDir, "vault", "token");
8182
+ const filePath = join19(baseDir, "vault", "token");
7361
8183
  let kt = null;
7362
8184
  try {
7363
8185
  kt = await getKeytar();
@@ -7368,7 +8190,7 @@ async function loadToken(baseDir) {
7368
8190
  kt = null;
7369
8191
  console.debug(`Keychain unavailable (${e instanceof Error ? e.message : String(e)})`);
7370
8192
  }
7371
- if (existsSync13(filePath)) {
8193
+ if (existsSync14(filePath)) {
7372
8194
  const legacyToken = (await readFile13(filePath, "utf8")).trim();
7373
8195
  if (!legacyToken) return null;
7374
8196
  if (kt) {
@@ -7387,7 +8209,7 @@ async function loadToken(baseDir) {
7387
8209
  return null;
7388
8210
  }
7389
8211
  async function clearToken(baseDir) {
7390
- const filePath = join17(baseDir, "vault", "token");
8212
+ const filePath = join19(baseDir, "vault", "token");
7391
8213
  try {
7392
8214
  const kt = await getKeytar();
7393
8215
  if (kt) await kt.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
@@ -7395,7 +8217,7 @@ async function clearToken(baseDir) {
7395
8217
  console.debug(`Keychain unavailable for deletion (${e instanceof Error ? e.message : String(e)})`);
7396
8218
  }
7397
8219
  try {
7398
- if (existsSync13(filePath)) {
8220
+ if (existsSync14(filePath)) {
7399
8221
  const fs3 = await import("fs/promises");
7400
8222
  await fs3.unlink(filePath);
7401
8223
  }
@@ -7417,8 +8239,8 @@ async function getOrCreateJournalKey() {
7417
8239
  const buf = Buffer.from(existing, "base64");
7418
8240
  if (buf.length === 32) return buf;
7419
8241
  }
7420
- const { randomBytes: randomBytes15 } = await import("crypto");
7421
- const key = randomBytes15(32);
8242
+ const { randomBytes: randomBytes16 } = await import("crypto");
8243
+ const key = randomBytes16(32);
7422
8244
  await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_JOURNAL_ACCOUNT, key.toString("base64"));
7423
8245
  return key;
7424
8246
  } catch (e) {
@@ -7428,8 +8250,8 @@ async function getOrCreateJournalKey() {
7428
8250
  }
7429
8251
  async function writeVaultRef(baseDir, label, value) {
7430
8252
  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);
8253
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8254
+ const refPath = join19(baseDir, "vault", "refs", label);
7433
8255
  const refTmp = `${refPath}.tmp`;
7434
8256
  await writeFile6(objectPath, value, "utf8");
7435
8257
  if (process.platform !== "win32") {
@@ -7440,11 +8262,11 @@ async function writeVaultRef(baseDir, label, value) {
7440
8262
  return hash;
7441
8263
  }
7442
8264
  async function readVaultRef(baseDir, label) {
7443
- const refPath = join17(baseDir, "vault", "refs", label);
7444
- if (!existsSync13(refPath)) return null;
8265
+ const refPath = join19(baseDir, "vault", "refs", label);
8266
+ if (!existsSync14(refPath)) return null;
7445
8267
  const hash = (await readFile13(refPath, "utf8")).trim();
7446
- const objectPath = join17(baseDir, "vault", "objects", hash);
7447
- if (!existsSync13(objectPath)) return null;
8268
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8269
+ if (!existsSync14(objectPath)) return null;
7448
8270
  return readFile13(objectPath, "utf8");
7449
8271
  }
7450
8272
  var _keytar, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, KeychainUnavailableError, KEYCHAIN_JOURNAL_ACCOUNT;
@@ -7468,20 +8290,20 @@ var init_vault = __esm({
7468
8290
  });
7469
8291
 
7470
8292
  // ../core/src/Storage/recipes.ts
7471
- import { join as join18 } from "path";
7472
- import { existsSync as existsSync14, renameSync as renameSync3 } from "fs";
8293
+ import { join as join20 } from "path";
8294
+ import { existsSync as existsSync15, renameSync as renameSync3 } from "fs";
7473
8295
  import { writeFile as writeFile7, readFile as readFile14 } from "fs/promises";
7474
8296
  import { createHash as createHash5 } from "crypto";
7475
8297
  async function registerRecipe(baseDir, name, content) {
7476
8298
  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");
8299
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8300
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7479
8301
  const indexTmp = `${indexPath}.tmp`;
7480
- if (!existsSync14(objectPath)) {
8302
+ if (!existsSync15(objectPath)) {
7481
8303
  await writeFile7(objectPath, content, "utf8");
7482
8304
  }
7483
8305
  let index = {};
7484
- if (existsSync14(indexPath)) {
8306
+ if (existsSync15(indexPath)) {
7485
8307
  try {
7486
8308
  index = JSON.parse(await readFile14(indexPath, "utf8"));
7487
8309
  } catch {
@@ -7494,15 +8316,15 @@ async function registerRecipe(baseDir, name, content) {
7494
8316
  return hash;
7495
8317
  }
7496
8318
  async function resolveRecipe(baseDir, nameOrRef) {
7497
- const indexPath = join18(baseDir, "cache", "recipes", "index.json");
8319
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7498
8320
  const atIdx = nameOrRef.indexOf("@");
7499
8321
  if (atIdx !== -1) {
7500
8322
  const hash2 = nameOrRef.slice(atIdx + 1);
7501
- const objectPath2 = join18(baseDir, "cache", "recipes", "objects", hash2);
7502
- if (!existsSync14(objectPath2)) return null;
8323
+ const objectPath2 = join20(baseDir, "cache", "recipes", "objects", hash2);
8324
+ if (!existsSync15(objectPath2)) return null;
7503
8325
  return { hash: hash2, content: await readFile14(objectPath2, "utf8") };
7504
8326
  }
7505
- if (!existsSync14(indexPath)) return null;
8327
+ if (!existsSync15(indexPath)) return null;
7506
8328
  let index;
7507
8329
  try {
7508
8330
  index = JSON.parse(await readFile14(indexPath, "utf8"));
@@ -7511,8 +8333,8 @@ async function resolveRecipe(baseDir, nameOrRef) {
7511
8333
  }
7512
8334
  const hash = index[nameOrRef];
7513
8335
  if (!hash) return null;
7514
- const objectPath = join18(baseDir, "cache", "recipes", "objects", hash);
7515
- if (!existsSync14(objectPath)) return null;
8336
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8337
+ if (!existsSync15(objectPath)) return null;
7516
8338
  return { hash, content: await readFile14(objectPath, "utf8") };
7517
8339
  }
7518
8340
  var init_recipes = __esm({
@@ -7523,25 +8345,25 @@ var init_recipes = __esm({
7523
8345
  });
7524
8346
 
7525
8347
  // ../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";
8348
+ import { join as join21, basename } from "path";
8349
+ import { existsSync as existsSync16, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "fs";
7528
8350
  import { writeFile as writeFile8, readFile as readFile15, appendFile } from "fs/promises";
7529
- import { createHash as createHash6, randomBytes as randomBytes10 } from "crypto";
8351
+ import { createHash as createHash6, randomBytes as randomBytes11 } from "crypto";
7530
8352
  function hashLine(line) {
7531
8353
  return createHash6("sha256").update(line, "utf8").digest("hex");
7532
8354
  }
7533
8355
  async function appendHistoryEntry(baseDir, historyMutex, content) {
7534
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
8356
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
7535
8357
  return historyMutex.run(async () => {
7536
8358
  let release3 = null;
7537
8359
  try {
7538
- if (!existsSync15(path2)) {
8360
+ if (!existsSync16(path2)) {
7539
8361
  await writeFile8(path2, "", "utf8");
7540
8362
  }
7541
8363
  release3 = await lock(path2, { retries: 5, retryWait: 50 });
7542
8364
  let prevHash = null;
7543
8365
  let seq = 1;
7544
- if (existsSync15(path2)) {
8366
+ if (existsSync16(path2)) {
7545
8367
  const raw = (await readFile15(path2, "utf8")).trimEnd();
7546
8368
  if (raw.length > 0) {
7547
8369
  const lines = raw.split("\n");
@@ -7574,8 +8396,8 @@ async function appendHistoryEntry(baseDir, historyMutex, content) {
7574
8396
  });
7575
8397
  }
7576
8398
  async function loadHistoryEntries(baseDir) {
7577
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
7578
- if (!existsSync15(path2)) return [];
8399
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
8400
+ if (!existsSync16(path2)) return [];
7579
8401
  const raw = await readFile15(path2, "utf8");
7580
8402
  const entries = [];
7581
8403
  for (const line of raw.split("\n")) {
@@ -7607,10 +8429,10 @@ async function verifyHistory(baseDir) {
7607
8429
  return breaks.length === 0 ? { ok: true } : { ok: false, breaks };
7608
8430
  }
7609
8431
  async function saveHistory(baseDir, historyMutex, history) {
7610
- const path2 = join19(baseDir, "history", "shell_history.json");
8432
+ const path2 = join21(baseDir, "history", "shell_history.json");
7611
8433
  let release3;
7612
8434
  try {
7613
- if (!existsSync15(path2)) writeFileSync3(path2, "[]", "utf8");
8435
+ if (!existsSync16(path2)) writeFileSync3(path2, "[]", "utf8");
7614
8436
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7615
8437
  await historyMutex.run(async () => {
7616
8438
  const tmpPath = `${path2}.tmp`;
@@ -7620,7 +8442,7 @@ async function saveHistory(baseDir, historyMutex, history) {
7620
8442
  renameSync4(tmpPath, path2);
7621
8443
  } catch (e) {
7622
8444
  try {
7623
- if (existsSync15(tmpPath)) {
8445
+ if (existsSync16(tmpPath)) {
7624
8446
  const fs3 = await import("fs/promises");
7625
8447
  await fs3.unlink(tmpPath);
7626
8448
  }
@@ -7640,21 +8462,21 @@ async function saveHistory(baseDir, historyMutex, history) {
7640
8462
  }
7641
8463
  }
7642
8464
  async function loadHistory(baseDir, historyMutex) {
7643
- const path2 = join19(baseDir, "history", "shell_history.json");
7644
- if (!existsSync15(path2)) return [];
8465
+ const path2 = join21(baseDir, "history", "shell_history.json");
8466
+ if (!existsSync16(path2)) return [];
7645
8467
  let release3;
7646
8468
  try {
7647
8469
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7648
8470
  return historyMutex.run(async () => {
7649
- if (existsSync15(path2)) {
8471
+ if (existsSync16(path2)) {
7650
8472
  const text = await readFile15(path2, "utf8");
7651
8473
  try {
7652
8474
  return JSON.parse(text);
7653
8475
  } catch (parseErr) {
7654
8476
  const filename = basename(path2) || "shell_history.json";
7655
8477
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7656
- const suffix = randomBytes10(4).toString("hex");
7657
- const corruptBackupPath = join19(
8478
+ const suffix = randomBytes11(4).toString("hex");
8479
+ const corruptBackupPath = join21(
7658
8480
  baseDir,
7659
8481
  "history",
7660
8482
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7689,15 +8511,15 @@ var init_history = __esm({
7689
8511
  });
7690
8512
 
7691
8513
  // ../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";
8514
+ import { join as join22 } from "path";
8515
+ import { existsSync as existsSync17, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
7694
8516
  import { writeFile as writeFile9, readFile as readFile16 } from "fs/promises";
7695
- import { randomBytes as randomBytes11 } from "crypto";
8517
+ import { randomBytes as randomBytes12 } from "crypto";
7696
8518
  async function savePermissions(baseDir, permissionsMutex, permissions) {
7697
- const path2 = join20(baseDir, "vault", "permissions.json");
8519
+ const path2 = join22(baseDir, "vault", "permissions.json");
7698
8520
  let release3;
7699
8521
  try {
7700
- if (!existsSync16(path2)) writeFileSync4(path2, "{}", "utf8");
8522
+ if (!existsSync17(path2)) writeFileSync4(path2, "{}", "utf8");
7701
8523
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7702
8524
  await permissionsMutex.run(async () => {
7703
8525
  const tmpPath = `${path2}.tmp`;
@@ -7707,7 +8529,7 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7707
8529
  renameSync5(tmpPath, path2);
7708
8530
  } catch (e) {
7709
8531
  try {
7710
- if (existsSync16(tmpPath)) {
8532
+ if (existsSync17(tmpPath)) {
7711
8533
  const fs3 = await import("fs/promises");
7712
8534
  await fs3.unlink(tmpPath);
7713
8535
  }
@@ -7727,21 +8549,21 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7727
8549
  }
7728
8550
  }
7729
8551
  async function loadPermissions(baseDir, permissionsMutex) {
7730
- const path2 = join20(baseDir, "vault", "permissions.json");
7731
- if (!existsSync16(path2)) return { trustedCommands: [], trustedPaths: [] };
8552
+ const path2 = join22(baseDir, "vault", "permissions.json");
8553
+ if (!existsSync17(path2)) return { trustedCommands: [], trustedPaths: [] };
7732
8554
  let release3;
7733
8555
  try {
7734
8556
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7735
8557
  return permissionsMutex.run(async () => {
7736
- if (existsSync16(path2)) {
8558
+ if (existsSync17(path2)) {
7737
8559
  const text = await readFile16(path2, "utf8");
7738
8560
  try {
7739
8561
  return JSON.parse(text);
7740
8562
  } catch (parseErr) {
7741
8563
  const filename = path2.split("/").pop() || "permissions.json";
7742
8564
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7743
- const suffix = randomBytes11(4).toString("hex");
7744
- const corruptBackupPath = join20(
8565
+ const suffix = randomBytes12(4).toString("hex");
8566
+ const corruptBackupPath = join22(
7745
8567
  baseDir,
7746
8568
  "vault",
7747
8569
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7776,11 +8598,11 @@ var init_permissions = __esm({
7776
8598
  });
7777
8599
 
7778
8600
  // ../core/src/Storage.ts
7779
- import { join as join21 } from "path";
7780
- import { homedir as homedir10 } from "os";
8601
+ import { join as join23 } from "path";
8602
+ import { homedir as homedir12 } from "os";
7781
8603
  import { chmodSync as chmodSync2 } from "fs";
7782
8604
  import { mkdir as mkdir6, writeFile as writeFile10 } from "fs/promises";
7783
- import { randomBytes as randomBytes12 } from "crypto";
8605
+ import { randomBytes as randomBytes13 } from "crypto";
7784
8606
  var StorageManager;
7785
8607
  var init_Storage = __esm({
7786
8608
  "../core/src/Storage.ts"() {
@@ -7807,7 +8629,7 @@ var init_Storage = __esm({
7807
8629
  */
7808
8630
  _ready;
7809
8631
  constructor() {
7810
- this.baseDir = join21(homedir10(), ".msapling");
8632
+ this.baseDir = join23(homedir12(), ".msapling");
7811
8633
  this._ready = this.ensureDirs();
7812
8634
  }
7813
8635
  async ensureDirs() {
@@ -7824,7 +8646,7 @@ var init_Storage = __esm({
7824
8646
  "cache/recipes/objects"
7825
8647
  ];
7826
8648
  for (const sub of subdirs) {
7827
- await mkdir6(join21(this.baseDir, sub), { recursive: true });
8649
+ await mkdir6(join23(this.baseDir, sub), { recursive: true });
7828
8650
  }
7829
8651
  if (process.platform !== "win32") {
7830
8652
  chmodSync2(this.baseDir, 448);
@@ -7901,8 +8723,8 @@ var init_Storage = __esm({
7901
8723
  await this._ready;
7902
8724
  const filename = filePath.split("/").pop() || "file";
7903
8725
  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`);
8726
+ const suffix = randomBytes13(4).toString("hex");
8727
+ const backupPath = join23(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
7906
8728
  await writeFile10(backupPath, content, "utf8");
7907
8729
  if (process.platform !== "win32") {
7908
8730
  chmodSync2(backupPath, 384);
@@ -7933,15 +8755,15 @@ var init_journalCrypto = __esm({
7933
8755
  });
7934
8756
 
7935
8757
  // ../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";
8758
+ import { homedir as homedir13 } from "os";
8759
+ import { join as join24 } from "path";
8760
+ import { existsSync as existsSync18 } from "fs";
7939
8761
  import * as fs from "fs";
7940
8762
  import { readFile as readFile17 } from "fs/promises";
7941
- import { randomBytes as randomBytes13 } from "crypto";
8763
+ import { randomBytes as randomBytes14 } from "crypto";
7942
8764
  function backupStaleFile(p) {
7943
8765
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7944
- const suffix = randomBytes13(4).toString("hex");
8766
+ const suffix = randomBytes14(4).toString("hex");
7945
8767
  fs.renameSync(p, `${p}.broken-${stamp}-${suffix}`);
7946
8768
  }
7947
8769
  function ensureConfigDir(p) {
@@ -7964,7 +8786,7 @@ function ensureConfigDir(p) {
7964
8786
  }
7965
8787
  async function readJson(path2) {
7966
8788
  try {
7967
- if (!existsSync17(path2)) return null;
8789
+ if (!existsSync18(path2)) return null;
7968
8790
  const text = await readFile17(path2, "utf8");
7969
8791
  if (!text.trim()) return null;
7970
8792
  return JSON.parse(text);
@@ -8019,9 +8841,9 @@ function mergeSettings(base, override) {
8019
8841
  }
8020
8842
  async function loadSettings(cwd = process.cwd(), env = process.env, warn) {
8021
8843
  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");
8844
+ const home = env.HOME || env.USERPROFILE || process.env.HOME || process.env.USERPROFILE || homedir13() || ".";
8845
+ const userPath = join24(home, ".msapling", "settings.json");
8846
+ const projectPath = join24(cwd, ".msapling", "settings.json");
8025
8847
  const [user, project] = await Promise.all([readJson(userPath), readJson(projectPath)]);
8026
8848
  if (user) sources.push(userPath);
8027
8849
  if (project) sources.push(projectPath);
@@ -8074,7 +8896,7 @@ var init_Settings = __esm({
8074
8896
  });
8075
8897
 
8076
8898
  // ../core/src/mcp/client.ts
8077
- import { spawn as spawn8 } from "child_process";
8899
+ import { spawn as spawn9 } from "child_process";
8078
8900
  var PROTOCOL_VERSION, CLIENT_INFO, MCPClientError, MCPClient, MCPRegistry;
8079
8901
  var init_client = __esm({
8080
8902
  "../core/src/mcp/client.ts"() {
@@ -8110,7 +8932,7 @@ var init_client = __esm({
8110
8932
  if (this.proc) return;
8111
8933
  const env = { ...process.env, ...this.config.env ?? {} };
8112
8934
  try {
8113
- this.proc = spawn8(this.config.command, this.config.args ?? [], {
8935
+ this.proc = spawn9(this.config.command, this.config.args ?? [], {
8114
8936
  stdio: ["pipe", "pipe", "pipe"],
8115
8937
  env
8116
8938
  });
@@ -8184,7 +9006,7 @@ var init_client = __esm({
8184
9006
  if (!this.proc) throw new MCPClientError(`MCP server "${this.name}" not started`);
8185
9007
  const id = this.nextId++;
8186
9008
  const frame = { jsonrpc: "2.0", id, method, params };
8187
- return new Promise((resolve20, reject) => {
9009
+ return new Promise((resolve21, reject) => {
8188
9010
  const timer = setTimeout(() => {
8189
9011
  this.pending.delete(id);
8190
9012
  reject(new MCPClientError(`MCP request ${method} timed out after ${timeoutMs}ms`));
@@ -8192,7 +9014,7 @@ var init_client = __esm({
8192
9014
  this.pending.set(id, {
8193
9015
  resolve: (v) => {
8194
9016
  clearTimeout(timer);
8195
- resolve20(v);
9017
+ resolve21(v);
8196
9018
  },
8197
9019
  reject: (e) => {
8198
9020
  clearTimeout(timer);
@@ -8219,7 +9041,7 @@ var init_client = __esm({
8219
9041
  if (!this.proc?.stdout) return;
8220
9042
  const stdout = this.proc.stdout;
8221
9043
  const decoder = new TextDecoder();
8222
- return new Promise((resolve20) => {
9044
+ return new Promise((resolve21) => {
8223
9045
  stdout.on("data", (chunk) => {
8224
9046
  this.buffer += decoder.decode(chunk, { stream: true });
8225
9047
  let idx;
@@ -8230,8 +9052,8 @@ var init_client = __esm({
8230
9052
  this.handleFrame(line);
8231
9053
  }
8232
9054
  });
8233
- stdout.on("end", () => resolve20());
8234
- stdout.on("error", () => resolve20());
9055
+ stdout.on("end", () => resolve21());
9056
+ stdout.on("error", () => resolve21());
8235
9057
  });
8236
9058
  }
8237
9059
  handleFrame(line) {
@@ -8371,6 +9193,8 @@ __export(src_exports2, {
8371
9193
  APPROVAL_GATED: () => APPROVAL_GATED,
8372
9194
  Agent: () => Agent,
8373
9195
  AsyncMutex: () => AsyncMutex,
9196
+ BackgroundShellRegistry: () => BackgroundShellRegistry,
9197
+ BashBackgroundTool: () => BashBackgroundTool,
8374
9198
  BashTool: () => BashTool,
8375
9199
  ContextBudget: () => ContextBudget,
8376
9200
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
@@ -8382,6 +9206,8 @@ __export(src_exports2, {
8382
9206
  GrepSearchTool: () => GrepSearchTool,
8383
9207
  HookRunner: () => HookRunner,
8384
9208
  KeychainUnavailableError: () => KeychainUnavailableError,
9209
+ KillBackgroundShellTool: () => KillBackgroundShellTool,
9210
+ ListBackgroundShellsTool: () => ListBackgroundShellsTool,
8385
9211
  ListDirectoryTool: () => ListDirectoryTool,
8386
9212
  MAX_PLAN_CHARS: () => MAX_PLAN_CHARS,
8387
9213
  MCPClient: () => MCPClient,
@@ -8392,8 +9218,10 @@ __export(src_exports2, {
8392
9218
  NotebookEditTool: () => NotebookEditTool,
8393
9219
  NotebookReadTool: () => NotebookReadTool,
8394
9220
  PatchFileTool: () => PatchFileTool,
9221
+ ReadBackgroundShellTool: () => ReadBackgroundShellTool,
8395
9222
  StorageManager: () => StorageManager,
8396
9223
  SwarmManager: () => SwarmManager,
9224
+ TOOL_NAME_ALIASES: () => TOOL_NAME_ALIASES,
8397
9225
  TodoReadTool: () => TodoReadTool,
8398
9226
  TodoStore: () => TodoStore,
8399
9227
  TodoWriteTool: () => TodoWriteTool,
@@ -8402,16 +9230,31 @@ __export(src_exports2, {
8402
9230
  WebFetchTool: () => WebFetchTool,
8403
9231
  WebSearchTool: () => WebSearchTool,
8404
9232
  WriteFileTool: () => WriteFileTool,
9233
+ _setBackupDirOverride: () => _setBackupDirOverride,
9234
+ backupDir: () => backupDir,
8405
9235
  buildCompactionPrompt: () => buildCompactionPrompt,
8406
9236
  buildHwContext: () => buildHwContext,
9237
+ closeTurn: () => closeTurn,
9238
+ discoverAgentFiles: () => discoverAgentFiles,
8407
9239
  ensureConfigDir: () => ensureConfigDir,
9240
+ findNamedAgent: () => findNamedAgent,
9241
+ formatBackgroundShells: () => formatBackgroundShells,
8408
9242
  formatCell: () => formatCell,
8409
9243
  formatNotebookHeader: () => formatNotebookHeader,
8410
9244
  formatTodos: () => formatTodos,
8411
9245
  getOrCreateJournalKey: () => getOrCreateJournalKey,
8412
9246
  initJournalEncryption: () => initJournalEncryption,
9247
+ listCheckpoints: () => listCheckpoints,
9248
+ loadNamedAgents: () => loadNamedAgents,
8413
9249
  loadProjectConfig: () => loadProjectConfig,
8414
9250
  loadSettings: () => loadSettings,
9251
+ manifestPath: () => manifestPath,
9252
+ normalizeToolName: () => normalizeToolName,
9253
+ openTurn: () => openTurn,
9254
+ parseAgentFile: () => parseAgentFile,
9255
+ parseToolsValue: () => parseToolsValue,
9256
+ recordBackup: () => recordBackup,
9257
+ restoreCheckpoint: () => restoreCheckpoint,
8415
9258
  takeSnapshot: () => takeSnapshot
8416
9259
  });
8417
9260
  var init_src3 = __esm({
@@ -8420,9 +9263,11 @@ var init_src3 = __esm({
8420
9263
  init_esm_shims();
8421
9264
  init_ToolExecutor();
8422
9265
  init_Agent();
9266
+ init_namedAgents();
8423
9267
  init_HardwareMonitor();
8424
9268
  init_TrustStore();
8425
9269
  init_ContextBudget();
9270
+ init_BackupIndex();
8426
9271
  init_Storage();
8427
9272
  init_Storage();
8428
9273
  init_journalCrypto();
@@ -8442,6 +9287,7 @@ var init_src3 = __esm({
8442
9287
  init_WebSearchTool();
8443
9288
  init_PlanModeTools();
8444
9289
  init_BashTool();
9290
+ init_BackgroundShellTool();
8445
9291
  init_NotebookReadTool();
8446
9292
  init_NotebookEditTool();
8447
9293
  init_MultiEditFileTool();
@@ -8466,7 +9312,7 @@ function setRawModeGuarded(stdin, mode) {
8466
9312
  }
8467
9313
  }
8468
9314
  async function promptPassword(prompt4) {
8469
- return new Promise((resolve20) => {
9315
+ return new Promise((resolve21) => {
8470
9316
  const stdin = process.stdin;
8471
9317
  const stdout = process.stdout;
8472
9318
  stdout.write(prompt4);
@@ -8480,7 +9326,7 @@ async function promptPassword(prompt4) {
8480
9326
  setRawModeGuarded(stdin, wasRaw);
8481
9327
  stdin.removeListener("data", onData);
8482
9328
  stdout.write("\n");
8483
- resolve20(value);
9329
+ resolve21(value);
8484
9330
  };
8485
9331
  const onData = (chunk) => {
8486
9332
  try {
@@ -8582,7 +9428,7 @@ async function loginWithGithubDevice(context) {
8582
9428
  const deadline = Date.now() + expires_in * 1e3;
8583
9429
  let githubToken = null;
8584
9430
  while (Date.now() < deadline) {
8585
- await new Promise((resolve20) => setTimeout(resolve20, pollMs));
9431
+ await new Promise((resolve21) => setTimeout(resolve21, pollMs));
8586
9432
  let tokenResp;
8587
9433
  try {
8588
9434
  tokenResp = await fetch(GITHUB_TOKEN_URL, {
@@ -8606,7 +9452,7 @@ async function loginWithGithubDevice(context) {
8606
9452
  if (tokenData.error === "authorization_pending") continue;
8607
9453
  if (tokenData.error === "slow_down") {
8608
9454
  pollMs += 5e3;
8609
- await new Promise((resolve20) => setTimeout(resolve20, 5e3));
9455
+ await new Promise((resolve21) => setTimeout(resolve21, 5e3));
8610
9456
  continue;
8611
9457
  }
8612
9458
  context.addMessage("system", `GitHub auth error: ${tokenData.error_description || tokenData.error}`);
@@ -8951,9 +9797,9 @@ var init_unlock = __esm({
8951
9797
  });
8952
9798
 
8953
9799
  // 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";
9800
+ import { homedir as homedir14 } from "os";
9801
+ import { join as join25 } from "path";
9802
+ import { existsSync as existsSync20 } from "fs";
8957
9803
  import { readFile as readFile18 } from "fs/promises";
8958
9804
  async function checkApiHealth(client) {
8959
9805
  try {
@@ -8979,9 +9825,9 @@ async function checkAuthStatus(client) {
8979
9825
  }
8980
9826
  }
8981
9827
  async function checkSettingsFile() {
8982
- const settingsPath = join23(homedir12(), ".msapling", "settings.json");
9828
+ const settingsPath = join25(homedir14(), ".msapling", "settings.json");
8983
9829
  try {
8984
- if (!existsSync19(settingsPath)) {
9830
+ if (!existsSync20(settingsPath)) {
8985
9831
  return { ok: false, message: `Not found: ${settingsPath}` };
8986
9832
  }
8987
9833
  const text = await readFile18(settingsPath, "utf8");
@@ -9656,7 +10502,7 @@ function setRawModeGuarded2(stdin, mode) {
9656
10502
  }
9657
10503
  }
9658
10504
  async function promptSecret(prompt4) {
9659
- return new Promise((resolve20) => {
10505
+ return new Promise((resolve21) => {
9660
10506
  const stdin = process.stdin;
9661
10507
  const stdout = process.stdout;
9662
10508
  stdout.write(prompt4);
@@ -9669,12 +10515,12 @@ async function promptSecret(prompt4) {
9669
10515
  setRawModeGuarded2(stdin, wasRaw);
9670
10516
  stdin.removeListener("data", onData);
9671
10517
  stdout.write("\n");
9672
- resolve20(secret);
10518
+ resolve21(secret);
9673
10519
  } else if (char === "") {
9674
10520
  setRawModeGuarded2(stdin, wasRaw);
9675
10521
  stdin.removeListener("data", onData);
9676
10522
  stdout.write("\n");
9677
- resolve20("");
10523
+ resolve21("");
9678
10524
  } else if (char === "\x7F" || char === "\b") {
9679
10525
  secret = secret.slice(0, -1);
9680
10526
  } else if (char >= " " && char <= "~") {
@@ -9834,8 +10680,8 @@ var init_memories = __esm({
9834
10680
 
9835
10681
  // src/commands/mdrive.ts
9836
10682
  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";
10683
+ import { existsSync as existsSync21 } from "fs";
10684
+ import { basename as basename2, resolve as resolve15, relative as relative15, isAbsolute as isAbsolute15, sep as sep3 } from "path";
9839
10685
  function formatBytes(b) {
9840
10686
  if (!b) return "0";
9841
10687
  if (b < 1024) return `${b}`;
@@ -9843,10 +10689,10 @@ function formatBytes(b) {
9843
10689
  return `${(b / 1024 / 1024).toFixed(1)}M`;
9844
10690
  }
9845
10691
  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)) {
10692
+ const resolvedRoot = resolve15(root);
10693
+ const resolved = isAbsolute15(targetPath) ? resolve15(targetPath) : resolve15(resolvedRoot, targetPath);
10694
+ const rel = relative15(resolvedRoot, resolved);
10695
+ if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") || isAbsolute15(rel)) {
9850
10696
  return null;
9851
10697
  }
9852
10698
  return resolved;
@@ -9927,7 +10773,7 @@ var init_mdrive = __esm({
9927
10773
  context.addMessage("error", `Refusing to read local file outside the working directory: ${local}`);
9928
10774
  return;
9929
10775
  }
9930
- if (!existsSync20(absLocal)) {
10776
+ if (!existsSync21(absLocal)) {
9931
10777
  context.addMessage("error", `Local file not found: ${absLocal}`);
9932
10778
  return;
9933
10779
  }
@@ -10048,14 +10894,14 @@ var init_clear = __esm({
10048
10894
  });
10049
10895
 
10050
10896
  // 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";
10897
+ import { homedir as homedir15 } from "os";
10898
+ import { join as join26 } from "path";
10899
+ import { existsSync as existsSync22 } from "fs";
10054
10900
  import { readFile as readFile20, writeFile as writeFile12, mkdir as mkdir7 } from "fs/promises";
10055
10901
  async function persistApprovalMode(mode, ttlMs) {
10056
10902
  try {
10057
10903
  let existing = {};
10058
- if (existsSync21(SETTINGS_PATH)) {
10904
+ if (existsSync22(SETTINGS_PATH)) {
10059
10905
  const text = await readFile20(SETTINGS_PATH, "utf8");
10060
10906
  if (text.trim()) {
10061
10907
  existing = JSON.parse(text);
@@ -10067,8 +10913,8 @@ async function persistApprovalMode(mode, ttlMs) {
10067
10913
  ...ttlMs && { ttlMs }
10068
10914
  };
10069
10915
  existing.approvalMode = entry;
10070
- const settingsDir = join24(homedir13(), ".msapling");
10071
- if (!existsSync21(settingsDir)) {
10916
+ const settingsDir = join26(homedir15(), ".msapling");
10917
+ if (!existsSync22(settingsDir)) {
10072
10918
  await mkdir7(settingsDir, { recursive: true });
10073
10919
  }
10074
10920
  await writeFile12(SETTINGS_PATH, JSON.stringify(existing, null, 2), "utf8");
@@ -10080,7 +10926,7 @@ var init_mode = __esm({
10080
10926
  "src/commands/mode.ts"() {
10081
10927
  "use strict";
10082
10928
  init_esm_shims();
10083
- SETTINGS_PATH = join24(homedir13(), ".msapling", "settings.json");
10929
+ SETTINGS_PATH = join26(homedir15(), ".msapling", "settings.json");
10084
10930
  modeCommand = {
10085
10931
  name: "mode",
10086
10932
  args: "[default|plan|acceptEdits|bypassPermissions] [...options]",
@@ -10502,8 +11348,8 @@ var init_compact = __esm({
10502
11348
  });
10503
11349
 
10504
11350
  // src/commands/init.ts
10505
- import { join as join25 } from "path";
10506
- import { existsSync as existsSync22 } from "fs";
11351
+ import { join as join27 } from "path";
11352
+ import { existsSync as existsSync23 } from "fs";
10507
11353
  import { writeFile as writeFile13 } from "fs/promises";
10508
11354
  var initCommand;
10509
11355
  var init_init = __esm({
@@ -10517,8 +11363,8 @@ var init_init = __esm({
10517
11363
  handler: async (args2, context) => {
10518
11364
  try {
10519
11365
  const cwd = process.cwd();
10520
- const path2 = join25(cwd, "MSAPLING.md");
10521
- if (existsSync22(path2)) {
11366
+ const path2 = join27(cwd, "MSAPLING.md");
11367
+ if (existsSync23(path2)) {
10522
11368
  context.addMessage("error", "MSAPLING.md already exists in current directory.");
10523
11369
  return;
10524
11370
  }
@@ -10544,7 +11390,7 @@ var init_init = __esm({
10544
11390
  });
10545
11391
 
10546
11392
  // src/commands/review.ts
10547
- import { existsSync as existsSync23 } from "fs";
11393
+ import { existsSync as existsSync24 } from "fs";
10548
11394
  import { readFile as readFile21 } from "fs/promises";
10549
11395
  var reviewCommand;
10550
11396
  var init_review = __esm({
@@ -10564,7 +11410,7 @@ var init_review = __esm({
10564
11410
  }
10565
11411
  let content = "";
10566
11412
  try {
10567
- if (existsSync23(target)) {
11413
+ if (existsSync24(target)) {
10568
11414
  content = await readFile21(target, "utf8");
10569
11415
  } else {
10570
11416
  content = `Review target: ${target}`;
@@ -10658,15 +11504,15 @@ var init_swarm = __esm({
10658
11504
 
10659
11505
  // src/commands/recipe.ts
10660
11506
  import { parse as parseYaml } from "yaml";
10661
- import { existsSync as existsSync24 } from "fs";
11507
+ import { existsSync as existsSync25 } from "fs";
10662
11508
  import { readFile as readFile22 } from "fs/promises";
10663
- import { join as join26 } from "path";
11509
+ import { join as join28 } from "path";
10664
11510
  function findRecipe(name, cwd) {
10665
11511
  for (const dir of RECIPE_DIRS) {
10666
11512
  for (const suffix of NAME_SUFFIXES) {
10667
11513
  for (const ext of FILE_EXTS) {
10668
- const p = join26(cwd, dir, `${name}${suffix}${ext}`);
10669
- if (existsSync24(p)) return p;
11514
+ const p = join28(cwd, dir, `${name}${suffix}${ext}`);
11515
+ if (existsSync25(p)) return p;
10670
11516
  }
10671
11517
  }
10672
11518
  }
@@ -10779,13 +11625,13 @@ ${rendered}` : rendered;
10779
11625
  });
10780
11626
 
10781
11627
  // src/commands/skill.ts
10782
- import { existsSync as existsSync25, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
11628
+ import { existsSync as existsSync26, readdirSync as readdirSync3, statSync as statSync7 } from "fs";
10783
11629
  import { readFile as readFile23 } from "fs/promises";
10784
- import { join as join27, resolve as resolve15 } from "path";
11630
+ import { join as join29, resolve as resolve16 } from "path";
10785
11631
  function findSkillsRoot(cwd) {
10786
11632
  for (const candidate of SKILLS_DIRS) {
10787
- const full = resolve15(cwd, candidate);
10788
- if (existsSync25(full) && statSync5(full).isDirectory()) return full;
11633
+ const full = resolve16(cwd, candidate);
11634
+ if (existsSync26(full) && statSync7(full).isDirectory()) return full;
10789
11635
  }
10790
11636
  return null;
10791
11637
  }
@@ -10793,28 +11639,28 @@ function listAllSkills(root) {
10793
11639
  const out = [];
10794
11640
  let domains;
10795
11641
  try {
10796
- domains = readdirSync2(root);
11642
+ domains = readdirSync3(root);
10797
11643
  } catch {
10798
11644
  return out;
10799
11645
  }
10800
11646
  for (const domain of domains) {
10801
- const dir = join27(root, domain);
11647
+ const dir = join29(root, domain);
10802
11648
  let s;
10803
11649
  try {
10804
- s = statSync5(dir);
11650
+ s = statSync7(dir);
10805
11651
  } catch {
10806
11652
  continue;
10807
11653
  }
10808
11654
  if (!s.isDirectory()) continue;
10809
11655
  let files;
10810
11656
  try {
10811
- files = readdirSync2(dir);
11657
+ files = readdirSync3(dir);
10812
11658
  } catch {
10813
11659
  continue;
10814
11660
  }
10815
11661
  for (const f of files) {
10816
11662
  if (!f.endsWith(".md")) continue;
10817
- out.push({ domain, name: f.slice(0, -3), path: join27(dir, f) });
11663
+ out.push({ domain, name: f.slice(0, -3), path: join29(dir, f) });
10818
11664
  }
10819
11665
  }
10820
11666
  return out.sort(
@@ -10903,9 +11749,9 @@ ${prompt4}`;
10903
11749
  });
10904
11750
 
10905
11751
  // 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";
11752
+ import { homedir as homedir16 } from "os";
11753
+ import { join as join30 } from "path";
11754
+ import { mkdirSync as mkdirSync5 } from "fs";
10909
11755
  import * as fs2 from "fs";
10910
11756
  function parseArgs(args2) {
10911
11757
  let models = null;
@@ -11024,10 +11870,10 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
11024
11870
  `[HW at run time: CPU ${hwAtEnd.cpuPct}% / RAM ${hwAtEnd.ramPct}% | ${hw.ramGiB} GiB RAM, ${hw.cores} cores]`
11025
11871
  );
11026
11872
  try {
11027
- const dir = join28(homedir14(), ".msapling", "benchmarks");
11028
- mkdirSync4(dir, { recursive: true });
11873
+ const dir = join30(homedir16(), ".msapling", "benchmarks");
11874
+ mkdirSync5(dir, { recursive: true });
11029
11875
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
11030
- const file = join28(dir, `${ts}.json`);
11876
+ const file = join30(dir, `${ts}.json`);
11031
11877
  const run = {
11032
11878
  ts: (/* @__PURE__ */ new Date()).toISOString(),
11033
11879
  rounds,
@@ -11273,22 +12119,22 @@ var init_theme = __esm({
11273
12119
  });
11274
12120
 
11275
12121
  // 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";
12122
+ import { join as join31 } from "path";
12123
+ import { homedir as homedir17 } from "os";
12124
+ import { existsSync as existsSync27 } from "fs";
11279
12125
  import { readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
11280
12126
  async function persistTheme(storage, themeName) {
11281
- const settingsPath = join29(homedir15(), ".msapling", "settings.json");
12127
+ const settingsPath = join31(homedir17(), ".msapling", "settings.json");
11282
12128
  let existing = {};
11283
12129
  try {
11284
- if (existsSync26(settingsPath)) {
12130
+ if (existsSync27(settingsPath)) {
11285
12131
  const text = await readFile24(settingsPath, "utf8");
11286
12132
  if (text.trim()) existing = JSON.parse(text);
11287
12133
  }
11288
12134
  } catch {
11289
12135
  }
11290
12136
  existing["theme"] = themeName;
11291
- ensureConfigDir(join29(homedir15(), ".msapling"));
12137
+ ensureConfigDir(join31(homedir17(), ".msapling"));
11292
12138
  await writeFile14(settingsPath, JSON.stringify(existing, null, 2), "utf8");
11293
12139
  }
11294
12140
  var VALID_THEMES, themeCommand;
@@ -11360,7 +12206,7 @@ var init_version = __esm({
11360
12206
  description: "Show version information for CLI and core packages",
11361
12207
  category: "debug",
11362
12208
  handler: async (_args, context) => {
11363
- const cliVersion = true ? "2.3.6-beta.46" : "(dev)";
12209
+ const cliVersion = true ? "2.3.6-beta.48" : "(dev)";
11364
12210
  const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
11365
12211
  const runtime = process.version;
11366
12212
  context.addMessage("system", "MSapling Version Info");
@@ -11369,7 +12215,7 @@ var init_version = __esm({
11369
12215
  context.addMessage("system", row2("Core (@msapling/core)", coreVersion));
11370
12216
  context.addMessage("system", row2("Runtime (Node/Bun)", runtime));
11371
12217
  try {
11372
- const ts = "2026-06-20T07:22:28.613Z";
12218
+ const ts = "2026-06-20T08:17:10.331Z";
11373
12219
  if (ts && ts !== "__BUILD_TIMESTAMP__") {
11374
12220
  context.addMessage("system", row2("Build Timestamp", ts));
11375
12221
  }
@@ -11382,14 +12228,14 @@ var init_version = __esm({
11382
12228
  });
11383
12229
 
11384
12230
  // src/commands/feedback.ts
11385
- import { join as join30 } from "path";
11386
- import { existsSync as existsSync27 } from "fs";
12231
+ import { join as join32 } from "path";
12232
+ import { existsSync as existsSync28 } from "fs";
11387
12233
  import { readFile as readFile25 } from "fs/promises";
11388
12234
  async function readCliVersion() {
11389
12235
  try {
11390
12236
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
11391
- const pkgPath = join30(baseDir, "..", "..", "package.json");
11392
- if (!existsSync27(pkgPath)) return "unknown";
12237
+ const pkgPath = join32(baseDir, "..", "..", "package.json");
12238
+ if (!existsSync28(pkgPath)) return "unknown";
11393
12239
  const text = await readFile25(pkgPath, "utf8");
11394
12240
  const json = JSON.parse(text);
11395
12241
  return json.version ?? "unknown";
@@ -11430,8 +12276,8 @@ var init_feedback = __esm({
11430
12276
  });
11431
12277
 
11432
12278
  // src/commands/export.ts
11433
- import { homedir as homedir16 } from "os";
11434
- import { join as join31 } from "path";
12279
+ import { homedir as homedir18 } from "os";
12280
+ import { join as join33 } from "path";
11435
12281
  import { writeFile as writeFile15, mkdir as mkdir8 } from "fs/promises";
11436
12282
  function formatTimestamp(date) {
11437
12283
  return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
@@ -11481,10 +12327,10 @@ var init_export = __esm({
11481
12327
  let outputPath;
11482
12328
  let content;
11483
12329
  if (arg === "" || arg === "json") {
11484
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.json`);
12330
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.json`);
11485
12331
  content = buildJsonExport(history);
11486
12332
  } else if (arg === "markdown" || arg === "md") {
11487
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.md`);
12333
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.md`);
11488
12334
  content = buildMarkdownExport(history);
11489
12335
  } else {
11490
12336
  outputPath = arg;
@@ -11496,7 +12342,7 @@ var init_export = __esm({
11496
12342
  }
11497
12343
  }
11498
12344
  try {
11499
- const dir = join31(outputPath, "..");
12345
+ const dir = join33(outputPath, "..");
11500
12346
  await mkdir8(dir, { recursive: true });
11501
12347
  await writeFile15(outputPath, content, "utf8");
11502
12348
  context.addMessage("system", `Exported to: ${outputPath}`);
@@ -11691,16 +12537,16 @@ var init_plan = __esm({
11691
12537
  });
11692
12538
 
11693
12539
  // 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";
12540
+ import { homedir as homedir19 } from "os";
12541
+ import { join as join34 } from "path";
12542
+ import { existsSync as existsSync29 } from "fs";
11697
12543
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
11698
12544
  function getNotesFilePath() {
11699
- return join32(homedir17(), ".msapling", "notes.json");
12545
+ return join34(homedir19(), ".msapling", "notes.json");
11700
12546
  }
11701
12547
  async function readNotes(filePath = getNotesFilePath()) {
11702
12548
  try {
11703
- if (!existsSync28(filePath)) return [];
12549
+ if (!existsSync29(filePath)) return [];
11704
12550
  const raw = await readFile26(filePath, "utf8");
11705
12551
  const parsed = JSON.parse(raw);
11706
12552
  if (!Array.isArray(parsed)) return [];
@@ -11710,7 +12556,7 @@ async function readNotes(filePath = getNotesFilePath()) {
11710
12556
  }
11711
12557
  }
11712
12558
  async function writeNotes(notes, filePath = getNotesFilePath()) {
11713
- const dir = join32(homedir17(), ".msapling");
12559
+ const dir = join34(homedir19(), ".msapling");
11714
12560
  ensureConfigDir(dir);
11715
12561
  await writeFile16(filePath, JSON.stringify(notes, null, 2), "utf8");
11716
12562
  }
@@ -11856,17 +12702,17 @@ var init_todo = __esm({
11856
12702
  });
11857
12703
 
11858
12704
  // 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";
12705
+ import { homedir as homedir20 } from "os";
12706
+ import { join as join35, basename as basename3, extname as extname3 } from "path";
12707
+ import { existsSync as existsSync30, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
11862
12708
  function resolveHome() {
11863
- return process.env.HOME || process.env.USERPROFILE || homedir18();
12709
+ return process.env.HOME || process.env.USERPROFILE || homedir20();
11864
12710
  }
11865
12711
  function stylesDir() {
11866
- return join33(resolveHome(), ".msapling", "output-styles");
12712
+ return join35(resolveHome(), ".msapling", "output-styles");
11867
12713
  }
11868
12714
  function activeFile() {
11869
- return join33(stylesDir(), ".active");
12715
+ return join35(stylesDir(), ".active");
11870
12716
  }
11871
12717
  function parseStyleFile(text) {
11872
12718
  const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
@@ -11887,13 +12733,13 @@ function parseStyleFile(text) {
11887
12733
  }
11888
12734
  function listUserStyles() {
11889
12735
  const dir = stylesDir();
11890
- if (!existsSync29(dir)) return [];
12736
+ if (!existsSync30(dir)) return [];
11891
12737
  const out = [];
11892
- for (const entry of readdirSync3(dir)) {
12738
+ for (const entry of readdirSync4(dir)) {
11893
12739
  if (extname3(entry).toLowerCase() !== ".md") continue;
11894
- const full = join33(dir, entry);
12740
+ const full = join35(dir, entry);
11895
12741
  try {
11896
- const text = readFileSync2(full, "utf8");
12742
+ const text = readFileSync4(full, "utf8");
11897
12743
  const { description, body } = parseStyleFile(text);
11898
12744
  out.push({
11899
12745
  name: basename3(entry, ".md"),
@@ -11919,15 +12765,15 @@ function findStyle(name) {
11919
12765
  function getActiveStyleName() {
11920
12766
  try {
11921
12767
  const f = activeFile();
11922
- if (!existsSync29(f)) return "default";
11923
- return readFileSync2(f, "utf8").trim() || "default";
12768
+ if (!existsSync30(f)) return "default";
12769
+ return readFileSync4(f, "utf8").trim() || "default";
11924
12770
  } catch {
11925
12771
  return "default";
11926
12772
  }
11927
12773
  }
11928
12774
  function setActiveStyleName(name) {
11929
12775
  const dir = stylesDir();
11930
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
12776
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
11931
12777
  writeFileSync5(activeFile(), `${name}
11932
12778
  `, "utf8");
11933
12779
  }
@@ -11940,8 +12786,8 @@ function createUserStyle(name, description, body) {
11940
12786
  throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
11941
12787
  }
11942
12788
  const dir = stylesDir();
11943
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
11944
- const target = join33(dir, `${name}.md`);
12789
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
12790
+ const target = join35(dir, `${name}.md`);
11945
12791
  const frontmatter = `---
11946
12792
  description: ${description.replace(/\n/g, " ")}
11947
12793
  ---
@@ -13105,6 +13951,259 @@ Note: server may return a [server notice] caveat for v1-scaffold features.`
13105
13951
  }
13106
13952
  });
13107
13953
 
13954
+ // src/commands/rewind.ts
13955
+ var rewindCommand;
13956
+ var init_rewind = __esm({
13957
+ "src/commands/rewind.ts"() {
13958
+ "use strict";
13959
+ init_esm_shims();
13960
+ init_src3();
13961
+ rewindCommand = {
13962
+ name: "rewind",
13963
+ aliases: ["checkpoint", "restore"],
13964
+ args: "[n] [--confirm]",
13965
+ description: "List file-edit checkpoints or restore files to a prior snapshot. Usage: /rewind (list) \xB7 /rewind <n> (preview) \xB7 /rewind <n> --confirm (restore).",
13966
+ category: "chat",
13967
+ handler(args2, context) {
13968
+ const checkpoints = listCheckpoints();
13969
+ if (args2.length === 0) {
13970
+ if (checkpoints.length === 0) {
13971
+ context.addMessage(
13972
+ "system",
13973
+ "No checkpoints found. File edits made by the agent will be listed here."
13974
+ );
13975
+ return;
13976
+ }
13977
+ const lines = [
13978
+ `${checkpoints.length} checkpoint(s) \u2014 most recent first:`,
13979
+ ""
13980
+ ];
13981
+ checkpoints.forEach((cp, idx) => {
13982
+ const num = idx + 1;
13983
+ const fileCount = cp.backups.length;
13984
+ const when = new Date(cp.timestamp).toLocaleString();
13985
+ lines.push(
13986
+ ` ${num}. [${when}] ${cp.label} (${fileCount} file${fileCount !== 1 ? "s" : ""})`
13987
+ );
13988
+ });
13989
+ lines.push("");
13990
+ lines.push("To preview a restore: /rewind <n>");
13991
+ lines.push("To restore: /rewind <n> --confirm");
13992
+ context.addMessage("system", lines.join("\n"));
13993
+ return;
13994
+ }
13995
+ const positional = args2.filter((a) => !a.startsWith("-"));
13996
+ const hasConfirm = args2.includes("--confirm") || args2.includes("-y") || args2.includes("--yes");
13997
+ if (positional.length === 0) {
13998
+ context.addMessage("system", "Usage: /rewind <n> [--confirm]");
13999
+ return;
14000
+ }
14001
+ const rawN = positional[0];
14002
+ const n = parseInt(rawN, 10);
14003
+ if (isNaN(n) || n < 1) {
14004
+ context.addMessage(
14005
+ "error",
14006
+ `Invalid checkpoint number: "${rawN}". Run /rewind to list available checkpoints.`
14007
+ );
14008
+ return;
14009
+ }
14010
+ if (checkpoints.length === 0) {
14011
+ context.addMessage("system", "No checkpoints available to restore.");
14012
+ return;
14013
+ }
14014
+ if (n > checkpoints.length) {
14015
+ context.addMessage(
14016
+ "error",
14017
+ `Checkpoint #${n} does not exist. ${checkpoints.length} checkpoint(s) available. Run /rewind to list them.`
14018
+ );
14019
+ return;
14020
+ }
14021
+ const target = checkpoints[n - 1];
14022
+ if (!hasConfirm) {
14023
+ const when = new Date(target.timestamp).toLocaleString();
14024
+ const lines = [
14025
+ `Checkpoint #${n}: ${target.label} [${when}]`,
14026
+ ""
14027
+ ];
14028
+ if (target.backups.length === 0) {
14029
+ lines.push(" (no file edits recorded in this checkpoint)");
14030
+ } else {
14031
+ lines.push("Files that will be reverted:");
14032
+ for (const rec of target.backups) {
14033
+ lines.push(` - ${rec.originalPath}`);
14034
+ }
14035
+ }
14036
+ lines.push("");
14037
+ lines.push(`To restore these files, run: /rewind ${n} --confirm`);
14038
+ lines.push(
14039
+ "Note: only file contents are restored (chat history is NOT truncated)."
14040
+ );
14041
+ context.addMessage("system", lines.join("\n"));
14042
+ return;
14043
+ }
14044
+ if (target.backups.length === 0) {
14045
+ context.addMessage(
14046
+ "system",
14047
+ `Checkpoint #${n} has no file edits \u2014 nothing to restore.`
14048
+ );
14049
+ return;
14050
+ }
14051
+ const results = restoreCheckpoint(target);
14052
+ const restored = results.filter((r) => r.startsWith("Restored:"));
14053
+ const skipped = results.filter((r) => r.startsWith("Skip"));
14054
+ const errors = results.filter((r) => r.startsWith("Error"));
14055
+ const summary = [
14056
+ `Rewind to checkpoint #${n} complete.`,
14057
+ "",
14058
+ ...results
14059
+ ];
14060
+ if (errors.length > 0) {
14061
+ summary.push("");
14062
+ summary.push(
14063
+ `${errors.length} error(s) occurred. Check that the backup files in ~/.msapling/backups/ are accessible.`
14064
+ );
14065
+ }
14066
+ if (restored.length > 0 && errors.length === 0) {
14067
+ summary.push("");
14068
+ summary.push(
14069
+ `${restored.length} file(s) restored successfully. Note: chat history was NOT truncated (follow-up: REWIND-FOLLOW-UP-01).`
14070
+ );
14071
+ }
14072
+ context.addMessage("system", summary.join("\n"));
14073
+ }
14074
+ };
14075
+ }
14076
+ });
14077
+
14078
+ // src/commands/bashes.ts
14079
+ var bashesCommand;
14080
+ var init_bashes = __esm({
14081
+ "src/commands/bashes.ts"() {
14082
+ "use strict";
14083
+ init_esm_shims();
14084
+ init_src3();
14085
+ bashesCommand = {
14086
+ name: "bashes",
14087
+ aliases: ["bg", "background"],
14088
+ args: "[read <id> [n] | kill <id|all> | <id>]",
14089
+ description: "List, inspect, or kill background shells",
14090
+ category: "debug",
14091
+ handler(args2, context) {
14092
+ const sub = (args2[0] ?? "").toLowerCase();
14093
+ if (sub === "kill") {
14094
+ const target = args2[1] ?? "";
14095
+ if (!target) {
14096
+ context.addMessage("error", "Usage: /bashes kill <id|all>");
14097
+ return;
14098
+ }
14099
+ if (target.toLowerCase() === "all") {
14100
+ const running = BackgroundShellRegistry.list().filter((s) => s.status === "running");
14101
+ BackgroundShellRegistry.killAll();
14102
+ context.addMessage("system", `Killed ${running.length} running background shell(s).`);
14103
+ return;
14104
+ }
14105
+ const result = BackgroundShellRegistry.kill(target);
14106
+ if (result === "killed") context.addMessage("system", `Killed background shell ${target}.`);
14107
+ else if (result === "already")
14108
+ context.addMessage("system", `Background shell ${target} had already finished.`);
14109
+ else context.addMessage("error", `No background shell with id "${target}".`);
14110
+ return;
14111
+ }
14112
+ const explicitRead = sub === "read";
14113
+ const id = explicitRead ? args2[1] : args2[0];
14114
+ if (id) {
14115
+ const lineArg = explicitRead ? args2[2] : args2[1];
14116
+ const lines = lineArg && /^\d+$/.test(lineArg) ? parseInt(lineArg, 10) : void 0;
14117
+ const read = BackgroundShellRegistry.readOutput(id, lines);
14118
+ const info = BackgroundShellRegistry.get(id);
14119
+ if (!read || !info) {
14120
+ context.addMessage("error", `No background shell with id "${id}".`);
14121
+ return;
14122
+ }
14123
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
14124
+ `;
14125
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
14126
+ const trunc = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
14127
+ context.addMessage("system", header + body + trunc);
14128
+ return;
14129
+ }
14130
+ context.addMessage("system", formatBackgroundShells(BackgroundShellRegistry.list()));
14131
+ }
14132
+ };
14133
+ }
14134
+ });
14135
+
14136
+ // src/commands/agents.ts
14137
+ import { homedir as homedir21 } from "os";
14138
+ function resolveHome2() {
14139
+ return process.env.HOME || process.env.USERPROFILE || homedir21();
14140
+ }
14141
+ function describeTools(agent) {
14142
+ if (agent.tools === void 0) return "all (inherits full tool set)";
14143
+ if (agent.tools.length === 0) return "none (read/reason only)";
14144
+ return agent.tools.join(", ");
14145
+ }
14146
+ var agentsCommand;
14147
+ var init_agents = __esm({
14148
+ "src/commands/agents.ts"() {
14149
+ "use strict";
14150
+ init_esm_shims();
14151
+ init_src3();
14152
+ agentsCommand = {
14153
+ name: "agents",
14154
+ aliases: ["agent"],
14155
+ args: "[name]",
14156
+ description: "List named subagents (.msapling/agents/*.md), or show one. Named agents have their own system prompt, model, and tool allowlist.",
14157
+ category: "swarm",
14158
+ handler: (args2, context) => {
14159
+ const cwd = process.cwd();
14160
+ const home = resolveHome2();
14161
+ if (args2.length >= 1 && args2[0] !== "list" && args2[0] !== "ls") {
14162
+ const ref = args2[0];
14163
+ const agent = findNamedAgent(ref, cwd, home);
14164
+ if (!agent) {
14165
+ context.addMessage(
14166
+ "error",
14167
+ `No named agent "${ref}". Run /agents (no args) to list available agents.`
14168
+ );
14169
+ return;
14170
+ }
14171
+ context.addMessage("system", `Agent: ${agent.name} [${agent.scope}]`);
14172
+ context.addMessage("system", ` description: ${agent.description}`);
14173
+ context.addMessage("system", ` model: ${agent.model ?? "(inherit session model)"}`);
14174
+ context.addMessage("system", ` tools: ${describeTools(agent)}`);
14175
+ context.addMessage("system", ` source: ${agent.path}`);
14176
+ context.addMessage("system", " \u2500\u2500\u2500 system prompt \u2500\u2500\u2500");
14177
+ context.addMessage("system", agent.systemPrompt || "(empty)");
14178
+ context.addMessage(
14179
+ "system",
14180
+ `Run it: the model can call dispatch_agent with agent="${agent.name}".`
14181
+ );
14182
+ return;
14183
+ }
14184
+ const agents = loadNamedAgents(cwd, home);
14185
+ if (agents.length === 0) {
14186
+ context.addMessage(
14187
+ "system",
14188
+ "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."
14189
+ );
14190
+ return;
14191
+ }
14192
+ context.addMessage("system", `Named subagents (${agents.length}):`);
14193
+ for (const a of agents) {
14194
+ const model = a.model ? ` model=${a.model}` : "";
14195
+ const toolCount = a.tools === void 0 ? "tools=all" : `tools=${a.tools.length}`;
14196
+ context.addMessage(
14197
+ "system",
14198
+ ` ${a.name.padEnd(20)} [${a.scope}] ${toolCount}${model} \u2014 ${a.description}`
14199
+ );
14200
+ }
14201
+ context.addMessage("system", "Run /agents <name> to see an agent in full.");
14202
+ }
14203
+ };
14204
+ }
14205
+ });
14206
+
13108
14207
  // src/commands/index.ts
13109
14208
  var commands_exports = {};
13110
14209
  __export(commands_exports, {
@@ -13176,6 +14275,9 @@ var init_commands = __esm({
13176
14275
  init_diag();
13177
14276
  init_parallel();
13178
14277
  init_remoteAgent();
14278
+ init_rewind();
14279
+ init_bashes();
14280
+ init_agents();
13179
14281
  commands = [
13180
14282
  loginCommand,
13181
14283
  logoutCommand,
@@ -13239,7 +14341,10 @@ var init_commands = __esm({
13239
14341
  syncCommand,
13240
14342
  diagCommand,
13241
14343
  parallelCommand,
13242
- remoteAgentCommand
14344
+ remoteAgentCommand,
14345
+ rewindCommand,
14346
+ bashesCommand,
14347
+ agentsCommand
13243
14348
  ];
13244
14349
  }
13245
14350
  });
@@ -13309,15 +14414,15 @@ var exec_exports = {};
13309
14414
  __export(exec_exports, {
13310
14415
  runExec: () => runExec
13311
14416
  });
13312
- import { existsSync as existsSync32 } from "fs";
14417
+ import { existsSync as existsSync33 } from "fs";
13313
14418
  import { readFile as readFile29 } from "fs/promises";
13314
- import { homedir as homedir20 } from "os";
13315
- import { join as join35 } from "path";
14419
+ import { homedir as homedir23 } from "os";
14420
+ import { join as join37 } from "path";
13316
14421
  async function loadPersistedSettings() {
13317
14422
  const out = { mode: "default", theme: null };
13318
14423
  try {
13319
- const p = join35(homedir20(), ".msapling", "settings.json");
13320
- if (!existsSync32(p)) return out;
14424
+ const p = join37(homedir23(), ".msapling", "settings.json");
14425
+ if (!existsSync33(p)) return out;
13321
14426
  const raw = JSON.parse(await readFile29(p, "utf8"));
13322
14427
  const parsed = parseApprovalMode(raw, Date.now());
13323
14428
  if (parsed.kind === "ok") out.mode = parsed.mode;
@@ -13553,7 +14658,7 @@ var init_format = __esm({
13553
14658
  });
13554
14659
 
13555
14660
  // src/commands/billing/open-browser.ts
13556
- import { spawn as spawn10 } from "child_process";
14661
+ import { spawn as spawn11 } from "child_process";
13557
14662
  async function openBrowser(url) {
13558
14663
  const platform5 = process.platform;
13559
14664
  let cmd;
@@ -13568,13 +14673,13 @@ async function openBrowser(url) {
13568
14673
  cmd = "xdg-open";
13569
14674
  args2 = [url];
13570
14675
  }
13571
- return new Promise((resolve20) => {
14676
+ return new Promise((resolve21) => {
13572
14677
  try {
13573
- const child = spawn10(cmd, args2, { stdio: "ignore", detached: true });
14678
+ const child = spawn11(cmd, args2, { stdio: "ignore", detached: true });
13574
14679
  child.unref();
13575
14680
  } catch {
13576
14681
  }
13577
- resolve20();
14682
+ resolve21();
13578
14683
  });
13579
14684
  }
13580
14685
  var init_open_browser = __esm({
@@ -13664,7 +14769,7 @@ var init_checkout = __esm({
13664
14769
  // src/commands/billing/sub.ts
13665
14770
  import * as readline from "readline";
13666
14771
  function prompt(rl, question) {
13667
- return new Promise((resolve20) => rl.question(question, resolve20));
14772
+ return new Promise((resolve21) => rl.question(question, resolve21));
13668
14773
  }
13669
14774
  async function runSub(argv) {
13670
14775
  const subCmd = argv[0] ?? "";
@@ -13790,11 +14895,11 @@ var init_sub = __esm({
13790
14895
  // src/commands/billing/topup.ts
13791
14896
  import * as readline2 from "readline";
13792
14897
  function prompt2(rl, question) {
13793
- return new Promise((resolve20) => rl.question(question, resolve20));
14898
+ return new Promise((resolve21) => rl.question(question, resolve21));
13794
14899
  }
13795
14900
  function promptDefault(rl, question, defaultVal) {
13796
14901
  return new Promise(
13797
- (resolve20) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve20(ans.trim() || defaultVal))
14902
+ (resolve21) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve21(ans.trim() || defaultVal))
13798
14903
  );
13799
14904
  }
13800
14905
  async function runTopup(argv) {
@@ -13929,7 +15034,7 @@ var init_redeem = __esm({
13929
15034
  // src/commands/billing/gift.ts
13930
15035
  import * as readline3 from "readline";
13931
15036
  function prompt3(rl, question) {
13932
- return new Promise((resolve20) => rl.question(question, resolve20));
15037
+ return new Promise((resolve21) => rl.question(question, resolve21));
13933
15038
  }
13934
15039
  async function runGift(argv) {
13935
15040
  const subCmd = argv[0] ?? "";
@@ -14175,11 +15280,11 @@ var doctor_exports = {};
14175
15280
  __export(doctor_exports, {
14176
15281
  runDoctor: () => runDoctor
14177
15282
  });
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";
15283
+ import { homedir as homedir24, platform as platform4, tmpdir } from "os";
15284
+ import { join as join38 } from "path";
15285
+ import { existsSync as existsSync34, statSync as statSync8 } from "fs";
14181
15286
  import { readdir as readdir3, mkdir as mkdir9, rm as rm3 } from "fs/promises";
14182
- import { exec } from "child_process";
15287
+ import { exec as exec2 } from "child_process";
14183
15288
  import { promisify } from "util";
14184
15289
  async function checkNodeVersion() {
14185
15290
  const version = process.version;
@@ -14200,8 +15305,8 @@ async function checkNodeVersion() {
14200
15305
  };
14201
15306
  }
14202
15307
  async function checkConfigDir() {
14203
- const configDir = join36(homedir21(), ".msapling");
14204
- if (!existsSync33(configDir)) {
15308
+ const configDir = join38(homedir24(), ".msapling");
15309
+ if (!existsSync34(configDir)) {
14205
15310
  return {
14206
15311
  name: "Config directory",
14207
15312
  status: "WARN",
@@ -14209,7 +15314,7 @@ async function checkConfigDir() {
14209
15314
  remediation: `mkdir -p "${configDir}" && chmod 700 "${configDir}"`
14210
15315
  };
14211
15316
  }
14212
- const stats = statSync6(configDir);
15317
+ const stats = statSync8(configDir);
14213
15318
  if (!stats.isDirectory()) {
14214
15319
  return {
14215
15320
  name: "Config directory",
@@ -14270,7 +15375,7 @@ async function checkPathConflicts() {
14270
15375
  const timedOutDirs = [];
14271
15376
  const DIR_TIMEOUT_MS = 1500;
14272
15377
  for (const dir of paths) {
14273
- if (!dir || !existsSync33(dir)) continue;
15378
+ if (!dir || !existsSync34(dir)) continue;
14274
15379
  try {
14275
15380
  const files = await Promise.race([
14276
15381
  readdir3(dir),
@@ -14283,7 +15388,7 @@ async function checkPathConflicts() {
14283
15388
  ]);
14284
15389
  for (const file of files) {
14285
15390
  if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
14286
- const fullPath = join36(dir, file);
15391
+ const fullPath = join38(dir, file);
14287
15392
  conflicts.push(fullPath);
14288
15393
  }
14289
15394
  }
@@ -14401,9 +15506,9 @@ async function checkTokenValidity() {
14401
15506
  }
14402
15507
  async function checkOsSpecific() {
14403
15508
  if (platform4() === "win32") {
14404
- const testDir = join36(tmpdir(), `msapling-longpath-test-${Date.now()}`);
15509
+ const testDir = join38(tmpdir(), `msapling-longpath-test-${Date.now()}`);
14405
15510
  const longDirName = "A".repeat(260);
14406
- const testPath = join36(testDir, longDirName);
15511
+ const testPath = join38(testDir, longDirName);
14407
15512
  try {
14408
15513
  await mkdir9(testDir, { recursive: true });
14409
15514
  try {
@@ -14557,7 +15662,7 @@ var init_doctor2 = __esm({
14557
15662
  "use strict";
14558
15663
  init_esm_shims();
14559
15664
  init_doctorRedact();
14560
- execAsync = promisify(exec);
15665
+ execAsync = promisify(exec2);
14561
15666
  }
14562
15667
  });
14563
15668
 
@@ -14603,21 +15708,21 @@ var init_registry_merger = __esm({
14603
15708
  });
14604
15709
 
14605
15710
  // ../core/src/mcp/local_tools.ts
14606
- import { spawn as spawn11 } from "child_process";
15711
+ import { spawn as spawn12 } from "child_process";
14607
15712
  import { readdir as readdir4, stat as stat4, realpath as realpath3 } from "fs/promises";
14608
- import { resolve as resolve17 } from "path";
15713
+ import { resolve as resolve18 } from "path";
14609
15714
  function asResult(text, isError = false) {
14610
15715
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
14611
15716
  }
14612
15717
  async function runCommand(command, cwd) {
14613
- return new Promise((resolve20) => {
15718
+ return new Promise((resolve21) => {
14614
15719
  let p;
14615
15720
  const timeout = setTimeout(() => {
14616
15721
  if (p) p.kill();
14617
- resolve20({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
15722
+ resolve21({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
14618
15723
  }, 3e4);
14619
15724
  try {
14620
- p = spawn11("sh", ["-c", command], {
15725
+ p = spawn12("sh", ["-c", command], {
14621
15726
  cwd: cwd || process.cwd(),
14622
15727
  stdio: ["ignore", "pipe", "pipe"],
14623
15728
  timeout: 3e4
@@ -14632,15 +15737,15 @@ async function runCommand(command, cwd) {
14632
15737
  });
14633
15738
  p.on("error", (e) => {
14634
15739
  clearTimeout(timeout);
14635
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
15740
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
14636
15741
  });
14637
15742
  p.on("exit", (code) => {
14638
15743
  clearTimeout(timeout);
14639
- resolve20({ stdout, stderr, exit_code: code });
15744
+ resolve21({ stdout, stderr, exit_code: code });
14640
15745
  });
14641
15746
  } catch (e) {
14642
15747
  clearTimeout(timeout);
14643
- resolve20({
15748
+ resolve21({
14644
15749
  stdout: "",
14645
15750
  stderr: e?.message ?? "Failed to spawn process",
14646
15751
  exit_code: -1
@@ -14707,7 +15812,7 @@ async function callLocalTool(name, args2, projectRoot) {
14707
15812
  const command = String(args2.command ?? "");
14708
15813
  let cwd = projectRoot;
14709
15814
  if (args2.cwd) {
14710
- cwd = resolve17(projectRoot, String(args2.cwd));
15815
+ cwd = resolve18(projectRoot, String(args2.cwd));
14711
15816
  try {
14712
15817
  const resolvedCwd = await realpath3(cwd);
14713
15818
  const resolvedRoot = await realpath3(projectRoot);
@@ -14744,7 +15849,7 @@ ${res.stderr}`
14744
15849
  return asResult("path is required", true);
14745
15850
  }
14746
15851
  try {
14747
- const resolvedPath = await realpath3(resolve17(projectRoot, pathArg));
15852
+ const resolvedPath = await realpath3(resolve18(projectRoot, pathArg));
14748
15853
  const resolvedRoot = await realpath3(projectRoot);
14749
15854
  if (!resolvedPath.startsWith(resolvedRoot)) {
14750
15855
  return asResult("Error: path attempts to escape project root", true);
@@ -14763,7 +15868,7 @@ ${res.stderr}`
14763
15868
  }
14764
15869
  case "local_glob": {
14765
15870
  const pattern = String(args2.pattern ?? "");
14766
- let cwd = args2.cwd ? resolve17(projectRoot, String(args2.cwd)) : projectRoot;
15871
+ let cwd = args2.cwd ? resolve18(projectRoot, String(args2.cwd)) : projectRoot;
14767
15872
  if (!pattern) {
14768
15873
  return asResult("pattern is required", true);
14769
15874
  }
@@ -14792,7 +15897,7 @@ ${res.stderr}`
14792
15897
  }
14793
15898
  if (path2) {
14794
15899
  try {
14795
- const resolvedPath = await realpath3(resolve17(projectRoot, path2));
15900
+ const resolvedPath = await realpath3(resolve18(projectRoot, path2));
14796
15901
  const resolvedRoot = await realpath3(projectRoot);
14797
15902
  if (!resolvedPath.startsWith(resolvedRoot)) {
14798
15903
  return asResult("Error: path attempts to escape project root", true);
@@ -14812,7 +15917,7 @@ ${res.stderr}`
14812
15917
  return asResult("cwd is required", true);
14813
15918
  }
14814
15919
  try {
14815
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15920
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14816
15921
  const resolvedRoot = await realpath3(projectRoot);
14817
15922
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14818
15923
  return asResult("Error: cwd attempts to escape project root", true);
@@ -14838,7 +15943,7 @@ ${status2.porcelain || "(clean)"}`
14838
15943
  return asResult("cwd is required", true);
14839
15944
  }
14840
15945
  try {
14841
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15946
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14842
15947
  const resolvedRoot = await realpath3(projectRoot);
14843
15948
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14844
15949
  return asResult("Error: cwd attempts to escape project root", true);
@@ -15032,13 +16137,13 @@ var init_base = __esm({
15032
16137
  editLength++;
15033
16138
  };
15034
16139
  if (callback) {
15035
- (function exec2() {
16140
+ (function exec3() {
15036
16141
  setTimeout(function() {
15037
16142
  if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
15038
16143
  return callback(void 0);
15039
16144
  }
15040
16145
  if (!execEditLength()) {
15041
- exec2();
16146
+ exec3();
15042
16147
  }
15043
16148
  }, 0);
15044
16149
  })();
@@ -16080,8 +17185,8 @@ var init_libesm = __esm({
16080
17185
  });
16081
17186
 
16082
17187
  // ../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";
17188
+ import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync9 } from "fs";
17189
+ import { join as join39, relative as relative16 } from "path";
16085
17190
  function buildFileTree(root, maxFiles) {
16086
17191
  const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "build", "dist", ".venv", "venv", ".next", "__pycache__", ".dart_tool", ".bun", "target"]);
16087
17192
  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 +17196,17 @@ function buildFileTree(root, maxFiles) {
16091
17196
  const dir = queue.shift();
16092
17197
  let entries;
16093
17198
  try {
16094
- entries = readdirSync4(dir);
17199
+ entries = readdirSync5(dir);
16095
17200
  } catch {
16096
17201
  continue;
16097
17202
  }
16098
17203
  for (const name of entries) {
16099
17204
  if (out.length >= maxFiles) break;
16100
17205
  if (SKIP_DIRS2.has(name)) continue;
16101
- const full = join37(dir, name);
17206
+ const full = join39(dir, name);
16102
17207
  let s;
16103
17208
  try {
16104
- s = statSync7(full);
17209
+ s = statSync9(full);
16105
17210
  } catch {
16106
17211
  continue;
16107
17212
  }
@@ -16122,12 +17227,12 @@ function readFilesAsContext(root, files, maxKB) {
16122
17227
  for (const f of files) {
16123
17228
  let body;
16124
17229
  try {
16125
- body = readFileSync3(f, "utf8");
17230
+ body = readFileSync5(f, "utf8");
16126
17231
  } catch {
16127
17232
  continue;
16128
17233
  }
16129
17234
  if (body.length > cap) body = body.slice(0, cap) + "\n[...truncated]";
16130
- const rel = relative15(root, f).replace(/\\/g, "/");
17235
+ const rel = relative16(root, f).replace(/\\/g, "/");
16131
17236
  parts.push(`### ${rel}
16132
17237
 
16133
17238
  \`\`\`
@@ -16329,7 +17434,7 @@ var init_types = __esm({
16329
17434
  });
16330
17435
 
16331
17436
  // ../core/src/mcp/handlers.ts
16332
- import { resolve as resolve19 } from "path";
17437
+ import { resolve as resolve20 } from "path";
16333
17438
  async function callTool(name, args2, client, getIsProCached) {
16334
17439
  switch (name) {
16335
17440
  case "msapling_chat": {
@@ -16427,7 +17532,7 @@ ${r.response ?? ""}`;
16427
17532
  return asResult2(JSON.stringify(result));
16428
17533
  }
16429
17534
  case "msapling_project_context": {
16430
- const root = resolve19(String(args2.path ?? "."));
17535
+ const root = resolve20(String(args2.path ?? "."));
16431
17536
  const maxFiles = Number.isFinite(args2.max_files) ? Number(args2.max_files) : 30;
16432
17537
  const maxKB = Number.isFinite(args2.max_file_size_kb) ? Number(args2.max_file_size_kb) : 50;
16433
17538
  const files = buildFileTree(root, maxFiles);
@@ -16682,13 +17787,13 @@ var init_server = __esm({
16682
17787
  if (inflight === 0) {
16683
17788
  return [];
16684
17789
  }
16685
- const forcedResponses = await new Promise((resolve20) => {
16686
- this._drainResolve = () => resolve20([]);
17790
+ const forcedResponses = await new Promise((resolve21) => {
17791
+ this._drainResolve = () => resolve21([]);
16687
17792
  setTimeout(() => {
16688
17793
  this._drainResolve = null;
16689
17794
  const remaining = Array.from(this._inflightCalls.values());
16690
17795
  if (remaining.length === 0) {
16691
- resolve20([]);
17796
+ resolve21([]);
16692
17797
  return;
16693
17798
  }
16694
17799
  process.stderr.write(
@@ -16704,7 +17809,7 @@ var init_server = __esm({
16704
17809
  }
16705
17810
  }));
16706
17811
  this._inflightCalls.clear();
16707
- resolve20(errorResponses);
17812
+ resolve21(errorResponses);
16708
17813
  }, DRAIN_TIMEOUT_MS);
16709
17814
  });
16710
17815
  return forcedResponses;
@@ -16904,7 +18009,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
16904
18009
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
16905
18010
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
16906
18011
  "\u25CF MSapling CLI v",
16907
- "2.3.6-beta.46"
18012
+ "2.3.6-beta.48"
16908
18013
  ] }),
16909
18014
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
16910
18015
  ] });
@@ -17309,19 +18414,19 @@ function createIdleAwarePoll(pollingIntervalRef, lastActivityRef, client, setUse
17309
18414
  init_esm_shims();
17310
18415
  init_commands();
17311
18416
  init_plan();
17312
- import { spawn as spawn9 } from "child_process";
18417
+ import { spawn as spawn10 } from "child_process";
17313
18418
 
17314
18419
  // src/state/persistentState.ts
17315
18420
  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";
18421
+ import { homedir as homedir22 } from "os";
18422
+ import { join as join36, dirname as dirname5 } from "path";
18423
+ import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "fs";
17319
18424
  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");
18425
+ import { randomBytes as randomBytes15 } from "crypto";
18426
+ var STATE_PATH = join36(homedir22(), ".msapling", "state.json");
17322
18427
  async function loadPersistentState(statePath = STATE_PATH) {
17323
18428
  try {
17324
- if (!existsSync30(statePath)) return { version: 1 };
18429
+ if (!existsSync31(statePath)) return { version: 1 };
17325
18430
  const text = await readFile27(statePath, "utf8");
17326
18431
  const parsed = JSON.parse(text);
17327
18432
  if (parsed.version !== 1) return { version: 1 };
@@ -17337,7 +18442,7 @@ async function loadPersistentState(statePath = STATE_PATH) {
17337
18442
  async function savePersistentState(state, statePath = STATE_PATH) {
17338
18443
  try {
17339
18444
  const dir = dirname5(statePath);
17340
- if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
18445
+ if (!existsSync31(dir)) mkdirSync7(dir, { recursive: true });
17341
18446
  const existing = await loadPersistentState(statePath);
17342
18447
  const merged = {
17343
18448
  version: 1,
@@ -17345,7 +18450,7 @@ async function savePersistentState(state, statePath = STATE_PATH) {
17345
18450
  lastChatId: state.lastChatId ?? existing.lastChatId
17346
18451
  };
17347
18452
  const pid = process.pid;
17348
- const rand = randomBytes14(4).toString("hex");
18453
+ const rand = randomBytes15(4).toString("hex");
17349
18454
  const tmp = `${statePath}.tmp.${pid}.${rand}`;
17350
18455
  await writeFile17(tmp, JSON.stringify(merged, null, 2), "utf8");
17351
18456
  await rename3(tmp, statePath);
@@ -17432,7 +18537,7 @@ ${prompt4}` : prompt4;
17432
18537
  ctx.addMessage("system", "\u26A0 Local shell command executed without MCP/tool-level safety controls. Ensure command is trusted.");
17433
18538
  try {
17434
18539
  const shellArgv = process.platform === "win32" ? ["cmd.exe", "/c", execCmd] : ["sh", "-c", execCmd];
17435
- const proc = spawn9(shellArgv[0], shellArgv.slice(1), {
18540
+ const proc = spawn10(shellArgv[0], shellArgv.slice(1), {
17436
18541
  stdio: ["inherit", "pipe", "pipe"]
17437
18542
  });
17438
18543
  let stdout = "";
@@ -17443,9 +18548,9 @@ ${prompt4}` : prompt4;
17443
18548
  if (proc.stderr) proc.stderr.on("data", (chunk) => {
17444
18549
  stderr += chunk.toString();
17445
18550
  });
17446
- await new Promise((resolve20, reject) => {
18551
+ await new Promise((resolve21, reject) => {
17447
18552
  proc.on("close", (code) => {
17448
- if (code === 0 || code === null) resolve20();
18553
+ if (code === 0 || code === null) resolve21();
17449
18554
  else reject(new Error(`Process exited with code ${code}`));
17450
18555
  });
17451
18556
  proc.on("error", reject);
@@ -17484,9 +18589,9 @@ ${prompt4}` : prompt4;
17484
18589
  for (const mention of fileMentions) {
17485
18590
  const filePath = mention.slice(1);
17486
18591
  try {
17487
- const { existsSync: existsSync34 } = await import("fs");
18592
+ const { existsSync: existsSync35 } = await import("fs");
17488
18593
  const { readFile: readFile30 } = await import("fs/promises");
17489
- if (existsSync34(filePath)) {
18594
+ if (existsSync35(filePath)) {
17490
18595
  const content = await readFile30(filePath, "utf8");
17491
18596
  const MAX_LEN = 32768;
17492
18597
  const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
@@ -17548,7 +18653,7 @@ init_src3();
17548
18653
  init_src();
17549
18654
  init_parseApprovalMode();
17550
18655
  import { readFile as readFile28 } from "fs/promises";
17551
- import { existsSync as existsSync31 } from "fs";
18656
+ import { existsSync as existsSync32 } from "fs";
17552
18657
  async function initSession(ctx) {
17553
18658
  try {
17554
18659
  const journalEncrypted = await initJournalEncryption();
@@ -17573,10 +18678,10 @@ async function initSession(ctx) {
17573
18678
  ctx.setShellEscapeEnabled(settings.shellEscapeEnabled !== false);
17574
18679
  }
17575
18680
  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)) {
18681
+ const { homedir: homedir25 } = await import("os");
18682
+ const { join: join41 } = await import("path");
18683
+ const userSettingsPath = join41(homedir25(), ".msapling", "settings.json");
18684
+ if (existsSync32(userSettingsPath)) {
17580
18685
  const userText = await readFile28(userSettingsPath, "utf8");
17581
18686
  let parsed;
17582
18687
  try {
@@ -17734,8 +18839,8 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
17734
18839
  { kind: "approval-request", tool: request.tool, command: request.command, reason: request.reason },
17735
18840
  request.tool
17736
18841
  );
17737
- return new Promise((resolve20) => {
17738
- setPendingApproval({ request, resolve: resolve20 });
18842
+ return new Promise((resolve21) => {
18843
+ setPendingApproval({ request, resolve: resolve21 });
17739
18844
  });
17740
18845
  }, []);
17741
18846
  const agent = useRef(new Agent(client, process.cwd(), requestApproval)).current;
@@ -17993,14 +19098,14 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
17993
19098
 
17994
19099
  // src/runtime/bootstrap.ts
17995
19100
  init_esm_shims();
17996
- import { readFileSync as readFileSync4 } from "fs";
19101
+ import { readFileSync as readFileSync6 } from "fs";
17997
19102
  import { fileURLToPath as fileURLToPath2 } from "url";
17998
- import { dirname as dirname6, join as join38 } from "path";
19103
+ import { dirname as dirname6, join as join40 } from "path";
17999
19104
  function readCliVersion2() {
18000
19105
  const here = dirname6(fileURLToPath2(import.meta.url));
18001
19106
  for (const rel of ["../package.json", "../../package.json"]) {
18002
19107
  try {
18003
- const pkg = JSON.parse(readFileSync4(join38(here, rel), "utf8"));
19108
+ const pkg = JSON.parse(readFileSync6(join40(here, rel), "utf8"));
18004
19109
  if (pkg.name && pkg.version) {
18005
19110
  return { name: pkg.name, version: pkg.version };
18006
19111
  }
@@ -18106,6 +19211,7 @@ function handleCliArgs(args2) {
18106
19211
  }
18107
19212
 
18108
19213
  // src/index.tsx
19214
+ init_src3();
18109
19215
  import { jsx as jsx9 } from "react/jsx-runtime";
18110
19216
  var index_default = App;
18111
19217
  function restoreTerminalMode() {
@@ -18131,6 +19237,12 @@ msapling: fatal ${label}: ${msg}`);
18131
19237
  if (!process.env.NODE_ENV?.includes("test")) {
18132
19238
  process.on("uncaughtException", (err) => handleFatal("uncaught exception", err));
18133
19239
  process.on("unhandledRejection", (reason) => handleFatal("unhandled rejection", reason));
19240
+ process.on("exit", () => {
19241
+ try {
19242
+ BackgroundShellRegistry.killAll();
19243
+ } catch {
19244
+ }
19245
+ });
18134
19246
  }
18135
19247
  var args = process.argv.slice(2);
18136
19248
  var compact = args.includes("--compact");