@webpieces/pr-gate 0.4.691 → 0.4.693

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +2 -2
  2. package/src/scripts/commands/build-command.d.ts +3 -3
  3. package/src/scripts/commands/build-command.js +4 -4
  4. package/src/scripts/commands/build-command.js.map +1 -1
  5. package/src/scripts/commands/cleanup-command.d.ts +3 -2
  6. package/src/scripts/commands/cleanup-command.js +27 -20
  7. package/src/scripts/commands/cleanup-command.js.map +1 -1
  8. package/src/scripts/commands/finish-upsert-pr-command.d.ts +12 -3
  9. package/src/scripts/commands/finish-upsert-pr-command.js +25 -10
  10. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
  11. package/src/scripts/commands/reap-worktree-command.js +3 -3
  12. package/src/scripts/commands/reap-worktree-command.js.map +1 -1
  13. package/src/scripts/commands/review-upsert-pr-command.d.ts +15 -1
  14. package/src/scripts/commands/review-upsert-pr-command.js +25 -4
  15. package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
  16. package/src/scripts/commands/worktree-cleanup.d.ts +4 -3
  17. package/src/scripts/commands/worktree-cleanup.js +10 -4
  18. package/src/scripts/commands/worktree-cleanup.js.map +1 -1
  19. package/src/scripts/workflow/build-affected.d.ts +11 -30
  20. package/src/scripts/workflow/build-affected.js +25 -61
  21. package/src/scripts/workflow/build-affected.js.map +1 -1
  22. package/src/scripts/workflow/build-gate-log.d.ts +14 -29
  23. package/src/scripts/workflow/build-gate-log.js +34 -91
  24. package/src/scripts/workflow/build-gate-log.js.map +1 -1
  25. package/src/scripts/workflow/gate-log-file.d.ts +55 -0
  26. package/src/scripts/workflow/gate-log-file.js +136 -0
  27. package/src/scripts/workflow/gate-log-file.js.map +1 -0
  28. package/src/scripts/workflow/run-update.js +2 -1
  29. package/src/scripts/workflow/run-update.js.map +1 -1
  30. package/src/scripts/workflow/stage-output-log.d.ts +40 -0
  31. package/src/scripts/workflow/stage-output-log.js +137 -0
  32. package/src/scripts/workflow/stage-output-log.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.691",
3
+ "version": "0.4.693",
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.691",
18
+ "@webpieces/rules-config": "0.4.693",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -33,17 +33,17 @@ export declare class BuildOptions {
33
33
  *
34
34
  * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────
35
35
  * `BuildAffected.runBuildGate` is the single code path that resolves the command
36
- * (`resolveBuildCommand`), announces it, runs it, honours `buildGateLogCapture`, and renders the
36
+ * (`resolveBuildCommand`), announces it, runs it, captures its output to a log file, and renders the
37
37
  * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second
38
38
  * failure string. A second reader is a second thing that can drift, and a second failure message is a
39
39
  * second thing that can teach a command the gate does not run; drift is the entire reason this bin
40
40
  * exists, so reproducing it one level in would be self-defeating.
41
41
  *
42
42
  * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the
43
- * headline, the log-file stage id, and `alwaysCapture`.
43
+ * headline, and the log-file stage id.
44
44
  *
45
45
  * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────
46
- * `alwaysCapture: true` is the one behaviour wp-build does not share with the PR-flow stages, and it is
46
+ * Every caller of the gate captures, and there is no flag or config key that selects otherwise. That is
47
47
  * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`
48
48
  * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at
49
49
  * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code
@@ -47,17 +47,17 @@ exports.BuildOptions = BuildOptions;
47
47
  *
48
48
  * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────
49
49
  * `BuildAffected.runBuildGate` is the single code path that resolves the command
50
- * (`resolveBuildCommand`), announces it, runs it, honours `buildGateLogCapture`, and renders the
50
+ * (`resolveBuildCommand`), announces it, runs it, captures its output to a log file, and renders the
51
51
  * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second
52
52
  * failure string. A second reader is a second thing that can drift, and a second failure message is a
53
53
  * second thing that can teach a command the gate does not run; drift is the entire reason this bin
54
54
  * exists, so reproducing it one level in would be self-defeating.
55
55
  *
56
56
  * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the
57
- * headline, the log-file stage id, and `alwaysCapture`.
57
+ * headline, and the log-file stage id.
58
58
  *
59
59
  * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────
60
- * `alwaysCapture: true` is the one behaviour wp-build does not share with the PR-flow stages, and it is
60
+ * Every caller of the gate captures, and there is no flag or config key that selects otherwise. That is
61
61
  * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`
62
62
  * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at
63
63
  * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code
@@ -82,7 +82,7 @@ let BuildCommand = class BuildCommand {
82
82
  // runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so
83
83
  // runMain owns the exit — the same three things it does for stage ② and stage ③. It also writes
84
84
  // this build's START/DONE pair to the ledger the check above just read.
85
- return this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ wp-build', 'pnpm wp-build', 'Build failed.', build_gate_log_1.BUILD_STAGE, true));
85
+ return this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ wp-build', 'pnpm wp-build', 'Build failed.', build_gate_log_1.BUILD_STAGE));
86
86
  }
87
87
  /**
88
88
  * Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.
@@ -1 +1 @@
1
- {"version":3,"file":"build-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/build-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BuildsLog, HomeConfigService, Option, RepoRootFinder, RuleFailError, RunningBuild,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BUILD_STAGE } from '../workflow/build-gate-log';\n\n/** The rule name the refusal is reported under, so it is greppable from a transcript. */\nexport const TOO_MANY_CONCURRENT_BUILDS = 'too-many-concurrent-builds';\n\n/**\n * What `wp-build` was asked to do. Data-only (a class, per CLAUDE.md), with exactly one field so far.\n *\n * `force` SKIPS THE CONCURRENCY CHECK AND NOTHING ELSE. It does not touch the command, and it must never\n * grow a field that does: `wp-build` runs `commands.pr-gate.buildCommand` verbatim, and a flag that\n * changed the command would make it a different build from the one the PR gate runs — which is the exact\n * drift this bin exists to delete.\n */\nexport class BuildOptions {\n force: boolean;\n\n // REQUIRED, no default. `new BuildOptions()` and `new BuildOptions(false)` would be two spellings of\n // one decision, and the bare form would keep compiling unchanged if this class ever grew a second\n // field — silently meaning \"not forced\" at a call site that was never revisited. That is the same\n // argument HomeConfig's constructor makes about its own four required parameters; a defaulted\n // parameter is exactly the shim this repo does not ship.\n constructor(force: boolean) {\n this.force = force;\n }\n}\n\n/**\n * `wp-build` — run THE project's build, and nothing else.\n *\n * ─── Why this bin exists ───────────────────────────────────────────────────────────────────────────\n * The correct verification command has lived in `commands.pr-gate.buildCommand` all along, and NOTHING\n * SURFACED IT. So every repo hand-composed its own verify chain, and those chains drifted: one sibling\n * repo's `ci:local` was `prettier --check .` (whole repo) + `wp-ci` (whole monorepo) +\n * `nx affected -t test` with NO `--base` (whole repo again) — three whole-world passes on an inner\n * loop, none of them the command the gate runs. `wp-build` closes that by making the configured value\n * the only thing anyone has to type.\n *\n * ─── It composes NOTHING, deliberately ─────────────────────────────────────────────────────────────\n * This command runs `commands.pr-gate.buildCommand` VERBATIM and adds no leg of its own — no format\n * check, no lint pass, no extra test run. Composition is exactly what drifted. Anything that must run\n * on every build belongs INSIDE `buildCommand`, where the PR gate runs it too; a leg added here would\n * be a leg the gate never sees, which is the same defect one level in.\n *\n * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────\n * `BuildAffected.runBuildGate` is the single code path that resolves the command\n * (`resolveBuildCommand`), announces it, runs it, honours `buildGateLogCapture`, and renders the\n * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second\n * failure string. A second reader is a second thing that can drift, and a second failure message is a\n * second thing that can teach a command the gate does not run; drift is the entire reason this bin\n * exists, so reproducing it one level in would be self-defeating.\n *\n * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the\n * headline, the log-file stage id, and `alwaysCapture`.\n *\n * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────\n * `alwaysCapture: true` is the one behaviour wp-build does not share with the PR-flow stages, and it is\n * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`\n * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at\n * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code\n * change in between, purely to see a different slice of output that had scrolled past. Reading a\n * different slice must cost a `grep`, not a build — see BuildGateLog.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildCommand {\n constructor(\n private readonly buildAffected: BuildAffected,\n private readonly repoRootFinder: RepoRootFinder,\n private readonly buildsLog: BuildsLog,\n private readonly homeConfig: HomeConfigService,\n ) {}\n\n run(opts: BuildOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n if (!opts.force) this.assertMachineHasRoom();\n // runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so\n // runMain owns the exit — the same three things it does for stage ② and stage ③. It also writes\n // this build's START/DONE pair to the ledger the check above just read.\n return this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ wp-build',\n 'pnpm wp-build',\n 'Build failed.',\n BUILD_STAGE,\n true,\n ));\n }\n\n /**\n * Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.\n *\n * ─── WHY ONLY HERE, AND NEVER ON THE GATE STAGES ─────────────────────────────────────────────────\n * `wp-review-upsert-pr` and `wp-finish-upsert-pr` are the SANCTIONED path, and a refusal there wedges\n * a PR that has nowhere else to go. So they always run — and their builds still write ledger rows, so\n * they count toward what refuses an ad-hoc `wp-build`. The asymmetry is the whole design: the thing\n * that gets throttled is the extra build, not the one the process needs.\n *\n * The cures are `Option`s handed to `RuleFailError`, which `runMain` renders through\n * `formatFixOptions`. They are never numbered by hand in the message — the framework owns\n * \"Fix Option N:\", and a hand-numbered list in a string literal is an automatic review reject.\n */\n private assertMachineHasRoom(): void {\n const live = this.buildsLog.running();\n const max = this.homeConfig.load().maxConcurrentBuilds;\n if (live.length < max) return;\n throw new RuleFailError(\n TOO_MANY_CONCURRENT_BUILDS,\n `This machine is already running ${String(live.length)} build(s), and the limit is ` +\n `${String(max)}. Starting another makes all of them slower — CPU contention between agents ` +\n `building at once was measured at ~3.2x total test time.\\n\\n${this.describe(live)}`,\n undefined,\n undefined,\n [\n new Option(\n 'Re-use the gate you should be running anyway: `pnpm wp-start-upsert-pr` then\\n'\n + '`pnpm wp-review-upsert-pr`. Stage ② runs the SAME `commands.pr-gate.buildCommand`\\n'\n + 'this would have run, so its green is the same evidence — and it posts the PR.',\n true),\n new Option(\n 'Wait for one of the builds above to finish and run `pnpm wp-build` again. A build\\n'\n + 'that has already died leaves no row: the count only holds live processes.'),\n new Option(\n 'Verify only what you are editing instead of the whole affected set:\\n'\n + '`pnpm exec vitest run <one spec file>`, or `pnpm nx run <project>:test`.'),\n new Option(\n 'Raise the limit for THIS machine, if it genuinely has the cores: put\\n'\n + '`{\"experimental\": {\"maxConcurrentBuilds\": <n>}}` in `~/.webpieces/config.json`.'),\n new Option(\n 'If you are really stuck and cannot use a gate and really really need wp-build\\n'\n + 'then use `pnpm wp-build --force`.'),\n ],\n );\n }\n\n // One line per live build: which repo, which worktree, on what branch, and how long it has been\n // going. Age is what tells a reader whether to wait thirty seconds or go and look at a stuck agent.\n private describe(live: readonly RunningBuild[]): string {\n return live.map((build: RunningBuild): string => {\n const age = Math.max(0, Math.round((Date.now() - build.startedMs) / 1000));\n return ` • ${build.repo} [tree=${build.tree || 'primary'}] `\n + `branch=${build.branch || '?'} by=${build.by} pid=${String(build.pid)} `\n + `running for ${String(age)}s`;\n }).join('\\n');\n }\n}\n"]}
1
+ {"version":3,"file":"build-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/build-command.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BuildsLog, HomeConfigService, Option, RepoRootFinder, RuleFailError, RunningBuild,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BUILD_STAGE } from '../workflow/build-gate-log';\n\n/** The rule name the refusal is reported under, so it is greppable from a transcript. */\nexport const TOO_MANY_CONCURRENT_BUILDS = 'too-many-concurrent-builds';\n\n/**\n * What `wp-build` was asked to do. Data-only (a class, per CLAUDE.md), with exactly one field so far.\n *\n * `force` SKIPS THE CONCURRENCY CHECK AND NOTHING ELSE. It does not touch the command, and it must never\n * grow a field that does: `wp-build` runs `commands.pr-gate.buildCommand` verbatim, and a flag that\n * changed the command would make it a different build from the one the PR gate runs — which is the exact\n * drift this bin exists to delete.\n */\nexport class BuildOptions {\n force: boolean;\n\n // REQUIRED, no default. `new BuildOptions()` and `new BuildOptions(false)` would be two spellings of\n // one decision, and the bare form would keep compiling unchanged if this class ever grew a second\n // field — silently meaning \"not forced\" at a call site that was never revisited. That is the same\n // argument HomeConfig's constructor makes about its own four required parameters; a defaulted\n // parameter is exactly the shim this repo does not ship.\n constructor(force: boolean) {\n this.force = force;\n }\n}\n\n/**\n * `wp-build` — run THE project's build, and nothing else.\n *\n * ─── Why this bin exists ───────────────────────────────────────────────────────────────────────────\n * The correct verification command has lived in `commands.pr-gate.buildCommand` all along, and NOTHING\n * SURFACED IT. So every repo hand-composed its own verify chain, and those chains drifted: one sibling\n * repo's `ci:local` was `prettier --check .` (whole repo) + `wp-ci` (whole monorepo) +\n * `nx affected -t test` with NO `--base` (whole repo again) — three whole-world passes on an inner\n * loop, none of them the command the gate runs. `wp-build` closes that by making the configured value\n * the only thing anyone has to type.\n *\n * ─── It composes NOTHING, deliberately ─────────────────────────────────────────────────────────────\n * This command runs `commands.pr-gate.buildCommand` VERBATIM and adds no leg of its own — no format\n * check, no lint pass, no extra test run. Composition is exactly what drifted. Anything that must run\n * on every build belongs INSIDE `buildCommand`, where the PR gate runs it too; a leg added here would\n * be a leg the gate never sees, which is the same defect one level in.\n *\n * ─── ONE resolver, and ONE gate ────────────────────────────────────────────────────────────────────\n * `BuildAffected.runBuildGate` is the single code path that resolves the command\n * (`resolveBuildCommand`), announces it, runs it, captures its output to a log file, and renders the\n * failure. `wp-build` calls THAT — it does not re-read the config key and does not hand-write a second\n * failure string. A second reader is a second thing that can drift, and a second failure message is a\n * second thing that can teach a command the gate does not run; drift is the entire reason this bin\n * exists, so reproducing it one level in would be self-defeating.\n *\n * Only `BuildGateOptions` differs from stage ② and stage ③ — the label, the command to re-run, the\n * headline, and the log-file stage id.\n *\n * ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────\n * Every caller of the gate captures, and there is no flag or config key that selects otherwise. That is\n * the reason this bin is worth running: the build's full stdout+stderr land in `.webpieces/build.log`\n * (previous run kept as `.webpieces/build.log.bak`), and the console gets a heartbeat plus a pointer at\n * that file. A measured session burned ~19 minutes re-running `nx affected` five times with no code\n * change in between, purely to see a different slice of output that had scrolled past. Reading a\n * different slice must cost a `grep`, not a build — see BuildGateLog.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildCommand {\n constructor(\n private readonly buildAffected: BuildAffected,\n private readonly repoRootFinder: RepoRootFinder,\n private readonly buildsLog: BuildsLog,\n private readonly homeConfig: HomeConfigService,\n ) {}\n\n run(opts: BuildOptions): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n if (!opts.force) this.assertMachineHasRoom();\n // runBuildGate announces the resolved command, runs it, and throws CliExitError on failure so\n // runMain owns the exit — the same three things it does for stage ② and stage ③. It also writes\n // this build's START/DONE pair to the ledger the check above just read.\n return this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ wp-build',\n 'pnpm wp-build',\n 'Build failed.',\n BUILD_STAGE,\n ));\n }\n\n /**\n * Refuse when this MACHINE is already running `maxConcurrentBuilds` builds.\n *\n * ─── WHY ONLY HERE, AND NEVER ON THE GATE STAGES ─────────────────────────────────────────────────\n * `wp-review-upsert-pr` and `wp-finish-upsert-pr` are the SANCTIONED path, and a refusal there wedges\n * a PR that has nowhere else to go. So they always run — and their builds still write ledger rows, so\n * they count toward what refuses an ad-hoc `wp-build`. The asymmetry is the whole design: the thing\n * that gets throttled is the extra build, not the one the process needs.\n *\n * The cures are `Option`s handed to `RuleFailError`, which `runMain` renders through\n * `formatFixOptions`. They are never numbered by hand in the message — the framework owns\n * \"Fix Option N:\", and a hand-numbered list in a string literal is an automatic review reject.\n */\n private assertMachineHasRoom(): void {\n const live = this.buildsLog.running();\n const max = this.homeConfig.load().maxConcurrentBuilds;\n if (live.length < max) return;\n throw new RuleFailError(\n TOO_MANY_CONCURRENT_BUILDS,\n `This machine is already running ${String(live.length)} build(s), and the limit is ` +\n `${String(max)}. Starting another makes all of them slower — CPU contention between agents ` +\n `building at once was measured at ~3.2x total test time.\\n\\n${this.describe(live)}`,\n undefined,\n undefined,\n [\n new Option(\n 'Re-use the gate you should be running anyway: `pnpm wp-start-upsert-pr` then\\n'\n + '`pnpm wp-review-upsert-pr`. Stage ② runs the SAME `commands.pr-gate.buildCommand`\\n'\n + 'this would have run, so its green is the same evidence — and it posts the PR.',\n true),\n new Option(\n 'Wait for one of the builds above to finish and run `pnpm wp-build` again. A build\\n'\n + 'that has already died leaves no row: the count only holds live processes.'),\n new Option(\n 'Verify only what you are editing instead of the whole affected set:\\n'\n + '`pnpm exec vitest run <one spec file>`, or `pnpm nx run <project>:test`.'),\n new Option(\n 'Raise the limit for THIS machine, if it genuinely has the cores: put\\n'\n + '`{\"experimental\": {\"maxConcurrentBuilds\": <n>}}` in `~/.webpieces/config.json`.'),\n new Option(\n 'If you are really stuck and cannot use a gate and really really need wp-build\\n'\n + 'then use `pnpm wp-build --force`.'),\n ],\n );\n }\n\n // One line per live build: which repo, which worktree, on what branch, and how long it has been\n // going. Age is what tells a reader whether to wait thirty seconds or go and look at a stuck agent.\n private describe(live: readonly RunningBuild[]): string {\n return live.map((build: RunningBuild): string => {\n const age = Math.max(0, Math.round((Date.now() - build.startedMs) / 1000));\n return ` • ${build.repo} [tree=${build.tree || 'primary'}] `\n + `branch=${build.branch || '?'} by=${build.by} pid=${String(build.pid)} `\n + `running for ${String(age)}s`;\n }).join('\\n');\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { BranchArchiver, BranchReaper, RepoRootFinder } from '@webpieces/rules-config';
1
+ import { BranchArchiver, BranchMutationLog, BranchReaper, RepoRootFinder } from '@webpieces/rules-config';
2
2
  import { WorktreeCleanupSection } from './worktree-cleanup';
3
3
  import { CleanupOptions } from './cleanup-options';
4
4
  /**
@@ -45,7 +45,8 @@ export declare class CleanupCommand {
45
45
  private readonly branchReaper;
46
46
  private readonly archiver;
47
47
  private readonly worktreeSection;
48
- constructor(repoRootFinder: RepoRootFinder, branchReaper: BranchReaper, archiver: BranchArchiver, worktreeSection: WorktreeCleanupSection);
48
+ private readonly mutationLog;
49
+ constructor(repoRootFinder: RepoRootFinder, branchReaper: BranchReaper, archiver: BranchArchiver, worktreeSection: WorktreeCleanupSection, mutationLog: BranchMutationLog);
49
50
  /**
50
51
  * WORKTREES FIRST, then branches. The order is the fix, not a detail: a worktree HOLDS its branch,
51
52
  * so that branch is spared `in-use` ("remove that worktree before deleting the branch") and nothing
@@ -80,11 +80,17 @@ let CleanupCommand = class CleanupCommand {
80
80
  branchReaper;
81
81
  archiver;
82
82
  worktreeSection;
83
- constructor(repoRootFinder, branchReaper, archiver, worktreeSection) {
83
+ mutationLog;
84
+ constructor(repoRootFinder, branchReaper, archiver, worktreeSection,
85
+ // The log path is COMPUTED, never restated. It is per-worktree, so the relative literal these
86
+ // messages used to carry named a file that does not exist in a linked worktree — the reader
87
+ // greps nothing and reads the silence as "nothing was deleted".
88
+ mutationLog) {
84
89
  this.repoRootFinder = repoRootFinder;
85
90
  this.branchReaper = branchReaper;
86
91
  this.archiver = archiver;
87
92
  this.worktreeSection = worktreeSection;
93
+ this.mutationLog = mutationLog;
88
94
  }
89
95
  /**
90
96
  * WORKTREES FIRST, then branches. The order is the fix, not a detail: a worktree HOLDS its branch,
@@ -122,7 +128,7 @@ let CleanupCommand = class CleanupCommand {
122
128
  const treeBlock = this.rest(treePromptable, treeHusks);
123
129
  if (treeBlock.length > 0) {
124
130
  process.stdout.write(this.worktreeSection.promptBlock(treeBlock));
125
- process.stdout.write(this.flagHint(WORKTREE_KINDS, cleanup_options_1.FLAG_DELETE_WORKTREES, treeBlock.length));
131
+ process.stdout.write(this.flagHint(repoRoot, WORKTREE_KINDS, cleanup_options_1.FLAG_DELETE_WORKTREES, treeBlock.length));
126
132
  }
127
133
  // Retention 'keep' turns BranchReaper.reap into a pure verdict computation — every branch
128
134
  // lands in `spared`, nothing is touched. That is exactly what a report needs, and it means
@@ -138,7 +144,7 @@ let CleanupCommand = class CleanupCommand {
138
144
  const block = this.rest(promptable, husks);
139
145
  if (block.length > 0) {
140
146
  process.stdout.write(this.classifiedBlock(block));
141
- process.stdout.write(this.flagHint(BRANCH_KINDS, cleanup_options_1.FLAG_DELETE_BRANCHES, block.length));
147
+ process.stdout.write(this.flagHint(repoRoot, BRANCH_KINDS, cleanup_options_1.FLAG_DELETE_BRANCHES, block.length));
142
148
  }
143
149
  process.stdout.write(`\nNothing was deleted (--report). Retention policy in effect: ${retention}.\n`
144
150
  + 'Re-run without --report to act, or with the flags above to say exactly what to take.\n');
@@ -155,7 +161,7 @@ let CleanupCommand = class CleanupCommand {
155
161
  // No cache argument: wp-cleanup recomputes the verdicts itself. The file on disk is allowed to
156
162
  // go stale, and stale evidence is fine for BLOCKING but never for DELETING.
157
163
  const result = this.branchReaper.reap(repoRoot, 'wp-cleanup', null, retention);
158
- const first = this.report(result);
164
+ const first = this.report(repoRoot, result);
159
165
  process.stdout.write(first);
160
166
  const promptable = this.promptable(result.spared);
161
167
  const husks = this.husks(promptable);
@@ -165,7 +171,7 @@ let CleanupCommand = class CleanupCommand {
165
171
  options.branches.pick(block);
166
172
  if (husks.length > 0) {
167
173
  process.stdout.write(this.huskBlock(BRANCH_KINDS, husks.map((entry) => entry.branch)));
168
- process.stdout.write(this.report(this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', husks, retention)));
174
+ process.stdout.write(this.report(repoRoot, this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', husks, retention)));
169
175
  }
170
176
  if (block.length === 0) {
171
177
  if (first === '' && husks.length === 0)
@@ -173,12 +179,12 @@ let CleanupCommand = class CleanupCommand {
173
179
  return;
174
180
  }
175
181
  process.stdout.write(this.classifiedBlock(block));
176
- const approved = await this.decide(block, BRANCH_KIND, BRANCH_KINDS, cleanup_options_1.FLAG_DELETE_BRANCHES, options.branches, options);
182
+ const approved = await this.decide(repoRoot, block, BRANCH_KIND, BRANCH_KINDS, cleanup_options_1.FLAG_DELETE_BRANCHES, options.branches, options);
177
183
  if (approved.length === 0) {
178
184
  process.stdout.write(`\nNothing else deleted — the ${String(block.length)} ${BRANCH_KINDS} above were kept.\n`);
179
185
  return;
180
186
  }
181
- process.stdout.write(this.report(this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention)));
187
+ process.stdout.write(this.report(repoRoot, this.branchReaper.reapApproved(repoRoot, 'wp-cleanup', approved, retention)));
182
188
  }
183
189
  /**
184
190
  * Reap the provably-dead worktrees and the zero-commit husks, then decide about the rest.
@@ -192,7 +198,7 @@ let CleanupCommand = class CleanupCommand {
192
198
  const verdicts = this.worktreeSection.verdicts(repoRoot);
193
199
  const dead = this.worktreeSection.provablyDead(verdicts);
194
200
  if (dead.length > 0) {
195
- process.stdout.write(this.worktreeSection.report(this.worktreeSection.reap(repoRoot, 'wp-cleanup', dead, retention)));
201
+ process.stdout.write(this.worktreeSection.report(repoRoot, this.worktreeSection.reap(repoRoot, 'wp-cleanup', dead, retention)));
196
202
  }
197
203
  process.stdout.write(this.worktreeSection.sparedBlock(verdicts, dead));
198
204
  const promptable = this.worktreeSection.promptable(verdicts);
@@ -204,17 +210,17 @@ let CleanupCommand = class CleanupCommand {
204
210
  options.worktrees.pick(block);
205
211
  if (husks.length > 0) {
206
212
  process.stdout.write(this.huskBlock(WORKTREE_KINDS, husks.map((tree) => `${tree.path} [${tree.branch}]`)));
207
- process.stdout.write(this.worktreeSection.report(this.worktreeSection.reap(repoRoot, 'wp-cleanup', husks, retention)));
213
+ process.stdout.write(this.worktreeSection.report(repoRoot, this.worktreeSection.reap(repoRoot, 'wp-cleanup', husks, retention)));
208
214
  }
209
215
  if (block.length === 0)
210
216
  return;
211
217
  process.stdout.write(this.worktreeSection.promptBlock(block));
212
- const approved = await this.decide(block, WORKTREE_KIND, WORKTREE_KINDS, cleanup_options_1.FLAG_DELETE_WORKTREES, options.worktrees, options);
218
+ const approved = await this.decide(repoRoot, block, WORKTREE_KIND, WORKTREE_KINDS, cleanup_options_1.FLAG_DELETE_WORKTREES, options.worktrees, options);
213
219
  if (approved.length === 0) {
214
220
  process.stdout.write(`\nNothing else removed — the ${String(block.length)} ${WORKTREE_KINDS} above were kept.\n`);
215
221
  return;
216
222
  }
217
- process.stdout.write(this.worktreeSection.report(this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));
223
+ process.stdout.write(this.worktreeSection.report(repoRoot, this.worktreeSection.reap(repoRoot, 'wp-cleanup', approved, retention)));
218
224
  }
219
225
  // What a bare run with an empty repo says. It points at --help because the flags are the whole
220
226
  // reason a caller who DOES have something to say never has to be prompted.
@@ -232,7 +238,7 @@ let CleanupCommand = class CleanupCommand {
232
238
  out += ` · ${name}\n`;
233
239
  return out;
234
240
  }
235
- report(result) {
241
+ report(repoRoot, result) {
236
242
  const gone = result.alreadyGone.length;
237
243
  if (result.reaped.length === 0 && result.failed.length === 0 && gone === 0)
238
244
  return '';
@@ -257,7 +263,7 @@ let CleanupCommand = class CleanupCommand {
257
263
  }
258
264
  // Printed even on success: a deletion the human cannot undo is a deletion they have to trust
259
265
  // blindly, and the whole argument for auto-cleanup is that they never have to.
260
- out += '\nEvery deletion is logged with its pre-delete SHA in .webpieces/logs/branch-mutations.log —\n'
266
+ out += `\nEvery deletion is logged with its pre-delete SHA in\n${this.mutationLog.branchMutationLogPath(repoRoot)} —\n`
261
267
  + 'recover any of them with the `recover=` command on its line.\n';
262
268
  return out;
263
269
  }
@@ -338,17 +344,17 @@ let CleanupCommand = class CleanupCommand {
338
344
  * a run that deletes part of a numbered list invalidates the numbers it just printed, and those
339
345
  * numbers are the whole interface the next run is given.
340
346
  */
341
- async decide(block, kind, kinds, flag, selection, options) {
347
+ async decide(repoRoot, block, kind, kinds, flag, selection, options) {
342
348
  if (selection.given()) {
343
349
  const picked = selection.pick(block);
344
350
  process.stdout.write(`\n${flag} chose ${String(picked.length)} of the ${String(block.length)} `
345
351
  + `${kinds} above.\n`);
346
352
  if (picked.length < block.length)
347
- process.stdout.write(this.flagHint(kinds, flag, block.length));
353
+ process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));
348
354
  return picked;
349
355
  }
350
356
  if (!options.prompts()) {
351
- process.stdout.write(this.flagHint(kinds, flag, block.length));
357
+ process.stdout.write(this.flagHint(repoRoot, kinds, flag, block.length));
352
358
  return [];
353
359
  }
354
360
  const answer = (await this.question(`\nDelete which ${kind}(s)? [all / none / e.g. "1,3"] (default none): `)).trim().toLowerCase();
@@ -364,7 +370,7 @@ let CleanupCommand = class CleanupCommand {
364
370
  * It states the numbering contract out loud because a shifted number is the one way this command
365
371
  * can delete the wrong ref, and the caller reading this is usually a program.
366
372
  */
367
- flagHint(kinds, flag, count) {
373
+ flagHint(repoRoot, kinds, flag, count) {
368
374
  return `\nLeft for you to decide (${String(count)} ${kinds}, numbered above).\n`
369
375
  + 'To delete them yourself, re-run with:\n'
370
376
  + ` pnpm wp-cleanup ${flag}=all # every one listed above\n`
@@ -373,8 +379,8 @@ let CleanupCommand = class CleanupCommand {
373
379
  + 'Those numbers are the numbers of THIS run. wp-cleanup renumbers from a fresh scan every\n'
374
380
  + 'time, so once anything in the list is deleted, re-run `pnpm wp-cleanup --report` (which\n'
375
381
  + 'deletes nothing) and read the new numbers before passing numbers again.\n'
376
- + 'Every delete is archived to a tag first; `recover=` lines land in '
377
- + '.webpieces/logs/branch-mutations.log\n';
382
+ + 'Every delete is archived to a tag first; `recover=` lines land in\n'
383
+ + `${this.mutationLog.branchMutationLogPath(repoRoot)}\n`;
378
384
  }
379
385
  // Parse `1,3` / `1 3` from a HUMAN at the prompt, ignoring anything out of range. An unparseable
380
386
  // answer selects nothing, which is the fail-safe direction for a typed answer to a delete question.
@@ -407,6 +413,7 @@ exports.CleanupCommand = CleanupCommand = tslib_1.__decorate([
407
413
  tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
408
414
  rules_config_1.BranchReaper,
409
415
  rules_config_1.BranchArchiver,
410
- worktree_cleanup_1.WorktreeCleanupSection])
416
+ worktree_cleanup_1.WorktreeCleanupSection,
417
+ rules_config_1.BranchMutationLog])
411
418
  ], CleanupCommand);
412
419
  //# sourceMappingURL=cleanup-command.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/cleanup-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as readline from 'readline';\nimport {\n BranchArchiver,\n BranchReaper,\n DeletableBranch,\n DeletableWorktree,\n ReapResult,\n ReapedBranch,\n RepoRootFinder,\n loadAndValidate,\n 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 ) {}\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(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(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(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(\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 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(\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(\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(\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 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(\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(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 .webpieces/logs/branch-mutations.log —\\n'\n + 'recover any of them with the `recover=` command on its line.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedBranch): string {\n const sha = entry.sha !== '' ? ` (was ${entry.sha.slice(0, 8)})` : '';\n // The archive tag is printed inline because it is the ONE thing that makes this delete casually\n // reversible — a name a human can type, rather than a sha they have to go dig out of a log.\n const archived = entry.archiveTag !== ''\n ? `\\n archived → ${entry.archiveTag} (restore: ${this.archiver.restoreCommand(entry.branch, entry.archiveTag)})`\n : '';\n return ` ✓ ${entry.branch}${sha} — ${entry.reason}${archived}\\n`;\n }\n\n /**\n * The spared branches a human can meaningfully rule on, grouped and ordered most-safe first.\n *\n * Branches spared as IN_USE — checked out in a worktree — are still excluded here, but the reason\n * is no longer \"git would simply refuse\". That premise died the moment worktrees became reapable.\n * The real reason is the ORDER in run(): the worktree pass has already run, so an IN_USE branch is\n * one of exactly two things. Either its worktree was dead and the reap took the branch with it (so\n * it is not in this list at all), or its worktree is one we are deliberately keeping — locked, held\n * open by uncommitted work, or the one we are standing in — and offering to delete the branch out\n * from under a live checkout is not a question worth asking. The pair is offered TOGETHER by the\n * worktree prompt, which shows both the path and the branch it holds, or it is not offered at all.\n */\n private promptable(spared: DeletableBranch[]): DeletableBranch[] {\n const out: DeletableBranch[] = [];\n for (const classification of 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 block: T[], kind: string, kinds: string, flag: string, selection: DeleteSelection,\n 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(kinds, flag, block.length));\n return picked;\n }\n if (!options.prompts()) {\n process.stdout.write(this.flagHint(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(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 + '.webpieces/logs/branch-mutations.log\\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 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"]}
@@ -8,6 +8,7 @@ import { BuildAffected } from '../workflow/build-affected';
8
8
  import { BuildGateLog } from '../workflow/build-gate-log';
9
9
  import { MergeState } from '../workflow/merge-state';
10
10
  import { ReviewStageReceiptService } from '../workflow/review-stage-receipt';
11
+ import { StageOutputLog } from '../workflow/stage-output-log';
11
12
  import { PrMerger } from '../workflow/pr-merger';
12
13
  import { FinishBanner } from '../workflow/finish-banner';
13
14
  import { MergeBodyTempFile } from '../workflow/merge-body-temp-file';
@@ -39,8 +40,16 @@ export declare class FinishUpsertPrCommand {
39
40
  private readonly mergeBodyFile;
40
41
  private readonly commentUpserter;
41
42
  private readonly squashSettings;
42
- constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, buildLog: BuildGateLog, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistComment: ChecklistCommentRenderer, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenanceEnforcer: ProvenanceEnforcer, receipts: ReviewStageReceiptService, banner: FinishBanner, mergeBodyFile: MergeBodyTempFile, commentUpserter: PrCommentUpserter, squashSettings: SquashSettingsEnforcer);
43
+ private readonly stageConsole;
44
+ constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, buildLog: BuildGateLog, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistComment: ChecklistCommentRenderer, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenanceEnforcer: ProvenanceEnforcer, receipts: ReviewStageReceiptService, banner: FinishBanner, mergeBodyFile: MergeBodyTempFile, commentUpserter: PrCommentUpserter, squashSettings: SquashSettingsEnforcer, stageConsole: StageOutputLog);
45
+ /**
46
+ * Runs with this stage's console CAPTURED to a file (see StageOutputLog). What still reaches the
47
+ * terminal is what an agent must act on: the build heartbeat and result, and the closing banner with
48
+ * the PR link and whatever is still owed. The dashboard computation, the gh round trips and the
49
+ * comment upserts are in the file.
50
+ */
43
51
  run(): Promise<void>;
52
+ private runStage;
44
53
  /**
45
54
  * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.
46
55
  *
@@ -56,8 +65,8 @@ export declare class FinishUpsertPrCommand {
56
65
  *
57
66
  * The path is recomputed rather than stored, and that is sound precisely BECAUSE this branch is only
58
67
  * reached when the receipt covers the current HEAD: same worktree, same branch, same sha ⇒ the same
59
- * deterministic filename stage ② wrote. Empty string whenever capture is off or the file is gone, so
60
- * a user without `~/.webpieces/config.json` sees the pre-existing single line unchanged.
68
+ * deterministic filename stage ② wrote. Empty string when the file is gone a log that was reaped or
69
+ * never written must not be named as though a reader could open it.
61
70
  */
62
71
  private skippedBuildLogNote;
63
72
  /**