@webpieces/pr-gate 0.4.688 → 0.4.690
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 +2 -2
- package/src/scripts/commands/checkout-clean-main-command.js +6 -1
- package/src/scripts/commands/checkout-clean-main-command.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +73 -52
- package/src/scripts/commands/cleanup-command.js +219 -91
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/cleanup-options.d.ts +88 -0
- package/src/scripts/commands/cleanup-options.js +174 -0
- package/src/scripts/commands/cleanup-options.js.map +1 -0
- package/src/scripts/commands/worktree-cleanup.d.ts +19 -2
- package/src/scripts/commands/worktree-cleanup.js +44 -3
- package/src/scripts/commands/worktree-cleanup.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +6 -2
- package/src/scripts/pr-gate-app.js +6 -3
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/wp-cleanup.js +11 -3
- package/src/scripts/wp-cleanup.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.690",
|
|
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.
|
|
18
|
+
"@webpieces/rules-config": "0.4.690",
|
|
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
|
-
|
|
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":"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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,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,
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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. otherwise — take NOTHING, and print the exact command that takes it.
|
|
89
115
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
|
120
|
+
private decide;
|
|
99
121
|
/**
|
|
100
|
-
*
|
|
122
|
+
* The exact command that takes what this run left — the half a non-tty caller never used to get.
|
|
101
123
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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
|
|
127
|
+
private flagHint;
|
|
107
128
|
private pickByNumber;
|
|
108
129
|
protected question(prompt: string): Promise<string>;
|
|
109
130
|
}
|