@webpieces/pr-gate 0.4.688 → 0.4.689

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.688",
3
+ "version": "0.4.689",
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.688",
18
+ "@webpieces/rules-config": "0.4.689",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const inversify_1 = require("inversify");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
7
7
  const cleanup_command_1 = require("./cleanup-command");
8
+ const cleanup_options_1 = require("./cleanup-options");
8
9
  const main_checkout_1 = require("./main-checkout");
9
10
  const working_tree_gate_1 = require("./working-tree-gate");
10
11
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
@@ -71,7 +72,11 @@ let CheckoutCleanMainCommand = class CheckoutCleanMainCommand {
71
72
  const untracked = this.workingTreeGate.assertNoTrackedChanges(repoRoot);
72
73
  const stashed = this.mainCheckout.goToMain(repoRoot, untracked);
73
74
  this.reportUntracked(untracked, stashed);
74
- await this.cleanupCommand.run();
75
+ // No flags: this is the "go to main and take out the trash" path, so cleanup behaves exactly as a
76
+ // bare `pnpm wp-cleanup` does here — reap what is provably dead and every zero-commit husk, ask
77
+ // (or report) about the rest. Spelled out rather than defaulted so that if CleanupOptions ever
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));
75
80
  this.sweep(repoRoot);
76
81
  this.reportStashed(stashed);
77
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 { 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 await this.cleanupCommand.run();\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,CAAC,CAAC,CAAC;QACnB,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));\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,36 +1,44 @@
1
1
  import { BranchArchiver, BranchReaper, RepoRootFinder } from '@webpieces/rules-config';
2
2
  import { WorktreeCleanupSection } from './worktree-cleanup';
3
+ import { CleanupOptions } from './cleanup-options';
3
4
  /**
4
- * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, then ASK
5
- * about the ones that are merely probably-dead.
5
+ * wp-cleanup: remove the dead WORKTREES, delete the local branches whose PR is already MERGED, REAP
6
+ * THE ZERO-COMMIT HUSKS, then report everything that is left with the exact command that takes it.
6
7
  *
7
- * A merged PR is the ONLY proof that reaps anything unattended. "Holds no commits" used to be a second
8
- * proof and no longer is: it is equally the signature of a worktree an agent is working in right now,
9
- * so it moved into the group this command ASKS about.
8
+ * ─── WHY A ZERO-COMMIT REF IS REAPED AND NOT ASKED ABOUT ─────────────────────────────────────────
9
+ * A ref with 0 unique commits is byte-identical to origin/main. Deleting it loses a NAME, not a
10
+ * commit and the name comes back with one `git checkout -b`, or from the archive tag this writes
11
+ * first. It was previously PROMPTED about, for one real reason: a WORKING TREE looks exactly like
12
+ * that husk from `git worktree add -b` until its first commit, i.e. for exactly the window an agent
13
+ * is working in it. But that case is DETECTABLE, and detecting it is cheaper than asking a human:
10
14
  *
11
- * WHY WORKTREES ARE PART OF THIS: they were the half that never got reaped. The verdicts existed —
12
- * merged-branches.ts has been writing a full `DeletableWorktree[]` into the cache all along and their
13
- * only consumer used them to BLOCK the next `git worktree add`, never to remove anything. Meanwhile a
14
- * live worktree pins its branch, so the branch was spared too. Two things the tooling could PROVE were
15
- * dead, accumulating forever, until the guard refused to create the next branch and the only remedy it
16
- * could offer was loosening its own cap. See WorktreeCleanupSection and WorktreeReaper.
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
18
+ * · it is the tree we are standing in, or a detached HEAD spared, likewise
17
19
  *
18
- * WHY a named command instead of the `git branch -D a b c` the guards used to print: an AI agent
19
- * reads a raw `-D` as destructive, so it asks permission and stops which is exactly why branches
20
- * piled up despite the tooling knowing precisely which ones were dead. `pnpm wp-cleanup` is one
21
- * boring, allowlistable verb whose safety is a property of the command itself rather than of the
22
- * agent's judgement about a git flag.
20
+ * So the bar moved from "prove it is dead" to "prove somebody is holding it", deliberately, because
21
+ * the cost of being wrong is a re-`checkout -b` and the cost of being cautious was a prompt that
22
+ * stopped a human's terminal to adjudicate two refs that could not possibly hold work. Refs that DO
23
+ * carry unique commits keep every bit of the old caution.
23
24
  *
24
- * WHY IT NOW PROMPTS: sparing silently was the other half of the same problem. Every spared branch
25
- * reported the identical `no merged PR found a human must decide`, so the human could not decide,
26
- * so nothing got deleted, so the pile grew until branch-creation-guard refused to make the next branch
27
- * and an agent went looking for a config knob to loosen. Shown a real classification with unique-commit
28
- * counts, the human in that session answered in five words: "these should all be delete branches".
29
- * The prompt is cheap because archiving happens FIRST — a yes costs a tag, not the history.
25
+ * ─── WHY FLAGS, AND WHY THEY BEAT THE TTY SNIFF ──────────────────────────────────────────────────
26
+ * `process.stdin.isTTY !== true` used to be the whole basis for "is a human standing here?", and it
27
+ * is a proxy, not a fact: a human running `pnpm wp-cleanup | tee log` has no tty, an agent on a pty
28
+ * has one. The sniff stays as the DEFAULT selector; `--delete-branches` / `--delete-worktrees` /
29
+ * `--interactive` / `--report` let the caller who KNOWS say so, and an explicit flag always wins.
30
30
  *
31
- * All the danger still lives in the verdicts, not here see BranchReaper for why every AUTOMATICALLY
32
- * deleted branch is provably dead and recoverable, and note that nothing in the prompted group is ever
33
- * deleted without an explicit typed answer.
31
+ * ─── WHY A NON-TTY RUN NOW DELETES NOTHING IT WAS NOT ASKED TO ───────────────────────────────────
32
+ * It used to silently take the "redundant" groups and print a bare list of names it left. That is
33
+ * strictly less than the human sees, and — worse — it broke the one invariant that makes numeric
34
+ * selection safe: THE NUMBERS IN THE FLAG ARE THE NUMBERS JUST PRINTED. A run that deletes half of a
35
+ * numbered list renumbers the rest, so the `--delete-branches=3,4` it printed would land on entries
36
+ * 5 and 6 next time. So the unattended path now prints the IDENTICAL numbered, classified block a
37
+ * human sees plus the exact command that takes it, and takes nothing from that block itself. The
38
+ * husks it does reap are never IN the block, so reaping them cannot shift a single number.
39
+ *
40
+ * All the danger still lives in the verdicts, not here — see BranchReaper for why every automatically
41
+ * deleted branch is recoverable, and note that EVERY delete on every path here archives first.
34
42
  */
35
43
  export declare class CleanupCommand {
36
44
  private readonly repoRootFinder;
@@ -45,11 +53,19 @@ export declare class CleanupCommand {
45
53
  * next one. Reaping the worktree takes its branch with it, and the branch pass then recomputes its
46
54
  * verdicts from scratch against the post-removal truth.
47
55
  */
48
- run(): Promise<void>;
56
+ run(options: CleanupOptions): Promise<void>;
57
+ /**
58
+ * `--report`: the whole classified picture, and NOT ONE DELETE.
59
+ *
60
+ * This is the only run whose numbers are guaranteed still valid when the next command starts,
61
+ * because a run that deletes nothing cannot renumber anything. That is what makes it the honest
62
+ * first half of `--report` → read the numbers → `--delete-branches=1,3`.
63
+ */
64
+ private reportOnly;
65
+ private wouldReapBlock;
49
66
  private cleanUpBranches;
50
67
  /**
51
- * Reap the provably-dead worktrees, then ASK about the probably-dead ones the same two-tier
52
- * posture the branch half has, because it is the same verdict on the same branch.
68
+ * Reap the provably-dead worktrees and the zero-commit husks, then decide about the rest.
53
69
  *
54
70
  * WorktreeReaper enforces the safety rails regardless of what is passed or answered: never the
55
71
  * primary clone, never the tree this command is running in, and never `--force` (git's refusal to
@@ -57,6 +73,8 @@ export declare class CleanupCommand {
57
73
  * cleanup command becomes a data-loss command).
58
74
  */
59
75
  private cleanUpWorktrees;
76
+ private nothingToDo;
77
+ private huskBlock;
60
78
  private report;
61
79
  private reapedLine;
62
80
  /**
@@ -72,38 +90,41 @@ export declare class CleanupCommand {
72
90
  * worktree prompt, which shows both the path and the branch it holds, or it is not offered at all.
73
91
  */
74
92
  private promptable;
75
- private classifiedBlock;
76
93
  /**
77
- * Ask which of the classified branches to delete. Answers: `all`, `none` (default), or a
78
- * comma/space-separated list of the numbers shown.
94
+ * The zero-commit husks out of a promptable list reaped by default, and never numbered.
79
95
  *
80
- * NON-INTERACTIVE (no TTY CI, a hook, an AGENT'S piped shell) takes everything except
81
- * NEVER PROPOSED, and says exactly what it took.
96
+ * Keeping them OUT of the numbered block is what keeps `--delete-branches=1,3` honest: whatever
97
+ * this reaps, the entries the caller can name are the same entries in the same order as on the
98
+ * `--report` run that printed those numbers.
99
+ *
100
+ * THE CLASSIFICATION TOKEN IS THE ONLY SPELLING OF "husk". It would read as belt-and-braces to
101
+ * also accept `commits === 0` here, and it is the opposite: MergedBranchesService.classify tests
102
+ * commits===0 BEFORE anything else, so a second test can only ever disagree with the verdict — and
103
+ * disagreeing in the widening direction, auto-reaping something no classification called a husk.
104
+ */
105
+ private husks;
106
+ private rest;
107
+ private classifiedBlock;
108
+ /**
109
+ * Who decides about the numbered block, in priority order:
82
110
  *
83
- * It used to take nothing at all, on the reasoning that "a prompt nobody can see must never be
84
- * read as consent". Correct about consent, wrong about who is standing there: the overwhelmingly
85
- * common non-TTY caller is an agent that was TOLD to clean up, and for it the prompt was a dead
86
- * endwp-cleanup deleted nothing, the branch cap still blocked, and the only path left was to
87
- * hand the human a table of branches to adjudicate. The human's response to receiving one of
88
- * those: "I am tired of AI asking me to cleanup things it can do by itself".
111
+ * 1. an explicit `--delete-branches` / `--delete-worktrees` the caller said so, and a caller who
112
+ * said so is never overruled by what stdin happens to be attached to;
113
+ * 2. a terminal, or `--interactive` ask;
114
+ * 3. otherwisetake NOTHING, and print the exact command that takes it.
89
115
  *
90
- * So consent is honoured where it is actually needed rather than everywhere. NEVER PROPOSED is
91
- * the group whose commits may be the only copy in existence; it is never taken unattended and is
92
- * the one thing left to ask about. The rest — a closed-unmerged PR superseded by later merges,
93
- * commits already patch-equivalent in main, a ref identical to origin/main — are redundant by
94
- * evidence, and every one of them is archived to an `archive/<date>/<branch>` tag with a
95
- * `recover=` command logged before it is touched. An interactive human still gets the full
96
- * prompt, including NEVER PROPOSED.
116
+ * (3) is the case an agent lands in, and it is deliberately empty-handed. See the class comment:
117
+ * a run that deletes part of a numbered list invalidates the numbers it just printed, and those
118
+ * numbers are the whole interface the next run is given.
97
119
  */
98
- private askWhichToDelete;
120
+ private decide;
99
121
  /**
100
- * What an unattended run takes: everything promptable except NEVER PROPOSED (see above).
122
+ * The exact command that takes what this run left the half a non-tty caller never used to get.
101
123
  *
102
- * Prints the split either way, because a cleanup that deletes silently is a cleanup nobody can
103
- * audit and the leftover NEVER PROPOSED names are exactly the short question an agent should
104
- * put to a human, in place of the six-branch table this replaced.
124
+ * It states the numbering contract out loud because a shifted number is the one way this command
125
+ * can delete the wrong ref, and the caller reading this is usually a program.
105
126
  */
106
- private unattendedPicks;
127
+ private flagHint;
107
128
  private pickByNumber;
108
129
  protected question(prompt: string): Promise<string>;
109
130
  }