@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.
- package/package.json +2 -2
- package/src/scripts/commands/build-command.d.ts +3 -3
- package/src/scripts/commands/build-command.js +4 -4
- package/src/scripts/commands/build-command.js.map +1 -1
- package/src/scripts/commands/cleanup-command.d.ts +3 -2
- package/src/scripts/commands/cleanup-command.js +27 -20
- package/src/scripts/commands/cleanup-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +12 -3
- package/src/scripts/commands/finish-upsert-pr-command.js +25 -10
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/reap-worktree-command.js +3 -3
- package/src/scripts/commands/reap-worktree-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +15 -1
- package/src/scripts/commands/review-upsert-pr-command.js +25 -4
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/worktree-cleanup.d.ts +4 -3
- package/src/scripts/commands/worktree-cleanup.js +10 -4
- package/src/scripts/commands/worktree-cleanup.js.map +1 -1
- package/src/scripts/workflow/build-affected.d.ts +11 -30
- package/src/scripts/workflow/build-affected.js +25 -61
- package/src/scripts/workflow/build-affected.js.map +1 -1
- package/src/scripts/workflow/build-gate-log.d.ts +14 -29
- package/src/scripts/workflow/build-gate-log.js +34 -91
- package/src/scripts/workflow/build-gate-log.js.map +1 -1
- package/src/scripts/workflow/gate-log-file.d.ts +55 -0
- package/src/scripts/workflow/gate-log-file.js +136 -0
- package/src/scripts/workflow/gate-log-file.js.map +1 -0
- package/src/scripts/workflow/run-update.js +2 -1
- package/src/scripts/workflow/run-update.js.map +1 -1
- package/src/scripts/workflow/stage-output-log.d.ts +40 -0
- package/src/scripts/workflow/stage-output-log.js +137 -0
- 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.
|
|
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.
|
|
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,
|
|
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
|
|
43
|
+
* headline, and the log-file stage id.
|
|
44
44
|
*
|
|
45
45
|
* ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────
|
|
46
|
-
*
|
|
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,
|
|
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
|
|
57
|
+
* headline, and the log-file stage id.
|
|
58
58
|
*
|
|
59
59
|
* ─── The output goes to a FILE, unconditionally ────────────────────────────────────────────────────
|
|
60
|
-
*
|
|
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
|
|
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":";;;;AAAA,0DAEiC;AACjC,yCAA2D;AAE3D,+DAA6E;AAC7E,+DAAyD;AAEzD,yFAAyF;AAC5E,QAAA,0BAA0B,GAAG,4BAA4B,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAa,YAAY;IACrB,KAAK,CAAU;IAEf,qGAAqG;IACrG,kGAAkG;IAClG,kGAAkG;IAClG,8FAA8F;IAC9F,yDAAyD;IACzD,YAAY,KAAc;QACtB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AAXD,oCAWC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IACA;IACA;IAJrB,YACqB,aAA4B,EAC5B,cAA8B,EAC9B,SAAoB,EACpB,UAA6B;QAH7B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,cAAS,GAAT,SAAS,CAAW;QACpB,eAAU,GAAV,UAAU,CAAmB;IAC/C,CAAC;IAEJ,GAAG,CAAC,IAAkB;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC7C,8FAA8F;QAC9F,gGAAgG;QAChG,wEAAwE;QACxE,OAAO,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CACjE,eAAe,EACf,eAAe,EACf,eAAe,EACf,4BAAW,EACX,IAAI,CACP,CAAC,CAAC;IACP,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,oBAAoB;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,mBAAmB,CAAC;QACvD,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG;YAAE,OAAO;QAC9B,MAAM,IAAI,4BAAa,CACnB,kCAA0B,EAC1B,mCAAmC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,8BAA8B;YACpF,GAAG,MAAM,CAAC,GAAG,CAAC,8EAA8E;YAC5F,8DAA8D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EACnF,SAAS,EACT,SAAS,EACT;YACI,IAAI,qBAAM,CACN,gFAAgF;kBAC9E,qFAAqF;kBACrF,+EAA+E,EACjF,IAAI,CAAC;YACT,IAAI,qBAAM,CACN,qFAAqF;kBACnF,2EAA2E,CAAC;YAClF,IAAI,qBAAM,CACN,uEAAuE;kBACrE,0EAA0E,CAAC;YACjF,IAAI,qBAAM,CACN,wEAAwE;kBACtE,iFAAiF,CAAC;YACxF,IAAI,qBAAM,CACN,iFAAiF;kBAC/E,mCAAmC,CAAC;SAC7C,CACJ,CAAC;IACN,CAAC;IAED,gGAAgG;IAChG,oGAAoG;IAC5F,QAAQ,CAAC,IAA6B;QAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,KAAmB,EAAU,EAAE;YAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;YAC3E,OAAO,OAAO,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,IAAI,IAAI,SAAS,IAAI;kBACvD,UAAU,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,EAAE,QAAQ,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG;kBACxE,eAAe,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;QACxC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;CACJ,CAAA;AA/EY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGD,8BAAa;QACZ,6BAAc;QACnB,wBAAS;QACR,gCAAiB;GALzC,YAAY,CA+ExB","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
|
-
|
|
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
|
-
|
|
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 +=
|
|
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
|
-
+
|
|
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
|
-
|
|
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
|
|
60
|
-
*
|
|
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
|
/**
|