@webpieces/pr-gate 0.4.603 → 0.4.604

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.603",
3
+ "version": "0.4.604",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.603",
18
+ "@webpieces/rules-config": "0.4.604",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -5,6 +5,7 @@ import { ChecklistScanner } from '../workflow/checklist-scanner';
5
5
  import { ReviewerVerdictGate } from '../workflow/reviewer-verdict-gate';
6
6
  import { GitExec } from '../workflow/git-exec';
7
7
  import { BuildAffected } from '../workflow/build-affected';
8
+ import { BuildGateLog } from '../workflow/build-gate-log';
8
9
  import { MergeState } from '../workflow/merge-state';
9
10
  import { ReviewStageReceiptService } from '../workflow/review-stage-receipt';
10
11
  import { PrMerger } from '../workflow/pr-merger';
@@ -22,6 +23,7 @@ export declare class FinishUpsertPrCommand {
22
23
  private readonly branchNaming;
23
24
  private readonly gitExec;
24
25
  private readonly buildAffected;
26
+ private readonly buildLog;
25
27
  private readonly mergeState;
26
28
  private readonly prMerger;
27
29
  private readonly publisher;
@@ -37,7 +39,7 @@ export declare class FinishUpsertPrCommand {
37
39
  private readonly mergeBodyFile;
38
40
  private readonly commentUpserter;
39
41
  private readonly squashSettings;
40
- constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistComment: ChecklistCommentRenderer, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenanceEnforcer: ProvenanceEnforcer, receipts: ReviewStageReceiptService, banner: FinishBanner, mergeBodyFile: MergeBodyTempFile, commentUpserter: PrCommentUpserter, squashSettings: SquashSettingsEnforcer);
42
+ constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, buildLog: BuildGateLog, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistComment: ChecklistCommentRenderer, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenanceEnforcer: ProvenanceEnforcer, receipts: ReviewStageReceiptService, banner: FinishBanner, mergeBodyFile: MergeBodyTempFile, commentUpserter: PrCommentUpserter, squashSettings: SquashSettingsEnforcer);
41
43
  run(): Promise<void>;
42
44
  /**
43
45
  * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.
@@ -47,6 +49,17 @@ export declare class FinishUpsertPrCommand {
47
49
  * three-stage flow cost ONE build rather than two, and it is why moving the gate earlier was affordable.
48
50
  */
49
51
  private runOrSkipBuildGate;
52
+ /**
53
+ * When the gate is SKIPPED and build-log capture is on, name STAGE ②'s log — the one build that
54
+ * actually ran for this commit. Deliberately never names a finish-stage log: no finish build ran, and
55
+ * pointing at a file this command did not write would claim a build that did not happen.
56
+ *
57
+ * The path is recomputed rather than stored, and that is sound precisely BECAUSE this branch is only
58
+ * reached when the receipt covers the current HEAD: same worktree, same branch, same sha ⇒ the same
59
+ * deterministic filename stage ② wrote. Empty string whenever capture is off or the file is gone, so
60
+ * a user without `~/.webpieces/config.json` sees the pre-existing single line unchanged.
61
+ */
62
+ private skippedBuildLogNote;
50
63
  /**
51
64
  * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.
52
65
  * Returns true when that receipt covers the CURRENT HEAD, i.e. the build gate can be skipped.
@@ -13,6 +13,7 @@ const checklist_scanner_1 = require("../workflow/checklist-scanner");
13
13
  const reviewer_verdict_gate_1 = require("../workflow/reviewer-verdict-gate");
14
14
  const git_exec_1 = require("../workflow/git-exec");
15
15
  const build_affected_1 = require("../workflow/build-affected");
16
+ const build_gate_log_1 = require("../workflow/build-gate-log");
16
17
  const merge_state_1 = require("../workflow/merge-state");
17
18
  const review_stage_receipt_1 = require("../workflow/review-stage-receipt");
18
19
  const pr_merger_1 = require("../workflow/pr-merger");
@@ -100,6 +101,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
100
101
  branchNaming;
101
102
  gitExec;
102
103
  buildAffected;
104
+ buildLog;
103
105
  mergeState;
104
106
  prMerger;
105
107
  publisher;
@@ -115,7 +117,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
115
117
  mergeBodyFile;
116
118
  commentUpserter;
117
119
  squashSettings;
118
- constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard,
120
+ constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, buildLog, mergeState, prMerger, publisher, dashboard,
119
121
  // The 2nd PR comment. Its own class because it is its own surface — see ChecklistCommentRenderer.
120
122
  checklistComment, checklistScanner, verdictGate, reviewJsonService, gateTokenService,
121
123
  // Owns the reviewer-provenance integrity check and its audit record — see ProvenanceEnforcer.
@@ -137,6 +139,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
137
139
  this.branchNaming = branchNaming;
138
140
  this.gitExec = gitExec;
139
141
  this.buildAffected = buildAffected;
142
+ this.buildLog = buildLog;
140
143
  this.mergeState = mergeState;
141
144
  this.prMerger = prMerger;
142
145
  this.publisher = publisher;
@@ -218,10 +221,27 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
218
221
  */
219
222
  runOrSkipBuildGate(repoRoot, alreadyGreen) {
220
223
  if (alreadyGreen) {
221
- process.stdout.write('\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\n');
224
+ process.stdout.write('\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\n'
225
+ + this.skippedBuildLogNote(repoRoot));
222
226
  return;
223
227
  }
224
- this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.'));
228
+ this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.', build_gate_log_1.FINISH_STAGE));
229
+ }
230
+ /**
231
+ * When the gate is SKIPPED and build-log capture is on, name STAGE ②'s log — the one build that
232
+ * actually ran for this commit. Deliberately never names a finish-stage log: no finish build ran, and
233
+ * pointing at a file this command did not write would claim a build that did not happen.
234
+ *
235
+ * The path is recomputed rather than stored, and that is sound precisely BECAUSE this branch is only
236
+ * reached when the receipt covers the current HEAD: same worktree, same branch, same sha ⇒ the same
237
+ * deterministic filename stage ② wrote. Empty string whenever capture is off or the file is gone, so
238
+ * a user without `~/.webpieces/config.json` sees the pre-existing single line unchanged.
239
+ */
240
+ skippedBuildLogNote(repoRoot) {
241
+ if (!this.buildAffected.isCaptureEnabled())
242
+ return '';
243
+ const log = this.buildLog.existingLogFor(repoRoot, build_gate_log_1.REVIEW_STAGE);
244
+ return log === '' ? '' : ` Stage ②'s full build output: ${log}\n`;
225
245
  }
226
246
  /**
227
247
  * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.
@@ -596,6 +616,7 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
596
616
  branch_naming_1.BranchNaming,
597
617
  git_exec_1.GitExec,
598
618
  build_affected_1.BuildAffected,
619
+ build_gate_log_1.BuildGateLog,
599
620
  merge_state_1.MergeState,
600
621
  pr_merger_1.PrMerger,
601
622
  gated_pr_publisher_1.GatedPrPublisher,
@@ -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, GateTokenService,\n InformAiError, toError,\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 { ReviewerVerdictGate } from '../workflow/reviewer-verdict-gate';\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, MergeIntent, MergeOutcome, MERGE_RESULT_FAILED } from '../workflow/pr-merger';\nimport { FinishBanner, FinishBannerInput } from '../workflow/finish-banner';\nimport { MergeBodyTempFile } from '../workflow/merge-body-temp-file';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { ProvenanceEnforcer, ProvenanceReport } from '../workflow/provenance-enforcer';\nimport { PrCommentRequest, PrCommentUpserter } from '../workflow/pr-comment-upserter';\nimport { SquashSettingsEnforcer } from '../workflow/squash-settings-enforcer';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, DETAIL_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\nimport {\n ChecklistCommentRenderer, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/checklist-comment-renderer';\nimport { ChecklistCommentRow } from '../../dashboard/checklist-comment-row';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The three inputs the PR COMMENTS are rendered from, bundled so `publishAll` takes one parameter for\n * them instead of three it only forwards. Data-only, per CLAUDE.md.\n */\nclass PrCommentSources {\n scan: ChecklistScan;\n review: ReviewJson;\n provenance: ProvenanceReport;\n\n constructor(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport) {\n this.scan = scan;\n this.review = review;\n this.provenance = provenance;\n }\n}\n\n/**\n * Everything the PR upsert needs. Data-only, per CLAUDE.md — it replaced a 5-positional-parameter method\n * once `tokenSuffix` had to travel too, so the body can be re-rendered with the PR's own URL after\n * `gh pr create` returns it (see the INVARIANT comment in upsertPr).\n */\nclass PrUpsertRequest {\n repoRoot = '';\n baseBranch = '';\n title = '';\n /** The description as first published — rendered with whatever URL was known at the time. */\n body = '';\n /**\n * The hidden gate-token marker exactly as appended to `body`, so a re-render can reproduce the same\n * bytes. Kept separate from `body` because Dashboard renders the human/git-log half and must not know\n * about HMACs.\n */\n tokenSuffix = '';\n input: DashboardInput;\n\n constructor(input: DashboardInput) {\n this.input = input;\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 // The 2nd PR comment. Its own class because it is its own surface — see ChecklistCommentRenderer.\n private readonly checklistComment: ChecklistCommentRenderer,\n private readonly checklistScanner: ChecklistScanner,\n private readonly verdictGate: ReviewerVerdictGate,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n // Owns the reviewer-provenance integrity check and its audit record — see ProvenanceEnforcer.\n private readonly provenanceEnforcer: ProvenanceEnforcer,\n // NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its \"You MUST run\n // these N reviewer subagent(s)\" block is now rendered by ReviewerVerdictGate and ONLY for checklists\n // that genuinely never ran, so no other code path in this command can print it at a refusal.\n private readonly receipts: ReviewStageReceiptService,\n private readonly banner: FinishBanner,\n // `gh pr merge --body-file` takes a path, so the bytes just published as the PR description are\n // spilled to a throwaway file for the length of one `gh` call. Nothing durable — the PR holds it.\n private readonly mergeBodyFile: MergeBodyTempFile,\n // ONE marker-keyed upsert, shared by both PR comments (the full dashboard and the checklist).\n private readonly commentUpserter: PrCommentUpserter,\n // Pins the two GitHub repo settings that decide whether a UI merge writes the body we just\n // rendered. Server-side, so no config can express them — see SquashSettingsEnforcer.\n private readonly squashSettings: SquashSettingsEnforcer,\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, 'stage3-finish'));\n const required = scan.applicable;\n // The applicable checklists that are supposed to HAVE a verdict — everything except the optional ones\n // nobody ran. Used for provenance and for the dashboard rows, both of which ask \"who reviewed this?\"\n // and would otherwise demand an answer from a review the human declined: provenance would refuse the\n // PR because a subagent that was never meant to run cannot be proven to have run, and the dashboard\n // would render a MISSING verdict for it.\n const verdicted = this.verdictedOf(scan);\n // FAIL FAST on an unclear checklist, 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. ReviewerVerdictGate owns the\n // distinction between unreadable / REFUSED / never-ran, and retires the red verdicts it acts on.\n this.verdictGate.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.provenanceEnforcer.enforce(verdicted, 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, verdicted);\n const result = this.publishAll(repoRoot, base, input, new PrCommentSources(scan, review, provenance));\n this.archiveConsumedReview(repoRoot, featureName, result);\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 * Retire the review this run just used — move review.json to old-review.json beside it, stamped as\n * audit-only (see ReviewJsonService.archiveReviewJson).\n *\n * WHY it moves rather than staying put: review.json left behind is a live-looking file describing a\n * review that has already shipped, and the next `wp-review-upsert-pr` on this branch finds it sitting\n * there. A reviewer subagent that judges the PR's stated INTENT — its title, summary or risk level —\n * then reads the previous round's review and can return GREEN against a title that no longer exists,\n * with nothing in the verdict distinguishing that from a real pass. Moving it makes the only route back\n * to this command a freshly-written review.\n *\n * ONLY on a PR that actually went up. A run that died before publishing must stay re-runnable without\n * making the AI rewrite a review that was never used, and prNumber === '' is exactly that case.\n * Non-fatal: the PR is live by here, so an unwritable archive warns rather than failing the command.\n */\n private archiveConsumedReview(repoRoot: string, featureName: string, result: UpsertResult): void {\n if (result.prNumber === '') return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the PR is already up; a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const archived = this.reviewJsonService.archiveReviewJson(reviewJsonPath(repoRoot, featureName));\n if (archived !== '') process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\\n`);\n // Beside it, so an archived review keeps the transcript links belonging to the round that\n // produced it — a review whose provenance was overwritten by the NEXT round audits nothing.\n this.provenanceEnforcer.archiveRecord(prDirFor(repoRoot, featureName));\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`⚠️ Could not archive review.json (non-fatal — the PR is already up): ${error.message}\\n`);\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 // buildCommand travels into the dashboard so the PR-body footer can NAME the command that vouched\n // for this commit. The footer used to assert \"build ran via nx affected\" on every repo, which was\n // simply false wherever buildCommand is not nx.\n return new DashboardInput(\n title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows,\n config.buildCommand);\n }\n\n /**\n * The applicable checklists MINUS the optional ones nobody ran.\n *\n * Not folded into `ChecklistScan` as yet another field: `applicable` (what matched) and `optionalNotRun`\n * (what was declined) are both facts about the scan, whereas this is one command's view of them, and the\n * roster comment deliberately wants the OTHER view — it lists the declined ones precisely so the PR does\n * not imply they passed.\n */\n private verdictedOf(scan: ChecklistScan): RequiredChecklist[] {\n const skipped = new Set(scan.optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return scan.applicable.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard.\n //\n // A checklist reaching this point is always PASS, WARN or OVERRIDDEN, and the reason is ReviewerVerdictGate\n // — NOT loadReviewJson. The gate runs one line earlier and throws on every other state (FAIL, MISSING,\n // BAD_FORMAT), so loadReviewJson's own checklist validation can no longer be the thing that rejects them;\n // it re-validates the same set and finds it clean. This comment used to credit loadReviewJson, which made\n // the ordering look deliberate while the gate's generic \"no verdict yet\" message masked every refusal.\n // WARN belongs in that list: yellow SHIPS, so it is not outstanding and reaches the dashboard.\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 entry.def.required);\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 row.required = entry.def.required;\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\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 ChecklistCommentRenderer).\n */\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 request = new PrCommentRequest();\n request.prNumber = prNumber;\n request.marker = CHECKLIST_COMMENT_MARKER;\n request.body = this.checklistComment.render(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n request.payloadDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n request.payloadName = 'checklist-comment.json';\n request.label = 'checklist review comment';\n this.commentUpserter.upsert(request);\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(request: PrUpsertRequest): UpsertResult {\n const repoRoot = request.repoRoot;\n const baseBranch = request.baseBranch;\n const title = request.title;\n const input = request.input;\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, request.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\n /*\n * THE INVARIANT: the merge body IS the PR description, byte for byte.\n *\n * That is what makes every landing route agree — the GitHub Merge button and a bare `gh pr merge`\n * copy the description via `squash_merge_commit_message: PR_BODY`, while `wp-land-pr` and finish's\n * own auto-merge pass these bytes with `--body-file`. If the two strings could differ, which route\n * landed the commit would change what history says, which is precisely the bug this whole change\n * exists to remove. `pr-body-is-merge-body.spec.ts` pins it.\n *\n * The one wrinkle is the self-link. A brand-new PR has no URL until `gh pr create` returns, so the\n * body published a moment ago was rendered with `prUrl: ''`. Re-render now that the URL is known\n * and push the corrected description back, so the two strings match and the commit carries its own\n * link. Only the CREATE path pays that extra edit: on every later run the URL was already known\n * before publishing, `finalBody` matches what went up, and nothing is re-sent.\n */\n const finalBody = this.dashboard.renderPrBody(input, ref.url) + request.tokenSuffix;\n if (finalBody !== request.body) this.backfillPrBody(ref.number, bodyFile, finalBody);\n\n // The DURABLE copy of these bytes is the PR description published above; this is a throwaway\n // file because `--body-file` takes a path. `wp-land-pr` reads the description back from GitHub,\n // so nothing here has to survive the process (see decisions/0005).\n const mergeBodyFile = this.mergeBodyFile.write(finalBody + '\\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 // `false`: this caller is POLICY-driven — only a config that really says AUTO merges here.\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, new MergeIntent(mergeMode, false));\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n /**\n * Everything the gated flow PUBLISHES, in the one order that is safe — the three surfaces plus the\n * merge, as a single unit.\n *\n * 1. the PR DESCRIPTION = the compact git-log body (+ the hidden gate token), written before the\n * push so CI's `synchronize` read can never see the previous run's token,\n * 2. the 1st comment = the full dashboard,\n * 3. the 2nd comment = each reviewer's output.\n *\n * The token is bound to the LOCAL HEAD sha and minted here because we only reach this line after the\n * build gate and every BLOCK checklist passed, so minting is legitimate. Nothing about\n * HMAC(salt, HEAD) needs the remote to have the commit yet.\n *\n * Extracted from `run` when the two comments made it 82 lines against a hard 80-line rule — and it\n * earns its own name: these four steps have a REQUIRED order, and the comment explaining that order\n * belongs with them rather than inside a method that also loads config and runs a build gate.\n */\n private publishAll(repoRoot: string, base: string, input: DashboardInput, sources: PrCommentSources): UpsertResult {\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const upsert = new PrUpsertRequest(input);\n upsert.repoRoot = repoRoot;\n upsert.baseBranch = base;\n upsert.title = input.title;\n upsert.tokenSuffix = this.gateTokenBody(gateSalt, headSha);\n // `existingPrUrl` is '' only for a brand-new PR — there is no URL to self-link to until\n // `gh pr create` returns one, and upsertPr back-fills it the moment it does.\n upsert.body = this.dashboard.renderPrBody(input, this.existingPrUrl(base)) + upsert.tokenSuffix;\n const result = this.upsertPr(upsert);\n\n // In the order the PR body's pointer promises: full dashboard 1st, reviewer output 2nd. Both are\n // idempotent by hidden marker and both non-fatal — the PR is up by now, so a `gh` hiccup on a\n // comment must not turn a finished run into a failed one.\n this.postDetailComment(repoRoot, result.prNumber, input);\n this.postChecklistComment(repoRoot, result.prNumber, sources.scan, sources.review, sources.provenance);\n\n // Having just written the description AS the commit body, make sure GitHub will actually use it.\n // Here rather than anywhere earlier because it is about what happens to the artifact we just\n // published, and it must run on EVERY repo — including mergeMode=NONE ones that never reach\n // PrMerger, which are exactly the repos where a UI merge decides what main's history says.\n this.squashSettings.ensure();\n return result;\n }\n\n /**\n * Re-publish the description now that the PR's own URL is known (create path only).\n *\n * Non-fatal on purpose. The PR exists, its body already carries a valid gate token for the pushed\n * head, and the code is up — the ONLY thing missing is the self-link on the first line. Aborting a\n * finished run over a cosmetic back-link would be a worse outcome than the missing link, and the very\n * next `wp-finish-upsert-pr` writes it (by then the URL is known before publishing, so it goes up with\n * the body). The merge body still gets the linked version regardless: it is filed from `finalBody`.\n */\n private backfillPrBody(prNumber: string, bodyFile: string, finalBody: string): void {\n if (prNumber === '') return;\n fs.writeFileSync(bodyFile, finalBody + '\\n');\n const res = spawnSync('gh', ['pr', 'edit', prNumber, '--body-file', bodyFile], { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write(\n '⚠️ Could not add the PR\\'s own link to its description (non-fatal — the PR and the gate\\n' +\n ' token are already up). The next wp-finish-upsert-pr writes it.\\n');\n return;\n }\n process.stdout.write(' back-filled the PR description with its own link ✓\\n');\n }\n\n /**\n * The PR's web URL if one is already open for this branch, else '' — read BEFORE publishing so the\n * description can carry its own link on the first render. Only a brand-new PR gets '' (and then\n * {@link backfillPrBody} fixes it up after `gh pr create` returns a URL).\n */\n private existingPrUrl(baseBranch: string): string {\n return this.prRef(baseBranch).url;\n }\n\n /**\n * The 1st PR comment: the FULL dashboard — every row including the green ones, the whole summary, the\n * 3-point hash points. This is what the PR description used to be, and moving it here is the entire\n * point of the change: the description is now the git-log body, and a squash commit must not carry a\n * risk table.\n *\n * Posted unconditionally (any repo, checklists or not), because the description's last bullet PROMISES\n * a 1st comment holding the detail. A pointer to a comment that does not exist is worse than no\n * pointer. Non-fatal — the PR is already up, and the description alone is a complete, readable record.\n */\n private postDetailComment(repoRoot: string, prNumber: string, input: DashboardInput): void {\n if (prNumber === '') return;\n const request = new PrCommentRequest();\n request.prNumber = prNumber;\n request.marker = DETAIL_COMMENT_MARKER;\n request.body = DETAIL_COMMENT_MARKER + '\\n' + this.dashboard.renderDetailComment(input);\n request.payloadDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n request.payloadName = 'detail-comment.json';\n request.label = 'full dashboard comment';\n this.commentUpserter.upsert(request);\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, GateTokenService,\n InformAiError, toError,\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 { ReviewerVerdictGate } from '../workflow/reviewer-verdict-gate';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BuildGateLog, FINISH_STAGE, REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { PrMerger, MergeIntent, MergeOutcome, MERGE_RESULT_FAILED } from '../workflow/pr-merger';\nimport { FinishBanner, FinishBannerInput } from '../workflow/finish-banner';\nimport { MergeBodyTempFile } from '../workflow/merge-body-temp-file';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { ProvenanceEnforcer, ProvenanceReport } from '../workflow/provenance-enforcer';\nimport { PrCommentRequest, PrCommentUpserter } from '../workflow/pr-comment-upserter';\nimport { SquashSettingsEnforcer } from '../workflow/squash-settings-enforcer';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, DETAIL_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\nimport {\n ChecklistCommentRenderer, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/checklist-comment-renderer';\nimport { ChecklistCommentRow } from '../../dashboard/checklist-comment-row';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The three inputs the PR COMMENTS are rendered from, bundled so `publishAll` takes one parameter for\n * them instead of three it only forwards. Data-only, per CLAUDE.md.\n */\nclass PrCommentSources {\n scan: ChecklistScan;\n review: ReviewJson;\n provenance: ProvenanceReport;\n\n constructor(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport) {\n this.scan = scan;\n this.review = review;\n this.provenance = provenance;\n }\n}\n\n/**\n * Everything the PR upsert needs. Data-only, per CLAUDE.md — it replaced a 5-positional-parameter method\n * once `tokenSuffix` had to travel too, so the body can be re-rendered with the PR's own URL after\n * `gh pr create` returns it (see the INVARIANT comment in upsertPr).\n */\nclass PrUpsertRequest {\n repoRoot = '';\n baseBranch = '';\n title = '';\n /** The description as first published — rendered with whatever URL was known at the time. */\n body = '';\n /**\n * The hidden gate-token marker exactly as appended to `body`, so a re-render can reproduce the same\n * bytes. Kept separate from `body` because Dashboard renders the human/git-log half and must not know\n * about HMACs.\n */\n tokenSuffix = '';\n input: DashboardInput;\n\n constructor(input: DashboardInput) {\n this.input = input;\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 buildLog: BuildGateLog,\n private readonly mergeState: MergeState,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n // The 2nd PR comment. Its own class because it is its own surface — see ChecklistCommentRenderer.\n private readonly checklistComment: ChecklistCommentRenderer,\n private readonly checklistScanner: ChecklistScanner,\n private readonly verdictGate: ReviewerVerdictGate,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n // Owns the reviewer-provenance integrity check and its audit record — see ProvenanceEnforcer.\n private readonly provenanceEnforcer: ProvenanceEnforcer,\n // NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its \"You MUST run\n // these N reviewer subagent(s)\" block is now rendered by ReviewerVerdictGate and ONLY for checklists\n // that genuinely never ran, so no other code path in this command can print it at a refusal.\n private readonly receipts: ReviewStageReceiptService,\n private readonly banner: FinishBanner,\n // `gh pr merge --body-file` takes a path, so the bytes just published as the PR description are\n // spilled to a throwaway file for the length of one `gh` call. Nothing durable — the PR holds it.\n private readonly mergeBodyFile: MergeBodyTempFile,\n // ONE marker-keyed upsert, shared by both PR comments (the full dashboard and the checklist).\n private readonly commentUpserter: PrCommentUpserter,\n // Pins the two GitHub repo settings that decide whether a UI merge writes the body we just\n // rendered. Server-side, so no config can express them — see SquashSettingsEnforcer.\n private readonly squashSettings: SquashSettingsEnforcer,\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, 'stage3-finish'));\n const required = scan.applicable;\n // The applicable checklists that are supposed to HAVE a verdict — everything except the optional ones\n // nobody ran. Used for provenance and for the dashboard rows, both of which ask \"who reviewed this?\"\n // and would otherwise demand an answer from a review the human declined: provenance would refuse the\n // PR because a subagent that was never meant to run cannot be proven to have run, and the dashboard\n // would render a MISSING verdict for it.\n const verdicted = this.verdictedOf(scan);\n // FAIL FAST on an unclear checklist, 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. ReviewerVerdictGate owns the\n // distinction between unreadable / REFUSED / never-ran, and retires the red verdicts it acts on.\n this.verdictGate.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.provenanceEnforcer.enforce(verdicted, 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, verdicted);\n const result = this.publishAll(repoRoot, base, input, new PrCommentSources(scan, review, provenance));\n this.archiveConsumedReview(repoRoot, featureName, result);\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 + this.skippedBuildLogNote(repoRoot));\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 FINISH_STAGE,\n ));\n }\n\n /**\n * When the gate is SKIPPED and build-log capture is on, name STAGE ②'s log — the one build that\n * actually ran for this commit. Deliberately never names a finish-stage log: no finish build ran, and\n * pointing at a file this command did not write would claim a build that did not happen.\n *\n * The path is recomputed rather than stored, and that is sound precisely BECAUSE this branch is only\n * reached when the receipt covers the current HEAD: same worktree, same branch, same sha ⇒ the same\n * deterministic filename stage ② wrote. Empty string whenever capture is off or the file is gone, so\n * a user without `~/.webpieces/config.json` sees the pre-existing single line unchanged.\n */\n private skippedBuildLogNote(repoRoot: string): string {\n if (!this.buildAffected.isCaptureEnabled()) return '';\n const log = this.buildLog.existingLogFor(repoRoot, REVIEW_STAGE);\n return log === '' ? '' : ` Stage ②'s full build output: ${log}\\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 * Retire the review this run just used — move review.json to old-review.json beside it, stamped as\n * audit-only (see ReviewJsonService.archiveReviewJson).\n *\n * WHY it moves rather than staying put: review.json left behind is a live-looking file describing a\n * review that has already shipped, and the next `wp-review-upsert-pr` on this branch finds it sitting\n * there. A reviewer subagent that judges the PR's stated INTENT — its title, summary or risk level —\n * then reads the previous round's review and can return GREEN against a title that no longer exists,\n * with nothing in the verdict distinguishing that from a real pass. Moving it makes the only route back\n * to this command a freshly-written review.\n *\n * ONLY on a PR that actually went up. A run that died before publishing must stay re-runnable without\n * making the AI rewrite a review that was never used, and prNumber === '' is exactly that case.\n * Non-fatal: the PR is live by here, so an unwritable archive warns rather than failing the command.\n */\n private archiveConsumedReview(repoRoot: string, featureName: string, result: UpsertResult): void {\n if (result.prNumber === '') return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the PR is already up; a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const archived = this.reviewJsonService.archiveReviewJson(reviewJsonPath(repoRoot, featureName));\n if (archived !== '') process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\\n`);\n // Beside it, so an archived review keeps the transcript links belonging to the round that\n // produced it — a review whose provenance was overwritten by the NEXT round audits nothing.\n this.provenanceEnforcer.archiveRecord(prDirFor(repoRoot, featureName));\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`⚠️ Could not archive review.json (non-fatal — the PR is already up): ${error.message}\\n`);\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 // buildCommand travels into the dashboard so the PR-body footer can NAME the command that vouched\n // for this commit. The footer used to assert \"build ran via nx affected\" on every repo, which was\n // simply false wherever buildCommand is not nx.\n return new DashboardInput(\n title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows,\n config.buildCommand);\n }\n\n /**\n * The applicable checklists MINUS the optional ones nobody ran.\n *\n * Not folded into `ChecklistScan` as yet another field: `applicable` (what matched) and `optionalNotRun`\n * (what was declined) are both facts about the scan, whereas this is one command's view of them, and the\n * roster comment deliberately wants the OTHER view — it lists the declined ones precisely so the PR does\n * not imply they passed.\n */\n private verdictedOf(scan: ChecklistScan): RequiredChecklist[] {\n const skipped = new Set(scan.optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return scan.applicable.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard.\n //\n // A checklist reaching this point is always PASS, WARN or OVERRIDDEN, and the reason is ReviewerVerdictGate\n // — NOT loadReviewJson. The gate runs one line earlier and throws on every other state (FAIL, MISSING,\n // BAD_FORMAT), so loadReviewJson's own checklist validation can no longer be the thing that rejects them;\n // it re-validates the same set and finds it clean. This comment used to credit loadReviewJson, which made\n // the ordering look deliberate while the gate's generic \"no verdict yet\" message masked every refusal.\n // WARN belongs in that list: yellow SHIPS, so it is not outstanding and reaches the dashboard.\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 entry.def.required);\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 row.required = entry.def.required;\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\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 ChecklistCommentRenderer).\n */\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 request = new PrCommentRequest();\n request.prNumber = prNumber;\n request.marker = CHECKLIST_COMMENT_MARKER;\n request.body = this.checklistComment.render(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n request.payloadDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n request.payloadName = 'checklist-comment.json';\n request.label = 'checklist review comment';\n this.commentUpserter.upsert(request);\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(request: PrUpsertRequest): UpsertResult {\n const repoRoot = request.repoRoot;\n const baseBranch = request.baseBranch;\n const title = request.title;\n const input = request.input;\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, request.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\n /*\n * THE INVARIANT: the merge body IS the PR description, byte for byte.\n *\n * That is what makes every landing route agree — the GitHub Merge button and a bare `gh pr merge`\n * copy the description via `squash_merge_commit_message: PR_BODY`, while `wp-land-pr` and finish's\n * own auto-merge pass these bytes with `--body-file`. If the two strings could differ, which route\n * landed the commit would change what history says, which is precisely the bug this whole change\n * exists to remove. `pr-body-is-merge-body.spec.ts` pins it.\n *\n * The one wrinkle is the self-link. A brand-new PR has no URL until `gh pr create` returns, so the\n * body published a moment ago was rendered with `prUrl: ''`. Re-render now that the URL is known\n * and push the corrected description back, so the two strings match and the commit carries its own\n * link. Only the CREATE path pays that extra edit: on every later run the URL was already known\n * before publishing, `finalBody` matches what went up, and nothing is re-sent.\n */\n const finalBody = this.dashboard.renderPrBody(input, ref.url) + request.tokenSuffix;\n if (finalBody !== request.body) this.backfillPrBody(ref.number, bodyFile, finalBody);\n\n // The DURABLE copy of these bytes is the PR description published above; this is a throwaway\n // file because `--body-file` takes a path. `wp-land-pr` reads the description back from GitHub,\n // so nothing here has to survive the process (see decisions/0005).\n const mergeBodyFile = this.mergeBodyFile.write(finalBody + '\\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 // `false`: this caller is POLICY-driven — only a config that really says AUTO merges here.\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, new MergeIntent(mergeMode, false));\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n /**\n * Everything the gated flow PUBLISHES, in the one order that is safe — the three surfaces plus the\n * merge, as a single unit.\n *\n * 1. the PR DESCRIPTION = the compact git-log body (+ the hidden gate token), written before the\n * push so CI's `synchronize` read can never see the previous run's token,\n * 2. the 1st comment = the full dashboard,\n * 3. the 2nd comment = each reviewer's output.\n *\n * The token is bound to the LOCAL HEAD sha and minted here because we only reach this line after the\n * build gate and every BLOCK checklist passed, so minting is legitimate. Nothing about\n * HMAC(salt, HEAD) needs the remote to have the commit yet.\n *\n * Extracted from `run` when the two comments made it 82 lines against a hard 80-line rule — and it\n * earns its own name: these four steps have a REQUIRED order, and the comment explaining that order\n * belongs with them rather than inside a method that also loads config and runs a build gate.\n */\n private publishAll(repoRoot: string, base: string, input: DashboardInput, sources: PrCommentSources): UpsertResult {\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const upsert = new PrUpsertRequest(input);\n upsert.repoRoot = repoRoot;\n upsert.baseBranch = base;\n upsert.title = input.title;\n upsert.tokenSuffix = this.gateTokenBody(gateSalt, headSha);\n // `existingPrUrl` is '' only for a brand-new PR — there is no URL to self-link to until\n // `gh pr create` returns one, and upsertPr back-fills it the moment it does.\n upsert.body = this.dashboard.renderPrBody(input, this.existingPrUrl(base)) + upsert.tokenSuffix;\n const result = this.upsertPr(upsert);\n\n // In the order the PR body's pointer promises: full dashboard 1st, reviewer output 2nd. Both are\n // idempotent by hidden marker and both non-fatal — the PR is up by now, so a `gh` hiccup on a\n // comment must not turn a finished run into a failed one.\n this.postDetailComment(repoRoot, result.prNumber, input);\n this.postChecklistComment(repoRoot, result.prNumber, sources.scan, sources.review, sources.provenance);\n\n // Having just written the description AS the commit body, make sure GitHub will actually use it.\n // Here rather than anywhere earlier because it is about what happens to the artifact we just\n // published, and it must run on EVERY repo — including mergeMode=NONE ones that never reach\n // PrMerger, which are exactly the repos where a UI merge decides what main's history says.\n this.squashSettings.ensure();\n return result;\n }\n\n /**\n * Re-publish the description now that the PR's own URL is known (create path only).\n *\n * Non-fatal on purpose. The PR exists, its body already carries a valid gate token for the pushed\n * head, and the code is up — the ONLY thing missing is the self-link on the first line. Aborting a\n * finished run over a cosmetic back-link would be a worse outcome than the missing link, and the very\n * next `wp-finish-upsert-pr` writes it (by then the URL is known before publishing, so it goes up with\n * the body). The merge body still gets the linked version regardless: it is filed from `finalBody`.\n */\n private backfillPrBody(prNumber: string, bodyFile: string, finalBody: string): void {\n if (prNumber === '') return;\n fs.writeFileSync(bodyFile, finalBody + '\\n');\n const res = spawnSync('gh', ['pr', 'edit', prNumber, '--body-file', bodyFile], { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write(\n '⚠️ Could not add the PR\\'s own link to its description (non-fatal — the PR and the gate\\n' +\n ' token are already up). The next wp-finish-upsert-pr writes it.\\n');\n return;\n }\n process.stdout.write(' back-filled the PR description with its own link ✓\\n');\n }\n\n /**\n * The PR's web URL if one is already open for this branch, else '' — read BEFORE publishing so the\n * description can carry its own link on the first render. Only a brand-new PR gets '' (and then\n * {@link backfillPrBody} fixes it up after `gh pr create` returns a URL).\n */\n private existingPrUrl(baseBranch: string): string {\n return this.prRef(baseBranch).url;\n }\n\n /**\n * The 1st PR comment: the FULL dashboard — every row including the green ones, the whole summary, the\n * 3-point hash points. This is what the PR description used to be, and moving it here is the entire\n * point of the change: the description is now the git-log body, and a squash commit must not carry a\n * risk table.\n *\n * Posted unconditionally (any repo, checklists or not), because the description's last bullet PROMISES\n * a 1st comment holding the detail. A pointer to a comment that does not exist is worse than no\n * pointer. Non-fatal — the PR is already up, and the description alone is a complete, readable record.\n */\n private postDetailComment(repoRoot: string, prNumber: string, input: DashboardInput): void {\n if (prNumber === '') return;\n const request = new PrCommentRequest();\n request.prNumber = prNumber;\n request.marker = DETAIL_COMMENT_MARKER;\n request.body = DETAIL_COMMENT_MARKER + '\\n' + this.dashboard.renderDetailComment(input);\n request.payloadDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n request.payloadName = 'detail-comment.json';\n request.label = 'full dashboard comment';\n this.commentUpserter.upsert(request);\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"]}
@@ -7,6 +7,7 @@ const rules_config_1 = require("@webpieces/rules-config");
7
7
  const inversify_1 = require("inversify");
8
8
  const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
9
9
  const build_affected_1 = require("../workflow/build-affected");
10
+ const build_gate_log_1 = require("../workflow/build-gate-log");
10
11
  const build_artifact_gate_1 = require("../workflow/build-artifact-gate");
11
12
  const checklist_scanner_1 = require("../workflow/checklist-scanner");
12
13
  const diff_materializer_1 = require("../workflow/diff-materializer");
@@ -135,7 +136,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
135
136
  * earlier did not turn one build into two.
136
137
  */
137
138
  runBuildGate(repoRoot) {
138
- this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.'));
139
+ this.buildAffected.runBuildGate(repoRoot, new build_affected_1.BuildGateOptions('🛠️ Build gate', 'pnpm wp-review-upsert-pr', 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.', build_gate_log_1.REVIEW_STAGE));
139
140
  // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in
140
141
  // finish, because this is the stage that ran buildCommand and is therefore holding the dirty
141
142
  // tree at the exact moment the question is answerable. Replaces the per-project
@@ -1 +1 @@
1
- {"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\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 true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n process.stdout.write(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
1
+ {"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,0DAGiC;AACjC,yCAA2D;AAC3D,2EAAgE;AAChE,+DAA6E;AAC7E,+DAA0D;AAC1D,yEAAoE;AACpE,qEAAsG;AACtG,qEAAiE;AACjE,mDAA+C;AAC/C,yDAAuD;AACvD,qDAAkE;AAClE,yDAAqD;AACrD,qEAAgE;AAChE,qFAAgF;AAChF,6DAA6F;AAC7F,2EAAiG;AAEjG,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,kGAAkG;AAClG,MAAa,qBAAqB;IAC9B;;;;;OAKG;IACH,YAAY,CAAU;IAEtB,YAAY,YAAY,GAAG,KAAK;QAC5B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAGT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IAjBrB,yDAAyD;IACzD,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,OAAgB,EAChB,aAA4B,EAC5B,iBAAoC,EACpC,UAAsB,EACtB,QAAkB,EAClB,gBAAkC,EAClC,YAA0B,EAC1B,YAA8B,EAC9B,eAAgC,EAChC,eAAwC,EACxC,oBAAiD,EACjD,QAAmC;IACpD,oGAAoG;IACnF,iBAAoC;QAfpC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,kBAAa,GAAb,aAAa,CAAe;QAC5B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAY;QACtB,aAAQ,GAAR,QAAQ,CAAU;QAClB,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,iBAAY,GAAZ,YAAY,CAAkB;QAC9B,oBAAe,GAAf,eAAe,CAAiB;QAChC,oBAAe,GAAf,eAAe,CAAyB;QACxC,yBAAoB,GAApB,oBAAoB,CAA6B;QACjD,aAAQ,GAAR,QAAQ,CAA2B;QAEnC,sBAAiB,GAAjB,iBAAiB,CAAmB;IACtD,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,OAA8B,IAAI,qBAAqB,EAAE;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAEhD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAElD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,wCAAoB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,mEAAmE;QAC/K,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,yCAAkB,CAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,aAAa,EACnG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAC7D,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,0BAA0B,CAAC,QAAgB;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7E,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,mCAAmC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACpF,wDAAwD;QACxD,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CACxB,QAAQ,EAAE,qBAAqB,EAAE,SAAS,EAC1C,IAAI,0BAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,EACjG,IAAI,2BAAe,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CACrD,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,QAAgB;QACjC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CAC1D,iBAAiB,EACjB,0BAA0B,EAC1B,wFAAwF,EACxF,6BAAY,CACf,CAAC,CAAC;QACH,+FAA+F;QAC/F,6FAA6F;QAC7F,gFAAgF;QAChF,4FAA4F;QAC5F,IAAI,CAAC,iBAAiB,CAAC,iCAAiC,CAAC,QAAQ,CAAC,CAAC;QACnE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,cAAc,CAAC,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,MAAoB;QACnG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAC1C,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACpF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpE,iGAAiG;QACjG,oGAAoG;QACpG,gGAAgG;QAChG,iGAAiG;QACjG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CACtC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAChF,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,yDAAyD;QAC3G,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YACxB,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,EACjF,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;OAOG;IACH,yDAAyD;IACjD,MAAM,CACV,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,SAAsC,EAClG,IAA2B;QAE3B,MAAM,KAAK,GAAG,IAAI,iCAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5E,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAC/C,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC/C,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,QAAQ,CAAC,IAAmB;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAsB,EAAmB,EAAE,CAAC,IAAI,+BAAe,CAC/E,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrH,CAAC;CACJ,CAAA;AA/JY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAIA,6BAAc;QAChB,mCAAY;QACjB,kBAAO;QACD,8BAAa;QACT,uCAAiB;QACxB,wBAAU;QACZ,oBAAQ;QACA,oCAAgB;QACpB,4BAAY;QACZ,oCAAgB;QACb,mCAAe;QACf,mDAAuB;QAClB,0CAA2B;QACvC,gDAAyB;QAEhB,gCAAiB;GAlBhD,qBAAqB,CA+JjC","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\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 true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n process.stdout.write(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
@@ -1,17 +1,32 @@
1
+ import { HomeConfigService } from '@webpieces/rules-config';
2
+ import { BuildGateLog } from './build-gate-log';
1
3
  export declare const DEFAULT_BUILD_COMMAND = "pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)";
2
4
  /**
3
- * The caller-supplied framing for the build gate (label, re-run command, failure headline). Kept as a
4
- * parameter object so runBuildGate stays agnostic of who invokes it; today the sole caller is
5
- * wp-finish-upsert-pr. A class (not an object literal) per the codebase's data-structure convention.
5
+ * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
6
+ * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)
7
+ * per the codebase's data-structure convention.
6
8
  */
7
9
  export declare class BuildGateOptions {
8
10
  label: string;
9
11
  rerunCommand: string;
10
12
  failureHeadline: string;
11
- constructor(label: string, rerunCommand: string, failureHeadline: string);
13
+ stage: string;
14
+ constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string);
12
15
  }
13
- /** Runs the authoritative nx-affected build gate for wp-finish-upsert-pr (the only PR-flow build gate). */
16
+ /** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */
14
17
  export declare class BuildAffected {
18
+ private readonly homeConfig;
19
+ private readonly buildLog;
20
+ constructor(homeConfig: HomeConfigService, buildLog: BuildGateLog);
21
+ /**
22
+ * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
23
+ *
24
+ * That file does not exist for essentially anyone, and its absence is not an error, a warning or a
25
+ * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is
26
+ * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it
27
+ * executed before this feature existed.
28
+ */
29
+ isCaptureEnabled(): boolean;
15
30
  /**
16
31
  * Resolve the exact build command this gate will run: the project's configured
17
32
  * PrGateConfig.buildCommand, or the default affected-ci command when none is set.
@@ -32,4 +47,10 @@ export declare class BuildAffected {
32
47
  * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).
33
48
  */
34
49
  runBuildGate(repoRoot: string, opts: BuildGateOptions): void;
50
+ /**
51
+ * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.
52
+ * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the
53
+ * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.
54
+ */
55
+ private failureText;
35
56
  }
@@ -5,6 +5,7 @@ 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
+ const build_gate_log_1 = require("./build-gate-log");
8
9
  // Single source of truth for the build gate. Only `wp-finish-upsert-pr` runs it (authoritatively,
9
10
  // before the one push) — `wp-start-upsert-pr` deliberately runs NO build gate, so `pr-gate.buildCommand`
10
11
  // is a finish-only knob. nx `affected` only rebuilds changed projects.
@@ -14,23 +15,44 @@ const inversify_1 = require("inversify");
14
15
  exports.DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';
15
16
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
16
17
  /**
17
- * The caller-supplied framing for the build gate (label, re-run command, failure headline). Kept as a
18
- * parameter object so runBuildGate stays agnostic of who invokes it; today the sole caller is
19
- * wp-finish-upsert-pr. A class (not an object literal) per the codebase's data-structure convention.
18
+ * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
19
+ * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)
20
+ * per the codebase's data-structure convention.
20
21
  */
21
22
  class BuildGateOptions {
22
23
  label; // section header shown above the gate
23
24
  rerunCommand; // command the AI re-runs after fixing the build
24
25
  failureHeadline; // first line printed on failure
25
- constructor(label, rerunCommand, failureHeadline) {
26
+ // WHICH stage's gate this is — REVIEW_STAGE or FINISH_STAGE. Required, with no default: it is part of
27
+ // the captured log's filename, and a default would silently make two stages share one log file.
28
+ stage;
29
+ constructor(label, rerunCommand, failureHeadline, stage) {
26
30
  this.label = label;
27
31
  this.rerunCommand = rerunCommand;
28
32
  this.failureHeadline = failureHeadline;
33
+ this.stage = stage;
29
34
  }
30
35
  }
31
36
  exports.BuildGateOptions = BuildGateOptions;
32
- /** Runs the authoritative nx-affected build gate for wp-finish-upsert-pr (the only PR-flow build gate). */
37
+ /** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */
33
38
  let BuildAffected = class BuildAffected {
39
+ homeConfig;
40
+ buildLog;
41
+ constructor(homeConfig, buildLog) {
42
+ this.homeConfig = homeConfig;
43
+ this.buildLog = buildLog;
44
+ }
45
+ /**
46
+ * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.
47
+ *
48
+ * That file does not exist for essentially anyone, and its absence is not an error, a warning or a
49
+ * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is
50
+ * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it
51
+ * executed before this feature existed.
52
+ */
53
+ isCaptureEnabled() {
54
+ return this.homeConfig.load().buildGateLogCapture;
55
+ }
34
56
  /**
35
57
  * Resolve the exact build command this gate will run: the project's configured
36
58
  * PrGateConfig.buildCommand, or the default affected-ci command when none is set.
@@ -65,17 +87,34 @@ let BuildAffected = class BuildAffected {
65
87
  // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is
66
88
  // only useful when the build FAILS, so it now lives solely on the failure path below.
67
89
  process.stdout.write(`\n${opts.label}: ${buildCommand}\n`);
68
- const buildCode = this.runConfiguredBuildGate(repoRoot);
69
- if (buildCode !== 0) {
70
- throw new rules_config_1.CliExitError(buildCode, `\n❌ ${opts.failureHeadline}\n\n` +
71
- `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\n\n` +
72
- ` ${buildCommand}\n`);
73
- }
90
+ // '' means NOT capturing, which is the case for every user who has not created the OPTIONAL
91
+ // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before this
92
+ // feature existed — same spawn, same message, no extra file, no extra line of output.
93
+ const logPath = this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';
94
+ const buildCode = logPath === ''
95
+ ? this.runConfiguredBuildGate(repoRoot)
96
+ : this.buildLog.run(repoRoot, buildCommand, logPath);
97
+ if (buildCode !== 0)
98
+ throw new rules_config_1.CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));
74
99
  process.stdout.write('\n✅ Build passed.\n');
75
100
  }
101
+ /**
102
+ * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.
103
+ * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the
104
+ * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.
105
+ */
106
+ failureText(opts, buildCommand, logPath) {
107
+ if (logPath !== '')
108
+ return this.buildLog.failureMessage(buildCommand, logPath);
109
+ return `\n❌ ${opts.failureHeadline}\n\n` +
110
+ `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\n\n` +
111
+ ` ${buildCommand}\n`;
112
+ }
76
113
  };
77
114
  exports.BuildAffected = BuildAffected;
78
115
  exports.BuildAffected = BuildAffected = tslib_1.__decorate([
79
- (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
116
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
117
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.HomeConfigService,
118
+ build_gate_log_1.BuildGateLog])
80
119
  ], BuildAffected);
81
120
  //# sourceMappingURL=build-affected.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAwE;AACxE,yCAA2D;AAE3D,kGAAkG;AAClG,yGAAyG;AACzG,uEAAuE;AACvE,gGAAgG;AAChG,wGAAwG;AACxG,oGAAoG;AACvF,QAAA,qBAAqB,GAAG,wEAAwE,CAAC;AAE9G,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;GAIG;AACH,MAAa,gBAAgB;IACzB,KAAK,CAAS,CAAY,sCAAsC;IAChE,YAAY,CAAS,CAAK,gDAAgD;IAC1E,eAAe,CAAS,CAAE,gCAAgC;IAE1D,YAAY,KAAa,EAAE,YAAoB,EAAE,eAAuB;QACpE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAC3C,CAAC;CACJ;AAVD,4CAUC;AAED,2GAA2G;AAEpG,IAAM,aAAa,GAAnB,MAAM,aAAa;IACtB;;;OAGG;IACH,mBAAmB,CAAC,QAAgB;QAChC,MAAM,UAAU,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QACjE,OAAO,UAAU,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,6BAAqB,CAAC;IACrG,CAAC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,QAAgB,EAAE,YAAqB;QACpD,MAAM,GAAG,GAAG,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,6BAAqB,CAAC;QAC5G,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChF,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,6FAA6F;IAC7F,sBAAsB,CAAC,QAAgB;QACnC,OAAO,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IAC1F,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,QAAgB,EAAE,IAAsB;QACjD,MAAM,YAAY,GAAG,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACxD,mGAAmG;QACnG,mGAAmG;QACnG,sFAAsF;QACtF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,CAAC;QAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QACxD,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,2BAAY,CAAC,SAAS,EAC5B,OAAO,IAAI,CAAC,eAAe,MAAM;gBACjC,uEAAuE,IAAI,CAAC,YAAY,OAAO;gBAC/F,OAAO,YAAY,IAAI,CAC1B,CAAC;QACN,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IAChD,CAAC;CACJ,CAAA;AAhDY,sCAAa;wBAAb,aAAa;IADzB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,aAAa,CAgDzB","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// Single source of truth for the build gate. Only `wp-finish-upsert-pr` runs it (authoritatively,\n// before the one push) — `wp-start-upsert-pr` deliberately runs NO build gate, so `pr-gate.buildCommand`\n// is a finish-only knob. nx `affected` only rebuilds changed projects.\n// `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:\n// origin/main rebuilds projects touched by OTHER people's merged PRs. The fork point scopes affected to\n// only YOUR branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.\nexport const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline). Kept as a\n * parameter object so runBuildGate stays agnostic of who invokes it; today the sole caller is\n * wp-finish-upsert-pr. A class (not an object literal) per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n\n constructor(label: string, rerunCommand: string, failureHeadline: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-finish-upsert-pr (the only PR-flow build gate). */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\n */\n runBuildGate(repoRoot: string, opts: BuildGateOptions): void {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n const buildCode = this.runConfiguredBuildGate(repoRoot);\n if (buildCode !== 0) {\n throw new CliExitError(buildCode,\n `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`,\n );\n }\n process.stdout.write('\\n✅ Build passed.\\n');\n }\n}\n"]}
1
+ {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError, HomeConfigService } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\n\n// Single source of truth for the build gate. Only `wp-finish-upsert-pr` runs it (authoritatively,\n// before the one push) — `wp-start-upsert-pr` deliberately runs NO build gate, so `pr-gate.buildCommand`\n// is a finish-only knob. nx `affected` only rebuilds changed projects.\n// `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:\n// origin/main rebuilds projects touched by OTHER people's merged PRs. The fork point scopes affected to\n// only YOUR branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.\nexport const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE or FINISH_STAGE. Required, with no default: it is part of\n // the captured log's filename, and a default would silently make two stages share one log file.\n stage: string;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\n }\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\n */\n runBuildGate(repoRoot: string, opts: BuildGateOptions): void {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing, which is the case for every user who has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before this\n // feature existed — same spawn, same message, no extra file, no extra line of output.\n const logPath = this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const buildCode = logPath === ''\n ? this.runConfiguredBuildGate(repoRoot)\n : this.buildLog.run(repoRoot, buildCommand, logPath);\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write('\\n✅ Build passed.\\n');\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never
3
+ * write the same file even at the same commit on the same branch.
4
+ */
5
+ export declare const REVIEW_STAGE = "review";
6
+ export declare const FINISH_STAGE = "finish";
7
+ /**
8
+ * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is
9
+ * handed instead of a rebuild instruction.
10
+ *
11
+ * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────
12
+ * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each
13
+ * component rules out one class of concurrent writer:
14
+ * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree
15
+ * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which
16
+ * is what makes "N agents in N worktrees" safe by construction rather than by naming.
17
+ * • `<stage>` separates review from finish, the two gates that CAN run against one commit.
18
+ * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one
19
+ * log directory the branch is effectively constant; it is in the name so a human reading the directory
20
+ * can tell whose log is whose, and so switching branches never appends to a stale file.
21
+ * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is
22
+ * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is
23
+ * the same build of the same tree, and the fresh one is the one worth reading.
24
+ * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,
25
+ * simultaneously. That is already unsupported — both would be driving one git index and one merge state —
26
+ * and it is the only case this scheme does not separate.
27
+ */
28
+ export declare class BuildGateLog {
29
+ /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */
30
+ pathFor(repoRoot: string, stage: string): string;
31
+ /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */
32
+ existingLogFor(repoRoot: string, stage: string): string;
33
+ /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */
34
+ fileNameFor(repoRoot: string, stage: string): string;
35
+ /**
36
+ * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to
37
+ * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.
38
+ */
39
+ run(repoRoot: string, buildCommand: string, logPath: string): number;
40
+ /**
41
+ * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the
42
+ * agent reads ONE file rather than carrying a build transcript in its context.
43
+ *
44
+ * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong
45
+ * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent
46
+ * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
47
+ */
48
+ failureMessage(buildCommand: string, logPath: string): string;
49
+ /**
50
+ * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command
51
+ * ending in a `#` comment cannot swallow what follows it.
52
+ *
53
+ * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,
54
+ * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that
55
+ * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is
56
+ * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.
57
+ */
58
+ private wrap;
59
+ private readStatus;
60
+ private quote;
61
+ private slug;
62
+ private git;
63
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BuildGateLog = exports.FINISH_STAGE = exports.REVIEW_STAGE = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const rules_config_1 = require("@webpieces/rules-config");
9
+ const inversify_1 = require("inversify");
10
+ // EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this
11
+ // file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`
12
+ // (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.
13
+ //
14
+ // ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────
15
+ // The build gate already builds everything. When it fails, the pre-existing message says "run THIS exact
16
+ // command to reproduce", and an AI agent obeys it — so the repo is built a SECOND time purely to see the
17
+ // errors that scrolled past the first time. This captures the first build's output instead.
18
+ //
19
+ // ─── Why a shell `tee` and not spawnSync's pipes ───────────────────────────────────────────────────────
20
+ // spawnSync BUFFERS a piped stream and hands it over only after the child exits, so the human would watch
21
+ // a silent terminal for the length of a full build. The output has to be split at the OS level, which is
22
+ // what `tee` is. The gate keeps `stdio: 'inherit'`, so the terminal is byte-identical to today.
23
+ //
24
+ // ─── Why an exit-status side file and not `pipefail` ───────────────────────────────────────────────────
25
+ // In a pipeline the shell reports the LAST command's status, i.e. tee's, which is 0 whether the build
26
+ // passed or failed. `set -o pipefail` and `${PIPESTATUS[0]}` are bash/zsh, and spawnSync's `shell: true`
27
+ // gives `/bin/sh` — dash on Debian. Writing `$?` to a side file inside the group is plain POSIX and works
28
+ // under every one of them. The redirect keeps it out of the pipe, so it never lands in the log.
29
+ const STATUS_SUFFIX = '.status';
30
+ /**
31
+ * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never
32
+ * write the same file even at the same commit on the same branch.
33
+ */
34
+ exports.REVIEW_STAGE = 'review';
35
+ exports.FINISH_STAGE = 'finish';
36
+ /**
37
+ * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is
38
+ * handed instead of a rebuild instruction.
39
+ *
40
+ * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────
41
+ * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each
42
+ * component rules out one class of concurrent writer:
43
+ * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree
44
+ * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which
45
+ * is what makes "N agents in N worktrees" safe by construction rather than by naming.
46
+ * • `<stage>` separates review from finish, the two gates that CAN run against one commit.
47
+ * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one
48
+ * log directory the branch is effectively constant; it is in the name so a human reading the directory
49
+ * can tell whose log is whose, and so switching branches never appends to a stale file.
50
+ * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is
51
+ * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is
52
+ * the same build of the same tree, and the fresh one is the one worth reading.
53
+ * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,
54
+ * simultaneously. That is already unsupported — both would be driving one git index and one merge state —
55
+ * and it is the only case this scheme does not separate.
56
+ */
57
+ let BuildGateLog = class BuildGateLog {
58
+ /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */
59
+ pathFor(repoRoot, stage) {
60
+ const file = rules_config_1.dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));
61
+ fs.mkdirSync(path.dirname(file), { recursive: true });
62
+ return file;
63
+ }
64
+ /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */
65
+ existingLogFor(repoRoot, stage) {
66
+ const file = rules_config_1.dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));
67
+ return fs.existsSync(file) ? file : '';
68
+ }
69
+ /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */
70
+ fileNameFor(repoRoot, stage) {
71
+ const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));
72
+ const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));
73
+ return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;
74
+ }
75
+ /**
76
+ * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to
77
+ * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.
78
+ */
79
+ run(repoRoot, buildCommand, logPath) {
80
+ const statusFile = `${logPath}${STATUS_SUFFIX}`;
81
+ fs.rmSync(statusFile, { force: true });
82
+ const result = (0, child_process_1.spawnSync)(this.wrap(buildCommand, logPath, statusFile), { stdio: 'inherit', cwd: repoRoot, shell: true });
83
+ const fromFile = this.readStatus(statusFile);
84
+ fs.rmSync(statusFile, { force: true });
85
+ if (fromFile !== null)
86
+ return fromFile;
87
+ // The side file never appeared ⇒ the shell died before the echo. The PIPELINE's status is tee's,
88
+ // which is 0, and reporting 0 there would call a dead build green — so fail CLOSED instead.
89
+ return result.status !== null && result.status !== 0 ? result.status : 1;
90
+ }
91
+ /**
92
+ * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the
93
+ * agent reads ONE file rather than carrying a build transcript in its context.
94
+ *
95
+ * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong
96
+ * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent
97
+ * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.
98
+ */
99
+ failureMessage(buildCommand, logPath) {
100
+ return `\n❌ The CI build failed. We ran\n\n` +
101
+ ` ${buildCommand} > ${logPath}\n\n` +
102
+ `and it failed, so read that file for the failures. Do NOT re-run the build to see them.\n` +
103
+ `If you do not see failures in that log, report that to the user and stop.\n`;
104
+ }
105
+ /**
106
+ * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command
107
+ * ending in a `#` comment cannot swallow what follows it.
108
+ *
109
+ * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,
110
+ * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that
111
+ * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is
112
+ * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.
113
+ */
114
+ wrap(buildCommand, logPath, statusFile) {
115
+ return `{\n(\n${buildCommand}\n)\necho $? > ${this.quote(statusFile)}\n} 2>&1 | tee ${this.quote(logPath)}`;
116
+ }
117
+ // The build's own exit code, or null when the side file never appeared (the shell died before the echo).
118
+ readStatus(statusFile) {
119
+ if (!fs.existsSync(statusFile))
120
+ return null;
121
+ const parsed = Number.parseInt(fs.readFileSync(statusFile, 'utf8').trim(), 10);
122
+ return Number.isNaN(parsed) ? null : parsed;
123
+ }
124
+ // POSIX single-quoting: everything is literal inside '…', and a literal ' is spelled '\''.
125
+ quote(value) {
126
+ return `'${value.split("'").join(`'\\''`)}'`;
127
+ }
128
+ // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.
129
+ slug(value) {
130
+ return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
131
+ }
132
+ // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a
133
+ // filename may never be the reason a build gate does not run.
134
+ git(repoRoot, args) {
135
+ const result = (0, child_process_1.spawnSync)('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });
136
+ if (result.status !== 0)
137
+ return '';
138
+ return (result.stdout ?? '').trim();
139
+ }
140
+ };
141
+ exports.BuildGateLog = BuildGateLog;
142
+ exports.BuildGateLog = BuildGateLog = tslib_1.__decorate([
143
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
144
+ ], BuildGateLog);
145
+ //# sourceMappingURL=build-gate-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-gate-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-gate-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this\n// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`\n// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.\n//\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// The build gate already builds everything. When it fails, the pre-existing message says \"run THIS exact\n// command to reproduce\", and an AI agent obeys it — so the repo is built a SECOND time purely to see the\n// errors that scrolled past the first time. This captures the first build's output instead.\n//\n// ─── Why a shell `tee` and not spawnSync's pipes ───────────────────────────────────────────────────────\n// spawnSync BUFFERS a piped stream and hands it over only after the child exits, so the human would watch\n// a silent terminal for the length of a full build. The output has to be split at the OS level, which is\n// what `tee` is. The gate keeps `stdio: 'inherit'`, so the terminal is byte-identical to today.\n//\n// ─── Why an exit-status side file and not `pipefail` ───────────────────────────────────────────────────\n// In a pipeline the shell reports the LAST command's status, i.e. tee's, which is 0 whether the build\n// passed or failed. `set -o pipefail` and `${PIPESTATUS[0]}` are bash/zsh, and spawnSync's `shell: true`\n// gives `/bin/sh` — dash on Debian. Writing `$?` to a side file inside the group is plain POSIX and works\n// under every one of them. The redirect keeps it out of the pipe, so it never lands in the log.\nconst STATUS_SUFFIX = '.status';\n\n/**\n * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never\n * write the same file even at the same commit on the same branch.\n */\nexport const REVIEW_STAGE = 'review';\nexport const FINISH_STAGE = 'finish';\n\n/**\n * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is\n * handed instead of a rebuild instruction.\n *\n * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────\n * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each\n * component rules out one class of concurrent writer:\n * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree\n * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which\n * is what makes \"N agents in N worktrees\" safe by construction rather than by naming.\n * • `<stage>` separates review from finish, the two gates that CAN run against one commit.\n * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one\n * log directory the branch is effectively constant; it is in the name so a human reading the directory\n * can tell whose log is whose, and so switching branches never appends to a stale file.\n * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is\n * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is\n * the same build of the same tree, and the fresh one is the one worth reading.\n * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,\n * simultaneously. That is already unsupported — both would be driving one git index and one merge state —\n * and it is the only case this scheme does not separate.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildGateLog {\n /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */\n pathFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n\n /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */\n existingLogFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n return fs.existsSync(file) ? file : '';\n }\n\n /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */\n fileNameFor(repoRoot: string, stage: string): string {\n const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));\n const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));\n return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;\n }\n\n /**\n * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to\n * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.\n */\n run(repoRoot: string, buildCommand: string, logPath: string): number {\n const statusFile = `${logPath}${STATUS_SUFFIX}`;\n fs.rmSync(statusFile, { force: true });\n const result = spawnSync(this.wrap(buildCommand, logPath, statusFile),\n { stdio: 'inherit', cwd: repoRoot, shell: true });\n const fromFile = this.readStatus(statusFile);\n fs.rmSync(statusFile, { force: true });\n if (fromFile !== null) return fromFile;\n // The side file never appeared ⇒ the shell died before the echo. The PIPELINE's status is tee's,\n // which is 0, and reporting 0 there would call a dead build green — so fail CLOSED instead.\n return result.status !== null && result.status !== 0 ? result.status : 1;\n }\n\n /**\n * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the\n * agent reads ONE file rather than carrying a build transcript in its context.\n *\n * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong\n * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent\n * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.\n */\n failureMessage(buildCommand: string, logPath: string): string {\n return `\\n❌ The CI build failed. We ran\\n\\n` +\n ` ${buildCommand} > ${logPath}\\n\\n` +\n `and it failed, so read that file for the failures. Do NOT re-run the build to see them.\\n` +\n `If you do not see failures in that log, report that to the user and stop.\\n`;\n }\n\n /**\n * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command\n * ending in a `#` comment cannot swallow what follows it.\n *\n * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,\n * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that\n * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is\n * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.\n */\n private wrap(buildCommand: string, logPath: string, statusFile: string): string {\n return `{\\n(\\n${buildCommand}\\n)\\necho $? > ${this.quote(statusFile)}\\n} 2>&1 | tee ${this.quote(logPath)}`;\n }\n\n // The build's own exit code, or null when the side file never appeared (the shell died before the echo).\n private readStatus(statusFile: string): number | null {\n if (!fs.existsSync(statusFile)) return null;\n const parsed = Number.parseInt(fs.readFileSync(statusFile, 'utf8').trim(), 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n\n // POSIX single-quoting: everything is literal inside '…', and a literal ' is spelled '\\''.\n private quote(value: string): string {\n return `'${value.split(\"'\").join(`'\\\\''`)}'`;\n }\n\n // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.\n private slug(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');\n }\n\n // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a\n // filename may never be the reason a build gate does not run.\n private git(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n return (result.stdout ?? '').trim();\n }\n}\n"]}