@webpieces/pr-gate 0.4.571 → 0.4.573

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.571",
3
+ "version": "0.4.573",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -27,7 +27,7 @@
27
27
  "directory": "packages/tooling/pr-gate"
28
28
  },
29
29
  "dependencies": {
30
- "@webpieces/rules-config": "0.4.571",
30
+ "@webpieces/rules-config": "0.4.573",
31
31
  "@inversifyjs/binding-decorators": "1.1.5",
32
32
  "inversify": "7.10.4",
33
33
  "reflect-metadata": "0.2.2"
@@ -122,12 +122,24 @@ let CleanupCommand = class CleanupCommand {
122
122
  process.stdout.write(this.worktreeSection.report(this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));
123
123
  }
124
124
  report(result) {
125
- if (result.reaped.length === 0 && result.failed.length === 0) {
125
+ const gone = result.alreadyGone.length;
126
+ if (result.reaped.length === 0 && result.failed.length === 0 && gone === 0) {
126
127
  return '\n✅ Nothing to clean up — no local branch is provably dead.\n';
127
128
  }
128
129
  let out = '\n' + SEP + `🧹 Cleaned up ${String(result.reaped.length)} dead local branch(es)\n` + SEP + '\n';
129
130
  for (const entry of result.reaped)
130
131
  out += this.reapedLine(entry);
132
+ // Stated, not warned about. These branches ARE gone — the detached refresher's auto-reap
133
+ // deleted and archived them moments before this pass reached them, which it is entitled to do
134
+ // (it acts on its own fresh verdicts, and the hooks start it on the same commands you run
135
+ // wp-cleanup from). Before this line they came out under "⚠️ N branch(es) could not be
136
+ // deleted", which reads as work left undone and sends a human hunting for branches that no
137
+ // longer exist.
138
+ if (gone > 0) {
139
+ const names = result.alreadyGone.map((entry) => entry.branch).join(', ');
140
+ out += `\nℹ️ ${String(gone)} branch(es) were already gone — a concurrent auto-reap deleted and\n`
141
+ + ` archived them first, so there was nothing left to do: ${names}\n`;
142
+ }
131
143
  if (result.failed.length > 0) {
132
144
  out += `\n⚠️ ${String(result.failed.length)} branch(es) could not be deleted:\n`;
133
145
  for (const entry of result.failed)
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as readline from 'readline';\nimport {\n BranchArchiver,\n BranchReaper,\n DeletableBranch,\n DeletableWorktree,\n ReapResult,\n ReapedBranch,\n RepoRootFinder,\n loadAndValidate,\n CLASSIFICATION_SUPERSEDED,\n CLASSIFICATION_CONTENT_IN_MAIN,\n CLASSIFICATION_NEVER_PROPOSED,\n CLASSIFICATION_NO_COMMITS,\n PROMPTABLE_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// One line of human-facing explanation per promptable classification, ordered most-safe first. These\n// replace the single string `no merged PR found — a human must decide`, which covered all three of\n// these situations identically and so told a human nothing they could act on.\nconst CLASSIFICATION_HEADINGS: Readonly<Record<string, string>> = {\n [CLASSIFICATION_SUPERSEDED]:\n 'SUPERSEDED — the PR was closed WITHOUT merging and later PRs have merged since. Near-certainly\\n'\n + ' the abandoned first attempt at work that landed under a different number. Safest group to delete.',\n [CLASSIFICATION_CONTENT_IN_MAIN]:\n 'CONTENT ALREADY IN MAIN — every commit has a patch-equivalent in origin/main (git cherry).\\n'\n + ' The work is not unique to this branch; only the commit objects are.',\n [CLASSIFICATION_NEVER_PROPOSED]:\n 'NEVER PROPOSED — no PR was ever opened, and these commits may be the ONLY copy in existence.\\n'\n + ' Read the unique-commit counts before answering. This is the group to say no to if unsure.',\n [CLASSIFICATION_NO_COMMITS]:\n 'NO COMMITS YET — identical to origin/main, so deleting the REF loses nothing. But this is also\\n'\n + ' exactly what a worktree looks like while somebody is still working in it and has not committed,\\n'\n + ' so it is asked about rather than reaped. Say no to any of these you (or an agent) are using.',\n};\n\n/**\n * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, then ASK\n * about the ones that are merely probably-dead.\n *\n * A merged PR is the ONLY proof that reaps anything unattended. \"Holds no commits\" used to be a second\n * proof and no longer is: it is equally the signature of a worktree an agent is working in right now,\n * so it moved into the group this command ASKS about.\n *\n * WHY WORKTREES ARE PART OF THIS: they were the half that never got reaped. The verdicts existed —\n * merged-branches.ts has been writing a full `DeletableWorktree[]` into the cache all along — and their\n * only consumer used them to BLOCK the next `git worktree add`, never to remove anything. Meanwhile a\n * live worktree pins its branch, so the branch was spared too. Two things the tooling could PROVE were\n * dead, accumulating forever, until the guard refused to create the next branch and the only remedy it\n * could offer was loosening its own cap. See WorktreeCleanupSection and WorktreeReaper.\n *\n * WHY a named command instead of the `git branch -D a b c` the guards used to print: an AI agent\n * reads a raw `-D` as destructive, so it asks permission and stops — which is exactly why branches\n * piled up despite the tooling knowing precisely which ones were dead. `pnpm wp-cleanup` is one\n * boring, allowlistable verb whose safety is a property of the command itself rather than of the\n * agent's judgement about a git flag.\n *\n * WHY IT NOW PROMPTS: sparing silently was the other half of the same problem. Every spared branch\n * reported the identical `no merged PR found — a human must decide`, so the human could not decide,\n * so nothing got deleted, so the pile grew until branch-creation-guard refused to make the next branch\n * and an agent went looking for a config knob to loosen. Shown a real classification with unique-commit\n * counts, the human in that session answered in five words: \"these should all be delete branches\".\n * The prompt is cheap because archiving happens FIRST — a yes costs a tag, not the history.\n *\n * All the danger still lives in the verdicts, not here — see BranchReaper for why every AUTOMATICALLY\n * deleted branch is provably dead and recoverable, and note that nothing in the prompted group is ever\n * deleted without an explicit typed answer.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanupCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly branchReaper: BranchReaper,\n private readonly archiver: BranchArchiver,\n private readonly worktreeSection: WorktreeCleanupSection,\n ) {}\n\n /**\n * WORKTREES FIRST, then branches. The order is the fix, not a detail: a worktree HOLDS its branch,\n * so that branch is spared `in-use` (\"remove that worktree before deleting the branch\") and nothing\n * used to remove the worktree — so both piled up until branch-creation-guard refused to make the\n * next one. Reaping the worktree takes its branch with it, and the branch pass then recomputes its\n * verdicts from scratch against the post-removal truth.\n */\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n await this.cleanUpWorktrees(repoRoot, retention);\n await this.cleanUpBranches(repoRoot, retention);\n }\n\n private async cleanUpBranches(repoRoot: string, retention: string): Promise<void> {\n // No cache argument: wp-cleanup recomputes the verdicts itself. The file on disk is allowed to\n // go stale, and stale evidence is fine for BLOCKING but never for DELETING.\n const result = this.branchReaper.reap(repoRoot, 'wp-cleanup', null, retention);\n process.stdout.write(this.report(result));\n\n const promptable = this.promptable(result.spared);\n if (promptable.length === 0) return;\n process.stdout.write(this.classifiedBlock(promptable));\n const approved = await this.askWhichToDelete(promptable, 'branch');\n if (approved.length === 0) {\n process.stdout.write('\\nNothing deleted — the branches above were kept.\\n');\n return;\n }\n const second = this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention);\n process.stdout.write(this.report(second));\n }\n\n /**\n * Reap the provably-dead worktrees, then ASK about the probably-dead ones — the same two-tier\n * posture the branch half has, because it is the same verdict on the same branch.\n *\n * WorktreeReaper enforces the safety rails regardless of what is passed or answered: never the\n * primary clone, never the tree this command is running in, and never `--force` (git's refusal to\n * remove a worktree holding untracked or modified files is a feature, and forcing it is how a\n * cleanup command becomes a data-loss command).\n */\n private async cleanUpWorktrees(repoRoot: string, retention: string): Promise<void> {\n const verdicts = this.worktreeSection.verdicts(repoRoot);\n const dead = this.worktreeSection.provablyDead(verdicts);\n if (dead.length > 0) {\n process.stdout.write(\n this.worktreeSection.report(\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', dead, retention)));\n }\n process.stdout.write(this.worktreeSection.sparedBlock(verdicts, dead));\n\n const promptable = this.worktreeSection.promptable(verdicts);\n if (promptable.length === 0) return;\n process.stdout.write(this.worktreeSection.promptBlock(promptable));\n const approved = await this.askWhichToDelete(promptable, 'worktree');\n if (approved.length === 0) {\n process.stdout.write('\\nNothing removed — the worktrees above were kept.\\n');\n return;\n }\n process.stdout.write(\n this.worktreeSection.report(\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n private report(result: ReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n return '\\n✅ Nothing to clean up — no local branch is provably dead.\\n';\n }\n\n let out = '\\n' + SEP + `🧹 Cleaned up ${String(result.reaped.length)} dead local branch(es)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} branch(es) could not be deleted:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.branch} — ${entry.error}\\n`;\n }\n\n // Printed even on success: a deletion the human cannot undo is a deletion they have to trust\n // blindly, and the whole argument for auto-cleanup is that they never have to.\n out += '\\nEvery deletion is logged with its pre-delete SHA in .webpieces/logs/branch-mutations.log —\\n'\n + 'recover any of them with the `recover=` command on its line.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedBranch): string {\n const sha = entry.sha !== '' ? ` (was ${entry.sha.slice(0, 8)})` : '';\n // The archive tag is printed inline because it is the ONE thing that makes this delete casually\n // reversible — a name a human can type, rather than a sha they have to go dig out of a log.\n const archived = entry.archiveTag !== ''\n ? `\\n archived → ${entry.archiveTag} (restore: ${this.archiver.restoreCommand(entry.branch, entry.archiveTag)})`\n : '';\n return ` ✓ ${entry.branch}${sha} — ${entry.reason}${archived}\\n`;\n }\n\n /**\n * The spared branches a human can meaningfully rule on, grouped and ordered most-safe first.\n *\n * Branches spared as IN_USE — checked out in a worktree — are still excluded here, but the reason\n * is no longer \"git would simply refuse\". That premise died the moment worktrees became reapable.\n * The real reason is the ORDER in run(): the worktree pass has already run, so an IN_USE branch is\n * one of exactly two things. Either its worktree was dead and the reap took the branch with it (so\n * it is not in this list at all), or its worktree is one we are deliberately keeping — locked, held\n * open by uncommitted work, or the one we are standing in — and offering to delete the branch out\n * from under a live checkout is not a question worth asking. The pair is offered TOGETHER by the\n * worktree prompt, which shows both the path and the branch it holds, or it is not offered at all.\n */\n private promptable(spared: DeletableBranch[]): DeletableBranch[] {\n const out: DeletableBranch[] = [];\n for (const classification of PROMPTABLE_CLASSIFICATIONS) {\n for (const entry of spared) {\n if (entry.classification === classification) out.push(entry);\n }\n }\n return out;\n }\n\n // The classification table: what each group means, and per branch its unique-commit count — the one\n // number that says whether a yes costs nothing or costs the only copy of somebody's work.\n private classifiedBlock(promptable: DeletableBranch[]): string {\n let out = '\\n' + SEP + `🤔 ${String(promptable.length)} branch(es) are probably dead — your call\\n` + SEP;\n let current = '';\n for (let i = 0; i < promptable.length; i += 1) {\n const entry = promptable[i];\n if (entry.classification !== current) {\n current = entry.classification;\n out += `\\n${CLASSIFICATION_HEADINGS[current] ?? current}\\n\\n`;\n }\n const commits = entry.commits >= 0 ? `${String(entry.commits)} unique commit(s)` : 'unique commits unknown';\n out += ` [${String(i + 1)}] ${entry.branch}\\n ${commits} — ${entry.reason}\\n`;\n }\n return out;\n }\n\n /**\n * Ask which of the classified branches to delete. Answers: `all`, `none` (default), or a\n * comma/space-separated list of the numbers shown.\n *\n * NON-INTERACTIVE (no TTY — CI, a hook, an AGENT'S piped shell) takes everything except\n * NEVER PROPOSED, and says exactly what it took.\n *\n * It used to take nothing at all, on the reasoning that \"a prompt nobody can see must never be\n * read as consent\". Correct about consent, wrong about who is standing there: the overwhelmingly\n * common non-TTY caller is an agent that was TOLD to clean up, and for it the prompt was a dead\n * end — wp-cleanup deleted nothing, the branch cap still blocked, and the only path left was to\n * hand the human a table of branches to adjudicate. The human's response to receiving one of\n * those: \"I am tired of AI asking me to cleanup things it can do by itself\".\n *\n * So consent is honoured where it is actually needed rather than everywhere. NEVER PROPOSED is\n * the group whose commits may be the only copy in existence; it is never taken unattended and is\n * the one thing left to ask about. The rest — a closed-unmerged PR superseded by later merges,\n * commits already patch-equivalent in main, a ref identical to origin/main — are redundant by\n * evidence, and every one of them is archived to an `archive/<date>/<branch>` tag with a\n * `recover=` command logged before it is touched. An interactive human still gets the full\n * prompt, including NEVER PROPOSED.\n */\n private async askWhichToDelete<T extends DeletableBranch | DeletableWorktree>(\n promptable: T[], kind: string,\n ): Promise<T[]> {\n if (process.stdin.isTTY !== true) return this.unattendedPicks(promptable, kind);\n const answer = (await this.question(\n `\\nDelete which ${kind}(s)? [all / none / e.g. \"1,3\"] (default none): `)).trim().toLowerCase();\n if (answer === '' || answer === 'none' || answer === 'n') return [];\n if (answer === 'all' || answer === 'a') return promptable;\n return this.pickByNumber(promptable, answer);\n }\n\n /**\n * What an unattended run takes: everything promptable except NEVER PROPOSED (see above).\n *\n * Prints the split either way, because a cleanup that deletes silently is a cleanup nobody can\n * audit — and the leftover NEVER PROPOSED names are exactly the short question an agent should\n * put to a human, in place of the six-branch table this replaced.\n */\n private unattendedPicks<T extends DeletableBranch | DeletableWorktree>(\n promptable: T[], kind: string,\n ): T[] {\n const taken = promptable.filter(\n (entry: T): boolean => entry.classification !== CLASSIFICATION_NEVER_PROPOSED);\n const left = promptable.filter(\n (entry: T): boolean => entry.classification === CLASSIFICATION_NEVER_PROPOSED);\n\n process.stdout.write(`\\nNot a terminal, so no prompt — taking the ${String(taken.length)} redundant `\n + `${kind}(s) above (each archived to a tag first, every delete recoverable).\\n`);\n if (left.length > 0) {\n // Both verdict classes carry `branch` — a worktree's is the branch it holds.\n const names = left.map((entry: T): string => entry.branch || '(detached)').join(', ');\n process.stdout.write(\n `Left alone — NEVER PROPOSED, may be the only copy: ${names}\\n`\n + 'Ask a human about those specifically; do not delete them yourself.\\n');\n }\n return taken;\n }\n\n // Parse `1,3` / `1 3` into branches, ignoring anything out of range. An unparseable answer selects\n // nothing, which is the fail-safe direction for a question about deleting.\n private pickByNumber<T extends DeletableBranch | DeletableWorktree>(promptable: T[], answer: string): T[] {\n const out: T[] = [];\n for (const token of answer.split(/[\\s,]+/)) {\n const index = Number(token);\n if (!Number.isInteger(index) || index < 1 || index > promptable.length) continue;\n out.push(promptable[index - 1]);\n }\n return out;\n }\n\n // Seam: overridden in the spec so the prompt parsing is testable with no terminal.\n protected question(prompt: string): Promise<string> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n return new Promise<string>((resolve: (value: string) => void): void => {\n rl.question(prompt, (answer: string): void => {\n rl.close();\n resolve(answer);\n });\n });\n }\n}\n"]}
1
+ {"version":3,"file":"cleanup-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as readline from 'readline';\nimport {\n BranchArchiver,\n BranchReaper,\n DeletableBranch,\n DeletableWorktree,\n ReapResult,\n ReapedBranch,\n RepoRootFinder,\n loadAndValidate,\n CLASSIFICATION_SUPERSEDED,\n CLASSIFICATION_CONTENT_IN_MAIN,\n CLASSIFICATION_NEVER_PROPOSED,\n CLASSIFICATION_NO_COMMITS,\n PROMPTABLE_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// One line of human-facing explanation per promptable classification, ordered most-safe first. These\n// replace the single string `no merged PR found — a human must decide`, which covered all three of\n// these situations identically and so told a human nothing they could act on.\nconst CLASSIFICATION_HEADINGS: Readonly<Record<string, string>> = {\n [CLASSIFICATION_SUPERSEDED]:\n 'SUPERSEDED — the PR was closed WITHOUT merging and later PRs have merged since. Near-certainly\\n'\n + ' the abandoned first attempt at work that landed under a different number. Safest group to delete.',\n [CLASSIFICATION_CONTENT_IN_MAIN]:\n 'CONTENT ALREADY IN MAIN — every commit has a patch-equivalent in origin/main (git cherry).\\n'\n + ' The work is not unique to this branch; only the commit objects are.',\n [CLASSIFICATION_NEVER_PROPOSED]:\n 'NEVER PROPOSED — no PR was ever opened, and these commits may be the ONLY copy in existence.\\n'\n + ' Read the unique-commit counts before answering. This is the group to say no to if unsure.',\n [CLASSIFICATION_NO_COMMITS]:\n 'NO COMMITS YET — identical to origin/main, so deleting the REF loses nothing. But this is also\\n'\n + ' exactly what a worktree looks like while somebody is still working in it and has not committed,\\n'\n + ' so it is asked about rather than reaped. Say no to any of these you (or an agent) are using.',\n};\n\n/**\n * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, then ASK\n * about the ones that are merely probably-dead.\n *\n * A merged PR is the ONLY proof that reaps anything unattended. \"Holds no commits\" used to be a second\n * proof and no longer is: it is equally the signature of a worktree an agent is working in right now,\n * so it moved into the group this command ASKS about.\n *\n * WHY WORKTREES ARE PART OF THIS: they were the half that never got reaped. The verdicts existed —\n * merged-branches.ts has been writing a full `DeletableWorktree[]` into the cache all along — and their\n * only consumer used them to BLOCK the next `git worktree add`, never to remove anything. Meanwhile a\n * live worktree pins its branch, so the branch was spared too. Two things the tooling could PROVE were\n * dead, accumulating forever, until the guard refused to create the next branch and the only remedy it\n * could offer was loosening its own cap. See WorktreeCleanupSection and WorktreeReaper.\n *\n * WHY a named command instead of the `git branch -D a b c` the guards used to print: an AI agent\n * reads a raw `-D` as destructive, so it asks permission and stops — which is exactly why branches\n * piled up despite the tooling knowing precisely which ones were dead. `pnpm wp-cleanup` is one\n * boring, allowlistable verb whose safety is a property of the command itself rather than of the\n * agent's judgement about a git flag.\n *\n * WHY IT NOW PROMPTS: sparing silently was the other half of the same problem. Every spared branch\n * reported the identical `no merged PR found — a human must decide`, so the human could not decide,\n * so nothing got deleted, so the pile grew until branch-creation-guard refused to make the next branch\n * and an agent went looking for a config knob to loosen. Shown a real classification with unique-commit\n * counts, the human in that session answered in five words: \"these should all be delete branches\".\n * The prompt is cheap because archiving happens FIRST — a yes costs a tag, not the history.\n *\n * All the danger still lives in the verdicts, not here — see BranchReaper for why every AUTOMATICALLY\n * deleted branch is provably dead and recoverable, and note that nothing in the prompted group is ever\n * deleted without an explicit typed answer.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanupCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly branchReaper: BranchReaper,\n private readonly archiver: BranchArchiver,\n private readonly worktreeSection: WorktreeCleanupSection,\n ) {}\n\n /**\n * WORKTREES FIRST, then branches. The order is the fix, not a detail: a worktree HOLDS its branch,\n * so that branch is spared `in-use` (\"remove that worktree before deleting the branch\") and nothing\n * used to remove the worktree — so both piled up until branch-creation-guard refused to make the\n * next one. Reaping the worktree takes its branch with it, and the branch pass then recomputes its\n * verdicts from scratch against the post-removal truth.\n */\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n await this.cleanUpWorktrees(repoRoot, retention);\n await this.cleanUpBranches(repoRoot, retention);\n }\n\n private async cleanUpBranches(repoRoot: string, retention: string): Promise<void> {\n // No cache argument: wp-cleanup recomputes the verdicts itself. The file on disk is allowed to\n // go stale, and stale evidence is fine for BLOCKING but never for DELETING.\n const result = this.branchReaper.reap(repoRoot, 'wp-cleanup', null, retention);\n process.stdout.write(this.report(result));\n\n const promptable = this.promptable(result.spared);\n if (promptable.length === 0) return;\n process.stdout.write(this.classifiedBlock(promptable));\n const approved = await this.askWhichToDelete(promptable, 'branch');\n if (approved.length === 0) {\n process.stdout.write('\\nNothing deleted — the branches above were kept.\\n');\n return;\n }\n const second = this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention);\n process.stdout.write(this.report(second));\n }\n\n /**\n * Reap the provably-dead worktrees, then ASK about the probably-dead ones — the same two-tier\n * posture the branch half has, because it is the same verdict on the same branch.\n *\n * WorktreeReaper enforces the safety rails regardless of what is passed or answered: never the\n * primary clone, never the tree this command is running in, and never `--force` (git's refusal to\n * remove a worktree holding untracked or modified files is a feature, and forcing it is how a\n * cleanup command becomes a data-loss command).\n */\n private async cleanUpWorktrees(repoRoot: string, retention: string): Promise<void> {\n const verdicts = this.worktreeSection.verdicts(repoRoot);\n const dead = this.worktreeSection.provablyDead(verdicts);\n if (dead.length > 0) {\n process.stdout.write(\n this.worktreeSection.report(\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', dead, retention)));\n }\n process.stdout.write(this.worktreeSection.sparedBlock(verdicts, dead));\n\n const promptable = this.worktreeSection.promptable(verdicts);\n if (promptable.length === 0) return;\n process.stdout.write(this.worktreeSection.promptBlock(promptable));\n const approved = await this.askWhichToDelete(promptable, 'worktree');\n if (approved.length === 0) {\n process.stdout.write('\\nNothing removed — the worktrees above were kept.\\n');\n return;\n }\n process.stdout.write(\n this.worktreeSection.report(\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n private report(result: ReapResult): string {\n const gone = result.alreadyGone.length;\n if (result.reaped.length === 0 && result.failed.length === 0 && gone === 0) {\n return '\\n✅ Nothing to clean up — no local branch is provably dead.\\n';\n }\n\n let out = '\\n' + SEP + `🧹 Cleaned up ${String(result.reaped.length)} dead local branch(es)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n // Stated, not warned about. These branches ARE gone — the detached refresher's auto-reap\n // deleted and archived them moments before this pass reached them, which it is entitled to do\n // (it acts on its own fresh verdicts, and the hooks start it on the same commands you run\n // wp-cleanup from). Before this line they came out under \"⚠️ N branch(es) could not be\n // deleted\", which reads as work left undone and sends a human hunting for branches that no\n // longer exist.\n if (gone > 0) {\n const names = result.alreadyGone.map((entry: ReapedBranch): string => entry.branch).join(', ');\n out += `\\nℹ️ ${String(gone)} branch(es) were already gone — a concurrent auto-reap deleted and\\n`\n + ` archived them first, so there was nothing left to do: ${names}\\n`;\n }\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} branch(es) could not be deleted:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.branch} — ${entry.error}\\n`;\n }\n\n // Printed even on success: a deletion the human cannot undo is a deletion they have to trust\n // blindly, and the whole argument for auto-cleanup is that they never have to.\n out += '\\nEvery deletion is logged with its pre-delete SHA in .webpieces/logs/branch-mutations.log —\\n'\n + 'recover any of them with the `recover=` command on its line.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedBranch): string {\n const sha = entry.sha !== '' ? ` (was ${entry.sha.slice(0, 8)})` : '';\n // The archive tag is printed inline because it is the ONE thing that makes this delete casually\n // reversible — a name a human can type, rather than a sha they have to go dig out of a log.\n const archived = entry.archiveTag !== ''\n ? `\\n archived → ${entry.archiveTag} (restore: ${this.archiver.restoreCommand(entry.branch, entry.archiveTag)})`\n : '';\n return ` ✓ ${entry.branch}${sha} — ${entry.reason}${archived}\\n`;\n }\n\n /**\n * The spared branches a human can meaningfully rule on, grouped and ordered most-safe first.\n *\n * Branches spared as IN_USE — checked out in a worktree — are still excluded here, but the reason\n * is no longer \"git would simply refuse\". That premise died the moment worktrees became reapable.\n * The real reason is the ORDER in run(): the worktree pass has already run, so an IN_USE branch is\n * one of exactly two things. Either its worktree was dead and the reap took the branch with it (so\n * it is not in this list at all), or its worktree is one we are deliberately keeping — locked, held\n * open by uncommitted work, or the one we are standing in — and offering to delete the branch out\n * from under a live checkout is not a question worth asking. The pair is offered TOGETHER by the\n * worktree prompt, which shows both the path and the branch it holds, or it is not offered at all.\n */\n private promptable(spared: DeletableBranch[]): DeletableBranch[] {\n const out: DeletableBranch[] = [];\n for (const classification of PROMPTABLE_CLASSIFICATIONS) {\n for (const entry of spared) {\n if (entry.classification === classification) out.push(entry);\n }\n }\n return out;\n }\n\n // The classification table: what each group means, and per branch its unique-commit count — the one\n // number that says whether a yes costs nothing or costs the only copy of somebody's work.\n private classifiedBlock(promptable: DeletableBranch[]): string {\n let out = '\\n' + SEP + `🤔 ${String(promptable.length)} branch(es) are probably dead — your call\\n` + SEP;\n let current = '';\n for (let i = 0; i < promptable.length; i += 1) {\n const entry = promptable[i];\n if (entry.classification !== current) {\n current = entry.classification;\n out += `\\n${CLASSIFICATION_HEADINGS[current] ?? current}\\n\\n`;\n }\n const commits = entry.commits >= 0 ? `${String(entry.commits)} unique commit(s)` : 'unique commits unknown';\n out += ` [${String(i + 1)}] ${entry.branch}\\n ${commits} — ${entry.reason}\\n`;\n }\n return out;\n }\n\n /**\n * Ask which of the classified branches to delete. Answers: `all`, `none` (default), or a\n * comma/space-separated list of the numbers shown.\n *\n * NON-INTERACTIVE (no TTY — CI, a hook, an AGENT'S piped shell) takes everything except\n * NEVER PROPOSED, and says exactly what it took.\n *\n * It used to take nothing at all, on the reasoning that \"a prompt nobody can see must never be\n * read as consent\". Correct about consent, wrong about who is standing there: the overwhelmingly\n * common non-TTY caller is an agent that was TOLD to clean up, and for it the prompt was a dead\n * end — wp-cleanup deleted nothing, the branch cap still blocked, and the only path left was to\n * hand the human a table of branches to adjudicate. The human's response to receiving one of\n * those: \"I am tired of AI asking me to cleanup things it can do by itself\".\n *\n * So consent is honoured where it is actually needed rather than everywhere. NEVER PROPOSED is\n * the group whose commits may be the only copy in existence; it is never taken unattended and is\n * the one thing left to ask about. The rest — a closed-unmerged PR superseded by later merges,\n * commits already patch-equivalent in main, a ref identical to origin/main — are redundant by\n * evidence, and every one of them is archived to an `archive/<date>/<branch>` tag with a\n * `recover=` command logged before it is touched. An interactive human still gets the full\n * prompt, including NEVER PROPOSED.\n */\n private async askWhichToDelete<T extends DeletableBranch | DeletableWorktree>(\n promptable: T[], kind: string,\n ): Promise<T[]> {\n if (process.stdin.isTTY !== true) return this.unattendedPicks(promptable, kind);\n const answer = (await this.question(\n `\\nDelete which ${kind}(s)? [all / none / e.g. \"1,3\"] (default none): `)).trim().toLowerCase();\n if (answer === '' || answer === 'none' || answer === 'n') return [];\n if (answer === 'all' || answer === 'a') return promptable;\n return this.pickByNumber(promptable, answer);\n }\n\n /**\n * What an unattended run takes: everything promptable except NEVER PROPOSED (see above).\n *\n * Prints the split either way, because a cleanup that deletes silently is a cleanup nobody can\n * audit — and the leftover NEVER PROPOSED names are exactly the short question an agent should\n * put to a human, in place of the six-branch table this replaced.\n */\n private unattendedPicks<T extends DeletableBranch | DeletableWorktree>(\n promptable: T[], kind: string,\n ): T[] {\n const taken = promptable.filter(\n (entry: T): boolean => entry.classification !== CLASSIFICATION_NEVER_PROPOSED);\n const left = promptable.filter(\n (entry: T): boolean => entry.classification === CLASSIFICATION_NEVER_PROPOSED);\n\n process.stdout.write(`\\nNot a terminal, so no prompt — taking the ${String(taken.length)} redundant `\n + `${kind}(s) above (each archived to a tag first, every delete recoverable).\\n`);\n if (left.length > 0) {\n // Both verdict classes carry `branch` — a worktree's is the branch it holds.\n const names = left.map((entry: T): string => entry.branch || '(detached)').join(', ');\n process.stdout.write(\n `Left alone — NEVER PROPOSED, may be the only copy: ${names}\\n`\n + 'Ask a human about those specifically; do not delete them yourself.\\n');\n }\n return taken;\n }\n\n // Parse `1,3` / `1 3` into branches, ignoring anything out of range. An unparseable answer selects\n // nothing, which is the fail-safe direction for a question about deleting.\n private pickByNumber<T extends DeletableBranch | DeletableWorktree>(promptable: T[], answer: string): T[] {\n const out: T[] = [];\n for (const token of answer.split(/[\\s,]+/)) {\n const index = Number(token);\n if (!Number.isInteger(index) || index < 1 || index > promptable.length) continue;\n out.push(promptable[index - 1]);\n }\n return out;\n }\n\n // Seam: overridden in the spec so the prompt parsing is testable with no terminal.\n protected question(prompt: string): Promise<string> {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n return new Promise<string>((resolve: (value: string) => void): void => {\n rl.question(prompt, (answer: string): void => {\n rl.close();\n resolve(answer);\n });\n });\n }\n}\n"]}