@agentproto/worktree 0.5.1 → 0.5.4

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.d.ts CHANGED
@@ -996,6 +996,15 @@ type LivenessState = {
996
996
  * `daemon-unreachable` fires only when the sessions snapshot itself is
997
997
  * present but unreadable (corrupt/permission-denied), not merely absent —
998
998
  * "no daemon has ever run" is a legitimate empty registry, not an error.
999
+ *
1000
+ * `options.sessionsPath` is passed straight to `readSessionsRegistry`
1001
+ * un-defaulted — same as `computeProvenance` — so an omitted path resolves
1002
+ * to the bucket-union-plus-legacy read (AIP-46 §State partitioning,
1003
+ * `provenance.ts`'s header), not just the frozen legacy file. Defaulting to
1004
+ * `SESSIONS_FILE_PATH()` here used to silently blind this check to every
1005
+ * session living in a per-workspace bucket — i.e. every session created
1006
+ * after the AIP-46 partitioning shipped — which is how a running session's
1007
+ * own cwd got classified `reclaim` by `gc` in production.
999
1008
  */
1000
1009
  declare function computeLiveness(worktreePath: string, options?: ComputeProvenanceOptions): Promise<LivenessState>;
1001
1010
  type GateState = {
@@ -1074,7 +1083,7 @@ interface ListWorktreeStatusesInput {
1074
1083
  declare function listWorktreeStatuses(input: ListWorktreeStatusesInput): Promise<WorktreeStatusEntry[]>;
1075
1084
 
1076
1085
  /** The subset of `HarnessClient` the host drives — narrowed for fake-client tests. */
1077
- type DaemonClient = Pick<HarnessClient, "start" | "prompt" | "output" | "kill" | "waitForAny" | "close">;
1086
+ type DaemonClient = Pick<HarnessClient, "start" | "prompt" | "output" | "kill" | "waitForAny" | "currentEventsCursor" | "close">;
1078
1087
  interface DaemonAgentSessionHost extends AgentSessionHost {
1079
1088
  close(): Promise<void>;
1080
1089
  /** Full `agent_start`, returning the raw descriptor and forwarding every
@@ -1099,7 +1108,11 @@ interface DaemonAgentSessionHost extends AgentSessionHost {
1099
1108
  waitForAny(sessionIds: string[], opts?: {
1100
1109
  timeoutMs?: number;
1101
1110
  event?: TurnEvent;
1111
+ since?: number;
1102
1112
  }): Promise<TurnResult>;
1113
+ /** Race-free cursor for a subsequent `waitForAny({ since })` — see
1114
+ * `HarnessClient.currentEventsCursor`. */
1115
+ currentEventsCursor(): Promise<number>;
1103
1116
  }
1104
1117
  /**
1105
1118
  * Connect to the agentproto daemon's MCP endpoint and return a real,
@@ -1173,8 +1186,18 @@ interface ClassifyForGcOptions {
1173
1186
  * also depends on.
1174
1187
  */
1175
1188
  declare function classifyForGc(tree: TreeState, integration: IntegrationState, liveness: LivenessState, options?: ClassifyForGcOptions): GcClass;
1176
- /** The only reclaim reason this module can attach today. */
1177
- type GcReclaimReason = "dep-bump";
1189
+ /**
1190
+ * The reclaim reasons this module can attach. `dep-bump` promotes a `hold`
1191
+ * to `reclaim` (see above). `orphan` is different in kind — it never starts
1192
+ * from a `hold` verdict at all, because an orphan was never classified in
1193
+ * the first place (see the "orphan reclaim" section below): a directory
1194
+ * physically present under the repo's worktree pool that `git worktree
1195
+ * list` no longer knows about, so none of `classify`'s three axes can be
1196
+ * computed for it.
1197
+ */
1198
+ type GcReclaimReason = "dep-bump" | "orphan";
1199
+ /** The hold reasons this module can attach, distinct from `classify`'s snapshot-based holds. */
1200
+ type GcHoldReason = "live-session-cwd";
1178
1201
  /**
1179
1202
  * Layer 3+4 of the exemption: EVERY commit ahead of `baseRef` must have a
1180
1203
  * dep-bump subject (`git log --format=%s`, two-dot range — exactly the
@@ -1214,13 +1237,30 @@ interface GcPlanEntry {
1214
1237
  path: string;
1215
1238
  branch: string | null;
1216
1239
  head: string;
1217
- tree: TreeState;
1218
- integration: IntegrationState;
1219
- liveness: LivenessState;
1240
+ /**
1241
+ * Absent only for an orphan entry (`orphan: true`) — git itself cannot
1242
+ * answer the tree/integration/liveness questions for a directory it no
1243
+ * longer recognizes as a worktree, so this module never fabricates a
1244
+ * value for them rather than reporting a fact it can't actually check.
1245
+ */
1246
+ tree?: TreeState;
1247
+ integration?: IntegrationState;
1248
+ liveness?: LivenessState;
1220
1249
  /** Classification per PLAN.md §5.1, after `--include-detached` (if set). */
1221
1250
  class: GcClass;
1222
- /** Set only when `class === "reclaim"` via the dep-bump exemption — see `resolveGcClass`. */
1251
+ /** Set only when `class === "reclaim"` via the dep-bump exemption or the orphan reclaim path — see `resolveGcClass` / `scanOrphanWorktreePaths`. */
1223
1252
  reclaimReason?: GcReclaimReason;
1253
+ /** Set only when `class === "hold"` via `protectedPaths` (`isProtectedPath`) — distinguishes this from an ordinary snapshot-based hold. */
1254
+ holdReason?: GcHoldReason;
1255
+ /**
1256
+ * `true` only for a directory the orphan scan found: physically present
1257
+ * under the repo's worktree pool, but absent from `git worktree list`
1258
+ * (even after `git worktree prune`) — the interrupted-`worktree remove`
1259
+ * shape (module docblock's §5 note). Always paired with `class: "reclaim"`
1260
+ * and `reclaimReason: "orphan"`. Never set for a linked worktree, however
1261
+ * it classifies.
1262
+ */
1263
+ orphan?: boolean;
1224
1264
  }
1225
1265
  interface PlanGcInput {
1226
1266
  repoRoot: string;
@@ -1233,6 +1273,23 @@ interface PlanGcInput {
1233
1273
  now?: () => string;
1234
1274
  /** See `ComputeWorktreeStatusInput.nowMs` (status.ts) — kept separate from `now`. Defaults to `Date.now()`. */
1235
1275
  nowMs?: number;
1276
+ /**
1277
+ * The bucket directory this repo's worktrees are provisioned under — e.g.
1278
+ * `<worktrees.root>/<repoLabel>` (see `resolveWorktreesRoot` / `repoLabel`,
1279
+ * both resolved by the caller; this module does no root resolution of its
1280
+ * own, see the orphan-reclaim section below). When omitted, orphan
1281
+ * scanning is skipped entirely and `planGc`/`applyGc` behave exactly as
1282
+ * before — every existing caller that doesn't pass this sees zero change.
1283
+ */
1284
+ worktreesRoot?: string;
1285
+ /**
1286
+ * Absolute paths (typically live session cwds) that must never be
1287
+ * classified `reclaim`/`salvage`, exact-or-subdirectory (`isProtectedPath`)
1288
+ * — see the "live-session-cwd protection" section above. Applies to both
1289
+ * linked worktrees and orphan-scan entries. Omitted ⇒ no additional
1290
+ * protection beyond `classify`'s own snapshot-based liveness axis.
1291
+ */
1292
+ protectedPaths?: string[];
1236
1293
  }
1237
1294
  /**
1238
1295
  * The dry-run plan (PLAN.md §5.2 layer 1): classify every linked worktree,
@@ -1255,6 +1312,8 @@ interface ApplyGcOptions {
1255
1312
  nowMs?: number;
1256
1313
  /** Override for `~/.agentproto/worktree-salvage` — tests use a temp dir. */
1257
1314
  salvageRoot?: string;
1315
+ /** See `PlanGcInput.protectedPaths` — re-checked here at apply time (layer 2), independent of what the plan entry says, so a stale plan can never remove a now-protected path. */
1316
+ protectedPaths?: string[];
1258
1317
  }
1259
1318
  type GcApplyOutcome = {
1260
1319
  path: string;
@@ -1270,6 +1329,7 @@ type GcApplyOutcome = {
1270
1329
  path: string;
1271
1330
  branch: string | null;
1272
1331
  result: "held";
1332
+ holdReason?: GcHoldReason;
1273
1333
  }
1274
1334
  /** Salvage-class, but `--salvage-dirty` was not passed: left untouched by design. */
1275
1335
  | {
@@ -1284,6 +1344,7 @@ type GcApplyOutcome = {
1284
1344
  result: "aborted-reclassified";
1285
1345
  from: GcClass;
1286
1346
  to: GcClass;
1347
+ holdReason?: GcHoldReason;
1287
1348
  }
1288
1349
  /** The worktree named in the plan no longer exists (already removed by something else since the plan was made). */
1289
1350
  | {
@@ -1306,5 +1367,20 @@ type GcApplyOutcome = {
1306
1367
  * `git worktree remove`.
1307
1368
  */
1308
1369
  declare function applyGc(plan: readonly GcPlanEntry[], options: ApplyGcOptions): Promise<GcApplyOutcome[]>;
1370
+ /**
1371
+ * Attempt to reclaim exactly one worktree by path — the exit-time auto-
1372
+ * reclaim primitive `sessions.ts` calls (via the injected
1373
+ * `WorktreeAutoReclaimer` port) when a policy-provisioned session reaches a
1374
+ * terminal state. Classifies it fresh (never trusts a caller-supplied
1375
+ * verdict) and only ever removes it when that classification is `reclaim`
1376
+ * (merged-or-fresh, clean, idle) — a dirty or held worktree is returned
1377
+ * as-is, exactly like any other `gc` outcome, still visible via
1378
+ * `worktree_status` and still manually reclaimable. Returns `null` when the
1379
+ * path no longer names a linked worktree at all (already removed by
1380
+ * something else — nothing to do, not an error).
1381
+ */
1382
+ declare function reclaimOneWorktree(worktreePath: string, options: ApplyGcOptions & {
1383
+ includeDetached?: boolean;
1384
+ }): Promise<GcApplyOutcome | null>;
1309
1385
 
1310
- export { type AgentprotoConfig, type ApplyGcOptions, CONFIG_FILENAME, type Classification, type ClassifyForGcOptions, type ComputeProvenanceOptions, type ComputeWorktreeStatusInput, ConfigError, DEFAULT_PROXY_PORT, type DaemonAgentSessionHost, type DaemonClient, ENV_VARS, type ExecOptions, type ExecResult, FileVerdictMemoStore, type ForgeClient, type ForgePullRequestRef, ForgeUnavailableError, type GateState, type GcApplyOutcome, type GcClass, type GcPlanEntry, type GcReclaimReason, GhCliForgeClient, type GitWorktreeRef, HookError, type HookRun, type HostnameParts, InMemoryVerdictMemoStore, type IntegrationState, type ListWorktreeStatusesInput, type LivenessState, type NamedScript, type PeerService, type PlanGcInput, type ProvenanceConfidence, type ProvenanceInfo, ProxyTable, type ReconcileIntegrationInput, type ResolveGcClassOptions, type ResolveSupervisorInput, type ResolvedGcClass, type ResolvedWorktreeRuntime, RestForgeClient, type RunningProxy, SALVAGE_ROOT, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, type SalvageManifest, type SalvageResult, type SalvageWorktreeInput, type ScriptConfig, type ServiceRuntime, type ServiceStatus, ServiceSupervisor, type SessionRef, type SupervisorOptions, type TreeState, UnreachableForgeClient, VERDICT_MEMO_PATH, type VerdictMemoRecord, type VerdictMemoStore, WORKTREES_TURBO_CACHE_DIR_ENV, type WorktreeAgentInput, type WorktreeClass, type WorktreeEnvContext, type WorktreeMarker, WorktreeNotRemovableError, type WorktreeStatusEntry, allocatePort, applyGc, classify, classifyForGc, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, connectDaemonAgentSessionHost, createForgeClient, createProxyServer, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isMechanicalDepBumpRange, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, makeDaemonAgentSessionHost, normalizeHook, parseConfig, parseGithubOwnerRepo, peerEnv, peerEnvKey, planGc, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveGcClass, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, salvageWorktree, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, startProxy, stripPort, worktreeAgentInputSchema, worktreeAgentWorkflow, writeWorktreeMarker };
1386
+ export { type AgentprotoConfig, type ApplyGcOptions, CONFIG_FILENAME, type Classification, type ClassifyForGcOptions, type ComputeProvenanceOptions, type ComputeWorktreeStatusInput, ConfigError, DEFAULT_PROXY_PORT, type DaemonAgentSessionHost, type DaemonClient, ENV_VARS, type ExecOptions, type ExecResult, FileVerdictMemoStore, type ForgeClient, type ForgePullRequestRef, ForgeUnavailableError, type GateState, type GcApplyOutcome, type GcClass, type GcHoldReason, type GcPlanEntry, type GcReclaimReason, GhCliForgeClient, type GitWorktreeRef, HookError, type HookRun, type HostnameParts, InMemoryVerdictMemoStore, type IntegrationState, type ListWorktreeStatusesInput, type LivenessState, type NamedScript, type PeerService, type PlanGcInput, type ProvenanceConfidence, type ProvenanceInfo, ProxyTable, type ReconcileIntegrationInput, type ResolveGcClassOptions, type ResolveSupervisorInput, type ResolvedGcClass, type ResolvedWorktreeRuntime, RestForgeClient, type RunningProxy, SALVAGE_ROOT, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, type SalvageManifest, type SalvageResult, type SalvageWorktreeInput, type ScriptConfig, type ServiceRuntime, type ServiceStatus, ServiceSupervisor, type SessionRef, type SupervisorOptions, type TreeState, UnreachableForgeClient, VERDICT_MEMO_PATH, type VerdictMemoRecord, type VerdictMemoStore, WORKTREES_TURBO_CACHE_DIR_ENV, type WorktreeAgentInput, type WorktreeClass, type WorktreeEnvContext, type WorktreeMarker, WorktreeNotRemovableError, type WorktreeStatusEntry, allocatePort, applyGc, classify, classifyForGc, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, connectDaemonAgentSessionHost, createForgeClient, createProxyServer, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isMechanicalDepBumpRange, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, makeDaemonAgentSessionHost, normalizeHook, parseConfig, parseGithubOwnerRepo, peerEnv, peerEnvKey, planGc, readSessionsRegistry, readWorktreeMarker, reclaimOneWorktree, reconcileIntegration, repoLabel, resolveGcClass, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, salvageWorktree, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, startProxy, stripPort, worktreeAgentInputSchema, worktreeAgentWorkflow, writeWorktreeMarker };
package/dist/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
- export { connectDaemonAgentSessionHost, makeDaemonAgentSessionHost, worktreeAgentInputSchema, worktreeAgentWorkflow } from './chunk-PSNNWGAF.mjs';
1
+ export { connectDaemonAgentSessionHost, makeDaemonAgentSessionHost, worktreeAgentInputSchema, worktreeAgentWorkflow } from './chunk-UC36T7F7.mjs';
2
2
  import './chunk-E3EM24BS.mjs';
3
- import { computeProvenance, execArgv, classify, computeWorktreeStatus, listGitWorktrees, worktreeProvider } from './chunk-S5ARFUTK.mjs';
4
- export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, HookError, InMemoryVerdictMemoStore, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WORKTREES_TURBO_CACHE_DIR_ENV, WorktreeNotRemovableError, allocatePort, classify, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, normalizeHook, parseConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, worktreeProvider, writeWorktreeMarker } from './chunk-S5ARFUTK.mjs';
3
+ import { computeProvenance, execArgv, classify, computeWorktreeStatus, listGitWorktrees, worktreeProvider } from './chunk-Y2TW7PBC.mjs';
4
+ export { CONFIG_FILENAME, ConfigError, DEFAULT_PROXY_PORT, ENV_VARS, FileVerdictMemoStore, HookError, InMemoryVerdictMemoStore, ProxyTable, SESSIONS_BUCKETS_ROOT, SESSIONS_FILE_PATH, ServiceSupervisor, VERDICT_MEMO_PATH, WORKTREES_TURBO_CACHE_DIR_ENV, WorktreeNotRemovableError, allocatePort, classify, computeLiveness, computeProvenance, computeTreeState, computeWorktreeStatus, detectDefaultBranch, disposeSupervisor, ephemeralPort, execArgv, execGit, execShell, expandGlob, getScript, getSupervisor, globToRegExp, hookEnv, isPortFree, listGitWorktrees, listScripts, listServices, listWorktreeStatuses, loadConfigFromBase, normalizeHook, parseConfig, peerEnv, peerEnvKey, readSessionsRegistry, readWorktreeMarker, reconcileIntegration, repoLabel, resolveSupervisor, resolveWorktreesTurboCacheDir, runSetup, runTeardown, serviceEnv, serviceEnvToken, serviceHostname, serviceUrl, sessionInWorktree, sharedProxyTable, slugify, stripPort, worktreeProvider, writeWorktreeMarker } from './chunk-Y2TW7PBC.mjs';
5
5
  import { cleanupWorktreeTool } from './chunk-CM3XGWTY.mjs';
6
6
  export { cleanupWorktreeTool, listServicesTool, provisionWorktreeTool, runGateTool, runScriptTool, serviceStatusSchema, startServiceTool, stopServiceTool } from './chunk-CM3XGWTY.mjs';
7
- import { mkdir, copyFile, writeFile, open } from 'fs/promises';
7
+ import { mkdir, copyFile, writeFile, open, readdir, realpath, rm, readFile, stat } from 'fs/promises';
8
8
  import { homedir } from 'os';
9
- import { resolve, join, dirname, basename } from 'path';
9
+ import { resolve, join, dirname, basename, sep, relative, isAbsolute } from 'path';
10
10
  import http from 'http';
11
11
  import { connect } from 'net';
12
12
  import { z } from 'zod';
@@ -149,12 +149,12 @@ function startProxy(table, port = 0) {
149
149
  sockets.add(socket);
150
150
  socket.once("close", () => sockets.delete(socket));
151
151
  });
152
- return new Promise((resolve2, reject) => {
152
+ return new Promise((resolve3, reject) => {
153
153
  server.once("error", reject);
154
154
  server.listen(port, () => {
155
155
  const address = server.address();
156
156
  const boundPort = address && typeof address === "object" ? address.port : port;
157
- resolve2({
157
+ resolve3({
158
158
  port: boundPort,
159
159
  server,
160
160
  close: () => new Promise((res) => {
@@ -402,8 +402,8 @@ var RestForgeClient = class {
402
402
  };
403
403
  async function ghIsUsable(repoRoot) {
404
404
  try {
405
- const res = await execArgv("gh", ["auth", "status"], repoRoot);
406
- return res.exitCode === 0;
405
+ const res = await execArgv("gh", ["auth", "token"], repoRoot);
406
+ return res.exitCode === 0 && res.stdout.trim() !== "";
407
407
  } catch {
408
408
  return false;
409
409
  }
@@ -428,6 +428,10 @@ function classifyForGc(tree, integration, liveness, options = {}) {
428
428
  }
429
429
  return classify(tree, integration, liveness, options.nowMs).class;
430
430
  }
431
+ function isProtectedPath(worktreePath, protectedPaths) {
432
+ if (!protectedPaths || protectedPaths.length === 0) return false;
433
+ return protectedPaths.some((p) => p === worktreePath || p.startsWith(worktreePath + sep));
434
+ }
431
435
  var DEP_BUMP_SUBJECT_RE = /^(?:chore|fix)\(deps[\w-]*\):\s/;
432
436
  function isDepBumpSubject(subject) {
433
437
  return DEP_BUMP_SUBJECT_RE.test(subject);
@@ -462,7 +466,116 @@ async function linkedWorktreesOf(repoRoot) {
462
466
  const worktrees = await listGitWorktrees(repoRoot);
463
467
  return worktrees.filter((w) => w.path !== repoRoot);
464
468
  }
465
- async function toPlanEntry(repoRoot, defaultBranchRef, worktree, status, includeDetached, nowMs) {
469
+ async function gitWorktreesDir(repoRoot) {
470
+ const res = await execArgv("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], repoRoot);
471
+ if (res.exitCode !== 0) {
472
+ throw new Error(
473
+ `git rev-parse --git-common-dir failed in ${repoRoot} (exit ${res.exitCode}): ${res.stderr.trim()}`
474
+ );
475
+ }
476
+ return join(resolve(repoRoot, res.stdout.trim()), "worktrees");
477
+ }
478
+ async function readGitdirPointer(worktreePath) {
479
+ let raw;
480
+ try {
481
+ raw = await readFile(join(worktreePath, ".git"), "utf8");
482
+ } catch {
483
+ return null;
484
+ }
485
+ const match = raw.match(/^gitdir:\s*(.+?)\s*$/m);
486
+ const target = match?.[1];
487
+ if (!target) return null;
488
+ return resolve(worktreePath, target);
489
+ }
490
+ async function belongsToRepo(repoRoot, worktreePath) {
491
+ const pointer = await readGitdirPointer(worktreePath);
492
+ if (!pointer) return false;
493
+ let worktreesDir;
494
+ try {
495
+ worktreesDir = await gitWorktreesDir(repoRoot);
496
+ } catch {
497
+ return false;
498
+ }
499
+ const rel = relative(worktreesDir, pointer);
500
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
501
+ }
502
+ async function pathExists(path) {
503
+ try {
504
+ await stat(path);
505
+ return true;
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+ async function realOrResolved(path) {
511
+ try {
512
+ return await realpath(path);
513
+ } catch {
514
+ return resolve(path);
515
+ }
516
+ }
517
+ async function scanOrphanWorktreePaths(repoRoot, worktreesRoot) {
518
+ await execArgv("git", ["-C", repoRoot, "worktree", "prune"], repoRoot).catch(() => null);
519
+ const linkedRefs = await linkedWorktreesOf(repoRoot);
520
+ const linked = new Set(await Promise.all(linkedRefs.map((w) => realOrResolved(w.path))));
521
+ const entries = await readdir(worktreesRoot, { withFileTypes: true }).catch(() => []);
522
+ const orphans = [];
523
+ for (const entry of entries) {
524
+ if (!entry.isDirectory()) continue;
525
+ const candidate = resolve(worktreesRoot, entry.name);
526
+ if (linked.has(await realOrResolved(candidate))) continue;
527
+ if (await belongsToRepo(repoRoot, candidate)) orphans.push(candidate);
528
+ }
529
+ return orphans;
530
+ }
531
+ function makeOrphanPlanEntry(path, protectedPaths) {
532
+ if (isProtectedPath(path, protectedPaths)) {
533
+ return { path, branch: null, head: "", class: "hold", holdReason: "live-session-cwd" };
534
+ }
535
+ return { path, branch: null, head: "", class: "reclaim", reclaimReason: "orphan", orphan: true };
536
+ }
537
+ async function verifyOrphan(repoRoot, worktreePath) {
538
+ if (!await pathExists(worktreePath)) return "gone";
539
+ await execArgv("git", ["-C", repoRoot, "worktree", "prune"], repoRoot).catch(() => null);
540
+ const linked = await linkedWorktreesOf(repoRoot);
541
+ const realWorktreePath = await realOrResolved(worktreePath);
542
+ for (const w of linked) {
543
+ if (await realOrResolved(w.path) === realWorktreePath) return "not-orphan";
544
+ }
545
+ return await belongsToRepo(repoRoot, worktreePath) ? "confirmed" : "not-orphan";
546
+ }
547
+ async function reclaimOrphan(entry, options) {
548
+ const state = await verifyOrphan(options.repoRoot, entry.path);
549
+ if (state === "not-orphan") {
550
+ return { path: entry.path, branch: null, result: "aborted-reclassified", from: "reclaim", to: "hold" };
551
+ }
552
+ if (state === "confirmed") {
553
+ try {
554
+ await rm(entry.path, { recursive: true, force: true });
555
+ } catch (err) {
556
+ return {
557
+ path: entry.path,
558
+ branch: null,
559
+ result: "failed",
560
+ message: err instanceof Error ? err.message : String(err)
561
+ };
562
+ }
563
+ }
564
+ return { path: entry.path, branch: null, result: "reclaimed", reclaimReason: "orphan" };
565
+ }
566
+ async function toPlanEntry(repoRoot, defaultBranchRef, worktree, status, includeDetached, nowMs, protectedPaths) {
567
+ if (isProtectedPath(worktree.path, protectedPaths)) {
568
+ return {
569
+ path: worktree.path,
570
+ branch: worktree.branch,
571
+ head: worktree.head,
572
+ tree: status.tree,
573
+ integration: status.integration,
574
+ liveness: status.liveness,
575
+ class: "hold",
576
+ holdReason: "live-session-cwd"
577
+ };
578
+ }
466
579
  const resolved = await resolveGcClass(status.tree, status.integration, status.liveness, {
467
580
  repoRoot,
468
581
  tipSha: worktree.head,
@@ -497,7 +610,21 @@ async function planGc(input) {
497
610
  sessionsPath: input.sessionsPath,
498
611
  now: input.now
499
612
  });
500
- entries.push(await toPlanEntry(input.repoRoot, input.defaultBranchRef, worktree, status, includeDetached, nowMs));
613
+ entries.push(
614
+ await toPlanEntry(
615
+ input.repoRoot,
616
+ input.defaultBranchRef,
617
+ worktree,
618
+ status,
619
+ includeDetached,
620
+ nowMs,
621
+ input.protectedPaths
622
+ )
623
+ );
624
+ }
625
+ if (input.worktreesRoot) {
626
+ const orphanPaths = await scanOrphanWorktreePaths(input.repoRoot, input.worktreesRoot);
627
+ for (const path of orphanPaths) entries.push(makeOrphanPlanEntry(path, input.protectedPaths));
501
628
  }
502
629
  return entries;
503
630
  }
@@ -592,6 +719,22 @@ async function salvageOne(options, worktree) {
592
719
  return { path: worktree.path, branch: worktree.branch, result: "salvaged", salvageDir };
593
720
  }
594
721
  async function applyOne(entry, options) {
722
+ if (isProtectedPath(entry.path, options.protectedPaths)) {
723
+ if (entry.class === "hold") {
724
+ return { path: entry.path, branch: entry.branch, result: "held", holdReason: "live-session-cwd" };
725
+ }
726
+ return {
727
+ path: entry.path,
728
+ branch: entry.branch,
729
+ result: "aborted-reclassified",
730
+ from: entry.class,
731
+ to: "hold",
732
+ holdReason: "live-session-cwd"
733
+ };
734
+ }
735
+ if (entry.orphan) {
736
+ return reclaimOrphan(entry, options);
737
+ }
595
738
  if (entry.class === "hold") {
596
739
  return { path: entry.path, branch: entry.branch, result: "held" };
597
740
  }
@@ -638,7 +781,31 @@ async function applyGc(plan, options) {
638
781
  }
639
782
  return outcomes;
640
783
  }
784
+ async function reclaimOneWorktree(worktreePath, options) {
785
+ const worktree = await findWorktree(options.repoRoot, worktreePath);
786
+ if (!worktree) return null;
787
+ const status = await computeWorktreeStatus({
788
+ repoRoot: options.repoRoot,
789
+ repoName: options.repoName,
790
+ worktree,
791
+ forge: options.forge,
792
+ memo: options.memo,
793
+ defaultBranchRef: options.defaultBranchRef,
794
+ sessionsPath: options.sessionsPath,
795
+ now: options.now
796
+ });
797
+ const entry = await toPlanEntry(
798
+ options.repoRoot,
799
+ options.defaultBranchRef,
800
+ worktree,
801
+ status,
802
+ Boolean(options.includeDetached),
803
+ options.nowMs ?? Date.now(),
804
+ options.protectedPaths
805
+ );
806
+ return applyOne(entry, options);
807
+ }
641
808
 
642
- export { ForgeUnavailableError, GhCliForgeClient, RestForgeClient, SALVAGE_ROOT, UnreachableForgeClient, applyGc, classifyForGc, createForgeClient, createProxyServer, isMechanicalDepBumpRange, parseGithubOwnerRepo, planGc, resolveGcClass, salvageWorktree, startProxy };
809
+ export { ForgeUnavailableError, GhCliForgeClient, RestForgeClient, SALVAGE_ROOT, UnreachableForgeClient, applyGc, classifyForGc, createForgeClient, createProxyServer, isMechanicalDepBumpRange, parseGithubOwnerRepo, planGc, reclaimOneWorktree, resolveGcClass, salvageWorktree, startProxy };
643
810
  //# sourceMappingURL=index.mjs.map
644
811
  //# sourceMappingURL=index.mjs.map