@webpieces/pr-gate 0.4.500 → 0.4.501

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.500",
3
+ "version": "0.4.501",
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",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/tooling/pr-gate"
26
26
  },
27
27
  "dependencies": {
28
- "@webpieces/rules-config": "0.4.500",
28
+ "@webpieces/rules-config": "0.4.501",
29
29
  "@inversifyjs/binding-decorators": "1.1.5",
30
30
  "inversify": "7.10.4",
31
31
  "reflect-metadata": "0.2.2"
@@ -7,6 +7,7 @@ import { BuildAffected } from '../workflow/build-affected';
7
7
  import { MergeState } from '../workflow/merge-state';
8
8
  import { ReviewStageReceiptService } from '../workflow/review-stage-receipt';
9
9
  import { PrMerger } from '../workflow/pr-merger';
10
+ import { FinishBanner } from '../workflow/finish-banner';
10
11
  import { GatedPrPublisher } from '../workflow/gated-pr-publisher';
11
12
  import { Dashboard } from '../../dashboard/dashboard';
12
13
  export declare class FinishUpsertPrCommand {
@@ -25,9 +26,9 @@ export declare class FinishUpsertPrCommand {
25
26
  private readonly provenance;
26
27
  private readonly instructions;
27
28
  private readonly receipts;
28
- constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistScanner: ChecklistScanner, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, instructions: ChecklistInstructionsService, receipts: ReviewStageReceiptService);
29
+ private readonly banner;
30
+ constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistScanner: ChecklistScanner, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, instructions: ChecklistInstructionsService, receipts: ReviewStageReceiptService, banner: FinishBanner);
29
31
  run(): Promise<void>;
30
- private emitClickableLinkDirective;
31
32
  /**
32
33
  * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.
33
34
  *
@@ -15,6 +15,7 @@ const build_affected_1 = require("../workflow/build-affected");
15
15
  const merge_state_1 = require("../workflow/merge-state");
16
16
  const review_stage_receipt_1 = require("../workflow/review-stage-receipt");
17
17
  const pr_merger_1 = require("../workflow/pr-merger");
18
+ const finish_banner_1 = require("../workflow/finish-banner");
18
19
  const gated_pr_publisher_1 = require("../workflow/gated-pr-publisher");
19
20
  const dashboard_1 = require("../../dashboard/dashboard");
20
21
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
@@ -78,7 +79,8 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
78
79
  provenance;
79
80
  instructions;
80
81
  receipts;
81
- constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard, checklistScanner, reviewJsonService, gateTokenService, provenance, instructions, receipts) {
82
+ banner;
83
+ constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard, checklistScanner, reviewJsonService, gateTokenService, provenance, instructions, receipts, banner) {
82
84
  this.repoRootFinder = repoRootFinder;
83
85
  this.aiBranchName = aiBranchName;
84
86
  this.branchNaming = branchNaming;
@@ -94,6 +96,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
94
96
  this.provenance = provenance;
95
97
  this.instructions = instructions;
96
98
  this.receipts = receipts;
99
+ this.banner = banner;
97
100
  }
98
101
  async run() {
99
102
  const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
@@ -144,26 +147,12 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
144
147
  // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never
145
148
  // fatal — the PR is already up by now, so a comment failure only warns.
146
149
  this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);
147
- const prNum = result.prNumber;
148
- process.stdout.write('\n' + SEP + '✅ PR finished here is exactly what I did\n' + SEP + '\n' +
149
- ` 1. validated the build gate (authoritative)\n` +
150
- ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: "${title}"\n` +
151
- ` 3. force-pushed your work to origin/${base} (after the body, so CI reads the right token)\n` +
152
- ` 4. ${result.merge.message}\n` +
153
- ` You are on ${base} — same name as the remote branch and the PR head.\n\n`);
154
- // Close by handing the AI a clickable link + directive to end its summary with it (see helper).
155
- this.emitClickableLinkDirective(prNum, result.prUrl, title);
156
- }
157
- // Print the closing AI directive: the resolved PR's number/title/URL and an instruction to end the
158
- // user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it. No-op when
159
- // the PR could not be resolved — better to say nothing than to point the user at a link that isn't real.
160
- emitClickableLinkDirective(prNum, prUrl, title) {
161
- if (prNum === '' || prUrl === '')
162
- return;
163
- process.stdout.write(SEP + `🔗 PR #${prNum}: ${title}\n ${prUrl}\n\n` + SEP +
164
- `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\n` +
165
- `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\n\n` +
166
- ` [#${prNum} ${title}](${prUrl})\n\n`);
150
+ // The closing recap + the clickable-link directive, BOTH derived from the real merge outcome.
151
+ // Nothing here may hard-code success: a stranded PR under a green checkmark is how PRs got
152
+ // abandoned (see FinishBanner).
153
+ const bannerInput = new finish_banner_1.FinishBannerInput(result.prNumber, result.prUrl, title, base, result.merge);
154
+ process.stdout.write(this.banner.render(bannerInput));
155
+ process.stdout.write(this.banner.linkDirective(bannerInput));
167
156
  }
168
157
  // Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here
169
158
  // does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only
@@ -449,7 +438,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
449
438
  const published = this.publisher.publish(baseBranch, title, bodyFile);
450
439
  if (published.createFailed) {
451
440
  process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
452
- return new UpsertResult('', '', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
441
+ return new UpsertResult('', '', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)', pr_merger_1.MERGE_RESULT_FAILED));
453
442
  }
454
443
  const num = published.number;
455
444
  // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an
@@ -496,6 +485,7 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
496
485
  rules_config_1.GateTokenService,
497
486
  rules_config_1.SubagentProvenanceService,
498
487
  rules_config_1.ChecklistInstructionsService,
499
- review_stage_receipt_1.ReviewStageReceiptService])
488
+ review_stage_receipt_1.ReviewStageReceiptService,
489
+ finish_banner_1.FinishBanner])
500
490
  ], FinishUpsertPrCommand);
501
491
  //# sourceMappingURL=finish-upsert-pr-command.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-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 loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist, ChecklistVerdict,\n writeTemplate, RepoRootFinder, ReviewJsonService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest, PrGateConfig,\n ChecklistInstructionsService, InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { PrMerger, MergeOutcome } from '../workflow/pr-merger';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, ChecklistCommentRow, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which\n * blocks) plus what each one actually read (the quality signal, which is published). Data-only.\n */\nclass ProvenanceReport {\n verified: boolean;\n evidence: ReviewerEvidence[];\n\n constructor(verified: boolean, evidence: ReviewerEvidence[]) {\n this.verified = verified;\n this.evidence = evidence;\n }\n}\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number + web URL ('' each when unresolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success. The\n// URL is carried so the closing block can hand the AI a clickable link to relay to the user verbatim.\nclass UpsertResult {\n prNumber: string;\n prUrl: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, prUrl: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.merge = merge;\n }\n}\n\n// STAGE ③ — FINISH of the AI-first PR flow, and the ONLY command that posts PRs. Runs after\n// `wp-review-upsert-pr` verified the branch and the AI spawned the reviewers + wrote review.json.\n//\n// In order: (1) REFUSE on an unvalidated 3-point merge and REQUIRE stage ②'s receipt; (2) REQUIRE\n// review.json + every reviewer's verdict + provenance; (3) run the build gate UNLESS the receipt already\n// covers this exact HEAD; (4) render the dashboard; (5) create/update the PR via `gh`.\n//\n// It no longer FINALIZES a merge — stage ② does. Two commands owning conflict-resolution validation is two\n// implementations that drift, and finalizing here came too late to help anyway: the reviewers had already\n// reviewed the pre-merge tree by then.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n private readonly instructions: ChecklistInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // 1. REQUIRE stage ② — see assertStageTwoRan. Returns true when its receipt covers THIS commit,\n // which is what lets the build gate below be skipped rather than re-run for a foregone answer.\n const buildAlreadyGreen = this.assertStageTwoRan(repoRoot);\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n // The SAME scan wp-review-upsert-pr runs, with filterAlreadyReviewed:true so `outstanding` is exactly what\n // still owes a verdict (Z of N of X). Sharing the computation is the point: the command that REPORTS\n // and the command that GATES must not be able to disagree about what is owed. review-<id>.json files\n // persist locally, so a re-run re-validates the EXISTING verdicts against the (possibly changed)\n // applicable set for free — an unchanged checklist needs no re-review, a newly-applicable one refuses\n // until its file is written.\n const featureName = this.aiBranchName.getFeatureName();\n const scan = this.checklistScanner.scan(repoRoot, loadAndValidate(repoRoot).prGate.checklists, new ChecklistScanOptions(true));\n const required = scan.applicable;\n // FAIL FAST on missing reviewers, BEFORE review.json is parsed and before the build gate runs. A\n // missing reviewer is not a review.json defect (folding it in made the AI fix the wrong thing), and\n // nobody should wait on a build to be told a reviewer never ran.\n this.assertEveryReviewerRan(scan);\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, featureName), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenance = this.enforceProvenance(required, currentBranch, repoRoot, loadAndValidate(repoRoot).prGate);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).\n this.runOrSkipBuildGate(repoRoot, buildAlreadyGreen);\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because\n // GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A\n // valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI\n // (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist\n // passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);\n const prNum = result.prNumber;\n\n process.stdout.write(\n '\\n' + SEP + '✅ PR finished — here is exactly what I did\\n' + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: \"${title}\"\\n` +\n ` 3. force-pushed your work to origin/${base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${result.merge.message}\\n` +\n ` You are on ${base} — same name as the remote branch and the PR head.\\n\\n`,\n );\n // Close by handing the AI a clickable link + directive to end its summary with it (see helper).\n this.emitClickableLinkDirective(prNum, result.prUrl, title);\n }\n\n // Print the closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n // user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it. No-op when\n // the PR could not be resolved — better to say nothing than to point the user at a link that isn't real.\n private emitClickableLinkDirective(prNum: string, prUrl: string, title: string): void {\n if (prNum === '' || prUrl === '') return;\n process.stdout.write(\n SEP + `🔗 PR #${prNum}: ${title}\\n ${prUrl}\\n\\n` + SEP +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${prNum} ${title}](${prUrl})\\n\\n`,\n );\n }\n\n // Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here\n // does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only\n // after review.json + every BLOCK checklist + the build gate pass and the gated PR body is written.\n /**\n * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.\n *\n * The gate is authoritative per-COMMIT, not per-command: re-running it on a tree that has not moved\n * since stage ② verified it buys nothing and costs a full `nx affected`. That skip is what makes the\n * three-stage flow cost ONE build rather than two, and it is why moving the gate earlier was affordable.\n */\n private runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): void {\n if (alreadyGreen) {\n process.stdout.write('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n');\n return;\n }\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n }\n\n /**\n * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.\n * Returns true when that receipt covers the CURRENT HEAD, i.e. the build gate can be skipped.\n */\n private assertStageTwoRan(repoRoot: string): boolean {\n this.assertNoUnvalidatedMerge(repoRoot);\n return this.assertReviewStageRan(\n repoRoot, this.aiBranchName.getFeatureName(), this.gitOut(['rev-parse', 'HEAD']));\n }\n\n /**\n * REFUSE while a 3-point merge is still unvalidated — do not finalize it here.\n *\n * This command used to do the finalizing itself. Finalizing means validating a conflict resolution, and\n * two commands owning that means two implementations that can drift; `PrContextWriter`'s docstring\n * records this codebase already paying for exactly that once. It also came too late to matter: by the\n * time finish ran, the reviewers had already reviewed the pre-merge tree.\n *\n * So stage ② owns it, and finish only checks. Nothing is lost — the recovery is one command away, and\n * running it also builds the merged tree and re-briefs the reviewers against it, which finalizing here\n * never did.\n */\n private assertNoUnvalidatedMerge(repoRoot: string): void {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return;\n throw new InformAiError(\n '⛔ NO PR — a 3-point merge on this branch is still unvalidated, so nothing here has been\\n' +\n 'verified: the conflict resolution is unchecked, the merged tree was never built, and any\\n' +\n 'reviewer that ran judged the PRE-merge code.\\n\\n' +\n `Conflicted file(s): ${marker.conflictedFiles.join(', ')}\\n\\n` +\n 'Resolve them (see .webpieces/instruct-ai/webpieces.mergeprocess.md), then run:\\n' +\n ' pnpm wp-review-upsert-pr\\n' +\n 'It validates the resolution, commits it, builds it, and re-briefs the reviewers.',\n );\n }\n\n /**\n * REQUIRE stage ② (`wp-review-upsert-pr`) to have run, and report whether it ran on THIS commit.\n *\n * Returns true when the receipt matches the current HEAD — the caller then skips its own build, because\n * the receipt IS the gate for that sha and rebuilding an unchanged tree is a second full `nx affected`\n * run for a foregone answer.\n *\n * Without this check a repo with NO checklists has nothing forcing stage ②: `assertEveryReviewerRan`\n * is vacuous, and review.json — the only other interlock — is a file the AI writes itself. It could\n * write it and come straight here, skipping the merge validation and the build entirely.\n */\n private assertReviewStageRan(repoRoot: string, featureName: string, headSha: string): boolean {\n const receipt = this.receipts.read(repoRoot, featureName);\n if (receipt === null) {\n throw new InformAiError(\n '⛔ NO PR — stage ② never ran on this branch. That means the 3-point merge is unvalidated,\\n' +\n 'the build gate has not run, no diff was extracted, and no reviewer was briefed.\\n\\n' +\n 'Run: pnpm wp-review-upsert-pr\\n' +\n '(then spawn the reviewers it names, write review.json, and re-run this command)',\n );\n }\n if (receipt.headSha === headSha) return true;\n // Not fatal. Re-reviewing on every follow-up commit would be intolerable, and most drift is a typo\n // fix. But it is never silent: the build re-runs here, and the PR says the reviewers saw an older tree.\n process.stderr.write(\n `\\n⚠️ HEAD moved since stage ② ran (reviewed ${receipt.headSha.slice(0, 8)}, now ${headSha.slice(0, 8)}).\\n` +\n ' The build gate will re-run, and the PR will record that reviewers judged an earlier tree.\\n' +\n ' If the change was substantive, re-run pnpm wp-review-upsert-pr and re-spawn the reviewers.\\n\\n');\n return false;\n }\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a verdict, naming exactly those reviewers and\n * exactly what to tell them — via the same renderer `wp-review-upsert-pr` uses, so the AI sees the identical\n * block it would have seen there. Lists ONLY the outstanding ones: re-instructing an already-reviewed\n * checklist invites a redundant second run and reads as though the earlier verdict did not count.\n * No-op for a repo with no applicable checklists.\n */\n private assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success`\n // format is otherwise indistinguishable from one that never ran, and the AI would go re-run a\n // subagent instead of correcting four characters of JSON.\n const format = scan.formatErrors.length === 0\n ? ''\n : `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n` +\n scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n throw new InformAiError(\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this ` +\n `branch have no passing verdict yet: ${this.instructions.names(scan.outstanding)}\\n\\n` +\n format +\n `${this.instructions.render(scan.outstanding, scan.reviewPath, scan.context)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr\\n` +\n `(Each reviewer's generated instructions file is already written — re-running wp-review-upsert-pr\\n` +\n ` is only needed if the code changed since it ran.)`,\n );\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard. (A checklist reaching this\n // point is always PASS/OVERRIDDEN — loadReviewJson already threw on FAIL/MISSING.)\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n /**\n * One comment row per DEFINED checklist — matched or not — pairing the roster's match evidence with the\n * resolved verdict. Built from `scan.roster` rather than from the applicable set: the skipped ones are\n * absent from `applicable` by construction, and their \"why not\" evidence (the configured globs and the\n * changed-file total) exists nowhere else without recomputing the diff a second way.\n */\n private commentRows(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): ChecklistCommentRow[] {\n // agentType -> did it open the diff. Absent ⇒ not assessed, which prints nothing (see ChecklistCommentRow.diffRead).\n const readByAgent = new Map<string, boolean>();\n for (const e of provenance.evidence) readByAgent.set(e.agentType, e.readDiff);\n return scan.roster.entries.map((entry: TriggeredChecklist): ChecklistCommentRow => {\n const ran = entry.matchedFiles.length > 0;\n const req = new RequiredChecklist(\n entry.def.id, entry.def.subagent, entry.def.doc, entry.matchedFiles, entry.matchedPatterns);\n // A skipped checklist has no verdict to resolve — asking for one would report it as MISSING,\n // i.e. as an unreviewed obligation, when in fact it never had one.\n const verdict = ran\n ? this.reviewJsonService.resolveVerdict(req, review.results)\n : new ChecklistVerdict(entry.def.id, '', '');\n const row = new ChecklistCommentRow(\n entry.def.subagent, verdict.status, verdict.detail, ran,\n entry.def.patterns, entry.matchedPatterns, entry.matchedFiles, scan.roster.changedFileCount);\n const read = readByAgent.get(entry.def.subagent);\n row.diffRead = read === undefined ? '' : (read ? 'yes' : 'no');\n return row;\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n // eslint-disable-next-line @typescript-eslint/max-params\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\n const errors: string[] = [];\n const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n if (subagents.length > 0) {\n const result = this.provenance.verifyDistinct(subagents, branch);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n report.evidence = this.gatherEvidence(repoRoot, required, result, branch);\n errors.push(...this.evidenceErrors(report.evidence, config));\n }\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return report;\n }\n\n /**\n * What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides\n * whether any of it blocks, and by default none of it does.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private gatherEvidence(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, branch: string): ReviewerEvidence[] {\n const docPaths: Record<string, string> = {};\n for (const req of required) {\n if (req.subagent.trim() !== '') docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n }\n const diffDir = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), 'diff');\n return this.provenance.evidenceFor(new EvidenceRequest(branch, result.agentIds, diffDir, docPaths));\n }\n\n /**\n * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.\n *\n * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the\n * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.\n * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.\n */\n private evidenceErrors(evidence: readonly ReviewerEvidence[], config: PrGateConfig): string[] {\n const blind = evidence.filter((e: ReviewerEvidence): boolean => !e.readDiff);\n if (blind.length === 0) return [];\n const names = blind.map((e: ReviewerEvidence): string => e.agentType).join(', ');\n if (!config.requireDiffEvidence) {\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\\n` +\n ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\\n');\n return [];\n }\n return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];\n }\n\n /**\n * Publish the roster + every reviewer's full `output` as ONE combined PR comment, idempotently (find the\n * marker comment → PATCH it, else POST). Never fatal: by here the PR is already created/updated, so a\n * `gh` failure only warns.\n *\n * Posted on EVERY run of a repo that defines checklists — including one where nothing matched, because\n * \"all five were evaluated and none applied\" is the good news the old comment could not deliver. The\n * guard is `scan.defined.length`, deliberately NOT the number of rows that ran: a repo with no\n * checklists configured must still see no comment at all (see ChecklistNotice / renderDashboard).\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n // eslint-disable-next-line @typescript-eslint/max-params\n private postChecklistComment(repoRoot: string, prNumber: string, scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): void {\n if (prNumber === '' || scan.defined.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n // GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before\n // the push, so CI's `synchronize` read cannot see the previous run's token.\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const published = this.publisher.publish(baseBranch, title, bodyFile);\n if (published.createFailed) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', '', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));\n }\n const num = published.number;\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
1
+ {"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-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 loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist, ChecklistVerdict,\n writeTemplate, RepoRootFinder, ReviewJsonService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest, PrGateConfig,\n ChecklistInstructionsService, InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { PrMerger, MergeOutcome, MERGE_RESULT_FAILED } from '../workflow/pr-merger';\nimport { FinishBanner, FinishBannerInput } from '../workflow/finish-banner';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, ChecklistCommentRow, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which\n * blocks) plus what each one actually read (the quality signal, which is published). Data-only.\n */\nclass ProvenanceReport {\n verified: boolean;\n evidence: ReviewerEvidence[];\n\n constructor(verified: boolean, evidence: ReviewerEvidence[]) {\n this.verified = verified;\n this.evidence = evidence;\n }\n}\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number + web URL ('' each when unresolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success. The\n// URL is carried so the closing block can hand the AI a clickable link to relay to the user verbatim.\nclass UpsertResult {\n prNumber: string;\n prUrl: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, prUrl: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.merge = merge;\n }\n}\n\n// STAGE ③ — FINISH of the AI-first PR flow, and the ONLY command that posts PRs. Runs after\n// `wp-review-upsert-pr` verified the branch and the AI spawned the reviewers + wrote review.json.\n//\n// In order: (1) REFUSE on an unvalidated 3-point merge and REQUIRE stage ②'s receipt; (2) REQUIRE\n// review.json + every reviewer's verdict + provenance; (3) run the build gate UNLESS the receipt already\n// covers this exact HEAD; (4) render the dashboard; (5) create/update the PR via `gh`.\n//\n// It no longer FINALIZES a merge — stage ② does. Two commands owning conflict-resolution validation is two\n// implementations that drift, and finalizing here came too late to help anyway: the reviewers had already\n// reviewed the pre-merge tree by then.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n private readonly instructions: ChecklistInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly banner: FinishBanner,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // 1. REQUIRE stage ② — see assertStageTwoRan. Returns true when its receipt covers THIS commit,\n // which is what lets the build gate below be skipped rather than re-run for a foregone answer.\n const buildAlreadyGreen = this.assertStageTwoRan(repoRoot);\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n // The SAME scan wp-review-upsert-pr runs, with filterAlreadyReviewed:true so `outstanding` is exactly what\n // still owes a verdict (Z of N of X). Sharing the computation is the point: the command that REPORTS\n // and the command that GATES must not be able to disagree about what is owed. review-<id>.json files\n // persist locally, so a re-run re-validates the EXISTING verdicts against the (possibly changed)\n // applicable set for free — an unchanged checklist needs no re-review, a newly-applicable one refuses\n // until its file is written.\n const featureName = this.aiBranchName.getFeatureName();\n const scan = this.checklistScanner.scan(repoRoot, loadAndValidate(repoRoot).prGate.checklists, new ChecklistScanOptions(true));\n const required = scan.applicable;\n // FAIL FAST on missing reviewers, BEFORE review.json is parsed and before the build gate runs. A\n // missing reviewer is not a review.json defect (folding it in made the AI fix the wrong thing), and\n // nobody should wait on a build to be told a reviewer never ran.\n this.assertEveryReviewerRan(scan);\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, featureName), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenance = this.enforceProvenance(required, currentBranch, repoRoot, loadAndValidate(repoRoot).prGate);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).\n this.runOrSkipBuildGate(repoRoot, buildAlreadyGreen);\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because\n // GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A\n // valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI\n // (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist\n // passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);\n\n // The closing recap + the clickable-link directive, BOTH derived from the real merge outcome.\n // Nothing here may hard-code success: a stranded PR under a green checkmark is how PRs got\n // abandoned (see FinishBanner).\n const bannerInput = new FinishBannerInput(result.prNumber, result.prUrl, title, base, result.merge);\n process.stdout.write(this.banner.render(bannerInput));\n process.stdout.write(this.banner.linkDirective(bannerInput));\n }\n\n // Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here\n // does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only\n // after review.json + every BLOCK checklist + the build gate pass and the gated PR body is written.\n /**\n * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.\n *\n * The gate is authoritative per-COMMIT, not per-command: re-running it on a tree that has not moved\n * since stage ② verified it buys nothing and costs a full `nx affected`. That skip is what makes the\n * three-stage flow cost ONE build rather than two, and it is why moving the gate earlier was affordable.\n */\n private runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): void {\n if (alreadyGreen) {\n process.stdout.write('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n');\n return;\n }\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n }\n\n /**\n * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.\n * Returns true when that receipt covers the CURRENT HEAD, i.e. the build gate can be skipped.\n */\n private assertStageTwoRan(repoRoot: string): boolean {\n this.assertNoUnvalidatedMerge(repoRoot);\n return this.assertReviewStageRan(\n repoRoot, this.aiBranchName.getFeatureName(), this.gitOut(['rev-parse', 'HEAD']));\n }\n\n /**\n * REFUSE while a 3-point merge is still unvalidated — do not finalize it here.\n *\n * This command used to do the finalizing itself. Finalizing means validating a conflict resolution, and\n * two commands owning that means two implementations that can drift; `PrContextWriter`'s docstring\n * records this codebase already paying for exactly that once. It also came too late to matter: by the\n * time finish ran, the reviewers had already reviewed the pre-merge tree.\n *\n * So stage ② owns it, and finish only checks. Nothing is lost — the recovery is one command away, and\n * running it also builds the merged tree and re-briefs the reviewers against it, which finalizing here\n * never did.\n */\n private assertNoUnvalidatedMerge(repoRoot: string): void {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return;\n throw new InformAiError(\n '⛔ NO PR — a 3-point merge on this branch is still unvalidated, so nothing here has been\\n' +\n 'verified: the conflict resolution is unchecked, the merged tree was never built, and any\\n' +\n 'reviewer that ran judged the PRE-merge code.\\n\\n' +\n `Conflicted file(s): ${marker.conflictedFiles.join(', ')}\\n\\n` +\n 'Resolve them (see .webpieces/instruct-ai/webpieces.mergeprocess.md), then run:\\n' +\n ' pnpm wp-review-upsert-pr\\n' +\n 'It validates the resolution, commits it, builds it, and re-briefs the reviewers.',\n );\n }\n\n /**\n * REQUIRE stage ② (`wp-review-upsert-pr`) to have run, and report whether it ran on THIS commit.\n *\n * Returns true when the receipt matches the current HEAD — the caller then skips its own build, because\n * the receipt IS the gate for that sha and rebuilding an unchanged tree is a second full `nx affected`\n * run for a foregone answer.\n *\n * Without this check a repo with NO checklists has nothing forcing stage ②: `assertEveryReviewerRan`\n * is vacuous, and review.json — the only other interlock — is a file the AI writes itself. It could\n * write it and come straight here, skipping the merge validation and the build entirely.\n */\n private assertReviewStageRan(repoRoot: string, featureName: string, headSha: string): boolean {\n const receipt = this.receipts.read(repoRoot, featureName);\n if (receipt === null) {\n throw new InformAiError(\n '⛔ NO PR — stage ② never ran on this branch. That means the 3-point merge is unvalidated,\\n' +\n 'the build gate has not run, no diff was extracted, and no reviewer was briefed.\\n\\n' +\n 'Run: pnpm wp-review-upsert-pr\\n' +\n '(then spawn the reviewers it names, write review.json, and re-run this command)',\n );\n }\n if (receipt.headSha === headSha) return true;\n // Not fatal. Re-reviewing on every follow-up commit would be intolerable, and most drift is a typo\n // fix. But it is never silent: the build re-runs here, and the PR says the reviewers saw an older tree.\n process.stderr.write(\n `\\n⚠️ HEAD moved since stage ② ran (reviewed ${receipt.headSha.slice(0, 8)}, now ${headSha.slice(0, 8)}).\\n` +\n ' The build gate will re-run, and the PR will record that reviewers judged an earlier tree.\\n' +\n ' If the change was substantive, re-run pnpm wp-review-upsert-pr and re-spawn the reviewers.\\n\\n');\n return false;\n }\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a verdict, naming exactly those reviewers and\n * exactly what to tell them — via the same renderer `wp-review-upsert-pr` uses, so the AI sees the identical\n * block it would have seen there. Lists ONLY the outstanding ones: re-instructing an already-reviewed\n * checklist invites a redundant second run and reads as though the earlier verdict did not count.\n * No-op for a repo with no applicable checklists.\n */\n private assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success`\n // format is otherwise indistinguishable from one that never ran, and the AI would go re-run a\n // subagent instead of correcting four characters of JSON.\n const format = scan.formatErrors.length === 0\n ? ''\n : `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n` +\n scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n throw new InformAiError(\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this ` +\n `branch have no passing verdict yet: ${this.instructions.names(scan.outstanding)}\\n\\n` +\n format +\n `${this.instructions.render(scan.outstanding, scan.reviewPath, scan.context)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr\\n` +\n `(Each reviewer's generated instructions file is already written — re-running wp-review-upsert-pr\\n` +\n ` is only needed if the code changed since it ran.)`,\n );\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard. (A checklist reaching this\n // point is always PASS/OVERRIDDEN — loadReviewJson already threw on FAIL/MISSING.)\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n /**\n * One comment row per DEFINED checklist — matched or not — pairing the roster's match evidence with the\n * resolved verdict. Built from `scan.roster` rather than from the applicable set: the skipped ones are\n * absent from `applicable` by construction, and their \"why not\" evidence (the configured globs and the\n * changed-file total) exists nowhere else without recomputing the diff a second way.\n */\n private commentRows(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): ChecklistCommentRow[] {\n // agentType -> did it open the diff. Absent ⇒ not assessed, which prints nothing (see ChecklistCommentRow.diffRead).\n const readByAgent = new Map<string, boolean>();\n for (const e of provenance.evidence) readByAgent.set(e.agentType, e.readDiff);\n return scan.roster.entries.map((entry: TriggeredChecklist): ChecklistCommentRow => {\n const ran = entry.matchedFiles.length > 0;\n const req = new RequiredChecklist(\n entry.def.id, entry.def.subagent, entry.def.doc, entry.matchedFiles, entry.matchedPatterns);\n // A skipped checklist has no verdict to resolve — asking for one would report it as MISSING,\n // i.e. as an unreviewed obligation, when in fact it never had one.\n const verdict = ran\n ? this.reviewJsonService.resolveVerdict(req, review.results)\n : new ChecklistVerdict(entry.def.id, '', '');\n const row = new ChecklistCommentRow(\n entry.def.subagent, verdict.status, verdict.detail, ran,\n entry.def.patterns, entry.matchedPatterns, entry.matchedFiles, scan.roster.changedFileCount);\n const read = readByAgent.get(entry.def.subagent);\n row.diffRead = read === undefined ? '' : (read ? 'yes' : 'no');\n return row;\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n // eslint-disable-next-line @typescript-eslint/max-params\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\n const errors: string[] = [];\n const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n if (subagents.length > 0) {\n const result = this.provenance.verifyDistinct(subagents, branch);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n report.evidence = this.gatherEvidence(repoRoot, required, result, branch);\n errors.push(...this.evidenceErrors(report.evidence, config));\n }\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return report;\n }\n\n /**\n * What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides\n * whether any of it blocks, and by default none of it does.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private gatherEvidence(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, branch: string): ReviewerEvidence[] {\n const docPaths: Record<string, string> = {};\n for (const req of required) {\n if (req.subagent.trim() !== '') docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n }\n const diffDir = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), 'diff');\n return this.provenance.evidenceFor(new EvidenceRequest(branch, result.agentIds, diffDir, docPaths));\n }\n\n /**\n * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.\n *\n * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the\n * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.\n * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.\n */\n private evidenceErrors(evidence: readonly ReviewerEvidence[], config: PrGateConfig): string[] {\n const blind = evidence.filter((e: ReviewerEvidence): boolean => !e.readDiff);\n if (blind.length === 0) return [];\n const names = blind.map((e: ReviewerEvidence): string => e.agentType).join(', ');\n if (!config.requireDiffEvidence) {\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\\n` +\n ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\\n');\n return [];\n }\n return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];\n }\n\n /**\n * Publish the roster + every reviewer's full `output` as ONE combined PR comment, idempotently (find the\n * marker comment → PATCH it, else POST). Never fatal: by here the PR is already created/updated, so a\n * `gh` failure only warns.\n *\n * Posted on EVERY run of a repo that defines checklists — including one where nothing matched, because\n * \"all five were evaluated and none applied\" is the good news the old comment could not deliver. The\n * guard is `scan.defined.length`, deliberately NOT the number of rows that ran: a repo with no\n * checklists configured must still see no comment at all (see ChecklistNotice / renderDashboard).\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n // eslint-disable-next-line @typescript-eslint/max-params\n private postChecklistComment(repoRoot: string, prNumber: string, scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): void {\n if (prNumber === '' || scan.defined.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n // GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before\n // the push, so CI's `synchronize` read cannot see the previous run's token.\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const published = this.publisher.publish(baseBranch, title, bodyFile);\n if (published.createFailed) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', '', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)', MERGE_RESULT_FAILED));\n }\n const num = published.number;\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
@@ -0,0 +1,43 @@
1
+ import { MergeOutcome } from './pr-merger';
2
+ export declare class FinishBannerInput {
3
+ prNumber: string;
4
+ prUrl: string;
5
+ title: string;
6
+ base: string;
7
+ merge: MergeOutcome;
8
+ constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome);
9
+ }
10
+ /**
11
+ * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that
12
+ * this class exists to get right.
13
+ *
14
+ * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded
15
+ * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a
16
+ * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which
17
+ * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked
18
+ * away from stranded PRs. Three of them independently reported the output as untrustworthy.
19
+ *
20
+ * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and
21
+ * carries the exact commands that fix it — including in the clickable-link directive, whose whole job
22
+ * is to be the last thing the AI says.
23
+ */
24
+ export declare class FinishBanner {
25
+ render(input: FinishBannerInput): string;
26
+ isDone(merge: MergeOutcome): boolean;
27
+ private header;
28
+ private whatIsOwed;
29
+ private behindRemedy;
30
+ private doneNote;
31
+ /**
32
+ * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the
33
+ * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.
34
+ *
35
+ * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got
36
+ * reported as finished, and the directive says the link must be last — so the truth has to live
37
+ * inside the link itself, not merely above it.
38
+ *
39
+ * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.
40
+ */
41
+ linkDirective(input: FinishBannerInput): string;
42
+ private notDoneTag;
43
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FinishBanner = exports.FinishBannerInput = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
+ const pr_merger_1 = require("./pr-merger");
7
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
8
+ // Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both
9
+ // `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).
10
+ class FinishBannerInput {
11
+ prNumber;
12
+ prUrl;
13
+ title;
14
+ base;
15
+ merge;
16
+ // eslint-disable-next-line @typescript-eslint/max-params
17
+ constructor(prNumber, prUrl, title, base, merge) {
18
+ this.prNumber = prNumber;
19
+ this.prUrl = prUrl;
20
+ this.title = title;
21
+ this.base = base;
22
+ this.merge = merge;
23
+ }
24
+ }
25
+ exports.FinishBannerInput = FinishBannerInput;
26
+ /**
27
+ * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that
28
+ * this class exists to get right.
29
+ *
30
+ * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded
31
+ * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a
32
+ * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which
33
+ * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked
34
+ * away from stranded PRs. Three of them independently reported the output as untrustworthy.
35
+ *
36
+ * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and
37
+ * carries the exact commands that fix it — including in the clickable-link directive, whose whole job
38
+ * is to be the last thing the AI says.
39
+ */
40
+ let FinishBanner = class FinishBanner {
41
+ // The full closing recap: header keyed to the real outcome, the four things this command did, and —
42
+ // when the PR is not done — what must happen next.
43
+ render(input) {
44
+ const prNum = input.prNumber;
45
+ return '\n' + SEP + this.header(input.merge) + SEP + '\n' +
46
+ ` 1. validated the build gate (authoritative)\n` +
47
+ ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: "${input.title}"\n` +
48
+ ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\n` +
49
+ ` 4. ${input.merge.message}\n` +
50
+ ` You are on ${input.base} — same name as the remote branch and the PR head.\n` +
51
+ this.whatIsOwed(input) + '\n';
52
+ }
53
+ // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,
54
+ // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —
55
+ // is unfinished work, and the banner must not decorate it with a checkmark.
56
+ isDone(merge) {
57
+ return merge.result === pr_merger_1.MERGE_RESULT_MERGED
58
+ || merge.result === pr_merger_1.MERGE_RESULT_AUTO_QUEUED
59
+ || merge.result === pr_merger_1.MERGE_RESULT_LEFT_TO_HUMAN;
60
+ }
61
+ header(merge) {
62
+ if (merge.result === pr_merger_1.MERGE_RESULT_MERGED)
63
+ return '✅ PR finished AND MERGED — here is exactly what I did\n';
64
+ if (merge.result === pr_merger_1.MERGE_RESULT_AUTO_QUEUED) {
65
+ return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\n';
66
+ }
67
+ if (merge.result === pr_merger_1.MERGE_RESULT_LEFT_TO_HUMAN) {
68
+ return '✅ PR finished — posted for a human to merge (that is this repo\'s policy)\n';
69
+ }
70
+ if (merge.result === pr_merger_1.MERGE_RESULT_BEHIND) {
71
+ return '⛔ PR NOT FINISHED — the branch is BEHIND main and will NEVER merge on its own\n';
72
+ }
73
+ return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\n';
74
+ }
75
+ // The follow-up block. '' for a done outcome — a finished run should not invent chores.
76
+ whatIsOwed(input) {
77
+ if (this.isDone(input.merge))
78
+ return this.doneNote(input.merge);
79
+ if (input.merge.result === pr_merger_1.MERGE_RESULT_BEHIND)
80
+ return this.behindRemedy(input);
81
+ return '\n' + SEP +
82
+ ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\n' +
83
+ ' Fix that, then re-run: pnpm wp-finish-upsert-pr\n' +
84
+ ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\n`;
85
+ }
86
+ // The ONE remedy that actually clears BEHIND. wp-start-upsert-pr is what re-syncs from main (3-point
87
+ // merge); finishing again re-runs the gate and re-attempts the merge on an up-to-date branch.
88
+ behindRemedy(input) {
89
+ const num = input.prNumber === '' ? '<n>' : input.prNumber;
90
+ return '\n' + SEP +
91
+ ' ⛔ DO NOT WALK AWAY — this PR is STRANDED, not pending.\n\n' +
92
+ ' BEHIND means the head branch is out of date with main. Auto-merge does NOT update\n' +
93
+ ' branches, so unlike a BLOCKED (checks-running) PR this one never resolves itself. Main\n' +
94
+ ' moved while you worked; re-sync and finish again, from THIS worktree:\n\n' +
95
+ ' pnpm wp-start-upsert-pr # 3-point merge from main — this is what clears BEHIND\n' +
96
+ ' pnpm wp-finish-upsert-pr # build gate + gated body + merge, now up to date\n\n' +
97
+ ' (If the re-sync conflicts, wp-start-upsert-pr walks you through resolving it.)\n' +
98
+ ` Verify independently, do not trust this banner:\n` +
99
+ ` gh pr view ${num} --json mergeable,mergeStateStatus,state\n`;
100
+ }
101
+ // A short, positive "you are free to stop" line for the two non-merged-but-fine outcomes, so an AI
102
+ // reading a queued PR does not go hunting for work that is not owed.
103
+ doneNote(merge) {
104
+ if (merge.result === pr_merger_1.MERGE_RESULT_AUTO_QUEUED) {
105
+ return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\n';
106
+ }
107
+ if (merge.result === pr_merger_1.MERGE_RESULT_LEFT_TO_HUMAN) {
108
+ return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\n';
109
+ }
110
+ return '';
111
+ }
112
+ /**
113
+ * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the
114
+ * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.
115
+ *
116
+ * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got
117
+ * reported as finished, and the directive says the link must be last — so the truth has to live
118
+ * inside the link itself, not merely above it.
119
+ *
120
+ * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.
121
+ */
122
+ linkDirective(input) {
123
+ if (input.prNumber === '' || input.prUrl === '')
124
+ return '';
125
+ const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;
126
+ const preamble = this.isDone(input.merge)
127
+ ? ''
128
+ : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly and\n` +
129
+ `MUST include the remedy commands printed above. Do not describe this run as complete.\n\n`;
130
+ return SEP + `🔗 PR #${input.prNumber}: ${input.title}\n ${input.prUrl}\n\n` + SEP + preamble +
131
+ `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\n` +
132
+ `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\n\n` +
133
+ ` [#${input.prNumber} ${label}](${input.prUrl})\n\n`;
134
+ }
135
+ notDoneTag(merge) {
136
+ return merge.result === pr_merger_1.MERGE_RESULT_BEHIND ? 'NOT MERGED — BEHIND main, needs a re-sync' : 'NOT MERGED';
137
+ }
138
+ };
139
+ exports.FinishBanner = FinishBanner;
140
+ exports.FinishBanner = FinishBanner = tslib_1.__decorate([
141
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
142
+ ], FinishBanner);
143
+ //# sourceMappingURL=finish-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finish-banner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/finish-banner.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n MergeOutcome, MERGE_RESULT_MERGED, MERGE_RESULT_AUTO_QUEUED, MERGE_RESULT_LEFT_TO_HUMAN,\n MERGE_RESULT_BEHIND,\n} from './pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// Everything the closing block of wp-finish-upsert-pr needs to describe what actually happened. Both\n// `prNumber` and `prUrl` are '' when the PR could not be resolved (e.g. `gh pr create` failed).\nexport class FinishBannerInput {\n prNumber: string;\n prUrl: string;\n title: string;\n base: string;\n merge: MergeOutcome;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prNumber: string, prUrl: string, title: string, base: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.title = title;\n this.base = base;\n this.merge = merge;\n }\n}\n\n/**\n * Renders the closing block of `wp-finish-upsert-pr` — and it is the FRAME, not the merge message, that\n * this class exists to get right.\n *\n * PrMerger has long been honest in its `message`. The banner around it was not: it printed a hard-coded\n * `✅ PR finished` on every path, so a run whose merge failed still looked like a completed one at a\n * glance. The worst case is `mergeStateStatus: BEHIND` — unlike BLOCKED (waiting on checks), which\n * auto-merge resolves on its own, a BEHIND branch NEVER lands. Under a green checkmark, agents walked\n * away from stranded PRs. Three of them independently reported the output as untrustworthy.\n *\n * So the header is derived from `MergeOutcome.result`, and a not-done outcome is loud, distinct, and\n * carries the exact commands that fix it — including in the clickable-link directive, whose whole job\n * is to be the last thing the AI says.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class FinishBanner {\n // The full closing recap: header keyed to the real outcome, the four things this command did, and —\n // when the PR is not done — what must happen next.\n render(input: FinishBannerInput): string {\n const prNum = input.prNumber;\n return '\\n' + SEP + this.header(input.merge) + SEP + '\\n' +\n ` 1. validated the build gate (authoritative)\\n` +\n ` 2. ${prNum ? `wrote the gated body to PR #${prNum}` : 'composed the gated PR body'} titled: \"${input.title}\"\\n` +\n ` 3. force-pushed your work to origin/${input.base} (after the body, so CI reads the right token)\\n` +\n ` 4. ${input.merge.message}\\n` +\n ` You are on ${input.base} — same name as the remote branch and the PR head.\\n` +\n this.whatIsOwed(input) + '\\n';\n }\n\n // TRUE only for the outcomes where nothing further is owed: merged, queued behind green-able checks,\n // or deliberately left for a human. Everything else — BEHIND, config mismatch, gh failure, no PR —\n // is unfinished work, and the banner must not decorate it with a checkmark.\n isDone(merge: MergeOutcome): boolean {\n return merge.result === MERGE_RESULT_MERGED\n || merge.result === MERGE_RESULT_AUTO_QUEUED\n || merge.result === MERGE_RESULT_LEFT_TO_HUMAN;\n }\n\n private header(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_MERGED) return '✅ PR finished AND MERGED — here is exactly what I did\\n';\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return '✅ PR finished — auto-merge is ON; it lands itself when the checks pass\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return '✅ PR finished — posted for a human to merge (that is this repo\\'s policy)\\n';\n }\n if (merge.result === MERGE_RESULT_BEHIND) {\n return '⛔ PR NOT FINISHED — the branch is BEHIND main and will NEVER merge on its own\\n';\n }\n return '⚠️ PR NOT FINISHED — the PR is up, but the merge did NOT happen\\n';\n }\n\n // The follow-up block. '' for a done outcome — a finished run should not invent chores.\n private whatIsOwed(input: FinishBannerInput): string {\n if (this.isDone(input.merge)) return this.doneNote(input.merge);\n if (input.merge.result === MERGE_RESULT_BEHIND) return this.behindRemedy(input);\n return '\\n' + SEP +\n ' ⚠️ DO NOT report this PR as done. The merge failed for the reason in step 4 above.\\n' +\n ' Fix that, then re-run: pnpm wp-finish-upsert-pr\\n' +\n ` Confirm for yourself: gh pr view ${input.prNumber === '' ? '<n>' : input.prNumber} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // The ONE remedy that actually clears BEHIND. wp-start-upsert-pr is what re-syncs from main (3-point\n // merge); finishing again re-runs the gate and re-attempts the merge on an up-to-date branch.\n private behindRemedy(input: FinishBannerInput): string {\n const num = input.prNumber === '' ? '<n>' : input.prNumber;\n return '\\n' + SEP +\n ' ⛔ DO NOT WALK AWAY — this PR is STRANDED, not pending.\\n\\n' +\n ' BEHIND means the head branch is out of date with main. Auto-merge does NOT update\\n' +\n ' branches, so unlike a BLOCKED (checks-running) PR this one never resolves itself. Main\\n' +\n ' moved while you worked; re-sync and finish again, from THIS worktree:\\n\\n' +\n ' pnpm wp-start-upsert-pr # 3-point merge from main — this is what clears BEHIND\\n' +\n ' pnpm wp-finish-upsert-pr # build gate + gated body + merge, now up to date\\n\\n' +\n ' (If the re-sync conflicts, wp-start-upsert-pr walks you through resolving it.)\\n' +\n ` Verify independently, do not trust this banner:\\n` +\n ` gh pr view ${num} --json mergeable,mergeStateStatus,state\\n`;\n }\n\n // A short, positive \"you are free to stop\" line for the two non-merged-but-fine outcomes, so an AI\n // reading a queued PR does not go hunting for work that is not owed.\n private doneNote(merge: MergeOutcome): string {\n if (merge.result === MERGE_RESULT_AUTO_QUEUED) {\n return ' Nothing else is owed: GitHub lands it when the checks go green. You can stop here.\\n';\n }\n if (merge.result === MERGE_RESULT_LEFT_TO_HUMAN) {\n return ' Nothing else is owed by the tooling — a person merges it. You can stop here.\\n';\n }\n return '';\n }\n\n /**\n * The closing AI directive: the resolved PR's number/title/URL and an instruction to end the\n * user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it.\n *\n * The link text CARRIES the outcome. Ending on a bare cheerful link is exactly how a stranded PR got\n * reported as finished, and the directive says the link must be last — so the truth has to live\n * inside the link itself, not merely above it.\n *\n * '' when the PR could not be resolved: better to say nothing than to point at a link that is not real.\n */\n linkDirective(input: FinishBannerInput): string {\n if (input.prNumber === '' || input.prUrl === '') return '';\n const label = this.isDone(input.merge) ? input.title : `${input.title} — ${this.notDoneTag(input.merge)}`;\n const preamble = this.isDone(input.merge)\n ? ''\n : `AI — this PR is NOT done (${this.notDoneTag(input.merge)}). Your summary MUST say so plainly and\\n` +\n `MUST include the remedy commands printed above. Do not describe this run as complete.\\n\\n`;\n return SEP + `🔗 PR #${input.prNumber}: ${input.title}\\n ${input.prUrl}\\n\\n` + SEP + preamble +\n `AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\\n` +\n `NOTHING printed after it (no sign-off, no next steps, no blank commentary):\\n\\n` +\n ` [#${input.prNumber} ${label}](${input.prUrl})\\n\\n`;\n }\n\n private notDoneTag(merge: MergeOutcome): string {\n return merge.result === MERGE_RESULT_BEHIND ? 'NOT MERGED — BEHIND main, needs a re-sync' : 'NOT MERGED';\n }\n}\n"]}
@@ -1,8 +1,36 @@
1
+ /**
2
+ * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string
3
+ * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to
4
+ * tell "done" from "stranded" without parsing prose.
5
+ */
6
+ export declare const MERGE_RESULT_MERGED = "MERGED";
7
+ /** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */
8
+ export declare const MERGE_RESULT_AUTO_QUEUED = "AUTO_QUEUED";
9
+ /** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */
10
+ export declare const MERGE_RESULT_LEFT_TO_HUMAN = "LEFT_TO_HUMAN";
11
+ /**
12
+ * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.
13
+ * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND
14
+ * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a
15
+ * green "✅ PR finished" and get abandoned.
16
+ */
17
+ export declare const MERGE_RESULT_BEHIND = "BEHIND";
18
+ /** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */
19
+ export declare const MERGE_RESULT_FAILED = "FAILED";
1
20
  export declare class MergeOutcome {
2
21
  merged: boolean;
3
22
  autoMergeEnabled: boolean;
4
23
  message: string;
5
- constructor(merged: boolean, autoMergeEnabled: boolean, message: string);
24
+ result: string;
25
+ constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string);
26
+ }
27
+ export declare class PrMergeState {
28
+ mergeable: string;
29
+ mergeStateStatus: string;
30
+ state: string;
31
+ constructor(mergeable: string, mergeStateStatus: string, state: string);
32
+ isBehind(): boolean;
33
+ describe(): string;
6
34
  }
7
35
  /**
8
36
  * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:
@@ -30,6 +58,28 @@ export declare class PrMerger {
30
58
  * is treated as NONE: when the policy is unreadable, do NOT touch main.
31
59
  */
32
60
  merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome;
61
+ /**
62
+ * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the
63
+ * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —
64
+ * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it
65
+ * is NOT success either, and reporting it as such is precisely the bug this whole file guards.
66
+ */
67
+ private fallBackToAutoMerge;
68
+ private behindOutcome;
69
+ private announceDirectFailure;
70
+ /**
71
+ * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is
72
+ * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on
73
+ * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost
74
+ * us the extra diagnosis, never turn a working run into a crash.
75
+ */
76
+ protected prMergeState(baseBranch: string): PrMergeState;
33
77
  protected autoMergeAllowed(): boolean;
34
78
  protected gh(args: string[], quiet?: boolean): number;
79
+ protected ghCapture(args: string[]): GhResult;
80
+ }
81
+ export declare class GhResult {
82
+ status: number;
83
+ output: string;
84
+ constructor(status: number, output: string);
35
85
  }
@@ -1,25 +1,73 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PrMerger = exports.MergeOutcome = void 0;
3
+ exports.GhResult = exports.PrMerger = exports.PrMergeState = exports.MergeOutcome = exports.MERGE_RESULT_FAILED = exports.MERGE_RESULT_BEHIND = exports.MERGE_RESULT_LEFT_TO_HUMAN = exports.MERGE_RESULT_AUTO_QUEUED = exports.MERGE_RESULT_MERGED = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
7
7
  const inversify_1 = require("inversify");
8
+ /**
9
+ * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string
10
+ * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to
11
+ * tell "done" from "stranded" without parsing prose.
12
+ */
13
+ exports.MERGE_RESULT_MERGED = 'MERGED';
14
+ /** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */
15
+ exports.MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';
16
+ /** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */
17
+ exports.MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';
18
+ /**
19
+ * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.
20
+ * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND
21
+ * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a
22
+ * green "✅ PR finished" and get abandoned.
23
+ */
24
+ exports.MERGE_RESULT_BEHIND = 'BEHIND';
25
+ /** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */
26
+ exports.MERGE_RESULT_FAILED = 'FAILED';
27
+ // The `mergeStateStatus` value GitHub reports for "head branch is not up to date with the base branch".
28
+ const GH_STATE_BEHIND = 'BEHIND';
8
29
  // What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the
9
30
  // final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —
10
31
  // the old code ignored `spawnSync().status` entirely and printed "✅ PR finished" even when `gh pr
11
- // merge` had errored out, which is what hid the auto-merge-disabled failure for weeks.
32
+ // merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the
33
+ // machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around
34
+ // the message can no longer say "finished" while the message says "did NOT merge".
12
35
  class MergeOutcome {
13
36
  merged;
14
37
  autoMergeEnabled;
15
38
  message;
16
- constructor(merged, autoMergeEnabled, message) {
39
+ result;
40
+ // eslint-disable-next-line @typescript-eslint/max-params
41
+ constructor(merged, autoMergeEnabled, message, result) {
17
42
  this.merged = merged;
18
43
  this.autoMergeEnabled = autoMergeEnabled;
19
44
  this.message = message;
45
+ this.result = result;
20
46
  }
21
47
  }
22
48
  exports.MergeOutcome = MergeOutcome;
49
+ // GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.
50
+ // All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.
51
+ class PrMergeState {
52
+ mergeable;
53
+ mergeStateStatus;
54
+ state;
55
+ constructor(mergeable, mergeStateStatus, state) {
56
+ this.mergeable = mergeable;
57
+ this.mergeStateStatus = mergeStateStatus;
58
+ this.state = state;
59
+ }
60
+ isBehind() {
61
+ return this.mergeStateStatus === GH_STATE_BEHIND;
62
+ }
63
+ // One-line rendering for the failure message, or '' when GitHub could not be asked.
64
+ describe() {
65
+ if (this.mergeStateStatus === '')
66
+ return '';
67
+ return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;
68
+ }
69
+ }
70
+ exports.PrMergeState = PrMergeState;
23
71
  /**
24
72
  * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:
25
73
  *
@@ -50,36 +98,94 @@ let PrMerger = class PrMerger {
50
98
  // posted/updated by this point, which is the whole job in this mode.
51
99
  if (mergeMode !== rules_config_1.MERGE_MODE_AUTO) {
52
100
  return new MergeOutcome(false, false, `did NOT merge — pr-gate.mergeMode is ${mergeMode === rules_config_1.MERGE_MODE_NONE ? 'NONE' : `"${mergeMode}"`}, so the PR is left for a human to merge.\n` +
53
- ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: "${subject}"`);
101
+ ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: "${subject}"`, exports.MERGE_RESULT_LEFT_TO_HUMAN);
54
102
  }
55
103
  // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to
56
104
  // main's history regardless of the repo's squash_merge_commit_title/message defaults — and
57
105
  // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.
58
- const direct = this.gh(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);
59
- if (direct === 0) {
60
- return new MergeOutcome(true, false, `squash-merged the PR as: "${subject}"`);
106
+ //
107
+ // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy
108
+ // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal
109
+ // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,
110
+ // framed as the expected first step of a two-step dance (see announceDirectFailure).
111
+ const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);
112
+ if (direct.status === 0) {
113
+ process.stdout.write(direct.output);
114
+ return new MergeOutcome(true, false, `squash-merged the PR as: "${subject}"`, exports.MERGE_RESULT_MERGED);
61
115
  }
62
- // Past here the PR is not mergeable yet checks still running, or a review / branch protection
63
- // is blocking it. The auto-merge queue is the only way to still land it unattended, so only ask
64
- // GitHub about the queue HERE: a run whose direct merge succeeded costs no extra API call.
116
+ // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not
117
+ // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date
118
+ // (BEHIND) can NEVER land unattended. Ask GitHub itself one cheap, failure-tolerant call, made
119
+ // only on the failure path, so a run whose direct merge succeeded costs no extra API call.
120
+ const state = this.prMergeState(baseBranch);
121
+ this.announceDirectFailure(direct.output, state);
122
+ return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state);
123
+ }
124
+ /**
125
+ * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the
126
+ * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —
127
+ * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it
128
+ * is NOT success either, and reporting it as such is precisely the bug this whole file guards.
129
+ */
130
+ // eslint-disable-next-line @typescript-eslint/max-params
131
+ fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state) {
65
132
  if (!this.autoMergeAllowed()) {
133
+ if (state.isBehind())
134
+ return this.behindOutcome(state, false);
66
135
  return new MergeOutcome(false, false, '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\n' +
67
136
  ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\n' +
68
137
  ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\n' +
69
138
  ' Fix it at ONE of the two ends: turn on "Allow auto-merge" in the repo settings, or\n' +
70
139
  ' set commands.pr-gate.mergeMode to "NONE" (and set that repo\'s\n' +
71
- ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).');
140
+ ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).', exports.MERGE_RESULT_FAILED);
72
141
  }
142
+ process.stdout.write(' … retrying with --auto (the auto-merge queue)\n');
73
143
  // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second
74
144
  // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the
75
145
  // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).
76
146
  this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);
77
147
  const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);
78
148
  if (auto !== 0) {
149
+ if (state.isBehind())
150
+ return this.behindOutcome(state, false);
79
151
  return new MergeOutcome(false, false, '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\n' +
80
- ' NOTHING is queued. Re-run once the PR is healthy.');
152
+ ' NOTHING is queued. Re-run once the PR is healthy.', exports.MERGE_RESULT_FAILED);
81
153
  }
82
- return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as "${subject}" when the checks pass`);
154
+ if (state.isBehind())
155
+ return this.behindOutcome(state, true);
156
+ return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as "${subject}" when the checks pass`, exports.MERGE_RESULT_AUTO_QUEUED);
157
+ }
158
+ // The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get
159
+ // enabled, because "parked forever" and "not queued at all" need different sentences — but neither
160
+ // of them is done, so both carry MERGE_RESULT_BEHIND.
161
+ behindOutcome(state, queued) {
162
+ return new MergeOutcome(false, queued, `⛔ did NOT merge — the head branch is BEHIND its base (${state.describe()}).\n` +
163
+ ` BEHIND does NOT self-heal: auto-merge never updates your branch, so nothing will land\n` +
164
+ ` this PR until it is re-synced from main. ` +
165
+ (queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.'), exports.MERGE_RESULT_BEHIND);
166
+ }
167
+ // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is
168
+ // captured (never inherited) so this framing is the only thing on screen.
169
+ announceDirectFailure(ghOutput, state) {
170
+ const reason = ghOutput.trim().split('\n').filter((l) => l.trim() !== '').pop() ?? '';
171
+ process.stdout.write('ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\n' +
172
+ ' policy forbids direct merges):\n' +
173
+ (reason === '' ? '' : ` ${reason}\n`) +
174
+ (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\n`));
175
+ }
176
+ /**
177
+ * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is
178
+ * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on
179
+ * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost
180
+ * us the extra diagnosis, never turn a working run into a crash.
181
+ */
182
+ prMergeState(baseBranch) {
183
+ const result = (0, child_process_1.spawnSync)('gh', ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',
184
+ '--jq', '"\\(.mergeable)\\t\\(.mergeStateStatus)\\t\\(.state)"'], { encoding: 'utf8' });
185
+ if (result.status !== 0)
186
+ return new PrMergeState('', '', '');
187
+ const parts = (result.stdout ?? '').trim().split('\t');
188
+ return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');
83
189
  }
84
190
  // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false
85
191
  // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed
@@ -96,9 +202,26 @@ let PrMerger = class PrMerger {
96
202
  const result = (0, child_process_1.spawnSync)('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });
97
203
  return result.status ?? -1;
98
204
  }
205
+ // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be
206
+ // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.
207
+ ghCapture(args) {
208
+ const result = (0, child_process_1.spawnSync)('gh', args, { encoding: 'utf8' });
209
+ return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));
210
+ }
99
211
  };
100
212
  exports.PrMerger = PrMerger;
101
213
  exports.PrMerger = PrMerger = tslib_1.__decorate([
102
214
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
103
215
  ], PrMerger);
216
+ // A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams
217
+ // combined, in the order a terminal would have shown them.
218
+ class GhResult {
219
+ status;
220
+ output;
221
+ constructor(status, output) {
222
+ this.status = status;
223
+ this.output = output;
224
+ }
225
+ }
226
+ exports.GhResult = GhResult;
104
227
  //# sourceMappingURL=pr-merger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks.\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else\n * (including a repo still on a published rules-config that predates the field)\n * is treated as NONE: when the policy is unreadable, do NOT touch main.\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (mergeMode !== MERGE_MODE_AUTO) {\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: \"${subject}\"`);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n const direct = this.gh(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct === 0) {\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`);\n }\n\n // Past here the PR is not mergeable yet — checks still running, or a review / branch protection\n // is blocking it. The auto-merge queue is the only way to still land it unattended, so only ask\n // GitHub about the queue HERE: a run whose direct merge succeeded costs no extra API call.\n if (!this.autoMergeAllowed()) {\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).');\n }\n\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.');\n }\n return new MergeOutcome(false, true, `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`);\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n}\n"]}
1
+ {"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string\n * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to\n * tell \"done\" from \"stranded\" without parsing prose.\n */\nexport const MERGE_RESULT_MERGED = 'MERGED';\n/** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */\nexport const MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';\n/** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */\nexport const MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';\n/**\n * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.\n * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND\n * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a\n * green \"✅ PR finished\" and get abandoned.\n */\nexport const MERGE_RESULT_BEHIND = 'BEHIND';\n/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */\nexport const MERGE_RESULT_FAILED = 'FAILED';\n\n// The `mergeStateStatus` value GitHub reports for \"head branch is not up to date with the base branch\".\nconst GH_STATE_BEHIND = 'BEHIND';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the\n// machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around\n// the message can no longer say \"finished\" while the message says \"did NOT merge\".\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n result: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n this.result = result;\n }\n}\n\n// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.\n// All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.\nexport class PrMergeState {\n mergeable: string;\n mergeStateStatus: string;\n state: string;\n\n constructor(mergeable: string, mergeStateStatus: string, state: string) {\n this.mergeable = mergeable;\n this.mergeStateStatus = mergeStateStatus;\n this.state = state;\n }\n\n isBehind(): boolean {\n return this.mergeStateStatus === GH_STATE_BEHIND;\n }\n\n // One-line rendering for the failure message, or '' when GitHub could not be asked.\n describe(): string {\n if (this.mergeStateStatus === '') return '';\n return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param mergeMode pr-gate.mergeMode from webpieces.config.json — AUTO or NONE. Anything else\n * (including a repo still on a published rules-config that predates the field)\n * is treated as NONE: when the policy is unreadable, do NOT touch main.\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, mergeMode: string): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (mergeMode !== MERGE_MODE_AUTO) {\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` Subject GitHub will use is its own (squash_merge_commit_title), NOT: \"${subject}\"`,\n MERGE_RESULT_LEFT_TO_HUMAN);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n //\n // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy\n // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal\n // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,\n // framed as the expected first step of a two-step dance (see announceDirectFailure).\n const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct.status === 0) {\n process.stdout.write(direct.output);\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`, MERGE_RESULT_MERGED);\n }\n\n // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not\n // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date\n // (BEHIND) can NEVER land unattended. Ask GitHub itself — one cheap, failure-tolerant call, made\n // only on the failure path, so a run whose direct merge succeeded costs no extra API call.\n const state = this.prMergeState(baseBranch);\n this.announceDirectFailure(direct.output, state);\n return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state);\n }\n\n /**\n * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the\n * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —\n * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it\n * is NOT success either, and reporting it as such is precisely the bug this whole file guards.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private fallBackToAutoMerge(baseBranch: string, subject: string, mergeBodyFile: string, state: PrMergeState): MergeOutcome {\n if (!this.autoMergeAllowed()) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).',\n MERGE_RESULT_FAILED);\n }\n\n process.stdout.write(' … retrying with --auto (the auto-merge queue)\\n');\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.',\n MERGE_RESULT_FAILED);\n }\n if (state.isBehind()) return this.behindOutcome(state, true);\n return new MergeOutcome(false, true,\n `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`,\n MERGE_RESULT_AUTO_QUEUED);\n }\n\n // The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get\n // enabled, because \"parked forever\" and \"not queued at all\" need different sentences — but neither\n // of them is done, so both carry MERGE_RESULT_BEHIND.\n private behindOutcome(state: PrMergeState, queued: boolean): MergeOutcome {\n return new MergeOutcome(false, queued,\n `⛔ did NOT merge — the head branch is BEHIND its base (${state.describe()}).\\n` +\n ` BEHIND does NOT self-heal: auto-merge never updates your branch, so nothing will land\\n` +\n ` this PR until it is re-synced from main. ` +\n (queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.'),\n MERGE_RESULT_BEHIND);\n }\n\n // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is\n // captured (never inherited) so this framing is the only thing on screen.\n private announceDirectFailure(ghOutput: string, state: PrMergeState): void {\n const reason = ghOutput.trim().split('\\n').filter((l: string): boolean => l.trim() !== '').pop() ?? '';\n process.stdout.write(\n 'ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\\n' +\n ' policy forbids direct merges):\\n' +\n (reason === '' ? '' : ` ${reason}\\n`) +\n (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\\n`),\n );\n }\n\n /**\n * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is\n * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on\n * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost\n * us the extra diagnosis, never turn a working run into a crash.\n */\n protected prMergeState(baseBranch: string): PrMergeState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',\n '--jq', '\"\\\\(.mergeable)\\\\t\\\\(.mergeStateStatus)\\\\t\\\\(.state)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return new PrMergeState('', '', '');\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n\n // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be\n // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.\n protected ghCapture(args: string[]): GhResult {\n const result = spawnSync('gh', args, { encoding: 'utf8' });\n return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));\n }\n}\n\n// A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams\n// combined, in the order a terminal would have shown them.\nexport class GhResult {\n status: number;\n output: string;\n\n constructor(status: number, output: string) {\n this.status = status;\n this.output = output;\n }\n}\n"]}