@webpieces/pr-gate 0.4.691 → 0.4.692

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.691",
3
+ "version": "0.4.692",
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.692",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -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"]}
@@ -53,7 +53,7 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
53
53
  }
54
54
  const retention = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.landPr.branchRetention;
55
55
  const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);
56
- process.stdout.write(this.render(result));
56
+ process.stdout.write(this.render(repoRoot, result));
57
57
  process.stdout.write(this.signal.line(this.outcomeOf(result)));
58
58
  }
59
59
  /**
@@ -97,14 +97,14 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
97
97
  }
98
98
  // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's
99
99
  // point of view "nothing happened" is a whole answer. Here it never is: we asked for exactly one.
100
- render(result) {
100
+ render(repoRoot, result) {
101
101
  if (result.reaped.length === 0 && result.failed.length === 0) {
102
102
  let out = '\n' + SEP + '🛑 Nothing removed\n' + SEP + '\n';
103
103
  for (const tree of result.spared)
104
104
  out += ` · ${tree.path} — ${tree.reason}\n`;
105
105
  return out;
106
106
  }
107
- return this.worktreeSection.report(result);
107
+ return this.worktreeSection.report(repoRoot, result);
108
108
  }
109
109
  // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller
110
110
  // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.
@@ -1 +1 @@
1
- {"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n const target = this.worktreeSection.verdicts(repoRoot)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
1
+ {"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(repoRoot, result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n const target = this.worktreeSection.verdicts(repoRoot)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(repoRoot, result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapResult, WorktreeReaper, WorktreeService, WorktreeWorkInFlight } from '@webpieces/rules-config';
1
+ import { BranchMutationLog, MutationVerb, DeletableWorktree, MergedBranchesService, WorktreeReapResult, WorktreeReaper, WorktreeService, WorktreeWorkInFlight } from '@webpieces/rules-config';
2
2
  /**
3
3
  * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.
4
4
  *
@@ -18,7 +18,8 @@ export declare class WorktreeCleanupSection {
18
18
  private readonly mergedBranches;
19
19
  private readonly reaper;
20
20
  private readonly worktreeService;
21
- constructor(mergedBranches: MergedBranchesService, reaper: WorktreeReaper, worktreeService: WorktreeService);
21
+ private readonly mutationLog;
22
+ constructor(mergedBranches: MergedBranchesService, reaper: WorktreeReaper, worktreeService: WorktreeService, mutationLog: BranchMutationLog);
22
23
  /**
23
24
  * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
24
25
  * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine
@@ -53,7 +54,7 @@ export declare class WorktreeCleanupSection {
53
54
  withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[];
54
55
  protected workInFlight(worktreePath: string): WorktreeWorkInFlight;
55
56
  reap(repoRoot: string, verb: MutationVerb, targets: DeletableWorktree[], retention: string): WorktreeReapResult;
56
- report(result: WorktreeReapResult): string;
57
+ report(repoRoot: string, result: WorktreeReapResult): string;
57
58
  private reapedLine;
58
59
  /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */
59
60
  sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string;
@@ -24,10 +24,15 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
24
24
  mergedBranches;
25
25
  reaper;
26
26
  worktreeService;
27
- constructor(mergedBranches, reaper, worktreeService) {
27
+ mutationLog;
28
+ constructor(mergedBranches, reaper, worktreeService,
29
+ // COMPUTED, not restated: the branch-mutation log is per-worktree, so the relative literal this
30
+ // message used to carry named a file that does not exist in a linked worktree.
31
+ mutationLog) {
28
32
  this.mergedBranches = mergedBranches;
29
33
  this.reaper = reaper;
30
34
  this.worktreeService = worktreeService;
35
+ this.mutationLog = mutationLog;
31
36
  }
32
37
  /**
33
38
  * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is
@@ -100,7 +105,7 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
100
105
  reap(repoRoot, verb, targets, retention) {
101
106
  return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);
102
107
  }
103
- report(result) {
108
+ report(repoRoot, result) {
104
109
  if (result.reaped.length === 0 && result.failed.length === 0)
105
110
  return '';
106
111
  let out = '\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\n` + SEP + '\n';
@@ -113,7 +118,7 @@ let WorktreeCleanupSection = class WorktreeCleanupSection {
113
118
  }
114
119
  // Printed on success too: removing a worktree deletes real files, and a human who cannot see
115
120
  // how to undo that has to take it on trust — which is precisely what nobody should have to do.
116
- out += '\nEvery removal is logged in .webpieces/logs/branch-mutations.log (phase REAP_WORKTREE)\n'
121
+ out += `\nEvery removal is logged (phase REAP_WORKTREE) in\n${this.mutationLog.branchMutationLogPath(repoRoot)}\n`
117
122
  + 'with the `recover=` command that brings back both the directory and its branch.\n';
118
123
  return out;
119
124
  }
@@ -166,6 +171,7 @@ exports.WorktreeCleanupSection = WorktreeCleanupSection = tslib_1.__decorate([
166
171
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
167
172
  tslib_1.__metadata("design:paramtypes", [rules_config_1.MergedBranchesService,
168
173
  rules_config_1.WorktreeReaper,
169
- rules_config_1.WorktreeService])
174
+ rules_config_1.WorktreeService,
175
+ rules_config_1.BranchMutationLog])
170
176
  ], WorktreeCleanupSection);
171
177
  //# sourceMappingURL=worktree-cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n WorktreeService,\n WorktreeWorkInFlight,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n private readonly worktreeService: WorktreeService,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of ADJUDICATED_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or\n * untracked work — with a printed line for each one that is spared.\n *\n * THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits\n * of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed\n * in the last twenty minutes, and the two are indistinguishable by ref alone. `git status\n * --porcelain` is the difference, it is one local spawn, and it fails safe to \"dirty\".\n *\n * It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and\n * git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the\n * backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody\n * was asked about, which is why the husk path states the spare instead of tripping over it.\n */\n withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[] {\n const clean: DeletableWorktree[] = [];\n let spared = '';\n for (const tree of husks) {\n const held = this.workInFlight(tree.path);\n if (held.held) {\n // The REASON is printed verbatim, because \"it has uncommitted files\" and \"git would\n // not tell me\" send an operator to two different places.\n spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\\n`\n + ' nothing archives that, so it is left exactly where it is\\n';\n continue;\n }\n clean.push(tree);\n }\n if (spared !== '') {\n process.stdout.write('\\nZero-commit worktrees SPARED because work may be in flight in them:\\n' + spared);\n }\n return clean;\n }\n\n // Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no\n // real worktrees on disk.\n protected workInFlight(worktreePath: string): WorktreeWorkInFlight {\n return this.worktreeService.workInFlight(worktreePath);\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += '\\nEvery removal is logged in .webpieces/logs/branch-mutations.log (phase REAP_WORKTREE)\\n'\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
1
+ {"version":3,"file":"worktree-cleanup.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/worktree-cleanup.ts"],"names":[],"mappings":";;;;AAAA,0DAeiC;AACjC,yCAA2D;AAE3D,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AAEI,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IAEV;IACA;IACA;IAGA;IANrB,YACqB,cAAqC,EACrC,MAAsB,EACtB,eAAgC;IACjD,gGAAgG;IAChG,+EAA+E;IAC9D,WAA8B;QAL9B,mBAAc,GAAd,cAAc,CAAuB;QACrC,WAAM,GAAN,MAAM,CAAgB;QACtB,oBAAe,GAAf,eAAe,CAAiB;QAGhC,gBAAW,GAAX,WAAW,CAAmB;IAChD,CAAC;IAEJ;;;;OAIG;IACH,QAAQ,CAAC,QAAgB;QACrB,OAAO,IAAI,CAAC,cAAc,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC;IACzE,CAAC;IAED,YAAY,CAAC,QAA6B;QACtC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAuB,EAAW,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjF,CAAC;IAED;;;;;;;;OAQG;IACH,UAAU,CAAC,QAA6B;QACpC,MAAM,GAAG,GAAwB,EAAE,CAAC;QACpC,KAAK,MAAM,cAAc,IAAI,0CAA2B,EAAE,CAAC;YACvD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC1B,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,cAAc,KAAK,cAAc;oBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClF,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,kBAAkB,CAAC,KAA0B;QACzC,MAAM,KAAK,GAAwB,EAAE,CAAC;QACtC,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,oFAAoF;gBACpF,yDAAyD;gBACzD,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,KAAK;sBAC3D,oEAAoE,CAAC;gBAC3E,SAAS;YACb,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;QACD,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yEAAyE,GAAG,MAAM,CAAC,CAAC;QAC7G,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,gGAAgG;IAChG,0BAA0B;IAChB,YAAY,CAAC,YAAoB;QACvC,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,CACA,QAAgB,EAChB,IAAkB,EAClB,OAA4B,EAC5B,SAAiB;QAEjB,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,CAAC,QAAgB,EAAE,MAA0B;QAC/C,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAExE,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,cAAc,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,qBAAqB,GAAG,GAAG,GAAG,IAAI,CAAC;QACpG,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;YAAE,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEjE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,GAAG,IAAI,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,sCAAsC,CAAC;YACnF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;gBAAE,GAAG,IAAI,OAAO,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,KAAK,IAAI,CAAC;QACrF,CAAC;QACD,6FAA6F;QAC7F,+FAA+F;QAC/F,GAAG,IAAI,uDAAuD,IAAI,CAAC,WAAW,CAAC,qBAAqB,CAAC,QAAQ,CAAC,IAAI;cAC5G,mFAAmF,CAAC;QAC1F,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,UAAU,CAAC,KAAqB;QACpC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC;QAC1E,+FAA+F;QAC/F,0FAA0F;QAC1F,MAAM,OAAO,GAAG,oBAAoB,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,kGAAkG;QAClG,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa;YACvD,CAAC,CAAC,2BAA2B,KAAK,CAAC,MAAM,oCAAoC;YAC7E,CAAC,CAAC,EAAE,CAAC;QACT,OAAO,OAAO,KAAK,CAAC,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,MAAM,GAAG,OAAO,GAAG,OAAO,IAAI,CAAC;IAChF,CAAC;IAED,2FAA2F;IAC3F,WAAW,CAAC,QAA6B,EAAE,OAA4B;QACnE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAuB,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAClF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAC1B,CAAC,IAAuB,EAAW,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;eACtE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;QACnD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACnC,IAAI,GAAG,GAAG,wCAAwC,CAAC;QACnD,KAAK,MAAM,IAAI,IAAI,MAAM;YAAE,GAAG,IAAI,OAAO,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC;QACxE,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,YAAY,CAAC,cAAsB;QACvC,OAAO,cAAc,KAAK,oCAAqB;eACxC,cAAc,KAAK,qCAAsB;eACzC,cAAc,KAAK,sCAAuB;eAC1C,cAAc,KAAK,sCAAuB,CAAC;IACtD,CAAC;IAED,mGAAmG;IACnG,uCAAuC;IACvC,WAAW,CAAC,UAA+B;QACvC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG;cACd,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,8CAA8C,GAAG,GAAG,GAAG,IAAI;cAC1F,6FAA6F;cAC7F,8FAA8F;cAC9F,oFAAoF,CAAC;QAC3F,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;YAC3B,GAAG,IAAI,MAAM,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,cAAc,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC;QAC5F,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;CACJ,CAAA;AAzJY,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,oCAAqB;QAC7B,6BAAc;QACL,8BAAe;QAGnB,gCAAiB;GAP1C,sBAAsB,CAyJlC","sourcesContent":["import {\n BranchMutationLog,\n MutationVerb,\n DeletableWorktree,\n MergedBranchesService,\n ReapedWorktree,\n WorktreeReapResult,\n WorktreeReaper,\n WorktreeService,\n WorktreeWorkInFlight,\n CLASSIFICATION_LOCKED,\n CLASSIFICATION_CURRENT,\n CLASSIFICATION_DETACHED,\n CLASSIFICATION_PRUNABLE,\n ADJUDICATED_CLASSIFICATIONS,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The WORKTREE half of `wp-cleanup` — the verdicts, the reap and the human-facing text.\n *\n * Split out of CleanupCommand rather than bolted onto it because the two halves answer different\n * questions: a branch is a ref, a worktree is a DIRECTORY OF FILES, and the second one needs its own\n * report (paths, not just names), its own restore command (`git worktree add`, not `git checkout -b`)\n * and its own spared vocabulary (locked, detached, \"you are standing in it\"). Keeping them in one class\n * meant every string had to hedge about which kind of thing it was talking about.\n *\n * WHY worktrees are reaped BEFORE branches in wp-cleanup: a worktree HOLDS its branch, so that branch\n * is spared as `in-use` with \"remove that worktree before deleting the branch\". Reaping the worktree\n * first is what makes the branch reapable — the reap takes the branch with it, and the branch pass that\n * follows recomputes its verdicts against the post-removal truth. Run the other way round, every\n * worktree-held branch survives forever, which is exactly the deadlock this change exists to break.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeCleanupSection {\n constructor(\n private readonly mergedBranches: MergedBranchesService,\n private readonly reaper: WorktreeReaper,\n private readonly worktreeService: WorktreeService,\n // COMPUTED, not restated: the branch-mutation log is per-worktree, so the relative literal this\n // message used to carry named a file that does not exist in a linked worktree.\n private readonly mutationLog: BranchMutationLog,\n ) {}\n\n /**\n * FRESH verdicts — never the cache on disk. Same rule the branch half follows: the cached file is\n * deliberately allowed to go stale, which is fine for BLOCKING a `git worktree add` and never fine\n * for removing a directory, since the tree may have gained uncommitted work since it was written.\n */\n verdicts(repoRoot: string): DeletableWorktree[] {\n return this.mergedBranches.computeMergedBranches(repoRoot).worktrees;\n }\n\n provablyDead(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n return verdicts.filter((tree: DeletableWorktree): boolean => tree.deletable);\n }\n\n /**\n * The spared worktrees a human can meaningfully rule on, grouped most-safe first — the same\n * classification order the branch prompt uses, because it is literally the same verdict on the\n * branch the worktree holds.\n *\n * Deliberately excluded: LOCKED (a lock is standing and we cannot say whose), CURRENT (removing your own cwd\n * is not a thing to offer), DETACHED (no branch, so nothing to archive and nothing to judge) and\n * PRUNABLE (already provably dead — it is in the auto-reap list, not this one).\n */\n promptable(verdicts: DeletableWorktree[]): DeletableWorktree[] {\n const out: DeletableWorktree[] = [];\n for (const classification of ADJUDICATED_CLASSIFICATIONS) {\n for (const tree of verdicts) {\n if (!tree.deletable && tree.classification === classification) out.push(tree);\n }\n }\n return out;\n }\n\n /**\n * The zero-commit worktrees that are genuinely husks — the ones holding no uncommitted or\n * untracked work — with a printed line for each one that is spared.\n *\n * THIS IS THE ONE CHECK THAT MAKES REAPING A ZERO-COMMIT WORKTREE SAFE. A branch with no commits\n * of its own can lose nothing; a DIRECTORY with no commits can lose everything an agent has typed\n * in the last twenty minutes, and the two are indistinguishable by ref alone. `git status\n * --porcelain` is the difference, it is one local spawn, and it fails safe to \"dirty\".\n *\n * It is applied ONLY to the husks. Anything with unique commits is decided by flag or prompt, and\n * git's own refusal to remove a dirty worktree (WorktreeReaper never passes `--force`) is the\n * backstop there — but a backstop that reports a FAILURE is not good enough for a delete nobody\n * was asked about, which is why the husk path states the spare instead of tripping over it.\n */\n withoutUncommitted(husks: DeletableWorktree[]): DeletableWorktree[] {\n const clean: DeletableWorktree[] = [];\n let spared = '';\n for (const tree of husks) {\n const held = this.workInFlight(tree.path);\n if (held.held) {\n // The REASON is printed verbatim, because \"it has uncommitted files\" and \"git would\n // not tell me\" send an operator to two different places.\n spared += ` · ${tree.path} [${tree.branch}] — ${held.reason};\\n`\n + ' nothing archives that, so it is left exactly where it is\\n';\n continue;\n }\n clean.push(tree);\n }\n if (spared !== '') {\n process.stdout.write('\\nZero-commit worktrees SPARED because work may be in flight in them:\\n' + spared);\n }\n return clean;\n }\n\n // Seam: one git spawn per candidate, overridden in the spec so the decision is testable with no\n // real worktrees on disk.\n protected workInFlight(worktreePath: string): WorktreeWorkInFlight {\n return this.worktreeService.workInFlight(worktreePath);\n }\n\n reap(\n repoRoot: string,\n verb: MutationVerb,\n targets: DeletableWorktree[],\n retention: string,\n ): WorktreeReapResult {\n return this.reaper.reapWorktrees(repoRoot, process.cwd(), verb, targets, retention);\n }\n\n report(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) return '';\n\n let out = '\\n' + SEP + `🌲 Removed ${String(result.reaped.length)} dead worktree(s)\\n` + SEP + '\\n';\n for (const entry of result.reaped) out += this.reapedLine(entry);\n\n if (result.failed.length > 0) {\n out += `\\n⚠️ ${String(result.failed.length)} worktree(s) could not be removed:\\n`;\n for (const entry of result.failed) out += ` ✗ ${entry.path} — ${entry.error}\\n`;\n }\n // Printed on success too: removing a worktree deletes real files, and a human who cannot see\n // how to undo that has to take it on trust — which is precisely what nobody should have to do.\n out += `\\nEvery removal is logged (phase REAP_WORKTREE) in\\n${this.mutationLog.branchMutationLogPath(repoRoot)}\\n`\n + 'with the `recover=` command that brings back both the directory and its branch.\\n';\n return out;\n }\n\n private reapedLine(entry: ReapedWorktree): string {\n const branch = entry.branch !== '' ? ` [${entry.branch}]` : ' [detached]';\n // The restore command is printed inline for the same reason the branch half prints the archive\n // tag: it is the one thing that makes this reversible without going and digging in a log.\n const restore = `\\n restore: ${this.reaper.restoreCommand(entry)}`;\n // A directory that went while its branch survived is a real half-state and must not read as done.\n const partial = entry.branch !== '' && !entry.branchDeleted\n ? `\\n ⚠️ the branch '${entry.branch}' was NOT deleted — git refused it`\n : '';\n return ` ✓ ${entry.path}${branch} — ${entry.reason}${restore}${partial}\\n`;\n }\n\n /** The spared worktrees, with WHY — including the ones nobody will ever be asked about. */\n sparedBlock(verdicts: DeletableWorktree[], removed: DeletableWorktree[]): string {\n const gone = new Set(removed.map((tree: DeletableWorktree): string => tree.path));\n const spared = verdicts.filter(\n (tree: DeletableWorktree): boolean => !tree.deletable && !gone.has(tree.path)\n && this.isMechanical(tree.classification));\n if (spared.length === 0) return '';\n let out = '\\nWorktrees deliberately left alone:\\n';\n for (const tree of spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n\n private isMechanical(classification: string): boolean {\n return classification === CLASSIFICATION_LOCKED\n || classification === CLASSIFICATION_CURRENT\n || classification === CLASSIFICATION_DETACHED\n || classification === CLASSIFICATION_PRUNABLE;\n }\n\n // The table a human answers: path, branch, and the same reason the branch prompt would show, since\n // the verdict IS the branch's verdict.\n promptBlock(promptable: DeletableWorktree[]): string {\n let out = '\\n' + SEP\n + `🤔 ${String(promptable.length)} worktree(s) are probably dead — your call\\n` + SEP + '\\n'\n + 'Removing one deletes its DIRECTORY and its branch. The branch is archived as a tag first,\\n'\n + 'so both come back with one `git worktree add -b …` — but uncommitted or untracked files in\\n'\n + 'that directory are NOT archived, and git will refuse the removal if any exist.\\n\\n';\n for (let i = 0; i < promptable.length; i += 1) {\n const tree = promptable[i];\n out += ` [${String(i + 1)}] ${tree.path}\\n [${tree.branch}] — ${tree.reason}\\n`;\n }\n return out;\n }\n}\n"]}
@@ -25,7 +25,8 @@ let RunUpdate = class RunUpdate {
25
25
  }
26
26
  // `finishCommand` is the command the AI is told to run after resolving conflicts (standalone passes
27
27
  // `wp-finish-update`, PR flow passes `wp-finish-upsert-pr`). `verb` is the invoking bin, threaded so
28
- // every branch mutation is recorded in `.webpieces/logs/branch-mutations.log`. `pushRemote` is false
28
+ // every branch mutation is recorded in the branch-mutation log (BranchMutationLog.branchMutationLogPath
29
+ // resolves it for THIS tree — it is per-worktree). `pushRemote` is false
29
30
  // for the PR flow, which pushes exactly once from wp-finish-upsert-pr (see MergeEndOptions).
30
31
  // eslint-disable-next-line @typescript-eslint/max-params
31
32
  async runUpdateFromMain(repoRoot, verb, finishCommand, pushRemote) {
@@ -1 +1 @@
1
- {"version":3,"file":"run-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/run-update.ts"],"names":[],"mappings":"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","sourcesContent":["import { MutationVerb, BranchMutationEvent, logBranchMutation } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { MergeState } from './merge-state';\nimport { MergeStart, MergeContext } from './merge-start';\nimport { MergeEnd, MergeEndOptions } from './merge-end';\n\nexport type UpdateOutcome = 'finalized' | 'conflict' | 'unvalidatedResume';\n\n// The shared \"sync this feature branch from main\" engine — a thin composition of the two lifecycle\n// primitives (merge-START brings main in, merge-END finalizes). INTERNAL, not a bin: `wp-start-update`\n// (standalone) and `wp-start-upsert-pr` (PR flow) both call it. It NEVER creates a PR and NEVER\n// process.exits; it RETURNS an outcome so each caller prints the handoff that fits its context.\n@injectable(bindingScopeValues.Singleton)\nexport class RunUpdate {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly mergeState: MergeState,\n private readonly mergeStart: MergeStart,\n private readonly mergeEnd: MergeEnd,\n ) {}\n\n // `finishCommand` is the command the AI is told to run after resolving conflicts (standalone passes\n // `wp-finish-update`, PR flow passes `wp-finish-upsert-pr`). `verb` is the invoking bin, threaded so\n // every branch mutation is recorded in `.webpieces/logs/branch-mutations.log`. `pushRemote` is false\n // for the PR flow, which pushes exactly once from wp-finish-upsert-pr (see MergeEndOptions).\n // eslint-disable-next-line @typescript-eslint/max-params\n async runUpdateFromMain(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'START'));\n const outcome = await this.runUpdate(repoRoot, verb, finishCommand, pushRemote);\n const end = new BranchMutationEvent(verb, 'END');\n end.outcome = outcome;\n logBranchMutation(repoRoot, end);\n return outcome;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private async runUpdate(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n\n // Resume path: an in-progress merge is the `merge-<n>/` run dir that holds a marker.\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n if (activeDir) {\n const existing = this.mergeState.readMergeMarker(activeDir);\n if (existing === null || !existing.validated) return 'unvalidatedResume';\n // Already validated → just finalize the branch swap (reads THIS run dir's marker).\n await this.mergeEnd.mergeEnd(\n repoRoot, verb, activeDir,\n new MergeContext(existing.currentBranch, existing.squashBranch, existing.backupBranch, existing.prNumber),\n new MergeEndOptions(null, pushRemote),\n );\n return 'finalized';\n }\n\n // Fresh update: mergeStart picks the slot number, creates its own `merge-<n>/` run dir, and\n // returns that path for merge-END to finalize against.\n const result = await this.mergeStart.mergeStart(repoRoot, verb, home, finishCommand);\n if (result.status === 'conflict' || result.context === null) {\n return 'conflict';\n }\n await this.mergeEnd.mergeEnd(repoRoot, verb, result.runDir, result.context, new MergeEndOptions(null, pushRemote));\n return 'finalized';\n }\n}\n"]}
1
+ {"version":3,"file":"run-update.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/run-update.ts"],"names":[],"mappings":"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","sourcesContent":["import { MutationVerb, BranchMutationEvent, logBranchMutation } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { MergeState } from './merge-state';\nimport { MergeStart, MergeContext } from './merge-start';\nimport { MergeEnd, MergeEndOptions } from './merge-end';\n\nexport type UpdateOutcome = 'finalized' | 'conflict' | 'unvalidatedResume';\n\n// The shared \"sync this feature branch from main\" engine — a thin composition of the two lifecycle\n// primitives (merge-START brings main in, merge-END finalizes). INTERNAL, not a bin: `wp-start-update`\n// (standalone) and `wp-start-upsert-pr` (PR flow) both call it. It NEVER creates a PR and NEVER\n// process.exits; it RETURNS an outcome so each caller prints the handoff that fits its context.\n@injectable(bindingScopeValues.Singleton)\nexport class RunUpdate {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly mergeState: MergeState,\n private readonly mergeStart: MergeStart,\n private readonly mergeEnd: MergeEnd,\n ) {}\n\n // `finishCommand` is the command the AI is told to run after resolving conflicts (standalone passes\n // `wp-finish-update`, PR flow passes `wp-finish-upsert-pr`). `verb` is the invoking bin, threaded so\n // every branch mutation is recorded in the branch-mutation log (BranchMutationLog.branchMutationLogPath\n // resolves it for THIS tree — it is per-worktree). `pushRemote` is false\n // for the PR flow, which pushes exactly once from wp-finish-upsert-pr (see MergeEndOptions).\n // eslint-disable-next-line @typescript-eslint/max-params\n async runUpdateFromMain(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n logBranchMutation(repoRoot, new BranchMutationEvent(verb, 'START'));\n const outcome = await this.runUpdate(repoRoot, verb, finishCommand, pushRemote);\n const end = new BranchMutationEvent(verb, 'END');\n end.outcome = outcome;\n logBranchMutation(repoRoot, end);\n return outcome;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private async runUpdate(repoRoot: string, verb: MutationVerb, finishCommand: string, pushRemote: boolean): Promise<UpdateOutcome> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n\n // Resume path: an in-progress merge is the `merge-<n>/` run dir that holds a marker.\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n if (activeDir) {\n const existing = this.mergeState.readMergeMarker(activeDir);\n if (existing === null || !existing.validated) return 'unvalidatedResume';\n // Already validated → just finalize the branch swap (reads THIS run dir's marker).\n await this.mergeEnd.mergeEnd(\n repoRoot, verb, activeDir,\n new MergeContext(existing.currentBranch, existing.squashBranch, existing.backupBranch, existing.prNumber),\n new MergeEndOptions(null, pushRemote),\n );\n return 'finalized';\n }\n\n // Fresh update: mergeStart picks the slot number, creates its own `merge-<n>/` run dir, and\n // returns that path for merge-END to finalize against.\n const result = await this.mergeStart.mergeStart(repoRoot, verb, home, finishCommand);\n if (result.status === 'conflict' || result.context === null) {\n return 'conflict';\n }\n await this.mergeEnd.mergeEnd(repoRoot, verb, result.runDir, result.context, new MergeEndOptions(null, pushRemote));\n return 'finalized';\n }\n}\n"]}