@webpieces/pr-gate 0.4.590 → 0.4.591

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.
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LandPrCommand = void 0;
3
+ exports.LandPrCommand = exports.LandPrOptions = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const fs = tslib_1.__importStar(require("fs"));
7
+ const os = tslib_1.__importStar(require("os"));
7
8
  const path = tslib_1.__importStar(require("path"));
8
9
  const rules_config_1 = require("@webpieces/rules-config");
9
10
  const inversify_1 = require("inversify");
@@ -13,6 +14,16 @@ const landed_worktree_reaper_1 = require("../workflow/landed-worktree-reaper");
13
14
  const merge_info_index_1 = require("../workflow/merge-info-index");
14
15
  const pr_merger_1 = require("../workflow/pr-merger");
15
16
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
17
+ /** The `--fallback-title-only` opt-in. Data-only, per CLAUDE.md. */
18
+ class LandPrOptions {
19
+ /**
20
+ * Land with a commit body built from the PR TITLE and LINK alone, because the gated body rendered by
21
+ * `wp-finish-upsert-pr` is not on this machine. A HUMAN decision — see
22
+ * {@link LandPrCommand.fallbackBody} for why the PR description is deliberately not an option.
23
+ */
24
+ fallbackTitleOnly = false;
25
+ }
26
+ exports.LandPrOptions = LandPrOptions;
16
27
  /**
17
28
  * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.
18
29
  *
@@ -23,10 +34,24 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
23
34
  * still running when finish ran), this is the command that keeps main's history consistent.
24
35
  *
25
36
  * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`
26
- * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and
27
- * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is
28
- * missing, the honest answer is that finish has not run — so this fails and says so, rather than
29
- * inventing a body that would not match the PR.
37
+ * already did both and filed `merge-commit-body.md` under the PR's identity. Landing is a separate,
38
+ * later act, and rebuilding here would mean a second authoritative gate whose result nobody reads. THE
39
+ * BYTES THAT LAND ARE THE BYTES FINISH PRODUCED — that invariant survives everything below.
40
+ *
41
+ * ─── Two scopes, and only one of them is the tree's ────────────────────────────────────────────────
42
+ * This command does two different things, and they belong to different scopes:
43
+ *
44
+ * 1. THE MERGE, which belongs to the PR. The receipt is read from PrBodyStore —
45
+ * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/` — so landing works from the primary clone, from any
46
+ * linked worktree, and from a SECOND CLONE of the same repo, as long as the PR was posted from this
47
+ * machine. It used to be read out of this worktree's own `pr-review/<branch>/`, and the day the
48
+ * gated flow ran in the primary clone while landing happened in a worktree, it printed "Nothing to
49
+ * land" at a perfectly good PR.
50
+ * 2. THE BOOKKEEPING — archiving the pre-squash tip as `archive/<date>/<branch>`, promoting merge-info,
51
+ * and reaping the landed worktree. That half genuinely belongs to the tree that holds the branch,
52
+ * and it must NOT be attempted from anywhere else: another clone's `<branch>` is a different commit,
53
+ * so archiving it there would tag the wrong objects under the right name. When we are not that tree,
54
+ * the merge still happens and the bookkeeping is SKIPPED OUT LOUD, naming the tree that owes it.
30
55
  */
31
56
  let LandPrCommand = class LandPrCommand {
32
57
  repoRootFinder;
@@ -36,7 +61,9 @@ let LandPrCommand = class LandPrCommand {
36
61
  archiver;
37
62
  mergeInfoIndex;
38
63
  landedWorktree;
39
- constructor(repoRootFinder, aiBranchName, branchNaming, prMerger, archiver, mergeInfoIndex, landedWorktree) {
64
+ prBodies;
65
+ // eslint-disable-next-line @typescript-eslint/max-params
66
+ constructor(repoRootFinder, aiBranchName, branchNaming, prMerger, archiver, mergeInfoIndex, landedWorktree, prBodies) {
40
67
  this.repoRootFinder = repoRootFinder;
41
68
  this.aiBranchName = aiBranchName;
42
69
  this.branchNaming = branchNaming;
@@ -44,31 +71,27 @@ let LandPrCommand = class LandPrCommand {
44
71
  this.archiver = archiver;
45
72
  this.mergeInfoIndex = mergeInfoIndex;
46
73
  this.landedWorktree = landedWorktree;
74
+ this.prBodies = prBodies;
47
75
  }
48
- async run() {
76
+ async run(opts = new LandPrOptions()) {
49
77
  const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
50
78
  const base = this.branchNaming.baseBranchName((0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf8' }).trim());
51
- const prDir = (0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName());
52
- const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');
53
- if (!fs.existsSync(mergeBodyFile)) {
54
- throw new rules_config_1.InformAiError('\n' + SEP + '❌ Nothing to land — no rendered merge body\n' + SEP + '\n' +
55
- `Expected the compact squash-commit body at:\n ${mergeBodyFile}\n\n` +
56
- 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\n' +
57
- 'Its absence means finish has not run on this branch, so there is no reviewed PR to\n' +
58
- 'land. Run the gated flow first:\n\n' +
59
- ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\n' +
60
- ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\n' +
61
- ' # write review.json at the path wp-review-upsert-pr prints\n' +
62
- ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\n\n' +
63
- 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\n' + SEP);
64
- }
79
+ // The PR is resolved FIRST now, because its NUMBER is the key the body is filed under. That
80
+ // ordering is the whole change: the body is no longer a fact of this tree to be looked up by
81
+ // branch name, it is a fact of PR #N.
65
82
  const ref = this.prNumberAndTitle(base);
66
83
  if (ref === null) {
67
84
  throw new rules_config_1.InformAiError('\n' + SEP + '❌ No open PR found for this branch\n' + SEP + '\n' +
68
85
  `No open PR has head branch "${base}". Nothing to land.\n` +
69
86
  'If the PR is already merged, run `pnpm wp-cleanup`.\n' + SEP);
70
87
  }
88
+ const stored = this.prBodies.read(repoRoot, ref.number);
89
+ if (stored === null && !opts.fallbackTitleOnly)
90
+ throw this.notOnThisMachine(repoRoot, ref);
71
91
  process.stdout.write('\n' + SEP + `🚀 Landing PR #${ref.number}\n` + SEP + '\n');
92
+ const mergeBodyFile = stored !== null ? stored.bodyFile : this.writeFallbackBody(ref);
93
+ if (stored === null)
94
+ process.stdout.write(this.fallbackNotice(ref));
72
95
  const config = (0, rules_config_1.loadAndValidate)(repoRoot).prGate;
73
96
  const policy = config.mergeMode;
74
97
  // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever
@@ -80,18 +103,131 @@ let LandPrCommand = class LandPrCommand {
80
103
  // never assert a config value nobody set — that is exactly what used to print a CONFIG MISMATCH
81
104
  // about `mergeMode: AUTO` two lines above this command printing `mergeMode is NONE`.
82
105
  const outcome = this.prMerger.merge(base, `${ref.title} (#${ref.number})`, mergeBodyFile, new pr_merger_1.MergeIntent(policy ?? '', true));
83
- const archived = outcome.merged
84
- ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)
106
+ const bookkeeping = outcome.merged
107
+ ? this.bookkeeping(repoRoot, base, ref, config.landPr.branchRetention, stored)
85
108
  : '';
86
109
  process.stdout.write('\n' + SEP + (outcome.merged ? '✅ Landed\n' : 'ℹ️ Not landed yet\n') + SEP + '\n' +
87
110
  ` ${outcome.message}\n` +
88
- archived +
89
- (outcome.merged ? this.nextStep(repoRoot, base) : '') +
111
+ bookkeeping +
90
112
  (policy === rules_config_1.MERGE_MODE_AUTO
91
113
  ? ''
92
114
  : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\n`) +
93
115
  '\n');
94
116
  }
117
+ /**
118
+ * The refusal when this machine holds no gated body for the PR.
119
+ *
120
+ * It says MACHINE, not "branch" and not "worktree", because that is now the true scope: the receipt
121
+ * is filed under the PR's global identity, so the only way to be missing it is to be on a different
122
+ * computer from the one that posted the PR (or to be looking before finish ever ran).
123
+ */
124
+ notOnThisMachine(repoRoot, ref) {
125
+ const home = this.prBodies.home(repoRoot);
126
+ const degraded = home.degraded
127
+ ? `\n⚠️ This machine's webpieces state is DEGRADED to ${home.root}\n` +
128
+ ` (${home.reason}), so the body is only visible from this clone. Set\n` +
129
+ ` ${rules_config_1.WEBPIECES_STATE_HOME_ENV} to a writable directory to make it machine-global.\n`
130
+ : '';
131
+ return new rules_config_1.InformAiError('\n' + SEP + `❌ PR #${ref.number} was not found on this machine\n` + SEP + '\n' +
132
+ `Expected the gated squash-commit body at:\n ${path.join(this.prBodies.dirFor(repoRoot, ref.number), rules_config_1.MERGE_BODY_FILE)}\n\n` +
133
+ 'That file is written by `pnpm wp-finish-upsert-pr` ON THE MACHINE THAT POSTED THE PR, and it\n' +
134
+ 'is never regenerated here: it is the gate\'s receipt, and re-deriving it at land time would\n' +
135
+ 'be a second authoritative gate whose result nobody reads.\n' +
136
+ degraded +
137
+ this.legacySignpost(repoRoot) +
138
+ '\nSo either:\n\n' +
139
+ ' A. finish has not run on this branch — run the gated flow, which also posts the PR:\n' +
140
+ ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\n' +
141
+ ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\n' +
142
+ ' # write review.json at the path wp-review-upsert-pr prints\n' +
143
+ ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\n' +
144
+ ' Then re-run `pnpm wp-land-pr`. This is the right answer almost every time.\n\n' +
145
+ ' B. the PR was posted from a DIFFERENT machine. Land it from there, or re-run\n' +
146
+ ' `pnpm wp-finish-upsert-pr` here so this machine renders its own receipt.\n\n' +
147
+ ' C. A HUMAN may choose to land it WITHOUT the gated body:\n' +
148
+ ' pnpm wp-land-pr --fallback-title-only\n' +
149
+ ' That writes a commit body of the PR TITLE + LINK and a line saying the gated body was\n' +
150
+ ' unavailable. It is a degraded commit message and it says so in main\'s history forever.\n' +
151
+ ' DO NOT run it on your own initiative — ask the human, and let them decide.\n' + SEP);
152
+ }
153
+ /**
154
+ * The ONE-TIME signpost for a body written by the PREVIOUS release into this tree's `pr-review/`.
155
+ *
156
+ * LOUD, and never read — the RETIRED_CONFIG_KEYS pattern applied to an artifact. A silent fallback
157
+ * to the old path is exactly the shim CLAUDE.md forbids: it would re-create two homes for the
158
+ * receipt, and the stale one wins precisely when finish and land ran in different trees, which is
159
+ * the bug this whole change removes. Re-running finish re-files it under the PR's identity, which
160
+ * costs one command and leaves one home.
161
+ */
162
+ legacySignpost(repoRoot) {
163
+ const legacy = path.join((0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName()), rules_config_1.MERGE_BODY_FILE);
164
+ if (!fs.existsSync(legacy))
165
+ return '';
166
+ return '\n⚠️ A body written by an OLDER webpieces release is sitting at:\n' +
167
+ ` ${legacy}\n` +
168
+ ' It is deliberately NOT read. That path is per-worktree, so it is only correct while the\n' +
169
+ ' branch never changes trees — the assumption that broke. Re-run `pnpm wp-finish-upsert-pr`\n' +
170
+ ' to re-file it under this PR\'s identity, then land. (The stale file self-clears in 30 days.)\n';
171
+ }
172
+ /**
173
+ * The explicitly-degraded commit body: PR TITLE + LINK + a line saying what is missing. NEVER the PR
174
+ * DESCRIPTION.
175
+ *
176
+ * That exclusion is the point of this method, not an oversight. In a real consuming repo the PR
177
+ * description IS the full PR Gate Dashboard, and GitHub's default
178
+ * `squash_merge_commit_message=PR_BODY` dumping that dashboard into the commit is precisely the ugly
179
+ * git log this whole mechanism exists to prevent. A fallback that reached for the description would
180
+ * produce a WORSE commit than doing nothing, while looking more complete.
181
+ *
182
+ * It also announces itself in main's history, so an incomplete commit is self-identifying: anyone
183
+ * reading `git log` later can tell at a glance that this one did not carry a gate receipt.
184
+ */
185
+ fallbackBody(ref) {
186
+ return `${ref.url === '' ? `PR #${ref.number}` : ref.url}\n\n` +
187
+ '⚠️ FALLBACK COMMIT BODY — the gated risk/flags body rendered by `pnpm wp-finish-upsert-pr`\n' +
188
+ 'was not available on this machine, and a human chose to land anyway with\n' +
189
+ '`pnpm wp-land-pr --fallback-title-only`. This commit carries the PR title and link only.\n' +
190
+ 'The PR description is deliberately NOT included here.\n';
191
+ }
192
+ writeFallbackBody(ref) {
193
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-land-fallback-')), rules_config_1.MERGE_BODY_FILE);
194
+ fs.writeFileSync(file, this.fallbackBody(ref));
195
+ return file;
196
+ }
197
+ fallbackNotice(ref) {
198
+ return ` ⚠️ --fallback-title-only: no gated body for PR #${ref.number} on this machine, so this\n` +
199
+ ' commit gets the PR title + link and a line saying the gated body was unavailable.\n\n';
200
+ }
201
+ /**
202
+ * The landed branch's post-merge bookkeeping — but ONLY when this tree is the one that owes it.
203
+ *
204
+ * `origin.json` (written beside the body by finish) names the tree that posted the PR. When that is
205
+ * not the tree we are standing in, every step below would act on the wrong objects: `<branch>` in a
206
+ * second clone is a different commit, so `archive/<date>/<branch>` would tag the wrong tip under the
207
+ * right name; `merge-info/staged/<feature>` lives in the posting tree's state, not ours; and the
208
+ * worktree to reap is not this one. So the merge stands and the bookkeeping is declined ALOUD.
209
+ *
210
+ * No origin at all (the `--fallback-title-only` path) means we have nothing that says otherwise, and
211
+ * we ARE standing in the tree holding the branch — `base` came from this tree's HEAD. That is the
212
+ * pre-existing behaviour and it stays.
213
+ */
214
+ bookkeeping(repoRoot, base, ref, retention, stored) {
215
+ const owner = stored?.origin?.treeRoot ?? '';
216
+ if (owner !== '' && path.resolve(owner) !== path.resolve(repoRoot)) {
217
+ return this.foreignTreeNotice(owner, base);
218
+ }
219
+ return this.archiveAndPromote(repoRoot, base, ref, retention) + this.nextStep(repoRoot, base);
220
+ }
221
+ /** What was skipped, why, and the exact command that finishes it — in the tree that owes it. */
222
+ foreignTreeNotice(ownerTree, base) {
223
+ return '\n ⚠️ Archive + worktree cleanup SKIPPED — this PR was posted from a different tree:\n' +
224
+ ` ${ownerTree}\n` +
225
+ ' The pre-squash tip, the merge-info record and (if any) the worktree holding\n' +
226
+ ` ${base} all live THERE, and "${base}" here is a different commit, so archiving it\n` +
227
+ ' from this tree would tag the wrong objects under the right name.\n' +
228
+ ' Finish the bookkeeping there:\n' +
229
+ ` ${(0, rules_config_1.atRoot)(ownerTree, 'pnpm wp-cleanup')}\n`;
230
+ }
95
231
  /**
96
232
  * What happens next — which is NOT the same act when you landed from a worktree.
97
233
  *
@@ -152,17 +288,17 @@ let LandPrCommand = class LandPrCommand {
152
288
  const result = (0, child_process_1.spawnSync)('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });
153
289
  return result.status === 0 ? (result.stdout ?? '').trim() : '';
154
290
  }
155
- // The open PR's number + title for this head branch, or null when there is none. The TITLE comes
156
- // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even
157
- // if review.json was edited afterwards.
291
+ // The open PR's number, title and URL for this head branch, or null when there is none. The TITLE
292
+ // comes from the PR itself, not review.json, so the squash subject matches what a reviewer approved
293
+ // even if review.json was edited afterwards. The URL is only used by the fallback body.
158
294
  prNumberAndTitle(baseBranch) {
159
- const result = (0, child_process_1.spawnSync)('gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '"\\(.number)\\t\\(.title)"'], { encoding: 'utf8' });
295
+ const result = (0, child_process_1.spawnSync)('gh', ['pr', 'view', baseBranch, '--json', 'number,title,url', '--jq', '"\\(.number)\\t\\(.title)\\t\\(.url)"'], { encoding: 'utf8' });
160
296
  if (result.status !== 0)
161
297
  return null;
162
298
  const parts = (result.stdout ?? '').trim().split('\t');
163
299
  if ((parts[0] ?? '') === '')
164
300
  return null;
165
- return new PrIdentity(parts[0] ?? '', parts[1] ?? '');
301
+ return new PrIdentity(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');
166
302
  }
167
303
  };
168
304
  exports.LandPrCommand = LandPrCommand;
@@ -174,15 +310,18 @@ exports.LandPrCommand = LandPrCommand = tslib_1.__decorate([
174
310
  pr_merger_1.PrMerger,
175
311
  rules_config_1.BranchArchiver,
176
312
  merge_info_index_1.MergeInfoIndex,
177
- landed_worktree_reaper_1.LandedWorktreeReaper])
313
+ landed_worktree_reaper_1.LandedWorktreeReaper,
314
+ rules_config_1.PrBodyStore])
178
315
  ], LandPrCommand);
179
- // The open PR's number + title, as read back from GitHub.
316
+ // The open PR's number, title and web URL, as read back from GitHub.
180
317
  class PrIdentity {
181
318
  number;
182
319
  title;
183
- constructor(number, title) {
320
+ url;
321
+ constructor(number, title, url) {
184
322
  this.number = number;
185
323
  this.title = title;
324
+ this.url = url;
186
325
  }
187
326
  }
188
327
  //# sourceMappingURL=land-pr-command.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { MergeIntent, PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and\n * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is\n * missing, the honest answer is that finish has not run — so this fails and says so, rather than\n * inventing a body that would not match the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n\n if (!fs.existsSync(mergeBodyFile)) {\n throw new InformAiError(\n '\\n' + SEP + '❌ Nothing to land — no rendered merge body\\n' + SEP + '\\n' +\n `Expected the compact squash-commit body at:\\n ${mergeBodyFile}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\\n' +\n 'Its absence means finish has not run on this branch, so there is no reviewed PR to\\n' +\n 'land. Run the gated flow first:\\n\\n' +\n ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\\n' +\n ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\\n' +\n ' # write review.json at the path wp-review-upsert-pr prints\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n\\n' +\n 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\\n' + SEP,\n );\n }\n\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n //\n // `commanded: true` — running THIS command IS the intent to merge, so it is not gated on\n // pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand). The real `policy`\n // travels alongside it rather than being replaced by a literal AUTO, so PrMerger's diagnostics\n // never assert a config value nobody set — that is exactly what used to print a CONFIG MISMATCH\n // about `mergeMode: AUTO` two lines above this command printing `mergeMode is NONE`.\n const outcome = this.prMerger.merge(\n base, `${ref.title} (#${ref.number})`, mergeBodyFile, new MergeIntent(policy ?? '', true),\n );\n\n const archived = outcome.merged\n ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n archived +\n (outcome.merged ? this.nextStep(repoRoot, base) : '') +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number + title for this head branch, or null when there is none. The TITLE comes\n // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even\n // if review.json was edited afterwards.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '');\n }\n}\n\n// The open PR's number + title, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n\n constructor(number: string, title: string) {\n this.number = number;\n this.title = title;\n }\n}\n"]}
1
+ {"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n MERGE_BODY_FILE, PrBodyLocation, PrBodyStore, WEBPIECES_STATE_HOME_ENV, atRoot,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { MergeIntent, PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--fallback-title-only` opt-in. Data-only, per CLAUDE.md. */\nexport class LandPrOptions {\n /**\n * Land with a commit body built from the PR TITLE and LINK alone, because the gated body rendered by\n * `wp-finish-upsert-pr` is not on this machine. A HUMAN decision — see\n * {@link LandPrCommand.fallbackBody} for why the PR description is deliberately not an option.\n */\n fallbackTitleOnly = false;\n}\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and filed `merge-commit-body.md` under the PR's identity. Landing is a separate,\n * later act, and rebuilding here would mean a second authoritative gate whose result nobody reads. THE\n * BYTES THAT LAND ARE THE BYTES FINISH PRODUCED — that invariant survives everything below.\n *\n * ─── Two scopes, and only one of them is the tree's ────────────────────────────────────────────────\n * This command does two different things, and they belong to different scopes:\n *\n * 1. THE MERGE, which belongs to the PR. The receipt is read from PrBodyStore —\n * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/` — so landing works from the primary clone, from any\n * linked worktree, and from a SECOND CLONE of the same repo, as long as the PR was posted from this\n * machine. It used to be read out of this worktree's own `pr-review/<branch>/`, and the day the\n * gated flow ran in the primary clone while landing happened in a worktree, it printed \"Nothing to\n * land\" at a perfectly good PR.\n * 2. THE BOOKKEEPING — archiving the pre-squash tip as `archive/<date>/<branch>`, promoting merge-info,\n * and reaping the landed worktree. That half genuinely belongs to the tree that holds the branch,\n * and it must NOT be attempted from anywhere else: another clone's `<branch>` is a different commit,\n * so archiving it there would tag the wrong objects under the right name. When we are not that tree,\n * the merge still happens and the bookkeeping is SKIPPED OUT LOUD, naming the tree that owes it.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n private readonly prBodies: PrBodyStore,\n ) {}\n\n async run(opts: LandPrOptions = new LandPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n // The PR is resolved FIRST now, because its NUMBER is the key the body is filed under. That\n // ordering is the whole change: the body is no longer a fact of this tree to be looked up by\n // branch name, it is a fact of PR #N.\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n const stored = this.prBodies.read(repoRoot, ref.number);\n if (stored === null && !opts.fallbackTitleOnly) throw this.notOnThisMachine(repoRoot, ref);\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n const mergeBodyFile = stored !== null ? stored.bodyFile : this.writeFallbackBody(ref);\n if (stored === null) process.stdout.write(this.fallbackNotice(ref));\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n //\n // `commanded: true` — running THIS command IS the intent to merge, so it is not gated on\n // pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand). The real `policy`\n // travels alongside it rather than being replaced by a literal AUTO, so PrMerger's diagnostics\n // never assert a config value nobody set — that is exactly what used to print a CONFIG MISMATCH\n // about `mergeMode: AUTO` two lines above this command printing `mergeMode is NONE`.\n const outcome = this.prMerger.merge(\n base, `${ref.title} (#${ref.number})`, mergeBodyFile, new MergeIntent(policy ?? '', true),\n );\n\n const bookkeeping = outcome.merged\n ? this.bookkeeping(repoRoot, base, ref, config.landPr.branchRetention, stored)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n bookkeeping +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * The refusal when this machine holds no gated body for the PR.\n *\n * It says MACHINE, not \"branch\" and not \"worktree\", because that is now the true scope: the receipt\n * is filed under the PR's global identity, so the only way to be missing it is to be on a different\n * computer from the one that posted the PR (or to be looking before finish ever ran).\n */\n private notOnThisMachine(repoRoot: string, ref: PrIdentity): InformAiError {\n const home = this.prBodies.home(repoRoot);\n const degraded = home.degraded\n ? `\\n⚠️ This machine's webpieces state is DEGRADED to ${home.root}\\n` +\n ` (${home.reason}), so the body is only visible from this clone. Set\\n` +\n ` ${WEBPIECES_STATE_HOME_ENV} to a writable directory to make it machine-global.\\n`\n : '';\n return new InformAiError(\n '\\n' + SEP + `❌ PR #${ref.number} was not found on this machine\\n` + SEP + '\\n' +\n `Expected the gated squash-commit body at:\\n ${path.join(this.prBodies.dirFor(repoRoot, ref.number), MERGE_BODY_FILE)}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr` ON THE MACHINE THAT POSTED THE PR, and it\\n' +\n 'is never regenerated here: it is the gate\\'s receipt, and re-deriving it at land time would\\n' +\n 'be a second authoritative gate whose result nobody reads.\\n' +\n degraded +\n this.legacySignpost(repoRoot) +\n '\\nSo either:\\n\\n' +\n ' A. finish has not run on this branch — run the gated flow, which also posts the PR:\\n' +\n ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\\n' +\n ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\\n' +\n ' # write review.json at the path wp-review-upsert-pr prints\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n' +\n ' Then re-run `pnpm wp-land-pr`. This is the right answer almost every time.\\n\\n' +\n ' B. the PR was posted from a DIFFERENT machine. Land it from there, or re-run\\n' +\n ' `pnpm wp-finish-upsert-pr` here so this machine renders its own receipt.\\n\\n' +\n ' C. A HUMAN may choose to land it WITHOUT the gated body:\\n' +\n ' pnpm wp-land-pr --fallback-title-only\\n' +\n ' That writes a commit body of the PR TITLE + LINK and a line saying the gated body was\\n' +\n ' unavailable. It is a degraded commit message and it says so in main\\'s history forever.\\n' +\n ' DO NOT run it on your own initiative — ask the human, and let them decide.\\n' + SEP,\n );\n }\n\n /**\n * The ONE-TIME signpost for a body written by the PREVIOUS release into this tree's `pr-review/`.\n *\n * LOUD, and never read — the RETIRED_CONFIG_KEYS pattern applied to an artifact. A silent fallback\n * to the old path is exactly the shim CLAUDE.md forbids: it would re-create two homes for the\n * receipt, and the stale one wins precisely when finish and land ran in different trees, which is\n * the bug this whole change removes. Re-running finish re-files it under the PR's identity, which\n * costs one command and leaves one home.\n */\n private legacySignpost(repoRoot: string): string {\n const legacy = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), MERGE_BODY_FILE);\n if (!fs.existsSync(legacy)) return '';\n return '\\n⚠️ A body written by an OLDER webpieces release is sitting at:\\n' +\n ` ${legacy}\\n` +\n ' It is deliberately NOT read. That path is per-worktree, so it is only correct while the\\n' +\n ' branch never changes trees — the assumption that broke. Re-run `pnpm wp-finish-upsert-pr`\\n' +\n ' to re-file it under this PR\\'s identity, then land. (The stale file self-clears in 30 days.)\\n';\n }\n\n /**\n * The explicitly-degraded commit body: PR TITLE + LINK + a line saying what is missing. NEVER the PR\n * DESCRIPTION.\n *\n * That exclusion is the point of this method, not an oversight. In a real consuming repo the PR\n * description IS the full PR Gate Dashboard, and GitHub's default\n * `squash_merge_commit_message=PR_BODY` dumping that dashboard into the commit is precisely the ugly\n * git log this whole mechanism exists to prevent. A fallback that reached for the description would\n * produce a WORSE commit than doing nothing, while looking more complete.\n *\n * It also announces itself in main's history, so an incomplete commit is self-identifying: anyone\n * reading `git log` later can tell at a glance that this one did not carry a gate receipt.\n */\n private fallbackBody(ref: PrIdentity): string {\n return `${ref.url === '' ? `PR #${ref.number}` : ref.url}\\n\\n` +\n '⚠️ FALLBACK COMMIT BODY — the gated risk/flags body rendered by `pnpm wp-finish-upsert-pr`\\n' +\n 'was not available on this machine, and a human chose to land anyway with\\n' +\n '`pnpm wp-land-pr --fallback-title-only`. This commit carries the PR title and link only.\\n' +\n 'The PR description is deliberately NOT included here.\\n';\n }\n\n private writeFallbackBody(ref: PrIdentity): string {\n const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'wp-land-fallback-')), MERGE_BODY_FILE);\n fs.writeFileSync(file, this.fallbackBody(ref));\n return file;\n }\n\n private fallbackNotice(ref: PrIdentity): string {\n return ` ⚠️ --fallback-title-only: no gated body for PR #${ref.number} on this machine, so this\\n` +\n ' commit gets the PR title + link and a line saying the gated body was unavailable.\\n\\n';\n }\n\n /**\n * The landed branch's post-merge bookkeeping — but ONLY when this tree is the one that owes it.\n *\n * `origin.json` (written beside the body by finish) names the tree that posted the PR. When that is\n * not the tree we are standing in, every step below would act on the wrong objects: `<branch>` in a\n * second clone is a different commit, so `archive/<date>/<branch>` would tag the wrong tip under the\n * right name; `merge-info/staged/<feature>` lives in the posting tree's state, not ours; and the\n * worktree to reap is not this one. So the merge stands and the bookkeeping is declined ALOUD.\n *\n * No origin at all (the `--fallback-title-only` path) means we have nothing that says otherwise, and\n * we ARE standing in the tree holding the branch — `base` came from this tree's HEAD. That is the\n * pre-existing behaviour and it stays.\n */\n private bookkeeping(repoRoot: string, base: string, ref: PrIdentity, retention: string, stored: PrBodyLocation | null): string {\n const owner = stored?.origin?.treeRoot ?? '';\n if (owner !== '' && path.resolve(owner) !== path.resolve(repoRoot)) {\n return this.foreignTreeNotice(owner, base);\n }\n return this.archiveAndPromote(repoRoot, base, ref, retention) + this.nextStep(repoRoot, base);\n }\n\n /** What was skipped, why, and the exact command that finishes it — in the tree that owes it. */\n private foreignTreeNotice(ownerTree: string, base: string): string {\n return '\\n ⚠️ Archive + worktree cleanup SKIPPED — this PR was posted from a different tree:\\n' +\n ` ${ownerTree}\\n` +\n ' The pre-squash tip, the merge-info record and (if any) the worktree holding\\n' +\n ` ${base} all live THERE, and \"${base}\" here is a different commit, so archiving it\\n` +\n ' from this tree would tag the wrong objects under the right name.\\n' +\n ' Finish the bookkeeping there:\\n' +\n ` ${atRoot(ownerTree, 'pnpm wp-cleanup')}\\n`;\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number, title and URL for this head branch, or null when there is none. The TITLE\n // comes from the PR itself, not review.json, so the squash subject matches what a reviewer approved\n // even if review.json was edited afterwards. The URL is only used by the fallback body.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n}\n\n// The open PR's number, title and web URL, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n url: string;\n\n constructor(number: string, title: string, url: string) {\n this.number = number;\n this.title = title;\n this.url = url;\n }\n}\n"]}
@@ -3,7 +3,7 @@ import { FinishUpdateCommand } from './commands/finish-update-command';
3
3
  import { StartUpsertPrCommand } from './commands/start-upsert-pr-command';
4
4
  import { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';
5
5
  import { CleanupCommand } from './commands/cleanup-command';
6
- import { LandPrCommand } from './commands/land-pr-command';
6
+ import { LandPrCommand, LandPrOptions } from './commands/land-pr-command';
7
7
  import { CheckPrCommand } from './commands/check-pr-command';
8
8
  import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
9
9
  import { ReapWorktreeCommand } from './commands/reap-worktree-command';
@@ -62,7 +62,7 @@ export declare class PrGateApp {
62
62
  /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */
63
63
  cleanup(): Promise<void>;
64
64
  /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */
65
- landPr(): Promise<void>;
65
+ landPr(opts?: LandPrOptions): Promise<void>;
66
66
  /**
67
67
  * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
68
68
  * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN
@@ -107,9 +107,9 @@ let PrGateApp = class PrGateApp {
107
107
  return this.cleanupCommand.run();
108
108
  }
109
109
  /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */
110
- landPr() {
110
+ landPr(opts = new land_pr_command_1.LandPrOptions()) {
111
111
  this.assertNoResolveInProgress('wp-land-pr');
112
- return this.landPrCommand.run();
112
+ return this.landPrCommand.run(opts);
113
113
  }
114
114
  /**
115
115
  * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
@@ -1 +1 @@
1
- {"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
1
+ {"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAC5D,gEAA0E;AAC1E,kEAA6D;AAC7D,kFAAmG;AACnG,4EAAuE;AACvE,kEAA6E;AAC7E,gFAAgG;AAChG,8DAA8D;AAC9D,0DAAyD;AAEzD;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAbrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC,EACxC,cAA8B,EAC9B,oBAA0C,EAC1C,iBAAoC,EACpC,cAA8B;QAZ9B,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,mBAAc,GAAd,cAAc,CAAgB;IAChD,CAAC;IAEJ;;;;;;;OAOG;IACK,yBAAyB,CAAC,OAAe;QAC7C,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,OAAO,CAAC;YAAE,OAAO;QACpE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACnG,CAAC;IAED,+GAA+G;IAC/G,OAAO,CAAC,OAAuB,IAAI,iCAAc,EAAE;QAC/C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,mGAAmG;IACnG,aAAa,CAAC,OAA6B,IAAI,8CAAoB,EAAE;QACjE,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,IAAI,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,IAAI,CAAC,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED,mGAAmG;IACnG,OAAO;QACH,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;IAED,0FAA0F;IAC1F,MAAM,CAAC,OAAsB,IAAI,+BAAa,EAAE;QAC5C,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAA8B,IAAI,gDAAqB,EAAE;QACpE,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AAnGY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACf,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;QACxB,iCAAc;QACR,8CAAoB;QACvB,kCAAiB;QACpB,6BAAc;GAd1C,SAAS,CAmGrB","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { LandPrCommand, LandPrOptions } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /** `wp-cleanup`: reap what is provably merged, and ASK about everything that merely looks dead. */\n cleanup(): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(opts: LandPrOptions = new LandPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(opts);\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
@@ -1,21 +1,27 @@
1
- import { DotWebpieces, RepoRootFinder } from '@webpieces/rules-config';
1
+ import { AgedTreeSweeper, DotWebpieces, PrBodyStore, RepoRootFinder } from '@webpieces/rules-config';
2
2
  /**
3
- * 30-day garbage collection of the whole `.webpieces` tree.
3
+ * 30-day garbage collection of the whole `.webpieces` tree, AND of the machine-global PR-body store.
4
4
  *
5
- * Runs at the end of every merge/PR flow (see merge-end.ts). It walks `.webpieces` depth-first and:
6
- * 1. deletes ANY file whose mtime is older than the cutoff, anywhere in the tree, and
7
- * 2. removes ANY directory that is left empty afterwards (including dirs that were already empty).
5
+ * Runs at the end of every merge/PR flow (see merge-end.ts). Both roots get the identical policy from
6
+ * the identical implementation (`AgedTreeSweeper`): any file older than the cutoff is deleted, and any
7
+ * directory left empty afterwards is pruned. Neither root itself is ever removed.
8
8
  *
9
- * The `.webpieces` root itself is never removed. Everything the tooling still needs is rewritten on
10
- * each run under a fresh mtime (instruct-ai/ docs, the active hooks/ logs, the *-status.json state
11
- * files), and every writer `mkdirSync(..., { recursive: true })`s its target first — so pruning a
12
- * stale home dir is harmless; the next write recreates it. Stale per-feature merge-info/pr-review
13
- * subdirs and the retired legacy flat layout all fall out of this single pass with no special-casing.
9
+ * Everything the tooling still needs is rewritten on each run under a fresh mtime (instruct-ai/ docs,
10
+ * the active logs, the *-status.json state files, and an open PR's merge body), and every writer
11
+ * `mkdirSync(..., { recursive: true })`s its target first — so pruning a stale home dir is harmless;
12
+ * the next write recreates it.
13
+ *
14
+ * The SECOND root is the one with no other owner. `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/` survives
15
+ * `rm -rf <clone>`, so if this sweep did not reap it nothing ever would (`decisions/0001` § O3). It is
16
+ * swept from here — the one command that already runs at the end of every flow — rather than from a new
17
+ * mechanism, because a retention policy nobody runs is not a retention policy.
14
18
  */
15
19
  export declare class CleanTmp {
16
20
  private readonly repoRootFinder;
21
+ private readonly prBodies;
22
+ private readonly sweeper;
17
23
  private readonly dotDir;
18
- constructor(repoRootFinder: RepoRootFinder, dotDir?: DotWebpieces);
24
+ constructor(repoRootFinder: RepoRootFinder, prBodies: PrBodyStore, sweeper: AgedTreeSweeper, dotDir?: DotWebpieces);
19
25
  cleanTmp(): Promise<void>;
20
- private sweep;
26
+ private reporter;
21
27
  }