@staff0rd/assist 0.482.0 → 0.483.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.482.0",
9
+ version: "0.483.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -399,7 +399,7 @@ var DEFAULT_WAKE_WORDS = ["computer"];
399
399
  var DEFAULT_MODELS_DIR = "~/.assist/voice/models";
400
400
  var DEFAULT_BACKUP_DIR = "~/.assist/backups";
401
401
  var DEFAULT_CLONE_DIR = "~/git";
402
- var assistConfigSchema = z2.strictObject({
402
+ var assistConfigShape = {
403
403
  backup: z2.strictObject({
404
404
  dir: z2.string().default(DEFAULT_BACKUP_DIR)
405
405
  }).default({ dir: DEFAULT_BACKUP_DIR }),
@@ -451,7 +451,6 @@ var assistConfigSchema = z2.strictObject({
451
451
  required: z2.boolean().default(false),
452
452
  promptJira: z2.boolean().default(false)
453
453
  }).optional(),
454
- repos: z2.record(z2.string(), z2.record(z2.string(), z2.unknown())).optional(),
455
454
  worktree: z2.strictObject({
456
455
  enabled: z2.boolean().default(false),
457
456
  trunk: z2.boolean().default(false),
@@ -571,6 +570,11 @@ var assistConfigSchema = z2.strictObject({
571
570
  modelsDir: DEFAULT_MODELS_DIR,
572
571
  models: {}
573
572
  })
573
+ };
574
+ var repoConfigSchema = z2.strictObject(assistConfigShape).partial();
575
+ var assistConfigSchema = z2.strictObject({
576
+ ...assistConfigShape,
577
+ repos: z2.record(z2.string(), repoConfigSchema).optional()
574
578
  });
575
579
 
576
580
  // src/shared/loadConfigFrom.ts
@@ -6665,10 +6669,10 @@ async function prepareRun(id, resumeSessionId) {
6665
6669
  import chalk39 from "chalk";
6666
6670
  function reportDuplicateRun(itemId2, holder) {
6667
6671
  const since = holder.timestamp ? `, started ${holder.timestamp}` : "";
6668
- const reason2 = `another session is already running it (pid ${holder.pid}${since}); close that session before running it again`;
6672
+ const reason4 = `another session is already running it (pid ${holder.pid}${since}); close that session before running it again`;
6669
6673
  console.log(chalk39.yellow(`Already running: ${formatItemId(itemId2)}`));
6670
- console.log(chalk39.dim(`Refusing to start a second run \u2014 ${reason2}.`));
6671
- appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${reason2}`);
6674
+ console.log(chalk39.dim(`Refusing to start a second run \u2014 ${reason4}.`));
6675
+ appendDaemonLog(`backlog run ${itemId2}: refused \u2014 ${reason4}`);
6672
6676
  }
6673
6677
 
6674
6678
  // src/commands/backlog/consumePause.ts
@@ -8332,11 +8336,11 @@ import {
8332
8336
  var SPAWN_TIMEOUT_MS = 1e4;
8333
8337
  var RETRY_DELAY_MS2 = 200;
8334
8338
  var STALE_LOCK_MS = SPAWN_TIMEOUT_MS + 5e3;
8335
- async function ensureDaemonRunning(reason2 = "unspecified") {
8339
+ async function ensureDaemonRunning(reason4 = "unspecified") {
8336
8340
  if (await isDaemonRunning()) return;
8337
8341
  mkdirSync10(daemonPaths.dir, { recursive: true });
8338
8342
  const holdsLock = acquireSpawnLock();
8339
- if (holdsLock) spawnDaemon(reason2);
8343
+ if (holdsLock) spawnDaemon(reason4);
8340
8344
  try {
8341
8345
  await waitForDaemon();
8342
8346
  } finally {
@@ -8385,12 +8389,12 @@ function releaseSpawnLock() {
8385
8389
  } catch {
8386
8390
  }
8387
8391
  }
8388
- function spawnDaemon(reason2) {
8392
+ function spawnDaemon(reason4) {
8389
8393
  const log = openSync2(daemonPaths.log, "a");
8390
8394
  const child = spawn4(process.execPath, [process.argv[1], "daemon", "run"], {
8391
8395
  detached: true,
8392
8396
  stdio: ["ignore", log, log],
8393
- env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON: reason2 }
8397
+ env: { ...process.env, ASSIST_DAEMON_SPAWN_REASON: reason4 }
8394
8398
  });
8395
8399
  child.unref();
8396
8400
  }
@@ -8971,7 +8975,7 @@ async function patchSubtaskStatus(req, res, id, idx) {
8971
8975
  }
8972
8976
 
8973
8977
  // src/commands/backlog/rewindItemToPhase.ts
8974
- async function rewindItemToPhase(orm, item, phaseNumber, reason2) {
8978
+ async function rewindItemToPhase(orm, item, phaseNumber, reason4) {
8975
8979
  const plan2 = resolveRewindPlan(item);
8976
8980
  if (phaseNumber < 1 || phaseNumber > plan2.length) {
8977
8981
  return {
@@ -8990,7 +8994,7 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason2) {
8990
8994
  await appendComment(
8991
8995
  orm,
8992
8996
  item.id,
8993
- `Rewound to phase ${phaseNumber} (${phaseName}): ${reason2}`,
8997
+ `Rewound to phase ${phaseNumber} (${phaseName}): ${reason4}`,
8994
8998
  { phase: phaseNumber }
8995
8999
  );
8996
9000
  await updateCurrentPhase(orm, item.id, phaseNumber);
@@ -9001,11 +9005,11 @@ async function rewindItemToPhase(orm, item, phaseNumber, reason2) {
9001
9005
 
9002
9006
  // src/commands/backlog/web/rewindItemPhase.ts
9003
9007
  async function rewindItemPhase(req, res, id) {
9004
- const { phase, reason: reason2 } = await parseRewindBody(req);
9008
+ const { phase, reason: reason4 } = await parseRewindBody(req);
9005
9009
  const result = await findItemOr404(res, id);
9006
9010
  if (!result) return;
9007
9011
  const { orm, item } = result;
9008
- const outcome = await rewindItemToPhase(orm, item, phase, reason2);
9012
+ const outcome = await rewindItemToPhase(orm, item, phase, reason4);
9009
9013
  if (!outcome.ok) {
9010
9014
  respondJson(res, 400, { error: outcome.error });
9011
9015
  return;
@@ -9568,7 +9572,7 @@ function readConfigEntries(cwd) {
9568
9572
  value: getNestedValue(config, leaf.key),
9569
9573
  source,
9570
9574
  sources,
9571
- ...source === "repo" && layers.repoKey ? { repoKey: layers.repoKey } : {},
9575
+ ...layers.repoKey ? { repoKey: layers.repoKey } : {},
9572
9576
  globalOnly: isGlobalOnlyConfigKey(leaf.key),
9573
9577
  node: configEntryNode(schema2, leaf.key)
9574
9578
  };
@@ -10402,90 +10406,6 @@ function sessionView(_req, res) {
10402
10406
  respondJson(res, 200, { floatWaiting });
10403
10407
  }
10404
10408
 
10405
- // src/commands/config/setNestedValue.ts
10406
- function isPlainObject2(val) {
10407
- return val !== null && typeof val === "object" && !Array.isArray(val);
10408
- }
10409
- function isNumericKey(key) {
10410
- return /^\d+$/.test(key);
10411
- }
10412
- function resolveKey(key) {
10413
- return isNumericKey(key) ? Number.parseInt(key, 10) : key;
10414
- }
10415
- function getItem(container, key) {
10416
- if (Array.isArray(container)) return container[key];
10417
- return container[key];
10418
- }
10419
- function setItem(container, key, value) {
10420
- if (Array.isArray(container)) container[key] = value;
10421
- else container[key] = value;
10422
- }
10423
- function ensureArray(container, key) {
10424
- const existing = getItem(container, key);
10425
- const arr = Array.isArray(existing) ? [...existing] : [];
10426
- setItem(container, key, arr);
10427
- return arr;
10428
- }
10429
- function ensureObject(container, key) {
10430
- const existing = getItem(container, key);
10431
- const obj = isPlainObject2(existing) ? { ...existing } : {};
10432
- setItem(container, key, obj);
10433
- return obj;
10434
- }
10435
- function stepIntoNested(container, key, nextKey) {
10436
- const resolved = resolveKey(key);
10437
- if (nextKey !== void 0 && isNumericKey(nextKey)) {
10438
- return ensureArray(container, resolved);
10439
- }
10440
- return ensureObject(container, resolved);
10441
- }
10442
- function setNestedValue(obj, path71, value) {
10443
- const keys = path71.split(".");
10444
- const result = { ...obj };
10445
- let current = result;
10446
- for (let i = 0; i < keys.length - 1; i++) {
10447
- current = stepIntoNested(current, keys[i], keys[i + 1]);
10448
- }
10449
- setItem(current, resolveKey(keys[keys.length - 1]), value);
10450
- return result;
10451
- }
10452
-
10453
- // src/commands/config/validateConfig.ts
10454
- function validateConfig(updated, key) {
10455
- const result = assistConfigSchema.safeParse(updated);
10456
- if (result.success) return { ok: true };
10457
- return {
10458
- ok: false,
10459
- errors: result.error.issues.map((issue) => formatIssue(issue, key))
10460
- };
10461
- }
10462
- function formatIssue(issue, key) {
10463
- const path71 = issue.path.length > 0 ? issue.path.join(".") : key;
10464
- return `${path71}: ${issue.message}`;
10465
- }
10466
-
10467
- // src/commands/config/applyConfigSet.ts
10468
- function applyConfigSet(key, coerced, global, cwd = process.cwd()) {
10469
- if (!global && isGlobalOnlyConfigKey(key)) {
10470
- return {
10471
- ok: false,
10472
- errors: [
10473
- `"${key}" is a global-only key. Use --global to set it in ~/.assist.yml`
10474
- ]
10475
- };
10476
- }
10477
- const raw = global ? loadGlobalConfigRaw() : loadProjectConfig(cwd);
10478
- const updated = setNestedValue(raw, key, coerced);
10479
- const validation = validateConfig(updated, key);
10480
- if (!validation.ok) return validation;
10481
- if (global) {
10482
- saveGlobalConfig(updated);
10483
- return { ok: true, target: "global" };
10484
- }
10485
- saveConfig(updated, cwd);
10486
- return { ok: true, target: "project" };
10487
- }
10488
-
10489
10409
  // src/commands/config/coerceConfigScalar.ts
10490
10410
  function coerceConfigScalar(key, raw, type) {
10491
10411
  if (type === "boolean") return coerceBoolean(key, raw);
@@ -10534,10 +10454,10 @@ function coerceStructuredConfigValue(key, raw) {
10534
10454
  return { ok: true, value: parsed.data };
10535
10455
  return {
10536
10456
  ok: false,
10537
- error: parsed.error.issues.map((issue) => formatIssue2(key, issue)).join("\n")
10457
+ error: parsed.error.issues.map((issue) => formatIssue(key, issue)).join("\n")
10538
10458
  };
10539
10459
  }
10540
- function formatIssue2(key, issue) {
10460
+ function formatIssue(key, issue) {
10541
10461
  const path71 = formatConfigPath([
10542
10462
  { kind: "key", name: key },
10543
10463
  ...issue.path.map(toSegment)
@@ -10591,6 +10511,152 @@ function coerceUnion(key, raw, types) {
10591
10511
  return { ok: false, error: `${key}: expected ${types.join(" or ")}` };
10592
10512
  }
10593
10513
 
10514
+ // src/commands/config/setNestedValue.ts
10515
+ function isPlainObject2(val) {
10516
+ return val !== null && typeof val === "object" && !Array.isArray(val);
10517
+ }
10518
+ function isNumericKey(key) {
10519
+ return /^\d+$/.test(key);
10520
+ }
10521
+ function resolveKey(key) {
10522
+ return isNumericKey(key) ? Number.parseInt(key, 10) : key;
10523
+ }
10524
+ function getItem(container, key) {
10525
+ if (Array.isArray(container)) return container[key];
10526
+ return container[key];
10527
+ }
10528
+ function setItem(container, key, value) {
10529
+ if (Array.isArray(container)) container[key] = value;
10530
+ else container[key] = value;
10531
+ }
10532
+ function ensureArray(container, key) {
10533
+ const existing = getItem(container, key);
10534
+ const arr = Array.isArray(existing) ? [...existing] : [];
10535
+ setItem(container, key, arr);
10536
+ return arr;
10537
+ }
10538
+ function ensureObject(container, key) {
10539
+ const existing = getItem(container, key);
10540
+ const obj = isPlainObject2(existing) ? { ...existing } : {};
10541
+ setItem(container, key, obj);
10542
+ return obj;
10543
+ }
10544
+ function stepIntoNested(container, key, nextKey) {
10545
+ const resolved = resolveKey(key);
10546
+ if (nextKey !== void 0 && isNumericKey(nextKey)) {
10547
+ return ensureArray(container, resolved);
10548
+ }
10549
+ return ensureObject(container, resolved);
10550
+ }
10551
+ function setNestedValue(obj, path71, value) {
10552
+ const keys = path71.split(".");
10553
+ const result = { ...obj };
10554
+ let current = result;
10555
+ for (let i = 0; i < keys.length - 1; i++) {
10556
+ current = stepIntoNested(current, keys[i], keys[i + 1]);
10557
+ }
10558
+ setItem(current, resolveKey(keys[keys.length - 1]), value);
10559
+ return result;
10560
+ }
10561
+
10562
+ // src/commands/config/validateConfig.ts
10563
+ function validateConfig(updated, key, schema2 = assistConfigSchema) {
10564
+ const result = schema2.safeParse(updated);
10565
+ if (result.success) return { ok: true };
10566
+ return {
10567
+ ok: false,
10568
+ errors: result.error.issues.map((issue) => formatIssue2(issue, key))
10569
+ };
10570
+ }
10571
+ function formatIssue2(issue, key) {
10572
+ const path71 = issue.path.length > 0 ? issue.path.join(".") : key;
10573
+ return `${path71}: ${issue.message}`;
10574
+ }
10575
+
10576
+ // src/commands/config/applyConfigSet.ts
10577
+ function applyConfigSet(key, coerced, global, cwd = process.cwd()) {
10578
+ if (!global && isGlobalOnlyConfigKey(key)) {
10579
+ return {
10580
+ ok: false,
10581
+ errors: [
10582
+ `"${key}" is a global-only key. Use --global to set it in ~/.assist.yml`
10583
+ ]
10584
+ };
10585
+ }
10586
+ const raw = global ? loadGlobalConfigRaw() : loadProjectConfig(cwd);
10587
+ const updated = setNestedValue(raw, key, coerced);
10588
+ const validation = validateConfig(updated, key);
10589
+ if (!validation.ok) return validation;
10590
+ if (global) {
10591
+ saveGlobalConfig(updated);
10592
+ return { ok: true, target: "global" };
10593
+ }
10594
+ saveConfig(updated, cwd);
10595
+ return { ok: true, target: "project" };
10596
+ }
10597
+
10598
+ // src/shared/resolveNamedRepoWriteLabel.ts
10599
+ var UnknownRepoConfigError = class extends Error {
10600
+ name = "UnknownRepoConfigError";
10601
+ };
10602
+ function resolveNamedRepoWriteLabel(globalRaw, name) {
10603
+ const repos2 = globalRaw.repos;
10604
+ const entries = repos2 && typeof repos2 === "object" && !Array.isArray(repos2) ? repos2 : {};
10605
+ const candidates = originKeyCandidates(name);
10606
+ const matches = Object.keys(entries).filter((key) => candidates.has(key));
10607
+ if (matches.length === 0) {
10608
+ throw new UnknownRepoConfigError(
10609
+ `No repo in ~/.assist.yml matches "${name}". Use an existing repos key (full origin, org/repo, or bare repo name).`
10610
+ );
10611
+ }
10612
+ if (matches.length > 1) {
10613
+ throw new AmbiguousRepoConfigError(
10614
+ `Ambiguous repos config in ~/.assist.yml: keys ${matches.map((m) => `"${m}"`).join(", ")} all match "${name}". Keep a single key per repository.`
10615
+ );
10616
+ }
10617
+ return matches[0];
10618
+ }
10619
+
10620
+ // src/commands/config/applyRepoConfigSet.ts
10621
+ function applyRepoConfigSet(key, coerced, repoName, cwd = process.cwd()) {
10622
+ if (isGlobalOnlyConfigKey(key)) {
10623
+ return {
10624
+ ok: false,
10625
+ errors: [
10626
+ `"${key}" is a global-only key. Set it in ~/.assist.yml rather than under repos:`
10627
+ ]
10628
+ };
10629
+ }
10630
+ const globalRaw = loadGlobalConfigRaw();
10631
+ const label2 = repoName === void 0 ? resolveRepoWriteLabel(globalRaw, getCurrentOrigin(cwd)) : resolveNamedRepoWriteLabel(globalRaw, repoName);
10632
+ const repos2 = isPlainObject3(globalRaw.repos) ? { ...globalRaw.repos } : {};
10633
+ const existingBlock = isPlainObject3(repos2[label2]) ? repos2[label2] : {};
10634
+ const updatedBlock = setNestedValue(existingBlock, key, coerced);
10635
+ const validation = validateConfig(updatedBlock, key, repoConfigSchema);
10636
+ if (!validation.ok) return validation;
10637
+ repos2[label2] = updatedBlock;
10638
+ saveGlobalConfig({ ...globalRaw, repos: repos2 });
10639
+ return { ok: true, target: "repo", label: label2 };
10640
+ }
10641
+ function isPlainObject3(value) {
10642
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10643
+ }
10644
+
10645
+ // src/commands/sessions/web/applyScopedConfigSet.ts
10646
+ function applyScopedConfigSet(key, value, cwd, scope) {
10647
+ if (scope === "repo") {
10648
+ const result2 = applyRepoConfigSet(key, value, void 0, cwd);
10649
+ return result2.ok ? { ok: true, payload: { target: result2.target, repoKey: result2.label } } : result2;
10650
+ }
10651
+ const result = applyConfigSet(key, value, scope === "global", cwd);
10652
+ return result.ok ? { ok: true, payload: { target: result.target } } : result;
10653
+ }
10654
+
10655
+ // src/commands/config/ConfigWriteScope.ts
10656
+ function isConfigWriteScope(value) {
10657
+ return value === "project" || value === "global" || value === "repo";
10658
+ }
10659
+
10594
10660
  // src/commands/sessions/web/readJsonBody.ts
10595
10661
  async function readJsonBody(req) {
10596
10662
  const chunks = [];
@@ -10611,9 +10677,9 @@ async function parseConfigWriteRequest(req) {
10611
10677
  return { ok: false, error: "Missing key" };
10612
10678
  if (typeof cwd !== "string" || cwd === "")
10613
10679
  return { ok: false, error: "Missing cwd" };
10614
- if (scope !== "project" && scope !== "global")
10615
- return { ok: false, error: 'scope must be "project" or "global"' };
10616
- return { ok: true, key, value: body.value, cwd, global: scope === "global" };
10680
+ if (!isConfigWriteScope(scope))
10681
+ return { ok: false, error: 'scope must be "project", "global" or "repo"' };
10682
+ return { ok: true, key, value: body.value, cwd, scope };
10617
10683
  }
10618
10684
 
10619
10685
  // src/commands/sessions/web/handleConfigWrite.ts
@@ -10645,22 +10711,21 @@ function setConfig(req, res) {
10645
10711
  return handleConfigWrite(req, res, (request) => {
10646
10712
  const coerced = coerceConfigValue(request.key, request.value);
10647
10713
  if (!coerced.ok) return { ok: false, errors: [coerced.error] };
10648
- const result = applyConfigSet(
10714
+ return applyScopedConfigSet(
10649
10715
  request.key,
10650
10716
  coerced.value,
10651
- request.global,
10652
- request.cwd
10717
+ request.cwd,
10718
+ request.scope
10653
10719
  );
10654
- return result.ok ? { ok: true, payload: { target: result.target } } : result;
10655
10720
  });
10656
10721
  }
10657
10722
 
10658
10723
  // src/commands/config/unsetNestedValue.ts
10659
- function isPlainObject3(val) {
10724
+ function isPlainObject4(val) {
10660
10725
  return val !== null && typeof val === "object" && !Array.isArray(val);
10661
10726
  }
10662
10727
  function isContainer(val) {
10663
- return Array.isArray(val) || isPlainObject3(val);
10728
+ return Array.isArray(val) || isPlainObject4(val);
10664
10729
  }
10665
10730
  function isNumericKey2(key) {
10666
10731
  return /^\d+$/.test(key);
@@ -10696,7 +10761,7 @@ function withChild(container, key, child) {
10696
10761
  return { ...container, [key]: child };
10697
10762
  }
10698
10763
  function isPrunable(container) {
10699
- return isPlainObject3(container) && Object.keys(container).length === 0;
10764
+ return isPlainObject4(container) && Object.keys(container).length === 0;
10700
10765
  }
10701
10766
  function replaceOrPrune(container, key, child) {
10702
10767
  return isPrunable(child) ? withoutKey(container, key) : withChild(container, key, child);
@@ -10755,7 +10820,18 @@ function unsetConfig(req, res) {
10755
10820
  return handleConfigWrite(req, res, (request) => {
10756
10821
  if (!isKnownConfigKey(request.key))
10757
10822
  return { ok: false, errors: [`Unknown config key "${request.key}"`] };
10758
- const result = applyConfigUnset(request.key, request.global, request.cwd);
10823
+ if (request.scope === "repo")
10824
+ return {
10825
+ ok: false,
10826
+ errors: [
10827
+ "Clearing a repos override is not supported \u2014 remove the key from repos: in ~/.assist.yml"
10828
+ ]
10829
+ };
10830
+ const result = applyConfigUnset(
10831
+ request.key,
10832
+ request.scope === "global",
10833
+ request.cwd
10834
+ );
10759
10835
  return result.ok ? {
10760
10836
  ok: true,
10761
10837
  payload: { target: result.target, removed: result.removed }
@@ -11750,8 +11826,8 @@ function countCopyRows(data) {
11750
11826
 
11751
11827
  // src/commands/backlog/dump/parseDump.ts
11752
11828
  var NEWLINE = 10;
11753
- function invalid(reason2) {
11754
- throw new Error(`Invalid dump: ${reason2}`);
11829
+ function invalid(reason4) {
11830
+ throw new Error(`Invalid dump: ${reason4}`);
11755
11831
  }
11756
11832
  function readLine(dump, start3) {
11757
11833
  const eol = dump.indexOf(NEWLINE, start3);
@@ -13123,14 +13199,14 @@ async function unstar(id) {
13123
13199
 
13124
13200
  // src/commands/backlog/wontdo/index.ts
13125
13201
  import chalk98 from "chalk";
13126
- async function wontdo(id, reason2) {
13202
+ async function wontdo(id, reason4) {
13127
13203
  const found = await findOneItem(id);
13128
13204
  if (!found) return;
13129
13205
  const { orm, item } = found;
13130
13206
  await updateStatus(orm, item.id, "wontdo");
13131
- if (reason2) {
13207
+ if (reason4) {
13132
13208
  const phase = item.currentPhase ?? 1;
13133
- await appendComment(orm, item.id, reason2, { phase, type: "summary" });
13209
+ await appendComment(orm, item.id, reason4, { phase, type: "summary" });
13134
13210
  }
13135
13211
  console.log(
13136
13212
  chalk98.red(`Won't do item ${formatItemId(item.id)}: ${item.name}`)
@@ -14697,9 +14773,9 @@ function resolvePermission(toolName, parts) {
14697
14773
  if (denied) return denied;
14698
14774
  const reasons = [];
14699
14775
  for (const part of parts) {
14700
- const reason2 = isApprovedRead(part, toolName);
14701
- if (!reason2) return void 0;
14702
- reasons.push(reason2);
14776
+ const reason4 = isApprovedRead(part, toolName);
14777
+ if (!reason4) return void 0;
14778
+ reasons.push(reason4);
14703
14779
  }
14704
14780
  return {
14705
14781
  permissionDecision: "allow",
@@ -14823,13 +14899,13 @@ function cliHookCheck(command, toolName = "Bash") {
14823
14899
  if (reportDeny(toolName, parts)) return;
14824
14900
  const reasons = [];
14825
14901
  for (const part of parts) {
14826
- const reason2 = isApprovedRead(part, toolName);
14827
- if (!reason2) {
14902
+ const reason4 = isApprovedRead(part, toolName);
14903
+ if (!reason4) {
14828
14904
  console.log(`not approved (unrecognised: ${part})`);
14829
14905
  process.exitCode = 1;
14830
14906
  return;
14831
14907
  }
14832
- reasons.push(` ${part} -> ${reason2}`);
14908
+ reasons.push(` ${part} -> ${reason4}`);
14833
14909
  }
14834
14910
  console.log(`approved
14835
14911
  ${reasons.join("\n")}`);
@@ -16166,28 +16242,6 @@ function exitKeyNotSet(key) {
16166
16242
  // src/commands/config/configSet.ts
16167
16243
  import chalk131 from "chalk";
16168
16244
 
16169
- // src/shared/resolveNamedRepoWriteLabel.ts
16170
- var UnknownRepoConfigError = class extends Error {
16171
- name = "UnknownRepoConfigError";
16172
- };
16173
- function resolveNamedRepoWriteLabel(globalRaw, name) {
16174
- const repos2 = globalRaw.repos;
16175
- const entries = repos2 && typeof repos2 === "object" && !Array.isArray(repos2) ? repos2 : {};
16176
- const candidates = originKeyCandidates(name);
16177
- const matches = Object.keys(entries).filter((key) => candidates.has(key));
16178
- if (matches.length === 0) {
16179
- throw new UnknownRepoConfigError(
16180
- `No repo in ~/.assist.yml matches "${name}". Use an existing repos key (full origin, org/repo, or bare repo name).`
16181
- );
16182
- }
16183
- if (matches.length > 1) {
16184
- throw new AmbiguousRepoConfigError(
16185
- `Ambiguous repos config in ~/.assist.yml: keys ${matches.map((m) => `"${m}"`).join(", ")} all match "${name}". Keep a single key per repository.`
16186
- );
16187
- }
16188
- return matches[0];
16189
- }
16190
-
16191
16245
  // src/commands/config/exitWithConfigErrors.ts
16192
16246
  import chalk130 from "chalk";
16193
16247
  function exitWithConfigErrors(errors) {
@@ -16195,23 +16249,6 @@ function exitWithConfigErrors(errors) {
16195
16249
  process.exit(1);
16196
16250
  }
16197
16251
 
16198
- // src/commands/config/applyRepoConfigSet.ts
16199
- function applyRepoConfigSet(key, coerced, repoName) {
16200
- const globalRaw = loadGlobalConfigRaw();
16201
- const label2 = repoName === void 0 ? resolveRepoWriteLabel(globalRaw, getCurrentOrigin(process.cwd())) : resolveNamedRepoWriteLabel(globalRaw, repoName);
16202
- const repos2 = isPlainObject4(globalRaw.repos) ? { ...globalRaw.repos } : {};
16203
- const existingBlock = isPlainObject4(repos2[label2]) ? repos2[label2] : {};
16204
- const updatedBlock = setNestedValue(existingBlock, key, coerced);
16205
- const validation = validateConfig(updatedBlock, key);
16206
- if (!validation.ok) exitWithConfigErrors(validation.errors);
16207
- repos2[label2] = updatedBlock;
16208
- saveGlobalConfig({ ...globalRaw, repos: repos2 });
16209
- return label2;
16210
- }
16211
- function isPlainObject4(value) {
16212
- return value !== null && typeof value === "object" && !Array.isArray(value);
16213
- }
16214
-
16215
16252
  // src/commands/config/resolveRepoTarget.ts
16216
16253
  function resolveRepoTarget(key, value, repo) {
16217
16254
  if (typeof repo === "string") {
@@ -16237,7 +16274,7 @@ function configSet(key, value, options2 = {}) {
16237
16274
  process.exit(1);
16238
16275
  }
16239
16276
  const coerced = coerceValue(resolved.value);
16240
- const target = resolved.useRepo ? `repo: ${applyRepoConfigSet(resolved.key, coerced, resolved.repoName)}` : applyOrExit(resolved.key, coerced, options2.global ?? false);
16277
+ const target = resolved.useRepo ? `repo: ${applyRepoOrExit(resolved.key, coerced, resolved.repoName)}` : applyOrExit(resolved.key, coerced, options2.global ?? false);
16241
16278
  console.log(
16242
16279
  chalk131.green(`Set ${resolved.key} = ${JSON.stringify(coerced)} (${target})`)
16243
16280
  );
@@ -16247,6 +16284,11 @@ function applyOrExit(key, coerced, global) {
16247
16284
  if (!result.ok) exitWithConfigErrors(result.errors);
16248
16285
  return result.target;
16249
16286
  }
16287
+ function applyRepoOrExit(key, coerced, repoName) {
16288
+ const result = applyRepoConfigSet(key, coerced, repoName);
16289
+ if (!result.ok) exitWithConfigErrors(result.errors);
16290
+ return result.label;
16291
+ }
16250
16292
  function coerceValue(value) {
16251
16293
  if (value === "true") return true;
16252
16294
  if (value === "false") return false;
@@ -17888,14 +17930,14 @@ async function editHook() {
17888
17930
  const input = tryParseInput2(await readStdin());
17889
17931
  if (!input) return;
17890
17932
  const existing = input.tool_name === "Write" ? readExisting(input.tool_input.file_path) : void 0;
17891
- const reason2 = decideOverrideGuard(input, existing) ?? decideCommentGuard(input, existing) ?? decideMigrationGuard(input, existing);
17892
- if (!reason2) return;
17933
+ const reason4 = decideOverrideGuard(input, existing) ?? decideCommentGuard(input, existing) ?? decideMigrationGuard(input, existing);
17934
+ if (!reason4) return;
17893
17935
  console.log(
17894
17936
  JSON.stringify({
17895
17937
  hookSpecificOutput: {
17896
17938
  hookEventName: "PreToolUse",
17897
17939
  permissionDecision: "deny",
17898
- permissionDecisionReason: reason2
17940
+ permissionDecisionReason: reason4
17899
17941
  }
17900
17942
  })
17901
17943
  );
@@ -18841,10 +18883,12 @@ function persistedTreeRoots() {
18841
18883
  }
18842
18884
 
18843
18885
  // src/commands/sessions/daemon/worktree/seedWorktree.ts
18844
- import { execFile as execFile8 } from "child_process";
18845
18886
  import { copyFileSync, existsSync as existsSync46, mkdirSync as mkdirSync16 } from "fs";
18846
18887
  import { dirname as dirname24, join as join48 } from "path";
18847
18888
 
18889
+ // src/commands/sessions/daemon/worktree/runInstall.ts
18890
+ import { spawn as spawn5 } from "child_process";
18891
+
18848
18892
  // src/commands/sessions/daemon/worktree/resolveInstallCommand.ts
18849
18893
  import { existsSync as existsSync45 } from "fs";
18850
18894
  import { join as join47 } from "path";
@@ -18861,6 +18905,90 @@ function resolveInstallCommand(repoRoot, install) {
18861
18905
  return detectInstallCommand(repoRoot);
18862
18906
  }
18863
18907
 
18908
+ // src/commands/sessions/daemon/worktree/stopInstall.ts
18909
+ import { execFile as execFile8 } from "child_process";
18910
+ var running2 = /* @__PURE__ */ new Map();
18911
+ function trackInstall(worktreePath, child) {
18912
+ running2.set(worktreePath, child);
18913
+ }
18914
+ function untrackInstall(worktreePath, child) {
18915
+ if (running2.get(worktreePath) === child) running2.delete(worktreePath);
18916
+ }
18917
+ function stopInstall(worktreePath) {
18918
+ const child = running2.get(worktreePath);
18919
+ running2.delete(worktreePath);
18920
+ if (!child?.pid) return;
18921
+ daemonLog(
18922
+ `worktree ${worktreePath} install still running at teardown; killing pid ${child.pid} so removal is not racing it`
18923
+ );
18924
+ killTree(child.pid);
18925
+ }
18926
+ function killTree(pid) {
18927
+ if (process.platform === "win32") {
18928
+ execFile8("taskkill", ["/T", "/F", "/PID", String(pid)], () => {
18929
+ });
18930
+ return;
18931
+ }
18932
+ try {
18933
+ process.kill(-pid, "SIGKILL");
18934
+ } catch {
18935
+ try {
18936
+ process.kill(pid, "SIGKILL");
18937
+ } catch {
18938
+ }
18939
+ }
18940
+ }
18941
+
18942
+ // src/commands/sessions/daemon/worktree/installInvocation.ts
18943
+ function installInvocation(worktreePath, command) {
18944
+ const windows = /^[A-Za-z]:[\\/]/.test(worktreePath);
18945
+ const shell = windows ? "cmd.exe" : process.env.SHELL ?? "bash";
18946
+ const args = windows ? ["/c", `cd /d ${quoteForCmd(worktreePath)} && ${command}`] : ["-l", "-c", `cd ${quoteForPosixShell(worktreePath)} && ${command}`];
18947
+ return [
18948
+ shell,
18949
+ args,
18950
+ { cwd: worktreePath, detached: true, stdio: ["ignore", "ignore", "pipe"] }
18951
+ ];
18952
+ }
18953
+ function quoteForCmd(value) {
18954
+ return `"${value.replaceAll('"', "")}"`;
18955
+ }
18956
+ function quoteForPosixShell(value) {
18957
+ return `'${value.replaceAll("'", `'\\''`)}'`;
18958
+ }
18959
+
18960
+ // src/commands/sessions/daemon/worktree/runInstall.ts
18961
+ var STDERR_TAIL = 2e3;
18962
+ function runInstall(worktreePath, clone, install, onSeeded) {
18963
+ const command = resolveInstallCommand(clone, install);
18964
+ if (!command) {
18965
+ onSeeded();
18966
+ return;
18967
+ }
18968
+ daemonLog(`worktree ${worktreePath} installing deps: ${command}`);
18969
+ const child = spawn5(...installInvocation(worktreePath, command));
18970
+ trackInstall(worktreePath, child);
18971
+ let stderr = "";
18972
+ child.stderr?.on("data", (chunk) => {
18973
+ stderr = (stderr + chunk.toString()).slice(-STDERR_TAIL);
18974
+ });
18975
+ let settled = false;
18976
+ const settle = (outcome) => {
18977
+ if (settled) return;
18978
+ settled = true;
18979
+ untrackInstall(worktreePath, child);
18980
+ daemonLog(`worktree ${worktreePath} install ${outcome}`);
18981
+ onSeeded();
18982
+ };
18983
+ child.on("error", (error) => settle(`failed to start: ${error.message}`));
18984
+ child.on(
18985
+ "close",
18986
+ (code, signal) => settle(
18987
+ code === 0 ? "complete" : `failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${stderr.trim() || "no output"}`
18988
+ )
18989
+ );
18990
+ }
18991
+
18864
18992
  // src/commands/sessions/daemon/worktree/seedWorktree.ts
18865
18993
  function seedWorktree(worktreePath, clone, onSeeded = () => {
18866
18994
  }) {
@@ -18884,29 +19012,6 @@ function copyConfigFiles(worktreePath, clone, copy) {
18884
19012
  }
18885
19013
  }
18886
19014
  }
18887
- function runInstall(worktreePath, clone, install, onSeeded) {
18888
- const command = resolveInstallCommand(clone, install);
18889
- if (!command) {
18890
- onSeeded();
18891
- return;
18892
- }
18893
- const windows = /^[A-Za-z]:[\\/]/.test(worktreePath);
18894
- const shell = windows ? "cmd.exe" : process.env.SHELL ?? "bash";
18895
- const shellArgs = windows ? ["/c", `cd /d ${quoteForCmd(worktreePath)} && ${command}`] : ["-l", "-c", `cd ${quoteForPosixShell(worktreePath)} && ${command}`];
18896
- daemonLog(`worktree ${worktreePath} installing deps: ${command}`);
18897
- execFile8(shell, shellArgs, { cwd: worktreePath }, (error) => {
18898
- daemonLog(
18899
- error ? `worktree ${worktreePath} install failed: ${error.message}` : `worktree ${worktreePath} install complete`
18900
- );
18901
- onSeeded();
18902
- });
18903
- }
18904
- function quoteForPosixShell(value) {
18905
- return `'${value.replaceAll("'", `'\\''`)}'`;
18906
- }
18907
- function quoteForCmd(value) {
18908
- return `"${value.replaceAll('"', "")}"`;
18909
- }
18910
19015
  function message(error) {
18911
19016
  return error instanceof Error ? error.message : String(error);
18912
19017
  }
@@ -20881,16 +20986,16 @@ function reply(commentId, body) {
20881
20986
 
20882
20987
  // src/commands/prs/wontfix.ts
20883
20988
  import { execSync as execSync50 } from "child_process";
20884
- function validateReason(reason2) {
20885
- const lowerReason = reason2.toLowerCase();
20989
+ function validateReason(reason4) {
20990
+ const lowerReason = reason4.toLowerCase();
20886
20991
  if (lowerReason.includes("claude") || lowerReason.includes("opus")) {
20887
20992
  console.error('Error: Reason must not contain "claude" or "opus"');
20888
20993
  process.exit(1);
20889
20994
  }
20890
20995
  }
20891
- function validateShaReferences(reason2) {
20996
+ function validateShaReferences(reason4) {
20892
20997
  const commitLinkPattern = /\[[0-9a-f]{7}\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/commit\/([0-9a-f]{40})\)/gi;
20893
- const shas = Array.from(reason2.matchAll(commitLinkPattern), (m) => m[1]);
20998
+ const shas = Array.from(reason4.matchAll(commitLinkPattern), (m) => m[1]);
20894
20999
  if (shas.length === 0) {
20895
21000
  return;
20896
21001
  }
@@ -20909,11 +21014,11 @@ function validateShaReferences(reason2) {
20909
21014
  process.exit(1);
20910
21015
  }
20911
21016
  }
20912
- function wontfix(commentId, reason2) {
20913
- validateReason(reason2);
20914
- validateShaReferences(reason2);
21017
+ function wontfix(commentId, reason4) {
21018
+ validateReason(reason4);
21019
+ validateShaReferences(reason4);
20915
21020
  try {
20916
- resolveCommentWithReply(commentId, reason2);
21021
+ resolveCommentWithReply(commentId, reason4);
20917
21022
  } catch (error) {
20918
21023
  if (isGhNotInstalled(error)) {
20919
21024
  console.error("Error: GitHub CLI (gh) is not installed.");
@@ -21092,8 +21197,8 @@ function registerPrs(program2) {
21092
21197
  prsCommand.command("fixed <comment-id> <sha>").description("Reply with commit link and resolve thread").action((commentId, sha) => {
21093
21198
  fixed(Number.parseInt(commentId, 10), sha);
21094
21199
  });
21095
- prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId, reason2) => {
21096
- wontfix(Number.parseInt(commentId, 10), reason2);
21200
+ prsCommand.command("wontfix <comment-id> <reason>").description("Reply with reason and resolve thread").action((commentId, reason4) => {
21201
+ wontfix(Number.parseInt(commentId, 10), reason4);
21097
21202
  });
21098
21203
  prsCommand.command("reply <comment-id> <body>").description("Reply to a comment thread without resolving it").action((commentId, body) => {
21099
21204
  reply(Number.parseInt(commentId, 10), body);
@@ -21542,7 +21647,7 @@ function registerRavendb(program2) {
21542
21647
  }
21543
21648
 
21544
21649
  // src/commands/refactor/check/index.ts
21545
- import { spawn as spawn5 } from "child_process";
21650
+ import { spawn as spawn6 } from "child_process";
21546
21651
  import * as path36 from "path";
21547
21652
 
21548
21653
  // src/commands/refactor/logViolations.ts
@@ -21690,7 +21795,7 @@ function getViolations(pattern2, options2 = {}, maxLines = DEFAULT_MAX_LINES) {
21690
21795
  // src/commands/refactor/check/index.ts
21691
21796
  function runScript(script, cwd) {
21692
21797
  return new Promise((resolve20) => {
21693
- const child = spawn5("npm", ["run", script], {
21798
+ const child = spawn6("npm", ["run", script], {
21694
21799
  stdio: "pipe",
21695
21800
  shell: true,
21696
21801
  cwd
@@ -23074,10 +23179,10 @@ function listFilesRecursive(dir) {
23074
23179
  }
23075
23180
  return results;
23076
23181
  }
23077
- function addDirectoryFileMoves(moves, childDir, newLocation, reason2) {
23182
+ function addDirectoryFileMoves(moves, childDir, newLocation, reason4) {
23078
23183
  for (const file of listFilesRecursive(childDir)) {
23079
23184
  const rel = path55.relative(childDir, file);
23080
- moves.push({ from: file, to: path55.join(newLocation, rel), reason: reason2 });
23185
+ moves.push({ from: file, to: path55.join(newLocation, rel), reason: reason4 });
23081
23186
  }
23082
23187
  }
23083
23188
  function resolveChildDest(parentDir, childDir) {
@@ -23088,8 +23193,8 @@ function childMoveReason(parentDir) {
23088
23193
  }
23089
23194
  function registerDirectoryMove(result, childDir, dest, parentDir) {
23090
23195
  result.directories.push(dest);
23091
- const reason2 = childMoveReason(parentDir);
23092
- addDirectoryFileMoves(result.moves, childDir, dest, reason2);
23196
+ const reason4 = childMoveReason(parentDir);
23197
+ addDirectoryFileMoves(result.moves, childDir, dest, reason4);
23093
23198
  }
23094
23199
  function planChildDirectoryMove(result, parentDir, childDir) {
23095
23200
  const dest = resolveChildDest(parentDir, childDir);
@@ -24384,7 +24489,7 @@ function reportReviewerToolUse(name, use, spinner) {
24384
24489
  }
24385
24490
 
24386
24491
  // src/commands/review/runStreamingChild.ts
24387
- import { spawn as spawn6 } from "child_process";
24492
+ import { spawn as spawn7 } from "child_process";
24388
24493
 
24389
24494
  // src/commands/review/attachLineParser.ts
24390
24495
  function flushBuffer(buffer, onLine) {
@@ -24535,7 +24640,7 @@ function writeStdinSafely(child, payload) {
24535
24640
  }
24536
24641
  }
24537
24642
  function startChild(spec) {
24538
- const child = spawn6(spec.command, spec.args, {
24643
+ const child = spawn7(spec.command, spec.args, {
24539
24644
  stdio: ["pipe", "pipe", "pipe"],
24540
24645
  shell: process.platform === "win32"
24541
24646
  });
@@ -26192,7 +26297,7 @@ function setup() {
26192
26297
  }
26193
26298
 
26194
26299
  // src/commands/voice/start.ts
26195
- import { spawn as spawn7 } from "child_process";
26300
+ import { spawn as spawn8 } from "child_process";
26196
26301
  import { mkdirSync as mkdirSync23, writeFileSync as writeFileSync40 } from "fs";
26197
26302
  import { join as join65 } from "path";
26198
26303
 
@@ -26207,11 +26312,11 @@ function buildDaemonEnv(options2) {
26207
26312
  // src/commands/voice/start.ts
26208
26313
  function spawnForeground(python, script, env) {
26209
26314
  console.log("Starting voice daemon in foreground...");
26210
- const child = spawn7(python, [script], { stdio: "inherit", env });
26315
+ const child = spawn8(python, [script], { stdio: "inherit", env });
26211
26316
  child.on("exit", (code) => process.exit(code ?? 0));
26212
26317
  }
26213
26318
  function spawnBackground(python, script, env) {
26214
- const child = spawn7(python, [script], {
26319
+ const child = spawn8(python, [script], {
26215
26320
  detached: true,
26216
26321
  stdio: "ignore",
26217
26322
  env
@@ -27012,7 +27117,7 @@ function runPreCommands(pre, cwd) {
27012
27117
  }
27013
27118
 
27014
27119
  // src/commands/run/spawnRunCommand.ts
27015
- import { execFileSync as execFileSync12, spawn as spawn8 } from "child_process";
27120
+ import { execFileSync as execFileSync12, spawn as spawn9 } from "child_process";
27016
27121
  import { existsSync as existsSync58 } from "fs";
27017
27122
  import { dirname as dirname29, join as join67, resolve as resolve15 } from "path";
27018
27123
  function resolveCommand2(command) {
@@ -27028,7 +27133,7 @@ function resolveCommand2(command) {
27028
27133
  }
27029
27134
  function spawnRunCommand(command, args, env, cwd, quiet) {
27030
27135
  const start3 = Date.now();
27031
- const child = spawn8(resolveCommand2(command), args, {
27136
+ const child = spawn9(resolveCommand2(command), args, {
27032
27137
  stdio: quiet ? "pipe" : "inherit",
27033
27138
  env: env ? { ...process.env, ...expandEnv(env) } : void 0,
27034
27139
  cwd
@@ -28104,9 +28209,89 @@ function otherTreeHolders(sessions, session) {
28104
28209
  }
28105
28210
 
28106
28211
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
28107
- import { existsSync as existsSync62 } from "fs";
28212
+ import { existsSync as existsSync63 } from "fs";
28108
28213
  import { basename as basename16 } from "path";
28109
28214
 
28215
+ // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
28216
+ import { existsSync as existsSync62 } from "fs";
28217
+ import { join as join73 } from "path";
28218
+
28219
+ // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
28220
+ import { statSync as statSync11 } from "fs";
28221
+ import { rm as rm2 } from "fs/promises";
28222
+ import { join as join72 } from "path";
28223
+ async function deleteTreeDirectly(clone, worktreePath, why) {
28224
+ if (holdsAGitDirectoryRatherThanALink(worktreePath)) {
28225
+ daemonLog(
28226
+ `worktree ${worktreePath} not deleted directly (${why}): it is a clone of its own, not a linked worktree`
28227
+ );
28228
+ return false;
28229
+ }
28230
+ daemonLog(`worktree ${worktreePath} deleting its directory directly: ${why}`);
28231
+ try {
28232
+ await rm2(worktreePath, {
28233
+ recursive: true,
28234
+ force: true,
28235
+ maxRetries: 3,
28236
+ retryDelay: 200
28237
+ });
28238
+ } catch (error) {
28239
+ daemonLog(
28240
+ `worktree ${worktreePath} directory delete failed, left in place for the next reconcile: ${reason(error)}`
28241
+ );
28242
+ return false;
28243
+ }
28244
+ daemonLog(`worktree ${worktreePath} directory deleted directly`);
28245
+ await pruneBookkeeping(clone, worktreePath);
28246
+ return true;
28247
+ }
28248
+ function holdsAGitDirectoryRatherThanALink(worktreePath) {
28249
+ return statSync11(join72(worktreePath, ".git"), {
28250
+ throwIfNoEntry: false
28251
+ })?.isDirectory() === true;
28252
+ }
28253
+ async function pruneBookkeeping(clone, worktreePath) {
28254
+ try {
28255
+ await git2(clone, ["worktree", "prune"]);
28256
+ daemonLog(
28257
+ `worktree bookkeeping pruned in clone ${clone} after deleting ${worktreePath} directly`
28258
+ );
28259
+ } catch (error) {
28260
+ daemonLog(
28261
+ `worktree bookkeeping prune failed in clone ${clone} after deleting ${worktreePath}: ${reason(error)}`
28262
+ );
28263
+ }
28264
+ }
28265
+ function reason(error) {
28266
+ return error instanceof Error ? error.message : String(error);
28267
+ }
28268
+
28269
+ // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
28270
+ async function deleteStrandedTree(clone, worktreePath, cause) {
28271
+ const stranded = strandedReason(worktreePath, cause);
28272
+ if (!stranded) {
28273
+ daemonLog(
28274
+ `worktree ${worktreePath} left in place for the next reconcile: ${reason2(cause)}`
28275
+ );
28276
+ return false;
28277
+ }
28278
+ return await deleteTreeDirectly(
28279
+ clone,
28280
+ worktreePath,
28281
+ `git can no longer remove it, ${stranded}`
28282
+ );
28283
+ }
28284
+ function strandedReason(worktreePath, cause) {
28285
+ if (!existsSync62(join73(worktreePath, ".git")))
28286
+ return "its .git link is already gone";
28287
+ if (/not a working tree|not a git repository/i.test(reason2(cause)))
28288
+ return "git no longer recognises it as a working tree";
28289
+ return null;
28290
+ }
28291
+ function reason2(error) {
28292
+ return error instanceof Error ? error.message : String(error);
28293
+ }
28294
+
28110
28295
  // src/commands/sessions/daemon/worktree/removeTree.ts
28111
28296
  async function removeTree(clone, worktreePath, force) {
28112
28297
  const remove2 = (forced) => git2(clone, [
@@ -28119,12 +28304,17 @@ async function removeTree(clone, worktreePath, force) {
28119
28304
  await remove2(force);
28120
28305
  return true;
28121
28306
  } catch (error) {
28122
- daemonLog(`worktree ${worktreePath} removal failed: ${reason(error)}`);
28307
+ daemonLog(`worktree ${worktreePath} removal failed: ${reason3(error)}`);
28308
+ if (force)
28309
+ return await deleteTreeDirectly(
28310
+ clone,
28311
+ worktreePath,
28312
+ `its work was discarded and git could not remove it: ${reason3(error)}`
28313
+ );
28123
28314
  }
28124
- if (force) return false;
28125
- return await removeIgnoringLeftovers(remove2, worktreePath);
28315
+ return await removeIgnoringLeftovers(clone, remove2, worktreePath);
28126
28316
  }
28127
- async function removeIgnoringLeftovers(remove2, worktreePath) {
28317
+ async function removeIgnoringLeftovers(clone, remove2, worktreePath) {
28128
28318
  try {
28129
28319
  await remove2(true);
28130
28320
  daemonLog(
@@ -28133,18 +28323,18 @@ async function removeIgnoringLeftovers(remove2, worktreePath) {
28133
28323
  return true;
28134
28324
  } catch (error) {
28135
28325
  daemonLog(
28136
- `worktree ${worktreePath} left in place for the next reconcile: ${reason(error)}`
28326
+ `worktree ${worktreePath} forced removal failed: ${reason3(error)}`
28137
28327
  );
28138
- return false;
28328
+ return await deleteStrandedTree(clone, worktreePath, error);
28139
28329
  }
28140
28330
  }
28141
- function reason(error) {
28331
+ function reason3(error) {
28142
28332
  return error instanceof Error ? error.message : String(error);
28143
28333
  }
28144
28334
 
28145
28335
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
28146
28336
  async function reapWorktree(worktreePath, force = false) {
28147
- if (!existsSync62(worktreePath)) {
28337
+ if (!existsSync63(worktreePath)) {
28148
28338
  daemonLog(`worktree ${worktreePath} already gone; skipping reap`);
28149
28339
  return false;
28150
28340
  }
@@ -28155,13 +28345,24 @@ async function reapWorktree(worktreePath, force = false) {
28155
28345
  return false;
28156
28346
  }
28157
28347
  }
28158
- const clone = mainWorktree(worktreePath) ?? worktreePath;
28348
+ stopInstall(worktreePath);
28349
+ const clone = owningClone(worktreePath);
28159
28350
  if (!await removeTree(clone, worktreePath, force)) return false;
28160
28351
  await deleteWorktreeBranch(clone, basename16(worktreePath));
28161
28352
  forgetWorktree(worktreePath);
28162
28353
  daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
28163
28354
  return true;
28164
28355
  }
28356
+ function owningClone(worktreePath) {
28357
+ const recorded = worktreeAttributionIncludingReaped(worktreePath)?.clone;
28358
+ if (recorded && existsSync63(recorded)) return recorded;
28359
+ const detected = mainWorktree(worktreePath);
28360
+ if (detected) return detected;
28361
+ daemonLog(
28362
+ `worktree ${worktreePath} has no resolvable clone; tearing it down on its own`
28363
+ );
28364
+ return worktreePath;
28365
+ }
28165
28366
  async function deleteWorktreeBranch(clone, branch2) {
28166
28367
  if (!listLocalBranches(clone).includes(branch2)) return;
28167
28368
  const current = await gitOrNull2(clone, ["symbolic-ref", "--short", "HEAD"]);
@@ -28241,12 +28442,12 @@ function setStatus2(session, newStatus) {
28241
28442
  }
28242
28443
 
28243
28444
  // src/commands/sessions/daemon/worktree/watchGitState.ts
28244
- import { existsSync as existsSync63, watch } from "fs";
28445
+ import { existsSync as existsSync64, watch } from "fs";
28245
28446
  var DEBOUNCE_MS = 500;
28246
28447
  var POLL_MS = 3e4;
28247
28448
  function watchGitState(cwd, onChange) {
28248
28449
  const common = gitCommonDir(cwd);
28249
- if (!common || !existsSync63(common)) return void 0;
28450
+ if (!common || !existsSync64(common)) return void 0;
28250
28451
  const watchers = [
28251
28452
  watchGitDir(common, onChange),
28252
28453
  pollGitState(cwd, onChange)
@@ -28300,12 +28501,12 @@ async function resolveCloseDurability(session, finalize, notify2) {
28300
28501
  session.gitWatcher = void 0;
28301
28502
  finalize();
28302
28503
  }
28303
- function holdStopped(session, tree, reason2, finalize, notify2) {
28304
- if (session.undurable?.reason !== reason2)
28504
+ function holdStopped(session, tree, reason4, finalize, notify2) {
28505
+ if (session.undurable?.reason !== reason4)
28305
28506
  daemonLog(
28306
- `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason2}`
28507
+ `session ${session.id} stopped; ${tree.removable ? "reap" : "close"} blocked in ${tree.path}: ${reason4}`
28307
28508
  );
28308
- session.undurable = { reason: reason2, removesTree: tree.removable };
28509
+ session.undurable = { reason: reason4, removesTree: tree.removable };
28309
28510
  session.closing = void 0;
28310
28511
  setStatus2(session, "stopped");
28311
28512
  if (!session.gitWatcher) {
@@ -28650,10 +28851,10 @@ function emitSessionOutput(session, clients, data) {
28650
28851
  }
28651
28852
 
28652
28853
  // src/commands/sessions/daemon/exitReason.ts
28653
- import { existsSync as existsSync64 } from "fs";
28854
+ import { existsSync as existsSync65 } from "fs";
28654
28855
  function exitReason(session, exitCode) {
28655
28856
  const base = `process exited with code ${exitCode}`;
28656
- if (session.cwd && !existsSync64(session.cwd))
28857
+ if (session.cwd && !existsSync65(session.cwd))
28657
28858
  return `${base}: working directory ${session.cwd} no longer exists`;
28658
28859
  return base;
28659
28860
  }
@@ -28671,7 +28872,7 @@ function handleFailedResume(session, exitCode, onStatusChange) {
28671
28872
  }
28672
28873
 
28673
28874
  // src/commands/sessions/daemon/watchActivity.ts
28674
- import { existsSync as existsSync65, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
28875
+ import { existsSync as existsSync66, mkdirSync as mkdirSync26, watch as watch2 } from "fs";
28675
28876
  import { dirname as dirname31 } from "path";
28676
28877
 
28677
28878
  // src/commands/sessions/daemon/applyReviewPause.ts
@@ -28752,7 +28953,7 @@ function watchActivity(session, notify2, onClaudeSessionId) {
28752
28953
  if (timer) clearTimeout(timer);
28753
28954
  timer = setTimeout(read, DEBOUNCE_MS2);
28754
28955
  });
28755
- if (existsSync65(path71)) read();
28956
+ if (existsSync66(path71)) read();
28756
28957
  }
28757
28958
  function refreshActivity(session) {
28758
28959
  if (session.commandType !== "assist" || !session.cwd) return;
@@ -29767,14 +29968,14 @@ function restoreSession(id, persisted) {
29767
29968
  }
29768
29969
 
29769
29970
  // src/commands/sessions/daemon/restoreOne.ts
29770
- function restoreOne(persisted, spawn12, sessions) {
29971
+ function restoreOne(persisted, spawn13, sessions) {
29771
29972
  try {
29772
- const id = spawn12((sid) => restoreSession(sid, persisted));
29973
+ const id = spawn13((sid) => restoreSession(sid, persisted));
29773
29974
  logUnresumable(persisted.name, id, sessions.get(id));
29774
29975
  } catch (error) {
29775
- const reason2 = logRestoreError(persisted.name, error);
29976
+ const reason4 = logRestoreError(persisted.name, error);
29776
29977
  try {
29777
- spawn12((id) => errorSession(id, persisted, reason2));
29978
+ spawn13((id) => errorSession(id, persisted, reason4));
29778
29979
  } catch {
29779
29980
  }
29780
29981
  }
@@ -29786,9 +29987,9 @@ function logUnresumable(name, id, session) {
29786
29987
  );
29787
29988
  }
29788
29989
  function logRestoreError(name, error) {
29789
- const reason2 = error instanceof Error ? error.message : String(error);
29790
- daemonLog(`failed to restore session "${name}": ${reason2}`);
29791
- return reason2;
29990
+ const reason4 = error instanceof Error ? error.message : String(error);
29991
+ daemonLog(`failed to restore session "${name}": ${reason4}`);
29992
+ return reason4;
29792
29993
  }
29793
29994
 
29794
29995
  // src/commands/sessions/daemon/sessionLimits.ts
@@ -29809,7 +30010,7 @@ var sessionLimits = {
29809
30010
  };
29810
30011
 
29811
30012
  // src/commands/sessions/daemon/restoreAll.ts
29812
- function restoreAll(spawn12, sessions) {
30013
+ function restoreAll(spawn13, sessions) {
29813
30014
  const persisted = loadPersistedSessions();
29814
30015
  const toRestore = persisted.slice(0, sessionLimits.maxRestore);
29815
30016
  const skipped = persisted.length - toRestore.length;
@@ -29818,7 +30019,7 @@ function restoreAll(spawn12, sessions) {
29818
30019
  `restore capped at ${sessionLimits.maxRestore}; skipping ${skipped} persisted session(s)`
29819
30020
  );
29820
30021
  return toRestore.map((persisted2) => {
29821
- restoreOne(persisted2, spawn12, sessions);
30022
+ restoreOne(persisted2, spawn13, sessions);
29822
30023
  return persisted2.name;
29823
30024
  });
29824
30025
  }
@@ -29841,7 +30042,7 @@ function rearmStoppedSessions(sessions, notify2) {
29841
30042
  }
29842
30043
 
29843
30044
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
29844
- import { existsSync as existsSync68 } from "fs";
30045
+ import { existsSync as existsSync69 } from "fs";
29845
30046
  import { basename as basename18 } from "path";
29846
30047
 
29847
30048
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -29897,10 +30098,10 @@ function bindResumedWorktree(session, cwd, notify2) {
29897
30098
 
29898
30099
  // src/commands/sessions/daemon/worktree/describeHeldWork.ts
29899
30100
  var MAX_ITEMS = 20;
29900
- async function describeHeldWork(path71, reason2) {
29901
- if (reason2.startsWith("uncommitted")) return await changedFiles(path71);
29902
- if (reason2.startsWith("unpushed")) return await unpushedCommits(path71, reason2);
29903
- return { summary: reason2, items: [] };
30101
+ async function describeHeldWork(path71, reason4) {
30102
+ if (reason4.startsWith("uncommitted")) return await changedFiles(path71);
30103
+ if (reason4.startsWith("unpushed")) return await unpushedCommits(path71, reason4);
30104
+ return { summary: reason4, items: [] };
29904
30105
  }
29905
30106
  async function changedFiles(path71) {
29906
30107
  const status3 = await gitResult(path71, ["status", "--porcelain"]);
@@ -29910,9 +30111,9 @@ async function changedFiles(path71) {
29910
30111
  items: capped(lines)
29911
30112
  };
29912
30113
  }
29913
- async function unpushedCommits(path71, reason2) {
30114
+ async function unpushedCommits(path71, reason4) {
29914
30115
  const log = await gitResult(path71, ["log", "--oneline", "@{upstream}..HEAD"]);
29915
- if (!log.ok) return { summary: reason2, items: [] };
30116
+ if (!log.ok) return { summary: reason4, items: [] };
29916
30117
  const lines = nonEmptyLines(log.out);
29917
30118
  return {
29918
30119
  summary: `${lines.length} unpushed ${lines.length === 1 ? "commit" : "commits"}`,
@@ -29931,20 +30132,20 @@ function capped(lines) {
29931
30132
  }
29932
30133
 
29933
30134
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
29934
- import { existsSync as existsSync67 } from "fs";
30135
+ import { existsSync as existsSync68 } from "fs";
29935
30136
  async function reclaimVanishedWorktrees(clone, paths) {
29936
- if (!existsSync67(clone)) {
30137
+ if (!existsSync68(clone)) {
29937
30138
  for (const { path: path71 } of paths) forgetWorktree(path71);
29938
30139
  daemonLog(
29939
30140
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
29940
30141
  );
29941
30142
  return;
29942
30143
  }
29943
- await pruneBookkeeping(clone);
30144
+ await pruneBookkeeping2(clone);
29944
30145
  for (const { path: path71, branch: branch2 } of paths)
29945
30146
  if (await reclaimBranch(clone, branch2)) forgetWorktree(path71);
29946
30147
  }
29947
- async function pruneBookkeeping(clone) {
30148
+ async function pruneBookkeeping2(clone) {
29948
30149
  try {
29949
30150
  await git2(clone, ["worktree", "prune"]);
29950
30151
  daemonLog(`worktree bookkeeping pruned for clone ${clone}`);
@@ -29971,7 +30172,7 @@ async function reclaimBranch(clone, branch2) {
29971
30172
  // src/commands/sessions/daemon/worktree/resurfaceOrphanedWorktree.ts
29972
30173
  import { basename as basename17 } from "path";
29973
30174
  function resurfaceOrphanedWorktree(sessions, spawnWith, recovered, notify2) {
29974
- const { orphan, reason: reason2, held } = recovered;
30175
+ const { orphan, reason: reason4, held } = recovered;
29975
30176
  let id;
29976
30177
  try {
29977
30178
  id = spawnWith((sid) => orphanedSession(sid, recovered));
@@ -29982,14 +30183,14 @@ function resurfaceOrphanedWorktree(sessions, spawnWith, recovered, notify2) {
29982
30183
  return;
29983
30184
  }
29984
30185
  daemonLog(
29985
- `orphaned worktree ${orphan.path} resurfaced as stopped session ${id}: ${reason2} (${held.summary})`
30186
+ `orphaned worktree ${orphan.path} resurfaced as stopped session ${id}: ${reason4} (${held.summary})`
29986
30187
  );
29987
30188
  const session = sessions.get(id);
29988
30189
  if (session) armStoppedSession(sessions, session, notify2);
29989
30190
  notify2();
29990
30191
  }
29991
30192
  function orphanedSession(id, recovered) {
29992
- const { orphan, reason: reason2, held } = recovered;
30193
+ const { orphan, reason: reason4, held } = recovered;
29993
30194
  return {
29994
30195
  ...sessionBase(id, "stopped"),
29995
30196
  name: `recovered ${basename17(orphan.path)}`,
@@ -29998,7 +30199,7 @@ function orphanedSession(id, recovered) {
29998
30199
  pty: null,
29999
30200
  cwd: orphan.path,
30000
30201
  worktree: { path: orphan.path, clone: orphan.clone },
30001
- undurable: { reason: reason2, removesTree: true },
30202
+ undurable: { reason: reason4, removesTree: true },
30002
30203
  scrollback: recoveryNotice(recovered)
30003
30204
  };
30004
30205
  }
@@ -30026,7 +30227,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
30026
30227
  const vanished = /* @__PURE__ */ new Map();
30027
30228
  for (const { path: path71, clone } of readWorktreeRegistry()) {
30028
30229
  if (accounted.has(path71)) continue;
30029
- if (!existsSync68(path71)) {
30230
+ if (!existsSync69(path71)) {
30030
30231
  vanished.set(clone, [
30031
30232
  ...vanished.get(clone) ?? [],
30032
30233
  { path: path71, branch: basename18(path71) }
@@ -30239,6 +30440,12 @@ function startHeldSession(session, sessions, clients, onStatusChange, notify2) {
30239
30440
  );
30240
30441
  return;
30241
30442
  }
30443
+ if (session.closing) {
30444
+ daemonLog(
30445
+ `session ${session.id} not started after seeding: card is closing (${session.worktree?.path ?? "no worktree"})`
30446
+ );
30447
+ return;
30448
+ }
30242
30449
  session.startedAt = Date.now();
30243
30450
  if (session.status === "running") session.runningSince = session.startedAt;
30244
30451
  session.pty = start3();
@@ -30315,7 +30522,7 @@ async function isWindowsDaemonRunning() {
30315
30522
  }
30316
30523
 
30317
30524
  // src/commands/sessions/daemon/ensureWindowsDaemonRunning.ts
30318
- import { spawn as spawn10 } from "child_process";
30525
+ import { spawn as spawn11 } from "child_process";
30319
30526
 
30320
30527
  // src/commands/sessions/daemon/logChildStream.ts
30321
30528
  import { createInterface as createInterface7 } from "readline";
@@ -30340,7 +30547,7 @@ async function ensureWindowsDaemonRunning() {
30340
30547
  await waitForWindowsDaemon();
30341
30548
  }
30342
30549
  function launchWindowsDaemon() {
30343
- const child = spawn10("pwsh.exe", ["-Command", "assist daemon run"], {
30550
+ const child = spawn11("pwsh.exe", ["-Command", "assist daemon run"], {
30344
30551
  detached: true,
30345
30552
  stdio: ["ignore", "pipe", "pipe"]
30346
30553
  });
@@ -30388,13 +30595,13 @@ async function defaultConnect() {
30388
30595
  }
30389
30596
 
30390
30597
  // src/commands/sessions/daemon/hasPersistedWindowsSessions.ts
30391
- import { existsSync as existsSync69, readFileSync as readFileSync50 } from "fs";
30598
+ import { existsSync as existsSync70, readFileSync as readFileSync50 } from "fs";
30392
30599
  import { posix } from "path";
30393
30600
  function hasPersistedWindowsSessions() {
30394
30601
  const sessionsFile = windowsSessionsFileFromWsl();
30395
30602
  if (!sessionsFile) return false;
30396
30603
  try {
30397
- if (!existsSync69(sessionsFile)) return false;
30604
+ if (!existsSync70(sessionsFile)) return false;
30398
30605
  const data = JSON.parse(readFileSync50(sessionsFile, "utf8"));
30399
30606
  return Array.isArray(data) && data.length > 0;
30400
30607
  } catch (error) {
@@ -30643,7 +30850,7 @@ function handleWindowsClose(state) {
30643
30850
  }
30644
30851
 
30645
30852
  // src/commands/sessions/daemon/healWindowsDaemon.ts
30646
- import { spawn as spawn11 } from "child_process";
30853
+ import { spawn as spawn12 } from "child_process";
30647
30854
  var UPDATE_TIMEOUT_MS = 5 * 6e4;
30648
30855
  var STOP_TIMEOUT_MS2 = 3e4;
30649
30856
  async function healWindowsDaemon() {
@@ -30663,7 +30870,7 @@ async function healWindowsDaemon() {
30663
30870
  }
30664
30871
  function runOnWindowsHost(command, timeoutMs) {
30665
30872
  return new Promise((resolve20, reject) => {
30666
- const child = spawn11("pwsh.exe", ["-Command", command], {
30873
+ const child = spawn12("pwsh.exe", ["-Command", command], {
30667
30874
  stdio: ["ignore", "pipe", "pipe"]
30668
30875
  });
30669
30876
  logChildStream(child.stdout, `windows ${command}`);
@@ -31285,10 +31492,10 @@ import * as net3 from "net";
31285
31492
  import { createInterface as createInterface9 } from "readline";
31286
31493
 
31287
31494
  // src/commands/sessions/daemon/creator.ts
31288
- function creator(isNew, spawn12) {
31495
+ function creator(isNew, spawn13) {
31289
31496
  return (client, m, d) => {
31290
31497
  if (m.windowsProxy.route(client, d)) return;
31291
- sendTo(client, { type: "created", sessionId: spawn12(m, d), isNew });
31498
+ sendTo(client, { type: "created", sessionId: spawn13(m, d), isNew });
31292
31499
  };
31293
31500
  }
31294
31501
 
@@ -32018,13 +32225,13 @@ function buildLimitsSegment(rateLimits) {
32018
32225
  }
32019
32226
 
32020
32227
  // src/commands/readGitBranch.ts
32021
- import { readFileSync as readFileSync53, statSync as statSync12 } from "fs";
32022
- import { isAbsolute as isAbsolute3, join as join73, resolve as resolve18 } from "path";
32228
+ import { readFileSync as readFileSync53, statSync as statSync13 } from "fs";
32229
+ import { isAbsolute as isAbsolute3, join as join75, resolve as resolve18 } from "path";
32023
32230
  function resolveGitDir(cwd) {
32024
- const dotGit = join73(cwd, ".git");
32231
+ const dotGit = join75(cwd, ".git");
32025
32232
  let stat3;
32026
32233
  try {
32027
- stat3 = statSync12(dotGit);
32234
+ stat3 = statSync13(dotGit);
32028
32235
  } catch {
32029
32236
  return null;
32030
32237
  }
@@ -32051,7 +32258,7 @@ function readGitBranch(cwd) {
32051
32258
  }
32052
32259
  let head;
32053
32260
  try {
32054
- head = readFileSync53(join73(gitDir, "HEAD"), "utf8");
32261
+ head = readFileSync53(join75(gitDir, "HEAD"), "utf8");
32055
32262
  } catch {
32056
32263
  return null;
32057
32264
  }