agent-workbench 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // packages/server/src/index.ts
4
4
  import { existsSync as existsSync4 } from "node:fs";
5
5
  import { createServer } from "node:http";
6
- import path73 from "node:path";
6
+ import path75 from "node:path";
7
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import express from "express";
9
9
 
@@ -144,20 +144,25 @@ var WORKTREE_COPY_MAX_FILES = 2e3;
144
144
  var WORKTREE_COPY_MAX_BYTES = 50 * 1024 * 1024;
145
145
  var WORKTREE_COPY_SAMPLE = 8;
146
146
  var WORKTREE_CREATE_MODES = ["new", "existing"];
147
+ var WORKTREE_HOLDERS_MAX = 8;
147
148
  var WORKTREE_PLAN_PATHS_MAX = 20;
149
+ var WORKTREE_UNTRACKED_MAX_FILES = 2e4;
148
150
  var WORKTREE_ROW_FILES_MAX = 8;
149
151
  var WORKTREE_CHANGES_COMMITS_MAX = 50;
150
152
  var WORKTREE_CHANGES_FILES_MAX = 2e3;
151
153
  function asCommitId(value) {
152
154
  return typeof value === "string" && /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(value) ? value : null;
153
155
  }
156
+ function asUntrackedStamp(value) {
157
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value) ? value : null;
158
+ }
154
159
  function parseSessionWorktree(value) {
155
160
  const record = asRecord(value);
156
161
  if (record === null) return null;
157
162
  const branch = asNonEmptyString(record["branch"]);
158
- const exists6 = asBoolean(record["exists"]);
159
- if (branch === null || exists6 === null) return null;
160
- return { branch, exists: exists6, reopenable: record["reopenable"] === true };
163
+ const exists7 = asBoolean(record["exists"]);
164
+ if (branch === null || exists7 === null) return null;
165
+ return { branch, exists: exists7, reopenable: record["reopenable"] === true };
161
166
  }
162
167
  function parseWorktreeSource(value) {
163
168
  const record = asRecord(value);
@@ -172,6 +177,16 @@ function parseWorktreeSource(value) {
172
177
  }
173
178
  return null;
174
179
  }
180
+ var HOLDER_TEXT_MAX = 255;
181
+ function parseWorktreeHolder(value) {
182
+ const record = asRecord(value);
183
+ if (record === null) return null;
184
+ const pid = asPositiveInt(record["pid"]);
185
+ const name = asNonEmptyString(record["name"]);
186
+ if (pid === null || name === null || name.length > HOLDER_TEXT_MAX) return null;
187
+ const description = asString(record["description"])?.trim() ?? "";
188
+ return { pid, name, description: description.length > 0 && description.length <= HOLDER_TEXT_MAX ? description : null };
189
+ }
175
190
 
176
191
  // packages/shared/src/models.ts
177
192
  var SESSION_TITLE_SOURCES = [
@@ -1326,7 +1341,10 @@ function parseClientMessage(raw) {
1326
1341
  const requestId = asNonEmptyString(record["requestId"]);
1327
1342
  const worktreeId = asNonEmptyString(record["worktreeId"]);
1328
1343
  const discardConflicts = asBoolean(record["discardConflicts"]);
1329
- return requestId === null || worktreeId === null || discardConflicts === null ? null : { type: "worktree.remove", requestId, worktreeId, discardConflicts };
1344
+ if (requestId === null || worktreeId === null || discardConflicts === null) return null;
1345
+ const message = { type: "worktree.remove", requestId, worktreeId, discardConflicts };
1346
+ const discardUntracked = asUntrackedStamp(record["discardUntracked"]);
1347
+ return discardUntracked === null ? message : { ...message, discardUntracked };
1330
1348
  }
1331
1349
  case "worktree.reopen": {
1332
1350
  const requestId = asNonEmptyString(record["requestId"]);
@@ -1364,8 +1382,8 @@ function parseClientMessage(raw) {
1364
1382
  case "worktree.changesDiff": {
1365
1383
  const requestId = asNonEmptyString(record["requestId"]);
1366
1384
  const worktreeId = asNonEmptyString(record["worktreeId"]);
1367
- const path74 = asNonEmptyString(record["path"]);
1368
- return requestId === null || worktreeId === null || path74 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path74 };
1385
+ const path76 = asNonEmptyString(record["path"]);
1386
+ return requestId === null || worktreeId === null || path76 === null ? null : { type: "worktree.changesDiff", requestId, worktreeId, path: path76 };
1369
1387
  }
1370
1388
  case "worktree.requestReview": {
1371
1389
  const requestId = asNonEmptyString(record["requestId"]);
@@ -5962,18 +5980,18 @@ var BranchTracker = class {
5962
5980
  }
5963
5981
  /** La linea y todas las de las que cuelga. */
5964
5982
  pathFrom(leaf) {
5965
- const path74 = /* @__PURE__ */ new Set();
5983
+ const path76 = /* @__PURE__ */ new Set();
5966
5984
  let current = leaf;
5967
- while (current !== null && !path74.has(current)) {
5985
+ while (current !== null && !path76.has(current)) {
5968
5986
  const node = this.nodes.get(current);
5969
5987
  if (node === void 0) break;
5970
- path74.add(current);
5988
+ path76.add(current);
5971
5989
  current = node.parent;
5972
5990
  }
5973
- return path74;
5991
+ return path76;
5974
5992
  }
5975
5993
  recompute() {
5976
- const path74 = this.pathFrom(this.leaf());
5994
+ const path76 = this.pathFrom(this.leaf());
5977
5995
  const children = /* @__PURE__ */ new Map();
5978
5996
  for (const [uuid, node] of this.nodes) {
5979
5997
  const key = node.parent !== null && this.nodes.has(node.parent) ? node.parent : null;
@@ -5982,9 +6000,9 @@ var BranchTracker = class {
5982
6000
  else list.push(uuid);
5983
6001
  }
5984
6002
  const roots = [];
5985
- for (const uuid of children.get(null) ?? []) if (!path74.has(uuid)) roots.push(uuid);
5986
- for (const uuid of path74) {
5987
- for (const child of children.get(uuid) ?? []) if (!path74.has(child)) roots.push(child);
6003
+ for (const uuid of children.get(null) ?? []) if (!path76.has(uuid)) roots.push(uuid);
6004
+ for (const uuid of path76) {
6005
+ for (const child of children.get(uuid) ?? []) if (!path76.has(child)) roots.push(child);
5988
6006
  }
5989
6007
  const hidden = /* @__PURE__ */ new Set();
5990
6008
  for (const root of roots) {
@@ -19299,8 +19317,8 @@ function buildContinuationMessage(input) {
19299
19317
  }
19300
19318
  return sanitizeForPaste(lines.join("\n"));
19301
19319
  }
19302
- function transcriptReferenceFor(path74, style) {
19303
- return style === "at-quoted" ? fileReference(path74, "at-quoted") : `"${path74}"`;
19320
+ function transcriptReferenceFor(path76, style) {
19321
+ return style === "at-quoted" ? fileReference(path76, "at-quoted") : `"${path76}"`;
19304
19322
  }
19305
19323
  function planTranscript(input) {
19306
19324
  const turns = splitTurns(input.events);
@@ -21145,9 +21163,9 @@ function watchSessions(index, hub, agents, options = {}) {
21145
21163
  const probe = setInterval(() => {
21146
21164
  if (checking) return;
21147
21165
  checking = true;
21148
- void isDirectory3(root.path).then((exists6) => {
21166
+ void isDirectory3(root.path).then((exists7) => {
21149
21167
  checking = false;
21150
- if (!exists6 || stopped || !probes.has(probe)) return;
21168
+ if (!exists7 || stopped || !probes.has(probe)) return;
21151
21169
  clearInterval(probe);
21152
21170
  probes.delete(probe);
21153
21171
  watchRoot(agent, root, true);
@@ -21162,9 +21180,9 @@ function watchSessions(index, hub, agents, options = {}) {
21162
21180
  subscribeRoot(adapter.id, root, root.watch);
21163
21181
  continue;
21164
21182
  }
21165
- void isDirectory3(root.path).then((exists6) => {
21183
+ void isDirectory3(root.path).then((exists7) => {
21166
21184
  if (stopped) return;
21167
- if (exists6) watchRoot(adapter.id, root, false);
21185
+ if (exists7) watchRoot(adapter.id, root, false);
21168
21186
  else probeRoot(adapter.id, root);
21169
21187
  });
21170
21188
  }
@@ -22915,16 +22933,19 @@ async function searchFiles(cwd, query, folders, options = {}) {
22915
22933
  truncated: truncated || found.length > SEARCH_MAX_RESULTS
22916
22934
  };
22917
22935
  }
22918
- async function existingFiles(cwd, relativePaths, folders) {
22919
- const existing = [];
22936
+ async function existingPaths(cwd, relativePaths, folders) {
22937
+ const files = [];
22938
+ const directories = [];
22920
22939
  for (const relativePath of relativePaths) {
22921
22940
  try {
22922
22941
  const absolute = await resolveForFiles(cwd, relativePath, folders);
22923
- if ((await stat37(absolute)).isFile()) existing.push(relativePath);
22942
+ const info = await stat37(absolute);
22943
+ if (info.isFile()) files.push(relativePath);
22944
+ else if (info.isDirectory()) directories.push(relativePath);
22924
22945
  } catch {
22925
22946
  }
22926
22947
  }
22927
- return existing;
22948
+ return { files, directories };
22928
22949
  }
22929
22950
  function contentHash(content) {
22930
22951
  return createHash4("sha256").update(content).digest("hex");
@@ -25055,12 +25076,12 @@ var VaultService = class {
25055
25076
  // packages/server/src/worktrees/service.ts
25056
25077
  import { randomBytes as randomBytes6, randomUUID as randomUUID7 } from "node:crypto";
25057
25078
  import { EventEmitter as EventEmitter7 } from "node:events";
25058
- import { mkdir as mkdir22, rename as rename7, rm as rm9, rmdir, stat as stat44 } from "node:fs/promises";
25059
- import path70 from "node:path";
25079
+ import { lstat as lstat5, mkdir as mkdir22, rename as rename7, rm as rm10, rmdir, stat as stat45 } from "node:fs/promises";
25080
+ import path72 from "node:path";
25060
25081
 
25061
25082
  // packages/server/src/worktrees/git.ts
25062
25083
  import { execFile as execFile6 } from "node:child_process";
25063
- import { mkdtemp, rm as rm7 } from "node:fs/promises";
25084
+ import { mkdtemp, rm as rm7, stat as stat43 } from "node:fs/promises";
25064
25085
  import { tmpdir as tmpdir5 } from "node:os";
25065
25086
  import path65 from "node:path";
25066
25087
  var READ_TIMEOUT_MS = 15e3;
@@ -25073,7 +25094,7 @@ function run(cwd, args, options) {
25073
25094
  return new Promise((resolve, reject) => {
25074
25095
  const child = execFile6(
25075
25096
  "git",
25076
- ["-c", "core.quotepath=false", ...options.write ? [] : ["--no-optional-locks"], ...args],
25097
+ ["-c", "core.quotepath=false", "-c", "core.longpaths=true", ...options.write ? [] : ["--no-optional-locks"], ...args],
25077
25098
  {
25078
25099
  cwd,
25079
25100
  timeout: options.write ? WRITE_TIMEOUT_MS : READ_TIMEOUT_MS,
@@ -25126,15 +25147,42 @@ async function localBranches(cwd) {
25126
25147
  return result.stdout.split(/\r?\n/).map((line) => branchOfRef(line.trim())).filter((branch) => branch !== null);
25127
25148
  }
25128
25149
  async function listWorktrees(cwd) {
25150
+ return await readWorktreeList(cwd) ?? [];
25151
+ }
25152
+ async function readWorktreeList(cwd) {
25129
25153
  const result = await read(cwd, ["worktree", "list", "--porcelain", "-z"]);
25130
- return result.code === 0 ? parseWorktreeListZ(result.stdout) : [];
25154
+ return result.code === 0 ? parseWorktreeListZ(result.stdout) : null;
25131
25155
  }
25132
- async function uncommittedPaths(cwd) {
25156
+ async function uncommittedChanges(cwd) {
25133
25157
  const result = await read(cwd, ["status", "--porcelain=v2", "-z", "--untracked-files=normal"]);
25134
25158
  if (result.code !== 0) return null;
25159
+ const parsed = parsePorcelainV2(result.stdout);
25135
25160
  const seen = /* @__PURE__ */ new Set();
25136
- for (const change of parsePorcelainV2(result.stdout).changes) seen.add(change.path);
25137
- return [...seen];
25161
+ const untracked = [];
25162
+ for (const change of parsed.changes) {
25163
+ seen.add(change.path);
25164
+ if (change.kind === "untracked") untracked.push(change.path);
25165
+ }
25166
+ return { paths: [...seen], untracked, complete: !parsed.truncated };
25167
+ }
25168
+ async function uncommittedPaths(cwd) {
25169
+ return (await uncommittedChanges(cwd))?.paths ?? null;
25170
+ }
25171
+ async function hasLoadedSubmodule(cwd) {
25172
+ const [modules, staged] = await Promise.all([read(cwd, ["rev-parse", "--git-path", "modules"]), read(cwd, ["ls-files", "--stage", "-z"])]);
25173
+ if (modules.code !== 0 || staged.code !== 0) return null;
25174
+ if (await isDirectory4(path65.resolve(cwd, modules.stdout.trim()))) return true;
25175
+ const submodules = staged.stdout.split("\0").filter((line) => line.startsWith("160000 ")).map((line) => line.slice(line.indexOf(" ") + 1));
25176
+ for (const submodule of submodules) {
25177
+ if (await exists4(path65.join(cwd, submodule, ".git"))) return true;
25178
+ }
25179
+ return false;
25180
+ }
25181
+ async function isDirectory4(candidate) {
25182
+ return (await stat43(candidate).catch(() => null))?.isDirectory() === true;
25183
+ }
25184
+ async function exists4(candidate) {
25185
+ return await stat43(candidate).catch(() => null) !== null;
25138
25186
  }
25139
25187
  async function isIntegrated(cwd, branch, base) {
25140
25188
  const result = await read(cwd, ["merge-base", "--is-ancestor", `refs/heads/${branch}`, `refs/heads/${base}`]);
@@ -25342,10 +25390,247 @@ function fastForward(baseFolder, target) {
25342
25390
  return run(baseFolder, fastForwardArgs(target), { write: true });
25343
25391
  }
25344
25392
 
25393
+ // packages/server/src/worktrees/holders.ts
25394
+ import { spawn as spawn4 } from "node:child_process";
25395
+ import path66 from "node:path";
25396
+ var SCAN_MAX_ENTRIES = 2e5;
25397
+ var SCAN_MAX_MS = 4e3;
25398
+ var HOLDERS_TIMEOUT_MS = 15e3;
25399
+ var OUTPUT_MAX_CHARS = 256 * 1024;
25400
+ var CSHARP = String.raw`
25401
+ using System;
25402
+ using System.Collections.Generic;
25403
+ using System.Diagnostics;
25404
+ using System.IO;
25405
+ using System.Runtime.InteropServices;
25406
+ using System.Text;
25407
+ using Microsoft.Win32.SafeHandles;
25408
+
25409
+ public static class AwFolderHolders {
25410
+ [StructLayout(LayoutKind.Sequential)]
25411
+ struct IoStatus { public IntPtr Status; public IntPtr Information; }
25412
+
25413
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
25414
+ struct FindData {
25415
+ public uint Attributes;
25416
+ public uint Created1, Created2, Accessed1, Accessed2, Written1, Written2;
25417
+ public uint SizeHigh, SizeLow, Reserved0, Reserved1;
25418
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string Name;
25419
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string ShortName;
25420
+ }
25421
+
25422
+ [DllImport("ntdll.dll")]
25423
+ static extern int NtQueryInformationFile(SafeFileHandle file, out IoStatus status, IntPtr buffer, int length, int infoClass);
25424
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
25425
+ static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template);
25426
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
25427
+ static extern IntPtr FindFirstFileExW(string name, int level, out FindData data, int search, IntPtr filter, int flags);
25428
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
25429
+ static extern bool FindNextFileW(IntPtr find, out FindData data);
25430
+ [DllImport("kernel32.dll")]
25431
+ static extern bool FindClose(IntPtr find);
25432
+ [DllImport("kernel32.dll", SetLastError = true)]
25433
+ static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
25434
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
25435
+ static extern bool QueryFullProcessImageNameW(IntPtr process, int flags, StringBuilder name, ref int size);
25436
+ [DllImport("kernel32.dll")]
25437
+ static extern bool CloseHandle(IntPtr handle);
25438
+
25439
+ const uint ReadData = 0x1, ReadAttributes = 0x80, Delete = 0x10000, ShareAll = 7, OpenExisting = 3;
25440
+ /* BACKUP_SEMANTICS abre carpetas; OPEN_REPARSE_POINT abre el enlace y no su destino. */
25441
+ const uint OpenFlags = 0x02000000 | 0x00200000;
25442
+ const int ProcessIdsUsingFile = 47, QueryLimited = 0x1000, MaxNamed = 32;
25443
+ static readonly IntPtr NoFind = new IntPtr(-1);
25444
+
25445
+ static bool Taken(string path, bool top) {
25446
+ using (SafeFileHandle file = CreateFileW(path, top ? Delete : ReadData, top ? ShareAll : 0, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
25447
+ if (!file.IsInvalid) return false;
25448
+ int error = Marshal.GetLastWin32Error();
25449
+ return error != 2 && error != 3;
25450
+ }
25451
+ }
25452
+
25453
+ static void Collect(string path, HashSet<int> seen, List<int> found, int self) {
25454
+ using (SafeFileHandle file = CreateFileW(path, ReadAttributes, ShareAll, IntPtr.Zero, OpenExisting, OpenFlags, IntPtr.Zero)) {
25455
+ if (file.IsInvalid) return;
25456
+ for (int size = 4096; size <= 1 << 20; size *= 4) {
25457
+ IntPtr buffer = Marshal.AllocHGlobal(size);
25458
+ try {
25459
+ IoStatus status;
25460
+ int code = NtQueryInformationFile(file, out status, buffer, size, ProcessIdsUsingFile);
25461
+ if (code == unchecked((int)0xC0000004) || code == unchecked((int)0x80000005)) continue;
25462
+ if (code != 0) return;
25463
+ int count = Marshal.ReadInt32(buffer);
25464
+ if (IntPtr.Size * (count + 1) > size) continue;
25465
+ for (int i = 0; i < count; i++) {
25466
+ int pid = (int)Marshal.ReadIntPtr(buffer, IntPtr.Size * (i + 1)).ToInt64();
25467
+ if (pid != self && seen.Add(pid)) found.Add(pid);
25468
+ }
25469
+ return;
25470
+ } finally { Marshal.FreeHGlobal(buffer); }
25471
+ }
25472
+ }
25473
+ }
25474
+
25475
+ static string Json(string value) {
25476
+ if (value == null) return "null";
25477
+ StringBuilder text = new StringBuilder("\"");
25478
+ foreach (char c in value) {
25479
+ if (c == '"' || c == '\\') text.Append('\\').Append(c);
25480
+ else if (c < 0x20 || c > 0x7E) text.Append("\\u").Append(((int)c).ToString("x4"));
25481
+ else text.Append(c);
25482
+ }
25483
+ return text.Append('"').ToString();
25484
+ }
25485
+
25486
+ public static string Scan(string root, int maxEntries, int maxMs) {
25487
+ Stopwatch clock = Stopwatch.StartNew();
25488
+ int self = Process.GetCurrentProcess().Id;
25489
+ HashSet<int> seen = new HashSet<int>();
25490
+ List<int> found = new List<int>();
25491
+ List<string> files = new List<string>();
25492
+ Queue<string> dirs = new Queue<string>();
25493
+ bool complete = true;
25494
+ int entries = 0;
25495
+ if (Taken(root, true)) Collect(root, seen, found, self);
25496
+ dirs.Enqueue(root);
25497
+ while (dirs.Count > 0) {
25498
+ if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
25499
+ string dir = dirs.Dequeue();
25500
+ if (dir != root && Taken(dir, false)) Collect(dir, seen, found, self);
25501
+ FindData data;
25502
+ IntPtr find = FindFirstFileExW(dir + "\\*", 1, out data, 0, IntPtr.Zero, 2);
25503
+ if (find == NoFind) continue;
25504
+ try {
25505
+ do {
25506
+ if (data.Name == "." || data.Name == "..") continue;
25507
+ string child = dir + "\\" + data.Name;
25508
+ if ((data.Attributes & 0x10) != 0 && (data.Attributes & 0x400) == 0) dirs.Enqueue(child);
25509
+ else if (files.Count < maxEntries) files.Add(child);
25510
+ } while (FindNextFileW(find, out data));
25511
+ } finally { FindClose(find); }
25512
+ }
25513
+ if (complete) {
25514
+ foreach (string file in files) {
25515
+ if (++entries > maxEntries || clock.ElapsedMilliseconds > maxMs) { complete = false; break; }
25516
+ if (Taken(file, false)) Collect(file, seen, found, self);
25517
+ }
25518
+ }
25519
+ StringBuilder json = new StringBuilder("{\"complete\":");
25520
+ json.Append(complete ? "true" : "false").Append(",\"ms\":").Append(clock.ElapsedMilliseconds).Append(",\"holders\":[");
25521
+ int named = 0;
25522
+ foreach (int pid in found) {
25523
+ if (named >= MaxNamed) break;
25524
+ string name = null, description = null;
25525
+ IntPtr process = OpenProcess(QueryLimited, false, pid);
25526
+ if (process != IntPtr.Zero) {
25527
+ try {
25528
+ StringBuilder image = new StringBuilder(32768);
25529
+ int length = image.Capacity;
25530
+ if (QueryFullProcessImageNameW(process, 0, image, ref length)) {
25531
+ string full = image.ToString(0, length);
25532
+ name = Path.GetFileName(full);
25533
+ try { description = FileVersionInfo.GetVersionInfo(full).FileDescription; } catch { }
25534
+ }
25535
+ } finally { CloseHandle(process); }
25536
+ }
25537
+ if (name == null) {
25538
+ try { name = pid == 4 ? "System" : Process.GetProcessById(pid).ProcessName + ".exe"; } catch { continue; }
25539
+ }
25540
+ json.Append(named++ == 0 ? "" : ",").Append("{\"pid\":").Append(pid).Append(",\"name\":").Append(Json(name))
25541
+ .Append(",\"description\":").Append(Json(description)).Append('}');
25542
+ }
25543
+ return json.Append("]}").ToString();
25544
+ }
25545
+ }
25546
+ `;
25547
+ function longPath(folder) {
25548
+ const absolute = path66.win32.resolve(folder);
25549
+ if (absolute.startsWith("\\\\?\\")) return absolute;
25550
+ return absolute.startsWith("\\\\") ? `\\\\?\\UNC\\${absolute.slice(2)}` : `\\\\?\\${absolute}`;
25551
+ }
25552
+ function holdersScript(folder) {
25553
+ const encoded = Buffer.from(longPath(folder), "utf8").toString("base64");
25554
+ return [
25555
+ "$ErrorActionPreference = 'Stop'",
25556
+ `$source = @'
25557
+ ${CSHARP}
25558
+ '@`,
25559
+ "Add-Type -TypeDefinition $source",
25560
+ `$root = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}'))`,
25561
+ `[Console]::Out.WriteLine([AwFolderHolders]::Scan($root, ${SCAN_MAX_ENTRIES}, ${SCAN_MAX_MS}))`,
25562
+ // Con `-Command -`, la última línea corre recién con la línea en blanco de después.
25563
+ "",
25564
+ ""
25565
+ ].join("\n");
25566
+ }
25567
+ function parseHoldersOutput(output, selfPid) {
25568
+ const line = output.split(/\r?\n/).map((candidate) => candidate.trim()).find((candidate) => candidate.startsWith('{"complete":'));
25569
+ const record = line === void 0 ? null : asRecord(parseJson(line));
25570
+ if (record === null || typeof record["complete"] !== "boolean" || !Array.isArray(record["holders"])) return null;
25571
+ const holders = [];
25572
+ let self = false;
25573
+ for (const item of record["holders"]) {
25574
+ const holder = parseWorktreeHolder(item);
25575
+ if (holder === null || holders.some((other) => other.pid === holder.pid)) continue;
25576
+ if (holder.pid === selfPid) self = true;
25577
+ else if (holders.length < WORKTREE_HOLDERS_MAX) holders.push(holder);
25578
+ }
25579
+ return { holders, complete: record["complete"], self };
25580
+ }
25581
+ function powershellPath() {
25582
+ const systemRoot = process.env["SystemRoot"] ?? process.env["windir"] ?? "C:\\Windows";
25583
+ return path66.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
25584
+ }
25585
+ function runPowerShell(script) {
25586
+ return new Promise((resolve) => {
25587
+ const child = spawn4(powershellPath(), ["-NoProfile", "-NonInteractive", "-Command", "-"], {
25588
+ stdio: ["pipe", "pipe", "ignore"],
25589
+ windowsHide: true
25590
+ });
25591
+ let output = "";
25592
+ let settled = false;
25593
+ const finish = (value) => {
25594
+ if (settled) return;
25595
+ settled = true;
25596
+ clearTimeout(timer);
25597
+ resolve(value);
25598
+ };
25599
+ const timer = setTimeout(() => {
25600
+ child.kill();
25601
+ finish(null);
25602
+ }, HOLDERS_TIMEOUT_MS);
25603
+ timer.unref();
25604
+ child.stdout.setEncoding("utf8");
25605
+ child.stdout.on("data", (chunk) => {
25606
+ output += chunk;
25607
+ if (output.length <= OUTPUT_MAX_CHARS) return;
25608
+ child.kill();
25609
+ finish(null);
25610
+ });
25611
+ child.on("error", () => finish(null));
25612
+ child.on("close", () => finish(output));
25613
+ child.stdin.on("error", () => void 0);
25614
+ child.stdin.end(script);
25615
+ });
25616
+ }
25617
+ async function folderHolders(folder) {
25618
+ if (process.platform !== "win32") return [];
25619
+ const output = await runPowerShell(holdersScript(folder));
25620
+ const parsed = output === null ? null : parseHoldersOutput(output, process.pid);
25621
+ if (parsed === null) {
25622
+ console.warn(`[worktrees] couldn't tell which programs have ${folder} open`);
25623
+ return [];
25624
+ }
25625
+ if (parsed.self) console.warn(`[worktrees] this server has ${folder} open`);
25626
+ debugLog("worktrees", `holders of ${folder}: ${parsed.holders.length}, complete ${parsed.complete}`);
25627
+ return parsed.holders;
25628
+ }
25629
+
25345
25630
  // packages/server/src/worktrees/include.ts
25346
25631
  import { createHash as createHash6 } from "node:crypto";
25347
25632
  import { lstat as lstat3, mkdir as mkdir19, readdir as readdir19, readFile as readFile28, writeFile as writeFile11 } from "node:fs/promises";
25348
- import path66 from "node:path";
25633
+ import path67 from "node:path";
25349
25634
 
25350
25635
  // packages/server/src/worktrees/merge-notes.ts
25351
25636
  var MERGE_MAX_EDITS = 2e3;
@@ -25521,7 +25806,7 @@ var CLI_CONFIG_DIRS = /* @__PURE__ */ new Set([".claude", ".codex", ".gemini", "
25521
25806
  var CLI_CONFIG_FILES = /* @__PURE__ */ new Set([".mcp.json", "opencode.json", "opencode.jsonc"]);
25522
25807
  var INCLUDE_FILE = ".worktreeinclude";
25523
25808
  var INCLUDE_FILE_MAX_BYTES = 64 * 1024;
25524
- var toPosix2 = (value) => value.split(path66.sep).join("/");
25809
+ var toPosix2 = (value) => value.split(path67.sep).join("/");
25525
25810
  function hashOf(content) {
25526
25811
  return createHash6("sha256").update(content).digest("hex");
25527
25812
  }
@@ -25628,7 +25913,7 @@ async function copyToWorktree(mainRoot, folder, files, bases) {
25628
25913
  if (!info.isFile()) continue;
25629
25914
  content = await readFile28(source);
25630
25915
  const target = await resolveInside(folder, file.path);
25631
- await mkdir19(path66.dirname(target), { recursive: true });
25916
+ await mkdir19(path67.dirname(target), { recursive: true });
25632
25917
  await writeFile11(target, content, { flag: "wx" });
25633
25918
  copied.push({ path: file.path, hash: hashOf(content) });
25634
25919
  } catch (error) {
@@ -25768,7 +26053,7 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
25768
26053
  result.skipped.push(file);
25769
26054
  continue;
25770
26055
  }
25771
- await mkdir19(path66.dirname(target), { recursive: true });
26056
+ await mkdir19(path67.dirname(target), { recursive: true });
25772
26057
  await writeAtomically(target, outcome.content, {
25773
26058
  beforeRename: async () => {
25774
26059
  const now = await readInside(mainRoot, file);
@@ -25790,15 +26075,15 @@ async function returnNotes(mainRoot, folder, files, bases, options = {}) {
25790
26075
  }
25791
26076
 
25792
26077
  // packages/server/src/worktrees/names.ts
25793
- import path67 from "node:path";
25794
- function worktreeFolderFor(mainRoot, branch, pathApi = path67) {
26078
+ import path68 from "node:path";
26079
+ function worktreeFolderFor(mainRoot, branch, pathApi = path68) {
25795
26080
  const parent = pathApi.dirname(mainRoot);
25796
26081
  return pathApi.join(parent, worktreesDirName(pathApi.basename(mainRoot)), worktreeFolderName(branch));
25797
26082
  }
25798
- function worktreesParentFor(mainRoot, pathApi = path67) {
26083
+ function worktreesParentFor(mainRoot, pathApi = path68) {
25799
26084
  return pathApi.join(pathApi.dirname(mainRoot), worktreesDirName(pathApi.basename(mainRoot)));
25800
26085
  }
25801
- function mainRootFromShape(folder, pathApi = path67) {
26086
+ function mainRootFromShape(folder, pathApi = path68) {
25802
26087
  const parent = pathApi.dirname(folder);
25803
26088
  const parentName = pathApi.basename(parent);
25804
26089
  if (parentName.length > ".worktrees".length && parentName.toLowerCase().endsWith(".worktrees")) {
@@ -25813,8 +26098,8 @@ function mainRootFromShape(folder, pathApi = path67) {
25813
26098
 
25814
26099
  // packages/server/src/worktrees/note-bases.ts
25815
26100
  import { createHash as createHash7 } from "node:crypto";
25816
- import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as stat43, utimes as utimes3 } from "node:fs/promises";
25817
- import path68 from "node:path";
26101
+ import { mkdir as mkdir20, readdir as readdir20, readFile as readFile29, rm as rm8, stat as stat44, utimes as utimes3 } from "node:fs/promises";
26102
+ import path69 from "node:path";
25818
26103
  var TEXT_NOTE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".mdx", ".markdown", ".txt", ".rst", ".adoc"]);
25819
26104
  var NOTE_BASE_MAX_BYTES = 2 * 1024 * 1024;
25820
26105
  var PRUNE_GRACE_MS = 10 * 6e4;
@@ -25823,7 +26108,7 @@ function hashOf2(content) {
25823
26108
  return createHash7("sha256").update(content).digest("hex");
25824
26109
  }
25825
26110
  function keepsNoteBase(relative, content) {
25826
- return TEXT_NOTE_EXTENSIONS.has(path68.posix.extname(relative).toLowerCase()) && content.length <= NOTE_BASE_MAX_BYTES && !content.subarray(0, 8e3).includes(0);
26111
+ return TEXT_NOTE_EXTENSIONS.has(path69.posix.extname(relative).toLowerCase()) && content.length <= NOTE_BASE_MAX_BYTES && !content.subarray(0, 8e3).includes(0);
25827
26112
  }
25828
26113
  var NoteBases = class {
25829
26114
  constructor(dir = worktreeNotesDir(), options = {}) {
@@ -25843,7 +26128,7 @@ var NoteBases = class {
25843
26128
  async save(relative, content) {
25844
26129
  if (!keepsNoteBase(relative, content)) return;
25845
26130
  const hash = hashOf2(content);
25846
- const file = path68.join(this.dir, hash);
26131
+ const file = path69.join(this.dir, hash);
25847
26132
  const current = await readFile29(file).catch(() => null);
25848
26133
  if (current !== null && hashOf2(current) === hash) {
25849
26134
  const now = new Date(this.now());
@@ -25857,7 +26142,7 @@ var NoteBases = class {
25857
26142
  async load(hash) {
25858
26143
  if (!HASH.test(hash)) return null;
25859
26144
  try {
25860
- const content = await readFile29(path68.join(this.dir, hash));
26145
+ const content = await readFile29(path69.join(this.dir, hash));
25861
26146
  return hashOf2(content) === hash ? content : null;
25862
26147
  } catch {
25863
26148
  return null;
@@ -25874,9 +26159,9 @@ var NoteBases = class {
25874
26159
  const now = this.now();
25875
26160
  for (const name of names) {
25876
26161
  if (!HASH.test(name) || keep.has(name)) continue;
25877
- const file = path68.join(this.dir, name);
26162
+ const file = path69.join(this.dir, name);
25878
26163
  try {
25879
- const info = await stat43(file);
26164
+ const info = await stat44(file);
25880
26165
  if (!info.isFile() || now - info.mtimeMs < this.graceMs) continue;
25881
26166
  await rm8(file, { force: true });
25882
26167
  } catch {
@@ -25929,7 +26214,7 @@ function catchUpPrompt(base, conflicts, uncommitted) {
25929
26214
  }
25930
26215
  function commitPrompt(files, count) {
25931
26216
  const more = count > files.length ? ` and ${count - files.length} more` : "";
25932
- return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message, or tell me first if any of them should be discarded instead. Don't push.`;
26217
+ return `This branch folder has uncommitted changes: ${files.join(", ")}${more}. Commit them with a clear message. If any of them shouldn't be committed, leave it as it is and tell me which and why. Don't push.`;
25933
26218
  }
25934
26219
  function lastReplyText(events) {
25935
26220
  const textOf3 = (event2) => event2.parts.flatMap((part) => part.kind === "text" ? [part.text] : []).join("\n").trim();
@@ -25964,7 +26249,7 @@ function readVerdict(reply) {
25964
26249
 
25965
26250
  // packages/server/src/worktrees/store.ts
25966
26251
  import { mkdir as mkdir21, readFile as readFile30 } from "node:fs/promises";
25967
- import path69 from "node:path";
26252
+ import path70 from "node:path";
25968
26253
  var STATE_VERSION5 = 1;
25969
26254
  var MAX_REMOVED = 300;
25970
26255
  var MAX_REVIEW_SESSIONS = 20;
@@ -26102,16 +26387,106 @@ var WorktreeStore = class {
26102
26387
  }
26103
26388
  const content = Buffer.from(JSON.stringify({ version: STATE_VERSION5, worktrees: this.records }, null, 2), "utf8");
26104
26389
  this.writing = this.writing.catch(() => void 0).then(async () => {
26105
- await mkdir21(path69.dirname(this.file), { recursive: true });
26390
+ await mkdir21(path70.dirname(this.file), { recursive: true });
26106
26391
  await writeAtomically(this.file, content);
26107
26392
  });
26108
26393
  return this.writing;
26109
26394
  }
26110
26395
  };
26111
26396
 
26397
+ // packages/server/src/worktrees/untracked.ts
26398
+ import { createHash as createHash8 } from "node:crypto";
26399
+ import { lstat as lstat4, readdir as readdir21, realpath as realpath7, rm as rm9 } from "node:fs/promises";
26400
+ import path71 from "node:path";
26401
+ var sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
26402
+ function cleanEntry(entry) {
26403
+ const clean = entry.replace(/\/+$/, "");
26404
+ if (clean.length === 0 || clean.includes("\\")) return null;
26405
+ const parts = clean.split("/");
26406
+ return parts.some((part) => part.length === 0 || part === "." || part === ".." || sameName(part, ".git")) ? null : clean;
26407
+ }
26408
+ async function scanUntracked(folder, entries, maxFiles = WORKTREE_UNTRACKED_MAX_FILES) {
26409
+ const walk2 = { hash: createHash8("sha256"), files: 0, bytes: 0, max: maxFiles, blocked: null };
26410
+ const targets = [];
26411
+ for (const entry of [...entries].sort()) {
26412
+ const clean = cleanEntry(entry);
26413
+ if (clean === null) return null;
26414
+ let target;
26415
+ let real;
26416
+ try {
26417
+ const parent = await resolveInside(folder, path71.posix.dirname(clean), { mustExist: true });
26418
+ target = path71.join(parent, path71.posix.basename(clean));
26419
+ real = path71.join(await realpath7(parent), path71.posix.basename(clean));
26420
+ } catch {
26421
+ return null;
26422
+ }
26423
+ targets.push(target);
26424
+ walk2.hash.update(`${clean}\0`);
26425
+ await visit(target, real, clean, walk2);
26426
+ if (walk2.blocked !== null) return { files: walk2.files, bytes: walk2.bytes, stamp: null, blocked: walk2.blocked, targets: [] };
26427
+ }
26428
+ return { files: walk2.files, bytes: walk2.bytes, stamp: walk2.hash.digest("hex"), blocked: null, targets };
26429
+ }
26430
+ async function visit(absolute, real, relative, walk2) {
26431
+ if (walk2.files >= walk2.max) {
26432
+ walk2.blocked = "too-many";
26433
+ return;
26434
+ }
26435
+ let info;
26436
+ try {
26437
+ info = await lstat4(absolute);
26438
+ } catch {
26439
+ walk2.hash.update(`${relative}\0?
26440
+ `);
26441
+ return;
26442
+ }
26443
+ const linked = !info.isDirectory() || !sameName(await realpath7(absolute).catch(() => ""), real);
26444
+ if (linked) {
26445
+ walk2.files += 1;
26446
+ walk2.bytes += info.size;
26447
+ walk2.hash.update(`${relative}\0${info.isSymbolicLink() || info.isDirectory() ? "l" : "f"}\0${info.size}\0${info.mtimeMs}
26448
+ `);
26449
+ return;
26450
+ }
26451
+ walk2.hash.update(`${relative}\0d
26452
+ `);
26453
+ let names;
26454
+ try {
26455
+ names = (await readdir21(absolute)).sort();
26456
+ } catch {
26457
+ walk2.hash.update(`${relative}\0!
26458
+ `);
26459
+ return;
26460
+ }
26461
+ if (names.some((name) => sameName(name, ".git"))) {
26462
+ walk2.blocked = "nested-repo";
26463
+ return;
26464
+ }
26465
+ for (const name of names) {
26466
+ await visit(path71.join(absolute, name), path71.join(real, name), `${relative}/${name}`, walk2);
26467
+ if (walk2.blocked !== null) return;
26468
+ }
26469
+ }
26470
+ async function deleteUntracked(targets) {
26471
+ for (const target of targets) {
26472
+ try {
26473
+ await rm9(target, { recursive: true, maxRetries: 3 });
26474
+ } catch (error) {
26475
+ if (error.code !== "ENOENT") throw error;
26476
+ }
26477
+ }
26478
+ }
26479
+
26112
26480
  // packages/server/src/worktrees/service.ts
26113
26481
  var WorktreeError = class extends ServerTextError {
26114
26482
  };
26483
+ var WorktreeInUseError = class extends WorktreeError {
26484
+ constructor(plan) {
26485
+ super(serverText("worktreeInUse"));
26486
+ this.plan = plan;
26487
+ }
26488
+ plan;
26489
+ };
26115
26490
  var ROWS_TTL_MS = 5e3;
26116
26491
  var RELEASE_TIMEOUT_MS = 5e3;
26117
26492
  var RELEASE_POLL_MS = 100;
@@ -26120,17 +26495,20 @@ var REPLY_LIMITS = { textMaxChars: 2e5, toolInputMaxChars: 200, toolResultMaxCha
26120
26495
  var REPLY_EVENTS = 300;
26121
26496
  var REPLY_CACHE_MAX = 50;
26122
26497
  var REVIEW_ACTIVITY_DELAY_MS = 1e3;
26123
- async function exists4(candidate) {
26498
+ async function leftWithoutGit(folder) {
26499
+ return await exists5(folder) && await lstat5(path72.join(folder, ".git")).catch(() => null) === null;
26500
+ }
26501
+ async function exists5(candidate) {
26124
26502
  try {
26125
- await stat44(candidate);
26503
+ await stat45(candidate);
26126
26504
  return true;
26127
26505
  } catch {
26128
26506
  return false;
26129
26507
  }
26130
26508
  }
26131
26509
  function isInside4(outer, inner) {
26132
- const relative = path70.relative(path70.resolve(outer), path70.resolve(inner));
26133
- return relative.length === 0 || !relative.startsWith("..") && !path70.isAbsolute(relative);
26510
+ const relative = path72.relative(path72.resolve(outer), path72.resolve(inner));
26511
+ return relative.length === 0 || !relative.startsWith("..") && !path72.isAbsolute(relative);
26134
26512
  }
26135
26513
  function displayCommand(args) {
26136
26514
  return ["git", ...args].map((arg) => /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg).join(" ");
@@ -26186,7 +26564,7 @@ var WorktreeService = class extends EventEmitter7 {
26186
26564
  /** La carpeta principal del repo de un proyecto o de una pestaña. */
26187
26565
  async mainRootOf(source) {
26188
26566
  const cwd = source.kind === "terminal" ? this.deps.registry.get(source.terminalId)?.cwd ?? null : this.deps.projectCwd(source.projectKey);
26189
- if (cwd === null || !await exists4(cwd)) throw new WorktreeError(serverText("worktreeProjectUnknown"));
26567
+ if (cwd === null || !await exists5(cwd)) throw new WorktreeError(serverText("worktreeProjectUnknown"));
26190
26568
  const layout = await repoLayout(cwd);
26191
26569
  if (layout === null || layout.mainRoot === null) throw new WorktreeError(serverText("worktreeNotRepo"));
26192
26570
  return layout.mainRoot;
@@ -26205,7 +26583,7 @@ var WorktreeService = class extends EventEmitter7 {
26205
26583
  return {
26206
26584
  mainRoot,
26207
26585
  parentDir: worktreesParentFor(mainRoot),
26208
- separator: path70.sep,
26586
+ separator: path72.sep,
26209
26587
  currentBranch: current,
26210
26588
  branches: branches.map((name) => ({ name, checkedOut: checkedOut.has(name) })),
26211
26589
  mainChanges: changes?.length ?? 0,
@@ -26227,9 +26605,9 @@ var WorktreeService = class extends EventEmitter7 {
26227
26605
  throw new WorktreeError(serverText("worktreeBranchInUse", { branch }));
26228
26606
  }
26229
26607
  const folder = worktreeFolderFor(mainRoot, branch);
26230
- if (await exists4(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
26608
+ if (await exists5(folder)) throw new WorktreeError(serverText("worktreeFolderExists", { folder }));
26231
26609
  const copy = await planCopy(mainRoot);
26232
- await mkdir22(path70.dirname(folder), { recursive: true });
26610
+ await mkdir22(path72.dirname(folder), { recursive: true });
26233
26611
  const added = await addWorktree(mainRoot, folder, branch, mode === "new" ? base : null);
26234
26612
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
26235
26613
  const { copied, copiedDirs } = await this.copyInto(mainRoot, folder, copy, request.copyNotes);
@@ -26274,13 +26652,13 @@ var WorktreeService = class extends EventEmitter7 {
26274
26652
  /** Las filas de "Ramas aparte" para la pestaña que pregunta. Vacío fuera de git. */
26275
26653
  async rows(terminalId) {
26276
26654
  const descriptor = this.deps.registry.get(terminalId);
26277
- if (descriptor === null || !await exists4(descriptor.cwd)) return [];
26655
+ if (descriptor === null || !await exists5(descriptor.cwd)) return [];
26278
26656
  const layout = await repoLayout(descriptor.cwd);
26279
26657
  if (layout === null || layout.mainRoot === null) return [];
26280
26658
  const mainRoot = layout.mainRoot;
26281
26659
  const tips = await branchTips(mainRoot);
26282
26660
  const tipsStamp = [...tips].map(([branch, commit]) => `${branch}\0${commit}`).join("\n");
26283
- const cacheKey2 = path70.resolve(mainRoot).toLowerCase();
26661
+ const cacheKey2 = path72.resolve(mainRoot).toLowerCase();
26284
26662
  const cached = this.rowsCache.get(cacheKey2);
26285
26663
  const fresh = cached !== void 0 && cached.tips === tipsStamp && this.now() - cached.at < ROWS_TTL_MS;
26286
26664
  const generation = this.rowsGeneration;
@@ -26289,7 +26667,12 @@ var WorktreeService = class extends EventEmitter7 {
26289
26667
  return rows.map((row) => ({ ...row, isCurrent: samePathShape(row.path, layout.toplevel) }));
26290
26668
  }
26291
26669
  async readRows(mainRoot, tips) {
26292
- const entries = (await listWorktrees(mainRoot)).filter((entry) => !entry.bare);
26670
+ const listed = await readWorktreeList(mainRoot);
26671
+ const entries = [
26672
+ ...(listed ?? []).filter((entry) => !entry.bare),
26673
+ // Las que git soltó a medias: sin su fila, no había cómo terminar de quitarlas.
26674
+ ...listed === null ? [] : await this.releasedEntries(mainRoot, listed)
26675
+ ];
26293
26676
  const mainBranch = entries.find((entry) => entry.isMain)?.branch ?? null;
26294
26677
  const read2 = await Promise.all(
26295
26678
  entries.map(async (entry) => {
@@ -26336,7 +26719,7 @@ var WorktreeService = class extends EventEmitter7 {
26336
26719
  const baseTip = tips.get(base);
26337
26720
  const branchTip = tips.get(branch);
26338
26721
  if (baseTip === void 0 || branchTip === void 0) return Promise.resolve(null);
26339
- const key = [path70.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
26722
+ const key = [path72.resolve(mainRoot).toLowerCase(), base, branch, baseTip, branchTip].join("\0");
26340
26723
  const cached = this.pairs.get(key);
26341
26724
  if (cached !== void 0) {
26342
26725
  this.pairs.delete(key);
@@ -26381,12 +26764,12 @@ var WorktreeService = class extends EventEmitter7 {
26381
26764
  /** Una rama aparte que existe en disco, para lo que la lee o le deja un pedido. */
26382
26765
  async presentRecordOrThrow(id) {
26383
26766
  const record = this.recordOrThrow(id);
26384
- if (!await exists4(record.path)) throw new WorktreeError(serverText("worktreeUnknown"));
26767
+ if (!await exists5(record.path)) throw new WorktreeError(serverText("worktreeUnknown"));
26385
26768
  return record;
26386
26769
  }
26387
26770
  /** Las pestañas y consolas abiertas dentro de la carpeta de la rama. */
26388
26771
  tabsIn(folder) {
26389
- return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path70.basename(tab.cwd) }));
26772
+ return this.deps.registry.list().filter((tab) => isInside4(folder, tab.cwd)).map((tab) => ({ terminalId: tab.terminalId, kind: tab.kind, label: tab.label || path72.basename(tab.cwd) }));
26390
26773
  }
26391
26774
  /**
26392
26775
  * Las pestañas de una carpeta de trabajo, sin las de otra que viva adentro
@@ -26399,17 +26782,70 @@ var WorktreeService = class extends EventEmitter7 {
26399
26782
  return !nested.some((inner) => isInside4(inner, cwd));
26400
26783
  });
26401
26784
  }
26785
+ /**
26786
+ * Los archivos nuevos que se pueden perder al quitar la rama (23-10-2026):
26787
+ * sólo si todo lo que no está commiteado es nuevo y git lo listó entero. null
26788
+ * si no; si git se va a negar a quitarla igual, bloqueada o con un submódulo
26789
+ * cargado (no se borra nada antes de saberlo); o si una entrada no se puede
26790
+ * borrar sin salir de la carpeta. Con algo adentro que la app copió de la
26791
+ * principal y la rama no ignora, no se ofrece (`copied`): no vuelve como una
26792
+ * nota, y se perdería lo que la rama le cambió.
26793
+ */
26794
+ async untrackedIn(record, changes) {
26795
+ const onlyNew = changes.complete && changes.paths.length > 0 && changes.untracked.length === changes.paths.length;
26796
+ if (!onlyNew) return null;
26797
+ const [entries, submodule] = await Promise.all([listWorktrees(record.mainRoot), hasLoadedSubmodule(record.path)]);
26798
+ if (submodule !== false || entries.some((entry) => entry.locked && samePathShape(entry.path, record.path))) return null;
26799
+ const entryOf = (file) => changes.untracked.some((entry) => {
26800
+ const clean = entry.replace(/\/+$/, "").toLowerCase();
26801
+ return file.toLowerCase() === clean || file.toLowerCase().startsWith(`${clean}/`);
26802
+ });
26803
+ const copied = [];
26804
+ for (const file of record.copied) {
26805
+ if (entryOf(file.path) && await exists5(path72.join(record.path, file.path))) copied.push(file.path);
26806
+ }
26807
+ if (copied.length > 0) {
26808
+ const ignored = await ignoredAmong(record.path, copied);
26809
+ if (ignored === null || copied.some((file) => !ignored.has(file))) return { files: 0, bytes: 0, stamp: null, blocked: "copied", targets: [] };
26810
+ }
26811
+ return scanUntracked(record.path, changes.untracked);
26812
+ }
26813
+ /**
26814
+ * Borra los archivos nuevos que el usuario eligió perder, si siguen siendo
26815
+ * los que vio: se mira justo antes, con las pestañas de la rama cerradas y
26816
+ * las notas ya de vuelta. Si desaparecieron solos, no hay nada que borrar.
26817
+ * Devuelve cuántos archivos se perdieron.
26818
+ */
26819
+ async discardNew(record, stamp2) {
26820
+ const changes = await uncommittedChanges(record.path);
26821
+ if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
26822
+ if (changes.paths.length === 0) return 0;
26823
+ const scan = await this.untrackedIn(record, changes);
26824
+ if (scan === null || scan.stamp === null || scan.stamp !== stamp2) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
26825
+ try {
26826
+ await deleteUntracked(scan.targets);
26827
+ } catch (error) {
26828
+ throw new WorktreeError(serverText("worktreeDiscardFailed", { detail: error instanceof Error ? error.message : String(error) }));
26829
+ }
26830
+ return scan.files;
26831
+ }
26402
26832
  async planFor(record) {
26403
- const present = await exists4(record.path);
26404
- const [uncommitted, notes, integrated] = await Promise.all([
26405
- present ? uncommittedPaths(record.path) : Promise.resolve([]),
26833
+ const onDisk = await exists5(record.path);
26834
+ const present = onDisk && !await this.releasedByGit(record);
26835
+ const [changes, notes, integrated] = await Promise.all([
26836
+ present ? uncommittedChanges(record.path) : Promise.resolve({ paths: [], untracked: [], complete: true }),
26406
26837
  present ? reviewNotes(record.mainRoot, record.path, record, this.loadNoteBase) : Promise.resolve({ returning: [], merging: [], resolved: [], conflicts: [] }),
26407
26838
  record.createdBranch ? isIntegrated(record.mainRoot, record.branch, record.base) : Promise.resolve(false)
26408
26839
  ]);
26409
- if (uncommitted === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
26840
+ if (changes === null) throw new WorktreeError(serverText("worktreeGitFailed", { detail: "git status" }));
26841
+ const uncommitted = changes.paths;
26842
+ const untracked = await this.untrackedIn(record, changes);
26410
26843
  const tabs = this.tabsIn(record.path);
26411
26844
  const deletesBranch = integrated === true;
26412
26845
  const branchUnused = deletesBranch && stillAtStart(record, await branchCommit(record.mainRoot, record.branch) ?? void 0);
26846
+ let inUse = !onDisk ? false : tabs.length > 0 ? null : await folderInUse(record.path);
26847
+ const holders = inUse === true ? await this.holdersOf(record.path) : [];
26848
+ if (inUse === true && holders.length === 0) inUse = await folderInUse(record.path);
26413
26849
  return {
26414
26850
  id: record.id,
26415
26851
  branch: record.branch,
@@ -26420,13 +26856,15 @@ var WorktreeService = class extends EventEmitter7 {
26420
26856
  ],
26421
26857
  uncommitted: uncommitted.slice(0, WORKTREE_PLAN_PATHS_MAX),
26422
26858
  uncommittedCount: uncommitted.length,
26859
+ // Sin las rutas absolutas de `targets`: al cliente le basta con lo que muestra y la huella.
26860
+ untracked: untracked === null ? null : { files: untracked.files, bytes: untracked.bytes, stamp: untracked.stamp, blocked: untracked.blocked },
26423
26861
  tabs,
26424
26862
  returning: notes.returning,
26425
26863
  merging: notes.merging,
26426
26864
  resolved: notes.resolved,
26427
26865
  conflicts: notes.conflicts,
26428
- // Con pestañas de la app adentro, la prueba daría "en uso" por ellas mismas: se mira al confirmar.
26429
- inUse: !present ? false : tabs.length > 0 ? null : await folderInUse(record.path),
26866
+ inUse,
26867
+ holders,
26430
26868
  deletesBranch,
26431
26869
  branchUnused,
26432
26870
  createdBranch: record.createdBranch
@@ -26439,34 +26877,46 @@ var WorktreeService = class extends EventEmitter7 {
26439
26877
  /**
26440
26878
  * Quita la rama aparte. Rehace el plan y se detiene en el primer "no" (ver
26441
26879
  * el principio del archivo). `closeTab` cierra una pestaña como lo hace el
26442
- * socket, soltando todo lo que la seguía.
26880
+ * socket, soltando todo lo que la seguía. `discardUntracked` es la huella de
26881
+ * los archivos nuevos que el usuario eligió perder (23-10-2026).
26443
26882
  */
26444
- async remove(id, discardConflicts, closeTab) {
26883
+ async remove(id, discardConflicts, closeTab, discardUntracked = null) {
26445
26884
  const record = this.recordOrThrow(id);
26446
26885
  const plan = await this.planFor(record);
26447
26886
  if (plan.uncommittedCount > 0) {
26448
- throw new WorktreeError(serverText("worktreeUncommitted", { count: plan.uncommittedCount }));
26887
+ if (plan.untracked === null || plan.untracked.stamp === null || discardUntracked === null) {
26888
+ throw new WorktreeError(serverText("worktreeUncommitted", { count: plan.uncommittedCount }));
26889
+ }
26890
+ if (plan.untracked.stamp !== discardUntracked) throw new WorktreeError(serverText("worktreeUntrackedChanged"));
26449
26891
  }
26450
26892
  if (plan.conflicts.length > 0 && !discardConflicts) {
26451
26893
  throw new WorktreeError(serverText("worktreeNotesConflict", { count: plan.conflicts.length }));
26452
26894
  }
26453
- if (plan.inUse === true) throw new WorktreeError(serverText("worktreeInUse"));
26895
+ if (plan.inUse === true) throw new WorktreeInUseError(plan);
26454
26896
  let returned = 0;
26455
- if (await exists4(record.path)) {
26897
+ let discardedFiles = 0;
26898
+ if (await exists5(record.path)) {
26456
26899
  for (const tab of plan.tabs) closeTab(tab.terminalId);
26457
- if (!await this.waitUntilFree(record.path)) throw new WorktreeError(serverText("worktreeInUse"));
26458
- const bases = new Map(record.copied.map((file) => [file.path, file.hash]));
26459
- try {
26460
- returned = (await returnNotes(record.mainRoot, record.path, plan.returning, bases, {
26461
- loadBase: this.loadNoteBase,
26462
- onWritten: (file, branch) => this.noteReturned(record.id, file, branch),
26463
- notesAsked: record.notesAsked
26464
- })).returned.length;
26465
- } catch (error) {
26466
- throw new WorktreeError(serverText("worktreeNotesFailed", { detail: error instanceof Error ? error.message : String(error) }));
26900
+ if (!await this.waitUntilFree(record.path)) {
26901
+ throw new WorktreeInUseError({ ...plan, tabs: [], inUse: true, holders: await this.holdersOf(record.path) });
26902
+ }
26903
+ if (!await this.releasedByGit(record)) {
26904
+ const bases = new Map(record.copied.map((file) => [file.path, file.hash]));
26905
+ try {
26906
+ returned = (await returnNotes(record.mainRoot, record.path, plan.returning, bases, {
26907
+ loadBase: this.loadNoteBase,
26908
+ onWritten: (file, branch) => this.noteReturned(record.id, file, branch),
26909
+ notesAsked: record.notesAsked
26910
+ })).returned.length;
26911
+ } catch (error) {
26912
+ throw new WorktreeError(serverText("worktreeNotesFailed", { detail: error instanceof Error ? error.message : String(error) }));
26913
+ }
26914
+ if (plan.uncommittedCount > 0) discardedFiles = await this.discardNew(record, discardUntracked);
26915
+ const removed = await removeWorktree(record.mainRoot, record.path);
26916
+ if (removed.code !== 0 && !await this.releasedByGit(record)) {
26917
+ throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
26918
+ }
26467
26919
  }
26468
- const removed = await removeWorktree(record.mainRoot, record.path);
26469
- if (removed.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(removed) }));
26470
26920
  await this.removeLeftovers(record);
26471
26921
  }
26472
26922
  const endCommit = await branchCommit(record.mainRoot, record.branch);
@@ -26474,7 +26924,15 @@ var WorktreeService = class extends EventEmitter7 {
26474
26924
  await this.deps.store.update(id, { removedAt: this.now(), endCommit: endCommit ?? record.endCommit });
26475
26925
  await this.pruneNoteBases();
26476
26926
  this.changed();
26477
- return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0 };
26927
+ return { branchDeleted, returned, discarded: discardConflicts ? plan.conflicts.length : 0, discardedFiles };
26928
+ }
26929
+ /** Los programas que tienen la carpeta, para el diálogo. Sin saberlo, ninguno: el diálogo dice lo de antes. */
26930
+ async holdersOf(folder) {
26931
+ try {
26932
+ return await (this.deps.folderHolders ?? folderHolders)(folder);
26933
+ } catch {
26934
+ return [];
26935
+ }
26478
26936
  }
26479
26937
  /** Espera, con tope, a que ningún proceso tenga la carpeta. */
26480
26938
  async waitUntilFree(folder) {
@@ -26492,16 +26950,40 @@ var WorktreeService = class extends EventEmitter7 {
26492
26950
  * contenga, ni una carpeta que git todavía tenga registrada.
26493
26951
  */
26494
26952
  async removeLeftovers(record) {
26495
- const folder = path70.resolve(record.path);
26496
- if (!await exists4(folder)) return this.removeEmptyParent(folder);
26497
- if (isInside4(folder, record.mainRoot) || path70.dirname(folder) === folder) return;
26953
+ const folder = path72.resolve(record.path);
26954
+ if (!await exists5(folder)) return this.removeEmptyParent(folder);
26955
+ if (isInside4(folder, record.mainRoot) || path72.dirname(folder) === folder) return;
26498
26956
  if ((await listWorktrees(record.mainRoot)).some((entry) => samePathShape(entry.path, folder))) return;
26499
- await rm9(folder, { recursive: true, maxRetries: 3 });
26957
+ await rm10(folder, { recursive: true, maxRetries: 3 });
26500
26958
  await this.removeEmptyParent(folder);
26501
26959
  }
26960
+ /**
26961
+ * true si git ya soltó la rama sin terminar de borrar su carpeta
26962
+ * (23-10-2026). `worktree remove` borra después de sus controles y, si algo
26963
+ * no se deja borrar a mitad de camino, quita su registro igual (medido con git
26964
+ * 2.56: una ruta de más de 260 caracteres sin `core.longpaths`, o un nombre
26965
+ * terminado en punto). Lo que quedó ya pasó esos controles, y lo termina de
26966
+ * borrar la app. Sólo sin `.git` adentro, y si git contesta que no la tiene:
26967
+ * una carpeta con su `.git` puede tener trabajo aunque git ya no la conozca.
26968
+ */
26969
+ async releasedByGit(record) {
26970
+ if (!await leftWithoutGit(record.path)) return false;
26971
+ const entries = await readWorktreeList(record.mainRoot);
26972
+ return entries !== null && !entries.some((entry) => samePathShape(entry.path, record.path));
26973
+ }
26974
+ /** Las ramas de la app que git soltó a medias (`releasedByGit`), como si git las listara. */
26975
+ async releasedEntries(mainRoot, listed) {
26976
+ const released = [];
26977
+ for (const record of this.deps.store.list()) {
26978
+ if (record.removedAt !== null || !samePathShape(record.mainRoot, mainRoot)) continue;
26979
+ if (listed.some((entry) => samePathShape(entry.path, record.path)) || !await leftWithoutGit(record.path)) continue;
26980
+ released.push({ path: path72.resolve(record.path), branch: record.branch, isMain: false, bare: false, locked: false, prunable: true });
26981
+ }
26982
+ return released;
26983
+ }
26502
26984
  /** La carpeta `<proyecto>.worktrees`, sólo si quedó vacía. */
26503
26985
  async removeEmptyParent(folder) {
26504
- await rmdir(path70.dirname(folder)).catch(() => void 0);
26986
+ await rmdir(path72.dirname(folder)).catch(() => void 0);
26505
26987
  }
26506
26988
  /**
26507
26989
  * Vuelve a abrir la rama de una sesión cuya carpeta se quitó: la saca otra
@@ -26513,7 +26995,7 @@ var WorktreeService = class extends EventEmitter7 {
26513
26995
  const session = this.deps.findSession(agent, sessionId);
26514
26996
  if (session === null || session.cwd.length === 0) throw new WorktreeError(serverText("worktreeUnknown"));
26515
26997
  const record = this.removedRecordAbove(session.cwd);
26516
- if (record === null || await exists4(record.path) || !await exists4(record.mainRoot)) {
26998
+ if (record === null || await exists5(record.path) || !await exists5(record.mainRoot)) {
26517
26999
  throw new WorktreeError(serverText("worktreeUnknown"));
26518
27000
  }
26519
27001
  const branchExists = await branchCommit(record.mainRoot, record.branch) !== null;
@@ -26525,7 +27007,7 @@ var WorktreeService = class extends EventEmitter7 {
26525
27007
  throw new WorktreeError(serverText("worktreeBranchInUse", { branch: record.branch }));
26526
27008
  }
26527
27009
  const copy = await planCopy(record.mainRoot);
26528
- await mkdir22(path70.dirname(record.path), { recursive: true });
27010
+ await mkdir22(path72.dirname(record.path), { recursive: true });
26529
27011
  const added = await addWorktree(record.mainRoot, record.path, record.branch, recreateAt);
26530
27012
  if (added.code !== 0) throw new WorktreeError(serverText("worktreeGitFailed", { detail: gitFailureText(added) }));
26531
27013
  const { copied, copiedDirs } = await this.copyInto(record.mainRoot, record.path, copy, record.copyNotes);
@@ -26541,7 +27023,7 @@ var WorktreeService = class extends EventEmitter7 {
26541
27023
  */
26542
27024
  async keptBranches(terminalId) {
26543
27025
  const descriptor = this.deps.registry.get(terminalId);
26544
- if (descriptor === null || !await exists4(descriptor.cwd)) return [];
27026
+ if (descriptor === null || !await exists5(descriptor.cwd)) return [];
26545
27027
  const layout = await repoLayout(descriptor.cwd);
26546
27028
  if (layout === null || layout.mainRoot === null) return [];
26547
27029
  const mainRoot = layout.mainRoot;
@@ -26612,11 +27094,11 @@ var WorktreeService = class extends EventEmitter7 {
26612
27094
  return { branch: plan.branch };
26613
27095
  }
26614
27096
  removedRecordAbove(cwd) {
26615
- let current = path70.resolve(cwd);
27097
+ let current = path72.resolve(cwd);
26616
27098
  for (let depth = 0; depth < 24; depth += 1) {
26617
27099
  const record = this.deps.store.byPath(current);
26618
27100
  if (record !== null) return record.removedAt === null ? null : record;
26619
- const parent = path70.dirname(current);
27101
+ const parent = path72.dirname(current);
26620
27102
  if (parent === current) return null;
26621
27103
  current = parent;
26622
27104
  }
@@ -26728,7 +27210,7 @@ var WorktreeService = class extends EventEmitter7 {
26728
27210
  const open9 = this.authorTab(record);
26729
27211
  if (open9 !== null) return { terminal: open9, opened: false };
26730
27212
  const session = this.authorSession(record);
26731
- if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists4(session.cwd)) {
27213
+ if (session !== null && isAgentId(session.agent) && session.cwd.length > 0 && await exists5(session.cwd)) {
26732
27214
  const terminal = await this.deps.registry.open({ cwd: session.cwd, agent: session.agent, resumeSessionId: session.sessionId });
26733
27215
  return { terminal, opened: true };
26734
27216
  }
@@ -26945,11 +27427,11 @@ var WorktreeService = class extends EventEmitter7 {
26945
27427
  const baseEntry = entries.find((entry) => !entry.bare && !entry.prunable && entry.branch === record.base) ?? null;
26946
27428
  const [baseChanges, branchChanges, target] = await Promise.all([
26947
27429
  baseEntry === null ? Promise.resolve([]) : uncommittedPaths(baseEntry.path),
26948
- await exists4(record.path) ? uncommittedPaths(record.path) : Promise.resolve([]),
27430
+ await exists5(record.path) ? uncommittedPaths(record.path) : Promise.resolve([]),
26949
27431
  baseEntry === null ? Promise.resolve(`refs/heads/${record.branch}`) : mergeTarget(baseEntry.path, record.branch)
26950
27432
  ]);
26951
27433
  const localConflicts = pathsTouching(baseChanges ?? [], files);
26952
- const notes = baseEntry?.isMain === true && await exists4(record.path) ? await reviewNotes(mainRoot, record.path, record, this.loadNoteBase) : null;
27434
+ const notes = baseEntry?.isMain === true && await exists5(record.path) ? await reviewNotes(mainRoot, record.path, record, this.loadNoteBase) : null;
26953
27435
  const blocker = pair.ahead === 0 ? "nothing" : pair.behind > 0 ? "behind" : baseEntry === null ? "no-base-folder" : localConflicts.length > 0 ? "local-changes" : null;
26954
27436
  const plan = {
26955
27437
  id: record.id,
@@ -28644,17 +29126,24 @@ ${seen.join("\n-----\n")}`);
28644
29126
  })();
28645
29127
  break;
28646
29128
  /*
28647
- Cuales de esas rutas son archivos del proyecto (§6.33): lo que el
28648
- hilo vuelve enlace. Sin error para ningun caso —una pestana que ya no
28649
- esta, una ruta que cae fuera—: nada existe, y el hilo queda como
29129
+ Cuales de esas rutas son archivos o carpetas del proyecto (§6.33): lo
29130
+ que el hilo vuelve enlace. Sin error para ningun caso (una pestana que
29131
+ ya no esta, una ruta que cae fuera): nada existe, y el hilo queda como
28650
29132
  texto. Un cartel por una ruta que escribio el agente no le dice nada
28651
29133
  al usuario.
28652
29134
  */
28653
29135
  case "files.check":
28654
29136
  void (async () => {
28655
29137
  const cwd = cwdOf(message.terminalId);
28656
- const existing = cwd === null ? [] : await existingFiles(cwd, message.paths, await folders).catch(() => []);
28657
- send(socket, { type: "files.checked", terminalId: message.terminalId, requestId: message.requestId, existing });
29138
+ const none = { files: [], directories: [] };
29139
+ const { files, directories } = cwd === null ? none : await existingPaths(cwd, message.paths, await folders).catch(() => none);
29140
+ send(socket, {
29141
+ type: "files.checked",
29142
+ terminalId: message.terminalId,
29143
+ requestId: message.requestId,
29144
+ existing: files,
29145
+ directories
29146
+ });
28658
29147
  })();
28659
29148
  break;
28660
29149
  case "files.reveal":
@@ -29024,9 +29513,11 @@ ${seen.join("\n-----\n")}`);
29024
29513
  }
29025
29514
  case "worktree.remove": {
29026
29515
  const { requestId, worktreeId, discardConflicts } = message;
29027
- void worktrees.remove(worktreeId, discardConflicts, (terminalId) => void dropTab(terminalId)).then(
29516
+ const closeTab = (terminalId) => void dropTab(terminalId);
29517
+ void worktrees.remove(worktreeId, discardConflicts, closeTab, message.discardUntracked ?? null).then(
29028
29518
  (outcome) => send(socket, { type: "worktree.removed", requestId, worktreeId, outcome }),
29029
- (error) => sendWorktreeError(socket, error, requestId)
29519
+ // Otro programa tiene la carpeta: el plan otra vez, que dice cuál (23-10-2026).
29520
+ (error) => error instanceof WorktreeInUseError ? send(socket, { type: "worktree.removePlan", requestId, plan: error.plan }) : sendWorktreeError(socket, error, requestId)
29030
29521
  );
29031
29522
  break;
29032
29523
  }
@@ -29314,8 +29805,8 @@ ${seen.join("\n-----\n")}`);
29314
29805
  }
29315
29806
 
29316
29807
  // packages/server/src/vault/catalog.ts
29317
- import { open as open8, readdir as readdir21, stat as stat45 } from "node:fs/promises";
29318
- import path71 from "node:path";
29808
+ import { open as open8, readdir as readdir22, stat as stat46 } from "node:fs/promises";
29809
+ import path73 from "node:path";
29319
29810
  import { createInterface as createInterface4 } from "node:readline";
29320
29811
  var HEADER_MAX_BYTES = 64 * 1024;
29321
29812
  var LOAD_CONCURRENCY = 16;
@@ -29327,7 +29818,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29327
29818
  const folder = assetsDir(dir, agent, sessionId);
29328
29819
  let names;
29329
29820
  try {
29330
- names = await readdir21(folder);
29821
+ names = await readdir22(folder);
29331
29822
  } catch {
29332
29823
  return 0;
29333
29824
  }
@@ -29335,7 +29826,7 @@ async function assetBytesOf(dir, agent, sessionId) {
29335
29826
  for (const name of names) {
29336
29827
  if (!VAULT_ASSET_NAME_PATTERN.test(name)) continue;
29337
29828
  try {
29338
- const info = await stat45(path71.join(folder, name));
29829
+ const info = await stat46(path73.join(folder, name));
29339
29830
  if (info.isFile()) total += info.size;
29340
29831
  } catch {
29341
29832
  }
@@ -29346,7 +29837,7 @@ async function readEntry2(dir, agent, sessionId) {
29346
29837
  const file = sessionFile(dir, agent, sessionId);
29347
29838
  let sizeBytes;
29348
29839
  try {
29349
- const info = await stat45(file);
29840
+ const info = await stat46(file);
29350
29841
  if (!info.isFile()) return { kind: "absent" };
29351
29842
  sizeBytes = info.size;
29352
29843
  } catch {
@@ -29403,7 +29894,7 @@ var VaultCatalog = class {
29403
29894
  const keys = [];
29404
29895
  let agentFolders = [];
29405
29896
  try {
29406
- agentFolders = await readdir21(sessionsRoot2(dir));
29897
+ agentFolders = await readdir22(sessionsRoot2(dir));
29407
29898
  } catch {
29408
29899
  agentFolders = [];
29409
29900
  }
@@ -29411,7 +29902,7 @@ var VaultCatalog = class {
29411
29902
  if (!isSessionAgentId2(folder)) continue;
29412
29903
  let names;
29413
29904
  try {
29414
- names = await readdir21(agentSessionsDir(dir, folder));
29905
+ names = await readdir22(agentSessionsDir(dir, folder));
29415
29906
  } catch {
29416
29907
  continue;
29417
29908
  }
@@ -29555,22 +30046,22 @@ var VaultCatalog = class {
29555
30046
  };
29556
30047
 
29557
30048
  // packages/server/src/worktrees/links.ts
29558
- import { stat as stat46 } from "node:fs/promises";
29559
- import path72 from "node:path";
29560
- async function exists5(candidate) {
30049
+ import { stat as stat47 } from "node:fs/promises";
30050
+ import path74 from "node:path";
30051
+ async function exists6(candidate) {
29561
30052
  try {
29562
- await stat46(candidate);
30053
+ await stat47(candidate);
29563
30054
  return true;
29564
30055
  } catch {
29565
30056
  return false;
29566
30057
  }
29567
30058
  }
29568
30059
  function recordAbove(registry, cwd) {
29569
- let current = path72.resolve(cwd);
30060
+ let current = path74.resolve(cwd);
29570
30061
  for (let depth = 0; depth < 24; depth += 1) {
29571
30062
  const record = registry.byPath(current);
29572
- if (record !== null) return { record, inner: path72.relative(current, path72.resolve(cwd)) };
29573
- const parent = path72.dirname(current);
30063
+ if (record !== null) return { record, inner: path74.relative(current, path74.resolve(cwd)) };
30064
+ const parent = path74.dirname(current);
29574
30065
  if (parent === current) return null;
29575
30066
  current = parent;
29576
30067
  }
@@ -29584,14 +30075,14 @@ var WorktreeLinks = class {
29584
30075
  /** La rama aparte de una carpeta del historial, o null si es la de un proyecto (o no es de git). */
29585
30076
  async sessionLink(cwd) {
29586
30077
  if (cwd.length === 0) return null;
29587
- if (await exists5(cwd)) {
30078
+ if (await exists6(cwd)) {
29588
30079
  const checkout = await readCheckout(cwd);
29589
30080
  if (checkout === null || !checkout.linked) return null;
29590
30081
  const record = this.registry.byPath(checkout.root);
29591
30082
  return {
29592
30083
  mainCwd: mainCwdFor(cwd, checkout),
29593
30084
  worktree: {
29594
- branch: checkout.branch ?? record?.branch ?? path72.basename(checkout.root),
30085
+ branch: checkout.branch ?? record?.branch ?? path74.basename(checkout.root),
29595
30086
  exists: true,
29596
30087
  reopenable: false
29597
30088
  }
@@ -29601,13 +30092,13 @@ var WorktreeLinks = class {
29601
30092
  if (found !== null) {
29602
30093
  const { record, inner } = found;
29603
30094
  return {
29604
- mainCwd: inner.length === 0 ? record.mainRoot : path72.join(record.mainRoot, inner),
30095
+ mainCwd: inner.length === 0 ? record.mainRoot : path74.join(record.mainRoot, inner),
29605
30096
  worktree: { branch: record.branch, exists: false, reopenable: await this.reopenable(record) }
29606
30097
  };
29607
30098
  }
29608
- const mainRoot = mainRootFromShape(path72.resolve(cwd));
30099
+ const mainRoot = mainRootFromShape(path74.resolve(cwd));
29609
30100
  if (mainRoot === null) return null;
29610
- return { mainCwd: mainRoot, worktree: { branch: path72.basename(cwd), exists: false, reopenable: false } };
30101
+ return { mainCwd: mainRoot, worktree: { branch: path74.basename(cwd), exists: false, reopenable: false } };
29611
30102
  }
29612
30103
  /**
29613
30104
  * Se puede volver a abrir una rama quitada si la creó la app, su carpeta no
@@ -29616,7 +30107,7 @@ var WorktreeLinks = class {
29616
30107
  */
29617
30108
  async reopenable(record) {
29618
30109
  if (record.removedAt === null) return false;
29619
- if (await exists5(record.path) || !await exists5(record.mainRoot)) return false;
30110
+ if (await exists6(record.path) || !await exists6(record.mainRoot)) return false;
29620
30111
  try {
29621
30112
  if (await branchCommit(record.mainRoot, record.branch) !== null) return true;
29622
30113
  return record.endCommit !== null && await commitExists(record.mainRoot, record.endCommit);
@@ -29637,15 +30128,15 @@ var WorktreeLinks = class {
29637
30128
  };
29638
30129
 
29639
30130
  // packages/server/src/index.ts
29640
- var serverDir = path73.dirname(fileURLToPath2(import.meta.url));
30131
+ var serverDir = path75.dirname(fileURLToPath2(import.meta.url));
29641
30132
  var isPackaged = true;
29642
- var webRoot = isPackaged ? serverDir : path73.resolve(serverDir, "../../web");
29643
- var webDist = isPackaged ? path73.join(serverDir, "web") : path73.join(webRoot, "dist");
29644
- var repoRoot = path73.resolve(serverDir, "../../..");
30133
+ var webRoot = isPackaged ? serverDir : path75.resolve(serverDir, "../../web");
30134
+ var webDist = isPackaged ? path75.join(serverDir, "web") : path75.join(webRoot, "dist");
30135
+ var repoRoot = path75.resolve(serverDir, "../../..");
29645
30136
  var isProduction = isPackaged || process.argv.includes("--prod");
29646
30137
  function resolveDefaultCwd() {
29647
30138
  const override = process.env["AGENT_WORKBENCH_CWD"];
29648
- if (override !== void 0 && override.length > 0) return path73.resolve(override);
30139
+ if (override !== void 0 && override.length > 0) return path75.resolve(override);
29649
30140
  if (isPackaged) return process.cwd();
29650
30141
  return repoRoot;
29651
30142
  }
@@ -29720,7 +30211,7 @@ async function mountDevUi(app, httpServer) {
29720
30211
  }
29721
30212
  var LOOKS_LIKE_FILE = /\.[a-zA-Z0-9]{1,8}$/;
29722
30213
  function mountBuiltUi(app) {
29723
- const indexHtml = path73.join(webDist, "index.html");
30214
+ const indexHtml = path75.join(webDist, "index.html");
29724
30215
  if (!existsSync4(indexHtml)) {
29725
30216
  throw new Error(
29726
30217
  `There's no compiled interface in ${webDist}.
@@ -29808,11 +30299,11 @@ async function main() {
29808
30299
  findSession: (agent, sessionId) => index.find(agent, sessionId),
29809
30300
  // Hito 49: las conversaciones de una rama (para dejarle un pedido a su autor) y la respuesta del revisor.
29810
30301
  sessionsUnder: (folder) => {
29811
- const root = path73.resolve(folder);
30302
+ const root = path75.resolve(folder);
29812
30303
  return index.getProjects().flatMap((project) => project.sessions).filter((session) => {
29813
30304
  if (session.archived || session.cwd.length === 0) return false;
29814
- const relative = path73.relative(root, path73.resolve(session.cwd));
29815
- return relative.length === 0 || !relative.startsWith("..") && !path73.isAbsolute(relative);
30305
+ const relative = path75.relative(root, path75.resolve(session.cwd));
30306
+ return relative.length === 0 || !relative.startsWith("..") && !path75.isAbsolute(relative);
29816
30307
  });
29817
30308
  },
29818
30309
  history: (agent) => agents.get(agent)?.adapter.history ?? null