@webpieces/repo-workflow-core 0.0.1

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.
Files changed (46) hide show
  1. package/package.json +31 -0
  2. package/src/agent-worktree-lock.d.ts +21 -0
  3. package/src/agent-worktree-lock.js +77 -0
  4. package/src/agent-worktree-lock.js.map +1 -0
  5. package/src/branch-archiver.d.ts +70 -0
  6. package/src/branch-archiver.js +157 -0
  7. package/src/branch-archiver.js.map +1 -0
  8. package/src/branch-identity.d.ts +6 -0
  9. package/src/branch-identity.js +59 -0
  10. package/src/branch-identity.js.map +1 -0
  11. package/src/branch-mutation-log.d.ts +77 -0
  12. package/src/branch-mutation-log.js +221 -0
  13. package/src/branch-mutation-log.js.map +1 -0
  14. package/src/branch-reaper.d.ts +122 -0
  15. package/src/branch-reaper.js +261 -0
  16. package/src/branch-reaper.js.map +1 -0
  17. package/src/harness-agent-activity.d.ts +154 -0
  18. package/src/harness-agent-activity.js +346 -0
  19. package/src/harness-agent-activity.js.map +1 -0
  20. package/src/index.d.ts +14 -0
  21. package/src/index.js +18 -0
  22. package/src/index.js.map +1 -0
  23. package/src/main-sync-file.d.ts +121 -0
  24. package/src/main-sync-file.js +281 -0
  25. package/src/main-sync-file.js.map +1 -0
  26. package/src/main-sync-status.d.ts +197 -0
  27. package/src/main-sync-status.js +555 -0
  28. package/src/main-sync-status.js.map +1 -0
  29. package/src/merged-branch-verdicts.d.ts +189 -0
  30. package/src/merged-branch-verdicts.js +251 -0
  31. package/src/merged-branch-verdicts.js.map +1 -0
  32. package/src/merged-branches.d.ts +140 -0
  33. package/src/merged-branches.js +521 -0
  34. package/src/merged-branches.js.map +1 -0
  35. package/src/review-identity-stamp.d.ts +82 -0
  36. package/src/review-identity-stamp.js +172 -0
  37. package/src/review-identity-stamp.js.map +1 -0
  38. package/src/worktree-lock-verdicts.d.ts +149 -0
  39. package/src/worktree-lock-verdicts.js +225 -0
  40. package/src/worktree-lock-verdicts.js.map +1 -0
  41. package/src/worktree-reaper.d.ts +116 -0
  42. package/src/worktree-reaper.js +318 -0
  43. package/src/worktree-reaper.js.map +1 -0
  44. package/src/worktrees.d.ts +113 -0
  45. package/src/worktrees.js +260 -0
  46. package/src/worktrees.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main-sync-status.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/main-sync-status.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AtomicFile } from '@webpieces/tooling-common';\nimport {\n MAIN_SYNC_STATUS_VERSION,\n MainSyncFileStore,\n MainSyncStatus,\n MainSyncStatusFile,\n PullRequestIndex,\n} from './main-sync-file';\nimport { DotWebpieces, dotWebpieces } from '@webpieces/tooling-common';\nimport { toError } from '@webpieces/tooling-common/to-error';\nimport { WorktreeService } from './worktrees';\n\n// The on-disk shape lives in main-sync-file.ts; re-exported so every existing importer of\n// `MainSyncStatus` from this module keeps working.\nexport { MainSyncStatus, MainSyncStatusFile, PullRequestIndex, MAIN_SYNC_STATUS_VERSION };\n\n// Shared \"is my feature branch healthy relative to origin/main?\" state. The SLOW signals (git fetch +\n// merge-base + same-file-overlap + a merged-PR lookup) are computed by the ai-hook-rules refresher in a\n// DETACHED background process; the feature-branch-guard only READS this cached file. pr-gate's merge\n// flow also writes it synchronously after a merge. Lives here (the shared dep of both).\n\n// How long an `inprocess` refresher lock may sit before a new refresher assumes the prior run hung.\nexport const DEFAULT_HANG_TIMEOUT_MINUTES = 5;\n\nconst MAIN_SYNC_STATUS_FILE = 'main-sync-status.json';\nconst MAIN_SYNC_LOCK_FILE = 'main-sync.lock.json';\n\nconst LOCK_STATE_INPROCESS = 'inprocess';\nconst LOCK_STATE_FINISHED = 'finished';\n\n// Concurrency state machine for the detached refresher. `started` is epoch ms. `pid` is the refresher\n// process's pid (0 = unknown) — used so a KILLED refresher doesn't wedge `inprocess` for the timeout.\nexport class MainSyncLock {\n state: string;\n started: number;\n pid: number;\n\n constructor(state: string, started: number, pid: number = 0) {\n this.state = state;\n this.started = started;\n this.pid = pid;\n }\n}\n\n// Raw JSON shape for the cast at the parse boundary.\ninterface RawLock {\n state?: string;\n started?: number;\n pid?: number;\n}\n\n// Result of a captured git/gh invocation: ok=false on spawn failure or non-zero exit.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n}\n\n/**\n * Reads/writes the main-sync cache + lock and computes the slow \"is my branch healthy vs origin/main?\"\n * status. `@injectable(bindingScopeValues.Singleton)` so it's injectable and drawn in consumers’ DI designs.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class MainSyncStatusService {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n private readonly store: MainSyncFileStore = new MainSyncFileStore(atomicFile),\n private readonly worktrees: WorktreeService = new WorktreeService(),\n ) {}\n\n /**\n * SHARED scope: `<primary>/.webpieces/main-sync-status.json`, one per repo.\n *\n * It is the OUTPUT of the single-flight refresher below, so it has to live where the lock lives —\n * one `.git`, one `origin/main`, one fetch, one answer. A per-worktree copy under single-flight\n * would only ever be written for whichever worktree won the lock anyway.\n *\n * FORMERLY a known defect, now fixed: the file's CONTENT used to be keyed to ONE branch, and every\n * reader fails OPEN when the cached branch is not the branch being judged (`stale-cross-branch-cache\n * (fail-open)`). With several worktrees on several branches, only the winner of the lock was ever\n * described, so every other worktree's guards abstained — and which one was armed thrashed as the\n * lock changed hands. The file is now a MAP of branch -> status (see MainSyncStatusFile): the single\n * winning refresher enumerates every worktree and records all of their branches in one atomic write,\n * so one fetch arms everyone. Single-flight is unchanged; only what the winner writes changed.\n */\n mainSyncStatusPath(repoRoot: string): string {\n return this.dotDir.sharedFile(repoRoot, MAIN_SYNC_STATUS_FILE);\n }\n\n /**\n * SHARED scope — the single-flight lock for the whole repo.\n *\n * This is the fix for the filed bug \"the detached main-sync refresher's `git fetch` races the\n * agent's foreground `git fetch`/`git pull` and corrupts `.git/FETCH_HEAD`\". The lock was already\n * atomic (O_CREAT|O_EXCL) and already serialised refreshers against each other — but only WITHIN\n * one worktree, so N worktrees meant N locks and up to N concurrent `git fetch`es against the ONE\n * shared `.git`, which is exactly how a duplicate `for-merge` line lands in FETCH_HEAD. There is\n * one `.git`, so there is now one lock: at most one refresher in flight across all worktrees.\n *\n * Stale-lock handling is unchanged and still required (a lock nobody can clear is worse than the\n * race): the holder records pid + start epoch, and `tryAcquireMainSyncLock` reclaims it once\n * `isRefreshInProgress` proves the holder is finished, hung past hangTimeoutMinutes, or dead.\n */\n mainSyncLockPath(repoRoot: string): string {\n return this.dotDir.sharedFile(repoRoot, MAIN_SYNC_LOCK_FILE);\n }\n\n /**\n * The cached status FOR ONE BRANCH — the only read the four main-sync guards perform.\n *\n * `branch` is mandatory and is the branch being judged. A branch the last refresh never saw\n * returns null, which every guard already treats as `no-sync-cache (fail-open)`, so an unknown\n * branch degrades exactly as a missing file did. Any error (missing, truncated, malformed JSON)\n * also returns null — this must never throw on the blocking path.\n */\n readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null {\n return this.store.branchStatus(this.readMainSyncStatusFile(repoRoot), branch);\n }\n\n // The whole branch-keyed document, or null. Callers that need more than one branch use this.\n readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null {\n return this.store.readFile(this.mainSyncStatusPath(repoRoot));\n }\n\n /**\n * Merge ONE branch's entry into the existing map, atomically, leaving siblings untouched.\n *\n * Read-modify-write rather than replace: this is the writer used by everyone who is NOT the\n * single-flight refresher (notably pr-gate's post-merge stamp, which holds no lock and knows about\n * exactly one branch). Replacing the document from there would erase every other worktree's entry\n * and silently disarm their guards until the next refresh.\n */\n writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void {\n this.store.mergeBranch(this.mainSyncStatusPath(repoRoot), status);\n }\n\n /**\n * Replace the WHOLE document — only the winning refresher may do this.\n *\n * ATOMIC (temp file + `rename()` in the same directory). The refresher writes this file while\n * guards on the blocking path read it, possibly from another worktree; a plain `writeFileSync`\n * truncates first, so a reader landing in that window gets a torn document. See AtomicFile.\n */\n writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void {\n this.store.writeFile(this.mainSyncStatusPath(repoRoot), file);\n }\n\n readMainSyncLock(repoRoot: string): MainSyncLock | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const lockPath = this.mainSyncLockPath(repoRoot);\n if (!fs.existsSync(lockPath)) return null;\n const raw = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as RawLock;\n return new MainSyncLock(raw.state ?? LOCK_STATE_FINISHED, raw.started ?? 0, raw.pid ?? 0);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void {\n const lockPath = this.mainSyncLockPath(repoRoot);\n this.ensureDir(lockPath);\n fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\\n');\n }\n\n // A lock is stale (prior refresher assumed hung) once `inprocess` longer than hangTimeoutMinutes.\n isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now: number = Date.now()): boolean {\n return now - lock.started > hangTimeoutMinutes * 60 * 1000;\n }\n\n // True when another refresher is actively running and we should NOT start a second one.\n isRefreshInProgress(repoRoot: string, hangTimeoutMinutes: number, now: number = Date.now()): boolean {\n const lock = this.readMainSyncLock(repoRoot);\n if (!lock) return false;\n if (lock.state !== LOCK_STATE_INPROCESS) return false;\n if (this.isLockStale(lock, hangTimeoutMinutes, now)) return false;\n return this.isProcessAlive(lock.pid);\n }\n\n /**\n * ATOMICALLY take the refresher lock. Returns the held lock, or null when someone else holds it.\n *\n * Replaces the check-then-write pair (`isRefreshInProgress` then `writeMainSyncLock`), whose gap\n * let two detached refreshers both pass the check and then both run `git fetch` at once — the\n * concurrency this lock exists to prevent. The create uses the `wx` flag (O_CREAT|O_EXCL), so of\n * N racing refreshers exactly one creates the file.\n *\n * A lock file left behind by a FINISHED, hung, or killed refresher is reclaimed: we only unlink\n * and re-take it once `isRefreshInProgress` says the holder is provably not running, and we\n * re-read afterwards to confirm the entry we see is ours before claiming the lock.\n */\n tryAcquireMainSyncLock(\n repoRoot: string,\n hangTimeoutMinutes: number,\n now: number = Date.now(),\n pid: number = process.pid,\n ): MainSyncLock | null {\n const lockPath = this.mainSyncLockPath(repoRoot);\n this.ensureDir(lockPath);\n const lock = this.inProcessLock(now, pid);\n const payload = JSON.stringify(lock, null, 2) + '\\n';\n\n if (this.createExclusive(lockPath, payload)) return lock;\n\n // The file already exists. Leave it alone while a live refresher owns it.\n if (this.isRefreshInProgress(repoRoot, hangTimeoutMinutes, now)) return null;\n\n // Reclaim it: remove the dead entry, then re-take it exclusively so only one reclaimer wins.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.unlinkSync(lockPath);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // someone else reclaimed it first — the exclusive create below decides\n }\n if (!this.createExclusive(lockPath, payload)) return null;\n\n // Confirm the lock on disk is OURS (a simultaneous reclaimer could have unlinked ours and\n // written its own between the two calls above).\n const held = this.readMainSyncLock(repoRoot);\n if (!held || held.pid !== pid || held.started !== lock.started) return null;\n return lock;\n }\n\n // O_CREAT|O_EXCL write: true when THIS call created the file, false when it already existed.\n private createExclusive(lockPath: string, payload: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.writeFileSync(lockPath, payload, { flag: 'wx' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n inProcessLock(now: number = Date.now(), pid: number = process.pid): MainSyncLock {\n return new MainSyncLock(LOCK_STATE_INPROCESS, now, pid);\n }\n\n finishedLock(started: number): MainSyncLock {\n return new MainSyncLock(LOCK_STATE_FINISHED, started, 0);\n }\n\n /**\n * The SLOW path for ONE branch: the branch checked out in `repoRoot`. Runs its own fetch and its\n * own `gh` query, so it is the right entry point for a single-worktree caller and for tests.\n * The refresher uses computeAllMainSyncStatuses instead — see there for why.\n */\n computeMainSyncStatus(repoRoot: string): MainSyncStatus {\n // Best-effort network refresh; offline just means we evaluate against the last-fetched ref.\n this.fetchOriginMain(repoRoot);\n return this.computeBranchStatus(repoRoot, this.gitBranch(repoRoot), this.loadPullRequestIndex(repoRoot));\n }\n\n /**\n * The SLOW path for EVERY branch this repo has checked out — what the detached refresher runs.\n *\n * The cache is shared by all worktrees but was written for one branch, so only one worktree's\n * guards were ever armed. Enumerating the worktrees here and writing all of their branches in one\n * atomic map arms every worktree off a single refresh.\n *\n * NETWORK COST DOES NOT SCALE WITH BRANCH COUNT, which is the whole reason this is a separate\n * method: ONE `git fetch` and ONE repo-wide `gh pr list` for all N branches (the old code asked\n * `gh` twice per branch, so looping it would have cost 2N round trips). Everything after that is\n * local git in each worktree — merge-base, rev-parse, diff, ls-files.\n *\n * The map is rebuilt from the LIVE worktree set every refresh, so a deleted branch or removed\n * worktree simply stops appearing. No pruning pass, no unbounded growth.\n */\n computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile {\n this.fetchOriginMain(repoRoot);\n const index = this.loadPullRequestIndex(repoRoot);\n const branches: Record<string, MainSyncStatus> = {};\n for (const tree of this.worktrees.listWorktrees(repoRoot)) {\n // Detached HEAD (and a bare worktree) has no branch to key an entry on. Skipping it must\n // not stop the others being recorded, which is why this is a `continue`, not a bail-out.\n if (tree.branch === '' || this.hasBranch(branches, tree.branch)) continue;\n branches[tree.branch] = this.computeBranchStatus(tree.path, tree.branch, index);\n }\n // `main` always present, even with no worktree on it: read-stale-guard's state A looks itself\n // up under 'main', and an absent entry there would silently disarm the stale-main block.\n if (!this.hasBranch(branches, 'main')) branches['main'] = this.mainOnlyStatus(repoRoot);\n return new MainSyncStatusFile(MAIN_SYNC_STATUS_VERSION, new Date().toISOString(), branches);\n }\n\n private hasBranch(branches: Record<string, MainSyncStatus>, branch: string): boolean {\n return Object.prototype.hasOwnProperty.call(branches, branch);\n }\n\n /**\n * One branch's status, computed in `worktreePath` (the tree that has it checked out — the\n * working-tree overlap signals are only correct from there). Does NOT fetch and does NOT call\n * `gh`: both are done once per refresh by the caller.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive slow-path computation\n private computeBranchStatus(repoRoot: string, branch: string, index: PullRequestIndex): MainSyncStatus {\n return this.stampForge(this.computeBranchStatusInner(repoRoot, branch, index), index);\n }\n\n // Every return path of the computation below has to carry the forge flag, and threading it through\n // four constructions is how one of them would end up missing it. One wrapper, one assignment.\n private stampForge(status: MainSyncStatus, index: PullRequestIndex): MainSyncStatus {\n status.forgeReachable = index.forgeReachable;\n return status;\n }\n\n // webpieces-disable max-lines-new-methods -- one cohesive slow-path computation\n private computeBranchStatusInner(repoRoot: string, branch: string, index: PullRequestIndex): MainSyncStatus {\n const mergedPr = index.mergedFor(branch);\n const openPr = index.openFor(branch);\n\n const head = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);\n const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);\n const localMain = this.localMainHash(repoRoot);\n const featureHead = head.ok ? head.out : '';\n if (!head.ok || !originMain.ok) {\n const status = this.benignStatus(branch, featureHead);\n status.branchAlreadyMerged = mergedPr !== '';\n status.mergedPr = mergedPr;\n status.openPr = openPr;\n status.localMain = localMain;\n status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');\n return status;\n }\n\n const forkPoint = this.capture(repoRoot, 'git', ['merge-base', 'origin/main', 'HEAD']);\n if (!forkPoint.ok || forkPoint.out === '') {\n const noFork = new MainSyncStatus(branch, mergedPr !== '', mergedPr, false, null, originMain.out, featureHead, false, [], new Date().toISOString());\n noFork.openPr = openPr;\n noFork.localMain = localMain;\n noFork.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.out);\n return noFork;\n }\n\n const featureFiles = new Set(this.featureChangedFiles(repoRoot, forkPoint.out));\n const mainFiles = this.changedFiles(repoRoot, forkPoint.out, 'origin/main');\n const conflictFiles = mainFiles.filter((file: string): boolean => featureFiles.has(file));\n\n const status = new MainSyncStatus(\n branch,\n mergedPr !== '',\n mergedPr,\n true,\n forkPoint.out,\n originMain.out,\n featureHead,\n conflictFiles.length > 0,\n conflictFiles,\n new Date().toISOString(),\n );\n status.openPr = openPr;\n status.localMain = localMain;\n status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.out);\n return status;\n }\n\n // The recovery steps when there is no fork point with origin/main (a bad merge of main into branch).\n squashRecoverySteps(currentBranch: string): string[] {\n return [\n '1. Fetch latest main: git fetch origin main',\n `2. New branch off origin/main: git checkout -b ${currentBranch}-v2 origin/main`,\n `3. Squash-merge old branch: git merge --squash ${currentBranch}`,\n ' ^^ HUMAN-ONLY. `git merge` is blocked for AI (redirect-how-to-merge-main). AI: ask the',\n ' human to run step 3, and warn them it is a raw merge — only correct here because the',\n ' branch is already broken. For a normal update from main they should push back and',\n ' tell you to use the gated 3-point merge instead: `pnpm wp-start-update` (paired with',\n ' `pnpm wp-finish-update`) when no PR is open, or `pnpm wp-start-upsert-pr` (paired with',\n ' `pnpm wp-finish-upsert-pr`) when a PR IS open — a PR MUST use the upsert-pr pair.',\n `4. Commit the squash: git add -A && git commit -m \"Squashed from ${currentBranch}\"`,\n '5. If a PR exists: open a NEW PR for the -v2 branch and close the old one.',\n ];\n }\n\n /**\n * Synchronously stamp a clean \"up to date with main\" status — call right after a successful merge.\n *\n * This is the one writer that takes NO lock, and it knows about exactly one branch. It therefore\n * goes through writeMainSyncStatus, which MERGES its entry into the existing map; writing a whole\n * document from here would wipe every other worktree's entry.\n */\n stampCleanMainSyncStatus(repoRoot: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const branch = this.gitBranch(repoRoot);\n const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);\n const featureHead = this.capture(repoRoot, 'git', ['rev-parse', 'HEAD']);\n if (!originMain.ok || !featureHead.ok) return;\n const status = new MainSyncStatus(\n branch, false, '', true, originMain.out, originMain.out, featureHead.out, false, [], new Date().toISOString(),\n );\n status.localMain = this.localMainHash(repoRoot);\n status.commitsBehind = 0;\n this.writeMainSyncStatus(repoRoot, status);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Refresh `origin/main` WITHOUT writing `.git/FETCH_HEAD`.\n *\n * WHY the flag is not optional: this runs in a DETACHED background process while the agent is\n * running its own foreground `git fetch` / `git pull` in the very same repo. `.git/FETCH_HEAD` is\n * a single file that git takes no lock on, so two overlapping fetches interleave their writes and\n * can leave the SAME `for-merge` line twice. `git pull` then reads two for-merge entries and dies\n * with `fatal: Cannot fast-forward to multiple branches` — wedging the exact command the\n * read-stale-guard tells the agent to run. `--no-write-fetch-head` (git >= 2.29) still updates the\n * remote-tracking ref, which is all anything downstream reads (`origin/main`, merge-base), so\n * nothing here needs FETCH_HEAD written at all.\n *\n * Older git rejects the flag; only then do we retry the plain form, accepting the old behaviour\n * rather than losing the refresh entirely on a pre-2020 git.\n */\n private fetchOriginMain(repoRoot: string): void {\n const safe = spawnSync('git', ['fetch', '--no-write-fetch-head', 'origin', 'main'], {\n cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'ignore', 'pipe'],\n });\n if (safe.status === 0) return;\n if (!this.isUnknownGitOption(safe.stderr)) return; // a real failure (offline, auth) — not our business\n spawnSync('git', ['fetch', 'origin', 'main'], { cwd: repoRoot, stdio: 'ignore' });\n }\n\n // Did git reject the flag itself (too old), as opposed to failing the network refresh?\n private isUnknownGitOption(stderr: string | null): boolean {\n const text = (stderr ?? '').toLowerCase();\n return text.includes('unknown option') || text.includes('unknown switch') || text.includes('unrecognized option');\n }\n\n private ensureDir(filePath: string): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n }\n\n // Liveness probe: is `pid` still a running process? pid <= 0 (unknown) → assume alive.\n private isProcessAlive(pid: number): boolean {\n if (pid <= 0) return true;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n return !error.message.includes('ESRCH');\n }\n }\n\n // Run a command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.\n private capture(repoRoot: string, cmd: string, args: string[]): CmdCapture {\n const result = spawnSync(cmd, args, { cwd: repoRoot, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return { ok: false, out: '' };\n return { ok: true, out: result.stdout.trim() };\n }\n\n // The actual checked-out branch in repoRoot — cwd-correct so the cache's `branch` label matches.\n private gitBranch(repoRoot: string): string {\n const result = this.capture(repoRoot, 'git', ['rev-parse', '--abbrev-ref', 'HEAD']);\n return result.ok ? result.out : '';\n }\n\n // The LOCAL main hash. `refs/heads/main` (not the bare name) so it can never resolve to a remote\n // ref or a tag. '' when main does not exist locally — which the guard treats as fail-open.\n private localMainHash(repoRoot: string): string {\n const result = this.capture(repoRoot, 'git', ['rev-parse', 'refs/heads/main']);\n return result.ok ? result.out : '';\n }\n\n // Slow-path only: the detached refresher calls this after fetching. Guards never count commits.\n private commitDistance(repoRoot: string, localMain: string, originMain: string): number | null {\n if (localMain === '' || originMain === '') return null;\n const result = this.capture(repoRoot, 'git', ['rev-list', '--count', `${localMain}..${originMain}`]);\n if (!result.ok || !/^\\d+$/.test(result.out)) return null;\n return Number(result.out);\n }\n\n private changedFiles(repoRoot: string, base: string, head: string): string[] {\n const result = this.capture(repoRoot, 'git', ['diff', '--name-only', base, head]);\n if (!result.ok || result.out === '') return [];\n return result.out.split('\\n').map((line: string): string => line.trim()).filter((line: string): boolean => line.length > 0);\n }\n\n // Every file this feature branch has touched since the fork point — committed AND still in the\n // working tree (staged / unstaged / untracked), so a conflict is visible WHILE editing.\n private featureChangedFiles(repoRoot: string, forkPoint: string): string[] {\n const out = new Set<string>();\n const add = (args: string[]): void => {\n const r = this.capture(repoRoot, 'git', args);\n if (!r.ok || r.out === '') return;\n for (const line of r.out.split('\\n')) {\n const f = line.trim();\n if (f.length > 0) out.add(f);\n }\n };\n add(['diff', '--name-only', forkPoint, 'HEAD']); // committed since the fork point\n add(['diff', '--name-only', 'HEAD']); // unstaged working-tree edits\n add(['diff', '--name-only', '--cached', 'HEAD']); // staged edits\n add(['ls-files', '--others', '--exclude-standard']); // untracked new files (respects .gitignore)\n return [...out];\n }\n\n /**\n * ONE repo-wide `gh pr list` giving every branch's merged/open PR — replaces the two per-branch\n * `--head <branch>` queries. `--limit 200` bounds it; a branch whose only PR is older than that\n * falls out of the index and reads as \"no PR\", which is the fail-OPEN direction (no merged PR =\n * no block), the same way a missing `gh` or an offline machine already degraded.\n */\n private loadPullRequestIndex(repoRoot: string): PullRequestIndex {\n const result = this.capture(repoRoot, 'gh', [\n 'pr', 'list', '--state', 'all', '--json', 'number,headRefName,state', '--limit', '200',\n ]);\n // `gh` could not be run, or exited non-zero (missing, unauthenticated, rate-limited, offline).\n // An EMPTY index is the same fail-open answer as before — but it is now labelled UNREACHABLE, so\n // the guards can report an abstention rather than an approval. Empty STDOUT on a zero exit is a\n // real answer (\"this repo has no PRs\"), so only the failure path clears the flag.\n if (!result.ok) return new PullRequestIndex({}, {}, false);\n if (result.out === '') return new PullRequestIndex({}, {}, true);\n return this.store.indexPullRequests(result.out);\n }\n\n /**\n * A `main` entry synthesised WITHOUT a worktree standing on main. Carries only the two hashes\n * read-stale-guard / stale-main-bash-guard actually compare (originMain vs the live tree's\n * ancestry) — never merged, never conflicting, so it can only ever produce the stale-main verdict.\n */\n private mainOnlyStatus(repoRoot: string): MainSyncStatus {\n const originMain = this.capture(repoRoot, 'git', ['rev-parse', 'origin/main']);\n const localMain = this.localMainHash(repoRoot);\n const status = new MainSyncStatus(\n 'main', false, '', true, localMain === '' ? null : localMain,\n originMain.ok ? originMain.out : '', localMain, false, [], new Date().toISOString(),\n );\n status.localMain = localMain;\n status.commitsBehind = this.commitDistance(repoRoot, localMain, originMain.ok ? originMain.out : '');\n return status;\n }\n\n // A benign status that never blocks — used when origin/main can't be resolved.\n private benignStatus(branch: string, featureHead: string): MainSyncStatus {\n return new MainSyncStatus(branch, false, '', true, null, '', featureHead, false, [], new Date().toISOString());\n }\n}\n\n// Temporary migration delegators to MainSyncStatusService — removed once consumers inject it.\nconst mainSyncSvc = new MainSyncStatusService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function mainSyncStatusPath(repoRoot: string): string { return mainSyncSvc.mainSyncStatusPath(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function mainSyncLockPath(repoRoot: string): string { return mainSyncSvc.mainSyncLockPath(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function readMainSyncStatus(repoRoot: string, branch: string): MainSyncStatus | null { return mainSyncSvc.readMainSyncStatus(repoRoot, branch); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function readMainSyncStatusFile(repoRoot: string): MainSyncStatusFile | null { return mainSyncSvc.readMainSyncStatusFile(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function writeMainSyncStatus(repoRoot: string, status: MainSyncStatus): void { mainSyncSvc.writeMainSyncStatus(repoRoot, status); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function writeMainSyncStatusFile(repoRoot: string, file: MainSyncStatusFile): void { mainSyncSvc.writeMainSyncStatusFile(repoRoot, file); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function readMainSyncLock(repoRoot: string): MainSyncLock | null { return mainSyncSvc.readMainSyncLock(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function writeMainSyncLock(repoRoot: string, lock: MainSyncLock): void { mainSyncSvc.writeMainSyncLock(repoRoot, lock); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function isLockStale(lock: MainSyncLock, hangTimeoutMinutes: number, now: number = Date.now()): boolean { return mainSyncSvc.isLockStale(lock, hangTimeoutMinutes, now); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function isRefreshInProgress(repoRoot: string, hangTimeoutMinutes: number, now: number = Date.now()): boolean { return mainSyncSvc.isRefreshInProgress(repoRoot, hangTimeoutMinutes, now); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function tryAcquireMainSyncLock(repoRoot: string, hangTimeoutMinutes: number, now: number = Date.now(), pid: number = process.pid): MainSyncLock | null { return mainSyncSvc.tryAcquireMainSyncLock(repoRoot, hangTimeoutMinutes, now, pid); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function inProcessLock(now: number = Date.now(), pid: number = process.pid): MainSyncLock { return mainSyncSvc.inProcessLock(now, pid); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function finishedLock(started: number): MainSyncLock { return mainSyncSvc.finishedLock(started); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function computeMainSyncStatus(repoRoot: string): MainSyncStatus { return mainSyncSvc.computeMainSyncStatus(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function computeAllMainSyncStatuses(repoRoot: string): MainSyncStatusFile { return mainSyncSvc.computeAllMainSyncStatuses(repoRoot); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function squashRecoverySteps(currentBranch: string): string[] { return mainSyncSvc.squashRecoverySteps(currentBranch); }\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to MainSyncStatusService; removed once consumers inject it\nexport function stampCleanMainSyncStatus(repoRoot: string): void { mainSyncSvc.stampCleanMainSyncStatus(repoRoot); }\n"]}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The VERDICT vocabulary — every data class and stable classification token the branch/worktree
3
+ * cleanup story is written in. Split out of merged-branches.ts, which holds the SERVICE that produces
4
+ * these values; this file holds only what they mean.
5
+ *
6
+ * Re-exported from merged-branches.ts, so every existing import path keeps working.
7
+ */
8
+ export declare class MergedBranch {
9
+ branch: string;
10
+ pr: number;
11
+ constructor(branch: string, pr: number);
12
+ }
13
+ /**
14
+ * WHY a branch is dead, or why it was spared — as a STABLE token, alongside the English prose.
15
+ *
16
+ * `sha`/`commits`/`prState` (below) let a human SEE a spared branch. This adds the other half: a token
17
+ * saying WHICH KIND of spared it is. Every spared branch used to report the identical string
18
+ * `no merged PR found — a human must decide`, and in one observed repo that one string covered three
19
+ * genuinely different situations — a PR CLOSED UNMERGED whose work landed under a later number, a
20
+ * branch that NEVER had a PR and holds the only copy of its commits, and content already in main.
21
+ * Reporting all three identically is why nobody ever decided, and why the pile grew to 6 branches
22
+ * against a cap of 5. A token lets wp-cleanup GROUP them and ask a question that can be answered.
23
+ */
24
+ export declare const CLASSIFICATION_MERGED_PR = "merged-pr";
25
+ export declare const CLASSIFICATION_BACKUP_OF_MERGED = "backup-of-merged";
26
+ /**
27
+ * A `<feature>PreMerge<N>` / `<feature>Squash` snapshot whose BASE branch is still alive — its PR is
28
+ * open, or it simply has not been proposed yet.
29
+ *
30
+ * AUTO-DELETABLE. A snapshot is a copy of its base BY CONSTRUCTION, so while the base exists this
31
+ * branch cannot be "the only copy in existence" — that is a proof, not a judgement call, and it is
32
+ * why this is reaped rather than asked about. It used to be the FIRST promptable group ("the easiest
33
+ * yes in the list"), which is precisely the tell: a question whose only honest answer is yes should
34
+ * never have been a question. `wp-start-upsert-pr` mints one of these on every run, so leaving them
35
+ * to a human meant the branch cap filled with copies and the cap's own remedy became "ask the human
36
+ * to adjudicate six branches" — the toil this whole story exists to remove.
37
+ */
38
+ export declare const CLASSIFICATION_BACKUP_OF_LIVE = "backup-of-live";
39
+ /**
40
+ * NOT a proof of death — a SPARED verdict, and the reason this whole liveness model exists.
41
+ *
42
+ * "Zero commits of its own" used to be auto-deletable, on the reasoning that a branch holding no
43
+ * commits can lose no work. That reasoning is true of a REF and false of a WORKING TREE: every
44
+ * worktree created with `git worktree add -b … origin/main` has zero commits from the moment it is
45
+ * created until its first commit — i.e. for exactly the window in which an agent is doing its work
46
+ * in it. Observed 2026-07-30: three worktrees with live agents in them were listed as dead, under
47
+ * the sentence "so no work can be lost".
48
+ *
49
+ * Liveness for anything holding COMMITS is the same single rule branches already use: reapable == a
50
+ * MERGED PR, and anything short of that is never deleted without an explicit answer.
51
+ *
52
+ * THIS verdict is the exception, and it is not the old auto-delete coming back. wp-cleanup REAPS a
53
+ * zero-commit ref by default — after checking the thing the 2026-07-30 incident actually turned on:
54
+ * whether somebody is HOLDING it. A worktree with uncommitted or untracked files
55
+ * (`WorktreeService.workInFlight`), one LOCKED by a live agent, the tree you are standing in
56
+ * and a detached HEAD are all spared, each with a stated reason. Everything else with zero commits is
57
+ * a husk, and deleting a husk loses a NAME, not a commit — which the archive tag then hands back.
58
+ *
59
+ * So the bar is "prove somebody is holding it", not "prove it is dead". The 2026-07-30 loss was
60
+ * three LIVE worktrees called dead with no such check; the check is now what stands between them.
61
+ */
62
+ export declare const CLASSIFICATION_NO_COMMITS = "no-commits";
63
+ export declare const CLASSIFICATION_SUPERSEDED = "superseded";
64
+ export declare const CLASSIFICATION_CONTENT_IN_MAIN = "content-already-in-main";
65
+ export declare const CLASSIFICATION_NEVER_PROPOSED = "never-proposed";
66
+ export declare const CLASSIFICATION_IN_USE = "in-use";
67
+ /**
68
+ * WORKTREE-only classifications. A worktree verdict borrows every token above (its branch is what is
69
+ * being judged), but three of its outcomes have no branch analogue at all, and lumping them under
70
+ * `in-use` would tell wp-cleanup to shut up about exactly the ones a human might want to act on.
71
+ *
72
+ * - PRUNABLE — the directory is already gone; `git worktree prune` is the reap, not `remove`.
73
+ * - LOCKED — a `git worktree lock` is standing and whatever took it still is: a running Claude
74
+ * agent, or a reason we cannot attribute to anybody. Never promptable. A lock whose
75
+ * Claude agent is provably DEAD is not this — it is judged on its branch like any
76
+ * unlocked tree, and cleared on the way out (see agent-worktree-lock.ts).
77
+ * - CURRENT — the worktree the command is running IN. Removing your own cwd is a self-destruct.
78
+ * - DETACHED — detached HEAD, so there is no branch to judge and no branch to archive.
79
+ */
80
+ export declare const CLASSIFICATION_PRUNABLE = "prunable-worktree";
81
+ export declare const CLASSIFICATION_LOCKED = "locked-worktree";
82
+ export declare const CLASSIFICATION_CURRENT = "current-worktree";
83
+ export declare const CLASSIFICATION_DETACHED = "detached-worktree";
84
+ export declare const ADJUDICATED_CLASSIFICATIONS: readonly string[];
85
+ /**
86
+ * A local branch and the verdict on it. `pr` is 0 when no merged PR backs the verdict (a `keep`).
87
+ *
88
+ * `sha`, `commits` and `prState` exist for the SPARED branches specifically. When nothing is
89
+ * auto-reapable the branch cap has nothing safe to offer, and its only remaining advice was "raise
90
+ * maxLocalBranches" / "set turnOffRuleUntilEpoch" — both of which loosen the rule. An agent with no
91
+ * human present took the config edit, which is the exact failure the cap exists to prevent. To ask a
92
+ * human "may I delete these?" instead, the guard has to be able to SHOW what it would delete: tip SHA
93
+ * (so the delete is recoverable), PR state (closed? never opened?) and how much unique work is on it.
94
+ *
95
+ * All three are computed in the DETACHED refresher and read straight off merged-branches.json — the
96
+ * blocking hook path never recomputes them. Defaulted so a cache written by an older release (and
97
+ * every existing call site) revives without them.
98
+ */
99
+ export declare class DeletableBranch {
100
+ branch: string;
101
+ reason: string;
102
+ pr: number;
103
+ /** Short tip SHA — what makes the delete undoable (`git branch <name> <sha>`). '' when unknown. */
104
+ sha: string;
105
+ /** Commits on this branch that are not on origin/main. -1 when it could not be established. */
106
+ commits: number;
107
+ /** GitHub's state for the PR whose head is this branch: MERGED / CLOSED / OPEN, or '' for none. */
108
+ prState: string;
109
+ /**
110
+ * One of the CLASSIFICATION_* tokens. Defaulted like the fields above so every pre-existing call
111
+ * site — and every cache written by an older release — still constructs; an unclassified revived
112
+ * entry reads as 'never-proposed', the most conservative of the spared verdicts.
113
+ */
114
+ classification: string;
115
+ constructor(branch: string, reason: string, pr: number, sha?: string, commits?: number, prState?: string, classification?: string);
116
+ }
117
+ /**
118
+ * A worktree and the verdict on it. Carries `path` (what `git worktree remove` takes) AND `branch`
119
+ * (what `git branch -D` takes afterwards) because reaping a worktree is always those two steps, in
120
+ * that order — git refuses to delete a branch that is still checked out somewhere.
121
+ *
122
+ * `classification` is the same STABLE token the branch verdicts carry, plus the four worktree-only
123
+ * ones above. It exists for the same reason it does on DeletableBranch: `deletable` answers "may the
124
+ * tooling reap this unattended?", and everything false used to collapse into one undifferentiated
125
+ * "spared" that a human could not rule on. With a token, WorktreeReaper knows whether the reap is a
126
+ * `prune` or a `remove`, and wp-cleanup knows which spared worktrees are worth ASKING about.
127
+ * Defaulted so every pre-existing call site and every cache written by an older release still builds;
128
+ * an unclassified revived entry reads as 'never-proposed', the most conservative spared verdict.
129
+ */
130
+ export declare class DeletableWorktree {
131
+ path: string;
132
+ branch: string;
133
+ reason: string;
134
+ pr: number;
135
+ deletable: boolean;
136
+ classification: string;
137
+ /**
138
+ * This worktree still carries a `git worktree lock` that the reap must CLEAR before it can remove
139
+ * the directory — set only for a lock whose Claude agent is provably gone (see
140
+ * agent-worktree-lock.ts). Never set for a human lock or a live agent's: those are spared outright
141
+ * and never reach the reaper at all.
142
+ *
143
+ * A field rather than a constructor parameter for the same reason ReapedWorktree.archiveTag is:
144
+ * it is an annotation on an already-computed verdict, not one of the things the verdict IS.
145
+ */
146
+ unlockBeforeRemove: boolean;
147
+ constructor(path: string, branch: string, reason: string, pr: number, deletable: boolean, classification?: string);
148
+ }
149
+ /**
150
+ * `deletable` is PRECOMPUTED so the consumer just deletes the list — no re-deriving, no judgement
151
+ * call at block time. `keep` carries the branches we refuse to touch (no merged PR found), each with
152
+ * its reason, so a human can see what was spared and why.
153
+ *
154
+ * `worktrees` is the parallel verdict list for the SECOND budget (see worktrees.ts): every linked
155
+ * worktree, each flagged deletable or not. Both budgets are reaped from this one cache file.
156
+ */
157
+ export declare class MergedBranchesCache {
158
+ timestamp: string;
159
+ deletable: DeletableBranch[];
160
+ keep: DeletableBranch[];
161
+ worktrees: DeletableWorktree[];
162
+ constructor(timestamp: string, deletable: DeletableBranch[], keep: DeletableBranch[], worktrees?: DeletableWorktree[]);
163
+ }
164
+ /**
165
+ * How old the cache may get before a reader must stop asserting figures from it.
166
+ *
167
+ * The cache is ALLOWED to be stale — that is the whole design (see the file header): the slow lookup
168
+ * runs detached, and a branch that merged 30 seconds ago simply survives to the next refresh. What is
169
+ * NOT allowed is quoting a stale file as present-tense fact. Observed 2026-07-30: the guard announced
170
+ * "8 parked local branches … none of them are dead" when there was ONE, having counted branches that
171
+ * had been deleted minutes earlier, and then blocked a legitimate command on that figure.
172
+ *
173
+ * Five minutes, because the refresher is kicked off by the hooks themselves and normally rewrites the
174
+ * file within one tool call; anything older means the refresher did not run or could not finish.
175
+ */
176
+ export declare const CACHE_STALE_AFTER_MS: number;
177
+ /**
178
+ * Whether a cache read off disk may be quoted as fact, and how old it is — so a message can SAY
179
+ * "the cached verdicts are stale" instead of asserting a count it cannot stand behind.
180
+ */
181
+ export declare class CacheFreshness {
182
+ /** True when the file is older than CACHE_STALE_AFTER_MS, or its timestamp is missing/unparseable. */
183
+ stale: boolean;
184
+ /** Whole minutes since the cache was written. -1 when that cannot be established. */
185
+ ageMinutes: number;
186
+ /** The raw ISO timestamp as recorded, or '' — printed verbatim so a human can check it. */
187
+ timestamp: string;
188
+ constructor(stale: boolean, ageMinutes: number, timestamp: string);
189
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ /**
3
+ * The VERDICT vocabulary — every data class and stable classification token the branch/worktree
4
+ * cleanup story is written in. Split out of merged-branches.ts, which holds the SERVICE that produces
5
+ * these values; this file holds only what they mean.
6
+ *
7
+ * Re-exported from merged-branches.ts, so every existing import path keeps working.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.CacheFreshness = exports.CACHE_STALE_AFTER_MS = exports.MergedBranchesCache = exports.DeletableWorktree = exports.DeletableBranch = exports.ADJUDICATED_CLASSIFICATIONS = exports.CLASSIFICATION_DETACHED = exports.CLASSIFICATION_CURRENT = exports.CLASSIFICATION_LOCKED = exports.CLASSIFICATION_PRUNABLE = exports.CLASSIFICATION_IN_USE = exports.CLASSIFICATION_NEVER_PROPOSED = exports.CLASSIFICATION_CONTENT_IN_MAIN = exports.CLASSIFICATION_SUPERSEDED = exports.CLASSIFICATION_NO_COMMITS = exports.CLASSIFICATION_BACKUP_OF_LIVE = exports.CLASSIFICATION_BACKUP_OF_MERGED = exports.CLASSIFICATION_MERGED_PR = exports.MergedBranch = void 0;
11
+ // Data-only (per CLAUDE.md, classes for data).
12
+ class MergedBranch {
13
+ branch;
14
+ pr;
15
+ constructor(branch, pr) {
16
+ this.branch = branch;
17
+ this.pr = pr;
18
+ }
19
+ }
20
+ exports.MergedBranch = MergedBranch;
21
+ /**
22
+ * WHY a branch is dead, or why it was spared — as a STABLE token, alongside the English prose.
23
+ *
24
+ * `sha`/`commits`/`prState` (below) let a human SEE a spared branch. This adds the other half: a token
25
+ * saying WHICH KIND of spared it is. Every spared branch used to report the identical string
26
+ * `no merged PR found — a human must decide`, and in one observed repo that one string covered three
27
+ * genuinely different situations — a PR CLOSED UNMERGED whose work landed under a later number, a
28
+ * branch that NEVER had a PR and holds the only copy of its commits, and content already in main.
29
+ * Reporting all three identically is why nobody ever decided, and why the pile grew to 6 branches
30
+ * against a cap of 5. A token lets wp-cleanup GROUP them and ask a question that can be answered.
31
+ */
32
+ // Dead by proof — these are auto-deletable. Two proofs qualify: a MERGED PR, or a LIVE SIBLING REF
33
+ // that provably holds the same work (see CLASSIFICATION_BACKUP_OF_LIVE).
34
+ exports.CLASSIFICATION_MERGED_PR = 'merged-pr';
35
+ exports.CLASSIFICATION_BACKUP_OF_MERGED = 'backup-of-merged';
36
+ /**
37
+ * A `<feature>PreMerge<N>` / `<feature>Squash` snapshot whose BASE branch is still alive — its PR is
38
+ * open, or it simply has not been proposed yet.
39
+ *
40
+ * AUTO-DELETABLE. A snapshot is a copy of its base BY CONSTRUCTION, so while the base exists this
41
+ * branch cannot be "the only copy in existence" — that is a proof, not a judgement call, and it is
42
+ * why this is reaped rather than asked about. It used to be the FIRST promptable group ("the easiest
43
+ * yes in the list"), which is precisely the tell: a question whose only honest answer is yes should
44
+ * never have been a question. `wp-start-upsert-pr` mints one of these on every run, so leaving them
45
+ * to a human meant the branch cap filled with copies and the cap's own remedy became "ask the human
46
+ * to adjudicate six branches" — the toil this whole story exists to remove.
47
+ */
48
+ exports.CLASSIFICATION_BACKUP_OF_LIVE = 'backup-of-live';
49
+ /**
50
+ * NOT a proof of death — a SPARED verdict, and the reason this whole liveness model exists.
51
+ *
52
+ * "Zero commits of its own" used to be auto-deletable, on the reasoning that a branch holding no
53
+ * commits can lose no work. That reasoning is true of a REF and false of a WORKING TREE: every
54
+ * worktree created with `git worktree add -b … origin/main` has zero commits from the moment it is
55
+ * created until its first commit — i.e. for exactly the window in which an agent is doing its work
56
+ * in it. Observed 2026-07-30: three worktrees with live agents in them were listed as dead, under
57
+ * the sentence "so no work can be lost".
58
+ *
59
+ * Liveness for anything holding COMMITS is the same single rule branches already use: reapable == a
60
+ * MERGED PR, and anything short of that is never deleted without an explicit answer.
61
+ *
62
+ * THIS verdict is the exception, and it is not the old auto-delete coming back. wp-cleanup REAPS a
63
+ * zero-commit ref by default — after checking the thing the 2026-07-30 incident actually turned on:
64
+ * whether somebody is HOLDING it. A worktree with uncommitted or untracked files
65
+ * (`WorktreeService.workInFlight`), one LOCKED by a live agent, the tree you are standing in
66
+ * and a detached HEAD are all spared, each with a stated reason. Everything else with zero commits is
67
+ * a husk, and deleting a husk loses a NAME, not a commit — which the archive tag then hands back.
68
+ *
69
+ * So the bar is "prove somebody is holding it", not "prove it is dead". The 2026-07-30 loss was
70
+ * three LIVE worktrees called dead with no such check; the check is now what stands between them.
71
+ */
72
+ exports.CLASSIFICATION_NO_COMMITS = 'no-commits';
73
+ // Spared, but a human should be ASKED — in descending order of "obviously fine to delete".
74
+ exports.CLASSIFICATION_SUPERSEDED = 'superseded';
75
+ exports.CLASSIFICATION_CONTENT_IN_MAIN = 'content-already-in-main';
76
+ exports.CLASSIFICATION_NEVER_PROPOSED = 'never-proposed';
77
+ // Spared for a mechanical reason, not a judgement call (checked out somewhere).
78
+ exports.CLASSIFICATION_IN_USE = 'in-use';
79
+ /**
80
+ * WORKTREE-only classifications. A worktree verdict borrows every token above (its branch is what is
81
+ * being judged), but three of its outcomes have no branch analogue at all, and lumping them under
82
+ * `in-use` would tell wp-cleanup to shut up about exactly the ones a human might want to act on.
83
+ *
84
+ * - PRUNABLE — the directory is already gone; `git worktree prune` is the reap, not `remove`.
85
+ * - LOCKED — a `git worktree lock` is standing and whatever took it still is: a running Claude
86
+ * agent, or a reason we cannot attribute to anybody. Never promptable. A lock whose
87
+ * Claude agent is provably DEAD is not this — it is judged on its branch like any
88
+ * unlocked tree, and cleared on the way out (see agent-worktree-lock.ts).
89
+ * - CURRENT — the worktree the command is running IN. Removing your own cwd is a self-destruct.
90
+ * - DETACHED — detached HEAD, so there is no branch to judge and no branch to archive.
91
+ */
92
+ exports.CLASSIFICATION_PRUNABLE = 'prunable-worktree';
93
+ exports.CLASSIFICATION_LOCKED = 'locked-worktree';
94
+ exports.CLASSIFICATION_CURRENT = 'current-worktree';
95
+ exports.CLASSIFICATION_DETACHED = 'detached-worktree';
96
+ // The spared classifications wp-cleanup ADJUDICATES, most-safe first — everything else it either
97
+ // reaps on proof or spares for a mechanical reason (in-use, locked, current, detached).
98
+ //
99
+ // Three of the four are prompted / flagged, in exactly this order, so the easy yeses come before the
100
+ // ones that need thought. CLASSIFICATION_NO_COMMITS is the fourth and is REAPED rather than asked
101
+ // about (see its docstring): it stays in this list because this is the list wp-cleanup collects a
102
+ // candidate set from, and pulling it out would mean a husk never reaching the code that reaps it.
103
+ exports.ADJUDICATED_CLASSIFICATIONS = [
104
+ // CLASSIFICATION_BACKUP_OF_LIVE is deliberately NOT here — it is auto-reaped now. Anything that
105
+ // reaches this list is a real judgement call; if a group's answer is always yes, it belongs in
106
+ // the deletable set instead of on a human's plate.
107
+ exports.CLASSIFICATION_SUPERSEDED,
108
+ exports.CLASSIFICATION_CONTENT_IN_MAIN,
109
+ exports.CLASSIFICATION_NEVER_PROPOSED,
110
+ // LAST, and no longer a question at all. Ordering it last was the old, weak answer to "a worktree
111
+ // in active use looks exactly like this" — a prompt cannot tell a human which one they are
112
+ // looking at, and ordering does not help them either. wp-cleanup now ANSWERS that question
113
+ // instead of asking it (uncommitted files, live lock, current tree) and reaps whatever is left.
114
+ exports.CLASSIFICATION_NO_COMMITS,
115
+ ];
116
+ /**
117
+ * A local branch and the verdict on it. `pr` is 0 when no merged PR backs the verdict (a `keep`).
118
+ *
119
+ * `sha`, `commits` and `prState` exist for the SPARED branches specifically. When nothing is
120
+ * auto-reapable the branch cap has nothing safe to offer, and its only remaining advice was "raise
121
+ * maxLocalBranches" / "set turnOffRuleUntilEpoch" — both of which loosen the rule. An agent with no
122
+ * human present took the config edit, which is the exact failure the cap exists to prevent. To ask a
123
+ * human "may I delete these?" instead, the guard has to be able to SHOW what it would delete: tip SHA
124
+ * (so the delete is recoverable), PR state (closed? never opened?) and how much unique work is on it.
125
+ *
126
+ * All three are computed in the DETACHED refresher and read straight off merged-branches.json — the
127
+ * blocking hook path never recomputes them. Defaulted so a cache written by an older release (and
128
+ * every existing call site) revives without them.
129
+ */
130
+ class DeletableBranch {
131
+ branch;
132
+ reason;
133
+ pr;
134
+ /** Short tip SHA — what makes the delete undoable (`git branch <name> <sha>`). '' when unknown. */
135
+ sha;
136
+ /** Commits on this branch that are not on origin/main. -1 when it could not be established. */
137
+ commits;
138
+ /** GitHub's state for the PR whose head is this branch: MERGED / CLOSED / OPEN, or '' for none. */
139
+ prState;
140
+ /**
141
+ * One of the CLASSIFICATION_* tokens. Defaulted like the fields above so every pre-existing call
142
+ * site — and every cache written by an older release — still constructs; an unclassified revived
143
+ * entry reads as 'never-proposed', the most conservative of the spared verdicts.
144
+ */
145
+ classification;
146
+ // eslint-disable-next-line @typescript-eslint/max-params
147
+ constructor(branch, reason, pr, sha = '', commits = -1, prState = '', classification = exports.CLASSIFICATION_NEVER_PROPOSED) {
148
+ this.branch = branch;
149
+ this.reason = reason;
150
+ this.pr = pr;
151
+ this.sha = sha;
152
+ this.commits = commits;
153
+ this.prState = prState;
154
+ this.classification = classification;
155
+ }
156
+ }
157
+ exports.DeletableBranch = DeletableBranch;
158
+ /**
159
+ * A worktree and the verdict on it. Carries `path` (what `git worktree remove` takes) AND `branch`
160
+ * (what `git branch -D` takes afterwards) because reaping a worktree is always those two steps, in
161
+ * that order — git refuses to delete a branch that is still checked out somewhere.
162
+ *
163
+ * `classification` is the same STABLE token the branch verdicts carry, plus the four worktree-only
164
+ * ones above. It exists for the same reason it does on DeletableBranch: `deletable` answers "may the
165
+ * tooling reap this unattended?", and everything false used to collapse into one undifferentiated
166
+ * "spared" that a human could not rule on. With a token, WorktreeReaper knows whether the reap is a
167
+ * `prune` or a `remove`, and wp-cleanup knows which spared worktrees are worth ASKING about.
168
+ * Defaulted so every pre-existing call site and every cache written by an older release still builds;
169
+ * an unclassified revived entry reads as 'never-proposed', the most conservative spared verdict.
170
+ */
171
+ class DeletableWorktree {
172
+ path;
173
+ branch;
174
+ reason;
175
+ pr;
176
+ deletable;
177
+ classification;
178
+ /**
179
+ * This worktree still carries a `git worktree lock` that the reap must CLEAR before it can remove
180
+ * the directory — set only for a lock whose Claude agent is provably gone (see
181
+ * agent-worktree-lock.ts). Never set for a human lock or a live agent's: those are spared outright
182
+ * and never reach the reaper at all.
183
+ *
184
+ * A field rather than a constructor parameter for the same reason ReapedWorktree.archiveTag is:
185
+ * it is an annotation on an already-computed verdict, not one of the things the verdict IS.
186
+ */
187
+ unlockBeforeRemove = false;
188
+ // eslint-disable-next-line @typescript-eslint/max-params
189
+ constructor(path, branch, reason, pr, deletable, classification = exports.CLASSIFICATION_NEVER_PROPOSED) {
190
+ this.path = path;
191
+ this.branch = branch;
192
+ this.reason = reason;
193
+ this.pr = pr;
194
+ this.deletable = deletable;
195
+ this.classification = classification;
196
+ }
197
+ }
198
+ exports.DeletableWorktree = DeletableWorktree;
199
+ /**
200
+ * `deletable` is PRECOMPUTED so the consumer just deletes the list — no re-deriving, no judgement
201
+ * call at block time. `keep` carries the branches we refuse to touch (no merged PR found), each with
202
+ * its reason, so a human can see what was spared and why.
203
+ *
204
+ * `worktrees` is the parallel verdict list for the SECOND budget (see worktrees.ts): every linked
205
+ * worktree, each flagged deletable or not. Both budgets are reaped from this one cache file.
206
+ */
207
+ class MergedBranchesCache {
208
+ timestamp;
209
+ deletable;
210
+ keep;
211
+ worktrees;
212
+ constructor(timestamp, deletable, keep, worktrees = []) {
213
+ this.timestamp = timestamp;
214
+ this.deletable = deletable;
215
+ this.keep = keep;
216
+ this.worktrees = worktrees;
217
+ }
218
+ }
219
+ exports.MergedBranchesCache = MergedBranchesCache;
220
+ /**
221
+ * How old the cache may get before a reader must stop asserting figures from it.
222
+ *
223
+ * The cache is ALLOWED to be stale — that is the whole design (see the file header): the slow lookup
224
+ * runs detached, and a branch that merged 30 seconds ago simply survives to the next refresh. What is
225
+ * NOT allowed is quoting a stale file as present-tense fact. Observed 2026-07-30: the guard announced
226
+ * "8 parked local branches … none of them are dead" when there was ONE, having counted branches that
227
+ * had been deleted minutes earlier, and then blocked a legitimate command on that figure.
228
+ *
229
+ * Five minutes, because the refresher is kicked off by the hooks themselves and normally rewrites the
230
+ * file within one tool call; anything older means the refresher did not run or could not finish.
231
+ */
232
+ exports.CACHE_STALE_AFTER_MS = 5 * 60 * 1000;
233
+ /**
234
+ * Whether a cache read off disk may be quoted as fact, and how old it is — so a message can SAY
235
+ * "the cached verdicts are stale" instead of asserting a count it cannot stand behind.
236
+ */
237
+ class CacheFreshness {
238
+ /** True when the file is older than CACHE_STALE_AFTER_MS, or its timestamp is missing/unparseable. */
239
+ stale;
240
+ /** Whole minutes since the cache was written. -1 when that cannot be established. */
241
+ ageMinutes;
242
+ /** The raw ISO timestamp as recorded, or '' — printed verbatim so a human can check it. */
243
+ timestamp;
244
+ constructor(stale, ageMinutes, timestamp) {
245
+ this.stale = stale;
246
+ this.ageMinutes = ageMinutes;
247
+ this.timestamp = timestamp;
248
+ }
249
+ }
250
+ exports.CacheFreshness = CacheFreshness;
251
+ //# sourceMappingURL=merged-branch-verdicts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merged-branch-verdicts.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/merged-branch-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The VERDICT vocabulary — every data class and stable classification token the branch/worktree\n * cleanup story is written in. Split out of merged-branches.ts, which holds the SERVICE that produces\n * these values; this file holds only what they mean.\n *\n * Re-exported from merged-branches.ts, so every existing import path keeps working.\n */\n\n// Data-only (per CLAUDE.md, classes for data).\nexport class MergedBranch {\n branch: string;\n pr: number;\n\n constructor(branch: string, pr: number) {\n this.branch = branch;\n this.pr = pr;\n }\n}\n\n/**\n * WHY a branch is dead, or why it was spared — as a STABLE token, alongside the English prose.\n *\n * `sha`/`commits`/`prState` (below) let a human SEE a spared branch. This adds the other half: a token\n * saying WHICH KIND of spared it is. Every spared branch used to report the identical string\n * `no merged PR found — a human must decide`, and in one observed repo that one string covered three\n * genuinely different situations — a PR CLOSED UNMERGED whose work landed under a later number, a\n * branch that NEVER had a PR and holds the only copy of its commits, and content already in main.\n * Reporting all three identically is why nobody ever decided, and why the pile grew to 6 branches\n * against a cap of 5. A token lets wp-cleanup GROUP them and ask a question that can be answered.\n */\n// Dead by proof — these are auto-deletable. Two proofs qualify: a MERGED PR, or a LIVE SIBLING REF\n// that provably holds the same work (see CLASSIFICATION_BACKUP_OF_LIVE).\nexport const CLASSIFICATION_MERGED_PR = 'merged-pr';\nexport const CLASSIFICATION_BACKUP_OF_MERGED = 'backup-of-merged';\n/**\n * A `<feature>PreMerge<N>` / `<feature>Squash` snapshot whose BASE branch is still alive — its PR is\n * open, or it simply has not been proposed yet.\n *\n * AUTO-DELETABLE. A snapshot is a copy of its base BY CONSTRUCTION, so while the base exists this\n * branch cannot be \"the only copy in existence\" — that is a proof, not a judgement call, and it is\n * why this is reaped rather than asked about. It used to be the FIRST promptable group (\"the easiest\n * yes in the list\"), which is precisely the tell: a question whose only honest answer is yes should\n * never have been a question. `wp-start-upsert-pr` mints one of these on every run, so leaving them\n * to a human meant the branch cap filled with copies and the cap's own remedy became \"ask the human\n * to adjudicate six branches\" — the toil this whole story exists to remove.\n */\nexport const CLASSIFICATION_BACKUP_OF_LIVE = 'backup-of-live';\n/**\n * NOT a proof of death — a SPARED verdict, and the reason this whole liveness model exists.\n *\n * \"Zero commits of its own\" used to be auto-deletable, on the reasoning that a branch holding no\n * commits can lose no work. That reasoning is true of a REF and false of a WORKING TREE: every\n * worktree created with `git worktree add -b … origin/main` has zero commits from the moment it is\n * created until its first commit — i.e. for exactly the window in which an agent is doing its work\n * in it. Observed 2026-07-30: three worktrees with live agents in them were listed as dead, under\n * the sentence \"so no work can be lost\".\n *\n * Liveness for anything holding COMMITS is the same single rule branches already use: reapable == a\n * MERGED PR, and anything short of that is never deleted without an explicit answer.\n *\n * THIS verdict is the exception, and it is not the old auto-delete coming back. wp-cleanup REAPS a\n * zero-commit ref by default — after checking the thing the 2026-07-30 incident actually turned on:\n * whether somebody is HOLDING it. A worktree with uncommitted or untracked files\n * (`WorktreeService.workInFlight`), one LOCKED by a live agent, the tree you are standing in\n * and a detached HEAD are all spared, each with a stated reason. Everything else with zero commits is\n * a husk, and deleting a husk loses a NAME, not a commit — which the archive tag then hands back.\n *\n * So the bar is \"prove somebody is holding it\", not \"prove it is dead\". The 2026-07-30 loss was\n * three LIVE worktrees called dead with no such check; the check is now what stands between them.\n */\nexport const CLASSIFICATION_NO_COMMITS = 'no-commits';\n// Spared, but a human should be ASKED — in descending order of \"obviously fine to delete\".\nexport const CLASSIFICATION_SUPERSEDED = 'superseded';\nexport const CLASSIFICATION_CONTENT_IN_MAIN = 'content-already-in-main';\nexport const CLASSIFICATION_NEVER_PROPOSED = 'never-proposed';\n// Spared for a mechanical reason, not a judgement call (checked out somewhere).\nexport const CLASSIFICATION_IN_USE = 'in-use';\n\n/**\n * WORKTREE-only classifications. A worktree verdict borrows every token above (its branch is what is\n * being judged), but three of its outcomes have no branch analogue at all, and lumping them under\n * `in-use` would tell wp-cleanup to shut up about exactly the ones a human might want to act on.\n *\n * - PRUNABLE — the directory is already gone; `git worktree prune` is the reap, not `remove`.\n * - LOCKED — a `git worktree lock` is standing and whatever took it still is: a running Claude\n * agent, or a reason we cannot attribute to anybody. Never promptable. A lock whose\n * Claude agent is provably DEAD is not this — it is judged on its branch like any\n * unlocked tree, and cleared on the way out (see agent-worktree-lock.ts).\n * - CURRENT — the worktree the command is running IN. Removing your own cwd is a self-destruct.\n * - DETACHED — detached HEAD, so there is no branch to judge and no branch to archive.\n */\nexport const CLASSIFICATION_PRUNABLE = 'prunable-worktree';\nexport const CLASSIFICATION_LOCKED = 'locked-worktree';\nexport const CLASSIFICATION_CURRENT = 'current-worktree';\nexport const CLASSIFICATION_DETACHED = 'detached-worktree';\n\n// The spared classifications wp-cleanup ADJUDICATES, most-safe first — everything else it either\n// reaps on proof or spares for a mechanical reason (in-use, locked, current, detached).\n//\n// Three of the four are prompted / flagged, in exactly this order, so the easy yeses come before the\n// ones that need thought. CLASSIFICATION_NO_COMMITS is the fourth and is REAPED rather than asked\n// about (see its docstring): it stays in this list because this is the list wp-cleanup collects a\n// candidate set from, and pulling it out would mean a husk never reaching the code that reaps it.\nexport const ADJUDICATED_CLASSIFICATIONS: readonly string[] = [\n // CLASSIFICATION_BACKUP_OF_LIVE is deliberately NOT here — it is auto-reaped now. Anything that\n // reaches this list is a real judgement call; if a group's answer is always yes, it belongs in\n // the deletable set instead of on a human's plate.\n CLASSIFICATION_SUPERSEDED,\n CLASSIFICATION_CONTENT_IN_MAIN,\n CLASSIFICATION_NEVER_PROPOSED,\n // LAST, and no longer a question at all. Ordering it last was the old, weak answer to \"a worktree\n // in active use looks exactly like this\" — a prompt cannot tell a human which one they are\n // looking at, and ordering does not help them either. wp-cleanup now ANSWERS that question\n // instead of asking it (uncommitted files, live lock, current tree) and reaps whatever is left.\n CLASSIFICATION_NO_COMMITS,\n];\n\n/**\n * A local branch and the verdict on it. `pr` is 0 when no merged PR backs the verdict (a `keep`).\n *\n * `sha`, `commits` and `prState` exist for the SPARED branches specifically. When nothing is\n * auto-reapable the branch cap has nothing safe to offer, and its only remaining advice was \"raise\n * maxLocalBranches\" / \"set turnOffRuleUntilEpoch\" — both of which loosen the rule. An agent with no\n * human present took the config edit, which is the exact failure the cap exists to prevent. To ask a\n * human \"may I delete these?\" instead, the guard has to be able to SHOW what it would delete: tip SHA\n * (so the delete is recoverable), PR state (closed? never opened?) and how much unique work is on it.\n *\n * All three are computed in the DETACHED refresher and read straight off merged-branches.json — the\n * blocking hook path never recomputes them. Defaulted so a cache written by an older release (and\n * every existing call site) revives without them.\n */\nexport class DeletableBranch {\n branch: string;\n reason: string;\n pr: number;\n /** Short tip SHA — what makes the delete undoable (`git branch <name> <sha>`). '' when unknown. */\n sha: string;\n /** Commits on this branch that are not on origin/main. -1 when it could not be established. */\n commits: number;\n /** GitHub's state for the PR whose head is this branch: MERGED / CLOSED / OPEN, or '' for none. */\n prState: string;\n /**\n * One of the CLASSIFICATION_* tokens. Defaulted like the fields above so every pre-existing call\n * site — and every cache written by an older release — still constructs; an unclassified revived\n * entry reads as 'never-proposed', the most conservative of the spared verdicts.\n */\n classification: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n branch: string,\n reason: string,\n pr: number,\n sha: string = '',\n commits: number = -1,\n prState: string = '',\n classification: string = CLASSIFICATION_NEVER_PROPOSED,\n ) {\n this.branch = branch;\n this.reason = reason;\n this.pr = pr;\n this.sha = sha;\n this.commits = commits;\n this.prState = prState;\n this.classification = classification;\n }\n}\n\n/**\n * A worktree and the verdict on it. Carries `path` (what `git worktree remove` takes) AND `branch`\n * (what `git branch -D` takes afterwards) because reaping a worktree is always those two steps, in\n * that order — git refuses to delete a branch that is still checked out somewhere.\n *\n * `classification` is the same STABLE token the branch verdicts carry, plus the four worktree-only\n * ones above. It exists for the same reason it does on DeletableBranch: `deletable` answers \"may the\n * tooling reap this unattended?\", and everything false used to collapse into one undifferentiated\n * \"spared\" that a human could not rule on. With a token, WorktreeReaper knows whether the reap is a\n * `prune` or a `remove`, and wp-cleanup knows which spared worktrees are worth ASKING about.\n * Defaulted so every pre-existing call site and every cache written by an older release still builds;\n * an unclassified revived entry reads as 'never-proposed', the most conservative spared verdict.\n */\nexport class DeletableWorktree {\n path: string;\n branch: string;\n reason: string;\n pr: number;\n deletable: boolean;\n classification: string;\n /**\n * This worktree still carries a `git worktree lock` that the reap must CLEAR before it can remove\n * the directory — set only for a lock whose Claude agent is provably gone (see\n * agent-worktree-lock.ts). Never set for a human lock or a live agent's: those are spared outright\n * and never reach the reaper at all.\n *\n * A field rather than a constructor parameter for the same reason ReapedWorktree.archiveTag is:\n * it is an annotation on an already-computed verdict, not one of the things the verdict IS.\n */\n unlockBeforeRemove: boolean = false;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n path: string,\n branch: string,\n reason: string,\n pr: number,\n deletable: boolean,\n classification: string = CLASSIFICATION_NEVER_PROPOSED,\n ) {\n this.path = path;\n this.branch = branch;\n this.reason = reason;\n this.pr = pr;\n this.deletable = deletable;\n this.classification = classification;\n }\n}\n\n/**\n * `deletable` is PRECOMPUTED so the consumer just deletes the list — no re-deriving, no judgement\n * call at block time. `keep` carries the branches we refuse to touch (no merged PR found), each with\n * its reason, so a human can see what was spared and why.\n *\n * `worktrees` is the parallel verdict list for the SECOND budget (see worktrees.ts): every linked\n * worktree, each flagged deletable or not. Both budgets are reaped from this one cache file.\n */\nexport class MergedBranchesCache {\n timestamp: string;\n deletable: DeletableBranch[];\n keep: DeletableBranch[];\n worktrees: DeletableWorktree[];\n\n constructor(\n timestamp: string,\n deletable: DeletableBranch[],\n keep: DeletableBranch[],\n worktrees: DeletableWorktree[] = [],\n ) {\n this.timestamp = timestamp;\n this.deletable = deletable;\n this.keep = keep;\n this.worktrees = worktrees;\n }\n}\n\n/**\n * How old the cache may get before a reader must stop asserting figures from it.\n *\n * The cache is ALLOWED to be stale — that is the whole design (see the file header): the slow lookup\n * runs detached, and a branch that merged 30 seconds ago simply survives to the next refresh. What is\n * NOT allowed is quoting a stale file as present-tense fact. Observed 2026-07-30: the guard announced\n * \"8 parked local branches … none of them are dead\" when there was ONE, having counted branches that\n * had been deleted minutes earlier, and then blocked a legitimate command on that figure.\n *\n * Five minutes, because the refresher is kicked off by the hooks themselves and normally rewrites the\n * file within one tool call; anything older means the refresher did not run or could not finish.\n */\nexport const CACHE_STALE_AFTER_MS = 5 * 60 * 1000;\n\n/**\n * Whether a cache read off disk may be quoted as fact, and how old it is — so a message can SAY\n * \"the cached verdicts are stale\" instead of asserting a count it cannot stand behind.\n */\nexport class CacheFreshness {\n /** True when the file is older than CACHE_STALE_AFTER_MS, or its timestamp is missing/unparseable. */\n stale: boolean;\n /** Whole minutes since the cache was written. -1 when that cannot be established. */\n ageMinutes: number;\n /** The raw ISO timestamp as recorded, or '' — printed verbatim so a human can check it. */\n timestamp: string;\n\n constructor(stale: boolean, ageMinutes: number, timestamp: string) {\n this.stale = stale;\n this.ageMinutes = ageMinutes;\n this.timestamp = timestamp;\n }\n}\n"]}