@agentproto/worktree 0.5.2 → 0.5.5

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
@@ -3,7 +3,7 @@ export { worktreeProvider } from './provider/index.js';
3
3
  import { z } from 'zod';
4
4
  import http from 'node:http';
5
5
  import { RuntimeWorkflow, AgentSessionHost } from '@agentproto/workflow-runtime';
6
- import { StartAgentArgs, SessionDescriptor, TurnEvent, TurnResult, HarnessClient, ConnectHarnessOptions } from '@agentproto/harness';
6
+ import { StartAgentArgs, SessionDescriptor, TurnEvent, TurnResult, SessionUsageSnapshot, HarnessClient, ConnectHarnessOptions } from '@agentproto/harness';
7
7
  import '@agentproto/tool';
8
8
  import '@agentproto/driver';
9
9
 
@@ -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,11 @@ 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"> & {
1087
+ /** `session_usage` — optional so pre-existing fakes/clients without it still
1088
+ * satisfy the type; the host simply exposes no `usage` then. */
1089
+ usage?: HarnessClient["usage"];
1090
+ };
1078
1091
  interface DaemonAgentSessionHost extends AgentSessionHost {
1079
1092
  close(): Promise<void>;
1080
1093
  /** Full `agent_start`, returning the raw descriptor and forwarding every
@@ -1099,7 +1112,16 @@ interface DaemonAgentSessionHost extends AgentSessionHost {
1099
1112
  waitForAny(sessionIds: string[], opts?: {
1100
1113
  timeoutMs?: number;
1101
1114
  event?: TurnEvent;
1115
+ since?: number;
1102
1116
  }): Promise<TurnResult>;
1117
+ /** Race-free cursor for a subsequent `waitForAny({ since })` — see
1118
+ * `HarnessClient.currentEventsCursor`. */
1119
+ currentEventsCursor(): Promise<number>;
1120
+ /** Usage accounting for a session on this daemon (`session_usage`) — present
1121
+ * when the underlying client can answer it. `@agentproto/sandbox`'s host
1122
+ * inherits it, which is how a sandboxed session's cost/tokens/model reach
1123
+ * the HOST daemon's descriptor (the proxy's text stream carries none). */
1124
+ usage?(sessionId: string): Promise<SessionUsageSnapshot>;
1103
1125
  }
1104
1126
  /**
1105
1127
  * Connect to the agentproto daemon's MCP endpoint and return a real,
@@ -1173,8 +1195,18 @@ interface ClassifyForGcOptions {
1173
1195
  * also depends on.
1174
1196
  */
1175
1197
  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";
1198
+ /**
1199
+ * The reclaim reasons this module can attach. `dep-bump` promotes a `hold`
1200
+ * to `reclaim` (see above). `orphan` is different in kind — it never starts
1201
+ * from a `hold` verdict at all, because an orphan was never classified in
1202
+ * the first place (see the "orphan reclaim" section below): a directory
1203
+ * physically present under the repo's worktree pool that `git worktree
1204
+ * list` no longer knows about, so none of `classify`'s three axes can be
1205
+ * computed for it.
1206
+ */
1207
+ type GcReclaimReason = "dep-bump" | "orphan";
1208
+ /** The hold reasons this module can attach, distinct from `classify`'s snapshot-based holds. */
1209
+ type GcHoldReason = "live-session-cwd";
1178
1210
  /**
1179
1211
  * Layer 3+4 of the exemption: EVERY commit ahead of `baseRef` must have a
1180
1212
  * dep-bump subject (`git log --format=%s`, two-dot range — exactly the
@@ -1214,13 +1246,30 @@ interface GcPlanEntry {
1214
1246
  path: string;
1215
1247
  branch: string | null;
1216
1248
  head: string;
1217
- tree: TreeState;
1218
- integration: IntegrationState;
1219
- liveness: LivenessState;
1249
+ /**
1250
+ * Absent only for an orphan entry (`orphan: true`) — git itself cannot
1251
+ * answer the tree/integration/liveness questions for a directory it no
1252
+ * longer recognizes as a worktree, so this module never fabricates a
1253
+ * value for them rather than reporting a fact it can't actually check.
1254
+ */
1255
+ tree?: TreeState;
1256
+ integration?: IntegrationState;
1257
+ liveness?: LivenessState;
1220
1258
  /** Classification per PLAN.md §5.1, after `--include-detached` (if set). */
1221
1259
  class: GcClass;
1222
- /** Set only when `class === "reclaim"` via the dep-bump exemption — see `resolveGcClass`. */
1260
+ /** Set only when `class === "reclaim"` via the dep-bump exemption or the orphan reclaim path — see `resolveGcClass` / `scanOrphanWorktreePaths`. */
1223
1261
  reclaimReason?: GcReclaimReason;
1262
+ /** Set only when `class === "hold"` via `protectedPaths` (`isProtectedPath`) — distinguishes this from an ordinary snapshot-based hold. */
1263
+ holdReason?: GcHoldReason;
1264
+ /**
1265
+ * `true` only for a directory the orphan scan found: physically present
1266
+ * under the repo's worktree pool, but absent from `git worktree list`
1267
+ * (even after `git worktree prune`) — the interrupted-`worktree remove`
1268
+ * shape (module docblock's §5 note). Always paired with `class: "reclaim"`
1269
+ * and `reclaimReason: "orphan"`. Never set for a linked worktree, however
1270
+ * it classifies.
1271
+ */
1272
+ orphan?: boolean;
1224
1273
  }
1225
1274
  interface PlanGcInput {
1226
1275
  repoRoot: string;
@@ -1233,6 +1282,23 @@ interface PlanGcInput {
1233
1282
  now?: () => string;
1234
1283
  /** See `ComputeWorktreeStatusInput.nowMs` (status.ts) — kept separate from `now`. Defaults to `Date.now()`. */
1235
1284
  nowMs?: number;
1285
+ /**
1286
+ * The bucket directory this repo's worktrees are provisioned under — e.g.
1287
+ * `<worktrees.root>/<repoLabel>` (see `resolveWorktreesRoot` / `repoLabel`,
1288
+ * both resolved by the caller; this module does no root resolution of its
1289
+ * own, see the orphan-reclaim section below). When omitted, orphan
1290
+ * scanning is skipped entirely and `planGc`/`applyGc` behave exactly as
1291
+ * before — every existing caller that doesn't pass this sees zero change.
1292
+ */
1293
+ worktreesRoot?: string;
1294
+ /**
1295
+ * Absolute paths (typically live session cwds) that must never be
1296
+ * classified `reclaim`/`salvage`, exact-or-subdirectory (`isProtectedPath`)
1297
+ * — see the "live-session-cwd protection" section above. Applies to both
1298
+ * linked worktrees and orphan-scan entries. Omitted ⇒ no additional
1299
+ * protection beyond `classify`'s own snapshot-based liveness axis.
1300
+ */
1301
+ protectedPaths?: string[];
1236
1302
  }
1237
1303
  /**
1238
1304
  * The dry-run plan (PLAN.md §5.2 layer 1): classify every linked worktree,
@@ -1255,6 +1321,8 @@ interface ApplyGcOptions {
1255
1321
  nowMs?: number;
1256
1322
  /** Override for `~/.agentproto/worktree-salvage` — tests use a temp dir. */
1257
1323
  salvageRoot?: string;
1324
+ /** 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. */
1325
+ protectedPaths?: string[];
1258
1326
  }
1259
1327
  type GcApplyOutcome = {
1260
1328
  path: string;
@@ -1270,6 +1338,7 @@ type GcApplyOutcome = {
1270
1338
  path: string;
1271
1339
  branch: string | null;
1272
1340
  result: "held";
1341
+ holdReason?: GcHoldReason;
1273
1342
  }
1274
1343
  /** Salvage-class, but `--salvage-dirty` was not passed: left untouched by design. */
1275
1344
  | {
@@ -1284,6 +1353,7 @@ type GcApplyOutcome = {
1284
1353
  result: "aborted-reclassified";
1285
1354
  from: GcClass;
1286
1355
  to: GcClass;
1356
+ holdReason?: GcHoldReason;
1287
1357
  }
1288
1358
  /** The worktree named in the plan no longer exists (already removed by something else since the plan was made). */
1289
1359
  | {
@@ -1306,5 +1376,20 @@ type GcApplyOutcome = {
1306
1376
  * `git worktree remove`.
1307
1377
  */
1308
1378
  declare function applyGc(plan: readonly GcPlanEntry[], options: ApplyGcOptions): Promise<GcApplyOutcome[]>;
1379
+ /**
1380
+ * Attempt to reclaim exactly one worktree by path — the exit-time auto-
1381
+ * reclaim primitive `sessions.ts` calls (via the injected
1382
+ * `WorktreeAutoReclaimer` port) when a policy-provisioned session reaches a
1383
+ * terminal state. Classifies it fresh (never trusts a caller-supplied
1384
+ * verdict) and only ever removes it when that classification is `reclaim`
1385
+ * (merged-or-fresh, clean, idle) — a dirty or held worktree is returned
1386
+ * as-is, exactly like any other `gc` outcome, still visible via
1387
+ * `worktree_status` and still manually reclaimable. Returns `null` when the
1388
+ * path no longer names a linked worktree at all (already removed by
1389
+ * something else — nothing to do, not an error).
1390
+ */
1391
+ declare function reclaimOneWorktree(worktreePath: string, options: ApplyGcOptions & {
1392
+ includeDetached?: boolean;
1393
+ }): Promise<GcApplyOutcome | null>;
1309
1394
 
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 };
1395
+ 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-R6UERXV4.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) => {
@@ -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