@staff0rd/assist 0.437.0 → 0.438.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.437.0",
9
+ version: "0.438.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -98,6 +98,7 @@ var package_default = {
98
98
  "@types/pg-copy-streams": "^1.2.5",
99
99
  "@types/react": "^19.2.14",
100
100
  "@types/react-dom": "^19.2.3",
101
+ "@types/react-file-icon": "^1.0.5",
101
102
  "@types/semver": "^7.7.1",
102
103
  "@types/shell-quote": "^1.7.5",
103
104
  "@types/ws": "^8.18.1",
@@ -110,6 +111,7 @@ var package_default = {
110
111
  oxlint: "^1.69.0",
111
112
  react: "^19.2.4",
112
113
  "react-dom": "^19.2.4",
114
+ "react-file-icon": "^1.6.0",
113
115
  "react-router": "^7.14.0",
114
116
  "semantic-release": "^25.0.2",
115
117
  skott: "^0.35.11",
@@ -4101,12 +4103,12 @@ function getChangedFiles() {
4101
4103
  function filterByChangedFiles(entries) {
4102
4104
  const hasFilters = entries.some((e) => e.filter);
4103
4105
  if (!hasFilters) return entries;
4104
- const changedFiles = getChangedFiles();
4106
+ const changedFiles2 = getChangedFiles();
4105
4107
  return entries.filter((entry) => {
4106
4108
  const { filter } = entry;
4107
4109
  if (!filter) return true;
4108
- if (changedFiles.length === 0) return false;
4109
- return changedFiles.some((file) => minimatch4(file, filter));
4110
+ if (changedFiles2.length === 0) return false;
4111
+ return changedFiles2.some((file) => minimatch4(file, filter));
4110
4112
  });
4111
4113
  }
4112
4114
 
@@ -6380,10 +6382,10 @@ async function prepareRun(id, resumeSessionId) {
6380
6382
  import chalk39 from "chalk";
6381
6383
  function reportDuplicateRun(itemId2, holder) {
6382
6384
  const since = holder.timestamp ? `, started ${holder.timestamp}` : "";
6383
- const reason = `another session is already running it (pid ${holder.pid}${since}); close that session before running it again`;
6385
+ const reason2 = `another session is already running it (pid ${holder.pid}${since}); close that session before running it again`;
6384
6386
  console.log(chalk39.yellow(`Already running: ${formatItemId(itemId2)}`));
6385
- console.log(chalk39.dim(`Refusing to start a second run \u2014 ${reason}.`));
6386
- appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${reason}`);
6387
+ console.log(chalk39.dim(`Refusing to start a second run \u2014 ${reason2}.`));
6388
+ appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${reason2}`);
6387
6389
  }
6388
6390
 
6389
6391
  // src/commands/backlog/consumePause.ts
@@ -8030,11 +8032,11 @@ import {
8030
8032
  var SPAWN_TIMEOUT_MS = 1e4;
8031
8033
  var RETRY_DELAY_MS2 = 200;
8032
8034
  var STALE_LOCK_MS = SPAWN_TIMEOUT_MS + 5e3;
8033
- async function ensureDaemonRunning(reason = "unspecified") {
8035
+ async function ensureDaemonRunning(reason2 = "unspecified") {
8034
8036
  if (await isDaemonRunning()) return;
8035
8037
  mkdirSync10(daemonPaths.dir, { recursive: true });
8036
8038
  const holdsLock = acquireSpawnLock();
8037
- if (holdsLock) spawnDaemon(reason);
8039
+ if (holdsLock) spawnDaemon(reason2);
8038
8040
  try {
8039
8041
  await waitForDaemon();
8040
8042
  } finally {
@@ -8083,12 +8085,12 @@ function releaseSpawnLock() {
8083
8085
  } catch {
8084
8086
  }
8085
8087
  }
8086
- function spawnDaemon(reason) {
8088
+ function spawnDaemon(reason2) {
8087
8089
  const log = openSync2(daemonPaths.log, "a");
8088
8090
  const child = spawn4(process.execPath, [process.argv[1], "daemon", "run"], {
8089
8091
  detached: true,
8090
8092
  stdio: ["ignore", log, log],
8091
- env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON: reason }
8093
+ env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON: reason2 }
8092
8094
  });
8093
8095
  child.unref();
8094
8096
  }
@@ -8485,7 +8487,7 @@ async function patchSubtaskStatus(req, res, id, idx) {
8485
8487
  }
8486
8488
 
8487
8489
  // src/commands/backlog/rewindItemToPhase.ts
8488
- async function rewindItemToPhase(orm, item, phaseNumber, reason) {
8490
+ async function rewindItemToPhase(orm, item, phaseNumber, reason2) {
8489
8491
  const plan2 = resolveRewindPlan(item);
8490
8492
  if (phaseNumber < 1 || phaseNumber > plan2.length) {
8491
8493
  return {
@@ -8504,7 +8506,7 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason) {
8504
8506
  await appendComment(
8505
8507
  orm,
8506
8508
  item.id,
8507
- `Rewound to phase ${phaseNumber} (${phaseName}): ${reason}`,
8509
+ `Rewound to phase ${phaseNumber} (${phaseName}): ${reason2}`,
8508
8510
  { phase: phaseNumber }
8509
8511
  );
8510
8512
  await updateCurrentPhase(orm, item.id, phaseNumber);
@@ -8515,11 +8517,11 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason) {
8515
8517
 
8516
8518
  // src/commands/backlog/web/rewindItemPhase.ts
8517
8519
  async function rewindItemPhase(req, res, id) {
8518
- const { phase, reason } = await parseRewindBody(req);
8520
+ const { phase, reason: reason2 } = await parseRewindBody(req);
8519
8521
  const result = await findItemOr404(res, id);
8520
8522
  if (!result) return;
8521
8523
  const { orm, item } = result;
8522
- const outcome = await rewindItemToPhase(orm, item, phase, reason);
8524
+ const outcome = await rewindItemToPhase(orm, item, phase, reason2);
8523
8525
  if (!outcome.ok) {
8524
8526
  respondJson(res, 400, { error: outcome.error });
8525
8527
  return;
@@ -8977,6 +8979,85 @@ function jiraSite(_req, res) {
8977
8979
  respondJson(res, 200, { site: config.site ?? null });
8978
8980
  }
8979
8981
 
8982
+ // src/commands/sessions/web/scoreFilePath.ts
8983
+ var BASENAME_WEIGHT = 1e3;
8984
+ var START_BONUS = 15;
8985
+ var BOUNDARY_BONUS = 10;
8986
+ var CONSECUTIVE_BONUS = 8;
8987
+ var MAX_GAP_PENALTY = 6;
8988
+ var MAX_LENGTH_PENALTY = 100;
8989
+ function isBoundary(text17, index3) {
8990
+ if (index3 === 0) return true;
8991
+ const previous = text17[index3 - 1] ?? "";
8992
+ if (!/[A-Za-z0-9]/.test(previous)) return true;
8993
+ return /[a-z0-9]/.test(previous) && /[A-Z]/.test(text17[index3] ?? "");
8994
+ }
8995
+ function subsequenceScore(text17, query) {
8996
+ const lower = text17.toLowerCase();
8997
+ let score = 0;
8998
+ let cursor = 0;
8999
+ let previous = -1;
9000
+ for (const char of query) {
9001
+ const at2 = lower.indexOf(char, cursor);
9002
+ if (at2 === -1) return null;
9003
+ if (at2 === previous + 1) score += CONSECUTIVE_BONUS;
9004
+ if (isBoundary(text17, at2)) score += BOUNDARY_BONUS;
9005
+ if (at2 === 0) score += START_BONUS;
9006
+ score -= Math.min(at2 - cursor, MAX_GAP_PENALTY);
9007
+ previous = at2;
9008
+ cursor = at2 + 1;
9009
+ }
9010
+ return score - Math.min(text17.length, MAX_LENGTH_PENALTY);
9011
+ }
9012
+ function scoreFilePath(path71, query) {
9013
+ const needle = query.trim().toLowerCase();
9014
+ if (!needle) return 0;
9015
+ const basename18 = path71.slice(path71.lastIndexOf("/") + 1);
9016
+ const inBasename = subsequenceScore(basename18, needle);
9017
+ if (inBasename !== null) return BASENAME_WEIGHT + inBasename;
9018
+ return subsequenceScore(path71, needle);
9019
+ }
9020
+
9021
+ // src/commands/sessions/web/rankFilePaths.ts
9022
+ function rankFilePaths(paths, query, limit) {
9023
+ const scored = [];
9024
+ for (const path71 of paths) {
9025
+ const score = scoreFilePath(path71, query);
9026
+ if (score !== null) scored.push({ path: path71, score });
9027
+ }
9028
+ scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
9029
+ return scored.slice(0, limit).map((entry) => entry.path);
9030
+ }
9031
+
9032
+ // src/commands/sessions/web/listFiles.ts
9033
+ var MAX_RESULTS = 20;
9034
+ var CACHE_TTL_MS = 5e3;
9035
+ var LS_FILES_MAX_BUFFER = 32 * 1024 * 1024;
9036
+ var fileListCache = /* @__PURE__ */ new Map();
9037
+ async function repoFiles(cwd) {
9038
+ const cached = fileListCache.get(cwd);
9039
+ if (cached && cached.expires > Date.now()) return cached.files;
9040
+ const output = await execGit(
9041
+ cwd,
9042
+ ["ls-files", "--cached", "--others", "--exclude-standard"],
9043
+ { maxBuffer: LS_FILES_MAX_BUFFER }
9044
+ );
9045
+ const files = [...new Set(output.split(/\r?\n/).filter(Boolean))];
9046
+ fileListCache.set(cwd, { files, expires: Date.now() + CACHE_TTL_MS });
9047
+ return files;
9048
+ }
9049
+ async function listFiles(req, res) {
9050
+ const cwd = getCwdParam(req, res);
9051
+ if (!cwd) return;
9052
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams.get("q") ?? "";
9053
+ try {
9054
+ const files = await repoFiles(cwd);
9055
+ respondJson(res, 200, { files: rankFilePaths(files, query, MAX_RESULTS) });
9056
+ } catch {
9057
+ respondJson(res, 500, { error: "Failed to list files" });
9058
+ }
9059
+ }
9060
+
8980
9061
  // src/commands/news/shared.ts
8981
9062
  import { decodeHTML } from "entities";
8982
9063
  function extractText(xml, tag) {
@@ -9173,7 +9254,7 @@ async function openInCode(req, res) {
9173
9254
  import { execFile as execFile4 } from "child_process";
9174
9255
  import { promisify as promisify5 } from "util";
9175
9256
  var execFileAsync4 = promisify5(execFile4);
9176
- var CACHE_TTL_MS = 3e4;
9257
+ var CACHE_TTL_MS2 = 3e4;
9177
9258
  function createCachedGhJson(args, parse3, fallback, options2 = {}) {
9178
9259
  const { cacheFallback = true } = options2;
9179
9260
  const cache2 = /* @__PURE__ */ new Map();
@@ -9193,7 +9274,7 @@ function createCachedGhJson(args, parse3, fallback, options2 = {}) {
9193
9274
  value = fallback;
9194
9275
  }
9195
9276
  if (cacheFallback || value !== fallback)
9196
- cache2.set(key, { value, expires: now + CACHE_TTL_MS });
9277
+ cache2.set(key, { value, expires: now + CACHE_TTL_MS2 });
9197
9278
  return value;
9198
9279
  };
9199
9280
  }
@@ -9561,6 +9642,7 @@ var routes2 = {
9561
9642
  "GET /api/git-status": gitStatus,
9562
9643
  "GET /api/diff": diff2,
9563
9644
  "GET /api/file": fileContent,
9645
+ "GET /api/files": listFiles,
9564
9646
  "GET /api/jira-site": jiraSite,
9565
9647
  "GET /api/harness": harnessCapabilities,
9566
9648
  "GET /api/pr-status": prStatus,
@@ -10364,8 +10446,8 @@ function countCopyRows(data) {
10364
10446
 
10365
10447
  // src/commands/backlog/dump/parseDump.ts
10366
10448
  var NEWLINE = 10;
10367
- function invalid(reason) {
10368
- throw new Error(`Invalid dump: ${reason}`);
10449
+ function invalid(reason2) {
10450
+ throw new Error(`Invalid dump: ${reason2}`);
10369
10451
  }
10370
10452
  function readLine(dump, start3) {
10371
10453
  const eol = dump.indexOf(NEWLINE, start3);
@@ -11462,14 +11544,14 @@ async function unstar(id) {
11462
11544
 
11463
11545
  // src/commands/backlog/wontdo/index.ts
11464
11546
  import chalk96 from "chalk";
11465
- async function wontdo(id, reason) {
11547
+ async function wontdo(id, reason2) {
11466
11548
  const found = await findOneItem(id);
11467
11549
  if (!found) return;
11468
11550
  const { orm, item } = found;
11469
11551
  await updateStatus(orm, item.id, "wontdo");
11470
- if (reason) {
11552
+ if (reason2) {
11471
11553
  const phase = item.currentPhase ?? 1;
11472
- await appendComment(orm, item.id, reason, { phase, type: "summary" });
11554
+ await appendComment(orm, item.id, reason2, { phase, type: "summary" });
11473
11555
  }
11474
11556
  console.log(
11475
11557
  chalk96.red(`Won't do item ${formatItemId(item.id)}: ${item.name}`)
@@ -12795,9 +12877,9 @@ function resolvePermission(toolName, parts) {
12795
12877
  if (denied) return denied;
12796
12878
  const reasons = [];
12797
12879
  for (const part of parts) {
12798
- const reason = isApprovedRead(part, toolName);
12799
- if (!reason) return void 0;
12800
- reasons.push(reason);
12880
+ const reason2 = isApprovedRead(part, toolName);
12881
+ if (!reason2) return void 0;
12882
+ reasons.push(reason2);
12801
12883
  }
12802
12884
  return {
12803
12885
  permissionDecision: "allow",
@@ -12921,13 +13003,13 @@ function cliHookCheck(command, toolName = "Bash") {
12921
13003
  if (reportDeny(toolName, parts)) return;
12922
13004
  const reasons = [];
12923
13005
  for (const part of parts) {
12924
- const reason = isApprovedRead(part, toolName);
12925
- if (!reason) {
13006
+ const reason2 = isApprovedRead(part, toolName);
13007
+ if (!reason2) {
12926
13008
  console.log(`not approved (unrecognised: ${part})`);
12927
13009
  process.exitCode = 1;
12928
13010
  return;
12929
13011
  }
12930
- reasons.push(` ${part} -> ${reason}`);
13012
+ reasons.push(` ${part} -> ${reason2}`);
12931
13013
  }
12932
13014
  console.log(`approved
12933
13015
  ${reasons.join("\n")}`);
@@ -15665,9 +15747,9 @@ function resolveSolution(sln) {
15665
15747
  }
15666
15748
 
15667
15749
  // src/commands/dotnet/filterToChangedFiles.ts
15668
- function filterToChangedFiles(issues, changedFiles) {
15750
+ function filterToChangedFiles(issues, changedFiles2) {
15669
15751
  const normalize = (f) => f.replace(/\\/g, "/").replace(/^file:\/\/\//, "");
15670
- const changed = changedFiles.map(normalize);
15752
+ const changed = changedFiles2.map(normalize);
15671
15753
  return issues.filter((i) => {
15672
15754
  const file = normalize(i.file);
15673
15755
  return changed.some((c) => file === c || file.endsWith(`/${c}`));
@@ -15785,23 +15867,23 @@ function runRoslynInspect(slnPath) {
15785
15867
  }
15786
15868
 
15787
15869
  // src/commands/dotnet/runEngine.ts
15788
- function runEngine(resolved, changedFiles, options2) {
15870
+ function runEngine(resolved, changedFiles2, options2) {
15789
15871
  if (options2.roslyn) {
15790
15872
  const issues = runRoslynInspect(resolved);
15791
- return changedFiles ? filterToChangedFiles(issues, changedFiles) : issues;
15873
+ return changedFiles2 ? filterToChangedFiles(issues, changedFiles2) : issues;
15792
15874
  }
15793
15875
  return parseInspectReport(
15794
- runInspectCode(resolved, changedFiles?.join(";") ?? null, !!options2.swea)
15876
+ runInspectCode(resolved, changedFiles2?.join(";") ?? null, !!options2.swea)
15795
15877
  );
15796
15878
  }
15797
15879
 
15798
15880
  // src/commands/dotnet/inspect.ts
15799
- function logScope(changedFiles) {
15800
- if (changedFiles === null) {
15881
+ function logScope(changedFiles2) {
15882
+ if (changedFiles2 === null) {
15801
15883
  console.log(chalk149.dim("Inspecting full solution..."));
15802
15884
  } else {
15803
15885
  console.log(
15804
- chalk149.dim(`Inspecting ${changedFiles.length} changed file(s)...`)
15886
+ chalk149.dim(`Inspecting ${changedFiles2.length} changed file(s)...`)
15805
15887
  );
15806
15888
  }
15807
15889
  }
@@ -15817,14 +15899,14 @@ async function inspect(sln, options2) {
15817
15899
  if (options2.roslyn) assertMsbuildInstalled();
15818
15900
  else assertJbInstalled();
15819
15901
  const scope = parseScope(options2.scope);
15820
- const changedFiles = getChangedCsFiles(scope);
15821
- if (changedFiles !== null && changedFiles.length === 0) {
15902
+ const changedFiles2 = getChangedCsFiles(scope);
15903
+ if (changedFiles2 !== null && changedFiles2.length === 0) {
15822
15904
  console.log(chalk149.green("No changed .cs files found"));
15823
15905
  return;
15824
15906
  }
15825
- logScope(changedFiles);
15907
+ logScope(changedFiles2);
15826
15908
  const start3 = Date.now();
15827
- const issues = runEngine(resolved, changedFiles, options2);
15909
+ const issues = runEngine(resolved, changedFiles2, options2);
15828
15910
  const elapsed = Date.now() - start3;
15829
15911
  const { all = false, only = [], suppress = [] } = options2;
15830
15912
  reportResults(filterIssues(issues, all, only, suppress), elapsed);
@@ -16076,14 +16158,14 @@ async function editHook() {
16076
16158
  const input = tryParseInput2(await readStdin());
16077
16159
  if (!input) return;
16078
16160
  const existing = input.tool_name === "Write" ? readExisting(input.tool_input.file_path) : void 0;
16079
- const reason = decideOverrideGuard(input, existing) ?? decideCommentGuard(input, existing) ?? decideMigrationGuard(input, existing);
16080
- if (!reason) return;
16161
+ const reason2 = decideOverrideGuard(input, existing) ?? decideCommentGuard(input, existing) ?? decideMigrationGuard(input, existing);
16162
+ if (!reason2) return;
16081
16163
  console.log(
16082
16164
  JSON.stringify({
16083
16165
  hookSpecificOutput: {
16084
16166
  hookEventName: "PreToolUse",
16085
16167
  permissionDecision: "deny",
16086
- permissionDecisionReason: reason
16168
+ permissionDecisionReason: reason2
16087
16169
  }
16088
16170
  })
16089
16171
  );
@@ -18775,16 +18857,16 @@ function reply(commentId, body) {
18775
18857
 
18776
18858
  // src/commands/prs/wontfix.ts
18777
18859
  import { execSync as execSync48 } from "child_process";
18778
- function validateReason(reason) {
18779
- const lowerReason = reason.toLowerCase();
18860
+ function validateReason(reason2) {
18861
+ const lowerReason = reason2.toLowerCase();
18780
18862
  if (lowerReason.includes("claude") || lowerReason.includes("opus")) {
18781
18863
  console.error('Error: Reason must not contain "claude" or "opus"');
18782
18864
  process.exit(1);
18783
18865
  }
18784
18866
  }
18785
- function validateShaReferences(reason) {
18867
+ function validateShaReferences(reason2) {
18786
18868
  const commitLinkPattern = /\[[0-9a-f]{7}\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/commit\/([0-9a-f]{40})\)/gi;
18787
- const shas = Array.from(reason.matchAll(commitLinkPattern), (m) => m[1]);
18869
+ const shas = Array.from(reason2.matchAll(commitLinkPattern), (m) => m[1]);
18788
18870
  if (shas.length === 0) {
18789
18871
  return;
18790
18872
  }
@@ -18803,11 +18885,11 @@ function validateShaReferences(reason) {
18803
18885
  process.exit(1);
18804
18886
  }
18805
18887
  }
18806
- function wontfix(commentId, reason) {
18807
- validateReason(reason);
18808
- validateShaReferences(reason);
18888
+ function wontfix(commentId, reason2) {
18889
+ validateReason(reason2);
18890
+ validateShaReferences(reason2);
18809
18891
  try {
18810
- resolveCommentWithReply(commentId, reason);
18892
+ resolveCommentWithReply(commentId, reason2);
18811
18893
  } catch (error) {
18812
18894
  if (isGhNotInstalled(error)) {
18813
18895
  console.error("Error: GitHub CLI (gh) is not installed.");
@@ -18986,8 +19068,8 @@ function registerPrs(program2) {
18986
19068
  prsCommand.command("fixed <comment-id> <sha>").description("Reply with commit link and resolve thread").action((commentId, sha) => {
18987
19069
  fixed(Number.parseInt(commentId, 10), sha);
18988
19070
  });
18989
- prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId, reason) => {
18990
- wontfix(Number.parseInt(commentId, 10), reason);
19071
+ prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId, reason2) => {
19072
+ wontfix(Number.parseInt(commentId, 10), reason2);
18991
19073
  });
18992
19074
  prsCommand.command("reply <comment-id> <body>").description("Reply to a comment thread without resolving it").action((commentId, body) => {
18993
19075
  reply(Number.parseInt(commentId, 10), body);
@@ -20820,8 +20902,8 @@ function findRootParent(file, importedBy, visited) {
20820
20902
  function clusterFiles(graph) {
20821
20903
  const clusters = /* @__PURE__ */ new Map();
20822
20904
  for (const file of graph.files) {
20823
- const basename17 = path52.basename(file, path52.extname(file));
20824
- if (basename17 === "index") continue;
20905
+ const basename18 = path52.basename(file, path52.extname(file));
20906
+ if (basename18 === "index") continue;
20825
20907
  const importers = graph.importedBy.get(file);
20826
20908
  if (!importers || importers.size !== 1) continue;
20827
20909
  const parent = [...importers][0];
@@ -20968,10 +21050,10 @@ function listFilesRecursive(dir) {
20968
21050
  }
20969
21051
  return results;
20970
21052
  }
20971
- function addDirectoryFileMoves(moves, childDir, newLocation, reason) {
21053
+ function addDirectoryFileMoves(moves, childDir, newLocation, reason2) {
20972
21054
  for (const file of listFilesRecursive(childDir)) {
20973
21055
  const rel = path55.relative(childDir, file);
20974
- moves.push({ from: file, to: path55.join(newLocation, rel), reason });
21056
+ moves.push({ from: file, to: path55.join(newLocation, rel), reason: reason2 });
20975
21057
  }
20976
21058
  }
20977
21059
  function resolveChildDest(parentDir, childDir) {
@@ -20982,8 +21064,8 @@ function childMoveReason(parentDir) {
20982
21064
  }
20983
21065
  function registerDirectoryMove(result, childDir, dest, parentDir) {
20984
21066
  result.directories.push(dest);
20985
- const reason = childMoveReason(parentDir);
20986
- addDirectoryFileMoves(result.moves, childDir, dest, reason);
21067
+ const reason2 = childMoveReason(parentDir);
21068
+ addDirectoryFileMoves(result.moves, childDir, dest, reason2);
20987
21069
  }
20988
21070
  function planChildDirectoryMove(result, parentDir, childDir) {
20989
21071
  const dest = resolveChildDest(parentDir, childDir);
@@ -21383,7 +21465,7 @@ function gatherContext() {
21383
21465
  encoding: "utf8"
21384
21466
  }).trim();
21385
21467
  const prInfo = fetchPrDiffInfo();
21386
- const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
21468
+ const changedFiles2 = fetchPrChangedFiles(prInfo.prNumber);
21387
21469
  const diff3 = fetchPrDiff(prInfo.prNumber, prInfo.baseSha, prInfo.headSha);
21388
21470
  return {
21389
21471
  branch: branch2,
@@ -21394,7 +21476,7 @@ function gatherContext() {
21394
21476
  baseSha: prInfo.baseSha,
21395
21477
  headRef: prInfo.headRef,
21396
21478
  headSha: prInfo.headSha,
21397
- changedFiles,
21479
+ changedFiles: changedFiles2,
21398
21480
  diff: diff3
21399
21481
  };
21400
21482
  }
@@ -25904,6 +25986,16 @@ async function gitResult(cwd, args) {
25904
25986
  };
25905
25987
  }
25906
25988
  }
25989
+ function gitSyncResult(cwd, args) {
25990
+ try {
25991
+ return { ok: true, out: gitSync(cwd, args).trim() };
25992
+ } catch (error) {
25993
+ return {
25994
+ ok: false,
25995
+ error: error instanceof Error ? error.message : String(error)
25996
+ };
25997
+ }
25998
+ }
25907
25999
  function gitSync(cwd, args) {
25908
26000
  const { file, argv, options: options2 } = gitInvocation(cwd, args);
25909
26001
  return execFileSync12(file, argv, {
@@ -25918,6 +26010,8 @@ function gitSyncOrNull(cwd, args) {
25918
26010
  return null;
25919
26011
  }
25920
26012
  }
26013
+
26014
+ // src/commands/sessions/daemon/worktree/listWorktreePaths.ts
25921
26015
  function listWorktreePaths(cwd) {
25922
26016
  const out = gitSyncOrNull(cwd, ["worktree", "list", "--porcelain"]);
25923
26017
  if (!out) return [];
@@ -25942,6 +26036,59 @@ function gitCommonDir(cwd) {
25942
26036
  ]);
25943
26037
  }
25944
26038
 
26039
+ // src/commands/sessions/daemon/worktree/readWorktreeRegistry.ts
26040
+ var REGISTRY_FILE = "worktrees.json";
26041
+ function readWorktreeRegistry() {
26042
+ const data = loadJson(REGISTRY_FILE);
26043
+ return Object.entries(data).map(([path71, record]) => ({ path: path71, ...record }));
26044
+ }
26045
+ function recordWorktree(path71, clone, origin) {
26046
+ const data = loadJson(REGISTRY_FILE);
26047
+ data[path71] = { clone, origin };
26048
+ saveJson(REGISTRY_FILE, data);
26049
+ }
26050
+ function forgetWorktree(path71) {
26051
+ const data = loadJson(REGISTRY_FILE);
26052
+ if (!(path71 in data)) return;
26053
+ delete data[path71];
26054
+ saveJson(REGISTRY_FILE, data);
26055
+ }
26056
+
26057
+ // src/commands/sessions/daemon/worktree/removeTree.ts
26058
+ async function removeTree(clone, worktreePath, force) {
26059
+ const remove2 = (forced) => git3(clone, [
26060
+ "worktree",
26061
+ "remove",
26062
+ ...forced ? ["--force"] : [],
26063
+ worktreePath
26064
+ ]);
26065
+ try {
26066
+ await remove2(force);
26067
+ return true;
26068
+ } catch (error) {
26069
+ daemonLog(`worktree ${worktreePath} removal failed: ${reason(error)}`);
26070
+ }
26071
+ if (force) return false;
26072
+ return await removeIgnoringLeftovers(remove2, worktreePath);
26073
+ }
26074
+ async function removeIgnoringLeftovers(remove2, worktreePath) {
26075
+ try {
26076
+ await remove2(true);
26077
+ daemonLog(
26078
+ `worktree ${worktreePath} removed on retry; only ignored files remained, its work is landed`
26079
+ );
26080
+ return true;
26081
+ } catch (error) {
26082
+ daemonLog(
26083
+ `worktree ${worktreePath} left in place for the next reconcile: ${reason(error)}`
26084
+ );
26085
+ return false;
26086
+ }
26087
+ }
26088
+ function reason(error) {
26089
+ return error instanceof Error ? error.message : String(error);
26090
+ }
26091
+
25945
26092
  // src/commands/sessions/daemon/worktree/treeDurability.ts
25946
26093
  import { existsSync as existsSync58 } from "fs";
25947
26094
  function treeDurability(state) {
@@ -25950,28 +26097,29 @@ function treeDurability(state) {
25950
26097
  return { durable: false, reason: "unpushed commits" };
25951
26098
  return { durable: true };
25952
26099
  }
25953
- async function hasLocalOnlyCommits(cwd) {
25954
- const upstream = await gitResult(cwd, [
26100
+ function* durabilityProbes(cwd) {
26101
+ const status3 = yield ["status", "--porcelain"];
26102
+ if (!status3.ok)
26103
+ return { durable: false, reason: `tree state unreadable: ${status3.error}` };
26104
+ const upstream = yield [
25955
26105
  "rev-parse",
25956
26106
  "--abbrev-ref",
25957
26107
  "--symbolic-full-name",
25958
26108
  "@{upstream}"
25959
- ]);
26109
+ ];
26110
+ let localOnlyCommits;
25960
26111
  if (upstream.ok && upstream.out) {
25961
- const ahead = await gitResult(cwd, [
25962
- "rev-list",
25963
- "--count",
25964
- "@{upstream}..HEAD"
25965
- ]);
25966
- return !ahead.ok || Number(ahead.out) > 0;
26112
+ const ahead = yield ["rev-list", "--count", "@{upstream}..HEAD"];
26113
+ localOnlyCommits = !ahead.ok || Number(ahead.out) > 0;
26114
+ } else {
26115
+ const remoteBranches = yield ["branch", "-r", "--contains", "HEAD"];
26116
+ localOnlyCommits = !remoteBranches.ok || remoteBranches.out === "";
25967
26117
  }
25968
- const remoteBranches = await gitResult(cwd, [
25969
- "branch",
25970
- "-r",
25971
- "--contains",
25972
- "HEAD"
25973
- ]);
25974
- return !remoteBranches.ok || remoteBranches.out === "";
26118
+ return treeDurability({
26119
+ dirty: status3.out !== "",
26120
+ push: pushOnCommit(cwd),
26121
+ localOnlyCommits
26122
+ });
25975
26123
  }
25976
26124
  function pushOnCommit(cwd) {
25977
26125
  try {
@@ -25982,14 +26130,17 @@ function pushOnCommit(cwd) {
25982
26130
  }
25983
26131
  async function checkDurability(cwd) {
25984
26132
  if (!existsSync58(cwd)) return { durable: true };
25985
- const status3 = await gitResult(cwd, ["status", "--porcelain"]);
25986
- if (!status3.ok)
25987
- return { durable: false, reason: `tree state unreadable: ${status3.error}` };
25988
- return treeDurability({
25989
- dirty: status3.out !== "",
25990
- push: pushOnCommit(cwd),
25991
- localOnlyCommits: await hasLocalOnlyCommits(cwd)
25992
- });
26133
+ const probes = durabilityProbes(cwd);
26134
+ let step = probes.next();
26135
+ while (!step.done) step = probes.next(await gitResult(cwd, step.value));
26136
+ return step.value;
26137
+ }
26138
+ function checkDurabilitySync(cwd) {
26139
+ if (!existsSync58(cwd)) return { durable: true };
26140
+ const probes = durabilityProbes(cwd);
26141
+ let step = probes.next();
26142
+ while (!step.done) step = probes.next(gitSyncResult(cwd, step.value));
26143
+ return step.value;
25993
26144
  }
25994
26145
 
25995
26146
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
@@ -26011,22 +26162,11 @@ async function reapWorktree(worktreePath, force = false) {
26011
26162
  "--short",
26012
26163
  "HEAD"
26013
26164
  ]);
26014
- try {
26015
- await git3(clone, [
26016
- "worktree",
26017
- "remove",
26018
- ...force ? ["--force"] : [],
26019
- worktreePath
26020
- ]);
26021
- if (branch2) await deleteBranch(clone, branch2);
26022
- daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
26023
- return true;
26024
- } catch (error) {
26025
- daemonLog(
26026
- `worktree ${worktreePath} reap failed: ${error instanceof Error ? error.message : String(error)}`
26027
- );
26028
- return false;
26029
- }
26165
+ if (!await removeTree(clone, worktreePath, force)) return false;
26166
+ if (branch2) await deleteBranch(clone, branch2);
26167
+ forgetWorktree(worktreePath);
26168
+ daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
26169
+ return true;
26030
26170
  }
26031
26171
  async function deleteBranch(clone, branch2) {
26032
26172
  const current = await gitOrNull(clone, ["symbolic-ref", "--short", "HEAD"]);
@@ -26096,22 +26236,42 @@ function setStatus2(session, newStatus) {
26096
26236
  // src/commands/sessions/daemon/worktree/watchGitState.ts
26097
26237
  import { existsSync as existsSync60, watch } from "fs";
26098
26238
  var DEBOUNCE_MS = 500;
26239
+ var POLL_MS = 3e4;
26099
26240
  function watchGitState(cwd, onChange) {
26100
26241
  const common = gitCommonDir(cwd);
26101
26242
  if (!common || !existsSync60(common)) return void 0;
26243
+ const watchers = [
26244
+ watchGitDir(common, onChange),
26245
+ pollGitState(cwd, onChange)
26246
+ ].filter((w) => w !== void 0);
26247
+ return { close: () => watchers.forEach((w) => w.close()) };
26248
+ }
26249
+ function watchGitDir(common, onChange) {
26102
26250
  let timer = null;
26103
26251
  try {
26104
- return watch(common, { recursive: true }, () => {
26252
+ const watcher = watch(common, { recursive: true }, () => {
26105
26253
  if (timer) clearTimeout(timer);
26106
26254
  timer = setTimeout(onChange, DEBOUNCE_MS);
26107
26255
  });
26256
+ return {
26257
+ close: () => {
26258
+ if (timer) clearTimeout(timer);
26259
+ watcher.close();
26260
+ }
26261
+ };
26108
26262
  } catch (error) {
26109
26263
  daemonLog(
26110
- `git-state watch failed for ${cwd}: ${error instanceof Error ? error.message : String(error)}`
26264
+ `git-state watch unavailable for ${common}, polling only: ${error instanceof Error ? error.message : String(error)}`
26111
26265
  );
26112
26266
  return void 0;
26113
26267
  }
26114
26268
  }
26269
+ function pollGitState(cwd, onChange) {
26270
+ const timer = setInterval(onChange, POLL_MS);
26271
+ timer.unref?.();
26272
+ daemonLog(`git-state polled every ${POLL_MS / 1e3}s for ${cwd}`);
26273
+ return { close: () => clearInterval(timer) };
26274
+ }
26115
26275
 
26116
26276
  // src/commands/sessions/daemon/worktree/resolveCloseDurability.ts
26117
26277
  async function resolveCloseDurability(session, finalize, notify2) {
@@ -26133,19 +26293,24 @@ async function resolveCloseDurability(session, finalize, notify2) {
26133
26293
  session.gitWatcher = void 0;
26134
26294
  finalize();
26135
26295
  }
26136
- function holdStopped(session, tree, reason, finalize, notify2) {
26137
- if (session.undurable?.reason !== reason)
26296
+ function holdStopped(session, tree, reason2, finalize, notify2) {
26297
+ if (session.undurable?.reason !== reason2)
26138
26298
  daemonLog(
26139
- `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason}`
26299
+ `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason2}`
26140
26300
  );
26141
- session.undurable = { reason, removesTree: tree.removable };
26301
+ session.undurable = { reason: reason2, removesTree: tree.removable };
26142
26302
  session.closing = void 0;
26143
26303
  setStatus2(session, "stopped");
26144
- if (!session.gitWatcher)
26304
+ if (!session.gitWatcher) {
26305
+ let rechecking = false;
26145
26306
  session.gitWatcher = watchGitState(tree.path, () => {
26146
- if (session.status !== "stopped") return;
26147
- void resolveCloseDurability(session, finalize, notify2);
26307
+ if (session.status !== "stopped" || rechecking) return;
26308
+ rechecking = true;
26309
+ void resolveCloseDurability(session, finalize, notify2).finally(() => {
26310
+ rechecking = false;
26311
+ });
26148
26312
  });
26313
+ }
26149
26314
  notify2();
26150
26315
  }
26151
26316
 
@@ -26893,6 +27058,8 @@ function respawnPlan(session) {
26893
27058
  status: "waiting"
26894
27059
  };
26895
27060
  if (initialPrompt) return freshClaudePlan(session, initialPrompt, cwd);
27061
+ if (cwd && session.harness !== "pi")
27062
+ return freshClaudePlan(session, void 0, cwd);
26896
27063
  return null;
26897
27064
  }
26898
27065
  if (commandType === "assist" && assistArgs) {
@@ -26921,7 +27088,7 @@ function freshClaudePlan(session, prompt, cwd) {
26921
27088
  claudeSessionId
26922
27089
  });
26923
27090
  },
26924
- status: "running"
27091
+ status: prompt ? "running" : "waiting"
26925
27092
  };
26926
27093
  }
26927
27094
 
@@ -27346,9 +27513,9 @@ function restoreOne(persisted, spawn12, sessions) {
27346
27513
  const id = spawn12((sid) => restoreSession(sid, persisted));
27347
27514
  logUnresumable(persisted.name, id, sessions.get(id));
27348
27515
  } catch (error) {
27349
- const reason = logRestoreError(persisted.name, error);
27516
+ const reason2 = logRestoreError(persisted.name, error);
27350
27517
  try {
27351
- spawn12((id) => errorSession(id, persisted, reason));
27518
+ spawn12((id) => errorSession(id, persisted, reason2));
27352
27519
  } catch {
27353
27520
  }
27354
27521
  }
@@ -27360,9 +27527,9 @@ function logUnresumable(name, id, session) {
27360
27527
  );
27361
27528
  }
27362
27529
  function logRestoreError(name, error) {
27363
- const reason = error instanceof Error ? error.message : String(error);
27364
- daemonLog(`failed to restore session "${name}": ${reason}`);
27365
- return reason;
27530
+ const reason2 = error instanceof Error ? error.message : String(error);
27531
+ daemonLog(`failed to restore session "${name}": ${reason2}`);
27532
+ return reason2;
27366
27533
  }
27367
27534
 
27368
27535
  // src/commands/sessions/daemon/sessionLimits.ts
@@ -27398,22 +27565,42 @@ function restoreAll(spawn12, sessions) {
27398
27565
  }
27399
27566
 
27400
27567
  // src/commands/sessions/daemon/worktree/rearmStoppedSessions.ts
27568
+ function armStoppedSession(sessions, session, notify2) {
27569
+ void resolveCloseDurability(
27570
+ session,
27571
+ () => {
27572
+ if (dismissSession(sessions, session.id)) notify2();
27573
+ },
27574
+ notify2
27575
+ );
27576
+ }
27401
27577
  function rearmStoppedSessions(sessions, notify2) {
27402
27578
  for (const s of sessions.values()) {
27403
27579
  if (s.status !== "stopped" || !treeUnderClose(s)) continue;
27404
- void resolveCloseDurability(
27405
- s,
27406
- () => {
27407
- if (dismissSession(sessions, s.id)) notify2();
27408
- },
27409
- notify2
27410
- );
27580
+ armStoppedSession(sessions, s, notify2);
27411
27581
  }
27412
27582
  }
27413
27583
 
27414
27584
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
27415
- import { existsSync as existsSync66 } from "fs";
27416
- import { basename as basename14 } from "path";
27585
+ import { existsSync as existsSync67 } from "fs";
27586
+ import { basename as basename15 } from "path";
27587
+
27588
+ // src/commands/sessions/daemon/worktree/accountedTrees.ts
27589
+ function accountedTrees(sessions) {
27590
+ const accounted = /* @__PURE__ */ new Set();
27591
+ const account = (cwd) => {
27592
+ if (!cwd) return;
27593
+ accounted.add(cwd);
27594
+ const root = findRepoRoot(cwd);
27595
+ if (root) accounted.add(root);
27596
+ };
27597
+ for (const s of sessions.values()) {
27598
+ account(s.cwd);
27599
+ account(s.worktree?.path);
27600
+ }
27601
+ for (const s of loadPersistedSessions()) account(s.cwd);
27602
+ return accounted;
27603
+ }
27417
27604
 
27418
27605
  // src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
27419
27606
  function detectExistingWorktree(cwd) {
@@ -27490,12 +27677,6 @@ function message(error) {
27490
27677
  }
27491
27678
 
27492
27679
  // src/commands/sessions/daemon/worktree/bindNewWorktree.ts
27493
- function boundTreeRoots(sessions) {
27494
- const roots = /* @__PURE__ */ new Set();
27495
- for (const session of sessions.values())
27496
- if (session.cwd) roots.add(findRepoRoot(session.cwd) ?? session.cwd);
27497
- return roots;
27498
- }
27499
27680
  function bindNewWorktree(session, alloc, notify2, startHeld = () => {
27500
27681
  }) {
27501
27682
  if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
@@ -27520,66 +27701,169 @@ function bindResumedWorktree(session, cwd, notify2) {
27520
27701
  notify2();
27521
27702
  }
27522
27703
 
27523
- // src/commands/sessions/daemon/worktree/readWorktreeRegistry.ts
27524
- var REGISTRY_FILE = "worktrees.json";
27525
- function readWorktreeRegistry() {
27526
- const data = loadJson(REGISTRY_FILE);
27527
- return Object.entries(data).map(([path71, record]) => ({ path: path71, ...record }));
27704
+ // src/commands/sessions/daemon/worktree/describeHeldWork.ts
27705
+ var MAX_ITEMS = 20;
27706
+ async function describeHeldWork(path71, reason2) {
27707
+ if (reason2.startsWith("uncommitted")) return await changedFiles(path71);
27708
+ if (reason2.startsWith("unpushed")) return await unpushedCommits(path71, reason2);
27709
+ return { summary: reason2, items: [] };
27528
27710
  }
27529
- function recordWorktree(path71, clone, origin) {
27530
- const data = loadJson(REGISTRY_FILE);
27531
- data[path71] = { clone, origin };
27532
- saveJson(REGISTRY_FILE, data);
27711
+ async function changedFiles(path71) {
27712
+ const status3 = await gitResult(path71, ["status", "--porcelain"]);
27713
+ const lines = status3.ok ? nonEmptyLines(status3.out) : [];
27714
+ return {
27715
+ summary: `${lines.length} uncommitted ${lines.length === 1 ? "file" : "files"}`,
27716
+ items: capped(lines)
27717
+ };
27533
27718
  }
27534
-
27535
- // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
27536
- function reconcileWorktreesOnRestore(sessions) {
27537
- bindRestoredWorktrees(sessions);
27538
- void pruneWorktrees([...sessions.values()].map((s) => s.cwd));
27719
+ async function unpushedCommits(path71, reason2) {
27720
+ const log = await gitResult(path71, ["log", "--oneline", "@{upstream}..HEAD"]);
27721
+ if (!log.ok) return { summary: reason2, items: [] };
27722
+ const lines = nonEmptyLines(log.out);
27723
+ return {
27724
+ summary: `${lines.length} unpushed ${lines.length === 1 ? "commit" : "commits"}`,
27725
+ items: capped(lines)
27726
+ };
27539
27727
  }
27540
- async function pruneWorktrees(liveCwds) {
27541
- const accounted = /* @__PURE__ */ new Set();
27542
- for (const cwd of liveCwds) if (cwd) accounted.add(cwd);
27543
- for (const s of loadPersistedSessions()) accounted.add(s.cwd);
27544
- const clonesToPrune = /* @__PURE__ */ new Set();
27545
- const orphanBranches = [];
27546
- for (const { path: path71, clone } of readWorktreeRegistry()) {
27547
- if (accounted.has(path71)) continue;
27548
- if (!existsSync66(path71)) {
27549
- clonesToPrune.add(clone);
27550
- orphanBranches.push({ clone, branch: basename14(path71) });
27551
- continue;
27552
- }
27553
- daemonLog(`worktree ${path71} orphaned across restart; reconciling`);
27554
- await reapWorktree(path71);
27728
+ function nonEmptyLines(out) {
27729
+ return out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
27730
+ }
27731
+ function capped(lines) {
27732
+ if (lines.length <= MAX_ITEMS) return lines;
27733
+ return [
27734
+ ...lines.slice(0, MAX_ITEMS),
27735
+ `\u2026 and ${lines.length - MAX_ITEMS} more`
27736
+ ];
27737
+ }
27738
+
27739
+ // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
27740
+ import { existsSync as existsSync66 } from "fs";
27741
+ async function reclaimVanishedWorktrees(clone, paths) {
27742
+ if (!existsSync66(clone)) {
27743
+ for (const { path: path71 } of paths) forgetWorktree(path71);
27744
+ daemonLog(
27745
+ `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
27746
+ );
27747
+ return;
27555
27748
  }
27556
- for (const clone of clonesToPrune) {
27557
- if (!existsSync66(clone)) continue;
27558
- try {
27559
- await git3(clone, ["worktree", "prune"]);
27560
- daemonLog(`worktree bookkeeping pruned for clone ${clone}`);
27561
- } catch {
27562
- }
27749
+ await pruneBookkeeping(clone);
27750
+ for (const { path: path71, branch: branch2 } of paths)
27751
+ if (await reclaimBranch(clone, branch2)) forgetWorktree(path71);
27752
+ }
27753
+ async function pruneBookkeeping(clone) {
27754
+ try {
27755
+ await git3(clone, ["worktree", "prune"]);
27756
+ daemonLog(`worktree bookkeeping pruned for clone ${clone}`);
27757
+ } catch (error) {
27758
+ daemonLog(
27759
+ `worktree bookkeeping prune failed for clone ${clone}: ${error instanceof Error ? error.message : String(error)}`
27760
+ );
27563
27761
  }
27564
- for (const { clone, branch: branch2 } of orphanBranches)
27565
- if (existsSync66(clone)) await pruneStaleWorktreeBranch(clone, branch2);
27566
27762
  }
27567
- async function pruneStaleWorktreeBranch(clone, branch2) {
27568
- if (!listLocalBranches(clone).includes(branch2)) return;
27763
+ async function reclaimBranch(clone, branch2) {
27764
+ if (!listLocalBranches(clone).includes(branch2)) return true;
27569
27765
  try {
27570
27766
  await git3(clone, ["branch", "-d", branch2]);
27571
27767
  daemonLog(`stale worktree branch ${branch2} pruned from clone ${clone}`);
27768
+ return true;
27572
27769
  } catch {
27573
27770
  daemonLog(
27574
27771
  `stale worktree branch ${branch2} retained (unmerged; surfaced not destroyed) in clone ${clone}`
27575
27772
  );
27773
+ return false;
27774
+ }
27775
+ }
27776
+
27777
+ // src/commands/sessions/daemon/worktree/resurfaceOrphanedWorktree.ts
27778
+ import { basename as basename14 } from "path";
27779
+ function resurfaceOrphanedWorktree(sessions, spawnWith, recovered, notify2) {
27780
+ const { orphan, reason: reason2, held } = recovered;
27781
+ let id;
27782
+ try {
27783
+ id = spawnWith((sid) => orphanedSession(sid, recovered));
27784
+ } catch (error) {
27785
+ daemonLog(
27786
+ `orphaned worktree ${orphan.path} left in place, no card available: ${error instanceof Error ? error.message : String(error)}`
27787
+ );
27788
+ return;
27789
+ }
27790
+ daemonLog(
27791
+ `orphaned worktree ${orphan.path} resurfaced as stopped session ${id}: ${reason2} (${held.summary})`
27792
+ );
27793
+ const session = sessions.get(id);
27794
+ if (session) armStoppedSession(sessions, session, notify2);
27795
+ notify2();
27796
+ }
27797
+ function orphanedSession(id, recovered) {
27798
+ const { orphan, reason: reason2, held } = recovered;
27799
+ return {
27800
+ ...sessionBase(id, "stopped"),
27801
+ name: `recovered ${basename14(orphan.path)}`,
27802
+ subtitle: `${held.summary} in ${orphan.path}`,
27803
+ commandType: "claude",
27804
+ pty: null,
27805
+ cwd: orphan.path,
27806
+ worktree: { path: orphan.path, clone: orphan.clone },
27807
+ undurable: { reason: reason2, removesTree: true },
27808
+ scrollback: recoveryNotice(recovered)
27809
+ };
27810
+ }
27811
+ function recoveryNotice({ orphan, held }) {
27812
+ return [
27813
+ `\r
27814
+ \x1B[33mRecovered workspace ${orphan.path}\x1B[0m\r
27815
+ `,
27816
+ `Its session is gone, but the workspace still holds ${held.summary}:\r
27817
+ `,
27818
+ ...held.items.map((item) => ` ${item}\r
27819
+ `),
27820
+ "\r\nThis card is the handle for it: land the work and the workspace is removed on its own,\r\n",
27821
+ "restart to put an agent back in it, or discard to delete it and the work it holds.\r\n"
27822
+ ].join("");
27823
+ }
27824
+
27825
+ // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
27826
+ function reconcileWorktreesOnRestore(sessions, spawnWith, notify2) {
27827
+ bindRestoredWorktrees(sessions);
27828
+ void recoverOrphanedWorktrees(sessions, spawnWith, notify2);
27829
+ }
27830
+ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
27831
+ const accounted = accountedTrees(sessions);
27832
+ const vanished = /* @__PURE__ */ new Map();
27833
+ for (const { path: path71, clone } of readWorktreeRegistry()) {
27834
+ if (accounted.has(path71)) continue;
27835
+ if (!existsSync67(path71)) {
27836
+ vanished.set(clone, [
27837
+ ...vanished.get(clone) ?? [],
27838
+ { path: path71, branch: basename15(path71) }
27839
+ ]);
27840
+ continue;
27841
+ }
27842
+ await recoverOrphan(sessions, spawnWith, { path: path71, clone }, notify2);
27576
27843
  }
27844
+ for (const [clone, paths] of vanished)
27845
+ await reclaimVanishedWorktrees(clone, paths);
27846
+ }
27847
+ async function recoverOrphan(sessions, spawnWith, orphan, notify2) {
27848
+ daemonLog(`worktree ${orphan.path} orphaned across restart; reconciling`);
27849
+ const durability = await checkDurability(orphan.path);
27850
+ if (durability.durable) {
27851
+ await reapWorktree(orphan.path);
27852
+ return;
27853
+ }
27854
+ const held = await describeHeldWork(orphan.path, durability.reason);
27855
+ resurfaceOrphanedWorktree(
27856
+ sessions,
27857
+ spawnWith,
27858
+ { orphan, reason: durability.reason, held },
27859
+ notify2
27860
+ );
27577
27861
  }
27578
27862
 
27579
27863
  // src/commands/sessions/daemon/restoreAllSessions.ts
27580
27864
  function restoreAllSessions(spawnWith, sessions, notify2) {
27581
27865
  const names = restoreAll(spawnWith, sessions);
27582
- reconcileWorktreesOnRestore(sessions);
27866
+ reconcileWorktreesOnRestore(sessions, spawnWith, notify2);
27583
27867
  rearmStoppedSessions(sessions, notify2);
27584
27868
  return names;
27585
27869
  }
@@ -27727,7 +28011,7 @@ function treeSpawnContext(sessions, spawnWith, notify2, clients, onStatusChange)
27727
28011
  }
27728
28012
 
27729
28013
  // src/commands/sessions/daemon/watchTranscript.ts
27730
- import { existsSync as existsSync67, watch as watch3 } from "fs";
28014
+ import { existsSync as existsSync68, watch as watch3 } from "fs";
27731
28015
 
27732
28016
  // src/commands/sessions/daemon/reconcileTranscriptStatus.ts
27733
28017
  async function resolveTranscriptPath(session) {
@@ -27762,7 +28046,7 @@ function watchTranscript(session, onStatusChange) {
27762
28046
  if (!session.cwd || !session.claudeSessionId) return;
27763
28047
  if (session.watchedTranscriptId === session.claudeSessionId) return;
27764
28048
  const dir = projectDirForCwd(session.cwd);
27765
- if (!existsSync67(dir)) return;
28049
+ if (!existsSync68(dir)) return;
27766
28050
  if (session.transcriptWatcher) {
27767
28051
  session.transcriptWatcher.close();
27768
28052
  session.transcriptWatcher = void 0;
@@ -27913,13 +28197,13 @@ async function defaultConnect() {
27913
28197
  }
27914
28198
 
27915
28199
  // src/commands/sessions/daemon/hasPersistedWindowsSessions.ts
27916
- import { existsSync as existsSync68, readFileSync as readFileSync49 } from "fs";
28200
+ import { existsSync as existsSync69, readFileSync as readFileSync49 } from "fs";
27917
28201
  import { posix } from "path";
27918
28202
  function hasPersistedWindowsSessions() {
27919
28203
  const sessionsFile = windowsSessionsFileFromWsl();
27920
28204
  if (!sessionsFile) return false;
27921
28205
  try {
27922
- if (!existsSync68(sessionsFile)) return false;
28206
+ if (!existsSync69(sessionsFile)) return false;
27923
28207
  const data = JSON.parse(readFileSync49(sessionsFile, "utf8"));
27924
28208
  return Array.isArray(data) && data.length > 0;
27925
28209
  } catch (error) {
@@ -28507,16 +28791,16 @@ function resumeSession(id, sessionId, cwd, name) {
28507
28791
  }
28508
28792
 
28509
28793
  // src/commands/sessions/daemon/worktree/createWorktree.ts
28510
- import { existsSync as existsSync69 } from "fs";
28511
- import { basename as basename16, dirname as dirname31 } from "path";
28794
+ import { existsSync as existsSync70 } from "fs";
28795
+ import { basename as basename17, dirname as dirname31 } from "path";
28512
28796
 
28513
28797
  // src/commands/sessions/daemon/worktree/planAllocation.ts
28514
- import { basename as basename15, join as join72 } from "path";
28798
+ import { basename as basename16, join as join72 } from "path";
28515
28799
  function planAllocation(clone, boundTreeRoots2) {
28516
28800
  return boundTreeRoots2.has(clone) ? "spill" : "primary";
28517
28801
  }
28518
28802
  function nextWorktreePath(clone, base, isTaken) {
28519
- const name = basename15(clone);
28803
+ const name = basename16(clone);
28520
28804
  for (let n = 2; n < 1e3; n++) {
28521
28805
  const candidate = join72(base, `${name}-${n}`);
28522
28806
  if (!isTaken(candidate)) return candidate;
@@ -28532,7 +28816,7 @@ function createWorktree(clone, root, boundTreeRoots2) {
28532
28816
  const path71 = nextWorktreePath(
28533
28817
  clone,
28534
28818
  base,
28535
- (candidate) => registered.has(candidate) || existsSync69(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename16(candidate))
28819
+ (candidate) => registered.has(candidate) || existsSync70(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename17(candidate))
28536
28820
  );
28537
28821
  const trunk = cloneTrunkBranch(clone);
28538
28822
  gitSync(clone, ["config", "push.default", "upstream"]);
@@ -28541,13 +28825,13 @@ function createWorktree(clone, root, boundTreeRoots2) {
28541
28825
  "add",
28542
28826
  "--track",
28543
28827
  "-b",
28544
- basename16(path71),
28828
+ basename17(path71),
28545
28829
  path71,
28546
28830
  `origin/${trunk}`
28547
28831
  ]);
28548
28832
  recordWorktree(path71, clone, getCurrentOrigin(clone));
28549
28833
  daemonLog(
28550
- `worktree allocated ${path71} (branch ${basename16(path71)} tracking origin/${trunk}) for clone ${clone}`
28834
+ `worktree allocated ${path71} (branch ${basename17(path71)} tracking origin/${trunk}) for clone ${clone}`
28551
28835
  );
28552
28836
  return path71;
28553
28837
  }
@@ -28581,6 +28865,30 @@ function allocateTree(requestedCwd, boundTreeRoots2) {
28581
28865
  return { cwd: path71, kind: "worktree", created: true, clone };
28582
28866
  }
28583
28867
 
28868
+ // src/commands/sessions/daemon/worktree/boundTreeRoots.ts
28869
+ function boundTreeRoots(sessions) {
28870
+ const roots = /* @__PURE__ */ new Set();
28871
+ for (const session of sessions.values()) {
28872
+ const root = treeRootOf(session);
28873
+ if (root && !roots.has(root) && holdsTree(session, root)) roots.add(root);
28874
+ }
28875
+ return roots;
28876
+ }
28877
+ function treeRootOf(session) {
28878
+ if (!session.cwd) return void 0;
28879
+ return findRepoRoot(session.cwd) ?? session.cwd;
28880
+ }
28881
+ function holdsTree(session, root) {
28882
+ if (session.closing) return true;
28883
+ if (session.status !== "done" && session.status !== "error") return true;
28884
+ if (!worktreeConfigFor(root).enabled) return true;
28885
+ const durability = checkDurabilitySync(root);
28886
+ daemonLog(
28887
+ durability.durable ? `tree ${root} free for allocation: session ${session.id} finished and its work is landed` : `tree ${root} still held by finished session ${session.id}: ${durability.reason}`
28888
+ );
28889
+ return !durability.durable;
28890
+ }
28891
+
28584
28892
  // src/commands/sessions/daemon/worktree/spawnInTree.ts
28585
28893
  function allocateAndBind(ctx, cwd, create) {
28586
28894
  const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions));