@staff0rd/assist 0.488.0 → 0.488.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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.488.0",
9
+ version: "0.488.2",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -10891,6 +10891,18 @@ function resolveNamedRepoWriteLabel(globalRaw, name) {
10891
10891
  return matches[0];
10892
10892
  }
10893
10893
 
10894
+ // src/commands/config/resolveRepoConfigBlock.ts
10895
+ function resolveRepoConfigBlock(repoName, cwd) {
10896
+ const globalRaw = loadGlobalConfigRaw();
10897
+ const label2 = repoName === void 0 ? resolveRepoWriteLabel(globalRaw, getCurrentOrigin(cwd)) : resolveNamedRepoWriteLabel(globalRaw, repoName);
10898
+ const repos2 = isPlainObject3(globalRaw.repos) ? { ...globalRaw.repos } : {};
10899
+ const block = isPlainObject3(repos2[label2]) ? repos2[label2] : {};
10900
+ return { globalRaw, repos: repos2, label: label2, block };
10901
+ }
10902
+ function isPlainObject3(value) {
10903
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10904
+ }
10905
+
10894
10906
  // src/commands/config/applyRepoConfigSet.ts
10895
10907
  function applyRepoConfigSet(key, coerced, repoName, cwd = process.cwd()) {
10896
10908
  if (isGlobalOnlyConfigKey(key)) {
@@ -10901,20 +10913,17 @@ function applyRepoConfigSet(key, coerced, repoName, cwd = process.cwd()) {
10901
10913
  ]
10902
10914
  };
10903
10915
  }
10904
- const globalRaw = loadGlobalConfigRaw();
10905
- const label2 = repoName === void 0 ? resolveRepoWriteLabel(globalRaw, getCurrentOrigin(cwd)) : resolveNamedRepoWriteLabel(globalRaw, repoName);
10906
- const repos2 = isPlainObject3(globalRaw.repos) ? { ...globalRaw.repos } : {};
10907
- const existingBlock = isPlainObject3(repos2[label2]) ? repos2[label2] : {};
10908
- const updatedBlock = setNestedValue(existingBlock, key, coerced);
10916
+ const { globalRaw, repos: repos2, label: label2, block } = resolveRepoConfigBlock(
10917
+ repoName,
10918
+ cwd
10919
+ );
10920
+ const updatedBlock = setNestedValue(block, key, coerced);
10909
10921
  const validation = validateConfig(updatedBlock, key, repoConfigSchema);
10910
10922
  if (!validation.ok) return validation;
10911
10923
  repos2[label2] = updatedBlock;
10912
10924
  saveGlobalConfig({ ...globalRaw, repos: repos2 });
10913
10925
  return { ok: true, target: "repo", label: label2 };
10914
10926
  }
10915
- function isPlainObject3(value) {
10916
- return value !== null && typeof value === "object" && !Array.isArray(value);
10917
- }
10918
10927
 
10919
10928
  // src/commands/sessions/web/applyScopedConfigSet.ts
10920
10929
  function applyScopedConfigSet(key, value, cwd, scope) {
@@ -10994,6 +11003,11 @@ function setConfig(req, res) {
10994
11003
  });
10995
11004
  }
10996
11005
 
11006
+ // src/commands/config/isKnownConfigKey.ts
11007
+ function isKnownConfigKey(key) {
11008
+ return enumerateConfigLeafKeys(assistConfigSchema).includes(key);
11009
+ }
11010
+
10997
11011
  // src/commands/config/unsetNestedValue.ts
10998
11012
  function isPlainObject4(val) {
10999
11013
  return val !== null && typeof val === "object" && !Array.isArray(val);
@@ -11084,9 +11098,48 @@ function applyConfigUnset(key, global, cwd = process.cwd()) {
11084
11098
  return { ok: true, target, removed: true };
11085
11099
  }
11086
11100
 
11087
- // src/commands/config/isKnownConfigKey.ts
11088
- function isKnownConfigKey(key) {
11089
- return enumerateConfigLeafKeys(assistConfigSchema).includes(key);
11101
+ // src/commands/config/applyRepoConfigUnset.ts
11102
+ function applyRepoConfigUnset(key, repoName, cwd = process.cwd()) {
11103
+ if (isGlobalOnlyConfigKey(key)) {
11104
+ return {
11105
+ ok: false,
11106
+ errors: [
11107
+ `"${key}" is a global-only key. Unset it in ~/.assist.yml rather than under repos:`
11108
+ ]
11109
+ };
11110
+ }
11111
+ const { globalRaw, repos: repos2, label: label2, block } = resolveRepoConfigBlock(
11112
+ repoName,
11113
+ cwd
11114
+ );
11115
+ const { config: updatedBlock, removed } = unsetNestedValue(block, key);
11116
+ if (!removed) return { ok: true, target: "repo", label: label2, removed: false };
11117
+ const validation = validateConfig(updatedBlock, key, repoConfigSchema);
11118
+ if (!validation.ok) return validation;
11119
+ if (Object.keys(updatedBlock).length === 0) delete repos2[label2];
11120
+ else repos2[label2] = updatedBlock;
11121
+ const next3 = { ...globalRaw };
11122
+ if (Object.keys(repos2).length === 0) delete next3.repos;
11123
+ else next3.repos = repos2;
11124
+ saveGlobalConfig(next3);
11125
+ return { ok: true, target: "repo", label: label2, removed: true };
11126
+ }
11127
+
11128
+ // src/commands/sessions/web/applyScopedConfigUnset.ts
11129
+ function applyScopedConfigUnset(key, cwd, scope) {
11130
+ if (scope === "repo") {
11131
+ const result2 = applyRepoConfigUnset(key, void 0, cwd);
11132
+ return result2.ok ? {
11133
+ ok: true,
11134
+ payload: {
11135
+ target: result2.target,
11136
+ repoKey: result2.label,
11137
+ removed: result2.removed
11138
+ }
11139
+ } : result2;
11140
+ }
11141
+ const result = applyConfigUnset(key, scope === "global", cwd);
11142
+ return result.ok ? { ok: true, payload: { target: result.target, removed: result.removed } } : result;
11090
11143
  }
11091
11144
 
11092
11145
  // src/commands/sessions/web/unsetConfig.ts
@@ -11094,22 +11147,7 @@ function unsetConfig(req, res) {
11094
11147
  return handleConfigWrite(req, res, (request) => {
11095
11148
  if (!isKnownConfigKey(request.key))
11096
11149
  return { ok: false, errors: [`Unknown config key "${request.key}"`] };
11097
- if (request.scope === "repo")
11098
- return {
11099
- ok: false,
11100
- errors: [
11101
- "Clearing a repos override is not supported \u2014 remove the key from repos: in ~/.assist.yml"
11102
- ]
11103
- };
11104
- const result = applyConfigUnset(
11105
- request.key,
11106
- request.scope === "global",
11107
- request.cwd
11108
- );
11109
- return result.ok ? {
11110
- ok: true,
11111
- payload: { target: result.target, removed: result.removed }
11112
- } : result;
11150
+ return applyScopedConfigUnset(request.key, request.cwd, request.scope);
11113
11151
  });
11114
11152
  }
11115
11153
 
@@ -16566,16 +16604,48 @@ function applyRepoOrExit(key, coerced, repoName) {
16566
16604
 
16567
16605
  // src/commands/config/configUnset.ts
16568
16606
  import chalk132 from "chalk";
16607
+
16608
+ // src/commands/config/resolveRepoUnsetTarget.ts
16609
+ function resolveRepoUnsetTarget(key, repo) {
16610
+ if (typeof repo === "string") {
16611
+ if (key === void 0)
16612
+ return { key: repo, useRepo: true, repoName: void 0 };
16613
+ return { key, useRepo: true, repoName: repo };
16614
+ }
16615
+ return { key, useRepo: repo === true, repoName: void 0 };
16616
+ }
16617
+
16618
+ // src/commands/config/configUnset.ts
16569
16619
  function configUnset(key, options2 = {}) {
16570
- const result = applyConfigUnset(key, options2.global ?? false);
16620
+ if (options2.repo !== void 0 && !options2.global) {
16621
+ console.error(
16622
+ chalk132.red(
16623
+ "--repo removes from the global config; add -g (e.g. -g --repo)"
16624
+ )
16625
+ );
16626
+ process.exit(1);
16627
+ }
16628
+ const resolved = resolveRepoUnsetTarget(key, options2.repo);
16629
+ if (resolved.key === void 0) {
16630
+ console.error(chalk132.red("Missing required argument 'key'"));
16631
+ process.exit(1);
16632
+ return;
16633
+ }
16634
+ const result = resolved.useRepo ? applyRepoConfigUnset(resolved.key, resolved.repoName) : applyConfigUnset(resolved.key, options2.global ?? false);
16571
16635
  if (!result.ok) exitWithConfigErrors(result.errors);
16572
16636
  if (!result.removed) {
16573
16637
  console.log(
16574
- chalk132.yellow(`${key} is not set in the ${result.target} config`)
16638
+ chalk132.yellow(`${resolved.key} is not set in ${whereLabel(result)}`)
16575
16639
  );
16576
16640
  return;
16577
16641
  }
16578
- console.log(chalk132.green(`Unset ${key} (${result.target})`));
16642
+ console.log(chalk132.green(`Unset ${resolved.key} (${targetLabel(result)})`));
16643
+ }
16644
+ function whereLabel(result) {
16645
+ return result.target === "repo" ? `repos.${result.label}` : `the ${result.target} config`;
16646
+ }
16647
+ function targetLabel(result) {
16648
+ return result.target === "repo" ? `repo: ${result.label}` : result.target;
16579
16649
  }
16580
16650
 
16581
16651
  // src/commands/registerConfig.ts
@@ -16585,7 +16655,10 @@ function registerConfig(program2) {
16585
16655
  "-r, --repo [name]",
16586
16656
  "Requires -g: scope the global write to a repo's identity (defaults to the current repo)"
16587
16657
  ).action((key, value, options2) => configSet(key, value, options2));
16588
- configCommand.command("unset <key>").description("Remove a config value (e.g. commit.push)").option("-g, --global", "Remove from global ~/.assist.yml").action((key, options2) => configUnset(key, options2));
16658
+ configCommand.command("unset [key]").description("Remove a config value (e.g. commit.push)").option("-g, --global", "Remove from global ~/.assist.yml").option(
16659
+ "-r, --repo [name]",
16660
+ "Requires -g: remove the key from a repo's identity block (defaults to the current repo)"
16661
+ ).action((key, options2) => configUnset(key, options2));
16589
16662
  configCommand.command("get <key>").description("Get a config value").action(configGet);
16590
16663
  configCommand.command("list").description("List all config values").action(configList);
16591
16664
  }
@@ -18970,6 +19043,7 @@ function createWorktree(clone, strategy, boundTreeRoots2) {
18970
19043
 
18971
19044
  // src/commands/sessions/daemon/worktree/treeDurability.ts
18972
19045
  import { existsSync as existsSync45 } from "fs";
19046
+ var treeIsGone = { durable: true, gone: true };
18973
19047
  function treeDurability(state) {
18974
19048
  if (state.dirty) return { durable: false, reason: "uncommitted changes" };
18975
19049
  if (state.localOnlyCommits)
@@ -18999,14 +19073,14 @@ function* durabilityProbes() {
18999
19073
  });
19000
19074
  }
19001
19075
  async function checkDurability(cwd) {
19002
- if (!existsSync45(cwd)) return { durable: true };
19076
+ if (!existsSync45(cwd)) return treeIsGone;
19003
19077
  const probes = durabilityProbes();
19004
19078
  let step2 = probes.next();
19005
19079
  while (!step2.done) step2 = probes.next(await gitResult(cwd, step2.value));
19006
19080
  return step2.value;
19007
19081
  }
19008
19082
  function checkDurabilitySync(cwd) {
19009
- if (!existsSync45(cwd)) return { durable: true };
19083
+ if (!existsSync45(cwd)) return treeIsGone;
19010
19084
  const probes = durabilityProbes();
19011
19085
  let step2 = probes.next();
19012
19086
  while (!step2.done) step2 = probes.next(gitSyncResult(cwd, step2.value));
@@ -28400,6 +28474,7 @@ function readDesignSystemPrompt() {
28400
28474
  }
28401
28475
 
28402
28476
  // src/commands/sessions/daemon/spawnPty.ts
28477
+ import { existsSync as existsSync62 } from "fs";
28403
28478
  import * as pty from "node-pty";
28404
28479
 
28405
28480
  // src/commands/sessions/daemon/ensureSpawnHelperExecutable.ts
@@ -28424,7 +28499,15 @@ function ensureSpawnHelperExecutable() {
28424
28499
  }
28425
28500
 
28426
28501
  // src/commands/sessions/daemon/spawnPty.ts
28502
+ var MissingCwdError = class extends Error {
28503
+ constructor(cwd) {
28504
+ super(`working directory no longer exists: ${cwd}`);
28505
+ this.cwd = cwd;
28506
+ this.name = "MissingCwdError";
28507
+ }
28508
+ };
28427
28509
  function spawnPty(args, cwd, sessionId, extraEnv) {
28510
+ refuseMissingCwd(cwd, sessionId);
28428
28511
  ensureSpawnHelperExecutable();
28429
28512
  const shell = process.platform === "win32" ? "cmd.exe" : process.env.SHELL ?? "bash";
28430
28513
  const shellArgs = process.platform === "win32" ? ["/c", ...args] : ["-l", "-c", `exec ${args.map(shellEscape).join(" ")}`];
@@ -28445,6 +28528,13 @@ function spawnPty(args, cwd, sessionId, extraEnv) {
28445
28528
  }
28446
28529
  });
28447
28530
  }
28531
+ function refuseMissingCwd(cwd, sessionId) {
28532
+ if (!cwd || existsSync62(cwd)) return;
28533
+ daemonLog(
28534
+ `${sessionId ? `session ${sessionId}` : "pty"} not spawned: working directory ${cwd} no longer exists`
28535
+ );
28536
+ throw new MissingCwdError(cwd);
28537
+ }
28448
28538
  function shellEscape(s) {
28449
28539
  return `'${s.replace(/'/g, String.raw`'\''`)}'`;
28450
28540
  }
@@ -28578,11 +28668,11 @@ function otherTreeHolders(sessions, session) {
28578
28668
  }
28579
28669
 
28580
28670
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
28581
- import { existsSync as existsSync63 } from "fs";
28671
+ import { existsSync as existsSync64 } from "fs";
28582
28672
  import { basename as basename18 } from "path";
28583
28673
 
28584
28674
  // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
28585
- import { existsSync as existsSync62 } from "fs";
28675
+ import { existsSync as existsSync63 } from "fs";
28586
28676
  import { join as join74 } from "path";
28587
28677
 
28588
28678
  // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
@@ -28651,7 +28741,7 @@ async function deleteStrandedTree(clone, worktreePath, cause) {
28651
28741
  );
28652
28742
  }
28653
28743
  function strandedReason(worktreePath, cause) {
28654
- if (!existsSync62(join74(worktreePath, ".git")))
28744
+ if (!existsSync63(join74(worktreePath, ".git")))
28655
28745
  return "its .git link is already gone";
28656
28746
  if (/not a working tree|not a git repository/i.test(reason2(cause)))
28657
28747
  return "git no longer recognises it as a working tree";
@@ -28703,7 +28793,7 @@ function reason3(error) {
28703
28793
 
28704
28794
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
28705
28795
  async function reapWorktree(worktreePath, force = false) {
28706
- if (!existsSync63(worktreePath)) {
28796
+ if (!existsSync64(worktreePath)) {
28707
28797
  daemonLog(`worktree ${worktreePath} already gone; skipping reap`);
28708
28798
  return false;
28709
28799
  }
@@ -28724,7 +28814,7 @@ async function reapWorktree(worktreePath, force = false) {
28724
28814
  }
28725
28815
  function owningClone(worktreePath) {
28726
28816
  const recorded = worktreeAttributionIncludingReaped(worktreePath)?.clone;
28727
- if (recorded && existsSync63(recorded)) return recorded;
28817
+ if (recorded && existsSync64(recorded)) return recorded;
28728
28818
  const detected = mainWorktree(worktreePath);
28729
28819
  if (detected) return detected;
28730
28820
  daemonLog(
@@ -28811,12 +28901,12 @@ function setStatus2(session, newStatus) {
28811
28901
  }
28812
28902
 
28813
28903
  // src/commands/sessions/daemon/worktree/watchGitState.ts
28814
- import { existsSync as existsSync64, watch } from "fs";
28904
+ import { existsSync as existsSync65, watch } from "fs";
28815
28905
  var DEBOUNCE_MS = 500;
28816
28906
  var POLL_MS = 3e4;
28817
28907
  function watchGitState(cwd, onChange) {
28818
28908
  const common = gitCommonDir(cwd);
28819
- if (!common || !existsSync64(common)) return void 0;
28909
+ if (!common || !existsSync65(common)) return void 0;
28820
28910
  const watchers = [
28821
28911
  watchGitDir(common, onChange),
28822
28912
  pollGitState(cwd, onChange)
@@ -28862,6 +28952,10 @@ async function resolveCloseDurability(session, finalize, notify2) {
28862
28952
  holdStopped(session, tree, durability.reason, finalize, notify2);
28863
28953
  return;
28864
28954
  }
28955
+ if (durability.gone)
28956
+ daemonLog(
28957
+ `session ${session.id} closing: worktree ${tree.path} is gone from disk \u2014 released with nothing to land, not landed work`
28958
+ );
28865
28959
  if (tree.removable) await reapWorktree(tree.path);
28866
28960
  session.worktree = void 0;
28867
28961
  session.undurable = void 0;
@@ -29220,10 +29314,10 @@ function emitSessionOutput(session, clients, data) {
29220
29314
  }
29221
29315
 
29222
29316
  // src/commands/sessions/daemon/exitReason.ts
29223
- import { existsSync as existsSync65 } from "fs";
29317
+ import { existsSync as existsSync66 } from "fs";
29224
29318
  function exitReason(session, exitCode) {
29225
29319
  const base = `process exited with code ${exitCode}`;
29226
- if (session.cwd && !existsSync65(session.cwd))
29320
+ if (session.cwd && !existsSync66(session.cwd))
29227
29321
  return `${base}: working directory ${session.cwd} no longer exists`;
29228
29322
  return base;
29229
29323
  }
@@ -29241,7 +29335,7 @@ function handleFailedResume(session, exitCode, onStatusChange) {
29241
29335
  }
29242
29336
 
29243
29337
  // src/commands/sessions/daemon/watchActivity.ts
29244
- import { existsSync as existsSync66, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
29338
+ import { existsSync as existsSync67, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
29245
29339
  import { dirname as dirname33 } from "path";
29246
29340
 
29247
29341
  // src/commands/sessions/daemon/applyReviewPause.ts
@@ -29322,7 +29416,7 @@ function watchActivity(session, notify2, onClaudeSessionId) {
29322
29416
  if (timer) clearTimeout(timer);
29323
29417
  timer = setTimeout(read, DEBOUNCE_MS2);
29324
29418
  });
29325
- if (existsSync66(path71)) read();
29419
+ if (existsSync67(path71)) read();
29326
29420
  }
29327
29421
  function refreshActivity(session) {
29328
29422
  if (session.commandType !== "assist" || !session.cwd) return;
@@ -30034,6 +30128,26 @@ function makeStatusChangeHandler(sessions, dismiss, notify2, reuseForRun) {
30034
30128
  );
30035
30129
  }
30036
30130
 
30131
+ // src/commands/sessions/daemon/refuseSpawn.ts
30132
+ function refuseSpawn(session, error, clients, onStatusChange, stage = "respawned") {
30133
+ const reason4 = error instanceof Error ? error.message : String(error);
30134
+ session.pty = null;
30135
+ session.pendingStart = void 0;
30136
+ session.error = reason4;
30137
+ daemonLog(
30138
+ `session ${session.id} ("${session.name}") not ${stage}: ${reason4}`
30139
+ );
30140
+ broadcast(clients, { type: "clear", sessionId: session.id });
30141
+ emitSessionOutput(
30142
+ session,
30143
+ clients,
30144
+ `\r
30145
+ \x1B[31mCannot start this session: ${reason4}\x1B[0m\r
30146
+ `
30147
+ );
30148
+ onStatusChange(session, "error");
30149
+ }
30150
+
30037
30151
  // src/commands/sessions/daemon/respawnSession.ts
30038
30152
  function respawnSession(session, respawn, status3, clients, onStatusChange) {
30039
30153
  session.gitWatcher?.close();
@@ -30047,7 +30161,12 @@ function respawnSession(session, respawn, status3, clients, onStatusChange) {
30047
30161
  session.runningSince = null;
30048
30162
  setStatus2(session, status3);
30049
30163
  session.restored = void 0;
30050
- session.pty = respawn();
30164
+ try {
30165
+ session.pty = respawn();
30166
+ } catch (error) {
30167
+ refuseSpawn(session, error, clients, onStatusChange);
30168
+ return;
30169
+ }
30051
30170
  if (session.cols && session.rows)
30052
30171
  try {
30053
30172
  session.pty?.resize(session.cols, session.rows);
@@ -30182,6 +30301,9 @@ function restoreBase(id, persisted) {
30182
30301
  function errorSession(id, persisted, error) {
30183
30302
  return {
30184
30303
  ...restoreBase(id, persisted),
30304
+ scrollback: `\r
30305
+ \x1B[31m${error}\x1B[0m\r
30306
+ `,
30185
30307
  status: "error",
30186
30308
  startedAt: persisted.startedAt,
30187
30309
  runningMs: persisted.runningMs ?? 0,
@@ -30411,7 +30533,7 @@ function rearmStoppedSessions(sessions, notify2) {
30411
30533
  }
30412
30534
 
30413
30535
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
30414
- import { existsSync as existsSync69 } from "fs";
30536
+ import { existsSync as existsSync70 } from "fs";
30415
30537
  import { basename as basename20 } from "path";
30416
30538
 
30417
30539
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -30501,9 +30623,9 @@ function capped(lines) {
30501
30623
  }
30502
30624
 
30503
30625
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
30504
- import { existsSync as existsSync68 } from "fs";
30626
+ import { existsSync as existsSync69 } from "fs";
30505
30627
  async function reclaimVanishedWorktrees(clone, paths) {
30506
- if (!existsSync68(clone)) {
30628
+ if (!existsSync69(clone)) {
30507
30629
  for (const { path: path71 } of paths) forgetWorktree(path71);
30508
30630
  daemonLog(
30509
30631
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
@@ -30586,6 +30708,21 @@ function recoveryNotice({ orphan, held }) {
30586
30708
  ].join("");
30587
30709
  }
30588
30710
 
30711
+ // src/commands/sessions/daemon/worktree/logVanishedTree.ts
30712
+ function logVanishedTree(sessions, path71) {
30713
+ const claimants = [...sessions.values()].filter(
30714
+ (s) => s.cwd === path71 || s.worktree?.path === path71
30715
+ );
30716
+ if (claimants.length === 0) {
30717
+ daemonLog(`worktree ${path71} gone from disk and unclaimed; reclaiming it`);
30718
+ return;
30719
+ }
30720
+ for (const s of claimants)
30721
+ daemonLog(
30722
+ `session ${s.id} ("${s.name}") claims worktree ${path71}, which is gone from disk; reclaiming it`
30723
+ );
30724
+ }
30725
+
30589
30726
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
30590
30727
  function reconcileWorktreesOnRestore(sessions, spawnWith, notify2) {
30591
30728
  bindRestoredWorktrees(sessions);
@@ -30595,14 +30732,15 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
30595
30732
  const accounted = accountedTrees(sessions);
30596
30733
  const vanished = /* @__PURE__ */ new Map();
30597
30734
  for (const { path: path71, clone } of readWorktreeRegistry()) {
30598
- if (accounted.has(path71)) continue;
30599
- if (!existsSync69(path71)) {
30735
+ if (!existsSync70(path71)) {
30736
+ logVanishedTree(sessions, path71);
30600
30737
  vanished.set(clone, [
30601
30738
  ...vanished.get(clone) ?? [],
30602
30739
  { path: path71, branch: basename20(path71) }
30603
30740
  ]);
30604
30741
  continue;
30605
30742
  }
30743
+ if (accounted.has(path71)) continue;
30606
30744
  await recoverOrphan(sessions, spawnWith, { path: path71, clone }, notify2);
30607
30745
  }
30608
30746
  for (const [clone, paths] of vanished)
@@ -30742,11 +30880,16 @@ function holdsTree(session, root) {
30742
30880
  if (session.status !== "done" && session.status !== "error") return true;
30743
30881
  if (!worktreeConfigFor(root).enabled) return true;
30744
30882
  const durability = checkDurabilitySync(root);
30745
- daemonLog(
30746
- 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}`
30747
- );
30883
+ daemonLog(`tree ${root} ${releaseReason(durability, session)}`);
30748
30884
  return !durability.durable;
30749
30885
  }
30886
+ function releaseReason(durability, session) {
30887
+ if (!durability.durable)
30888
+ return `still held by finished session ${session.id}: ${durability.reason}`;
30889
+ if (durability.gone)
30890
+ return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
30891
+ return `free for allocation: session ${session.id} finished and its work is landed`;
30892
+ }
30750
30893
 
30751
30894
  // src/commands/sessions/daemon/worktree/planReuseTree.ts
30752
30895
  function planReuseTree(session, ctx) {
@@ -30768,13 +30911,24 @@ function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
30768
30911
  resetCardForRun(session, assistArgs);
30769
30912
  const alloc = planReuseTree(session, tree);
30770
30913
  if (alloc) session.cwd = alloc.cwd;
30771
- Object.assign(
30772
- session,
30773
- startOrHoldPty(
30774
- () => spawnPty(["assist", ...assistArgs], session.cwd, session.id),
30775
- alloc !== void 0
30776
- )
30777
- );
30914
+ try {
30915
+ Object.assign(
30916
+ session,
30917
+ startOrHoldPty(
30918
+ () => spawnPty(["assist", ...assistArgs], session.cwd, session.id),
30919
+ alloc !== void 0
30920
+ )
30921
+ );
30922
+ } catch (error) {
30923
+ refuseSpawn(
30924
+ session,
30925
+ error,
30926
+ clients,
30927
+ onStatusChange,
30928
+ `reused for backlog run ${itemId2}`
30929
+ );
30930
+ return;
30931
+ }
30778
30932
  broadcast(clients, { type: "clear", sessionId: session.id });
30779
30933
  if (alloc)
30780
30934
  bindNewWorktree(
@@ -30798,26 +30952,45 @@ function shutdownSessions(sessions) {
30798
30952
  }
30799
30953
  }
30800
30954
 
30801
- // src/commands/sessions/daemon/startHeldSession.ts
30802
- function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
30803
- const start3 = session.pendingStart;
30804
- if (!start3) return;
30805
- session.pendingStart = void 0;
30955
+ // src/commands/sessions/daemon/heldStartBlocked.ts
30956
+ function heldStartBlocked(session, sessions) {
30957
+ const where = session.worktree?.path ?? "no worktree";
30806
30958
  if (sessions.get(session.id) !== session) {
30807
30959
  daemonLog(
30808
- `session ${session.id} not started after seeding: card is gone (${session.worktree?.path ?? "no worktree"})`
30960
+ `session ${session.id} not started after seeding: card is gone (${where})`
30809
30961
  );
30810
- return;
30962
+ return true;
30811
30963
  }
30812
30964
  if (session.closing) {
30813
30965
  daemonLog(
30814
- `session ${session.id} not started after seeding: card is closing (${session.worktree?.path ?? "no worktree"})`
30966
+ `session ${session.id} not started after seeding: card is closing (${where})`
30815
30967
  );
30816
- return;
30968
+ return true;
30817
30969
  }
30970
+ return false;
30971
+ }
30972
+
30973
+ // src/commands/sessions/daemon/startHeldSession.ts
30974
+ function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
30975
+ const start3 = session.pendingStart;
30976
+ if (!start3) return;
30977
+ session.pendingStart = void 0;
30978
+ if (heldStartBlocked(session, sessions)) return;
30818
30979
  session.startedAt = Date.now();
30819
30980
  if (session.status === "running") session.runningSince = session.startedAt;
30820
- session.pty = start3();
30981
+ try {
30982
+ session.pty = start3();
30983
+ } catch (error) {
30984
+ refuseSpawn(
30985
+ session,
30986
+ error,
30987
+ clients,
30988
+ onStatusChange,
30989
+ "started after seeding"
30990
+ );
30991
+ notify2();
30992
+ return;
30993
+ }
30821
30994
  if (session.cols && session.rows)
30822
30995
  try {
30823
30996
  session.pty?.resize(session.cols, session.rows);
@@ -30964,13 +31137,13 @@ async function defaultConnect() {
30964
31137
  }
30965
31138
 
30966
31139
  // src/commands/sessions/daemon/hasPersistedWindowsSessions.ts
30967
- import { existsSync as existsSync70, readFileSync as readFileSync51 } from "fs";
31140
+ import { existsSync as existsSync71, readFileSync as readFileSync51 } from "fs";
30968
31141
  import { posix } from "path";
30969
31142
  function hasPersistedWindowsSessions() {
30970
31143
  const sessionsFile = windowsSessionsFileFromWsl();
30971
31144
  if (!sessionsFile) return false;
30972
31145
  try {
30973
- if (!existsSync70(sessionsFile)) return false;
31146
+ if (!existsSync71(sessionsFile)) return false;
30974
31147
  const data = JSON.parse(readFileSync51(sessionsFile, "utf8"));
30975
31148
  return Array.isArray(data) && data.length > 0;
30976
31149
  } catch (error) {
@@ -32005,9 +32178,9 @@ function safeParse2(line) {
32005
32178
  }
32006
32179
 
32007
32180
  // src/commands/sessions/daemon/repoDirExists.ts
32008
- import { existsSync as existsSync71 } from "fs";
32181
+ import { existsSync as existsSync72 } from "fs";
32009
32182
  function repoDirExists(cwd) {
32010
- return existsSync71(toGitCwd(cwd));
32183
+ return existsSync72(toGitCwd(cwd));
32011
32184
  }
32012
32185
 
32013
32186
  // src/commands/sessions/daemon/withRepoGroups.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.488.0",
3
+ "version": "0.488.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {