@webpieces/pr-gate 0.4.767 → 0.4.769

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.767",
3
+ "version": "0.4.769",
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",
@@ -16,8 +16,8 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@inversifyjs/binding-decorators": "1.1.5",
19
- "@webpieces/ai-hook-rules": "0.4.767",
20
- "@webpieces/rules-config": "0.4.767",
19
+ "@webpieces/ai-hook-rules": "0.4.769",
20
+ "@webpieces/rules-config": "0.4.769",
21
21
  "inversify": "7.10.4",
22
22
  "reflect-metadata": "0.2.2"
23
23
  },
@@ -201,7 +201,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
201
201
  // nobody should wait on a build to be told a reviewer never ran. ReviewerVerdictGate owns the
202
202
  // distinction between unreadable / REFUSED / never-ran, and retires the red verdicts it acts on.
203
203
  this.verdictGate.assertEveryReviewerRan(scan);
204
- const review = this.reviewJsonService.loadReviewJson((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), required);
204
+ const review = this.reviewJsonService.loadReviewJson((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), required, scan.singleRoundReview ? rules_config_1.SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS : '');
205
205
  // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own
206
206
  // artifacts) that such a subagent actually ran on this branch — the coding agent may not
207
207
  // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).
@@ -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 { BuildGateLog, FINISH_STAGE, REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { StageOutputLog, FINISH_CONSOLE_LOG } from '../workflow/stage-output-log';\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';\nimport { AuthorIdentityResolver } from '../../dashboard/author-identity';\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 private readonly authorIdentity: AuthorIdentityResolver,\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 private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Runs with this stage's console CAPTURED to a file (see StageOutputLog). What still reaches the\n * terminal is what an agent must act on: the build heartbeat and result, and the closing banner with\n * the PR link and whatever is still owed. The dashboard computation, the gh round trips and the\n * comment upserts are in the file.\n */\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, FINISH_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot));\n }\n\n private async runStage(repoRoot: string): Promise<void> {\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 await 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, scan);\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 // `say`: the PR link and what is still owed are the whole reason a caller ran this command, and a\n // not-done outcome carries the commands that fix it. Neither may end up only in a log file.\n this.stageConsole.say(this.banner.render(bannerInput));\n this.stageConsole.say(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 async runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): Promise<void> {\n if (alreadyGreen) {\n // `say`, for the same reason the gate's own two lines are: a caller watching for a build needs\n // to be told, live, that there is not going to be one.\n this.stageConsole.say('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n'\n + this.skippedBuildLogNote(repoRoot));\n return;\n }\n await 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 when the file is gone — a log that was reaped or\n * never written must not be named as though a reader could open it.\n */\n private skippedBuildLogNote(repoRoot: string): string {\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[], scan: ChecklistScan): 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 // `scan.suppressed.length`, never `scan.reviewersDisabled` alone: the dashboard and the commit\n // body have to state HOW MANY reviewers were killed, because a suppressed 4 and an applicable 0\n // are different facts that both render as an empty `rows`. See DashboardInput.\n return new DashboardInput(\n title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows,\n config.buildCommand, scan.suppressed.length, this.authorIdentity.resolve(review.model));\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 identity = review.results.find((result): boolean => result.id === entry.def.id);\n const row = new ChecklistCommentRow(identity?.agent ?? 'unknown', identity?.model ?? 'unknown',\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 scan.suppressed.length);\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 SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS,\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 { StageOutputLog, FINISH_CONSOLE_LOG } from '../workflow/stage-output-log';\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';\nimport { AuthorIdentityResolver } from '../../dashboard/author-identity';\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 private readonly authorIdentity: AuthorIdentityResolver,\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 private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Runs with this stage's console CAPTURED to a file (see StageOutputLog). What still reaches the\n * terminal is what an agent must act on: the build heartbeat and result, and the closing banner with\n * the PR link and whatever is still owed. The dashboard computation, the gh round trips and the\n * comment upserts are in the file.\n */\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, FINISH_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot));\n }\n\n private async runStage(repoRoot: string): Promise<void> {\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(\n reviewJsonPath(repoRoot, featureName), required,\n scan.singleRoundReview ? SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS : '');\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 await 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, scan);\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 // `say`: the PR link and what is still owed are the whole reason a caller ran this command, and a\n // not-done outcome carries the commands that fix it. Neither may end up only in a log file.\n this.stageConsole.say(this.banner.render(bannerInput));\n this.stageConsole.say(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 async runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): Promise<void> {\n if (alreadyGreen) {\n // `say`, for the same reason the gate's own two lines are: a caller watching for a build needs\n // to be told, live, that there is not going to be one.\n this.stageConsole.say('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n'\n + this.skippedBuildLogNote(repoRoot));\n return;\n }\n await 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 when the file is gone — a log that was reaped or\n * never written must not be named as though a reader could open it.\n */\n private skippedBuildLogNote(repoRoot: string): string {\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[], scan: ChecklistScan): 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 // `scan.suppressed.length`, never `scan.reviewersDisabled` alone: the dashboard and the commit\n // body have to state HOW MANY reviewers were killed, because a suppressed 4 and an applicable 0\n // are different facts that both render as an empty `rows`. See DashboardInput.\n return new DashboardInput(\n title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows,\n config.buildCommand, scan.suppressed.length, this.authorIdentity.resolve(review.model));\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 identity = review.results.find((result): boolean => result.id === entry.def.id);\n const row = new ChecklistCommentRow(identity?.agent ?? 'unknown', identity?.model ?? 'unknown',\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 scan.suppressed.length);\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"]}
@@ -123,10 +123,19 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
123
123
  this.gitExec.assertCleanTree(repoRoot);
124
124
  const buildPassedAt = await this.runBuildGate(repoRoot);
125
125
  const scan = this.checklistScanner.scan(repoRoot, config.checklists, new checklist_scanner_1.ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing
126
- const briefings = this.briefReviewers(repoRoot, featureName, scan, config);
127
- this.receipts.write(repoRoot, featureName, new review_stage_receipt_1.ReviewStageReceipt(scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt, briefings.map((b) => b.subagent)));
126
+ const previousReceipt = this.receipts.read(repoRoot, featureName);
127
+ const singleRoundReviewers = previousReceipt?.reviewersBriefed ?? [];
128
+ const singleRoundRepeat = scan.singleRoundReview && singleRoundReviewers.length > 0;
129
+ // A second stage-② invocation in single-round mode must not create a fresh launch surface. The
130
+ // original generated instructions stay on disk; this run only refreshes the build receipt and gives
131
+ // the caller one explicit next action: finish.
132
+ const briefings = singleRoundRepeat ? [] : this.briefReviewers(repoRoot, featureName, scan, config);
133
+ const recordedReviewers = singleRoundRepeat
134
+ ? singleRoundReviewers
135
+ : briefings.map((b) => b.subagent);
136
+ this.receipts.write(repoRoot, featureName, new review_stage_receipt_1.ReviewStageReceipt(scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt, recordedReviewers));
128
137
  this.reportActiveHatches(repoRoot);
129
- this.report(repoRoot, featureName, scan, briefings, opts);
138
+ this.report(repoRoot, featureName, scan, briefings, opts, singleRoundRepeat, recordedReviewers);
130
139
  }
131
140
  /**
132
141
  * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed
@@ -211,7 +220,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
211
220
  * obeyed the first line and posted a PR with no review at all.
212
221
  */
213
222
  // eslint-disable-next-line @typescript-eslint/max-params
214
- report(repoRoot, featureName, scan, briefings, opts) {
223
+ report(repoRoot, featureName, scan, briefings, opts, singleRoundRepeat, singleRoundReviewers) {
215
224
  const input = new review_report_1.ReviewReportInput(repoRoot, featureName, scan.reviewPath);
216
225
  input.definedCount = scan.defined.length;
217
226
  input.applicableCount = scan.applicable.length;
@@ -226,6 +235,9 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
226
235
  // was reviewed. See ChecklistScanner.
227
236
  input.reviewersSuppressed = scan.reviewersDisabled;
228
237
  input.suppressed = scan.suppressed.slice();
238
+ input.singleRoundReview = scan.singleRoundReview;
239
+ input.singleRoundRepeat = singleRoundRepeat;
240
+ input.singleRoundReviewers = singleRoundReviewers.slice();
229
241
  // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and
230
242
  // the command after that. Capturing it into the log would leave the terminal with a pointer and
231
243
  // no instruction, which is the one thing this stage may never do.
@@ -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 { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\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';\nimport { StageOutputLog, REVIEW_CONSOLE_LOG } from '../workflow/stage-output-log';\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 private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the\n * lines an agent must act on before it can do anything else reach the terminal — the build heartbeat\n * and result, and the closing instruction block. The merge validation, the hatch report, the checklist\n * scan and the diff extraction are all in the file, one `grep` away.\n *\n * This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it\n * expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has\n * printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the\n * output would only have traded that kill for a flooded context.\n */\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, REVIEW_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot, opts));\n }\n\n private async runStage(repoRoot: string, opts: ReviewUpsertPrOptions): Promise<void> {\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 = await 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.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\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 async runBuildGate(repoRoot: string): Promise<string> {\n await 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 // Straight off the SCAN — the one place `experimental.turnOffAllReviewers` is read. This command\n // deliberately does NOT consult the home config itself: a second read is a second answer, and the\n // command that lists and the command that blocks would be free to disagree about whether a branch\n // was reviewed. See ChecklistScanner.\n input.reviewersSuppressed = scan.reviewersDisabled;\n input.suppressed = scan.suppressed.slice();\n // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and\n // the command after that. Capturing it into the log would leave the terminal with a pointer and\n // no instruction, which is the one thing this stage may never do.\n this.stageConsole.say(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 — \"fix it, then\n * re-run\" — is only correct while that verdict file is still there, which here it is.\n *\n * The review path IS passed, because the ship-anyway route the refusal prints is a command that writes\n * `override-<id>.json` beside it. That command is as correct at this stage as at finish: the override\n * is a separate file with its own writer, so nothing about it depends on the verdict having been moved.\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,\n this.reviewJsonService.refusalError(\n req, this.reviewJsonService.resolveVerdict(req, scan.results), scan.reviewPath)));\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":"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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 { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\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';\nimport { StageOutputLog, REVIEW_CONSOLE_LOG } from '../workflow/stage-output-log';\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 private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n private readonly stageConsole: StageOutputLog,\n ) {}\n\n /**\n * Everything below runs with this stage's console CAPTURED to a file (see StageOutputLog). Only the\n * lines an agent must act on before it can do anything else reach the terminal — the build heartbeat\n * and result, and the closing instruction block. The merge validation, the hatch report, the checklist\n * scan and the diff extraction are all in the file, one `grep` away.\n *\n * This is the FIRST half of the fix for the piped-command watchdog kill: an agent pipes a command it\n * expects to flood, a pipe withholds every byte until exit, and the harness kills a command that has\n * printed nothing for 600 seconds — after a full build. Guarding the pipe without first shrinking the\n * output would only have traded that kill for a flooded context.\n */\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n await this.stageConsole.withCapture(\n repoRoot, REVIEW_CONSOLE_LOG, (): Promise<void> => this.runStage(repoRoot, opts));\n }\n\n private async runStage(repoRoot: string, opts: ReviewUpsertPrOptions): Promise<void> {\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 = await 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 previousReceipt = this.receipts.read(repoRoot, featureName);\n const singleRoundReviewers = previousReceipt?.reviewersBriefed ?? [];\n const singleRoundRepeat = scan.singleRoundReview && singleRoundReviewers.length > 0;\n // A second stage-② invocation in single-round mode must not create a fresh launch surface. The\n // original generated instructions stay on disk; this run only refreshes the build receipt and gives\n // the caller one explicit next action: finish.\n const briefings = singleRoundRepeat ? [] : this.briefReviewers(repoRoot, featureName, scan, config);\n const recordedReviewers = singleRoundRepeat\n ? singleRoundReviewers\n : briefings.map((b: ReviewerBriefing): string => b.subagent);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n recordedReviewers,\n ));\n this.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts, singleRoundRepeat, recordedReviewers);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\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 async runBuildGate(repoRoot: string): Promise<string> {\n await 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, singleRoundRepeat: boolean, singleRoundReviewers: readonly string[],\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 // Straight off the SCAN — the one place `experimental.turnOffAllReviewers` is read. This command\n // deliberately does NOT consult the home config itself: a second read is a second answer, and the\n // command that lists and the command that blocks would be free to disagree about whether a branch\n // was reviewed. See ChecklistScanner.\n input.reviewersSuppressed = scan.reviewersDisabled;\n input.suppressed = scan.suppressed.slice();\n input.singleRoundReview = scan.singleRoundReview;\n input.singleRoundRepeat = singleRoundRepeat;\n input.singleRoundReviewers = singleRoundReviewers.slice();\n // `say`: this block IS the next action — which reviewers to spawn, where review.json goes, and\n // the command after that. Capturing it into the log would leave the terminal with a pointer and\n // no instruction, which is the one thing this stage may never do.\n this.stageConsole.say(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 — \"fix it, then\n * re-run\" — is only correct while that verdict file is still there, which here it is.\n *\n * The review path IS passed, because the ship-anyway route the refusal prints is a command that writes\n * `override-<id>.json` beside it. That command is as correct at this stage as at finish: the override\n * is a separate file with its own writer, so nothing about it depends on the verdict having been moved.\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,\n this.reviewJsonService.refusalError(\n req, this.reviewJsonService.resolveVerdict(req, scan.results), scan.reviewPath)));\n }\n}\n"]}
@@ -99,7 +99,9 @@ export declare class ChecklistScan {
99
99
  * downstream would mean a second changed-file computation that can disagree with this one.
100
100
  */
101
101
  suppressed: RequiredChecklist[];
102
- constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], reviewersDisabled: boolean, suppressed: RequiredChecklist[], basis?: DiffBasis, changedFiles?: string[], results?: ChecklistResult[], optionalNotRun?: RequiredChecklist[]);
102
+ /** TRUE only for the top-level `singleRoundReview` machine opt-in. */
103
+ singleRoundReview: boolean;
104
+ constructor(defined: ChecklistDefinition[], applicable: RequiredChecklist[], reviewed: RequiredChecklist[], outstanding: RequiredChecklist[], context: ChecklistReviewContext, reviewPath: string, forkPoint: string, roster: ChecklistRoster, formatErrors: string[], reviewersDisabled: boolean, suppressed: RequiredChecklist[], basis?: DiffBasis, changedFiles?: string[], results?: ChecklistResult[], optionalNotRun?: RequiredChecklist[], singleRoundReview?: boolean);
103
105
  }
104
106
  /**
105
107
  * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and
@@ -114,6 +114,8 @@ class ChecklistScan {
114
114
  * downstream would mean a second changed-file computation that can disagree with this one.
115
115
  */
116
116
  suppressed;
117
+ /** TRUE only for the top-level `singleRoundReview` machine opt-in. */
118
+ singleRoundReview;
117
119
  // eslint-disable-next-line @typescript-eslint/max-params
118
120
  constructor(defined, applicable, reviewed, outstanding, context, reviewPath, forkPoint, roster, formatErrors,
119
121
  // REQUIRED, and placed BEFORE the defaulted tail deliberately. A defaulted `reviewersDisabled =
@@ -123,7 +125,7 @@ class ChecklistScan {
123
125
  reviewersDisabled, suppressed, basis = new diff_basis_1.DiffBasis(), changedFiles = [],
124
126
  // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)
125
127
  // stays a one-liner; the scanner itself always passes the real set.
126
- results = [], optionalNotRun = []) {
128
+ results = [], optionalNotRun = [], singleRoundReview = false) {
127
129
  this.defined = defined;
128
130
  this.applicable = applicable;
129
131
  this.reviewed = reviewed;
@@ -139,6 +141,7 @@ class ChecklistScan {
139
141
  this.changedFiles = changedFiles;
140
142
  this.results = results;
141
143
  this.optionalNotRun = optionalNotRun;
144
+ this.singleRoundReview = singleRoundReview;
142
145
  }
143
146
  }
144
147
  exports.ChecklistScan = ChecklistScan;
@@ -209,11 +212,12 @@ let ChecklistScanner = class ChecklistScanner {
209
212
  const context = opts.contextStage === ''
210
213
  ? this.prContextWriter.contextFor(repoRoot, featureName, basis)
211
214
  : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles);
212
- if (this.homeConfig.load().turnOffAllReviewers) {
215
+ const homeConfig = this.homeConfig.load();
216
+ if (homeConfig.turnOffAllReviewers) {
213
217
  // EMPTY: applicable, reviewed, outstanding, formatErrors, results, optionalNotRun. INTACT:
214
218
  // defined, roster, basis, changedFiles, context — so every downstream reader can still say
215
219
  // WHAT was suppressed, which is the difference between an honest record and a silent one.
216
- return new ChecklistScan(defined, [], [], [], context, reviewPath, base, roster, [], true, matched, basis, changedFiles, [], []);
220
+ return new ChecklistScan(defined, [], [], [], context, reviewPath, base, roster, [], true, matched, basis, changedFiles, [], [], homeConfig.singleRoundReview);
217
221
  }
218
222
  const applicable = matched;
219
223
  const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);
@@ -224,7 +228,7 @@ let ChecklistScanner = class ChecklistScanner {
224
228
  // that never happened.
225
229
  const reviewed = applicable.filter((r) => !owedIds.has(r.id));
226
230
  const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);
227
- return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable, context, reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), false, [], basis, changedFiles, results, optionalNotRun);
231
+ return new ChecklistScan(defined, applicable, reviewed, opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable, context, reviewPath, base, roster, this.reviewJsonService.checklistFormatErrors(applicable, results), false, [], basis, changedFiles, results, optionalNotRun, homeConfig.singleRoundReview);
228
232
  }
229
233
  /**
230
234
  * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional
@@ -1 +1 @@
1
- {"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n HomeConfigService, RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n /**\n * The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.\n *\n * Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`\n * mean something. Carried out as its own set rather than merely subtracted, because both readers need it\n * BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and\n * the PR dashboard must publish them as \"not run\" rather than let a shorter roster imply everything\n * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.\n */\n optionalNotRun: RequiredChecklist[];\n /**\n * TRUE when this machine's `~/.webpieces/config.json` carries\n * `experimental.turnOffAllReviewers: true`, and therefore when `applicable`, `reviewed`,\n * `outstanding`, `results`, `optionalNotRun` and `formatErrors` above are ALL EMPTY BY DECREE rather\n * than because nothing matched. Those two states are indistinguishable from the empty lists alone,\n * and telling them apart is the entire reason this field exists: an unreviewed PR must never render\n * as a reviewed one.\n *\n * `defined`, `roster`, `basis`, `changedFiles` and `context` are deliberately left INTACT under\n * suppression, so every reader can still say WHAT was switched off — including which REQUIRED\n * checklists would otherwise have run.\n *\n * See `HOME_KEY_TURN_OFF_ALL_REVIEWERS` in rules-config for the flag's contract; it is an\n * `experimental.*` flag, so only a human ever ends it.\n */\n reviewersDisabled: boolean;\n /**\n * The checklists that WOULD have been `applicable` had the reviewers not been suppressed — required\n * ones included. EMPTY on every ordinary run, and empty is not ambiguous there because\n * `reviewersDisabled` is false.\n *\n * Carried by NAME and not as a count because every reader needs a different slice of it: stage ②\n * prints them so a human can see exactly which required reviewer was killed, the dashboard names them\n * in the 1st comment, and the compact PR body needs only `.length`. Recomputing any of that\n * downstream would mean a second changed-file computation that can disagree with this one.\n */\n suppressed: RequiredChecklist[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n // REQUIRED, and placed BEFORE the defaulted tail deliberately. A defaulted `reviewersDisabled =\n // false` would let every existing construction keep compiling while asserting, silently, that no\n // reviewer was switched off — the widening-by-omission `.claude/rules/no-backwards-compat.md`\n // rejects, and on the one field whose whole job is to stop an unreviewed PR reading as reviewed.\n reviewersDisabled: boolean,\n suppressed: RequiredChecklist[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n optionalNotRun: RequiredChecklist[] = [],\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.reviewersDisabled = reviewersDisabled;\n this.suppressed = suppressed;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n this.optionalNotRun = optionalNotRun;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n // Injected BY TYPE (no Symbol token, per CLAUDE.md) so the ONE reviewer kill switch is read in the\n // ONE place that computes what a branch owes — see the suppression note on `scan`.\n private readonly homeConfig: HomeConfigService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const matched = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n // THE ONE CHOKE POINT for `experimental.turnOffAllReviewers`. Suppressing HERE — rather than in\n // `wp-review-upsert-pr` and again in `wp-finish-upsert-pr` — is what keeps the command that LISTS\n // and the command that BLOCKS in agreement by construction, which is this class's whole reason for\n // existing. A second check in either command would be a second answer to one question.\n const context = opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles);\n if (this.homeConfig.load().turnOffAllReviewers) {\n // EMPTY: applicable, reviewed, outstanding, formatErrors, results, optionalNotRun. INTACT:\n // defined, roster, basis, changedFiles, context — so every downstream reader can still say\n // WHAT was suppressed, which is the difference between an honest record and a silent one.\n return new ChecklistScan(\n defined, [], [], [], context, reviewPath, base, roster, [], true, matched, basis,\n changedFiles, [], []);\n }\n const applicable = matched;\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n // NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is\n // neither reviewed nor blocking, and calling it \"reviewed\" would put a ✓ on the dashboard for a review\n // that never happened.\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable,\n context,\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n false,\n [],\n basis,\n changedFiles,\n results,\n optionalNotRun,\n );\n }\n\n /**\n * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional\n * checklists nobody ran.\n *\n * The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so\n * there is a single answer to \"does this branch owe review?\". When the gate did its own filtering, the\n * command that lists and the command that blocks were two implementations of the same question, which is\n * exactly the divergence this class's docstring exists to prevent.\n */\n private blocking(stillOwed: readonly RequiredChecklist[], optionalNotRun: readonly RequiredChecklist[]): RequiredChecklist[] {\n const skipped = new Set(optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return stillOwed.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
1
+ {"version":3,"file":"checklist-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangedFilesOptions, ChecklistDefinition, ChecklistResult, ChecklistReviewContext, DiffScope,\n HomeConfigService, RequiredChecklist, ReviewJsonService, reviewJsonPath,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\nimport { ChecklistDetector, ChecklistRoster } from './checklist-detector';\nimport { DiffBasis, DiffBasisResolver } from './diff-basis';\nimport { PrContextWriter } from './pr-context-writer';\n\n/** How a caller wants the scan filtered. Data-only (per CLAUDE.md). */\nexport class ChecklistScanOptions {\n /**\n * false — `outstanding` is every applicable checklist (what `wp-review-upsert-pr` LISTS).\n * true — `outstanding` drops the ones already carrying a passing/overridden verdict, leaving only what\n * still owes review (what `wp-finish-upsert-pr` BLOCKS on).\n */\n filterAlreadyReviewed: boolean;\n /**\n * Which per-stage snapshot this scan should persist ('stage3-finish', …), or '' for \"do not write\n * pr-context.json here\".\n *\n * '' exists for stage ②, which cannot write yet: the context records `diffDir`, and the diff has not\n * been materialized at scan time. Letting the scan write anyway meant stage ② wrote the file twice,\n * ~60 lines apart, the first time with an empty diffDir. Now it writes once, after materializing.\n *\n * The DEFAULT is deliberately non-empty, so writing is what you get by NOT thinking about it. A\n * reviewer block that lost its diff command because nobody had written pr-context.json is a bug this\n * codebase has already shipped once (see PrContextWriter's docstring); opting out must be an explicit,\n * visible act by a caller that takes responsibility for writing it later — not an omission.\n */\n contextStage: string;\n\n constructor(filterAlreadyReviewed = false, contextStage = 'stage-scan') {\n this.filterAlreadyReviewed = filterAlreadyReviewed;\n this.contextStage = contextStage;\n }\n}\n\n/**\n * The answer to \"what review does this branch owe?\", in the X → N → Z terms the commands report:\n * X = `defined` — every checklist in pr-gate.checklists\n * N = `applicable` — those whose patterns matched (or that have no patterns, so always run)\n * Z = `outstanding` — of N, those still owing a verdict (only when filterAlreadyReviewed)\n * Data-only.\n */\nexport class ChecklistScan {\n defined: ChecklistDefinition[]; // X\n applicable: RequiredChecklist[]; // N\n reviewed: RequiredChecklist[]; // N − Z: already have a passing/warned/overridden review-<id>.json\n outstanding: RequiredChecklist[]; // Z (== applicable when not filtering)\n context: ChecklistReviewContext; // fork-point sha + pr-context.json path\n reviewPath: string; // the branch's review.json; verdict files sit beside it\n forkPoint: string; // '' when no fork point resolved\n // ALL X, matched or not, with why — what the PR comment publishes as its roster. Skipped checklists are\n // absent from `applicable` by construction, and recovering them downstream would mean a second\n // changed-file computation with different semantics (see the class comment).\n roster: ChecklistRoster;\n // Verdict files that exist but cannot be read as a verdict (e.g. still using the removed `success`).\n // Carried on the SCAN because wp-finish-upsert-pr refuses on missing reviewers before it ever parses\n // review.json — a complaint raised only in there would never reach the AI.\n formatErrors: string[];\n /**\n * The basis the matching ACTUALLY ran against. Carried out so a caller that materializes the diff\n * (stage ②) reuses the identical range instead of resolving its own — two independent resolutions is\n * exactly how the changed-file set and the printed `git diff` command came to disagree.\n */\n basis: DiffBasis;\n changedFiles: string[]; // the full changed-file set the matching ran against\n /**\n * The verdict files the scan ALREADY read, carried out rather than dropped.\n *\n * `outstanding` answers \"who still owes a verdict?\" but not \"why\" — and the two reasons demand opposite\n * actions from the reader: a reviewer that never ran must be SPAWNED, a reviewer that ran and REFUSED\n * must not be (it will refuse again; the finding has to be fixed first). Telling them apart means\n * resolving each checklist's verdict, and without the results here every caller either re-reads the same\n * files off disk — a second read that can disagree with this one — or merges the two cases into one\n * message, which is exactly the loop this field exists to break.\n */\n results: ChecklistResult[];\n /**\n * The OPTIONAL (`required: false`) applicable checklists carrying no verdict file — i.e. nobody ran them.\n *\n * Deliberately NOT in `outstanding` when filtering: that is the exemption that makes `required: false`\n * mean something. Carried out as its own set rather than merely subtracted, because both readers need it\n * BY NAME and neither can recover it from what is left: stage ② offers exactly these to the human, and\n * the PR dashboard must publish them as \"not run\" rather than let a shorter roster imply everything\n * passed. An optional checklist that ran and went RED is absent from here and stays in `outstanding`.\n */\n optionalNotRun: RequiredChecklist[];\n /**\n * TRUE when this machine's `~/.webpieces/config.json` carries\n * `experimental.turnOffAllReviewers: true`, and therefore when `applicable`, `reviewed`,\n * `outstanding`, `results`, `optionalNotRun` and `formatErrors` above are ALL EMPTY BY DECREE rather\n * than because nothing matched. Those two states are indistinguishable from the empty lists alone,\n * and telling them apart is the entire reason this field exists: an unreviewed PR must never render\n * as a reviewed one.\n *\n * `defined`, `roster`, `basis`, `changedFiles` and `context` are deliberately left INTACT under\n * suppression, so every reader can still say WHAT was switched off — including which REQUIRED\n * checklists would otherwise have run.\n *\n * See `HOME_KEY_TURN_OFF_ALL_REVIEWERS` in rules-config for the flag's contract; it is an\n * `experimental.*` flag, so only a human ever ends it.\n */\n reviewersDisabled: boolean;\n /**\n * The checklists that WOULD have been `applicable` had the reviewers not been suppressed — required\n * ones included. EMPTY on every ordinary run, and empty is not ambiguous there because\n * `reviewersDisabled` is false.\n *\n * Carried by NAME and not as a count because every reader needs a different slice of it: stage ②\n * prints them so a human can see exactly which required reviewer was killed, the dashboard names them\n * in the 1st comment, and the compact PR body needs only `.length`. Recomputing any of that\n * downstream would mean a second changed-file computation that can disagree with this one.\n */\n suppressed: RequiredChecklist[];\n /** TRUE only for the top-level `singleRoundReview` machine opt-in. */\n singleRoundReview: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n defined: ChecklistDefinition[],\n applicable: RequiredChecklist[],\n reviewed: RequiredChecklist[],\n outstanding: RequiredChecklist[],\n context: ChecklistReviewContext,\n reviewPath: string,\n forkPoint: string,\n roster: ChecklistRoster,\n formatErrors: string[],\n // REQUIRED, and placed BEFORE the defaulted tail deliberately. A defaulted `reviewersDisabled =\n // false` would let every existing construction keep compiling while asserting, silently, that no\n // reviewer was switched off — the widening-by-omission `.claude/rules/no-backwards-compat.md`\n // rejects, and on the one field whose whole job is to stop an unreviewed PR reading as reviewed.\n reviewersDisabled: boolean,\n suppressed: RequiredChecklist[],\n basis: DiffBasis = new DiffBasis(),\n changedFiles: string[] = [],\n // Defaulted so a caller that only cares about the X/N/Z counts (and every existing test construction)\n // stays a one-liner; the scanner itself always passes the real set.\n results: ChecklistResult[] = [],\n optionalNotRun: RequiredChecklist[] = [],\n singleRoundReview = false,\n ) {\n this.defined = defined;\n this.applicable = applicable;\n this.reviewed = reviewed;\n this.outstanding = outstanding;\n this.context = context;\n this.reviewPath = reviewPath;\n this.forkPoint = forkPoint;\n this.roster = roster;\n this.formatErrors = formatErrors;\n this.reviewersDisabled = reviewersDisabled;\n this.suppressed = suppressed;\n this.basis = basis;\n this.changedFiles = changedFiles;\n this.results = results;\n this.optionalNotRun = optionalNotRun;\n this.singleRoundReview = singleRoundReview;\n }\n}\n\n/**\n * The ONE computation of which reviewer subagents a branch owes, shared by `wp-review-upsert-pr` (which lists) and\n * `wp-finish-upsert-pr` (which blocks). They previously each assembled this from the same parts in slightly\n * different ways, and any divergence means the command that reports and the command that gates disagree.\n *\n * Two properties are deliberate and load-bearing:\n *\n * 1. **The base is the FORK POINT of main, computed directly** — never `DiffScope.resolveBase`, which\n * overlays `NX_BASE`/`NX_HEAD` from the environment. That made review coverage depend on an env var.\n * It now arrives via {@link DiffBasisResolver}, which injects ForkPoint; same sha, one resolution.\n * 2. **UNCOMMITTED work counts.** `getChangedFiles` is called with NO head, which is the branch of it that\n * diffs base → WORKING TREE and unions in untracked files. Passing a head would diff commit-to-commit and\n * silently miss staged, unstaged and untracked changes — so a checklist matching only uncommitted files\n * would never fire and its reviewer would never be listed.\n * 3. **The range and the command it prints are the SAME basis.** Property 2 used to be true of the file set\n * only: reviewers were handed `git diff <base> HEAD`, which on a dirty tree covers a different range and\n * prints nothing. Both now derive from one {@link DiffBasis}, carried on the scan so a materializing\n * caller cannot re-resolve and drift.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistScanner {\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly diffScope: DiffScope,\n private readonly diffBasisResolver: DiffBasisResolver,\n private readonly prContextWriter: PrContextWriter,\n private readonly reviewJsonService: ReviewJsonService,\n // Injected BY TYPE (no Symbol token, per CLAUDE.md) so the ONE reviewer kill switch is read in the\n // ONE place that computes what a branch owes — see the suppression note on `scan`.\n private readonly homeConfig: HomeConfigService,\n ) {}\n\n /**\n * `defined` is the caller's ALREADY-VALIDATED `prGate.checklists`. The scanner deliberately does not load\n * config itself: `loadAndValidate` is the one gate on the checklist set (it rejects a non-array\n * `checklists`, including the removed `{ doc }` manifest shape, and verifies every guidance doc and\n * reviewer-agent file exists), both callers already run it for other fields, and keeping the read out of\n * here leaves this a function of its inputs rather than of the filesystem.\n */\n scan(repoRoot: string, defined: ChecklistDefinition[], opts: ChecklistScanOptions): ChecklistScan {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n // ONE basis for the file set, the reproduce command and any downstream materialization. The fork\n // point still comes from ForkPoint (never DiffScope.resolveBase) — DiffBasisResolver injects it.\n const basis = this.diffBasisResolver.resolve(repoRoot);\n const base = basis.base;\n // ONE changed-file computation feeds both the roster (all X) and the applicable set (N). `detect` is\n // pure and defined as the roster minus its empty entries, so the two cannot disagree about a match.\n const changedFiles = this.changedFiles(repoRoot, base);\n const roster = new ChecklistRoster(\n this.checklistDetector.roster(defined, changedFiles), changedFiles.length, base !== '');\n const matched = this.checklistDetector.toRequired(this.checklistDetector.detect(defined, changedFiles));\n // THE ONE CHOKE POINT for `experimental.turnOffAllReviewers`. Suppressing HERE — rather than in\n // `wp-review-upsert-pr` and again in `wp-finish-upsert-pr` — is what keeps the command that LISTS\n // and the command that BLOCKS in agreement by construction, which is this class's whole reason for\n // existing. A second check in either command would be a second answer to one question.\n const context = opts.contextStage === ''\n ? this.prContextWriter.contextFor(repoRoot, featureName, basis)\n : this.prContextWriter.ensure(repoRoot, featureName, basis, opts.contextStage, changedFiles);\n const homeConfig = this.homeConfig.load();\n if (homeConfig.turnOffAllReviewers) {\n // EMPTY: applicable, reviewed, outstanding, formatErrors, results, optionalNotRun. INTACT:\n // defined, roster, basis, changedFiles, context — so every downstream reader can still say\n // WHAT was suppressed, which is the difference between an honest record and a silent one.\n return new ChecklistScan(\n defined, [], [], [], context, reviewPath, base, roster, [], true, matched, basis,\n changedFiles, [], [], homeConfig.singleRoundReview);\n }\n const applicable = matched;\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, applicable);\n const stillOwed = this.reviewJsonService.pendingChecklists(applicable, results);\n const owedIds = new Set(stillOwed.map((r: RequiredChecklist): string => r.id));\n // NOT `!owedIds.has(...)`-with-the-optional-exemption-folded-in: an optional checklist nobody ran is\n // neither reviewed nor blocking, and calling it \"reviewed\" would put a ✓ on the dashboard for a review\n // that never happened.\n const reviewed = applicable.filter((r: RequiredChecklist): boolean => !owedIds.has(r.id));\n const optionalNotRun = this.reviewJsonService.optionalWithoutVerdict(applicable, results);\n return new ChecklistScan(\n defined,\n applicable,\n reviewed,\n opts.filterAlreadyReviewed ? this.blocking(stillOwed, optionalNotRun) : applicable,\n context,\n reviewPath,\n base,\n roster,\n this.reviewJsonService.checklistFormatErrors(applicable, results),\n false,\n [],\n basis,\n changedFiles,\n results,\n optionalNotRun,\n homeConfig.singleRoundReview,\n );\n }\n\n /**\n * What `wp-finish-upsert-pr` actually REFUSES on: everything still owing a verdict, minus the optional\n * checklists nobody ran.\n *\n * The subtraction happens HERE — in the one place `outstanding` is computed — and not in the gate, so\n * there is a single answer to \"does this branch owe review?\". When the gate did its own filtering, the\n * command that lists and the command that blocks were two implementations of the same question, which is\n * exactly the divergence this class's docstring exists to prevent.\n */\n private blocking(stillOwed: readonly RequiredChecklist[], optionalNotRun: readonly RequiredChecklist[]): RequiredChecklist[] {\n const skipped = new Set(optionalNotRun.map((r: RequiredChecklist): string => r.id));\n return stillOwed.filter((r: RequiredChecklist): boolean => !skipped.has(r.id));\n }\n\n /**\n * Every file changed since the fork point, INCLUDING uncommitted and untracked ones. Two non-default\n * options, both load-bearing:\n *\n * `tsOnly:false` — the default drops every *.sql / Dockerfile / .env* file a checklist most wants\n * to key on, which would silently shrink the set a reviewer is pointed at.\n * `includeDeletions:true` — the default is `--diff-filter=d`, so a DELETED file is invisible. A PR that\n * deletes a migration, an auth check or a terraform rule changed exactly what a\n * checklist exists to catch, and under the default no checklist fires at all.\n */\n private changedFiles(repoRoot: string, base: string): string[] {\n if (base === '') return [];\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n // No head argument — see the class comment. This is what includes the working tree.\n return this.diffScope.getChangedFiles(repoRoot, base, undefined, opts);\n }\n}\n"]}
@@ -57,6 +57,9 @@ export declare class ReviewReportInput {
57
57
  * WHICH required reviewer was killed.
58
58
  */
59
59
  suppressed: RequiredChecklist[];
60
+ singleRoundReview: boolean;
61
+ singleRoundRepeat: boolean;
62
+ singleRoundReviewers: string[];
60
63
  constructor(repoRoot: string, featureName: string, reviewPath: string);
61
64
  }
62
65
  /**
@@ -169,6 +172,7 @@ export declare class ReviewReport {
169
172
  */
170
173
  private nextSteps;
171
174
  private writeReviewStep;
175
+ private singleRoundRepeatStep;
172
176
  /**
173
177
  * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is
174
178
  * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything
@@ -67,6 +67,9 @@ class ReviewReportInput {
67
67
  * WHICH required reviewer was killed.
68
68
  */
69
69
  suppressed;
70
+ singleRoundReview;
71
+ singleRoundRepeat;
72
+ singleRoundReviewers;
70
73
  constructor(repoRoot, featureName, reviewPath) {
71
74
  this.repoRoot = repoRoot;
72
75
  this.featureName = featureName;
@@ -80,6 +83,9 @@ class ReviewReportInput {
80
83
  this.skipOptional = false;
81
84
  this.reviewersSuppressed = false;
82
85
  this.suppressed = [];
86
+ this.singleRoundReview = false;
87
+ this.singleRoundRepeat = false;
88
+ this.singleRoundReviewers = [];
83
89
  }
84
90
  }
85
91
  exports.ReviewReportInput = ReviewReportInput;
@@ -128,6 +134,8 @@ let ReviewReport = class ReviewReport {
128
134
  * single line most likely to make it do exactly that.
129
135
  */
130
136
  header(input) {
137
+ if (input.singleRoundRepeat)
138
+ return '② ⏭️ SINGLE-ROUND REVIEW ALREADY STARTED — skip reviews and finish\n';
131
139
  // FIRST, and unconditional: with the kill switch on there is nothing to spawn and nothing to
132
140
  // offer, so every heading below would be true-but-misleading. The one thing a reader must take
133
141
  // from the first line of this block is that no reviewer looked at this branch.
@@ -145,6 +153,11 @@ let ReviewReport = class ReviewReport {
145
153
  * mistaken for the next action.
146
154
  */
147
155
  scanVerdict(input) {
156
+ if (input.singleRoundRepeat) {
157
+ return '\n⏭️ `singleRoundReview: true` is enabled and this branch already used its one review round.\n'
158
+ + ` Recorded reviewer(s): ${input.singleRoundReviewers.join(', ')}\n`
159
+ + ' DO NOT spawn or re-spawn any reviewer, regardless of later edits or an earlier red.\n';
160
+ }
148
161
  // BEFORE the zero-applicable notice, which would otherwise say "nothing matched this diff" — the
149
162
  // single most misleading sentence available here, because plenty matched and every one of them was
150
163
  // switched off.
@@ -269,12 +282,14 @@ let ReviewReport = class ReviewReport {
269
282
  * can never drift from the shape `wp-finish-upsert-pr` validates.
270
283
  */
271
284
  nextSteps(input) {
285
+ if (input.singleRoundRepeat)
286
+ return this.singleRoundRepeatStep();
272
287
  const required = this.requiredOwed(input);
273
288
  const offerable = this.offerableOwed(input);
274
289
  // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write
275
290
  // review.json, then a spawn step only if anything must run, then an offer step only if anything may.
276
291
  let step = 1;
277
- const write = this.writeReviewStep(input.reviewPath, step++);
292
+ const write = this.writeReviewStep(input.reviewPath, step++, input.singleRoundReview ? rules_config_1.SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS : '');
278
293
  // The wait block goes under the LAST reviewer-listing step, and only there. Printed under both
279
294
  // it would be two "what to do next" instructions in one output, which is the defect this
280
295
  // method's docstring describes — an agent reading top to bottom obeys the first one it meets.
@@ -284,13 +299,21 @@ let ReviewReport = class ReviewReport {
284
299
  + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\n` + SEP + '\n'
285
300
  + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);
286
301
  }
287
- writeReviewStep(reviewPath, step) {
302
+ writeReviewStep(reviewPath, step, mainAgentInstructions) {
288
303
  return (`STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\n` +
289
304
  ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\n' +
290
305
  ' checklist that judges the PR title, summary or risk level reads exactly this file, so\n' +
291
306
  ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\n' +
292
307
  ' stage, into judging the PREVIOUS run\'s review of code that has since changed.\n\n' +
293
- (0, rules_config_1.reviewJsonSchemaHint)(reviewPath) + '\n\n');
308
+ (0, rules_config_1.reviewJsonSchemaHint)(reviewPath, mainAgentInstructions) + '\n\n');
309
+ }
310
+ singleRoundRepeatStep() {
311
+ return '\n' + SEP
312
+ + '▶ NEXT — reviewers have already run once. SKIP reviews and run:\n' + SEP + '\n'
313
+ + ' pnpm wp-finish-upsert-pr\n\n'
314
+ + ' Do NOT invoke or re-run reviewer subagents. They are expensive and their one verdict\n'
315
+ + ' remains the review record for this PR. If a red remains, finish will block and tell you\n'
316
+ + ' how to fix it without another reviewer round.\n\n';
294
317
  }
295
318
  /**
296
319
  * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is
@@ -1 +1 @@
1
- {"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAC3D,0DAGiC;AACjC,yDAAqD;AAErD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;;;;;GAaG;AACH,MAAa,eAAe;IACxB,WAAW,CAAS;IACpB,OAAO,CAAS;IAEhB,YAAY,WAAmB,EAAE,OAAe;QAC5C,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AARD,0CAQC;AAED;;;;GAIG;AACH,MAAa,iBAAiB;IAC1B,QAAQ,CAAS;IACjB,WAAW,CAAS;IACpB,UAAU,CAAS,CAAa,6DAA6D;IAC7F,YAAY,CAAS,CAAW,iDAAiD;IACjF,eAAe,CAAS,CAAQ,yEAAyE;IACzG,QAAQ,CAAsB,CAAE,gDAAgD;IAChF,YAAY,CAAW,CAAS,0DAA0D;IAC1F,SAAS,CAAqB,CAAE,wDAAwD;IACxF,OAAO,CAAoB,CAAK,6EAA6E;IAC7G;;;;OAIG;IACH,YAAY,CAAU;IACtB;;;;;;;;;OASG;IACH,mBAAmB,CAAU;IAC7B;;;;OAIG;IACH,UAAU,CAAsB;IAEhC,YAAY,QAAgB,EAAE,WAAmB,EAAE,UAAkB;QACjE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;IACzB,CAAC;CACJ;AAhDD,8CAgDC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEA;IACA;IAFrB,YACqB,eAAgC,EAChC,oBAAiD;QADjD,oBAAe,GAAf,eAAe,CAAiB;QAChC,yBAAoB,GAApB,oBAAoB,CAA6B;IACnE,CAAC;IAEJ,MAAM,CAAC,KAAwB;QAC3B,OAAO,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG;cACtC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;cACvB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAED;;;;;;;;;;OAUG;IACK,MAAM,CAAC,KAAwB;QACnC,6FAA6F;QAC7F,+FAA+F;QAC/F,+EAA+E;QAC/E,IAAI,KAAK,CAAC,mBAAmB;YAAE,OAAO,sDAAsD,CAAC;QAC7F,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,mDAAmD,CAAC;QACpG,IAAI,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,uDAAuD,CAAC;QACzG,OAAO,yBAAyB,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,KAAwB;QACxC,iGAAiG;QACjG,mGAAmG;QACnG,gBAAgB;QAChB,IAAI,KAAK,CAAC,mBAAmB;YAAE,OAAO,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC3E,IAAI,KAAK,CAAC,eAAe,KAAK,CAAC;YAAE,OAAO,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC9F,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,+FAA+F;QAC/F,gGAAgG;QAChG,yFAAyF;QACzF,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,+EAA+E,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7H,CAAC;QACD,gGAAgG;QAChG,kGAAkG;QAClG,2CAA2C;QAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,YAAY;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAClF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAClC,OAAO,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;;OAWG;IACK,iBAAiB,CAAC,KAAwB;QAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACxF,MAAM,KAAK,GAAa;YACpB,0FAA0F;YAC1F,EAAE;YACF,MAAM,KAAK,CAAC,UAAU,CAAC,MAAM,wDAAwD;kBACnF,GAAG,QAAQ,CAAC,MAAM,oBAAoB;SAC3C,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;QAC1G,CAAC;QACD,KAAK,CAAC,IAAI,CACN,EAAE,EACF,qCAAqC,8CAA+B,QAAQ,EAC5E,0BAA0B,8BAAe,IAAI,+BAAgB,gDAAgD,EAC7G,EAAE,EACF,8FAA8F,EAC9F,+FAA+F,EAC/F,8FAA8F,EAC9F,EAAE,EACF,+FAA+F,EAC/F,yFAAyF,EACzF,6FAA6F,CAChG,CAAC;QACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,KAAwB;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC3D,OAAO;YACH,EAAE;YACF,SAAS,OAAO,CAAC,MAAM,4EAA4E;YACnG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,UAAU,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,oFAAoF;SACvF,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACK,QAAQ,CAAC,KAAwB;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;YACtE,CAAC,CAAC,+FAA+F;YACjG,CAAC,CAAC,wEAAwE,CAAC;QAC/E,OAAO,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACK,iBAAiB;QACrB,OAAO,CACH,mGAAmG;YACnG,qGAAqG;YACrG,kGAAkG;YAClG,iGAAiG,CACpG,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACK,SAAS,CAAC,KAAwB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5C,kGAAkG;QAClG,qGAAqG;QACrG,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,+FAA+F;QAC/F,yFAAyF;QACzF,8FAA8F;QAC9F,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC;QAC3G,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QAC3F,OAAO,IAAI,GAAG,GAAG;cACX,YAAY,IAAI,kDAAkD,GAAG,GAAG,GAAG,IAAI;cAC/E,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChG,CAAC;IAEO,eAAe,CAAC,UAAkB,EAAE,IAAY;QACpD,OAAO,CACH,QAAQ,IAAI,6FAA6F;YACzG,8FAA8F;YAC9F,kGAAkG;YAClG,oGAAoG;YACpG,6FAA6F;YAC7F,IAAA,mCAAoB,EAAC,UAAU,CAAC,GAAG,MAAM,CAC5C,CAAC;IACN,CAAC;IAED;;;;;;;;;OASG;IACK,SAAS,CAAC,KAAwB,EAAE,IAAiC,EAAE,IAAY,EAAE,SAAkB;QAC3G,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,kDAAkD,IAAI,CAAC,MAAM,oCAAoC;YAC7G,+FAA+F;YAC/F,gGAAgG;YAChG,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAChD,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QAClE,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACK,UAAU;QACd,OAAO;YACH,gGAAgG;YAChG,+FAA+F;YAC/F,6FAA6F;YAC7F,kBAAkB;YAClB,EAAE;YACF,oCAAoC;YACpC,EAAE;YACF,8FAA8F;YAC9F,wFAAwF;SAC3F,CAAC;IACN,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACK,SAAS,CAAC,KAAwB,EAAE,SAAsC,EAAE,IAAY,EAAE,SAAkB;QAChH,MAAM,KAAK,GAAa;YACpB,QAAQ,IAAI,YAAY,SAAS,CAAC,MAAM,wEAAwE;YAChH,wEAAwE;YACxE,EAAE;YACF,2FAA2F;YAC3F,+FAA+F;YAC/F,gEAAgE;YAChE,EAAE;YACF,+FAA+F;YAC/F,4EAA4E;YAC5E,6EAA6E;YAC7E,EAAE;YACF,gGAAgG;YAChG,uEAAuE;YACvE,EAAE;SACL,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,sGAAsG;IACtG,qGAAqG;IACrG,qGAAqG;IAC7F,cAAc,CAAC,KAAwB,EAAE,KAAkC;QAC/E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/F,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvB,OAAO;YACH,YAAY,CAAC,sEAAsE;YACnF,iGAAiG;YACjG,EAAE;SACL,CAAC;IACN,CAAC;IAEO,UAAU,CAAC,UAAkB,EAAE,YAAqB;QACxD,mGAAmG;QACnG,sGAAsG;QACtG,qBAAqB;QACrB,MAAM,YAAY,GAAG,YAAY;YAC7B,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,uBAAuB,CAAC;QAC9B,OAAO,CACH,QAAQ,UAAU,WAAW,YAAY,oCAAoC;YAC7E,sGAAsG,CACzG,CAAC;IACN,CAAC;IAED,sGAAsG;IACtG,yGAAyG;IACjG,aAAa,CAAC,KAAwB;QAC1C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACxF,OAAO,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;IACrG,CAAC;IAED,8CAA8C;IACtC,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1F,CAAC;IAED,mGAAmG;IAC3F,YAAY,CAAC,KAAwB;QACzC,OAAO,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAmB,EAAW,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3F,CAAC;IAED,qGAAqG;IAC7F,aAAa,CAAC,KAAwB;QAC1C,OAAO,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,wGAAwG;IACxG,gGAAgG;IAChG,oDAAoD;IAC5C,cAAc,CAAC,KAAwB;QAC3C,OAAO,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;;;OASG;IACK,aAAa,CAAC,KAAwB,EAAE,CAAmB;QAC/D,MAAM,gBAAgB,GAAG,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC1G,OAAO;YACH,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;YACxB,wBAAwB,CAAC,CAAC,QAAQ,EAAE;YACpC,+EAA+E;YAC/E,wBAAwB,gBAAgB,EAAE;YAC1C,EAAE;SACL,CAAC;IACN,CAAC;IAED,mGAAmG;IACnG,uFAAuF;IAC/E,MAAM,CAAC,KAAwB,EAAE,CAAmB;QACxD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAkB,EAAW,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC;QACrG,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,OAAO;YACH,OAAO,CAAC,CAAC,QAAQ,sFAAsF;YACvG,SAAS,OAAO,CAAC,OAAO,EAAE;YAC1B,iGAAiG;YACjG,8BAA8B;SACjC,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACK,OAAO,CAAC,CAAmB;QAC/B,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9C,OAAO,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,sGAAsG;IACtG,+FAA+F;IACvF,GAAG,CAAC,CAAmB;QAC3B,IAAI,CAAC,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,iEAAiE,CAAC,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC;QACvG,CAAC;QACD,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,oBAAoB,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtH,CAAC;CACJ,CAAA;AAtZY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGC,kCAAe;QACV,0CAA2B;GAH7D,YAAY,CAsZxB","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n HOME_CONFIG_DIR, HOME_CONFIG_FILE, HOME_KEY_TURN_OFF_ALL_REVIEWERS, reviewJsonSchemaHint,\n RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService,\n} from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run\", which is only true while\n * the verdict file is still live — and at stage ② it is. Retiring a verdict is finish's act, on the refusal\n * it is actually acting on. (The ship-anyway route it prints — the command that writes `override-<id>.json`\n * — is correct at either stage: that is a different file, with its own writer, and no archive touches it.)\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n /**\n * `experimental.turnOffAllReviewers` is TRUE in `~/.webpieces/config.json`, so NO reviewer ran and\n * none will — the REQUIRED ones included. Nothing else in this class may be read as evidence of that:\n * `applicableCount` is 0 under suppression exactly as it is on a docs-only PR that matched nothing,\n * and those two must never print the same thing.\n *\n * When true this stage prints a LOUD banner naming the flag, the file and every suppressed checklist,\n * and prints NO spawn block. Reviewers cannot be spawned — there are no briefings and no instructions\n * files — so a block telling an agent to spawn them would be an instruction it cannot obey.\n */\n reviewersSuppressed: boolean;\n /**\n * The checklists that WOULD have applied, had they not been suppressed. Empty unless\n * `reviewersSuppressed`. Named, not counted, because the one thing a human must be able to see is\n * WHICH required reviewer was killed.\n */\n suppressed: RequiredChecklist[];\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n this.reviewersSuppressed = false;\n this.suppressed = [];\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n // FIRST, and unconditional: with the kill switch on there is nothing to spawn and nothing to\n // offer, so every heading below would be true-but-misleading. The one thing a reader must take\n // from the first line of this block is that no reviewer looked at this branch.\n if (input.reviewersSuppressed) return '② ⚫ ALL REVIEWERS SUPPRESSED — review, then finish\\n';\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n // BEFORE the zero-applicable notice, which would otherwise say \"nothing matched this diff\" — the\n // single most misleading sentence available here, because plenty matched and every one of them was\n // switched off.\n if (input.reviewersSuppressed) return '\\n' + this.suppressionBanner(input);\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n // The prohibition rides on the REUSE line itself, not only in the all-clear below, because the\n // all-clear is not printed when anything is still owed — and \"some reviewers are reused, others\n // must be spawned\" is exactly the shape in which an agent re-spawns the reused ones too.\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * THE LOUD BANNER. The one output in this whole flow that says an unreviewed PR is about to be posted.\n *\n * Everything in it is there because a reader has to be able to act on it or audit it: the FLAG name and\n * the FILE it was read from (the only actionable thing — nothing in any repo turns this off), the COUNT\n * split by required/optional, and every suppressed checklist BY NAME with REQUIRED marked. A count\n * alone would leave \"which required reviewer did I just skip?\" unanswerable at the one moment it is\n * being answered.\n *\n * It deliberately does NOT name `wp-finish-upsert-pr` — the closing step already does, exactly once,\n * and that \"exactly once\" is an invariant this class's docstring records having been bought with a bug.\n */\n private suppressionBanner(input: ReviewReportInput): string {\n const required = input.suppressed.filter((r: RequiredChecklist): boolean => r.required);\n const lines: string[] = [\n '⚫ ALL REVIEWER SUBAGENTS ARE SWITCHED OFF ON THIS MACHINE. Nothing reviewed this branch.',\n '',\n ` ${input.suppressed.length} checklist(s) matched this diff and were SUPPRESSED — `\n + `${required.length} of them REQUIRED:`,\n ];\n for (const r of input.suppressed) {\n lines.push(` • ${r.subagent}${r.required ? ' (REQUIRED — suppressed anyway)' : ' (optional)'}`);\n }\n lines.push(\n '',\n ` Switched off by: experimental.${HOME_KEY_TURN_OFF_ALL_REVIEWERS}: true`,\n ` Read from: ~/${HOME_CONFIG_DIR}/${HOME_CONFIG_FILE} (machine-local; no repo config can set this)`,\n '',\n ' There is NOTHING to spawn — no reviewer was briefed and no instructions file was written,',\n ' so any attempt to spawn one has nothing to read. Do NOT hand-write a verdict file in their',\n ' place: a fabricated verdict is worse than the absent one this flag deliberately produces.',\n '',\n ' The dashboard is the whole review product for this PR, and the suppression is carried into',\n ' the PR body — which is the squash-merge commit body — so main\\'s history records it.',\n ' To get the reviewers back, set that key to false (or delete it) and re-run this command.',\n );\n return lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n /**\n * The all-clear, then the RULE that makes it actionable.\n *\n * \"nothing to spawn\" on its own is a description of the current state, and an agent that has just been\n * told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It\n * then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier\n * tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its\n * checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives\n * review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every\n * matched reviewer — and the output has to say so, because the alternative reading is the expensive one.\n *\n * The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a\n * gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.\n *\n * It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence\n * that would turn a deliberate skip into a false record of a review that happened.\n */\n private allClear(input: ReviewReportInput): string {\n const headline = input.skipOptional && this.optionalOwed(input).length > 0\n ? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'\n : '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n return headline + '\\n' + this.oncePerBranchRule();\n }\n\n /**\n * The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.\n *\n * Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the\n * majority of stage-② runs on any branch that gets review feedback.\n */\n private oncePerBranchRule(): string {\n return (\n ' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\\n' +\n ' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\\n' +\n ' listed above to \"re-check\" the newer code — it burns a full subagent run AND overwrites the\\n' +\n ' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.'\n );\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(input.reviewPath, step++);\n // The wait block goes under the LAST reviewer-listing step, and only there. Printed under both\n // it would be two \"what to do next\" instructions in one output, which is the defect this\n // method's docstring describes — an agent reading top to bottom obeys the first one it meets.\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++, offerable.length === 0);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++, true);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n'\n );\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number, withAwait: boolean): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n if (withAwait) lines.push(...this.awaitLines());\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * How to WAIT once the spawn list has been spawned — printed after the blocks, because it is the\n * next thing to do and nothing before it can be mistaken for it.\n *\n * It is here because the alternative is measured and expensive: `echo .` every three seconds at\n * ~557,000 tokens a turn, 18.3% of every token the fleet spent in the 24h to 2026-09-07 (#874).\n *\n * It names the efficient options and the wasteful one, and then stops (#902). It does NOT prescribe\n * ending the turn: waiting on the subagents you just spawned is something an agent already does\n * routinely, and whether to do that here is a judgement this string cannot make for it.\n *\n * It names no other stage. `finishStep` below is the ONE place this whole block names\n * `wp-finish-upsert-pr`, and a second mention here would be a second \"what to do next\" instruction\n * for an agent reading top to bottom — the exact defect the class docstring above describes.\n */\n private awaitLines(): string[] {\n return [\n ' Then WAIT. Be efficient with tokens: wait on the subagents you just spawned, or block',\n ' in one call with the command below. Do NOT send status checks every few seconds, and',\n ' do NOT run `echo` to keep your turn alive — a turn costs your whole context, ~557k',\n ' tokens.',\n '',\n ' pnpm wp-await-reviews',\n '',\n ' It heartbeats while it waits, returns as soon as the last verdict lands, and prints',\n ' what each reviewer said. If the wait is long it exits asking to be run again.',\n ];\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number, withAwait: boolean): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n if (withAwait) lines.push(...this.awaitLines());\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
1
+ {"version":3,"file":"review-report.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/review-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport {\n HOME_CONFIG_DIR, HOME_CONFIG_FILE, HOME_KEY_TURN_OFF_ALL_REVIEWERS, reviewJsonSchemaHint,\n RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService, SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS,\n} from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * One reviewer that ALREADY ANSWERED on this branch and refused, with the refusal rendered by\n * `ReviewJsonService.refusalError` — the ONE wording, shared with `wp-finish-upsert-pr`. Data-only.\n *\n * Stage ② carries these because it had the same defect finish did: a refused checklist has no passing\n * verdict, so it is \"owed\", so it got an ordinary spawn block identical to a reviewer that never ran. An\n * agent obeys that block, the reviewer re-reads unchanged code, refuses again — the loop, one stage earlier.\n *\n * The message is rendered by the COMMAND rather than in here for one reason: stage ② must not archive\n * anything. `refusalError` called with no archive path says \"fix it, then re-run\", which is only true while\n * the verdict file is still live — and at stage ② it is. Retiring a verdict is finish's act, on the refusal\n * it is actually acting on. (The ship-anyway route it prints — the command that writes `override-<id>.json`\n * — is correct at either stage: that is a different file, with its own writer, and no archive touches it.)\n */\nexport class RefusedReviewer {\n checklistId: string;\n message: string;\n\n constructor(checklistId: string, message: string) {\n this.checklistId = checklistId;\n this.message = message;\n }\n}\n\n/**\n * Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an\n * object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional\n * facts about the scan that default to \"nothing\", so a repo with no checklists constructs it in one line.\n */\nexport class ReviewReportInput {\n repoRoot: string;\n featureName: string;\n reviewPath: string; // the branch's review.json — the file the AI must write next\n definedCount: number; // how many checklists pr-gate.checklists defines\n applicableCount: number; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)\n reviewed: RequiredChecklist[]; // already have a passing verdict on this branch\n formatErrors: string[]; // verdict files that exist but cannot be read as verdicts\n briefings: ReviewerBriefing[]; // one per applicable checklist, already written to disk\n refused: RefusedReviewer[]; // of the owed ones, those that already ran and said no (see RefusedReviewer)\n /**\n * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the\n * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses\n * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.\n */\n skipOptional: boolean;\n /**\n * `experimental.turnOffAllReviewers` is TRUE in `~/.webpieces/config.json`, so NO reviewer ran and\n * none will — the REQUIRED ones included. Nothing else in this class may be read as evidence of that:\n * `applicableCount` is 0 under suppression exactly as it is on a docs-only PR that matched nothing,\n * and those two must never print the same thing.\n *\n * When true this stage prints a LOUD banner naming the flag, the file and every suppressed checklist,\n * and prints NO spawn block. Reviewers cannot be spawned — there are no briefings and no instructions\n * files — so a block telling an agent to spawn them would be an instruction it cannot obey.\n */\n reviewersSuppressed: boolean;\n /**\n * The checklists that WOULD have applied, had they not been suppressed. Empty unless\n * `reviewersSuppressed`. Named, not counted, because the one thing a human must be able to see is\n * WHICH required reviewer was killed.\n */\n suppressed: RequiredChecklist[];\n singleRoundReview: boolean;\n singleRoundRepeat: boolean;\n singleRoundReviewers: string[];\n\n constructor(repoRoot: string, featureName: string, reviewPath: string) {\n this.repoRoot = repoRoot;\n this.featureName = featureName;\n this.reviewPath = reviewPath;\n this.definedCount = 0;\n this.applicableCount = 0;\n this.reviewed = [];\n this.formatErrors = [];\n this.briefings = [];\n this.refused = [];\n this.skipOptional = false;\n this.reviewersSuppressed = false;\n this.suppressed = [];\n this.singleRoundReview = false;\n this.singleRoundRepeat = false;\n this.singleRoundReviewers = [];\n }\n}\n\n/**\n * Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE \"what to do\n * next\".\n *\n * Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the\n * contract. The bug it was extracted to fix: the zero-checklist notice ended with \"Carry on and run: pnpm\n * wp-finish-upsert-pr\", and the block printed directly beneath it said to write review.json first and\n * finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —\n * which is the entire reason this command prints them — took the first one and opened a PR with no review.\n * That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).\n *\n * The invariants, enforced by review-report.spec.ts:\n * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.\n * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).\n * 3. With zero checklists the all-clear precedes any configuration guidance.\n * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched\n * question. The two never share a step, because one instruction says \"do it\" and the other says\n * \"ask first\", and an agent reading a merged list will act on the stronger of the two.\n *\n * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewReport {\n constructor(\n private readonly checklistNotice: ChecklistNotice,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n ) {}\n\n render(input: ReviewReportInput): string {\n return '\\n' + SEP + this.header(input) + SEP\n + this.scanVerdict(input)\n + this.nextSteps(input);\n }\n\n /**\n * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with\n * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist\n * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the\n * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so\n * does a branch whose only outstanding reviews are optional ones the human already waved off.\n *\n * \"spawn\" and \"ask about\" are separate headings because they are separate obligations. A branch owing\n * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the\n * single line most likely to make it do exactly that.\n */\n private header(input: ReviewReportInput): string {\n if (input.singleRoundRepeat) return '② ⏭️ SINGLE-ROUND REVIEW ALREADY STARTED — skip reviews and finish\\n';\n // FIRST, and unconditional: with the kill switch on there is nothing to spawn and nothing to\n // offer, so every heading below would be true-but-misleading. The one thing a reader must take\n // from the first line of this block is that no reviewer looked at this branch.\n if (input.reviewersSuppressed) return '② ⚫ ALL REVIEWERS SUPPRESSED — review, then finish\\n';\n if (this.requiredOwed(input).length > 0) return '② Review, spawn subagent reviewers, then finish\\n';\n if (this.offerableOwed(input).length > 0) return '② Review, offer the optional reviewers, then finish\\n';\n return '② Review, then finish\\n';\n }\n\n /**\n * What the SCAN found: either \"nothing applies here\" or the already-reviewed / unreadable-verdict lines.\n * Verdicts, not instructions — every instruction lives in nextSteps() below, so no line up here can be\n * mistaken for the next action.\n */\n private scanVerdict(input: ReviewReportInput): string {\n if (input.singleRoundRepeat) {\n return '\\n⏭️ `singleRoundReview: true` is enabled and this branch already used its one review round.\\n'\n + ` Recorded reviewer(s): ${input.singleRoundReviewers.join(', ')}\\n`\n + ' DO NOT spawn or re-spawn any reviewer, regardless of later edits or an earlier red.\\n';\n }\n // BEFORE the zero-applicable notice, which would otherwise say \"nothing matched this diff\" — the\n // single most misleading sentence available here, because plenty matched and every one of them was\n // switched off.\n if (input.reviewersSuppressed) return '\\n' + this.suppressionBanner(input);\n if (input.applicableCount === 0) return '\\n' + this.checklistNotice.build(input.definedCount);\n const lines: string[] = [];\n // The prohibition rides on the REUSE line itself, not only in the all-clear below, because the\n // all-clear is not printed when anything is still owed — and \"some reviewers are reused, others\n // must be spawned\" is exactly the shape in which an agent re-spawns the reused ones too.\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch; verdict STANDS, do NOT re-spawn (review-${r.id}.json)`);\n }\n // A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this\n // reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of\n // correcting the file sitting right there.\n for (const e of input.formatErrors) lines.push(` ⛔ ${e}`);\n lines.push(...this.skippedLines(input));\n if (this.actionableOwed(input).length === 0) lines.push('', this.allClear(input));\n if (lines.length === 0) return '';\n return '\\n' + lines.join('\\n') + '\\n';\n }\n\n /**\n * THE LOUD BANNER. The one output in this whole flow that says an unreviewed PR is about to be posted.\n *\n * Everything in it is there because a reader has to be able to act on it or audit it: the FLAG name and\n * the FILE it was read from (the only actionable thing — nothing in any repo turns this off), the COUNT\n * split by required/optional, and every suppressed checklist BY NAME with REQUIRED marked. A count\n * alone would leave \"which required reviewer did I just skip?\" unanswerable at the one moment it is\n * being answered.\n *\n * It deliberately does NOT name `wp-finish-upsert-pr` — the closing step already does, exactly once,\n * and that \"exactly once\" is an invariant this class's docstring records having been bought with a bug.\n */\n private suppressionBanner(input: ReviewReportInput): string {\n const required = input.suppressed.filter((r: RequiredChecklist): boolean => r.required);\n const lines: string[] = [\n '⚫ ALL REVIEWER SUBAGENTS ARE SWITCHED OFF ON THIS MACHINE. Nothing reviewed this branch.',\n '',\n ` ${input.suppressed.length} checklist(s) matched this diff and were SUPPRESSED — `\n + `${required.length} of them REQUIRED:`,\n ];\n for (const r of input.suppressed) {\n lines.push(` • ${r.subagent}${r.required ? ' (REQUIRED — suppressed anyway)' : ' (optional)'}`);\n }\n lines.push(\n '',\n ` Switched off by: experimental.${HOME_KEY_TURN_OFF_ALL_REVIEWERS}: true`,\n ` Read from: ~/${HOME_CONFIG_DIR}/${HOME_CONFIG_FILE} (machine-local; no repo config can set this)`,\n '',\n ' There is NOTHING to spawn — no reviewer was briefed and no instructions file was written,',\n ' so any attempt to spawn one has nothing to read. Do NOT hand-write a verdict file in their',\n ' place: a fabricated verdict is worse than the absent one this flag deliberately produces.',\n '',\n ' The dashboard is the whole review product for this PR, and the suppression is carried into',\n ' the PR body — which is the squash-merge commit body — so main\\'s history records it.',\n ' To get the reviewers back, set that key to false (or delete it) and re-run this command.',\n );\n return lines.join('\\n') + '\\n';\n }\n\n /**\n * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the\n * whole reason the human is allowed to skip these is that they know this diff, and the only way they can\n * catch \"wait, not THAT one\" is to see which ones went unreviewed.\n */\n private skippedLines(input: ReviewReportInput): string[] {\n const skipped = this.optionalOwed(input);\n if (!input.skipOptional || skipped.length === 0) return [];\n return [\n '',\n ` ⏭️ ${skipped.length} OPTIONAL checklist(s) matched this diff and were SKIPPED (--no-optional):`,\n ...skipped.map((b: ReviewerBriefing): string => ` ${b.subagent} — ${this.why(b)}`),\n ' Not blocking. Drop the flag and re-run this command to offer them after all.',\n ];\n }\n\n /**\n * The all-clear, then the RULE that makes it actionable.\n *\n * \"nothing to spawn\" on its own is a description of the current state, and an agent that has just been\n * told a state — rather than a rule — treats re-spawning as a judgement call it is entitled to make. It\n * then makes it, reasoning (correctly, on the facts) that the carried-forward verdicts judged an earlier\n * tree. Reviews here are once per branch BY CONSTRUCTION: a passing review-<id>.json satisfies its\n * checklist for the branch's whole life, and `wp-finish-upsert-pr` never archives one the way it archives\n * review.json. So the reuse is deliberate — it is what keeps post-PR iteration from re-paying for every\n * matched reviewer — and the output has to say so, because the alternative reading is the expensive one.\n *\n * The overwrite warning is not decoration. A re-spawned reviewer writes to the SAME verdict path, so a\n * gratuitous re-run does not merely cost a subagent — it destroys the verdict that was already banked.\n *\n * It must NOT claim everything was reviewed when optional reviews were skipped — that is the one sentence\n * that would turn a deliberate skip into a false record of a review that happened.\n */\n private allClear(input: ReviewReportInput): string {\n const headline = input.skipOptional && this.optionalOwed(input).length > 0\n ? '✅ Nothing left to spawn — every REQUIRED checklist is reviewed (optional ones skipped above).'\n : '✅ Every checklist that applies is already reviewed — nothing to spawn.';\n return headline + '\\n' + this.oncePerBranchRule();\n }\n\n /**\n * The once-per-branch rule, stated as a prohibition rather than left to be inferred from a ✅.\n *\n * Printed whenever verdicts are being reused — which is every iteration of a PR after the first, i.e. the\n * majority of stage-② runs on any branch that gets review feedback.\n */\n private oncePerBranchRule(): string {\n return (\n ' Reviews here are ONCE PER BRANCH: a passing verdict carries forward to every later iteration\\n' +\n ' of this PR, deliberately, so post-PR edits cost no reviewer tokens. Do NOT re-spawn a reviewer\\n' +\n ' listed above to \"re-check\" the newer code — it burns a full subagent run AND overwrites the\\n' +\n ' verdict file it already wrote. The only reviewers you may spawn are ones a STEP below names.'\n );\n }\n\n /**\n * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked\n * (\"Then… Finally…\") so that skipping step 1 is visibly skipping a step, and worded so no earlier line\n * can be mistaken for the real next action.\n *\n * review.json is STEP 1 and the spawn blocks are STEP 2 — that ORDER is the contract, not a preference.\n * This block used to print the spawn blocks first and then say to write review.json \"WHILE any reviewer\n * subagents above are still running\", which does not merely permit spawning first, it instructs it.\n * Harmless for a reviewer that only reads the diff; wrong for one that judges the PR's stated INTENT —\n * its title, summary or risk level — because review.json is the only place that intent lives. Such a\n * reviewer either finds no file (a false RED and a wasted reviewer run) or, on a second run of this\n * stage on the same branch, finds the PREVIOUS run's file and validates a title that no longer exists —\n * a false GREEN, with nothing in the output saying which of the two happened. A consuming repo had to\n * write itself a rule telling its agents to DISOBEY this block to work around it.\n *\n * What the old ordering bought was overlap on a single local file write, not a subagent round-trip.\n *\n * The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here\n * can never drift from the shape `wp-finish-upsert-pr` validates.\n */\n private nextSteps(input: ReviewReportInput): string {\n if (input.singleRoundRepeat) return this.singleRoundRepeatStep();\n const required = this.requiredOwed(input);\n const offerable = this.offerableOwed(input);\n // Numbered by what is actually PRINTED, so the numbers a reader sees are 1..n with no gaps: write\n // review.json, then a spawn step only if anything must run, then an offer step only if anything may.\n let step = 1;\n const write = this.writeReviewStep(\n input.reviewPath, step++, input.singleRoundReview ? SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS : '');\n // The wait block goes under the LAST reviewer-listing step, and only there. Printed under both\n // it would be two \"what to do next\" instructions in one output, which is the defect this\n // method's docstring describes — an agent reading top to bottom obeys the first one it meets.\n const spawn = required.length === 0 ? '' : this.spawnStep(input, required, step++, offerable.length === 0);\n const offer = offerable.length === 0 ? '' : this.offerStep(input, offerable, step++, true);\n return '\\n' + SEP\n + `▶ NEXT — ${step} steps, in this order. Step 1 is NOT optional:\\n` + SEP + '\\n'\n + write + spawn + offer + this.finishStep(step, required.length + offerable.length > 0);\n }\n\n private writeReviewStep(reviewPath: string, step: number, mainAgentInstructions: string): string {\n return (\n `STEP ${step} — review your own changes, then write the review file. Write it FIRST — BEFORE you spawn\\n` +\n ' anything below. finish REFUSES without it, and a reviewer subagent may READ it: a\\n' +\n ' checklist that judges the PR title, summary or risk level reads exactly this file, so\\n' +\n ' writing it afterwards races that reviewer into seeing nothing — or, on a re-run of this\\n' +\n ' stage, into judging the PREVIOUS run\\'s review of code that has since changed.\\n\\n' +\n reviewJsonSchemaHint(reviewPath, mainAgentInstructions) + '\\n\\n'\n );\n }\n\n private singleRoundRepeatStep(): string {\n return '\\n' + SEP\n + '▶ NEXT — reviewers have already run once. SKIP reviews and run:\\n' + SEP + '\\n'\n + ' pnpm wp-finish-upsert-pr\\n\\n'\n + ' Do NOT invoke or re-run reviewer subagents. They are expensive and their one verdict\\n'\n + ' remains the review record for this PR. If a red remains, finish will block and tell you\\n'\n + ' how to fix it without another reviewer round.\\n\\n';\n }\n\n /**\n * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is\n * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything\n * restated here is a second copy that can go stale — which is exactly how a removed `success` field\n * outlived its own removal in print.\n *\n * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is\n * the repo saying the decision was already made; putting them to the human again would re-open a question\n * the config exists to settle.\n */\n private spawnStep(input: ReviewReportInput, owed: readonly ReviewerBriefing[], step: number, withAwait: boolean): string {\n const lines: string[] = [\n `STEP ${step} — only once that file is written, spawn these ${owed.length} REQUIRED reviewer subagent(s) — a`,\n ' SEPARATE one each. They block the PR, so do NOT ask whether to run them. You may NOT',\n ' review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '',\n ];\n lines.push(...this.refusedWarning(input, owed));\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n if (withAwait) lines.push(...this.awaitLines());\n lines.push('');\n return lines.join('\\n');\n }\n\n /**\n * How to WAIT once the spawn list has been spawned — printed after the blocks, because it is the\n * next thing to do and nothing before it can be mistaken for it.\n *\n * It is here because the alternative is measured and expensive: `echo .` every three seconds at\n * ~557,000 tokens a turn, 18.3% of every token the fleet spent in the 24h to 2026-09-07 (#874).\n *\n * It names the efficient options and the wasteful one, and then stops (#902). It does NOT prescribe\n * ending the turn: waiting on the subagents you just spawned is something an agent already does\n * routinely, and whether to do that here is a judgement this string cannot make for it.\n *\n * It names no other stage. `finishStep` below is the ONE place this whole block names\n * `wp-finish-upsert-pr`, and a second mention here would be a second \"what to do next\" instruction\n * for an agent reading top to bottom — the exact defect the class docstring above describes.\n */\n private awaitLines(): string[] {\n return [\n ' Then WAIT. Be efficient with tokens: wait on the subagents you just spawned, or block',\n ' in one call with the command below. Do NOT send status checks every few seconds, and',\n ' do NOT run `echo` to keep your turn alive — a turn costs your whole context, ~557k',\n ' tokens.',\n '',\n ' pnpm wp-await-reviews',\n '',\n ' It heartbeats while it waits, returns as soon as the last verdict lands, and prints',\n ' what each reviewer said. If the wait is long it exits asking to be run again.',\n ];\n }\n\n /**\n * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.\n *\n * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a\n * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party\n * who can judge whether this particular diff is worth them is the human looking at it.\n *\n * ONE batched multi-select question, explicitly. Asked one at a time, a human answering \"no\" nine times\n * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to\n * everything — which is the state this feature exists to leave. The \"None\" option has to be spelled out\n * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.\n *\n * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`\n * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then\n * shrugging off a red verdict would make the whole exercise theater.\n */\n private offerStep(input: ReviewReportInput, offerable: readonly ReviewerBriefing[], step: number, withAwait: boolean): string {\n const lines: string[] = [\n `STEP ${step} — these ${offerable.length} OPTIONAL review checklist(s) matched this diff. They do NOT block the`,\n ' PR, and you may NOT decide for the human whether to run them.',\n '',\n ' ASK THE HUMAN, in ONE multi-select question listing all of them plus an explicit',\n ' \"None — required only\" choice. Do not ask one question per reviewer. Then spawn ONLY',\n ' what they picked, the same way as any other reviewer.',\n '',\n ' If they pick none, that is a complete answer: go straight to the final step. If they',\n ' told you up front to submit without reviews, re-run this stage as',\n ' `pnpm wp-review-upsert-pr --no-optional` and this step disappears.',\n '',\n ' NOTE: whichever ones you DO run, their verdicts count in full — a red verdict from an',\n ' optional reviewer blocks the PR exactly like a required one.',\n '',\n ];\n lines.push(...this.refusedWarning(input, offerable));\n for (const b of offerable) lines.push(...this.oneSpawnBlock(input, b));\n if (withAwait) lines.push(...this.awaitLines());\n lines.push('');\n return lines.join('\\n');\n }\n\n // Said up front, not only beside the block: an agent that has decided to spawn everything listed here\n // needs to know BEFORE it starts that one of these entries is not a spawn-shaped task. Scoped to the\n // group being printed — a refusal among the REQUIRED reviewers is not a caveat on the optional list.\n private refusedWarning(input: ReviewReportInput, group: readonly ReviewerBriefing[]): string[] {\n const ids = new Set(group.map((b: ReviewerBriefing): string => b.checklistId));\n const n = input.refused.filter((r: RefusedReviewer): boolean => ids.has(r.checklistId)).length;\n if (n === 0) return [];\n return [\n ` ${n} of them already ANSWERED and refused (marked ⛔ below). Do not spawn`,\n ' those against unchanged code — fix what they found first; the fix is the prerequisite.',\n '',\n ];\n }\n\n private finishStep(stepNumber: number, anyReviewers: boolean): string {\n // \"every reviewer you ran\" rather than \"every reviewer above\": with an optional list the human may\n // legitimately have run none of them, and a precondition naming reviewers that were declined reads as\n // an unmeetable one.\n const precondition = anyReviewers\n ? 'once every reviewer you ran has written its verdict file'\n : 'once that file exists';\n return (\n `STEP ${stepNumber} — only ${precondition}, run: pnpm wp-finish-upsert-pr\\n` +\n ' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\\n\\n'\n );\n }\n\n // The briefings with no passing verdict yet — the ONE definition of \"owed\", shared by the header, the\n // scan verdict and the step numbering, so they cannot disagree about whether there is anything to spawn.\n private owedReviewers(input: ReviewReportInput): ReviewerBriefing[] {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n return input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n }\n\n // Owed AND blocking — spawned without asking.\n private requiredOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => b.required);\n }\n\n // Owed and optional. Still listed under `--no-optional` (as a skip verdict), just never as a step.\n private optionalOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return this.owedReviewers(input).filter((b: ReviewerBriefing): boolean => !b.required);\n }\n\n // The optional ones the human is actually to be ASKED about — none, once they have already answered.\n private offerableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return input.skipOptional ? [] : this.optionalOwed(input);\n }\n\n // Everything the AI still has to act on. Distinct from `owedReviewers`: a skipped optional checklist is\n // owed a verdict it will never get, and treating it as pending work is what would print a spawn\n // instruction for a review the human just declined.\n private actionableOwed(input: ReviewReportInput): ReviewerBriefing[] {\n return [...this.requiredOwed(input), ...this.offerableOwed(input)];\n }\n\n /**\n * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different\n * lead-in, because the action before spawning is different: its own words are printed, and the spawn is\n * explicitly conditioned on having fixed the finding first.\n *\n * It keeps its spawn block rather than being dropped from the list, because the reviewer genuinely does\n * still owe a fresh verdict — dropping it would leave nothing anywhere saying how to get one. What must\n * not happen is a bare \"spawn this\" that reads identically to a reviewer that never ran, which is the\n * loop this exists to break.\n */\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const instructionsFile = this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent);\n return [\n ...this.leadIn(input, b),\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${instructionsFile}`,\n '',\n ];\n }\n\n // The lines above the spawn coordinates: normally just why this reviewer is in scope; for one that\n // already refused, its verdict verbatim plus the order the two actions must happen in.\n private leadIn(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n const refusal = input.refused.find((r: RefusedReviewer): boolean => r.checklistId === b.checklistId);\n if (!refusal) return [` ▶ ${b.subagent} — ${this.why(b)}`, ...this.docLine(b)];\n return [\n ` ⛔ ${b.subagent} — ALREADY REVIEWED THIS BRANCH AND REFUSED. It will refuse again on unchanged code.`,\n ` ${refusal.message}`,\n ' FIX THE FINDING FIRST (or record a human-authored override). ONLY THEN spawn it again, to',\n ' write a fresh verdict:',\n ];\n }\n\n /**\n * The checklist's guidance doc, for OPTIONAL reviewers only.\n *\n * \"4 file(s) matched\" plus a broad glob does not tell a human what the review would actually look AT,\n * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision\n * to inform there — the reviewer runs either way, and the doc is already in its instructions file.\n */\n private docLine(b: ReviewerBriefing): string[] {\n if (b.required || b.docPath === '') return [];\n return [` reviews against: ${b.docPath}`];\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole\n // diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.\n private why(b: ReviewerBriefing): string {\n if (b.matchedPatterns.length === 0) {\n return `ALWAYS RUNS (no \"patterns\" configured), whole diff in scope — ${b.myFiles.length} file(s)`;\n }\n return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ')}`;\n }\n}\n"]}
@@ -54,9 +54,9 @@ let ReviewerVerdictGate = class ReviewerVerdictGate {
54
54
  const refusals = refused.map((req) => this.retireAndReport(scan, req));
55
55
  throw new rules_config_1.InformAiError(this.headline(scan, refused, neverRan)
56
56
  + this.unreadableSection(scan)
57
- + this.refusedSection(refusals)
57
+ + this.refusedSection(refusals, scan.singleRoundReview)
58
58
  + this.neverRanSection(scan, neverRan)
59
- + this.footer(refused.length > 0));
59
+ + this.footer(refused.length > 0, scan.singleRoundReview));
60
60
  }
61
61
  /**
62
62
  * Say plainly how many REFUSED and how many never ran.
@@ -105,9 +105,14 @@ let ReviewerVerdictGate = class ReviewerVerdictGate {
105
105
  * The refusals — SECOND, above anything that says to spawn a subagent, because a spawn instruction is the
106
106
  * one line an agent acts on first, and acting on it here IS the loop.
107
107
  */
108
- refusedSection(refusals) {
108
+ refusedSection(refusals, singleRoundReview) {
109
109
  if (refusals.length === 0)
110
110
  return '';
111
+ if (singleRoundReview) {
112
+ return '⛔ REFUSED — SINGLE-ROUND MODE. These reviewers ALREADY ANSWERED and MUST NOT be rerun.\n\n'
113
+ + refusals.map((r) => ` • ${r}`).join('\n\n') + '\n\n'
114
+ + ` main_agent_instructions: ${rules_config_1.SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS}\n\n`;
115
+ }
111
116
  return '⛔ REFUSED — these reviewers ALREADY ANSWERED. Re-spawning one against unchanged code only buys\n'
112
117
  + ' the same answer: fix what it found (or get a HUMAN to authorize an override), THEN review again.\n\n'
113
118
  + refusals.map((r) => ` • ${r}`).join('\n\n') + '\n\n';
@@ -138,6 +143,14 @@ let ReviewerVerdictGate = class ReviewerVerdictGate {
138
143
  */
139
144
  retireAndReport(scan, req) {
140
145
  const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);
146
+ if (scan.singleRoundReview) {
147
+ const live = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);
148
+ return `Checklist "${req.id}" FAILED review (status:"red"). The reviewer wrote:\n`
149
+ + ` ${verdict.detail}\n`
150
+ + ` DO NOT RERUN this reviewer. After fixing every finding, edit ${live}.\n`
151
+ + ' Change its addressed red status to "yellow". Yellow is accepted by the gate. If you genuinely\n'
152
+ + ' disagree, leave it red and flag the human BEFORE posting the PR; red remains blocking.';
153
+ }
141
154
  return this.reviewJsonService.refusalError(req, verdict, scan.reviewPath, this.archiveOrWarn(scan.reviewPath, req.id));
142
155
  }
143
156
  // The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).
@@ -157,8 +170,13 @@ let ReviewerVerdictGate = class ReviewerVerdictGate {
157
170
  // Re-running stage 2 is normally optional. It stops being optional after a refusal, because the only way
158
171
  // past one is to CHANGE the code — which is exactly the condition that makes the extracted diff and every
159
172
  // reviewer briefing stale.
160
- footer(anyRefused) {
173
+ footer(anyRefused, singleRoundReview) {
161
174
  if (anyRefused) {
175
+ if (singleRoundReview) {
176
+ return 'Do NOT run pnpm wp-review-upsert-pr and do NOT re-run a reviewer. Fix the findings, change\n'
177
+ + 'addressed red verdicts to yellow, then run: pnpm wp-finish-upsert-pr\n'
178
+ + 'If any red remains because you disagree, STOP and ask the human before posting the PR.';
179
+ }
162
180
  return 'Then re-run: pnpm wp-review-upsert-pr (the code changed, so the extracted diff and the reviewer\n'
163
181
  + 'briefings are stale), re-run the reviewer(s) above, and finally: pnpm wp-finish-upsert-pr';
164
182
  }
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer-verdict-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-verdict-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChecklistInstructionsService, InformAiError, RequiredChecklist, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * The gate `wp-finish-upsert-pr` runs before it parses review.json: REFUSE the PR while any applicable\n * checklist is not clear, and say — per checklist — which of three different things went wrong.\n *\n * It is its own class, not a private method on the command, for two reasons. It is the one piece of finish\n * that both MUTATES the branch (it retires red verdicts) and produces the text an AI acts on, so it is the\n * piece most worth asserting on directly; and the command it came out of is already the largest file in this\n * package, against a hard `@webpieces/max-file-lines` limit.\n *\n * THE BUG THIS EXISTS TO FIX. The message used to be one bucket and one imperative — \"You MUST run these N\n * reviewer subagent(s)\" — for three unrelated states. Handed to an agent, the literal, obedient response for\n * a checklist that had ALREADY REFUSED was to spawn the reviewer again; it re-read the same unchanged code,\n * refused again, and the loop cost a full subagent run per pass while the reviewer's actual finding was\n * never printed at all. Three states, three actions, in this order:\n *\n * 1. UNREADABLE (`scan.formatErrors`) -> fix four characters of JSON. Not a reviewer problem.\n * 2. REFUSED (CK_FAIL) -> fix the finding (or get a HUMAN override). Not a missing step.\n * 3. NEVER RAN (the rest) -> spawn the subagent. The ONLY case where that is the right move.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerVerdictGate {\n constructor(\n private readonly reviewJsonService: ReviewJsonService,\n private readonly instructions: ChecklistInstructionsService,\n ) {}\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a passing verdict — naming exactly what each\n * one needs. No-op for a repo with no applicable checklists.\n *\n * SIDE EFFECT, deliberately: every REFUSED checklist's verdict file is RETIRED to review-<id>.json.old on\n * the way out (see {@link retireAndReport}). Only red verdicts are ever moved — a green, yellow or\n * overridden one is reused across finish attempts and retiring it would force a needless subagent re-run.\n */\n assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // The split is computed from the verdicts the SCAN already loaded, never from a second read of disk:\n // a re-read here could disagree with the set that produced `outstanding`, and a checklist that is\n // \"outstanding\" per one read and \"clear\" per another belongs to neither section of the message.\n const refused = this.reviewJsonService.refusedChecklists(scan.outstanding, scan.results);\n const refusedIds = new Set(refused.map((r: RequiredChecklist): string => r.id));\n const neverRan = scan.outstanding.filter((r: RequiredChecklist): boolean => !refusedIds.has(r.id));\n // Rendered BEFORE the throw and, per checklist, with the verdict resolved before its file is moved —\n // the message quotes the reviewer's own words, so losing them to the move would defeat the point.\n const refusals = refused.map((req: RequiredChecklist): string => this.retireAndReport(scan, req));\n throw new InformAiError(\n this.headline(scan, refused, neverRan)\n + this.unreadableSection(scan)\n + this.refusedSection(refusals)\n + this.neverRanSection(scan, neverRan)\n + this.footer(refused.length > 0),\n );\n }\n\n /**\n * Say plainly how many REFUSED and how many never ran.\n *\n * The old headline said all of them \"have no passing verdict yet\", which is technically true of a refusal\n * and reads as though nobody had looked. It is the sentence that framed a decision as an omission.\n */\n private headline(scan: ChecklistScan, refused: readonly RequiredChecklist[], neverRan: readonly RequiredChecklist[]): string {\n const lines = [\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this branch are not clear:`,\n ];\n if (refused.length > 0) {\n lines.push(` • ${refused.length} REFUSED — a reviewer ran, judged this change, and said no: ${this.instructions.names(refused)}`);\n }\n if (neverRan.length > 0) {\n lines.push(` • ${neverRan.length} never ran — no verdict has been written yet: ${this.instructions.names(neverRan)}`);\n }\n return lines.join('\\n') + this.skippedNote(scan) + '\\n\\n';\n }\n\n /**\n * The optional checklists nobody ran, stated OUT LOUD on the refusal.\n *\n * They are not why the PR is being refused and this block says so — but a message that lists only what\n * blocked reads as \"everything else was reviewed\", and here that is false: those reviews were declined,\n * not passed. The one moment a human is looking at the review state is the moment to be honest about\n * which reviews did not happen, and it is also the prompt an AI needs to offer them again if the human\n * has changed their mind now that something else came back red.\n */\n private skippedNote(scan: ChecklistScan): string {\n if (scan.optionalNotRun.length === 0) return '';\n return `\\n (Not blocking: ${scan.optionalNotRun.length} OPTIONAL checklist(s) were not run — `\n + `${this.instructions.names(scan.optionalNotRun)}. They do not have to run; re-run\\n`\n + ' pnpm wp-review-upsert-pr if you now want to offer them.)';\n }\n\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success` format\n // is otherwise indistinguishable from one that never ran, and the AI would go re-run a subagent instead\n // of correcting four characters of JSON.\n private unreadableSection(scan: ChecklistScan): string {\n if (scan.formatErrors.length === 0) return '';\n return `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n`\n + scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n }\n\n /**\n * The refusals — SECOND, above anything that says to spawn a subagent, because a spawn instruction is the\n * one line an agent acts on first, and acting on it here IS the loop.\n */\n private refusedSection(refusals: readonly string[]): string {\n if (refusals.length === 0) return '';\n return '⛔ REFUSED — these reviewers ALREADY ANSWERED. Re-spawning one against unchanged code only buys\\n'\n + ' the same answer: fix what it found (or get a HUMAN to authorize an override), THEN review again.\\n\\n'\n + refusals.map((r: string): string => ` • ${r}`).join('\\n\\n') + '\\n\\n';\n }\n\n /**\n * The reviewers that genuinely never ran — LAST, and listing ONLY these. When there are none this block\n * is absent entirely, which is the whole point: the \"You MUST run these N reviewer subagent(s)\"\n * imperative must not appear at all on a run whose only problem is a refusal.\n */\n private neverRanSection(scan: ChecklistScan, neverRan: readonly RequiredChecklist[]): string {\n if (neverRan.length === 0) return '';\n return '❓ NO VERDICT YET — nothing has been written for these, so they must actually be run:\\n\\n'\n + `${this.instructions.render(neverRan, scan.reviewPath, scan.context)}\\n\\n`;\n }\n\n /**\n * Retire ONE refused checklist's verdict and render the refusal that reports it.\n *\n * ORDER IS LOAD-BEARING. The verdict is resolved from the already-loaded results, so the finding is in\n * hand before the file moves; the move then happens; and only then is the message rendered, because it\n * names the archive path and must not claim a move that did not happen.\n *\n * The move is NON-FATAL. A finish that died after retiring a verdict would leave the AI with a red\n * verdict gone from the live path and no message saying where it went or why the PR was refused — a\n * silent gap, and the worst possible outcome for a feature whose entire purpose is a durable record. So a\n * failed move warns and the refusal is reported without an archive path (the file is then still live,\n * which the un-archived wording of `refusalError` describes correctly).\n */\n private retireAndReport(scan: ChecklistScan, req: RequiredChecklist): string {\n const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);\n return this.reviewJsonService.refusalError(\n req, verdict, scan.reviewPath, this.archiveOrWarn(scan.reviewPath, req.id));\n }\n\n // The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).\n private archiveOrWarn(reviewPath: string, checklistId: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must never swallow the refusal it belongs to\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.reviewJsonService.archiveChecklistResult(reviewPath, checklistId);\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(\n `⚠️ Could not retire the refused verdict for \"${checklistId}\" (non-fatal — the refusal is still `\n + `reported below): ${error.message}\\n`);\n return '';\n }\n }\n\n // Re-running stage 2 is normally optional. It stops being optional after a refusal, because the only way\n // past one is to CHANGE the code — which is exactly the condition that makes the extracted diff and every\n // reviewer briefing stale.\n private footer(anyRefused: boolean): string {\n if (anyRefused) {\n return 'Then re-run: pnpm wp-review-upsert-pr (the code changed, so the extracted diff and the reviewer\\n'\n + 'briefings are stale), re-run the reviewer(s) above, and finally: pnpm wp-finish-upsert-pr';\n }\n return 'Then re-run: pnpm wp-finish-upsert-pr\\n'\n + '(Each reviewer\\'s generated instructions file is already written — re-running pnpm wp-review-upsert-pr\\n'\n + ' is only needed if the code changed since it ran.)';\n }\n}\n"]}
1
+ {"version":3,"file":"reviewer-verdict-gate.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-verdict-gate.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChecklistInstructionsService, InformAiError, RequiredChecklist, ReviewJsonService,\n SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * The gate `wp-finish-upsert-pr` runs before it parses review.json: REFUSE the PR while any applicable\n * checklist is not clear, and say — per checklist — which of three different things went wrong.\n *\n * It is its own class, not a private method on the command, for two reasons. It is the one piece of finish\n * that both MUTATES the branch (it retires red verdicts) and produces the text an AI acts on, so it is the\n * piece most worth asserting on directly; and the command it came out of is already the largest file in this\n * package, against a hard `@webpieces/max-file-lines` limit.\n *\n * THE BUG THIS EXISTS TO FIX. The message used to be one bucket and one imperative — \"You MUST run these N\n * reviewer subagent(s)\" — for three unrelated states. Handed to an agent, the literal, obedient response for\n * a checklist that had ALREADY REFUSED was to spawn the reviewer again; it re-read the same unchanged code,\n * refused again, and the loop cost a full subagent run per pass while the reviewer's actual finding was\n * never printed at all. Three states, three actions, in this order:\n *\n * 1. UNREADABLE (`scan.formatErrors`) -> fix four characters of JSON. Not a reviewer problem.\n * 2. REFUSED (CK_FAIL) -> fix the finding (or get a HUMAN override). Not a missing step.\n * 3. NEVER RAN (the rest) -> spawn the subagent. The ONLY case where that is the right move.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerVerdictGate {\n constructor(\n private readonly reviewJsonService: ReviewJsonService,\n private readonly instructions: ChecklistInstructionsService,\n ) {}\n\n /**\n * Refuse the PR while ANY applicable checklist still owes a passing verdict — naming exactly what each\n * one needs. No-op for a repo with no applicable checklists.\n *\n * SIDE EFFECT, deliberately: every REFUSED checklist's verdict file is RETIRED to review-<id>.json.old on\n * the way out (see {@link retireAndReport}). Only red verdicts are ever moved — a green, yellow or\n * overridden one is reused across finish attempts and retiring it would force a needless subagent re-run.\n */\n assertEveryReviewerRan(scan: ChecklistScan): void {\n if (scan.outstanding.length === 0) return;\n // The split is computed from the verdicts the SCAN already loaded, never from a second read of disk:\n // a re-read here could disagree with the set that produced `outstanding`, and a checklist that is\n // \"outstanding\" per one read and \"clear\" per another belongs to neither section of the message.\n const refused = this.reviewJsonService.refusedChecklists(scan.outstanding, scan.results);\n const refusedIds = new Set(refused.map((r: RequiredChecklist): string => r.id));\n const neverRan = scan.outstanding.filter((r: RequiredChecklist): boolean => !refusedIds.has(r.id));\n // Rendered BEFORE the throw and, per checklist, with the verdict resolved before its file is moved —\n // the message quotes the reviewer's own words, so losing them to the move would defeat the point.\n const refusals = refused.map((req: RequiredChecklist): string => this.retireAndReport(scan, req));\n throw new InformAiError(\n this.headline(scan, refused, neverRan)\n + this.unreadableSection(scan)\n + this.refusedSection(refusals, scan.singleRoundReview)\n + this.neverRanSection(scan, neverRan)\n + this.footer(refused.length > 0, scan.singleRoundReview),\n );\n }\n\n /**\n * Say plainly how many REFUSED and how many never ran.\n *\n * The old headline said all of them \"have no passing verdict yet\", which is technically true of a refusal\n * and reads as though nobody had looked. It is the sentence that framed a decision as an omission.\n */\n private headline(scan: ChecklistScan, refused: readonly RequiredChecklist[], neverRan: readonly RequiredChecklist[]): string {\n const lines = [\n `⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this branch are not clear:`,\n ];\n if (refused.length > 0) {\n lines.push(` • ${refused.length} REFUSED — a reviewer ran, judged this change, and said no: ${this.instructions.names(refused)}`);\n }\n if (neverRan.length > 0) {\n lines.push(` • ${neverRan.length} never ran — no verdict has been written yet: ${this.instructions.names(neverRan)}`);\n }\n return lines.join('\\n') + this.skippedNote(scan) + '\\n\\n';\n }\n\n /**\n * The optional checklists nobody ran, stated OUT LOUD on the refusal.\n *\n * They are not why the PR is being refused and this block says so — but a message that lists only what\n * blocked reads as \"everything else was reviewed\", and here that is false: those reviews were declined,\n * not passed. The one moment a human is looking at the review state is the moment to be honest about\n * which reviews did not happen, and it is also the prompt an AI needs to offer them again if the human\n * has changed their mind now that something else came back red.\n */\n private skippedNote(scan: ChecklistScan): string {\n if (scan.optionalNotRun.length === 0) return '';\n return `\\n (Not blocking: ${scan.optionalNotRun.length} OPTIONAL checklist(s) were not run — `\n + `${this.instructions.names(scan.optionalNotRun)}. They do not have to run; re-run\\n`\n + ' pnpm wp-review-upsert-pr if you now want to offer them.)';\n }\n\n // Unreadable verdict files come FIRST. A reviewer that wrote its verdict in the removed `success` format\n // is otherwise indistinguishable from one that never ran, and the AI would go re-run a subagent instead\n // of correcting four characters of JSON.\n private unreadableSection(scan: ChecklistScan): string {\n if (scan.formatErrors.length === 0) return '';\n return `${scan.formatErrors.length} verdict file(s) are in an UNREADABLE format:\\n\\n`\n + scan.formatErrors.map((e: string): string => ` • ${e}`).join('\\n') + '\\n\\n';\n }\n\n /**\n * The refusals — SECOND, above anything that says to spawn a subagent, because a spawn instruction is the\n * one line an agent acts on first, and acting on it here IS the loop.\n */\n private refusedSection(refusals: readonly string[], singleRoundReview: boolean): string {\n if (refusals.length === 0) return '';\n if (singleRoundReview) {\n return '⛔ REFUSED — SINGLE-ROUND MODE. These reviewers ALREADY ANSWERED and MUST NOT be rerun.\\n\\n'\n + refusals.map((r: string): string => ` • ${r}`).join('\\n\\n') + '\\n\\n'\n + ` main_agent_instructions: ${SINGLE_ROUND_MAIN_AGENT_INSTRUCTIONS}\\n\\n`;\n }\n return '⛔ REFUSED — these reviewers ALREADY ANSWERED. Re-spawning one against unchanged code only buys\\n'\n + ' the same answer: fix what it found (or get a HUMAN to authorize an override), THEN review again.\\n\\n'\n + refusals.map((r: string): string => ` • ${r}`).join('\\n\\n') + '\\n\\n';\n }\n\n /**\n * The reviewers that genuinely never ran — LAST, and listing ONLY these. When there are none this block\n * is absent entirely, which is the whole point: the \"You MUST run these N reviewer subagent(s)\"\n * imperative must not appear at all on a run whose only problem is a refusal.\n */\n private neverRanSection(scan: ChecklistScan, neverRan: readonly RequiredChecklist[]): string {\n if (neverRan.length === 0) return '';\n return '❓ NO VERDICT YET — nothing has been written for these, so they must actually be run:\\n\\n'\n + `${this.instructions.render(neverRan, scan.reviewPath, scan.context)}\\n\\n`;\n }\n\n /**\n * Retire ONE refused checklist's verdict and render the refusal that reports it.\n *\n * ORDER IS LOAD-BEARING. The verdict is resolved from the already-loaded results, so the finding is in\n * hand before the file moves; the move then happens; and only then is the message rendered, because it\n * names the archive path and must not claim a move that did not happen.\n *\n * The move is NON-FATAL. A finish that died after retiring a verdict would leave the AI with a red\n * verdict gone from the live path and no message saying where it went or why the PR was refused — a\n * silent gap, and the worst possible outcome for a feature whose entire purpose is a durable record. So a\n * failed move warns and the refusal is reported without an archive path (the file is then still live,\n * which the un-archived wording of `refusalError` describes correctly).\n */\n private retireAndReport(scan: ChecklistScan, req: RequiredChecklist): string {\n const verdict = this.reviewJsonService.resolveVerdict(req, scan.results);\n if (scan.singleRoundReview) {\n const live = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);\n return `Checklist \"${req.id}\" FAILED review (status:\"red\"). The reviewer wrote:\\n`\n + ` ${verdict.detail}\\n`\n + ` DO NOT RERUN this reviewer. After fixing every finding, edit ${live}.\\n`\n + ' Change its addressed red status to \"yellow\". Yellow is accepted by the gate. If you genuinely\\n'\n + ' disagree, leave it red and flag the human BEFORE posting the PR; red remains blocking.';\n }\n return this.reviewJsonService.refusalError(\n req, verdict, scan.reviewPath, this.archiveOrWarn(scan.reviewPath, req.id));\n }\n\n // The archive path, or '' when there was nothing to move or the move failed (see retireAndReport).\n private archiveOrWarn(reviewPath: string, checklistId: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must never swallow the refusal it belongs to\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return this.reviewJsonService.archiveChecklistResult(reviewPath, checklistId);\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(\n `⚠️ Could not retire the refused verdict for \"${checklistId}\" (non-fatal — the refusal is still `\n + `reported below): ${error.message}\\n`);\n return '';\n }\n }\n\n // Re-running stage 2 is normally optional. It stops being optional after a refusal, because the only way\n // past one is to CHANGE the code — which is exactly the condition that makes the extracted diff and every\n // reviewer briefing stale.\n private footer(anyRefused: boolean, singleRoundReview: boolean): string {\n if (anyRefused) {\n if (singleRoundReview) {\n return 'Do NOT run pnpm wp-review-upsert-pr and do NOT re-run a reviewer. Fix the findings, change\\n'\n + 'addressed red verdicts to yellow, then run: pnpm wp-finish-upsert-pr\\n'\n + 'If any red remains because you disagree, STOP and ask the human before posting the PR.';\n }\n return 'Then re-run: pnpm wp-review-upsert-pr (the code changed, so the extracted diff and the reviewer\\n'\n + 'briefings are stale), re-run the reviewer(s) above, and finally: pnpm wp-finish-upsert-pr';\n }\n return 'Then re-run: pnpm wp-finish-upsert-pr\\n'\n + '(Each reviewer\\'s generated instructions file is already written — re-running pnpm wp-review-upsert-pr\\n'\n + ' is only needed if the code changed since it ran.)';\n }\n}\n"]}