@webpieces/pr-gate 0.4.709 → 0.4.710

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.709",
3
+ "version": "0.4.710",
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",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.709",
18
+ "@webpieces/rules-config": "0.4.710",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -76,7 +76,7 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
76
76
  // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask
77
77
  // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever
78
78
  // grows a field, this call site is a compile error instead of a silent old behaviour.
79
- await this.cleanupCommand.run(new cleanup_options_1.CleanupOptions(new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_BRANCHES, false, ''), new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_WORKTREES, false, ''), false, false));
79
+ await this.cleanupCommand.run(new cleanup_options_1.CleanupOptions(new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_BRANCHES, false, ''), new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_WORKTREES, false, ''), false, false, false));
80
80
  this.sweep(repoRoot);
81
81
  this.reportStashed(stashed);
82
82
  }
@@ -1 +1 @@
1
- {"version":3,"file":"checkout-clean-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checkout-clean-main-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\nimport { MainCheckout, StashedFiles } from './main-checkout';\nimport { WorkingTreeGate, UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckoutCleanMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n private readonly workingTreeGate: WorkingTreeGate,\n private readonly mainCheckout: MainCheckout,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);\n const stashed = this.mainCheckout.goToMain(repoRoot, untracked);\n this.reportUntracked(untracked, stashed);\n // No flags: this is the \"go to main and take out the trash\" path, so cleanup behaves exactly as a\n // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask\n // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever\n // grows a field, this call site is a compile error instead of a silent old behaviour.\n await this.cleanupCommand.run(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, false, ''),\n new DeleteSelection(FLAG_DELETE_WORKTREES, false, ''),\n false, false));\n this.sweep(repoRoot);\n this.reportStashed(stashed);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Say which untracked files were let through, AFTER the move — so nobody has to wonder whether the\n * files still sitting in their tree came along for the ride. Printed rather than refused on: see\n * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.\n *\n * Silent when the checkout had to stash: those files are NOT sitting in the tree any more, and\n * \"nothing about them changed\" would be a lie. `reportStashed` speaks for that case instead.\n */\n private reportUntracked(untracked: UntrackedFiles, stashed: StashedFiles): void {\n if (!stashed.isEmpty()) return;\n const rendered = untracked.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * The stash banner goes LAST, on purpose. It is the one thing in this command's output a reader\n * must not miss — their files left the working tree — and the cleanup and sweep that follow the\n * checkout print enough to scroll it away if it were emitted where it happened.\n */\n private reportStashed(stashed: StashedFiles): void {\n const rendered = stashed.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n}\n"]}
1
+ {"version":3,"file":"checkout-clean-main-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checkout-clean-main-command.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,0DAIiC;AAEjC,uDAAmD;AACnD,uDAK2B;AAC3B,mDAA6D;AAC7D,2DAAsE;AAEtE,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEI,IAAM,wBAAwB,GAA9B,MAAM,wBAAwB;IAEZ;IACA;IACA;IACA;IACA;IALrB,YACqB,cAA8B,EAC9B,cAA8B,EAC9B,gBAAkC,EAClC,eAAgC,EAChC,YAA0B;QAJ1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,oBAAe,GAAf,eAAe,CAAiB;QAChC,iBAAY,GAAZ,YAAY,CAAc;IAC5C,CAAC;IAEJ,KAAK,CAAC,GAAG;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,kCAAkC,GAAG,IAAI,CAAC,CAAC;QACtE,IAAI,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YAC/B,OAAO;QACX,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;QAChE,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACzC,kGAAkG;QAClG,gGAAgG;QAChG,+FAA+F;QAC/F,sFAAsF;QACtF,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,gCAAc,CAC5C,IAAI,iCAAe,CAAC,sCAAoB,EAAE,KAAK,EAAE,EAAE,CAAC,EACpD,IAAI,iCAAe,CAAC,uCAAqB,EAAE,KAAK,EAAE,EAAE,CAAC,EACrD,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,SAAS,CAAC,QAAgB;QACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,4FAA4F;cAC1F,0DAA0D;cAC1D,4FAA4F;cAC5F,8FAA8F;cAC9F,wCAAwC,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACrB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;IAEO,gBAAgB,CAAC,QAAgB;QACrC,MAAM,IAAI,GAAG,2BAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC5C,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAClD,CAAC;IAED;;;;;;;OAOG;IACK,eAAe,CAAC,SAAyB,EAAE,OAAqB;QACpE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;YAAE,OAAO;QAC/B,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;QACpC,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED;;;;OAIG;IACK,aAAa,CAAC,OAAqB;QACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAClC,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,QAAgB;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QACjC,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAC1C,CAAC;CACJ,CAAA;AAzFY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACd,gCAAc;QACZ,+BAAgB;QACjB,mCAAe;QAClB,4BAAY;GANtC,wBAAwB,CAyFpC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n OrphanDirSweeper,\n RepoRootFinder,\n dotWebpieces,\n} from '@webpieces/rules-config';\n\nimport { CleanupCommand } from './cleanup-command';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\nimport { MainCheckout, StashedFiles } from './main-checkout';\nimport { WorkingTreeGate, UntrackedFiles } from './working-tree-gate';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checkout-clean-main` — go to main, get main, and take out the trash. ONE command, because the three\n * are one intention and splitting them is what let the third never happen.\n *\n * ─── ITS RELATIONSHIP TO `git checkout main && git pull origin main` (READ BEFORE \"DELETING\" THAT) ────\n * That pair was already the enforced spelling: bare `git checkout main` is blocked precisely so the pull\n * rides along. This command is the same pairing with cleanup and the sweep welded on, and the WORKFLOW\n * messages now prescribe this instead — two spellings where one sweeps and one does not is shim shape\n * #1, an agent types whichever is accepted and the corpses accumulate forever.\n *\n * The raw pair is NOT deleted, and the reason is a constraint that outranks the shim rule: it is a\n * TERMINAL ENTRY ON THE L0 ALLOWLIST (`CHECKOUT_MAIN_PULL_CMD` in ai-hook-rules\n * `src/bin/l0-allowlist.ts`, consumed as an L0 cure in `src/core/l0-matrix.ts`) — one of\n * the few commands still permitted while an L0 block denies every other tool call. In exactly that\n * state, `node_modules` is the thing that is untrustworthy, so a `pnpm wp-*` bin is the one kind of cure\n * that cannot be relied on to run. The same reasoning covers the L0 shim's own fix option in\n * `templates/ai-hook.sh` (rendered from `src/bin/shim-drift-fix.ts`).\n *\n * So the end state is TWO LAYERS, not a deletion, and it is now in place:\n * • L0 recovery (l0-allowlist, shim-drift-fix, l0-matrix) → keeps the raw pair. It is not a shim\n * there; it is the only thing that works when the package manager's output is what is in doubt.\n * • the WORKFLOW layer (stale-main-bash-guard's preferred cure, merged-branch-message/-bash-guard,\n * TreeRecovery's cleanupSteps and updateMainSteps, the L2 rows/doc, CLAUDE.md's \"Finishing a\n * Feature\") → prescribes `pnpm wp-checkout-clean-main`, and no longer prints the pair.\n *\n * The pair remains ALLOWED as a Bash command in both layers — this changed what the guards TEACH, not\n * what they permit. Blocking it would have deleted the L0 escape along with the shim.\n *\n * ─── WHY GOING TO MAIN IS THE RIGHT MOMENT TO SWEEP ───────────────────────────────────────────────────\n * The corpses appear when git moves the working tree onto a base where the tracked files under some\n * directory are gone. Landing a PR and returning to main is when every developer does that, on every\n * clone, roughly once per merged PR — so a sweep bolted here converges the whole team's machines with no\n * new trigger, no git hook to distribute, and no background daemon. It is also the quietest moment in the\n * day to move directories: nothing is building, no dev server is watching files.\n *\n * ─── WHY THE ORDER IS FIXED AND NOT PARALLEL ──────────────────────────────────────────────────────────\n * Cleanup runs BEFORE the sweep and not beside it. `wp-cleanup` removes dead worktrees, which changes\n * what is on disk, and a scan racing that would be reading a tree mid-demolition. The scan is one\n * ignore-walk and costs about a second; there is no wall-clock worth buying with that race.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckoutCleanMainCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly cleanupCommand: CleanupCommand,\n private readonly orphanDirSweeper: OrphanDirSweeper,\n private readonly workingTreeGate: WorkingTreeGate,\n private readonly mainCheckout: MainCheckout,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n process.stdout.write(`${SEP}🧹 Checkout main, pull, clean\\n${SEP}\\n`);\n if (this.isLinkedWorktree(repoRoot)) {\n await this.sweepOnly(repoRoot);\n return;\n }\n const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);\n const stashed = this.mainCheckout.goToMain(repoRoot, untracked);\n this.reportUntracked(untracked, stashed);\n // No flags: this is the \"go to main and take out the trash\" path, so cleanup behaves exactly as a\n // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask\n // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever\n // grows a field, this call site is a compile error instead of a silent old behaviour.\n await this.cleanupCommand.run(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, false, ''),\n new DeleteSelection(FLAG_DELETE_WORKTREES, false, ''),\n false, false, false));\n this.sweep(repoRoot);\n this.reportStashed(stashed);\n }\n\n /**\n * A linked worktree has no main to check out — `git checkout main` FATALS there with \"main is already\n * checked out at <primary clone>\", which is a footgun CLAUDE.md currently has to warn about in prose.\n * So the command does the half that IS meaningful here (worktrees accumulate corpses too, if a\n * long-lived one keeps merging main in) and says plainly where the other half lives.\n */\n private async sweepOnly(repoRoot: string): Promise<void> {\n process.stdout.write(\n 'This is a linked WORKTREE. main is checked out in the primary clone, so there is nothing\\n'\n + 'here to check out or pull — sweeping this tree only.\\n\\n'\n + 'Most worktrees never need this: `wp-cleanup` removes the whole directory once its branch\\n'\n + 'lands, which takes any orphan directories inside it along. Run this command again from the\\n'\n + 'primary clone to go to main there.\\n\\n');\n this.sweep(repoRoot);\n return Promise.resolve();\n }\n\n private isLinkedWorktree(repoRoot: string): boolean {\n const dirs = dotWebpieces.gitDirs(repoRoot);\n return dirs !== null && dirs.isLinkedWorktree;\n }\n\n /**\n * Say which untracked files were let through, AFTER the move — so nobody has to wonder whether the\n * files still sitting in their tree came along for the ride. Printed rather than refused on: see\n * `WorkingTreeGate` for why an untracked file is not the hazard a tracked one is.\n *\n * Silent when the checkout had to stash: those files are NOT sitting in the tree any more, and\n * \"nothing about them changed\" would be a lie. `reportStashed` speaks for that case instead.\n */\n private reportUntracked(untracked: UntrackedFiles, stashed: StashedFiles): void {\n if (!stashed.isEmpty()) return;\n const rendered = untracked.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * The stash banner goes LAST, on purpose. It is the one thing in this command's output a reader\n * must not miss — their files left the working tree — and the cleanup and sweep that follow the\n * checkout print enough to scroll it away if it were emitted where it happened.\n */\n private reportStashed(stashed: StashedFiles): void {\n const rendered = stashed.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n\n /**\n * Sweep, then print. Never throws: the checkout and pull above already succeeded, and a tidier is not\n * permitted to turn somebody's completed sync into a failed command.\n */\n private sweep(repoRoot: string): void {\n const report = this.orphanDirSweeper.sweep(repoRoot, new Date());\n const rendered = report.render();\n if (rendered === '') return;\n process.stdout.write(`\\n${rendered}`);\n }\n}\n"]}
@@ -13,8 +13,10 @@ import { CleanupOptions } from './cleanup-options';
13
13
  * is working in it. But that case is DETECTABLE, and detecting it is cheaper than asking a human:
14
14
  *
15
15
  * · the worktree holds uncommitted or untracked files (`git status --porcelain`) — spared, said out loud
16
- * · the worktree is LOCKED by a live holder (a lock reason naming something present, or a claude
17
- * agent whose pid is still running) spared by the verdicts before it ever reaches here
16
+ * · the worktree is LOCKED and that lock still stands — a reason naming somebody other than the
17
+ * Claude Code harness, or a harness lock whose agent the harness itself still reports as
18
+ * mid-tool-call — spared by the verdicts before it ever reaches here (see WorktreeLockVerdicts;
19
+ * the pid in a harness lock reason proves nothing, it is the shared session process)
18
20
  * · it is the tree we are standing in, or a detached HEAD — spared, likewise
19
21
  *
20
22
  * So the bar moved from "prove it is dead" to "prove somebody is holding it", deliberately, because
@@ -27,6 +29,10 @@ import { CleanupOptions } from './cleanup-options';
27
29
  * is a proxy, not a fact: a human running `pnpm wp-cleanup | tee log` has no tty, an agent on a pty
28
30
  * has one. The sniff stays as the DEFAULT selector; `--delete-branches` / `--delete-worktrees` /
29
31
  * `--interactive` / `--report` let the caller who KNOWS say so, and an explicit flag always wins.
32
+ * `--ignore-stale-locks` is the same idea for the OTHER thing this command could not previously be
33
+ * told: that a standing claude-agent lock is stale. It replaces the `git worktree unlock` a human had
34
+ * to run per directory — seven times, in the incident that produced it — before wp-cleanup would do
35
+ * its job. A dirty worktree and a genuinely live agent's are still spared under it.
30
36
  *
31
37
  * ─── WHY A NON-TTY RUN NOW DELETES NOTHING IT WAS NOT ASKED TO ───────────────────────────────────
32
38
  * It used to silently take the "redundant" groups and print a bare list of names it left. That is
@@ -48,8 +48,10 @@ const CLASSIFICATION_HEADINGS = {
48
48
  * is working in it. But that case is DETECTABLE, and detecting it is cheaper than asking a human:
49
49
  *
50
50
  * · the worktree holds uncommitted or untracked files (`git status --porcelain`) — spared, said out loud
51
- * · the worktree is LOCKED by a live holder (a lock reason naming something present, or a claude
52
- * agent whose pid is still running) spared by the verdicts before it ever reaches here
51
+ * · the worktree is LOCKED and that lock still stands — a reason naming somebody other than the
52
+ * Claude Code harness, or a harness lock whose agent the harness itself still reports as
53
+ * mid-tool-call — spared by the verdicts before it ever reaches here (see WorktreeLockVerdicts;
54
+ * the pid in a harness lock reason proves nothing, it is the shared session process)
53
55
  * · it is the tree we are standing in, or a detached HEAD — spared, likewise
54
56
  *
55
57
  * So the bar moved from "prove it is dead" to "prove somebody is holding it", deliberately, because
@@ -62,6 +64,10 @@ const CLASSIFICATION_HEADINGS = {
62
64
  * is a proxy, not a fact: a human running `pnpm wp-cleanup | tee log` has no tty, an agent on a pty
63
65
  * has one. The sniff stays as the DEFAULT selector; `--delete-branches` / `--delete-worktrees` /
64
66
  * `--interactive` / `--report` let the caller who KNOWS say so, and an explicit flag always wins.
67
+ * `--ignore-stale-locks` is the same idea for the OTHER thing this command could not previously be
68
+ * told: that a standing claude-agent lock is stale. It replaces the `git worktree unlock` a human had
69
+ * to run per directory — seven times, in the incident that produced it — before wp-cleanup would do
70
+ * its job. A dirty worktree and a genuinely live agent's are still spared under it.
65
71
  *
66
72
  * ─── WHY A NON-TTY RUN NOW DELETES NOTHING IT WAS NOT ASKED TO ───────────────────────────────────
67
73
  * It used to silently take the "redundant" groups and print a bare list of names it left. That is
@@ -103,7 +109,7 @@ let CleanupCommand = class CleanupCommand {
103
109
  const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
104
110
  const retention = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.landPr.branchRetention;
105
111
  if (options.report) {
106
- this.reportOnly(repoRoot, retention);
112
+ this.reportOnly(repoRoot, retention, options);
107
113
  return;
108
114
  }
109
115
  await this.cleanUpWorktrees(repoRoot, retention, options);
@@ -116,9 +122,9 @@ let CleanupCommand = class CleanupCommand {
116
122
  * because a run that deletes nothing cannot renumber anything. That is what makes it the honest
117
123
  * first half of `--report` → read the numbers → `--delete-branches=1,3`.
118
124
  */
119
- reportOnly(repoRoot, retention) {
125
+ reportOnly(repoRoot, retention, options) {
120
126
  process.stdout.write('\n' + SEP + '🔎 wp-cleanup --report — nothing will be deleted\n' + SEP);
121
- const verdicts = this.worktreeSection.verdicts(repoRoot);
127
+ const verdicts = this.worktreeSection.verdicts(repoRoot, options.ignoreStaleLocks);
122
128
  const deadTrees = this.worktreeSection.provablyDead(verdicts);
123
129
  process.stdout.write(this.wouldReapBlock(WORKTREE_KINDS, deadTrees.map((tree) => `${tree.path} — ${tree.reason}`)));
124
130
  const treePromptable = this.worktreeSection.promptable(verdicts);
@@ -195,7 +201,7 @@ let CleanupCommand = class CleanupCommand {
195
201
  * cleanup command becomes a data-loss command).
196
202
  */
197
203
  async cleanUpWorktrees(repoRoot, retention, options) {
198
- const verdicts = this.worktreeSection.verdicts(repoRoot);
204
+ const verdicts = this.worktreeSection.verdicts(repoRoot, options.ignoreStaleLocks);
199
205
  const dead = this.worktreeSection.provablyDead(verdicts);
200
206
  if (dead.length > 0) {
201
207
  process.stdout.write(this.worktreeSection.report(repoRoot, this.worktreeSection.reap(repoRoot, 'wp-cleanup', dead, retention)));
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-command.ts"],"names":[],"mappings":";;;;AAAA,2DAAqC;AACrC,0DAmBiC;AACjC,yCAA2D;AAE3D,yDAA4D;AAC5D,uDAK2B;AAE3B,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,sGAAsG;AACtG,8FAA8F;AAC9F,MAAM,WAAW,GAAG,QAAQ,CAAC;AAC7B,MAAM,YAAY,GAAG,UAAU,CAAC;AAChC,MAAM,aAAa,GAAG,UAAU,CAAC;AACjC,MAAM,cAAc,GAAG,WAAW,CAAC;AAEnC,mGAAmG;AACnG,oGAAoG;AACpG,MAAM,2BAA2B,GAAsB;IACnD,uCAAwB;IACxB,8CAA+B;IAC/B,4CAA6B;CAChC,CAAC;AAEF,qGAAqG;AACrG,mGAAmG;AACnG,8EAA8E;AAC9E,EAAE;AACF,oGAAoG;AACpG,oGAAoG;AACpG,gGAAgG;AAChG,MAAM,uBAAuB,GAAqC;IAC9D,CAAC,wCAAyB,CAAC,EACvB,kGAAkG;UAChG,qGAAqG;IAC3G,CAAC,6CAA8B,CAAC,EAC5B,8FAA8F;UAC5F,uEAAuE;IAC7E,CAAC,4CAA6B,CAAC,EAC3B,gGAAgG;UAC9F,6FAA6F;CACtG,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IAEF;IACA;IACA;IACA;IAIA;IARrB,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,QAAwB,EACxB,eAAuC;IACxD,8FAA8F;IAC9F,4FAA4F;IAC5F,gEAAgE;IAC/C,WAA8B;QAP9B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,aAAQ,GAAR,QAAQ,CAAgB;QACxB,oBAAe,GAAf,eAAe,CAAwB;QAIvC,gBAAW,GAAX,WAAW,CAAmB;IAChD,CAAC;IAEJ;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,CAAC,OAAuB;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,MAAM,SAAS,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC;QAC1E,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YACrC,OAAO;QACX,CAAC;QACD,MAAM,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;;OAMG;IACK,UAAU,CAAC,QAAgB,EAAE,SAAiB;QAClD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,oDAAoD,GAAG,GAAG,CAAC,CAAC;QAE9F,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CACpC,cAAc,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,IAAuB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1G,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACjE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,cAAc,EAAE,EACpE,SAAS,CAAC,GAAG,CAAC,CAAC,IAAuB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5E,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;QACvD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;YAClE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,cAAc,EAAE,uCAAqB,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3G,CAAC;QAED,0FAA0F;QAC1F,2FAA2F;QAC3F,4FAA4F;QAC5F,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,oCAAqB,CAAC,CAAC,MAAM,CAAC;QACpG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,YAAY,EAAE,QAAQ;aAC1D,MAAM,CAAC,CAAC,KAAsB,EAAW,EAAE,CACxC,2BAA2B,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;aAC9D,GAAG,CAAC,CAAC,KAAsB,EAAU,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,YAAY,EAAE,EAClE,KAAK,CAAC,GAAG,CAAC,CAAC,KAAsB,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAClE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC3C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;YAClD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,sCAAoB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACpG,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iEAAiE,SAAS,KAAK;cAC9F,wFAAwF,CAAC,CAAC;IACpG,CAAC;IAEO,cAAc,CAAC,KAAa,EAAE,KAAe;QACjD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAClC,IAAI,GAAG,GAAG,gBAAgB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,8CAA8C,CAAC;QACtG,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,GAAG,IAAI,OAAO,IAAI,IAAI,CAAC;QACjD,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,eAAe,CAAC,QAAgB,EAAE,SAAiB,EAAE,OAAuB;QACtF,+FAA+F;QAC/F,4EAA4E;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAE3C,8FAA8F;QAC9F,qEAAqE;QACrE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE7B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,CACvD,CAAC,KAAsB,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACxD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EACrC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QACnF,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrB,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACjF,OAAO;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAC9B,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,sCAAoB,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjG,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,gCAAgC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,YAAY,qBAAqB,CAAC,CAAC;YAC/F,OAAO;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EACrC,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACtF,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,gBAAgB,CAAC,QAAgB,EAAE,SAAiB,EAAE,OAAuB;QACvF,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,EAChC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QAEvE,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC7D,0FAA0F;QAC1F,6FAA6F;QAC7F,+FAA+F;QAC/F,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAE3C,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE9B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,KAAK,CAAC,GAAG,CACzD,CAAC,IAAuB,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;YAC5E,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,EAChC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QAClF,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAC9B,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,uCAAqB,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACvG,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,gCAAgC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,cAAc,qBAAqB,CAAC,CAAC;YACjG,OAAO;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAChB,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,EAChC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACrF,CAAC;IAED,+FAA+F;IAC/F,2EAA2E;IACnE,WAAW;QACf,OAAO,sEAAsE;cACvE,qEAAqE,CAAC;IAChF,CAAC;IAEO,SAAS,CAAC,KAAa,EAAE,KAAe;QAC5C,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG;cACd,eAAe,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,KAAK,iDAAiD;cACzG,GAAG,GAAG,IAAI;cACV,+FAA+F;cAC/F,iGAAiG,CAAC;QACxG,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,GAAG,IAAI,OAAO,IAAI,IAAI,CAAC;QACjD,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,MAAM,CAAC,QAAgB,EAAE,MAAkB;QAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;QACvC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEtF,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,iBAAiB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,0BAA0B,GAAG,GAAG,GAAG,IAAI,CAAC;QAC5G,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;YAAE,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEjE,yFAAyF;QACzF,8FAA8F;QAC9F,0FAA0F;QAC1F,uFAAuF;QACvF,2FAA2F;QAC3F,gBAAgB;QAChB,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACX,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/F,GAAG,IAAI,SAAS,MAAM,CAAC,IAAI,CAAC,sEAAsE;kBAC5F,4DAA4D,KAAK,IAAI,CAAC;QAChF,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,GAAG,IAAI,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,qCAAqC,CAAC;YAClF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;gBAAE,GAAG,IAAI,OAAO,KAAK,CAAC,MAAM,MAAM,KAAK,CAAC,KAAK,IAAI,CAAC;QACvF,CAAC;QAED,6FAA6F;QAC7F,+EAA+E;QAC/E,GAAG,IAAI,0DAA0D,IAAI,CAAC,WAAW,CAAC,qBAAqB,CAAC,QAAQ,CAAC,MAAM;cACjH,gEAAgE,CAAC;QACvE,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,UAAU,CAAC,KAAmB;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,gGAAgG;QAChG,4FAA4F;QAC5F,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,KAAK,EAAE;YACpC,CAAC,CAAC,sBAAsB,KAAK,CAAC,UAAU,gBAAgB,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,GAAG;YACvH,CAAC,CAAC,EAAE,CAAC;QACT,OAAO,OAAO,KAAK,CAAC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,GAAG,QAAQ,IAAI,CAAC;IACtE,CAAC;IAED;;;;;;;;;;;OAWG;IACK,UAAU,CAAC,MAAyB;QACxC,MAAM,GAAG,GAAsB,EAAE,CAAC;QAClC,KAAK,MAAM,cAAc,IAAI,0CAA2B,EAAE,CAAC;YACvD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACzB,IAAI,KAAK,CAAC,cAAc,KAAK,cAAc;oBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjE,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAgD,WAAgB;QACzE,OAAO,WAAW,CAAC,MAAM,CACrB,CAAC,KAAQ,EAAW,EAAE,CAAC,KAAK,CAAC,cAAc,KAAK,wCAAyB,CAAC,CAAC;IACnF,CAAC;IAEO,IAAI,CAAgD,UAAe,EAAE,KAAU;QACnF,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,KAAQ,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,oGAAoG;IACpG,0FAA0F;IAClF,eAAe,CAAC,UAA6B;QACjD,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,6CAA6C,GAAG,GAAG,CAAC;QAC1G,IAAI,OAAO,GAAG,EAAE,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,KAAK,CAAC,cAAc,KAAK,OAAO,EAAE,CAAC;gBACnC,OAAO,GAAG,KAAK,CAAC,cAAc,CAAC;gBAC/B,GAAG,IAAI,KAAK,uBAAuB,CAAC,OAAO,CAAC,IAAI,OAAO,MAAM,CAAC;YAClE,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,wBAAwB,CAAC;YAC5G,GAAG,IAAI,MAAM,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,aAAa,OAAO,MAAM,KAAK,CAAC,MAAM,IAAI,CAAC;QAC1F,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,MAAM,CAChB,QAAgB,EAAE,KAAU,EAAE,IAAY,EAAE,KAAa,EAAE,IAAY,EACvE,SAA0B,EAAE,OAAuB;QAEnD,IAAI,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,UAAU,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG;kBACzF,GAAG,KAAK,WAAW,CAAC,CAAC;YAC3B,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC3G,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YACzE,OAAO,EAAE,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,QAAQ,CAC/B,kBAAkB,IAAI,iDAAiD,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACnG,IAAI,MAAM,KAAK,EAAE,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,EAAE,CAAC;QACpE,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,KAAK,CAAC;QACrD,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACK,QAAQ,CAAC,QAAgB,EAAE,KAAa,EAAE,IAAY,EAAE,KAAa;QACzE,OAAO,6BAA6B,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,sBAAsB;cAC1E,yCAAyC;cACzC,qBAAqB,IAAI,wCAAwC;cACjE,qBAAqB,IAAI,0DAA0D;cACnF,qBAAqB,IAAI,iCAAiC;cAC1D,2FAA2F;cAC3F,2FAA2F;cAC3F,2EAA2E;cAC3E,qEAAqE;cACrE,GAAG,IAAI,CAAC,WAAW,CAAC,qBAAqB,CAAC,QAAQ,CAAC,IAAI,CAAC;IAClE,CAAC;IAED,iGAAiG;IACjG,oGAAoG;IACpG,gGAAgG;IAChG,sFAAsF;IAC9E,YAAY,CAAgD,UAAe,EAAE,MAAc;QAC/F,MAAM,GAAG,GAAQ,EAAE,CAAC;QACpB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,UAAU,CAAC,MAAM;gBAAE,SAAS;YACjF,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,mFAAmF;IACzE,QAAQ,CAAC,MAAc;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACtF,OAAO,IAAI,OAAO,CAAS,CAAC,OAAgC,EAAQ,EAAE;YAClE,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAc,EAAQ,EAAE;gBACzC,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO,CAAC,MAAM,CAAC,CAAC;YACpB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AA9WY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QAChB,2BAAY;QAChB,6BAAc;QACP,yCAAsB;QAI1B,gCAAiB;GAT1C,cAAc,CA8W1B","sourcesContent":["import * as readline from 'readline';\nimport {\n BranchArchiver,\n BranchMutationLog,\n BranchReaper,\n DeletableBranch,\n DeletableWorktree,\n ReapResult,\n ReapedBranch,\n RepoRootFinder,\n loadAndValidate,\n BRANCH_RETENTION_KEEP,\n CLASSIFICATION_SUPERSEDED,\n CLASSIFICATION_CONTENT_IN_MAIN,\n CLASSIFICATION_NEVER_PROPOSED,\n CLASSIFICATION_NO_COMMITS,\n CLASSIFICATION_MERGED_PR,\n CLASSIFICATION_BACKUP_OF_MERGED,\n CLASSIFICATION_BACKUP_OF_LIVE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Singular / plural as a pair, because every one of these strings is read by a human deciding whether\n// to delete something and `branch(s)` reads as a typo in the middle of exactly that sentence.\nconst BRANCH_KIND = 'branch';\nconst BRANCH_KINDS = 'branches';\nconst WORKTREE_KIND = 'worktree';\nconst WORKTREE_KINDS = 'worktrees';\n\n// The verdicts that reap themselves — a merged PR, or a snapshot of a ref that still exists. Named\n// here only so `--report` can SAY what a real run would take; the reaping itself is BranchReaper's.\nconst AUTO_REAPED_CLASSIFICATIONS: readonly string[] = [\n CLASSIFICATION_MERGED_PR,\n CLASSIFICATION_BACKUP_OF_MERGED,\n CLASSIFICATION_BACKUP_OF_LIVE,\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.\n//\n// CLASSIFICATION_NO_COMMITS has no entry and needs none: a zero-commit ref never reaches this block\n// any more — it is reaped on sight (see `husks`). A worktree spared for holding uncommitted work is\n// reported by the worktree section, with that as its reason, rather than offered as a question.\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};\n\n/**\n * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, REAP\n * THE ZERO-COMMIT HUSKS, then report everything that is left with the exact command that takes it.\n *\n * ─── WHY A ZERO-COMMIT REF IS REAPED AND NOT ASKED ABOUT ─────────────────────────────────────────\n * A ref with 0 unique commits is byte-identical to origin/main. Deleting it loses a NAME, not a\n * commit — and the name comes back with one `git checkout -b`, or from the archive tag this writes\n * first. It was previously PROMPTED about, for one real reason: a WORKING TREE looks exactly like\n * that husk from `git worktree add -b` until its first commit, i.e. for exactly the window an agent\n * is working in it. But that case is DETECTABLE, and detecting it is cheaper than asking a human:\n *\n * · the worktree holds uncommitted or untracked files (`git status --porcelain`) — spared, said out loud\n * · the worktree is LOCKED by a live holder (a lock reason naming something present, or a claude\n * agent whose pid is still running) — spared by the verdicts before it ever reaches here\n * · it is the tree we are standing in, or a detached HEAD — spared, likewise\n *\n * So the bar moved from \"prove it is dead\" to \"prove somebody is holding it\", deliberately, because\n * the cost of being wrong is a re-`checkout -b` and the cost of being cautious was a prompt that\n * stopped a human's terminal to adjudicate two refs that could not possibly hold work. Refs that DO\n * carry unique commits keep every bit of the old caution.\n *\n * ─── WHY FLAGS, AND WHY THEY BEAT THE TTY SNIFF ──────────────────────────────────────────────────\n * `process.stdin.isTTY !== true` used to be the whole basis for \"is a human standing here?\", and it\n * is a proxy, not a fact: a human running `pnpm wp-cleanup | tee log` has no tty, an agent on a pty\n * has one. The sniff stays as the DEFAULT selector; `--delete-branches` / `--delete-worktrees` /\n * `--interactive` / `--report` let the caller who KNOWS say so, and an explicit flag always wins.\n *\n * ─── WHY A NON-TTY RUN NOW DELETES NOTHING IT WAS NOT ASKED TO ───────────────────────────────────\n * It used to silently take the \"redundant\" groups and print a bare list of names it left. That is\n * strictly less than the human sees, and — worse — it broke the one invariant that makes numeric\n * selection safe: THE NUMBERS IN THE FLAG ARE THE NUMBERS JUST PRINTED. A run that deletes half of a\n * numbered list renumbers the rest, so the `--delete-branches=3,4` it printed would land on entries\n * 5 and 6 next time. So the unattended path now prints the IDENTICAL numbered, classified block a\n * human sees plus the exact command that takes it, and takes nothing from that block itself. The\n * husks it does reap are never IN the block, so reaping them cannot shift a single number.\n *\n * All the danger still lives in the verdicts, not here — see BranchReaper for why every automatically\n * deleted branch is recoverable, and note that EVERY delete on every path here archives first.\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 // The log path is COMPUTED, never restated. It is per-worktree, so the relative literal these\n // messages used to carry named a file that does not exist in a linked worktree — the reader\n // greps nothing and reads the silence as \"nothing was deleted\".\n private readonly mutationLog: BranchMutationLog,\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(options: CleanupOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n if (options.report) {\n this.reportOnly(repoRoot, retention);\n return;\n }\n await this.cleanUpWorktrees(repoRoot, retention, options);\n await this.cleanUpBranches(repoRoot, retention, options);\n }\n\n /**\n * `--report`: the whole classified picture, and NOT ONE DELETE.\n *\n * This is the only run whose numbers are guaranteed still valid when the next command starts,\n * because a run that deletes nothing cannot renumber anything. That is what makes it the honest\n * first half of `--report` → read the numbers → `--delete-branches=1,3`.\n */\n private reportOnly(repoRoot: string, retention: string): void {\n process.stdout.write('\\n' + SEP + '🔎 wp-cleanup --report — nothing will be deleted\\n' + SEP);\n\n const verdicts = this.worktreeSection.verdicts(repoRoot);\n const deadTrees = this.worktreeSection.provablyDead(verdicts);\n process.stdout.write(this.wouldReapBlock(\n WORKTREE_KINDS, deadTrees.map((tree: DeletableWorktree): string => `${tree.path} — ${tree.reason}`)));\n const treePromptable = this.worktreeSection.promptable(verdicts);\n const treeHusks = this.husks(treePromptable);\n process.stdout.write(this.wouldReapBlock(`zero-commit ${WORKTREE_KINDS}`,\n treeHusks.map((tree: DeletableWorktree): string => `${tree.path} [${tree.branch}]`)));\n process.stdout.write(this.worktreeSection.sparedBlock(verdicts, deadTrees));\n const treeBlock = this.rest(treePromptable, treeHusks);\n if (treeBlock.length > 0) {\n process.stdout.write(this.worktreeSection.promptBlock(treeBlock));\n process.stdout.write(this.flagHint(repoRoot, WORKTREE_KINDS, FLAG_DELETE_WORKTREES, treeBlock.length));\n }\n\n // Retention 'keep' turns BranchReaper.reap into a pure verdict computation — every branch\n // lands in `spared`, nothing is touched. That is exactly what a report needs, and it means\n // the classified block below is computed by the identical code path a real run uses, so the\n // numbering it prints IS the numbering the flag will act on.\n const branches = this.branchReaper.reap(repoRoot, 'wp-cleanup', null, BRANCH_RETENTION_KEEP).spared;\n process.stdout.write(this.wouldReapBlock(BRANCH_KINDS, branches\n .filter((entry: DeletableBranch): boolean =>\n AUTO_REAPED_CLASSIFICATIONS.includes(entry.classification))\n .map((entry: DeletableBranch): string => `${entry.branch} — ${entry.reason}`)));\n const promptable = this.promptable(branches);\n const husks = this.husks(promptable);\n process.stdout.write(this.wouldReapBlock(`zero-commit ${BRANCH_KINDS}`,\n husks.map((entry: DeletableBranch): string => entry.branch)));\n const block = this.rest(promptable, husks);\n if (block.length > 0) {\n process.stdout.write(this.classifiedBlock(block));\n process.stdout.write(this.flagHint(repoRoot, BRANCH_KINDS, FLAG_DELETE_BRANCHES, block.length));\n }\n process.stdout.write(`\\nNothing was deleted (--report). Retention policy in effect: ${retention}.\\n`\n + 'Re-run without --report to act, or with the flags above to say exactly what to take.\\n');\n }\n\n private wouldReapBlock(kinds: string, lines: string[]): string {\n if (lines.length === 0) return '';\n let out = `\\nWould reap ${String(lines.length)} ${kinds} — no question asked, each archived first:\\n`;\n for (const line of lines) out += ` ✓ ${line}\\n`;\n return out;\n }\n\n private async cleanUpBranches(repoRoot: string, retention: string, options: CleanupOptions): 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 const first = this.report(repoRoot, result);\n process.stdout.write(first);\n\n const promptable = this.promptable(result.spared);\n const husks = this.husks(promptable);\n const block = this.rest(promptable, husks);\n\n // Range-check the flag against the block BEFORE anything in this half is deleted, so a caller\n // holding stale numbers stops the run rather than half-executing it.\n options.branches.pick(block);\n\n if (husks.length > 0) {\n process.stdout.write(this.huskBlock(BRANCH_KINDS, husks.map(\n (entry: DeletableBranch): string => entry.branch)));\n process.stdout.write(this.report(repoRoot,\n this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', husks, retention)));\n }\n\n if (block.length === 0) {\n if (first === '' && husks.length === 0) process.stdout.write(this.nothingToDo());\n return;\n }\n process.stdout.write(this.classifiedBlock(block));\n const approved = await this.decide(\n repoRoot, block, BRANCH_KIND, BRANCH_KINDS, FLAG_DELETE_BRANCHES, options.branches, options);\n if (approved.length === 0) {\n process.stdout.write(\n `\\nNothing else deleted — the ${String(block.length)} ${BRANCH_KINDS} above were kept.\\n`);\n return;\n }\n process.stdout.write(this.report(repoRoot,\n this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n /**\n * Reap the provably-dead worktrees and the zero-commit husks, then decide about the rest.\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, options: CleanupOptions): 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(repoRoot,\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 // A zero-commit worktree is a husk ONLY if nobody is holding it. Uncommitted or untracked\n // files are the one thing that says \"somebody is\", and they are the one thing no archive tag\n // can bring back — so they are checked here rather than left to git's refusal at removal time.\n const husks = this.worktreeSection.withoutUncommitted(this.husks(promptable));\n const block = this.rest(promptable, husks);\n\n options.worktrees.pick(block);\n\n if (husks.length > 0) {\n process.stdout.write(this.huskBlock(WORKTREE_KINDS, husks.map(\n (tree: DeletableWorktree): string => `${tree.path} [${tree.branch}]`)));\n process.stdout.write(\n this.worktreeSection.report(repoRoot,\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', husks, retention)));\n }\n\n if (block.length === 0) return;\n process.stdout.write(this.worktreeSection.promptBlock(block));\n const approved = await this.decide(\n repoRoot, block, WORKTREE_KIND, WORKTREE_KINDS, FLAG_DELETE_WORKTREES, options.worktrees, options);\n if (approved.length === 0) {\n process.stdout.write(\n `\\nNothing else removed — the ${String(block.length)} ${WORKTREE_KINDS} above were kept.\\n`);\n return;\n }\n process.stdout.write(\n this.worktreeSection.report(repoRoot,\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n // What a bare run with an empty repo says. It points at --help because the flags are the whole\n // reason a caller who DOES have something to say never has to be prompted.\n private nothingToDo(): string {\n return '\\nNothing else to decide. `pnpm wp-cleanup --help` lists every flag '\n + '(--report, --delete-branches, --delete-worktrees, --interactive).\\n';\n }\n\n private huskBlock(kinds: string, names: string[]): string {\n let out = '\\n' + SEP\n + `♻️ Reaping ${String(names.length)} zero-commit ${kinds} — 0 unique commits, identical to origin/main\\n`\n + SEP + '\\n'\n + 'Nothing is held here: no uncommitted files, no live lock, not the tree you are standing in.\\n'\n + 'Deleting one of these loses a NAME, not a commit — and the name is archived to a tag first.\\n\\n';\n for (const name of names) out += ` · ${name}\\n`;\n return out;\n }\n\n private report(repoRoot: string, result: ReapResult): string {\n const gone = result.alreadyGone.length;\n if (result.reaped.length === 0 && result.failed.length === 0 && gone === 0) return '';\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\\n${this.mutationLog.branchMutationLogPath(repoRoot)} —\\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 ADJUDICATED_CLASSIFICATIONS) {\n for (const entry of spared) {\n if (entry.classification === classification) out.push(entry);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit husks out of a promptable list — reaped by default, and never numbered.\n *\n * Keeping them OUT of the numbered block is what keeps `--delete-branches=1,3` honest: whatever\n * this reaps, the entries the caller can name are the same entries in the same order as on the\n * `--report` run that printed those numbers.\n *\n * THE CLASSIFICATION TOKEN IS THE ONLY SPELLING OF \"husk\". It would read as belt-and-braces to\n * also accept `commits === 0` here, and it is the opposite: MergedBranchesService.classify tests\n * commits===0 BEFORE anything else, so a second test can only ever disagree with the verdict — and\n * disagreeing in the widening direction, auto-reaping something no classification called a husk.\n */\n private husks<T extends DeletableBranch | DeletableWorktree>(adjudicated: T[]): T[] {\n return adjudicated.filter(\n (entry: T): boolean => entry.classification === CLASSIFICATION_NO_COMMITS);\n }\n\n private rest<T extends DeletableBranch | DeletableWorktree>(promptable: T[], husks: T[]): T[] {\n return promptable.filter((entry: T): boolean => !husks.includes(entry));\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 * Who decides about the numbered block, in priority order:\n *\n * 1. an explicit `--delete-branches` / `--delete-worktrees` — the caller said so, and a caller who\n * said so is never overruled by what stdin happens to be attached to;\n * 2. a terminal, or `--interactive` — ask;\n * 3. otherwise — take NOTHING, and print the exact command that takes it.\n *\n * (3) is the case an agent lands in, and it is deliberately empty-handed. See the class comment:\n * a run that deletes part of a numbered list invalidates the numbers it just printed, and those\n * numbers are the whole interface the next run is given.\n */\n private async decide<T extends DeletableBranch | DeletableWorktree>(\n repoRoot: string, block: T[], kind: string, kinds: string, flag: string,\n selection: DeleteSelection, options: CleanupOptions,\n ): Promise<T[]> {\n if (selection.given()) {\n const picked = selection.pick(block);\n process.stdout.write(`\\n${flag} chose ${String(picked.length)} of the ${String(block.length)} `\n + `${kinds} above.\\n`);\n if (picked.length < block.length) process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));\n return picked;\n }\n if (!options.prompts()) {\n process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));\n return [];\n }\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 block;\n return this.pickByNumber(block, answer);\n }\n\n /**\n * The exact command that takes what this run left — the half a non-tty caller never used to get.\n *\n * It states the numbering contract out loud because a shifted number is the one way this command\n * can delete the wrong ref, and the caller reading this is usually a program.\n */\n private flagHint(repoRoot: string, kinds: string, flag: string, count: number): string {\n return `\\nLeft for you to decide (${String(count)} ${kinds}, numbered above).\\n`\n + 'To delete them yourself, re-run with:\\n'\n + ` pnpm wp-cleanup ${flag}=all # every one listed above\\n`\n + ` pnpm wp-cleanup ${flag}=1,3 # just those, BY THE NUMBERS PRINTED ABOVE\\n`\n + ` pnpm wp-cleanup ${flag}=none # say no out loud\\n`\n + 'Those numbers are the numbers of THIS run. wp-cleanup renumbers from a fresh scan every\\n'\n + 'time, so once anything in the list is deleted, re-run `pnpm wp-cleanup --report` (which\\n'\n + 'deletes nothing) and read the new numbers before passing numbers again.\\n'\n + 'Every delete is archived to a tag first; `recover=` lines land in\\n'\n + `${this.mutationLog.branchMutationLogPath(repoRoot)}\\n`;\n }\n\n // Parse `1,3` / `1 3` from a HUMAN at the prompt, ignoring anything out of range. An unparseable\n // answer selects nothing, which is the fail-safe direction for a typed answer to a delete question.\n // The `--delete-*` flags deliberately do NOT behave this way: a program passing an out-of-range\n // number is holding stale numbers, and that stops the run (see DeleteSelection.pick).\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 BranchMutationLog,\n BranchReaper,\n DeletableBranch,\n DeletableWorktree,\n ReapResult,\n ReapedBranch,\n RepoRootFinder,\n loadAndValidate,\n BRANCH_RETENTION_KEEP,\n CLASSIFICATION_SUPERSEDED,\n CLASSIFICATION_CONTENT_IN_MAIN,\n CLASSIFICATION_NEVER_PROPOSED,\n CLASSIFICATION_NO_COMMITS,\n CLASSIFICATION_MERGED_PR,\n CLASSIFICATION_BACKUP_OF_MERGED,\n CLASSIFICATION_BACKUP_OF_LIVE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\nimport {\n CleanupOptions,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n} from './cleanup-options';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Singular / plural as a pair, because every one of these strings is read by a human deciding whether\n// to delete something and `branch(s)` reads as a typo in the middle of exactly that sentence.\nconst BRANCH_KIND = 'branch';\nconst BRANCH_KINDS = 'branches';\nconst WORKTREE_KIND = 'worktree';\nconst WORKTREE_KINDS = 'worktrees';\n\n// The verdicts that reap themselves — a merged PR, or a snapshot of a ref that still exists. Named\n// here only so `--report` can SAY what a real run would take; the reaping itself is BranchReaper's.\nconst AUTO_REAPED_CLASSIFICATIONS: readonly string[] = [\n CLASSIFICATION_MERGED_PR,\n CLASSIFICATION_BACKUP_OF_MERGED,\n CLASSIFICATION_BACKUP_OF_LIVE,\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.\n//\n// CLASSIFICATION_NO_COMMITS has no entry and needs none: a zero-commit ref never reaches this block\n// any more — it is reaped on sight (see `husks`). A worktree spared for holding uncommitted work is\n// reported by the worktree section, with that as its reason, rather than offered as a question.\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};\n\n/**\n * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, REAP\n * THE ZERO-COMMIT HUSKS, then report everything that is left with the exact command that takes it.\n *\n * ─── WHY A ZERO-COMMIT REF IS REAPED AND NOT ASKED ABOUT ─────────────────────────────────────────\n * A ref with 0 unique commits is byte-identical to origin/main. Deleting it loses a NAME, not a\n * commit — and the name comes back with one `git checkout -b`, or from the archive tag this writes\n * first. It was previously PROMPTED about, for one real reason: a WORKING TREE looks exactly like\n * that husk from `git worktree add -b` until its first commit, i.e. for exactly the window an agent\n * is working in it. But that case is DETECTABLE, and detecting it is cheaper than asking a human:\n *\n * · the worktree holds uncommitted or untracked files (`git status --porcelain`) — spared, said out loud\n * · the worktree is LOCKED and that lock still stands — a reason naming somebody other than the\n * Claude Code harness, or a harness lock whose agent the harness itself still reports as\n * mid-tool-call — spared by the verdicts before it ever reaches here (see WorktreeLockVerdicts;\n * the pid in a harness lock reason proves nothing, it is the shared session process)\n * · it is the tree we are standing in, or a detached HEAD — spared, likewise\n *\n * So the bar moved from \"prove it is dead\" to \"prove somebody is holding it\", deliberately, because\n * the cost of being wrong is a re-`checkout -b` and the cost of being cautious was a prompt that\n * stopped a human's terminal to adjudicate two refs that could not possibly hold work. Refs that DO\n * carry unique commits keep every bit of the old caution.\n *\n * ─── WHY FLAGS, AND WHY THEY BEAT THE TTY SNIFF ──────────────────────────────────────────────────\n * `process.stdin.isTTY !== true` used to be the whole basis for \"is a human standing here?\", and it\n * is a proxy, not a fact: a human running `pnpm wp-cleanup | tee log` has no tty, an agent on a pty\n * has one. The sniff stays as the DEFAULT selector; `--delete-branches` / `--delete-worktrees` /\n * `--interactive` / `--report` let the caller who KNOWS say so, and an explicit flag always wins.\n * `--ignore-stale-locks` is the same idea for the OTHER thing this command could not previously be\n * told: that a standing claude-agent lock is stale. It replaces the `git worktree unlock` a human had\n * to run per directory — seven times, in the incident that produced it — before wp-cleanup would do\n * its job. A dirty worktree and a genuinely live agent's are still spared under it.\n *\n * ─── WHY A NON-TTY RUN NOW DELETES NOTHING IT WAS NOT ASKED TO ───────────────────────────────────\n * It used to silently take the \"redundant\" groups and print a bare list of names it left. That is\n * strictly less than the human sees, and — worse — it broke the one invariant that makes numeric\n * selection safe: THE NUMBERS IN THE FLAG ARE THE NUMBERS JUST PRINTED. A run that deletes half of a\n * numbered list renumbers the rest, so the `--delete-branches=3,4` it printed would land on entries\n * 5 and 6 next time. So the unattended path now prints the IDENTICAL numbered, classified block a\n * human sees plus the exact command that takes it, and takes nothing from that block itself. The\n * husks it does reap are never IN the block, so reaping them cannot shift a single number.\n *\n * All the danger still lives in the verdicts, not here — see BranchReaper for why every automatically\n * deleted branch is recoverable, and note that EVERY delete on every path here archives first.\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 // The log path is COMPUTED, never restated. It is per-worktree, so the relative literal these\n // messages used to carry named a file that does not exist in a linked worktree — the reader\n // greps nothing and reads the silence as \"nothing was deleted\".\n private readonly mutationLog: BranchMutationLog,\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(options: CleanupOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n if (options.report) {\n this.reportOnly(repoRoot, retention, options);\n return;\n }\n await this.cleanUpWorktrees(repoRoot, retention, options);\n await this.cleanUpBranches(repoRoot, retention, options);\n }\n\n /**\n * `--report`: the whole classified picture, and NOT ONE DELETE.\n *\n * This is the only run whose numbers are guaranteed still valid when the next command starts,\n * because a run that deletes nothing cannot renumber anything. That is what makes it the honest\n * first half of `--report` → read the numbers → `--delete-branches=1,3`.\n */\n private reportOnly(repoRoot: string, retention: string, options: CleanupOptions): void {\n process.stdout.write('\\n' + SEP + '🔎 wp-cleanup --report — nothing will be deleted\\n' + SEP);\n\n const verdicts = this.worktreeSection.verdicts(repoRoot, options.ignoreStaleLocks);\n const deadTrees = this.worktreeSection.provablyDead(verdicts);\n process.stdout.write(this.wouldReapBlock(\n WORKTREE_KINDS, deadTrees.map((tree: DeletableWorktree): string => `${tree.path} — ${tree.reason}`)));\n const treePromptable = this.worktreeSection.promptable(verdicts);\n const treeHusks = this.husks(treePromptable);\n process.stdout.write(this.wouldReapBlock(`zero-commit ${WORKTREE_KINDS}`,\n treeHusks.map((tree: DeletableWorktree): string => `${tree.path} [${tree.branch}]`)));\n process.stdout.write(this.worktreeSection.sparedBlock(verdicts, deadTrees));\n const treeBlock = this.rest(treePromptable, treeHusks);\n if (treeBlock.length > 0) {\n process.stdout.write(this.worktreeSection.promptBlock(treeBlock));\n process.stdout.write(this.flagHint(repoRoot, WORKTREE_KINDS, FLAG_DELETE_WORKTREES, treeBlock.length));\n }\n\n // Retention 'keep' turns BranchReaper.reap into a pure verdict computation — every branch\n // lands in `spared`, nothing is touched. That is exactly what a report needs, and it means\n // the classified block below is computed by the identical code path a real run uses, so the\n // numbering it prints IS the numbering the flag will act on.\n const branches = this.branchReaper.reap(repoRoot, 'wp-cleanup', null, BRANCH_RETENTION_KEEP).spared;\n process.stdout.write(this.wouldReapBlock(BRANCH_KINDS, branches\n .filter((entry: DeletableBranch): boolean =>\n AUTO_REAPED_CLASSIFICATIONS.includes(entry.classification))\n .map((entry: DeletableBranch): string => `${entry.branch} — ${entry.reason}`)));\n const promptable = this.promptable(branches);\n const husks = this.husks(promptable);\n process.stdout.write(this.wouldReapBlock(`zero-commit ${BRANCH_KINDS}`,\n husks.map((entry: DeletableBranch): string => entry.branch)));\n const block = this.rest(promptable, husks);\n if (block.length > 0) {\n process.stdout.write(this.classifiedBlock(block));\n process.stdout.write(this.flagHint(repoRoot, BRANCH_KINDS, FLAG_DELETE_BRANCHES, block.length));\n }\n process.stdout.write(`\\nNothing was deleted (--report). Retention policy in effect: ${retention}.\\n`\n + 'Re-run without --report to act, or with the flags above to say exactly what to take.\\n');\n }\n\n private wouldReapBlock(kinds: string, lines: string[]): string {\n if (lines.length === 0) return '';\n let out = `\\nWould reap ${String(lines.length)} ${kinds} — no question asked, each archived first:\\n`;\n for (const line of lines) out += ` ✓ ${line}\\n`;\n return out;\n }\n\n private async cleanUpBranches(repoRoot: string, retention: string, options: CleanupOptions): 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 const first = this.report(repoRoot, result);\n process.stdout.write(first);\n\n const promptable = this.promptable(result.spared);\n const husks = this.husks(promptable);\n const block = this.rest(promptable, husks);\n\n // Range-check the flag against the block BEFORE anything in this half is deleted, so a caller\n // holding stale numbers stops the run rather than half-executing it.\n options.branches.pick(block);\n\n if (husks.length > 0) {\n process.stdout.write(this.huskBlock(BRANCH_KINDS, husks.map(\n (entry: DeletableBranch): string => entry.branch)));\n process.stdout.write(this.report(repoRoot,\n this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', husks, retention)));\n }\n\n if (block.length === 0) {\n if (first === '' && husks.length === 0) process.stdout.write(this.nothingToDo());\n return;\n }\n process.stdout.write(this.classifiedBlock(block));\n const approved = await this.decide(\n repoRoot, block, BRANCH_KIND, BRANCH_KINDS, FLAG_DELETE_BRANCHES, options.branches, options);\n if (approved.length === 0) {\n process.stdout.write(\n `\\nNothing else deleted — the ${String(block.length)} ${BRANCH_KINDS} above were kept.\\n`);\n return;\n }\n process.stdout.write(this.report(repoRoot,\n this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n /**\n * Reap the provably-dead worktrees and the zero-commit husks, then decide about the rest.\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, options: CleanupOptions): Promise<void> {\n const verdicts = this.worktreeSection.verdicts(repoRoot, options.ignoreStaleLocks);\n const dead = this.worktreeSection.provablyDead(verdicts);\n if (dead.length > 0) {\n process.stdout.write(\n this.worktreeSection.report(repoRoot,\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 // A zero-commit worktree is a husk ONLY if nobody is holding it. Uncommitted or untracked\n // files are the one thing that says \"somebody is\", and they are the one thing no archive tag\n // can bring back — so they are checked here rather than left to git's refusal at removal time.\n const husks = this.worktreeSection.withoutUncommitted(this.husks(promptable));\n const block = this.rest(promptable, husks);\n\n options.worktrees.pick(block);\n\n if (husks.length > 0) {\n process.stdout.write(this.huskBlock(WORKTREE_KINDS, husks.map(\n (tree: DeletableWorktree): string => `${tree.path} [${tree.branch}]`)));\n process.stdout.write(\n this.worktreeSection.report(repoRoot,\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', husks, retention)));\n }\n\n if (block.length === 0) return;\n process.stdout.write(this.worktreeSection.promptBlock(block));\n const approved = await this.decide(\n repoRoot, block, WORKTREE_KIND, WORKTREE_KINDS, FLAG_DELETE_WORKTREES, options.worktrees, options);\n if (approved.length === 0) {\n process.stdout.write(\n `\\nNothing else removed — the ${String(block.length)} ${WORKTREE_KINDS} above were kept.\\n`);\n return;\n }\n process.stdout.write(\n this.worktreeSection.report(repoRoot,\n this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));\n }\n\n // What a bare run with an empty repo says. It points at --help because the flags are the whole\n // reason a caller who DOES have something to say never has to be prompted.\n private nothingToDo(): string {\n return '\\nNothing else to decide. `pnpm wp-cleanup --help` lists every flag '\n + '(--report, --delete-branches, --delete-worktrees, --interactive).\\n';\n }\n\n private huskBlock(kinds: string, names: string[]): string {\n let out = '\\n' + SEP\n + `♻️ Reaping ${String(names.length)} zero-commit ${kinds} — 0 unique commits, identical to origin/main\\n`\n + SEP + '\\n'\n + 'Nothing is held here: no uncommitted files, no live lock, not the tree you are standing in.\\n'\n + 'Deleting one of these loses a NAME, not a commit — and the name is archived to a tag first.\\n\\n';\n for (const name of names) out += ` · ${name}\\n`;\n return out;\n }\n\n private report(repoRoot: string, result: ReapResult): string {\n const gone = result.alreadyGone.length;\n if (result.reaped.length === 0 && result.failed.length === 0 && gone === 0) return '';\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\\n${this.mutationLog.branchMutationLogPath(repoRoot)} —\\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 ADJUDICATED_CLASSIFICATIONS) {\n for (const entry of spared) {\n if (entry.classification === classification) out.push(entry);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit husks out of a promptable list — reaped by default, and never numbered.\n *\n * Keeping them OUT of the numbered block is what keeps `--delete-branches=1,3` honest: whatever\n * this reaps, the entries the caller can name are the same entries in the same order as on the\n * `--report` run that printed those numbers.\n *\n * THE CLASSIFICATION TOKEN IS THE ONLY SPELLING OF \"husk\". It would read as belt-and-braces to\n * also accept `commits === 0` here, and it is the opposite: MergedBranchesService.classify tests\n * commits===0 BEFORE anything else, so a second test can only ever disagree with the verdict — and\n * disagreeing in the widening direction, auto-reaping something no classification called a husk.\n */\n private husks<T extends DeletableBranch | DeletableWorktree>(adjudicated: T[]): T[] {\n return adjudicated.filter(\n (entry: T): boolean => entry.classification === CLASSIFICATION_NO_COMMITS);\n }\n\n private rest<T extends DeletableBranch | DeletableWorktree>(promptable: T[], husks: T[]): T[] {\n return promptable.filter((entry: T): boolean => !husks.includes(entry));\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 * Who decides about the numbered block, in priority order:\n *\n * 1. an explicit `--delete-branches` / `--delete-worktrees` — the caller said so, and a caller who\n * said so is never overruled by what stdin happens to be attached to;\n * 2. a terminal, or `--interactive` — ask;\n * 3. otherwise — take NOTHING, and print the exact command that takes it.\n *\n * (3) is the case an agent lands in, and it is deliberately empty-handed. See the class comment:\n * a run that deletes part of a numbered list invalidates the numbers it just printed, and those\n * numbers are the whole interface the next run is given.\n */\n private async decide<T extends DeletableBranch | DeletableWorktree>(\n repoRoot: string, block: T[], kind: string, kinds: string, flag: string,\n selection: DeleteSelection, options: CleanupOptions,\n ): Promise<T[]> {\n if (selection.given()) {\n const picked = selection.pick(block);\n process.stdout.write(`\\n${flag} chose ${String(picked.length)} of the ${String(block.length)} `\n + `${kinds} above.\\n`);\n if (picked.length < block.length) process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));\n return picked;\n }\n if (!options.prompts()) {\n process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));\n return [];\n }\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 block;\n return this.pickByNumber(block, answer);\n }\n\n /**\n * The exact command that takes what this run left — the half a non-tty caller never used to get.\n *\n * It states the numbering contract out loud because a shifted number is the one way this command\n * can delete the wrong ref, and the caller reading this is usually a program.\n */\n private flagHint(repoRoot: string, kinds: string, flag: string, count: number): string {\n return `\\nLeft for you to decide (${String(count)} ${kinds}, numbered above).\\n`\n + 'To delete them yourself, re-run with:\\n'\n + ` pnpm wp-cleanup ${flag}=all # every one listed above\\n`\n + ` pnpm wp-cleanup ${flag}=1,3 # just those, BY THE NUMBERS PRINTED ABOVE\\n`\n + ` pnpm wp-cleanup ${flag}=none # say no out loud\\n`\n + 'Those numbers are the numbers of THIS run. wp-cleanup renumbers from a fresh scan every\\n'\n + 'time, so once anything in the list is deleted, re-run `pnpm wp-cleanup --report` (which\\n'\n + 'deletes nothing) and read the new numbers before passing numbers again.\\n'\n + 'Every delete is archived to a tag first; `recover=` lines land in\\n'\n + `${this.mutationLog.branchMutationLogPath(repoRoot)}\\n`;\n }\n\n // Parse `1,3` / `1 3` from a HUMAN at the prompt, ignoring anything out of range. An unparseable\n // answer selects nothing, which is the fail-safe direction for a typed answer to a delete question.\n // The `--delete-*` flags deliberately do NOT behave this way: a program passing an out-of-range\n // number is holding stale numbers, and that stops the run (see DeleteSelection.pick).\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"]}
@@ -12,6 +12,7 @@ export declare const FLAG_DELETE_BRANCHES = "--delete-branches";
12
12
  export declare const FLAG_DELETE_WORKTREES = "--delete-worktrees";
13
13
  export declare const FLAG_REPORT = "--report";
14
14
  export declare const FLAG_INTERACTIVE = "--interactive";
15
+ export declare const FLAG_IGNORE_STALE_LOCKS = "--ignore-stale-locks";
15
16
  export declare const SELECTION_UNSET = "unset";
16
17
  export declare const SELECTION_ALL = "all";
17
18
  export declare const SELECTION_NONE = "none";
@@ -62,13 +63,21 @@ export declare class DeleteSelection {
62
63
  * was never a fact about who is standing there, only a proxy: a human running
63
64
  * `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the
64
65
  * DEFAULT, and any explicit flag beats it.
66
+ *
67
+ * `ignoreStaleLocks` is the escape hatch for the ONE thing wp-cleanup could not previously express:
68
+ * "that agent lock is stale". Every locked worktree is then classified on its real branch and commit
69
+ * state, and the ones that come back deletable are `git worktree unlock`ed on the way out. It replaces
70
+ * the hand-run `git worktree unlock` per directory that a human had to do seven times before the tool
71
+ * would clean up at all. Two things it does NOT override, by design: a worktree with uncommitted or
72
+ * untracked files, and a lock whose agent the harness still reports as running.
65
73
  */
66
74
  export declare class CleanupOptions {
67
75
  readonly branches: DeleteSelection;
68
76
  readonly worktrees: DeleteSelection;
69
77
  readonly report: boolean;
70
78
  readonly interactive: boolean;
71
- constructor(branches: DeleteSelection, worktrees: DeleteSelection, report: boolean, interactive: boolean);
79
+ readonly ignoreStaleLocks: boolean;
80
+ constructor(branches: DeleteSelection, worktrees: DeleteSelection, report: boolean, interactive: boolean, ignoreStaleLocks: boolean);
72
81
  /**
73
82
  * Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.
74
83
  * A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CleanupUsage = exports.CleanupOptions = exports.DeleteSelection = exports.SELECTION_NUMBERS = exports.SELECTION_NONE = exports.SELECTION_ALL = exports.SELECTION_UNSET = exports.FLAG_INTERACTIVE = exports.FLAG_REPORT = exports.FLAG_DELETE_WORKTREES = exports.FLAG_DELETE_BRANCHES = void 0;
3
+ exports.CleanupUsage = exports.CleanupOptions = exports.DeleteSelection = exports.SELECTION_NUMBERS = exports.SELECTION_NONE = exports.SELECTION_ALL = exports.SELECTION_UNSET = exports.FLAG_IGNORE_STALE_LOCKS = exports.FLAG_INTERACTIVE = exports.FLAG_REPORT = exports.FLAG_DELETE_WORKTREES = exports.FLAG_DELETE_BRANCHES = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const rules_config_1 = require("@webpieces/rules-config");
6
6
  const inversify_1 = require("inversify");
@@ -19,6 +19,7 @@ exports.FLAG_DELETE_BRANCHES = '--delete-branches';
19
19
  exports.FLAG_DELETE_WORKTREES = '--delete-worktrees';
20
20
  exports.FLAG_REPORT = '--report';
21
21
  exports.FLAG_INTERACTIVE = '--interactive';
22
+ exports.FLAG_IGNORE_STALE_LOCKS = '--ignore-stale-locks';
22
23
  // What a `--delete-*` flag said. UNSET is "the flag was not passed at all" and is a genuinely
23
24
  // different answer from NONE: unset defers to the tty sniff, NONE is a caller saying no out loud.
24
25
  exports.SELECTION_UNSET = 'unset';
@@ -123,20 +124,30 @@ exports.DeleteSelection = DeleteSelection;
123
124
  * was never a fact about who is standing there, only a proxy: a human running
124
125
  * `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the
125
126
  * DEFAULT, and any explicit flag beats it.
127
+ *
128
+ * `ignoreStaleLocks` is the escape hatch for the ONE thing wp-cleanup could not previously express:
129
+ * "that agent lock is stale". Every locked worktree is then classified on its real branch and commit
130
+ * state, and the ones that come back deletable are `git worktree unlock`ed on the way out. It replaces
131
+ * the hand-run `git worktree unlock` per directory that a human had to do seven times before the tool
132
+ * would clean up at all. Two things it does NOT override, by design: a worktree with uncommitted or
133
+ * untracked files, and a lock whose agent the harness still reports as running.
126
134
  */
127
135
  class CleanupOptions {
128
136
  branches;
129
137
  worktrees;
130
138
  report;
131
139
  interactive;
140
+ ignoreStaleLocks;
132
141
  // Every parameter REQUIRED, no defaults — same reasoning as BuildOptions: a defaulted parameter
133
142
  // means a call site written before this class grew a field silently keeps the old behaviour, and
134
143
  // the old behaviour here is "delete without being told to".
135
- constructor(branches, worktrees, report, interactive) {
144
+ // eslint-disable-next-line @typescript-eslint/max-params
145
+ constructor(branches, worktrees, report, interactive, ignoreStaleLocks) {
136
146
  this.branches = branches;
137
147
  this.worktrees = worktrees;
138
148
  this.report = report;
139
149
  this.interactive = interactive;
150
+ this.ignoreStaleLocks = ignoreStaleLocks;
140
151
  }
141
152
  /**
142
153
  * Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.
@@ -164,6 +175,11 @@ let CleanupUsage = class CleanupUsage {
164
175
  new rules_config_1.CliFlag(exports.FLAG_REPORT, 'Print the full classified report and exit. Deletes NOTHING — the only\n'
165
176
  + ' run whose numbers are still valid for the next command.'),
166
177
  new rules_config_1.CliFlag(exports.FLAG_INTERACTIVE, 'Prompt even when stdin is not a terminal.'),
178
+ new rules_config_1.CliFlag(exports.FLAG_IGNORE_STALE_LOCKS, 'Treat a standing claude-agent worktree lock as no evidence:\n'
179
+ + ' classify every locked worktree on its real branch and\n'
180
+ + ' commit state, and unlock the ones that come back dead.\n'
181
+ + ' A DIRTY worktree, and one whose agent the harness still\n'
182
+ + ' reports as running, are spared anyway.'),
167
183
  ]);
168
184
  }
169
185
  };
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup-options.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-options.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, CliFlag, CliUsage } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * `wp-cleanup`'s flags, parsed — the whole decision about what this run deletes, as a value.\n *\n * WHY A CLASS AND NOT A BAG OF BOOLEANS THREADED THROUGH ARGUMENTS: the decision \"delete these\n * worktrees / those branches / nothing at all\" is ONE thing, made once at the bin, and every method\n * downstream has to agree about it. Passed as separate parameters it becomes four independent\n * arguments that a new call site can get half-right — and half-right, here, means deleting a ref\n * nobody chose. Same argument BuildOptions makes for its single `force` field.\n */\n\n// The flag names, shared by the bin (which declares them to CliArgs) and by the parser below, so the\n// spelling a caller is told about and the spelling that is honoured can never drift apart.\nexport const FLAG_DELETE_BRANCHES = '--delete-branches';\nexport const FLAG_DELETE_WORKTREES = '--delete-worktrees';\nexport const FLAG_REPORT = '--report';\nexport const FLAG_INTERACTIVE = '--interactive';\n\n// What a `--delete-*` flag said. UNSET is \"the flag was not passed at all\" and is a genuinely\n// different answer from NONE: unset defers to the tty sniff, NONE is a caller saying no out loud.\nexport const SELECTION_UNSET = 'unset';\nexport const SELECTION_ALL = 'all';\nexport const SELECTION_NONE = 'none';\nexport const SELECTION_NUMBERS = 'numbers';\n\n/**\n * One `--delete-branches=` / `--delete-worktrees=` answer: `all`, `none`, or the numbers printed in\n * the classified block on this run.\n *\n * THE NUMBERING CONTRACT: `numbers` index the block wp-cleanup just printed, 1-based, and an index\n * outside that block is a hard failure rather than a silent skip. A number that lands on the wrong\n * ref is the single way this command can delete something nobody asked for, so \"the list moved under\n * me\" must stop the run, not quietly delete four of the five refs the caller meant.\n */\nexport class DeleteSelection {\n readonly mode: string;\n readonly numbers: readonly number[];\n private readonly flag: string;\n\n /**\n * ONE constructor, taking argv's two facts about the flag: was it there, and what did it carry.\n *\n * Parsing in the constructor rather than in a static factory is what keeps this to one spelling —\n * there is no way to hold a DeleteSelection that was never checked. `present === false` is the\n * absent flag (the run falls back to its tty sniff); present with an EMPTY value is an error, not\n * an implicit `all` and not an implicit `none`: both readings are defensible, which is exactly\n * why neither may be guessed at when the outcome is a delete.\n */\n constructor(flag: string, present: boolean, raw: string) {\n this.flag = flag;\n this.numbers = [];\n if (!present) {\n this.mode = SELECTION_UNSET;\n return;\n }\n const value = raw.trim().toLowerCase();\n if (value === '') throw this.usage('a value is required.');\n if (value === SELECTION_ALL || value === SELECTION_NONE) {\n this.mode = value;\n return;\n }\n this.mode = SELECTION_NUMBERS;\n this.numbers = this.parseNumbers(value, raw);\n }\n\n private parseNumbers(value: string, raw: string): number[] {\n const numbers: number[] = [];\n for (const token of value.split(/[\\s,]+/)) {\n if (token === '') continue;\n const index = Number(token);\n if (!Number.isInteger(index) || index < 1) {\n throw this.usage(`'${raw}' — '${token}' is not one of the numbers printed.`);\n }\n numbers.push(index);\n }\n if (numbers.length === 0) throw this.usage(`'${raw}' named no numbers.`);\n return numbers;\n }\n\n /** Did the caller say anything? An explicit flag ALWAYS beats the terminal sniff. */\n given(): boolean {\n return this.mode !== SELECTION_UNSET;\n }\n\n /**\n * The chosen entries out of the block that was just printed.\n *\n * Range is checked against THAT block: a number past its end means the caller is holding numbers\n * from an older run, and the refs have moved under them. That stops the run.\n */\n pick<T>(block: readonly T[]): T[] {\n if (this.mode === SELECTION_ALL) return [...block];\n if (this.mode === SELECTION_NONE || this.mode === SELECTION_UNSET) return [];\n const out: T[] = [];\n for (const index of this.numbers) {\n if (index > block.length) {\n throw this.usage(\n `it names [${String(index)}], but the list above has only ${String(block.length)} entr(ies).\\n`\n + 'Those numbers came from a different run. Re-run `pnpm wp-cleanup --report`, read the\\n'\n + 'numbers it prints, and pass those — nothing was deleted from that list.');\n }\n out.push(block[index - 1]);\n }\n return out;\n }\n\n private usage(detail: string): CliExitError {\n return new CliExitError(2,\n `❌ ${this.flag}: ${detail}\\n\\n`\n + `Usage: ${this.flag}=all | ${this.flag}=none | ${this.flag}=1,3\\n`\n + 'The numbers are the ones wp-cleanup printed in the classified block on the SAME run.');\n }\n}\n\n/**\n * Everything argv said about this cleanup run.\n *\n * `report` prints the full classified report and deletes NOTHING — the print-and-exit case, and the\n * one honest way to get numbers that are still valid on the next command, because a run that deletes\n * nothing cannot renumber anything.\n *\n * `interactive` forces the prompt when stdin is not a tty. It exists because `process.stdin.isTTY`\n * was never a fact about who is standing there, only a proxy: a human running\n * `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the\n * DEFAULT, and any explicit flag beats it.\n */\nexport class CleanupOptions {\n readonly branches: DeleteSelection;\n readonly worktrees: DeleteSelection;\n readonly report: boolean;\n readonly interactive: boolean;\n\n // Every parameter REQUIRED, no defaults — same reasoning as BuildOptions: a defaulted parameter\n // means a call site written before this class grew a field silently keeps the old behaviour, and\n // the old behaviour here is \"delete without being told to\".\n constructor(\n branches: DeleteSelection,\n worktrees: DeleteSelection,\n report: boolean,\n interactive: boolean,\n ) {\n this.branches = branches;\n this.worktrees = worktrees;\n this.report = report;\n this.interactive = interactive;\n }\n\n /**\n * Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.\n * A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.\n */\n prompts(): boolean {\n return this.interactive || process.stdin.isTTY === true;\n }\n}\n\n/**\n * What `wp-cleanup --help` prints, and the ONE list CliArgs validates argv against.\n *\n * It lives here rather than inline in the bin so a spec can assert that every flag this command\n * honours is a flag it also tells you about. A flag that works but is undocumented, or documented but\n * rejected, is the same defect from either side — and the bin itself is a `runMain` call that cannot\n * be imported into a test without executing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanupUsage {\n declare(): CliUsage {\n return new CliUsage(\n 'wp-cleanup',\n 'Remove worktrees and branches that are provably dead, reap the zero-commit husks, and report the rest.',\n [\n new CliFlag(FLAG_DELETE_BRANCHES,\n 'all | none | 1,3 — which of the classified BRANCHES to delete. The\\n'\n + ' numbers are the ones printed in the same run\\'s block.', true),\n new CliFlag(FLAG_DELETE_WORKTREES,\n 'all | none | 1,3 — the same, for the classified WORKTREES.', true),\n new CliFlag(FLAG_REPORT,\n 'Print the full classified report and exit. Deletes NOTHING — the only\\n'\n + ' run whose numbers are still valid for the next command.'),\n new CliFlag(FLAG_INTERACTIVE,\n 'Prompt even when stdin is not a terminal.'),\n ]);\n }\n}\n"]}
1
+ {"version":3,"file":"cleanup-options.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-options.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliExitError, CliFlag, CliUsage } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * `wp-cleanup`'s flags, parsed — the whole decision about what this run deletes, as a value.\n *\n * WHY A CLASS AND NOT A BAG OF BOOLEANS THREADED THROUGH ARGUMENTS: the decision \"delete these\n * worktrees / those branches / nothing at all\" is ONE thing, made once at the bin, and every method\n * downstream has to agree about it. Passed as separate parameters it becomes four independent\n * arguments that a new call site can get half-right — and half-right, here, means deleting a ref\n * nobody chose. Same argument BuildOptions makes for its single `force` field.\n */\n\n// The flag names, shared by the bin (which declares them to CliArgs) and by the parser below, so the\n// spelling a caller is told about and the spelling that is honoured can never drift apart.\nexport const FLAG_DELETE_BRANCHES = '--delete-branches';\nexport const FLAG_DELETE_WORKTREES = '--delete-worktrees';\nexport const FLAG_REPORT = '--report';\nexport const FLAG_INTERACTIVE = '--interactive';\nexport const FLAG_IGNORE_STALE_LOCKS = '--ignore-stale-locks';\n\n// What a `--delete-*` flag said. UNSET is \"the flag was not passed at all\" and is a genuinely\n// different answer from NONE: unset defers to the tty sniff, NONE is a caller saying no out loud.\nexport const SELECTION_UNSET = 'unset';\nexport const SELECTION_ALL = 'all';\nexport const SELECTION_NONE = 'none';\nexport const SELECTION_NUMBERS = 'numbers';\n\n/**\n * One `--delete-branches=` / `--delete-worktrees=` answer: `all`, `none`, or the numbers printed in\n * the classified block on this run.\n *\n * THE NUMBERING CONTRACT: `numbers` index the block wp-cleanup just printed, 1-based, and an index\n * outside that block is a hard failure rather than a silent skip. A number that lands on the wrong\n * ref is the single way this command can delete something nobody asked for, so \"the list moved under\n * me\" must stop the run, not quietly delete four of the five refs the caller meant.\n */\nexport class DeleteSelection {\n readonly mode: string;\n readonly numbers: readonly number[];\n private readonly flag: string;\n\n /**\n * ONE constructor, taking argv's two facts about the flag: was it there, and what did it carry.\n *\n * Parsing in the constructor rather than in a static factory is what keeps this to one spelling —\n * there is no way to hold a DeleteSelection that was never checked. `present === false` is the\n * absent flag (the run falls back to its tty sniff); present with an EMPTY value is an error, not\n * an implicit `all` and not an implicit `none`: both readings are defensible, which is exactly\n * why neither may be guessed at when the outcome is a delete.\n */\n constructor(flag: string, present: boolean, raw: string) {\n this.flag = flag;\n this.numbers = [];\n if (!present) {\n this.mode = SELECTION_UNSET;\n return;\n }\n const value = raw.trim().toLowerCase();\n if (value === '') throw this.usage('a value is required.');\n if (value === SELECTION_ALL || value === SELECTION_NONE) {\n this.mode = value;\n return;\n }\n this.mode = SELECTION_NUMBERS;\n this.numbers = this.parseNumbers(value, raw);\n }\n\n private parseNumbers(value: string, raw: string): number[] {\n const numbers: number[] = [];\n for (const token of value.split(/[\\s,]+/)) {\n if (token === '') continue;\n const index = Number(token);\n if (!Number.isInteger(index) || index < 1) {\n throw this.usage(`'${raw}' — '${token}' is not one of the numbers printed.`);\n }\n numbers.push(index);\n }\n if (numbers.length === 0) throw this.usage(`'${raw}' named no numbers.`);\n return numbers;\n }\n\n /** Did the caller say anything? An explicit flag ALWAYS beats the terminal sniff. */\n given(): boolean {\n return this.mode !== SELECTION_UNSET;\n }\n\n /**\n * The chosen entries out of the block that was just printed.\n *\n * Range is checked against THAT block: a number past its end means the caller is holding numbers\n * from an older run, and the refs have moved under them. That stops the run.\n */\n pick<T>(block: readonly T[]): T[] {\n if (this.mode === SELECTION_ALL) return [...block];\n if (this.mode === SELECTION_NONE || this.mode === SELECTION_UNSET) return [];\n const out: T[] = [];\n for (const index of this.numbers) {\n if (index > block.length) {\n throw this.usage(\n `it names [${String(index)}], but the list above has only ${String(block.length)} entr(ies).\\n`\n + 'Those numbers came from a different run. Re-run `pnpm wp-cleanup --report`, read the\\n'\n + 'numbers it prints, and pass those — nothing was deleted from that list.');\n }\n out.push(block[index - 1]);\n }\n return out;\n }\n\n private usage(detail: string): CliExitError {\n return new CliExitError(2,\n `❌ ${this.flag}: ${detail}\\n\\n`\n + `Usage: ${this.flag}=all | ${this.flag}=none | ${this.flag}=1,3\\n`\n + 'The numbers are the ones wp-cleanup printed in the classified block on the SAME run.');\n }\n}\n\n/**\n * Everything argv said about this cleanup run.\n *\n * `report` prints the full classified report and deletes NOTHING — the print-and-exit case, and the\n * one honest way to get numbers that are still valid on the next command, because a run that deletes\n * nothing cannot renumber anything.\n *\n * `interactive` forces the prompt when stdin is not a tty. It exists because `process.stdin.isTTY`\n * was never a fact about who is standing there, only a proxy: a human running\n * `pnpm wp-cleanup | tee log` has no tty, and an agent on a pty has one. The sniff stays as the\n * DEFAULT, and any explicit flag beats it.\n *\n * `ignoreStaleLocks` is the escape hatch for the ONE thing wp-cleanup could not previously express:\n * \"that agent lock is stale\". Every locked worktree is then classified on its real branch and commit\n * state, and the ones that come back deletable are `git worktree unlock`ed on the way out. It replaces\n * the hand-run `git worktree unlock` per directory that a human had to do seven times before the tool\n * would clean up at all. Two things it does NOT override, by design: a worktree with uncommitted or\n * untracked files, and a lock whose agent the harness still reports as running.\n */\nexport class CleanupOptions {\n readonly branches: DeleteSelection;\n readonly worktrees: DeleteSelection;\n readonly report: boolean;\n readonly interactive: boolean;\n readonly ignoreStaleLocks: boolean;\n\n // Every parameter REQUIRED, no defaults — same reasoning as BuildOptions: a defaulted parameter\n // means a call site written before this class grew a field silently keeps the old behaviour, and\n // the old behaviour here is \"delete without being told to\".\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n branches: DeleteSelection,\n worktrees: DeleteSelection,\n report: boolean,\n interactive: boolean,\n ignoreStaleLocks: boolean,\n ) {\n this.branches = branches;\n this.worktrees = worktrees;\n this.report = report;\n this.interactive = interactive;\n this.ignoreStaleLocks = ignoreStaleLocks;\n }\n\n /**\n * Does this run get to ASK? A tty is the default evidence; `--interactive` says so outright.\n * A `--delete-*` flag overrides both for the half it names — see `CleanupCommand.decide`.\n */\n prompts(): boolean {\n return this.interactive || process.stdin.isTTY === true;\n }\n}\n\n/**\n * What `wp-cleanup --help` prints, and the ONE list CliArgs validates argv against.\n *\n * It lives here rather than inline in the bin so a spec can assert that every flag this command\n * honours is a flag it also tells you about. A flag that works but is undocumented, or documented but\n * rejected, is the same defect from either side — and the bin itself is a `runMain` call that cannot\n * be imported into a test without executing.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CleanupUsage {\n declare(): CliUsage {\n return new CliUsage(\n 'wp-cleanup',\n 'Remove worktrees and branches that are provably dead, reap the zero-commit husks, and report the rest.',\n [\n new CliFlag(FLAG_DELETE_BRANCHES,\n 'all | none | 1,3 — which of the classified BRANCHES to delete. The\\n'\n + ' numbers are the ones printed in the same run\\'s block.', true),\n new CliFlag(FLAG_DELETE_WORKTREES,\n 'all | none | 1,3 — the same, for the classified WORKTREES.', true),\n new CliFlag(FLAG_REPORT,\n 'Print the full classified report and exit. Deletes NOTHING — the only\\n'\n + ' run whose numbers are still valid for the next command.'),\n new CliFlag(FLAG_INTERACTIVE,\n 'Prompt even when stdin is not a terminal.'),\n new CliFlag(FLAG_IGNORE_STALE_LOCKS,\n 'Treat a standing claude-agent worktree lock as no evidence:\\n'\n + ' classify every locked worktree on its real branch and\\n'\n + ' commit state, and unlock the ones that come back dead.\\n'\n + ' A DIRTY worktree, and one whose agent the harness still\\n'\n + ' reports as running, are spared anyway.'),\n ]);\n }\n}\n"]}
@@ -74,7 +74,9 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
74
74
  */
75
75
  resolveTarget(repoRoot, request) {
76
76
  const wanted = path.resolve(request.worktreePath);
77
- const target = this.worktreeSection.verdicts(repoRoot)
77
+ // `false`: this reaps ONE named worktree after its PR landed, and it is not the place to
78
+ // override anybody's lock — a lock still standing here is exactly the refusal this prints.
79
+ const target = this.worktreeSection.verdicts(repoRoot, false)
78
80
  .find((tree) => path.resolve(tree.path) === wanted);
79
81
  if (target === undefined) {
80
82
  process.stdout.write(`\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\n`
@@ -1 +1 @@
1
- {"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(repoRoot, result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n const target = this.worktreeSection.verdicts(repoRoot)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(repoRoot, result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
1
+ {"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(repoRoot, result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n // `false`: this reaps ONE named worktree after its PR landed, and it is not the place to\n // override anybody's lock — a lock still standing here is exactly the refusal this prints.\n const target = this.worktreeSection.verdicts(repoRoot, false)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(repoRoot, result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
@@ -25,7 +25,7 @@ export declare class WorktreeCleanupSection {
25
25
  * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
26
26
  * for removing a directory, since the tree may have gained uncommitted work since it was written.
27
27
  */
28
- verdicts(repoRoot: string): DeletableWorktree[];
28
+ verdicts(repoRoot: string, ignoreStaleLocks: boolean): DeletableWorktree[];
29
29
  provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[];
30
30
  /**
31
31
  * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same
@@ -56,7 +56,15 @@ export declare class WorktreeCleanupSection {
56
56
  reap(repoRoot: string, verb: MutationVerb, targets: DeletableWorktree[], retention: string): WorktreeReapResult;
57
57
  report(repoRoot: string, result: WorktreeReapResult): string;
58
58
  private reapedLine;
59
- /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */
59
+ /**
60
+ * The spared worktrees, with WHY — including the ones nobody will ever be asked about.
61
+ *
62
+ * When any of them is a claude-agent lock whose agent could not be verified either way, the block
63
+ * ends with the flag that judges it on its branch instead. That hint lives HERE and not in the
64
+ * verdict's own reason for one reason: the flag is wp-cleanup's, its spelling is one constant in
65
+ * this package, and a rules-config message naming it would be a second copy of that spelling free
66
+ * to drift. The reason says what is KNOWN; this says what you can DO about it.
67
+ */
60
68
  sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string;
61
69
  private isMechanical;
62
70
  promptBlock(promptable: DeletableWorktree[]): string;
@@ -4,6 +4,7 @@ exports.WorktreeCleanupSection = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const rules_config_1 = require("@webpieces/rules-config");
6
6
  const inversify_1 = require("inversify");
7
+ const cleanup_options_1 = require("./cleanup-options");
7
8
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
8
9
  /**
9
10
  * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.
@@ -39,8 +40,8 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
39
40
  * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
40
41
  * for removing a directory, since the tree may have gained uncommitted work since it was written.
41
42
  */
42
- verdicts(repoRoot) {
43
- return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;
43
+ verdicts(repoRoot, ignoreStaleLocks) {
44
+ return this.mergedBranches.computeMergedBranches(repoRoot, ignoreStaleLocks).worktrees;
44
45
  }
45
46
  provablyDead(verdicts) {
46
47
  return verdicts.filter((tree) => tree.deletable);
@@ -133,7 +134,15 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
133
134
  : '';
134
135
  return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\n`;
135
136
  }
136
- /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */
137
+ /**
138
+ * The spared worktrees, with WHY — including the ones nobody will ever be asked about.
139
+ *
140
+ * When any of them is a claude-agent lock whose agent could not be verified either way, the block
141
+ * ends with the flag that judges it on its branch instead. That hint lives HERE and not in the
142
+ * verdict's own reason for one reason: the flag is wp-cleanup's, its spelling is one constant in
143
+ * this package, and a rules-config message naming it would be a second copy of that spelling free
144
+ * to drift. The reason says what is KNOWN; this says what you can DO about it.
145
+ */
137
146
  sparedBlock(verdicts, removed) {
138
147
  const gone = new Set(removed.map((tree) => tree.path));
139
148
  const spared = verdicts.filter((tree) => !tree.deletable && !gone.has(tree.path)
@@ -141,8 +150,17 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
141
150
  if (spared.length === 0)
142
151
  return '';
143
152
  let out = '\nWorktrees deliberately left alone:\n';
144
- for (const tree of spared)
153
+ let unverified = 0;
154
+ for (const tree of spared) {
145
155
  out += ` · ${tree.path} — ${tree.reason}\n`;
156
+ if (tree.reason.includes(rules_config_1.LOCK_LIVENESS_UNVERIFIABLE))
157
+ unverified += 1;
158
+ }
159
+ if (unverified > 0) {
160
+ out += `\n${String(unverified)} of those carry a claude-agent lock whose liveness cannot be `
161
+ + 'verified.\nTo judge those on their branch and commits alone — unlocking and removing only\n'
162
+ + `the ones that come back dead — re-run:\n pnpm wp-cleanup ${cleanup_options_1.FLAG_IGNORE_STALE_LOCKS}\n`;
163
+ }
146
164
  return out;
147
165
  }
148
166
  isMechanical(classification) {
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchMutationLog,\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n WorktreeService,\n WorktreeWorkInFlight,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n private readonly worktreeService: WorktreeService,\n // COMPUTED, not restated: the branch-mutation log is per-worktree, so the relative literal this\n // message used to carry named a file that does not exist in a linked worktree.\n private readonly mutationLog: BranchMutationLog,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of ADJUDICATED_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or\n * untracked work — with a printed line for each one that is spared.\n *\n * THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits\n * of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed\n * in the last twenty minutes, and the two are indistinguishable by ref alone. `git status\n * --porcelain` is the difference, it is one local spawn, and it fails safe to \"dirty\".\n *\n * It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and\n * git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the\n * backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody\n * was asked about, which is why the husk path states the spare instead of tripping over it.\n */\n withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[] {\n const clean: DeletableWorktree[] = [];\n let spared = '';\n for (const tree of husks) {\n const held = this.workInFlight(tree.path);\n if (held.held) {\n // The REASON is printed verbatim, because \"it has uncommitted files\" and \"git would\n // not tell me\" send an operator to two different places.\n spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\\n`\n + ' nothing archives that, so it is left exactly where it is\\n';\n continue;\n }\n clean.push(tree);\n }\n if (spared !== '') {\n process.stdout.write('\\nZero-commit worktrees SPARED because work may be in flight in them:\\n' + spared);\n }\n return clean;\n }\n\n // Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no\n // real worktrees on disk.\n protected workInFlight(worktreePath: string): WorktreeWorkInFlight {\n return this.worktreeService.workInFlight(worktreePath);\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\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)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += `\\nEvery removal is logged (phase REAP_WORKTREE) in\\n${this.mutationLog.branchMutationLogPath(repoRoot)}\\n`\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
1
+ {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BranchMutationLog,\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n WorktreeService,\n WorktreeWorkInFlight,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n ADJUDICATED_CLASSIFICATIONS,\n LOCK_LIVENESS_UNVERIFIABLE,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { FLAG_IGNORE_STALE_LOCKS } from './cleanup-options';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n private readonly worktreeService: WorktreeService,\n // COMPUTED, not restated: the branch-mutation log is per-worktree, so the relative literal this\n // message used to carry named a file that does not exist in a linked worktree.\n private readonly mutationLog: BranchMutationLog,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string, ignoreStaleLocks: boolean): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot, ignoreStaleLocks).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of ADJUDICATED_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or\n * untracked work — with a printed line for each one that is spared.\n *\n * THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits\n * of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed\n * in the last twenty minutes, and the two are indistinguishable by ref alone. `git status\n * --porcelain` is the difference, it is one local spawn, and it fails safe to \"dirty\".\n *\n * It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and\n * git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the\n * backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody\n * was asked about, which is why the husk path states the spare instead of tripping over it.\n */\n withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[] {\n const clean: DeletableWorktree[] = [];\n let spared = '';\n for (const tree of husks) {\n const held = this.workInFlight(tree.path);\n if (held.held) {\n // The REASON is printed verbatim, because \"it has uncommitted files\" and \"git would\n // not tell me\" send an operator to two different places.\n spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\\n`\n + ' nothing archives that, so it is left exactly where it is\\n';\n continue;\n }\n clean.push(tree);\n }\n if (spared !== '') {\n process.stdout.write('\\nZero-commit worktrees SPARED because work may be in flight in them:\\n' + spared);\n }\n return clean;\n }\n\n // Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no\n // real worktrees on disk.\n protected workInFlight(worktreePath: string): WorktreeWorkInFlight {\n return this.worktreeService.workInFlight(worktreePath);\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\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)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += `\\nEvery removal is logged (phase REAP_WORKTREE) in\\n${this.mutationLog.branchMutationLogPath(repoRoot)}\\n`\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /**\n * The spared worktrees, with WHY — including the ones nobody will ever be asked about.\n *\n * When any of them is a claude-agent lock whose agent could not be verified either way, the block\n * ends with the flag that judges it on its branch instead. That hint lives HERE and not in the\n * verdict's own reason for one reason: the flag is wp-cleanup's, its spelling is one constant in\n * this package, and a rules-config message naming it would be a second copy of that spelling free\n * to drift. The reason says what is KNOWN; this says what you can DO about it.\n */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n let unverified = 0;\n for (const tree of spared) {\n out += ` · ${tree.path} — ${tree.reason}\\n`;\n if (tree.reason.includes(LOCK_LIVENESS_UNVERIFIABLE)) unverified += 1;\n }\n if (unverified > 0) {\n out += `\\n${String(unverified)} of those carry a claude-agent lock whose liveness cannot be `\n + 'verified.\\nTo judge those on their branch and commits alone — unlocking and removing only\\n'\n + `the ones that come back dead — re-run:\\n pnpm wp-cleanup ${FLAG_IGNORE_STALE_LOCKS}\\n`;\n }\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
@@ -19,6 +19,6 @@ const cleanup_options_1 = require("./commands/cleanup-options");
19
19
  // Reject bogus flags BEFORE the app touches git — a mistyped `--delete-branchs` must never be
20
20
  // silently dropped and then run a cleanup that deletes something else instead.
21
21
  const args = container.get(rules_config_1.CliArgs).parse(container.get(cleanup_options_1.CleanupUsage).declare());
22
- await container.get(pr_gate_app_1.PrGateApp).cleanup(new cleanup_options_1.CleanupOptions(new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_BRANCHES, args.has(cleanup_options_1.FLAG_DELETE_BRANCHES), args.value(cleanup_options_1.FLAG_DELETE_BRANCHES)), new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_WORKTREES, args.has(cleanup_options_1.FLAG_DELETE_WORKTREES), args.value(cleanup_options_1.FLAG_DELETE_WORKTREES)), args.has(cleanup_options_1.FLAG_REPORT), args.has(cleanup_options_1.FLAG_INTERACTIVE)));
22
+ await container.get(pr_gate_app_1.PrGateApp).cleanup(new cleanup_options_1.CleanupOptions(new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_BRANCHES, args.has(cleanup_options_1.FLAG_DELETE_BRANCHES), args.value(cleanup_options_1.FLAG_DELETE_BRANCHES)), new cleanup_options_1.DeleteSelection(cleanup_options_1.FLAG_DELETE_WORKTREES, args.has(cleanup_options_1.FLAG_DELETE_WORKTREES), args.value(cleanup_options_1.FLAG_DELETE_WORKTREES)), args.has(cleanup_options_1.FLAG_REPORT), args.has(cleanup_options_1.FLAG_INTERACTIVE), args.has(cleanup_options_1.FLAG_IGNORE_STALE_LOCKS)));
23
23
  });
24
24
  //# sourceMappingURL=wp-cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wp-cleanup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-cleanup.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAA2D;AAC3D,+CAA0C;AAC1C,gEAQoC;AAEpC,mGAAmG;AACnG,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,sGAAsG;AACtG,kGAAkG;AAClG,yCAAyC;AACzC,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,8FAA8F;IAC9F,+EAA+E;IAC/E,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACjF,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,OAAO,CAAC,IAAI,gCAAc,CACrD,IAAI,iCAAe,CAAC,sCAAoB,EAAE,IAAI,CAAC,GAAG,CAAC,sCAAoB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,sCAAoB,CAAC,CAAC,EAC3G,IAAI,iCAAe,CAAC,uCAAqB,EAAE,IAAI,CAAC,GAAG,CAAC,uCAAqB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,uCAAqB,CAAC,CAAC,EAC9G,IAAI,CAAC,GAAG,CAAC,6BAAW,CAAC,EACrB,IAAI,CAAC,GAAG,CAAC,kCAAgB,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\nimport {\n CleanupOptions,\n CleanupUsage,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n FLAG_INTERACTIVE,\n FLAG_REPORT,\n} from './commands/cleanup-options';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\n//\n// WHY THIS BIN TAKES FLAGS AT ALL: the thing that used to decide whether wp-cleanup deletes anything\n// was `process.stdin.isTTY`, which is a guess about who is standing there rather than a fact — a human\n// piping to `tee` has no tty, an agent on a pty has one. The flags in CleanupUsage let the caller who\n// KNOWS say so, and an explicit flag always beats the sniff. See CleanupCommand for the numbering\n// contract the `--delete-*` flags carry.\nrunMain(async (): Promise<void> => {\n // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)\n const container = new Container({ autobind: true });\n // Reject bogus flags BEFORE the app touches git — a mistyped `--delete-branchs` must never be\n // silently dropped and then run a cleanup that deletes something else instead.\n const args = container.get(CliArgs).parse(container.get(CleanupUsage).declare());\n await container.get(PrGateApp).cleanup(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, args.has(FLAG_DELETE_BRANCHES), args.value(FLAG_DELETE_BRANCHES)),\n new DeleteSelection(FLAG_DELETE_WORKTREES, args.has(FLAG_DELETE_WORKTREES), args.value(FLAG_DELETE_WORKTREES)),\n args.has(FLAG_REPORT),\n args.has(FLAG_INTERACTIVE)));\n});\n"]}
1
+ {"version":3,"file":"wp-cleanup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/wp-cleanup.ts"],"names":[],"mappings":";;;AACA,4BAA0B;AAC1B,yCAAsC;AACtC,0DAA2D;AAC3D,+CAA0C;AAC1C,gEASoC;AAEpC,mGAAmG;AACnG,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,sGAAsG;AACtG,kGAAkG;AAClG,yCAAyC;AACzC,IAAA,sBAAO,EAAC,KAAK,IAAmB,EAAE;IAC9B,kHAAkH;IAClH,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,8FAA8F;IAC9F,+EAA+E;IAC/E,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAO,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACjF,MAAM,SAAS,CAAC,GAAG,CAAC,uBAAS,CAAC,CAAC,OAAO,CAAC,IAAI,gCAAc,CACrD,IAAI,iCAAe,CAAC,sCAAoB,EAAE,IAAI,CAAC,GAAG,CAAC,sCAAoB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,sCAAoB,CAAC,CAAC,EAC3G,IAAI,iCAAe,CAAC,uCAAqB,EAAE,IAAI,CAAC,GAAG,CAAC,uCAAqB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,uCAAqB,CAAC,CAAC,EAC9G,IAAI,CAAC,GAAG,CAAC,6BAAW,CAAC,EACrB,IAAI,CAAC,GAAG,CAAC,kCAAgB,CAAC,EAC1B,IAAI,CAAC,GAAG,CAAC,yCAAuB,CAAC,CAAC,CAAC,CAAC;AAC5C,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport 'reflect-metadata';\nimport { Container } from 'inversify';\nimport { runMain, CliArgs } from '@webpieces/rules-config';\nimport { PrGateApp } from './pr-gate-app';\nimport {\n CleanupOptions,\n CleanupUsage,\n DeleteSelection,\n FLAG_DELETE_BRANCHES,\n FLAG_DELETE_WORKTREES,\n FLAG_IGNORE_STALE_LOCKS,\n FLAG_INTERACTIVE,\n FLAG_REPORT,\n} from './commands/cleanup-options';\n\n// Composition root: build the container and resolve the app so inversify constructs the whole DAG.\n//\n// WHY THIS BIN TAKES FLAGS AT ALL: the thing that used to decide whether wp-cleanup deletes anything\n// was `process.stdin.isTTY`, which is a guess about who is standing there rather than a fact — a human\n// piping to `tee` has no tty, an agent on a pty has one. The flags in CleanupUsage let the caller who\n// KNOWS say so, and an explicit flag always beats the sniff. See CleanupCommand for the numbering\n// contract the `--delete-*` flags carry.\nrunMain(async (): Promise<void> => {\n // autobind self-binds every @injectable(Singleton) tooling class (replaces the buildProviderModule registry scan)\n const container = new Container({ autobind: true });\n // Reject bogus flags BEFORE the app touches git — a mistyped `--delete-branchs` must never be\n // silently dropped and then run a cleanup that deletes something else instead.\n const args = container.get(CliArgs).parse(container.get(CleanupUsage).declare());\n await container.get(PrGateApp).cleanup(new CleanupOptions(\n new DeleteSelection(FLAG_DELETE_BRANCHES, args.has(FLAG_DELETE_BRANCHES), args.value(FLAG_DELETE_BRANCHES)),\n new DeleteSelection(FLAG_DELETE_WORKTREES, args.has(FLAG_DELETE_WORKTREES), args.value(FLAG_DELETE_WORKTREES)),\n args.has(FLAG_REPORT),\n args.has(FLAG_INTERACTIVE),\n args.has(FLAG_IGNORE_STALE_LOCKS)));\n});\n"]}