@webpieces/pr-gate 0.4.525 → 0.4.527
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 +2 -2
- package/src/scripts/commands/check-pr-command.js +2 -1
- package/src/scripts/commands/check-pr-command.js.map +1 -1
- package/src/scripts/commands/land-pr-command.js +3 -2
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/review-upsert-pr-command.d.ts +11 -12
- package/src/scripts/commands/review-upsert-pr-command.js +19 -58
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/start-update-command.js +3 -1
- package/src/scripts/commands/start-update-command.js.map +1 -1
- package/src/scripts/workflow/checklist-notice.d.ts +26 -8
- package/src/scripts/workflow/checklist-notice.js +47 -23
- package/src/scripts/workflow/checklist-notice.js.map +1 -1
- package/src/scripts/workflow/open-pr-check.js +2 -1
- package/src/scripts/workflow/open-pr-check.js.map +1 -1
- package/src/scripts/workflow/review-report.d.ts +61 -0
- package/src/scripts/workflow/review-report.js +147 -0
- package/src/scripts/workflow/review-report.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.527",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.527",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -88,7 +88,8 @@ let CheckPrCommand = class CheckPrCommand {
|
|
|
88
88
|
`This PR was NOT created through the webpieces gated flow — or was pushed after finishing without re-running it.\n` +
|
|
89
89
|
`Every commit that lands here must go through it, so:\n\n` +
|
|
90
90
|
` 1. Install the webpieces hooks if you don't have them.\n` +
|
|
91
|
-
` 2. Recreate/update this PR by running
|
|
91
|
+
` 2. Recreate/update this PR by running the full gated flow, in order:\n` +
|
|
92
|
+
` pnpm wp-start-upsert-pr → pnpm wp-review-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\n\n` +
|
|
92
93
|
`That re-stamps the PR title, body, and the gate token for the current head commit, and this check goes green.`);
|
|
93
94
|
}
|
|
94
95
|
// Resolve the PR (number, head sha, body) from `gh`. Prefers an explicit WP_PR_NUMBER, then the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/check-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport {\n loadAndValidate, RepoRootFinder, GateTokenService, CliExitError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// The head sha + body of the PR under check, resolved from `gh`. Data-only.\nclass PrUnderCheck {\n number: string;\n headSha: string;\n body: string;\n\n constructor(number: string, headSha: string, body: string) {\n this.number = number;\n this.headSha = headSha;\n this.body = body;\n }\n}\n\n/**\n * `wp-check-pr` — the SERVER-SIDE half of the gate, meant to run as a required CI check. It is READ-ONLY:\n * it never touches git state, never pushes, never calls `gh pr create`. It recomputes\n * `HMAC(prGate.gateSalt, PR_head_sha)` from the committed salt and verifies the PR body carries that\n * token. Because `wp-finish-upsert-pr` refuses to mint the token unless the build gate + every BLOCK\n * checklist passed, a valid token IS proof the gated flow ran and passed on this exact commit.\n *\n * This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and\n * clicked \"Create pull request\" in the web UI carries no valid token for its head sha and fails here.\n *\n * This job is THE required check — the ONE thing a consumer marks required in branch protection. It used\n * to also post a `webpieces/pr-gate` commit status, because `wp-finish-upsert-pr` pushed BEFORE writing\n * the PR body and a status (newest post wins for a context) could supersede the failure that race caused.\n * GatedPrPublisher now writes the body first, so there is no race to recover from and no second entry to\n * confuse anyone about which one to require. This job's exit code is the whole verdict.\n *\n * A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe\n * to add the workflow before turning enforcement on.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and resolved by PrGateApp.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gateTokenService: GateTokenService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n if (gateSalt.trim() === '') {\n throw new CliExitError(0,\n 'ℹ️ wp-check-pr: no prGate.gateSalt configured — server-side gate token enforcement is disabled. ' +\n 'Add a committed \"gateSalt\" under the pr-gate section of webpieces.config.json to enable it.');\n }\n\n let pr = this.resolvePr();\n if (pr.headSha === '') {\n throw new CliExitError(1,\n '❌ wp-check-pr: could not resolve the PR head sha via `gh`. Ensure the workflow runs on a pull_request ' +\n 'event with `gh` authenticated (GH_TOKEN) and the PR number available (WP_PR_NUMBER or GITHUB_REF).');\n }\n\n pr = await this.verifyWithRetry(pr, gateSalt);\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) {\n process.stdout.write(`✅ wp-check-pr: valid webpieces gate token for PR #${pr.number} @ ${pr.headSha.slice(0, 12)} — created through the gated flow.\\n`);\n return;\n }\n\n // An UNHOOKED push never reaches wp-finish-upsert-pr, so it carries no valid token — fail the job.\n // THIS EXIT CODE IS THE WHOLE SIGNAL: there is no companion commit status any more (see the JSDoc).\n throw new CliExitError(1, this.failureMessage(pr));\n }\n\n // Re-read the PR ONCE after a short delay if the token looks stale. The push-before-body-edit RACE is\n // gone (GatedPrPublisher writes the body first), so this no longer backstops a known ordering bug —\n // it absorbs GitHub's own event/read replication jitter, which matters more now that this job is the\n // single required check. It costs nothing on the success path. Returns the freshest PR.\n private async verifyWithRetry(pr: PrUnderCheck, gateSalt: string): Promise<PrUnderCheck> {\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) return pr;\n process.stdout.write('… no valid token yet — waiting for the PR body edit to land, then re-checking once…\\n');\n await this.delay(15000);\n const refreshed = this.resolvePr();\n return refreshed.headSha !== '' ? refreshed : pr;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve: () => void): void => {\n setTimeout(resolve, ms);\n });\n }\n\n // The actionable red-check message: this PR did not come through the gated flow (or hooks are missing).\n private failureMessage(pr: PrUnderCheck): string {\n return (\n `❌ wp-check-pr: PR #${pr.number} (head ${pr.headSha.slice(0, 12)}) has no valid webpieces gate token.\\n\\n` +\n `This PR was NOT created through the webpieces gated flow — or was pushed after finishing without re-running it.\\n` +\n `Every commit that lands here must go through it, so:\\n\\n` +\n ` 1. Install the webpieces hooks if you don't have them.\\n` +\n ` 2. Recreate/update this PR by running: pnpm wp-start-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\\n\\n` +\n `That re-stamps the PR title, body, and the gate token for the current head commit, and this check goes green.`\n );\n }\n\n // Resolve the PR (number, head sha, body) from `gh`. Prefers an explicit WP_PR_NUMBER, then the\n // pull_request number in GITHUB_REF (refs/pull/<n>/merge), then `gh`'s current-branch detection.\n private resolvePr(): PrUnderCheck {\n const num = this.prNumber();\n const args = num !== ''\n ? ['pr', 'view', num, '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"']\n : ['pr', 'view', '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"'];\n const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 16 });\n if (result.status !== 0) return new PrUnderCheck(num, '', '');\n // jq joins body (which may contain tabs/newlines) last, so split on the FIRST two tabs only.\n const out = (result.stdout ?? '').trim();\n const firstTab = out.indexOf('\\t');\n const secondTab = firstTab >= 0 ? out.indexOf('\\t', firstTab + 1) : -1;\n if (firstTab < 0 || secondTab < 0) return new PrUnderCheck(num, '', '');\n const number = out.slice(0, firstTab);\n const headSha = out.slice(firstTab + 1, secondTab);\n const body = out.slice(secondTab + 1);\n return new PrUnderCheck(number, headSha, body);\n }\n\n private prNumber(): string {\n const explicit = (process.env['WP_PR_NUMBER'] ?? '').trim();\n if (explicit !== '') return explicit;\n // GitHub Actions pull_request: GITHUB_REF = refs/pull/<n>/merge\n const ref = process.env['GITHUB_REF'] ?? '';\n const m = /refs\\/pull\\/(\\d+)\\//.exec(ref);\n return m ? m[1] : '';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"check-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/check-pr-command.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAEiC;AACjC,yCAA2D;AAE3D,4EAA4E;AAC5E,MAAM,YAAY;IACd,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,IAAI,CAAS;IAEb,YAAY,MAAc,EAAE,OAAe,EAAE,IAAY;QACrD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IAEF;IACA;IAFrB,YACqB,cAA8B,EAC9B,gBAAkC;QADlC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,qBAAgB,GAAhB,gBAAgB,CAAkB;IACpD,CAAC;IAEJ,KAAK,CAAC,GAAG;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,MAAM,QAAQ,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;QAC3D,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,mGAAmG;gBACnG,6FAA6F,CAAC,CAAC;QACvG,CAAC;QAED,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1B,IAAI,EAAE,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;YACpB,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,wGAAwG;gBACxG,oGAAoG,CAAC,CAAC;QAC9G,CAAC;QAED,EAAE,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;YACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qDAAqD,EAAE,CAAC,MAAM,MAAM,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,sCAAsC,CAAC,CAAC;YACxJ,OAAO;QACX,CAAC;QAED,mGAAmG;QACnG,oGAAoG;QACpG,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,sGAAsG;IACtG,oGAAoG;IACpG,qGAAqG;IACrG,wFAAwF;IAChF,KAAK,CAAC,eAAe,CAAC,EAAgB,EAAE,QAAgB;QAC5D,IAAI,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QACpF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uFAAuF,CAAC,CAAC;QAC9G,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACnC,OAAO,SAAS,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;IAEO,KAAK,CAAC,EAAU;QACpB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAmB,EAAQ,EAAE;YAC7C,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;IACP,CAAC;IAED,wGAAwG;IAChG,cAAc,CAAC,EAAgB;QACnC,OAAO,CACH,sBAAsB,EAAE,CAAC,MAAM,UAAU,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,0CAA0C;YAC1G,mHAAmH;YACnH,0DAA0D;YAC1D,4DAA4D;YAC5D,0EAA0E;YAC1E,8GAA8G;YAC9G,+GAA+G,CAClH,CAAC;IACN,CAAC;IAED,gGAAgG;IAChG,iGAAiG;IACzF,SAAS;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,GAAG,KAAK,EAAE;YACnB,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,EAAE,6CAA6C,CAAC;YAChH,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,EAAE,6CAA6C,CAAC,CAAC;QAChH,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QACxF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,YAAY,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAC9D,6FAA6F;QAC7F,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,SAAS,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,QAAQ,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC;YAAE,OAAO,IAAI,YAAY,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QACtC,OAAO,IAAI,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACnD,CAAC;IAEO,QAAQ;QACZ,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5D,IAAI,QAAQ,KAAK,EAAE;YAAE,OAAO,QAAQ,CAAC;QACrC,gEAAgE;QAChE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzB,CAAC;CACJ,CAAA;AA5FY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACZ,+BAAgB;GAH9C,cAAc,CA4F1B","sourcesContent":["import { spawnSync } from 'child_process';\nimport {\n loadAndValidate, RepoRootFinder, GateTokenService, CliExitError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// The head sha + body of the PR under check, resolved from `gh`. Data-only.\nclass PrUnderCheck {\n number: string;\n headSha: string;\n body: string;\n\n constructor(number: string, headSha: string, body: string) {\n this.number = number;\n this.headSha = headSha;\n this.body = body;\n }\n}\n\n/**\n * `wp-check-pr` — the SERVER-SIDE half of the gate, meant to run as a required CI check. It is READ-ONLY:\n * it never touches git state, never pushes, never calls `gh pr create`. It recomputes\n * `HMAC(prGate.gateSalt, PR_head_sha)` from the committed salt and verifies the PR body carries that\n * token. Because `wp-finish-upsert-pr` refuses to mint the token unless the build gate + every BLOCK\n * checklist passed, a valid token IS proof the gated flow ran and passed on this exact commit.\n *\n * This is what catches a PR opened OUTSIDE the gated flow — an unhooked teammate who `git push`ed and\n * clicked \"Create pull request\" in the web UI carries no valid token for its head sha and fails here.\n *\n * This job is THE required check — the ONE thing a consumer marks required in branch protection. It used\n * to also post a `webpieces/pr-gate` commit status, because `wp-finish-upsert-pr` pushed BEFORE writing\n * the PR body and a status (newest post wins for a context) could supersede the failure that race caused.\n * GatedPrPublisher now writes the body first, so there is no race to recover from and no second entry to\n * confuse anyone about which one to require. This job's exit code is the whole verdict.\n *\n * A repo with no `gateSalt` configured has not opted in → this is a no-op success (exit 0), so it is safe\n * to add the workflow before turning enforcement on.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and resolved by PrGateApp.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class CheckPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly gateTokenService: GateTokenService,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n if (gateSalt.trim() === '') {\n throw new CliExitError(0,\n 'ℹ️ wp-check-pr: no prGate.gateSalt configured — server-side gate token enforcement is disabled. ' +\n 'Add a committed \"gateSalt\" under the pr-gate section of webpieces.config.json to enable it.');\n }\n\n let pr = this.resolvePr();\n if (pr.headSha === '') {\n throw new CliExitError(1,\n '❌ wp-check-pr: could not resolve the PR head sha via `gh`. Ensure the workflow runs on a pull_request ' +\n 'event with `gh` authenticated (GH_TOKEN) and the PR number available (WP_PR_NUMBER or GITHUB_REF).');\n }\n\n pr = await this.verifyWithRetry(pr, gateSalt);\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) {\n process.stdout.write(`✅ wp-check-pr: valid webpieces gate token for PR #${pr.number} @ ${pr.headSha.slice(0, 12)} — created through the gated flow.\\n`);\n return;\n }\n\n // An UNHOOKED push never reaches wp-finish-upsert-pr, so it carries no valid token — fail the job.\n // THIS EXIT CODE IS THE WHOLE SIGNAL: there is no companion commit status any more (see the JSDoc).\n throw new CliExitError(1, this.failureMessage(pr));\n }\n\n // Re-read the PR ONCE after a short delay if the token looks stale. The push-before-body-edit RACE is\n // gone (GatedPrPublisher writes the body first), so this no longer backstops a known ordering bug —\n // it absorbs GitHub's own event/read replication jitter, which matters more now that this job is the\n // single required check. It costs nothing on the success path. Returns the freshest PR.\n private async verifyWithRetry(pr: PrUnderCheck, gateSalt: string): Promise<PrUnderCheck> {\n if (this.gateTokenService.verifyGateToken(pr.body, gateSalt, pr.headSha)) return pr;\n process.stdout.write('… no valid token yet — waiting for the PR body edit to land, then re-checking once…\\n');\n await this.delay(15000);\n const refreshed = this.resolvePr();\n return refreshed.headSha !== '' ? refreshed : pr;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve: () => void): void => {\n setTimeout(resolve, ms);\n });\n }\n\n // The actionable red-check message: this PR did not come through the gated flow (or hooks are missing).\n private failureMessage(pr: PrUnderCheck): string {\n return (\n `❌ wp-check-pr: PR #${pr.number} (head ${pr.headSha.slice(0, 12)}) has no valid webpieces gate token.\\n\\n` +\n `This PR was NOT created through the webpieces gated flow — or was pushed after finishing without re-running it.\\n` +\n `Every commit that lands here must go through it, so:\\n\\n` +\n ` 1. Install the webpieces hooks if you don't have them.\\n` +\n ` 2. Recreate/update this PR by running the full gated flow, in order:\\n` +\n ` pnpm wp-start-upsert-pr → pnpm wp-review-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\\n\\n` +\n `That re-stamps the PR title, body, and the gate token for the current head commit, and this check goes green.`\n );\n }\n\n // Resolve the PR (number, head sha, body) from `gh`. Prefers an explicit WP_PR_NUMBER, then the\n // pull_request number in GITHUB_REF (refs/pull/<n>/merge), then `gh`'s current-branch detection.\n private resolvePr(): PrUnderCheck {\n const num = this.prNumber();\n const args = num !== ''\n ? ['pr', 'view', num, '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"']\n : ['pr', 'view', '--json', 'number,headRefOid,body', '--jq', '\"\\\\(.number)\\\\t\\\\(.headRefOid)\\\\t\\\\(.body)\"'];\n const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 16 });\n if (result.status !== 0) return new PrUnderCheck(num, '', '');\n // jq joins body (which may contain tabs/newlines) last, so split on the FIRST two tabs only.\n const out = (result.stdout ?? '').trim();\n const firstTab = out.indexOf('\\t');\n const secondTab = firstTab >= 0 ? out.indexOf('\\t', firstTab + 1) : -1;\n if (firstTab < 0 || secondTab < 0) return new PrUnderCheck(num, '', '');\n const number = out.slice(0, firstTab);\n const headSha = out.slice(firstTab + 1, secondTab);\n const body = out.slice(secondTab + 1);\n return new PrUnderCheck(number, headSha, body);\n }\n\n private prNumber(): string {\n const explicit = (process.env['WP_PR_NUMBER'] ?? '').trim();\n if (explicit !== '') return explicit;\n // GitHub Actions pull_request: GITHUB_REF = refs/pull/<n>/merge\n const ref = process.env['GITHUB_REF'] ?? '';\n const m = /refs\\/pull\\/(\\d+)\\//.exec(ref);\n return m ? m[1] : '';\n }\n}\n"]}
|
|
@@ -56,8 +56,9 @@ let LandPrCommand = class LandPrCommand {
|
|
|
56
56
|
'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\n' +
|
|
57
57
|
'Its absence means finish has not run on this branch, so there is no reviewed PR to\n' +
|
|
58
58
|
'land. Run the gated flow first:\n\n' +
|
|
59
|
-
' pnpm wp-start-upsert-pr # update from main
|
|
60
|
-
' #
|
|
59
|
+
' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\n' +
|
|
60
|
+
' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\n' +
|
|
61
|
+
' # write review.json at the path wp-review-upsert-pr prints\n' +
|
|
61
62
|
' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\n\n' +
|
|
62
63
|
'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\n' + SEP);
|
|
63
64
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and\n * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is\n * missing, the honest answer is that finish has not run — so this fails and says so, rather than\n * inventing a body that would not match the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n\n if (!fs.existsSync(mergeBodyFile)) {\n throw new InformAiError(\n '\\n' + SEP + '❌ Nothing to land — no rendered merge body\\n' + SEP + '\\n' +\n `Expected the compact squash-commit body at:\\n ${mergeBodyFile}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\\n' +\n 'Its absence means finish has not run on this branch, so there is no reviewed PR to\\n' +\n 'land. Run the gated flow first:\\n\\n' +\n ' pnpm wp-start-upsert-pr # update from main, push, build gate\\n' +\n ' # write .webpieces/review.json\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n\\n' +\n 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\\n' + SEP,\n );\n }\n\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n // MERGE_MODE_AUTO is passed explicitly: running this command IS the intent to merge, so it is\n // not gated on pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand).\n const outcome = this.prMerger.merge(base, `${ref.title} (#${ref.number})`, mergeBodyFile, MERGE_MODE_AUTO);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n const archived = outcome.merged\n ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n archived +\n (outcome.merged ? this.nextStep(repoRoot, base) : '') +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number + title for this head branch, or null when there is none. The TITLE comes\n // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even\n // if review.json was edited afterwards.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '');\n }\n}\n\n// The open PR's number + title, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n\n constructor(number: string, title: string) {\n this.number = number;\n this.title = title;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot produce that body. A UI merge is limited\n * to the repo's squash_merge_commit_title/message settings, and none of their values yields the\n * shortened risk/flags summary — only an explicit `gh pr merge --subject --body-file` does. So when a\n * merge has to happen outside `wp-finish-upsert-pr` (a mergeMode=NONE repo, or a PR whose checks were\n * still running when finish ran), this is the command that keeps main's history consistent.\n *\n * It deliberately does NOT re-run the build gate or re-render the dashboard: `wp-finish-upsert-pr`\n * already did both and left `merge-commit-body.md` on disk. Landing is a separate, later act, and\n * rebuilding here would mean a second authoritative gate whose result nobody reads. If that file is\n * missing, the honest answer is that finish has not run — so this fails and says so, rather than\n * inventing a body that would not match the PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n\n if (!fs.existsSync(mergeBodyFile)) {\n throw new InformAiError(\n '\\n' + SEP + '❌ Nothing to land — no rendered merge body\\n' + SEP + '\\n' +\n `Expected the compact squash-commit body at:\\n ${mergeBodyFile}\\n\\n` +\n 'That file is written by `pnpm wp-finish-upsert-pr`, which is also what posts the PR.\\n' +\n 'Its absence means finish has not run on this branch, so there is no reviewed PR to\\n' +\n 'land. Run the gated flow first:\\n\\n' +\n ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\\n' +\n ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\\n' +\n ' # write review.json at the path wp-review-upsert-pr prints\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n\\n' +\n 'Then re-run `pnpm wp-land-pr` if the PR still needs landing.\\n' + SEP,\n );\n }\n\n const ref = this.prNumberAndTitle(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' + SEP,\n );\n }\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n // MERGE_MODE_AUTO is passed explicitly: running this command IS the intent to merge, so it is\n // not gated on pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand).\n const outcome = this.prMerger.merge(base, `${ref.title} (#${ref.number})`, mergeBodyFile, MERGE_MODE_AUTO);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n const archived = outcome.merged\n ? this.archiveAndPromote(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n archived +\n (outcome.merged ? this.nextStep(repoRoot, base) : '') +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The open PR's number + title for this head branch, or null when there is none. The TITLE comes\n // from the PR itself, not review.json, so the squash subject matches what a reviewer approved even\n // if review.json was edited afterwards.\n private prNumberAndTitle(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title', '--jq', '\"\\\\(.number)\\\\t\\\\(.title)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n const parts = (result.stdout ?? '').trim().split('\\t');\n if ((parts[0] ?? '') === '') return null;\n return new PrIdentity(parts[0] ?? '', parts[1] ?? '');\n }\n}\n\n// The open PR's number + title, as read back from GitHub.\nclass PrIdentity {\n number: string;\n title: string;\n\n constructor(number: string, title: string) {\n this.number = number;\n this.title = title;\n }\n}\n"]}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { RepoRootFinder,
|
|
1
|
+
import { RepoRootFinder, ReviewerInstructionsService } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BuildAffected } from '../workflow/build-affected';
|
|
4
4
|
import { BuildArtifactGate } from '../workflow/build-artifact-gate';
|
|
5
|
-
import { ChecklistNotice } from '../workflow/checklist-notice';
|
|
6
5
|
import { ChecklistScanner } from '../workflow/checklist-scanner';
|
|
7
6
|
import { DiffMaterializer } from '../workflow/diff-materializer';
|
|
8
7
|
import { GitExec } from '../workflow/git-exec';
|
|
@@ -10,6 +9,7 @@ import { MergeEnd } from '../workflow/merge-end';
|
|
|
10
9
|
import { MergeState } from '../workflow/merge-state';
|
|
11
10
|
import { PrContextWriter } from '../workflow/pr-context-writer';
|
|
12
11
|
import { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';
|
|
12
|
+
import { ReviewReport } from '../workflow/review-report';
|
|
13
13
|
import { ReviewStageReceiptService } from '../workflow/review-stage-receipt';
|
|
14
14
|
/**
|
|
15
15
|
* `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)
|
|
@@ -43,14 +43,13 @@ export declare class ReviewUpsertPrCommand {
|
|
|
43
43
|
private readonly mergeState;
|
|
44
44
|
private readonly mergeEnd;
|
|
45
45
|
private readonly checklistScanner;
|
|
46
|
-
private readonly
|
|
47
|
-
private readonly instructions;
|
|
46
|
+
private readonly reviewReport;
|
|
48
47
|
private readonly materializer;
|
|
49
48
|
private readonly prContextWriter;
|
|
50
49
|
private readonly briefingBuilder;
|
|
51
50
|
private readonly reviewerInstructions;
|
|
52
51
|
private readonly receipts;
|
|
53
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, buildArtifactGate: BuildArtifactGate, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner,
|
|
52
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, buildArtifactGate: BuildArtifactGate, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner, reviewReport: ReviewReport, materializer: DiffMaterializer, prContextWriter: PrContextWriter, briefingBuilder: ReviewerBriefingBuilder, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService);
|
|
54
53
|
run(): Promise<void>;
|
|
55
54
|
/**
|
|
56
55
|
* Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from
|
|
@@ -78,13 +77,13 @@ export declare class ReviewUpsertPrCommand {
|
|
|
78
77
|
* reviewer reads covers exactly the files its checklist matched against.
|
|
79
78
|
*/
|
|
80
79
|
private briefReviewers;
|
|
81
|
-
/** What the AI must do next: spawn each reviewer, then write review.json. */
|
|
82
|
-
private report;
|
|
83
80
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
81
|
+
* What the AI must do next — rendered by `ReviewReport`, which owns the ordering.
|
|
82
|
+
*
|
|
83
|
+
* The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a
|
|
84
|
+
* real bug: the zero-checklist notice signed off with "Carry on and run: pnpm wp-finish-upsert-pr"
|
|
85
|
+
* while the block right beneath it said to write review.json first. An agent that reads top to bottom
|
|
86
|
+
* obeyed the first line and posted a PR with no review at all.
|
|
87
87
|
*/
|
|
88
|
-
private
|
|
89
|
-
private why;
|
|
88
|
+
private report;
|
|
90
89
|
}
|
|
@@ -8,7 +8,6 @@ const inversify_1 = require("inversify");
|
|
|
8
8
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
9
9
|
const build_affected_1 = require("../workflow/build-affected");
|
|
10
10
|
const build_artifact_gate_1 = require("../workflow/build-artifact-gate");
|
|
11
|
-
const checklist_notice_1 = require("../workflow/checklist-notice");
|
|
12
11
|
const checklist_scanner_1 = require("../workflow/checklist-scanner");
|
|
13
12
|
const diff_materializer_1 = require("../workflow/diff-materializer");
|
|
14
13
|
const git_exec_1 = require("../workflow/git-exec");
|
|
@@ -17,6 +16,7 @@ const merge_end_1 = require("../workflow/merge-end");
|
|
|
17
16
|
const merge_state_1 = require("../workflow/merge-state");
|
|
18
17
|
const pr_context_writer_1 = require("../workflow/pr-context-writer");
|
|
19
18
|
const reviewer_briefing_builder_1 = require("../workflow/reviewer-briefing-builder");
|
|
19
|
+
const review_report_1 = require("../workflow/review-report");
|
|
20
20
|
const review_stage_receipt_1 = require("../workflow/review-stage-receipt");
|
|
21
21
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
22
22
|
/**
|
|
@@ -51,15 +51,14 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
51
51
|
mergeState;
|
|
52
52
|
mergeEnd;
|
|
53
53
|
checklistScanner;
|
|
54
|
-
|
|
55
|
-
instructions;
|
|
54
|
+
reviewReport;
|
|
56
55
|
materializer;
|
|
57
56
|
prContextWriter;
|
|
58
57
|
briefingBuilder;
|
|
59
58
|
reviewerInstructions;
|
|
60
59
|
receipts;
|
|
61
60
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
62
|
-
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner,
|
|
61
|
+
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner, reviewReport, materializer, prContextWriter, briefingBuilder, reviewerInstructions, receipts) {
|
|
63
62
|
this.repoRootFinder = repoRootFinder;
|
|
64
63
|
this.aiBranchName = aiBranchName;
|
|
65
64
|
this.gitExec = gitExec;
|
|
@@ -68,8 +67,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
68
67
|
this.mergeState = mergeState;
|
|
69
68
|
this.mergeEnd = mergeEnd;
|
|
70
69
|
this.checklistScanner = checklistScanner;
|
|
71
|
-
this.
|
|
72
|
-
this.instructions = instructions;
|
|
70
|
+
this.reviewReport = reviewReport;
|
|
73
71
|
this.materializer = materializer;
|
|
74
72
|
this.prContextWriter = prContextWriter;
|
|
75
73
|
this.briefingBuilder = briefingBuilder;
|
|
@@ -152,58 +150,22 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
152
150
|
}
|
|
153
151
|
return briefings;
|
|
154
152
|
}
|
|
155
|
-
/** What the AI must do next: spawn each reviewer, then write review.json. */
|
|
156
|
-
report(repoRoot, featureName, scan, briefings) {
|
|
157
|
-
process.stdout.write('\n' + SEP + '② Spawn Subagent Reviews, then finish\n' + SEP + '\n');
|
|
158
|
-
if (scan.applicable.length === 0) {
|
|
159
|
-
process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));
|
|
163
|
-
}
|
|
164
|
-
process.stdout.write(`\nWhile your subagents are running, review your own changes and\n${(0, rules_config_1.reviewJsonSchemaHint)(scan.reviewPath)}\n\n` +
|
|
165
|
-
`Finally run: pnpm wp-finish-upsert-pr\n` +
|
|
166
|
-
`(The build gate is already green for this commit — finish reuses it unless HEAD moves.)\n\n`);
|
|
167
|
-
}
|
|
168
153
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
154
|
+
* What the AI must do next — rendered by `ReviewReport`, which owns the ordering.
|
|
155
|
+
*
|
|
156
|
+
* The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a
|
|
157
|
+
* real bug: the zero-checklist notice signed off with "Carry on and run: pnpm wp-finish-upsert-pr"
|
|
158
|
+
* while the block right beneath it said to write review.json first. An agent that reads top to bottom
|
|
159
|
+
* obeyed the first line and posted a PR with no review at all.
|
|
172
160
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this
|
|
182
|
-
// reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of
|
|
183
|
-
// correcting the file sitting right there.
|
|
184
|
-
for (const e of scan.formatErrors)
|
|
185
|
-
lines.push(` ⛔ ${e}`);
|
|
186
|
-
if (owed.length === 0) {
|
|
187
|
-
lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');
|
|
188
|
-
return lines.join('\n') + '\n';
|
|
189
|
-
}
|
|
190
|
-
lines.push('', `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`, 'work, and you may NOT write a reviewer\'s verdict file on its behalf.', '');
|
|
191
|
-
for (const b of owed) {
|
|
192
|
-
lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);
|
|
193
|
-
lines.push(` subagent_type: ${b.subagent}`);
|
|
194
|
-
lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');
|
|
195
|
-
lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);
|
|
196
|
-
lines.push('');
|
|
197
|
-
}
|
|
198
|
-
return lines.join('\n');
|
|
199
|
-
}
|
|
200
|
-
// Why this one is in scope. A patternless checklist is NOT "matched" — it always runs, over the whole
|
|
201
|
-
// diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.
|
|
202
|
-
why(b) {
|
|
203
|
-
if (b.matchedPatterns.length === 0) {
|
|
204
|
-
return `ALWAYS RUNS (no "patterns" configured), whole diff in scope — ${b.myFiles.length} file(s)`;
|
|
205
|
-
}
|
|
206
|
-
return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p) => `"${p}"`).join(', ')}`;
|
|
161
|
+
report(repoRoot, featureName, scan, briefings) {
|
|
162
|
+
const input = new review_report_1.ReviewReportInput(repoRoot, featureName, scan.reviewPath);
|
|
163
|
+
input.definedCount = scan.defined.length;
|
|
164
|
+
input.applicableCount = scan.applicable.length;
|
|
165
|
+
input.reviewed = scan.reviewed.slice();
|
|
166
|
+
input.formatErrors = scan.formatErrors.slice();
|
|
167
|
+
input.briefings = briefings.slice();
|
|
168
|
+
process.stdout.write(this.reviewReport.render(input));
|
|
207
169
|
}
|
|
208
170
|
};
|
|
209
171
|
exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand;
|
|
@@ -217,8 +179,7 @@ exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand = tslib_1.__decorate([
|
|
|
217
179
|
merge_state_1.MergeState,
|
|
218
180
|
merge_end_1.MergeEnd,
|
|
219
181
|
checklist_scanner_1.ChecklistScanner,
|
|
220
|
-
|
|
221
|
-
rules_config_1.ChecklistInstructionsService,
|
|
182
|
+
review_report_1.ReviewReport,
|
|
222
183
|
diff_materializer_1.DiffMaterializer,
|
|
223
184
|
pr_context_writer_1.PrContextWriter,
|
|
224
185
|
reviewer_briefing_builder_1.ReviewerBriefingBuilder,
|
|
@@ -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 * as path from 'path';\nimport {\n loadAndValidate, reviewJsonSchemaHint, writeTemplate, PrGateConfig, RepoRootFinder,\n ChecklistInstructionsService, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\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 { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\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 checklistNotice: ChecklistNotice,\n private readonly instructions: ChecklistInstructionsService,\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 ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /** What the AI must do next: spawn each reviewer, then write review.json. */\n private report(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): void {\n process.stdout.write('\\n' + SEP + '② Spawn Subagent Reviews, then finish\\n' + SEP + '\\n');\n if (scan.applicable.length === 0) {\n process.stdout.write(this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr'));\n } else {\n process.stdout.write(this.spawnBlocks(repoRoot, featureName, scan, briefings));\n }\n process.stdout.write(\n `\\nWhile your subagents are running, review your own changes and\\n${reviewJsonSchemaHint(scan.reviewPath)}\\n\\n` +\n `Finally 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 * One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the\n * generated instructions file is the contract, so anything restated here is a second copy that can go\n * stale — which is exactly how a removed `success` field outlived its own removal in print.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private spawnBlocks(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): string {\n const reviewedIds = new Set(scan.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n const lines: string[] = [];\n for (const r of scan.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its 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 scan.formatErrors) lines.push(` ⛔ ${e}`);\n if (owed.length === 0) {\n lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');\n return lines.join('\\n') + '\\n';\n }\n lines.push('',\n `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`,\n 'work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '');\n for (const b of owed) {\n lines.push(` ▶ ${b.subagent} — ${this.why(b)}`);\n lines.push(` subagent_type: ${b.subagent}`);\n lines.push(' prompt: Read your instructions file FIRST and follow it exactly:');\n lines.push(` ${this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent)}`);\n lines.push('');\n }\n return lines.join('\\n');\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-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,\n ReviewerBriefing, ReviewerInstructionsService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\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 ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n private report(repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[]): 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 process.stdout.write(this.reviewReport.render(input));\n }\n}\n"]}
|
|
@@ -57,7 +57,9 @@ let StartUpdateCommand = class StartUpdateCommand {
|
|
|
57
57
|
'flow instead — it re-merges main AND updates the PR:\n' +
|
|
58
58
|
' 1. pnpm wp-start-upsert-pr\n' +
|
|
59
59
|
' 2. /wp-merge (only if conflicts)\n' +
|
|
60
|
-
' 3. pnpm wp-
|
|
60
|
+
' 3. pnpm wp-review-upsert-pr (validates the merge, builds, briefs the reviewers)\n' +
|
|
61
|
+
' 4. write review.json at the path wp-review-upsert-pr prints\n' +
|
|
62
|
+
' 5. pnpm wp-finish-upsert-pr\n');
|
|
61
63
|
}
|
|
62
64
|
};
|
|
63
65
|
exports.StartUpdateCommand = StartUpdateCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-update-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-update-command.ts"],"names":[],"mappings":";;;;AAAA,iDAAyC;AACzC,0DAAsF;AACtF,yCAA2D;AAC3D,6DAAwD;AACxD,uDAAmD;AAEnD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,8FAA8F;AAC9F,sGAAsG;AACtG,sGAAsG;AACtG,kDAAkD;AAE3C,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEN;IACA;IACA;IAHrB,YACqB,cAA8B,EAC9B,WAAwB,EACxB,SAAoB;QAFpB,mBAAc,GAAd,cAAc,CAAgB;QAC9B,gBAAW,GAAX,WAAW,CAAa;QACxB,cAAS,GAAT,SAAS,CAAW;IACtC,CAAC;IAEJ,KAAK,CAAC,GAAG;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,gGAAgG;QAChG,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QAErD,8FAA8F;QAC9F,oFAAoF;QACpF,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAE9B,6FAA6F;QAC7F,6FAA6F;QAC7F,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAC9G,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;YACzB,4FAA4F;YAC5F,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,IAAI,OAAO,KAAK,mBAAmB,EAAE,CAAC;YAClC,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,6CAA6C,GAAG,GAAG,GAAG,IAAI;gBACvE,kEAAkE;gBAClE,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IACtG,CAAC;IAED,2FAA2F;IAC3F,mGAAmG;IACnG,kGAAkG;IAClG,wDAAwD;IACxD,cAAc,CAAC,QAAgB;QAC3B,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvG,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAClE,IAAI,MAAM,KAAK,EAAE;YAAE,OAAO;QAC1B,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,kBAAkB,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI;YAClF,kGAAkG;YAClG,+BAA+B,MAAM,6DAA6D;YAClG,wDAAwD;YACxD,gCAAgC;YAChC,wCAAwC;YACxC,iCAAiC,CACpC,CAAC;IACN,CAAC;CACJ,CAAA;
|
|
1
|
+
{"version":3,"file":"start-update-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/start-update-command.ts"],"names":[],"mappings":";;;;AAAA,iDAAyC;AACzC,0DAAsF;AACtF,yCAA2D;AAC3D,6DAAwD;AACxD,uDAAmD;AAEnD,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,8FAA8F;AAC9F,sGAAsG;AACtG,sGAAsG;AACtG,kDAAkD;AAE3C,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEN;IACA;IACA;IAHrB,YACqB,cAA8B,EAC9B,WAAwB,EACxB,SAAoB;QAFpB,mBAAc,GAAd,cAAc,CAAgB;QAC9B,gBAAW,GAAX,WAAW,CAAa;QACxB,cAAS,GAAT,SAAS,CAAW;IACtC,CAAC;IAEJ,KAAK,CAAC,GAAG;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,gGAAgG;QAChG,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QAErD,8FAA8F;QAC9F,oFAAoF;QACpF,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAE9B,6FAA6F;QAC7F,6FAA6F;QAC7F,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAC9G,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;YACzB,4FAA4F;YAC5F,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,IAAI,OAAO,KAAK,mBAAmB,EAAE,CAAC;YAClC,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,6CAA6C,GAAG,GAAG,GAAG,IAAI;gBACvE,kEAAkE;gBAClE,2BAA2B,CAC9B,CAAC;QACN,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IACtG,CAAC;IAED,2FAA2F;IAC3F,mGAAmG;IACnG,kGAAkG;IAClG,wDAAwD;IACxD,cAAc,CAAC,QAAgB;QAC3B,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvG,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAClE,IAAI,MAAM,KAAK,EAAE;YAAE,OAAO;QAC1B,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,kBAAkB,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI;YAClF,kGAAkG;YAClG,+BAA+B,MAAM,6DAA6D;YAClG,wDAAwD;YACxD,gCAAgC;YAChC,wCAAwC;YACxC,uFAAuF;YACvF,iEAAiE;YACjE,iCAAiC,CACpC,CAAC;IACN,CAAC;CACJ,CAAA;AArDY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACjB,2BAAW;QACb,sBAAS;GAJhC,kBAAkB,CAqD9B","sourcesContent":["import { execSync } from 'child_process';\nimport { writeTemplate, CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { OpenPrCheck } from '../workflow/open-pr-check';\nimport { RunUpdate } from '../workflow/run-update';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n// wp-start-update: the \"sync this feature branch from main\" entry (the redirect target of the\n// redirect-how-to-merge-main guard). Runs the FULL 3-point squash-update via RunUpdate: a CLEAN merge\n// finalizes everything; CONFLICT writes the 3-point context + merge process doc and hands back. NEVER\n// creates a PR (that is the wp-*-upsert-pr flow).\n@injectable(bindingScopeValues.Singleton)\nexport class StartUpdateCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly openPrCheck: OpenPrCheck,\n private readonly runUpdate: RunUpdate,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n\n // An open PR means the PR flow must be used — a 3-point update would strand the PR on the old\n // branch generation. Fails fast if GitHub can't be reached (never guesses \"no PR\").\n this.assertNoOpenPr(repoRoot);\n\n // pushRemote=true: this is the END of the update-only flow — no later command pushes, so not\n // pushing here would silently strand the squash rewrite locally. (The PR flow passes false.)\n const outcome = await this.runUpdate.runUpdateFromMain(repoRoot, 'wp-start-update', 'wp-finish-update', true);\n if (outcome === 'conflict') {\n // Context + marker + merge process doc already written by merge-start; hand back to the AI.\n throw new CliExitError(2, '');\n }\n if (outcome === 'unvalidatedResume') {\n throw new CliExitError(1,\n '\\n' + SEP + '⏸️ Merge in progress — not yet validated\\n' + SEP + '\\n' +\n 'Resolve the remaining conflicts in the working tree, then run:\\n' +\n ' pnpm wp-finish-update\\n',\n );\n }\n process.stdout.write('\\n✅ Updated from main — clean. No need to call `pnpm wp-finish-update`.\\n');\n }\n\n // If a PR is already OPEN for this branch, wp-start-update must refuse. The 3-point update\n // re-squashes your branch onto main and force-pushes it to origin/<feature> — the PR head — but it\n // does NOT refresh the PR body/review or run the authoritative build gate. Fail fast and steer to\n // the PR flow, which re-merges main AND updates the PR.\n assertNoOpenPr(repoRoot: string): void {\n const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoRoot, encoding: 'utf8' }).trim();\n const openPr = this.openPrCheck.openPrForBranch(repoRoot, branch);\n if (openPr === '') return;\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ An open PR (#${openPr}) already tracks this branch\\n` + SEP + '\\n' +\n 'wp-start-update re-squashes your branch onto main and force-pushes it, but it does NOT refresh\\n' +\n `the PR body/review — so PR #${openPr} would get new code without an updated review. Use the PR\\n` +\n 'flow instead — it re-merges main AND updates the PR:\\n' +\n ' 1. pnpm wp-start-upsert-pr\\n' +\n ' 2. /wp-merge (only if conflicts)\\n' +\n ' 3. pnpm wp-review-upsert-pr (validates the merge, builds, briefs the reviewers)\\n' +\n ' 4. write review.json at the path wp-review-upsert-pr prints\\n' +\n ' 5. pnpm wp-finish-upsert-pr\\n',\n );\n }\n}\n"]}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds the message `wp-
|
|
2
|
+
* Builds the message stage ② (`wp-review-upsert-pr`) prints when a diff matched ZERO review checklists.
|
|
3
3
|
*
|
|
4
4
|
* ZERO IS A VALID, SUPPORTED STATE — it is NOT an error and NEVER blocks the flow. This exists only
|
|
5
5
|
* because the previous behavior was to print nothing at all, which is indistinguishable from "the
|
|
6
6
|
* checklist ran and passed". Silence is the bug; refusing would be a worse one.
|
|
7
7
|
*
|
|
8
|
+
* ORDER IS THE FEATURE. This block used to open with ~10 lines of "here is how to configure checklists"
|
|
9
|
+
* and only reach the all-clear at the bottom, so a repo with none configured read as a repo with a
|
|
10
|
+
* problem for the several seconds it took to scroll. The verdict now comes FIRST — headline, then the
|
|
11
|
+
* all-clear — and the how-to sits below it, explicitly marked optional and for the human. The guidance is
|
|
12
|
+
* genuinely useful the first time; it is just not the answer to "is anything wrong?".
|
|
13
|
+
*
|
|
14
|
+
* It also no longer names a command to run. Naming one here produced TWO "what to do next" instructions in
|
|
15
|
+
* stage ②'s output — this one, and the real one below it that asks for review.json first — and an agent
|
|
16
|
+
* reading top to bottom obeyed the first, skipping the review it was told to write. Exactly one next-step
|
|
17
|
+
* instruction is emitted per stage now, and it is `ReviewReport`'s, not this class's.
|
|
18
|
+
*
|
|
8
19
|
* The two empty cases need different fixes, so they get different text:
|
|
9
20
|
* - NONE CONFIGURED — the repo configured no checklists at all. Perfectly fine; mention that a human
|
|
10
21
|
* MAY add checklist *.md docs if they want reviews, and move on.
|
|
@@ -21,12 +32,19 @@
|
|
|
21
32
|
*/
|
|
22
33
|
export declare class ChecklistNotice {
|
|
23
34
|
/**
|
|
24
|
-
* @param definedCount
|
|
25
|
-
|
|
35
|
+
* @param definedCount how many checklists `pr-gate.checklists` defines (0 = the repo configured none)
|
|
36
|
+
*/
|
|
37
|
+
build(definedCount: number): string;
|
|
38
|
+
private headline;
|
|
39
|
+
/**
|
|
40
|
+
* The all-clear, immediately under the headline and BEFORE any explanation or how-to. Stated plainly so
|
|
41
|
+
* neither the AI nor the human reads the notice as a gate that needs satisfying — and stated here, at
|
|
42
|
+
* the top, so nobody has to read a config tutorial to find out that nothing is wrong.
|
|
43
|
+
*
|
|
44
|
+
* It deliberately names no command: the single next step is printed once, at the end of the stage.
|
|
26
45
|
*/
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private noneMatched;
|
|
46
|
+
private allClear;
|
|
47
|
+
private details;
|
|
48
|
+
private howToConfigure;
|
|
49
|
+
private whyNothingMatched;
|
|
32
50
|
}
|
|
@@ -4,12 +4,23 @@ exports.ChecklistNotice = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const inversify_1 = require("inversify");
|
|
6
6
|
/**
|
|
7
|
-
* Builds the message `wp-
|
|
7
|
+
* Builds the message stage ② (`wp-review-upsert-pr`) prints when a diff matched ZERO review checklists.
|
|
8
8
|
*
|
|
9
9
|
* ZERO IS A VALID, SUPPORTED STATE — it is NOT an error and NEVER blocks the flow. This exists only
|
|
10
10
|
* because the previous behavior was to print nothing at all, which is indistinguishable from "the
|
|
11
11
|
* checklist ran and passed". Silence is the bug; refusing would be a worse one.
|
|
12
12
|
*
|
|
13
|
+
* ORDER IS THE FEATURE. This block used to open with ~10 lines of "here is how to configure checklists"
|
|
14
|
+
* and only reach the all-clear at the bottom, so a repo with none configured read as a repo with a
|
|
15
|
+
* problem for the several seconds it took to scroll. The verdict now comes FIRST — headline, then the
|
|
16
|
+
* all-clear — and the how-to sits below it, explicitly marked optional and for the human. The guidance is
|
|
17
|
+
* genuinely useful the first time; it is just not the answer to "is anything wrong?".
|
|
18
|
+
*
|
|
19
|
+
* It also no longer names a command to run. Naming one here produced TWO "what to do next" instructions in
|
|
20
|
+
* stage ②'s output — this one, and the real one below it that asks for review.json first — and an agent
|
|
21
|
+
* reading top to bottom obeyed the first, skipping the review it was told to write. Exactly one next-step
|
|
22
|
+
* instruction is emitted per stage now, and it is `ReviewReport`'s, not this class's.
|
|
23
|
+
*
|
|
13
24
|
* The two empty cases need different fixes, so they get different text:
|
|
14
25
|
* - NONE CONFIGURED — the repo configured no checklists at all. Perfectly fine; mention that a human
|
|
15
26
|
* MAY add checklist *.md docs if they want reviews, and move on.
|
|
@@ -26,28 +37,41 @@ const inversify_1 = require("inversify");
|
|
|
26
37
|
*/
|
|
27
38
|
let ChecklistNotice = class ChecklistNotice {
|
|
28
39
|
/**
|
|
29
|
-
* @param definedCount
|
|
30
|
-
* @param finishCommand the command to continue with, named in every branch
|
|
40
|
+
* @param definedCount how many checklists `pr-gate.checklists` defines (0 = the repo configured none)
|
|
31
41
|
*/
|
|
32
|
-
build(definedCount
|
|
33
|
-
return `${this.
|
|
42
|
+
build(definedCount) {
|
|
43
|
+
return `${this.headline(definedCount)}\n${this.allClear()}\n${this.details(definedCount)}\n`;
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
// Line one: the verdict, in one line. A reader who only reads this far has the right answer.
|
|
46
|
+
headline(definedCount) {
|
|
47
|
+
if (definedCount === 0) {
|
|
48
|
+
return '📋 Review checklists: NONE CONFIGURED (0 ran) — that is fine, this repo simply has none.';
|
|
49
|
+
}
|
|
50
|
+
return `📋 Review checklists: ${definedCount} defined in pr-gate.checklists, 0 matched this diff.`;
|
|
39
51
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The all-clear, immediately under the headline and BEFORE any explanation or how-to. Stated plainly so
|
|
54
|
+
* neither the AI nor the human reads the notice as a gate that needs satisfying — and stated here, at
|
|
55
|
+
* the top, so nobody has to read a config tutorial to find out that nothing is wrong.
|
|
56
|
+
*
|
|
57
|
+
* It deliberately names no command: the single next step is printed once, at the end of the stage.
|
|
58
|
+
*/
|
|
59
|
+
allClear() {
|
|
60
|
+
return ('✅ Zero checklists is a perfectly valid state — this is INFORMATION, not a blocker, and\n' +
|
|
61
|
+
' nothing here needs fixing. Nothing is owed on the checklist front; keep going with the\n' +
|
|
62
|
+
' NEXT steps printed at the end of this command.\n');
|
|
63
|
+
}
|
|
64
|
+
// Everything a reader may safely skip. Kept — it is what teaches a repo how to get reviews — but it
|
|
65
|
+
// lives below the verdict, not in front of it.
|
|
66
|
+
details(definedCount) {
|
|
67
|
+
if (definedCount === 0)
|
|
68
|
+
return this.howToConfigure();
|
|
69
|
+
return this.whyNothingMatched();
|
|
45
70
|
}
|
|
46
|
-
|
|
47
|
-
return ('
|
|
48
|
-
'\n' +
|
|
49
|
-
'
|
|
50
|
-
' paths, add checklist *.md docs and list them in webpieces.config.json:\n' +
|
|
71
|
+
howToConfigure() {
|
|
72
|
+
return (' ── optional, for the human (skip this unless you WANT per-area reviews) ──────────────\n' +
|
|
73
|
+
' If you ever want reviews enforced on PRs that touch certain paths, add checklist *.md\n' +
|
|
74
|
+
' docs and list them in webpieces.config.json:\n' +
|
|
51
75
|
' "commands": { "pr-gate": { "checklists": [\n' +
|
|
52
76
|
' { "subagent": "db-migration-reviewer",\n' +
|
|
53
77
|
' "doc": ".claude/review/db-migrations.md",\n' +
|
|
@@ -56,10 +80,10 @@ let ChecklistNotice = class ChecklistNotice {
|
|
|
56
80
|
' `doc` is REPO-relative, `patterns` are path globs, and each entry needs its OWN reviewer\n' +
|
|
57
81
|
' subagent (a .claude/agents/<subagent>.md) — that is how independent review is enforced.');
|
|
58
82
|
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
' path patterns hit a changed file. Expected for changes outside those areas
|
|
62
|
-
' one should have run, check its "patterns" against the changed-file list above.');
|
|
83
|
+
whyNothingMatched() {
|
|
84
|
+
return (' ── why ──────────────────────────────────────────────────────────────────────────────\n' +
|
|
85
|
+
' None of their path patterns hit a changed file. Expected for changes outside those areas;\n' +
|
|
86
|
+
' if you thought one should have run, check its "patterns" against the changed-file list above.');
|
|
63
87
|
}
|
|
64
88
|
};
|
|
65
89
|
exports.ChecklistNotice = ChecklistNotice;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-notice.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-notice.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D
|
|
1
|
+
{"version":3,"file":"checklist-notice.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/checklist-notice.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEI,IAAM,eAAe,GAArB,MAAM,eAAe;IACxB;;OAEG;IACH,KAAK,CAAC,YAAoB;QACtB,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC;IACjG,CAAC;IAED,6FAA6F;IACrF,QAAQ,CAAC,YAAoB;QACjC,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;YACrB,OAAO,0FAA0F,CAAC;QACtG,CAAC;QACD,OAAO,yBAAyB,YAAY,sDAAsD,CAAC;IACvG,CAAC;IAED;;;;;;OAMG;IACK,QAAQ;QACZ,OAAO,CACH,0FAA0F;YAC1F,6FAA6F;YAC7F,qDAAqD,CACxD,CAAC;IACN,CAAC;IAED,oGAAoG;IACpG,+CAA+C;IACvC,OAAO,CAAC,YAAoB;QAChC,IAAI,YAAY,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;QACrD,OAAO,IAAI,CAAC,iBAAiB,EAAE,CAAC;IACpC,CAAC;IAEO,cAAc;QAClB,OAAO,CACH,6FAA6F;YAC7F,4FAA4F;YAC5F,mDAAmD;YACnD,mDAAmD;YACnD,iDAAiD;YACjD,sDAAsD;YACtD,2DAA2D;YAC3D,cAAc;YACd,+FAA+F;YAC/F,4FAA4F,CAC/F,CAAC;IACN,CAAC;IAEO,iBAAiB;QACrB,OAAO,CACH,4FAA4F;YAC5F,gGAAgG;YAChG,kGAAkG,CACrG,CAAC;IACN,CAAC;CACJ,CAAA;AA5DY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,eAAe,CA4D3B","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Builds the message stage ② (`wp-review-upsert-pr`) prints when a diff matched ZERO review checklists.\n *\n * ZERO IS A VALID, SUPPORTED STATE — it is NOT an error and NEVER blocks the flow. This exists only\n * because the previous behavior was to print nothing at all, which is indistinguishable from \"the\n * checklist ran and passed\". Silence is the bug; refusing would be a worse one.\n *\n * ORDER IS THE FEATURE. This block used to open with ~10 lines of \"here is how to configure checklists\"\n * and only reach the all-clear at the bottom, so a repo with none configured read as a repo with a\n * problem for the several seconds it took to scroll. The verdict now comes FIRST — headline, then the\n * all-clear — and the how-to sits below it, explicitly marked optional and for the human. The guidance is\n * genuinely useful the first time; it is just not the answer to \"is anything wrong?\".\n *\n * It also no longer names a command to run. Naming one here produced TWO \"what to do next\" instructions in\n * stage ②'s output — this one, and the real one below it that asks for review.json first — and an agent\n * reading top to bottom obeyed the first, skipping the review it was told to write. Exactly one next-step\n * instruction is emitted per stage now, and it is `ReviewReport`'s, not this class's.\n *\n * The two empty cases need different fixes, so they get different text:\n * - NONE CONFIGURED — the repo configured no checklists at all. Perfectly fine; mention that a human\n * MAY add checklist *.md docs if they want reviews, and move on.\n * - NONE MATCHED — checklists exist, but none of their patterns hit this diff. Also fine; report the\n * count so a human can judge whether that is expected.\n *\n * There is deliberately NO \"misconfigured\" case. Checklists live only in `pr-gate.checklists`, which\n * loadAndValidate validates — including that every doc and reviewer agent file exists — so a broken set\n * throws before any command reaches this class. The old tolerant manifest loader that returned [] for a\n * malformed doc (and thus enforced NOTHING while looking configured) is gone, and with it that failure mode.\n *\n * Pure string building, no I/O, so it is unit-testable without git or a repo.\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistNotice {\n /**\n * @param definedCount how many checklists `pr-gate.checklists` defines (0 = the repo configured none)\n */\n build(definedCount: number): string {\n return `${this.headline(definedCount)}\\n${this.allClear()}\\n${this.details(definedCount)}\\n`;\n }\n\n // Line one: the verdict, in one line. A reader who only reads this far has the right answer.\n private headline(definedCount: number): string {\n if (definedCount === 0) {\n return '📋 Review checklists: NONE CONFIGURED (0 ran) — that is fine, this repo simply has none.';\n }\n return `📋 Review checklists: ${definedCount} defined in pr-gate.checklists, 0 matched this diff.`;\n }\n\n /**\n * The all-clear, immediately under the headline and BEFORE any explanation or how-to. Stated plainly so\n * neither the AI nor the human reads the notice as a gate that needs satisfying — and stated here, at\n * the top, so nobody has to read a config tutorial to find out that nothing is wrong.\n *\n * It deliberately names no command: the single next step is printed once, at the end of the stage.\n */\n private allClear(): string {\n return (\n '✅ Zero checklists is a perfectly valid state — this is INFORMATION, not a blocker, and\\n' +\n ' nothing here needs fixing. Nothing is owed on the checklist front; keep going with the\\n' +\n ' NEXT steps printed at the end of this command.\\n'\n );\n }\n\n // Everything a reader may safely skip. Kept — it is what teaches a repo how to get reviews — but it\n // lives below the verdict, not in front of it.\n private details(definedCount: number): string {\n if (definedCount === 0) return this.howToConfigure();\n return this.whyNothingMatched();\n }\n\n private howToConfigure(): string {\n return (\n ' ── optional, for the human (skip this unless you WANT per-area reviews) ──────────────\\n' +\n ' If you ever want reviews enforced on PRs that touch certain paths, add checklist *.md\\n' +\n ' docs and list them in webpieces.config.json:\\n' +\n ' \"commands\": { \"pr-gate\": { \"checklists\": [\\n' +\n ' { \"subagent\": \"db-migration-reviewer\",\\n' +\n ' \"doc\": \".claude/review/db-migrations.md\",\\n' +\n ' \"patterns\": [\"**/migrations/**\", \"**/*.sql\"] }\\n' +\n ' ] } }\\n' +\n ' `doc` is REPO-relative, `patterns` are path globs, and each entry needs its OWN reviewer\\n' +\n ' subagent (a .claude/agents/<subagent>.md) — that is how independent review is enforced.'\n );\n }\n\n private whyNothingMatched(): string {\n return (\n ' ── why ──────────────────────────────────────────────────────────────────────────────\\n' +\n ' None of their path patterns hit a changed file. Expected for changes outside those areas;\\n' +\n ' if you thought one should have run, check its \"patterns\" against the changed-file list above.'\n );\n }\n}\n"]}
|
|
@@ -27,7 +27,8 @@ let OpenPrCheck = class OpenPrCheck {
|
|
|
27
27
|
'This check must NOT be skipped: a 3-point update creates a NEW branch generation, so if a\n' +
|
|
28
28
|
'PR already exists, wp-start-update would leave it pointing at the OLD branch. Fix gh first\n' +
|
|
29
29
|
'(install / `gh auth login` / restore network), then re-run — or, if a PR does exist, use\n' +
|
|
30
|
-
'the PR flow: pnpm wp-start-upsert-pr → /wp-merge
|
|
30
|
+
'the PR flow: pnpm wp-start-upsert-pr → /wp-merge (only on conflict) →\n' +
|
|
31
|
+
'pnpm wp-review-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\n');
|
|
31
32
|
}
|
|
32
33
|
return (result.stdout ?? '').trim();
|
|
33
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"open-pr-check.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/open-pr-check.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAuD;AACvD,yCAA2D;AAC3D,mDAA+C;AAE/C,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,+FAA+F;AAExF,IAAM,WAAW,GAAjB,MAAM,WAAW;IACS;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAE3D,qGAAqG;IACrG,+FAA+F;IAC/F,gGAAgG;IAChG,wGAAwG;IACxG,eAAe,CAAC,QAAgB,EAAE,aAAqB;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAA,yBAAS,EACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAClG,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CACtC,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC;YAC5G,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,oEAAoE,GAAG,GAAG,GAAG,IAAI;gBAC9F,8BAA8B,IAAI,MAAM,MAAM,MAAM;gBACpD,6FAA6F;gBAC7F,8FAA8F;gBAC9F,4FAA4F;gBAC5F
|
|
1
|
+
{"version":3,"file":"open-pr-check.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/open-pr-check.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAuD;AACvD,yCAA2D;AAC3D,mDAA+C;AAE/C,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,+FAA+F;AAExF,IAAM,WAAW,GAAjB,MAAM,WAAW;IACS;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAE3D,qGAAqG;IACrG,+FAA+F;IAC/F,gGAAgG;IAChG,wGAAwG;IACxG,eAAe,CAAC,QAAgB,EAAE,aAAqB;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,IAAA,yBAAS,EACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAClG,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CACtC,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC;YAC5G,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG,GAAG,oEAAoE,GAAG,GAAG,GAAG,IAAI;gBAC9F,8BAA8B,IAAI,MAAM,MAAM,MAAM;gBACpD,6FAA6F;gBAC7F,8FAA8F;gBAC9F,4FAA4F;gBAC5F,yEAAyE;gBACzE,2EAA2E,CAC9E,CAAC;QACN,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,CAAC;CACJ,CAAA;AA3BY,kCAAW;sBAAX,WAAW;IADvB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEM,4BAAY;GAD9C,WAAW,CA2BvB","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BranchNaming } from './branch-naming';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** Hard gate: whether an OPEN PR already tracks a feature branch (by its stable base name). */\n@injectable(bindingScopeValues.Singleton)\nexport class OpenPrCheck {\n constructor(private readonly branchNaming: BranchNaming) {}\n\n // The open PR (if any) tracking a feature branch, looked up by its STABLE base name. Returns '' ONLY\n // when GitHub answered and there is genuinely no open PR. HARD GATE: FAILS FAST if `gh` cannot\n // answer — we must NOT degrade \"couldn't ask GitHub\" into \"no PR\" (a 3-point update makes a new\n // branch generation and the existing PR would be stranded on the OLD branch). Refuse rather than guess.\n openPrForBranch(repoRoot: string, currentBranch: string): string {\n const base = this.branchNaming.baseBranchName(currentBranch);\n const result = spawnSync(\n 'gh', ['pr', 'list', '--head', base, '--state', 'open', '--json', 'number', '--jq', '.[0].number'],\n { cwd: repoRoot, encoding: 'utf8' },\n );\n if (result.status !== 0) {\n const detail = (result.stderr ?? '').trim() || (result.error ? result.error.message : 'gh exited non-zero');\n throw new CliExitError(2,\n '\\n' + SEP + '❌ Could not ask GitHub whether an open PR exists for this branch\\n' + SEP + '\\n' +\n `gh failed for base branch \"${base}\": ${detail}\\n\\n` +\n 'This check must NOT be skipped: a 3-point update creates a NEW branch generation, so if a\\n' +\n 'PR already exists, wp-start-update would leave it pointing at the OLD branch. Fix gh first\\n' +\n '(install / `gh auth login` / restore network), then re-run — or, if a PR does exist, use\\n' +\n 'the PR flow: pnpm wp-start-upsert-pr → /wp-merge (only on conflict) →\\n' +\n 'pnpm wp-review-upsert-pr → write review.json → pnpm wp-finish-upsert-pr\\n',\n );\n }\n return (result.stdout ?? '').trim();\n }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';
|
|
2
|
+
import { ChecklistNotice } from './checklist-notice';
|
|
3
|
+
/**
|
|
4
|
+
* Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an
|
|
5
|
+
* object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional
|
|
6
|
+
* facts about the scan that default to "nothing", so a repo with no checklists constructs it in one line.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ReviewReportInput {
|
|
9
|
+
repoRoot: string;
|
|
10
|
+
featureName: string;
|
|
11
|
+
reviewPath: string;
|
|
12
|
+
definedCount: number;
|
|
13
|
+
applicableCount: number;
|
|
14
|
+
reviewed: RequiredChecklist[];
|
|
15
|
+
formatErrors: string[];
|
|
16
|
+
briefings: ReviewerBriefing[];
|
|
17
|
+
constructor(repoRoot: string, featureName: string, reviewPath: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE "what to do
|
|
21
|
+
* next".
|
|
22
|
+
*
|
|
23
|
+
* Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the
|
|
24
|
+
* contract. The bug it was extracted to fix: the zero-checklist notice ended with "Carry on and run: pnpm
|
|
25
|
+
* wp-finish-upsert-pr", and the block printed directly beneath it said to write review.json first and
|
|
26
|
+
* finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —
|
|
27
|
+
* which is the entire reason this command prints them — took the first one and opened a PR with no review.
|
|
28
|
+
* That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).
|
|
29
|
+
*
|
|
30
|
+
* The invariants, enforced by review-report.spec.ts:
|
|
31
|
+
* 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.
|
|
32
|
+
* 2. The review.json instruction comes BEFORE it.
|
|
33
|
+
* 3. With zero checklists the all-clear precedes any configuration guidance.
|
|
34
|
+
*
|
|
35
|
+
* Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.
|
|
36
|
+
*/
|
|
37
|
+
export declare class ReviewReport {
|
|
38
|
+
private readonly checklistNotice;
|
|
39
|
+
private readonly reviewerInstructions;
|
|
40
|
+
constructor(checklistNotice: ChecklistNotice, reviewerInstructions: ReviewerInstructionsService);
|
|
41
|
+
render(input: ReviewReportInput): string;
|
|
42
|
+
private header;
|
|
43
|
+
private checklistSection;
|
|
44
|
+
/**
|
|
45
|
+
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
46
|
+
* ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
|
|
47
|
+
* can be mistaken for the real next action.
|
|
48
|
+
*
|
|
49
|
+
* The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here
|
|
50
|
+
* can never drift from the shape `wp-finish-upsert-pr` validates.
|
|
51
|
+
*/
|
|
52
|
+
private nextSteps;
|
|
53
|
+
/**
|
|
54
|
+
* One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the
|
|
55
|
+
* generated instructions file is the contract, so anything restated here is a second copy that can go
|
|
56
|
+
* stale — which is exactly how a removed `success` field outlived its own removal in print.
|
|
57
|
+
*/
|
|
58
|
+
private spawnBlocks;
|
|
59
|
+
private oneSpawnBlock;
|
|
60
|
+
private why;
|
|
61
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReviewReport = exports.ReviewReportInput = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const checklist_notice_1 = require("./checklist-notice");
|
|
8
|
+
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
9
|
+
/**
|
|
10
|
+
* Everything the closing block of `wp-review-upsert-pr` needs. Data-only, and a class rather than an
|
|
11
|
+
* object literal per CLAUDE.md. The three identifying paths are constructor args; the rest are optional
|
|
12
|
+
* facts about the scan that default to "nothing", so a repo with no checklists constructs it in one line.
|
|
13
|
+
*/
|
|
14
|
+
class ReviewReportInput {
|
|
15
|
+
repoRoot;
|
|
16
|
+
featureName;
|
|
17
|
+
reviewPath; // the branch's review.json — the file the AI must write next
|
|
18
|
+
definedCount; // how many checklists pr-gate.checklists defines
|
|
19
|
+
applicableCount; // how many of them apply to THIS diff (0 ⇒ the notice, not spawn blocks)
|
|
20
|
+
reviewed; // already have a passing verdict on this branch
|
|
21
|
+
formatErrors; // verdict files that exist but cannot be read as verdicts
|
|
22
|
+
briefings; // one per applicable checklist, already written to disk
|
|
23
|
+
constructor(repoRoot, featureName, reviewPath) {
|
|
24
|
+
this.repoRoot = repoRoot;
|
|
25
|
+
this.featureName = featureName;
|
|
26
|
+
this.reviewPath = reviewPath;
|
|
27
|
+
this.definedCount = 0;
|
|
28
|
+
this.applicableCount = 0;
|
|
29
|
+
this.reviewed = [];
|
|
30
|
+
this.formatErrors = [];
|
|
31
|
+
this.briefings = [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.ReviewReportInput = ReviewReportInput;
|
|
35
|
+
/**
|
|
36
|
+
* Renders the closing block of `wp-review-upsert-pr` — the checklist verdict, then EXACTLY ONE "what to do
|
|
37
|
+
* next".
|
|
38
|
+
*
|
|
39
|
+
* Extracted from the command so it can be asserted on as a rendered string, because the ordering IS the
|
|
40
|
+
* contract. The bug it was extracted to fix: the zero-checklist notice ended with "Carry on and run: pnpm
|
|
41
|
+
* wp-finish-upsert-pr", and the block printed directly beneath it said to write review.json first and
|
|
42
|
+
* finish afterwards. Two next-steps, in the wrong order, and an agent that follows instructions literally —
|
|
43
|
+
* which is the entire reason this command prints them — took the first one and opened a PR with no review.
|
|
44
|
+
* That is precisely the failure the three-stage flow exists to prevent (reported on PR #519).
|
|
45
|
+
*
|
|
46
|
+
* The invariants, enforced by review-report.spec.ts:
|
|
47
|
+
* 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.
|
|
48
|
+
* 2. The review.json instruction comes BEFORE it.
|
|
49
|
+
* 3. With zero checklists the all-clear precedes any configuration guidance.
|
|
50
|
+
*
|
|
51
|
+
* Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.
|
|
52
|
+
*/
|
|
53
|
+
let ReviewReport = class ReviewReport {
|
|
54
|
+
checklistNotice;
|
|
55
|
+
reviewerInstructions;
|
|
56
|
+
constructor(checklistNotice, reviewerInstructions) {
|
|
57
|
+
this.checklistNotice = checklistNotice;
|
|
58
|
+
this.reviewerInstructions = reviewerInstructions;
|
|
59
|
+
}
|
|
60
|
+
render(input) {
|
|
61
|
+
return '\n' + SEP + this.header(input) + SEP + '\n'
|
|
62
|
+
+ this.checklistSection(input)
|
|
63
|
+
+ this.nextSteps(input.reviewPath);
|
|
64
|
+
}
|
|
65
|
+
// Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo
|
|
66
|
+
// with none is not, and promising subagents it does not have is the same kind of noise as explaining
|
|
67
|
+
// checklist configuration to a repo that configured none.
|
|
68
|
+
header(input) {
|
|
69
|
+
if (input.applicableCount === 0)
|
|
70
|
+
return '② Review, then finish\n';
|
|
71
|
+
return '② Spawn Subagent Reviews, then finish\n';
|
|
72
|
+
}
|
|
73
|
+
// Either "nothing applies here" or the copy-paste spawn blocks — never both, and neither one ends with
|
|
74
|
+
// a command to run: the single next step lives in nextSteps() below.
|
|
75
|
+
checklistSection(input) {
|
|
76
|
+
if (input.applicableCount === 0)
|
|
77
|
+
return this.checklistNotice.build(input.definedCount);
|
|
78
|
+
return this.spawnBlocks(input);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
|
|
82
|
+
* ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
|
|
83
|
+
* can be mistaken for the real next action.
|
|
84
|
+
*
|
|
85
|
+
* The schema hint is rendered by ReviewJsonService — the single renderer — so the shape printed here
|
|
86
|
+
* can never drift from the shape `wp-finish-upsert-pr` validates.
|
|
87
|
+
*/
|
|
88
|
+
nextSteps(reviewPath) {
|
|
89
|
+
return ('\n' + SEP +
|
|
90
|
+
'▶ NEXT — two steps, in this order. Step 1 is NOT optional:\n' + SEP + '\n' +
|
|
91
|
+
'STEP 1 — review your own changes, then write the review file (finish REFUSES without it).\n' +
|
|
92
|
+
' Do this WHILE any reviewer subagents above are still running:\n\n' +
|
|
93
|
+
(0, rules_config_1.reviewJsonSchemaHint)(reviewPath) + '\n\n' +
|
|
94
|
+
'STEP 2 — only once that file exists, run: pnpm wp-finish-upsert-pr\n' +
|
|
95
|
+
' (The build gate is already green for this commit — finish reuses it unless HEAD moves.)\n\n');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the
|
|
99
|
+
* generated instructions file is the contract, so anything restated here is a second copy that can go
|
|
100
|
+
* stale — which is exactly how a removed `success` field outlived its own removal in print.
|
|
101
|
+
*/
|
|
102
|
+
spawnBlocks(input) {
|
|
103
|
+
const reviewedIds = new Set(input.reviewed.map((r) => r.id));
|
|
104
|
+
const owed = input.briefings.filter((b) => !reviewedIds.has(b.checklistId));
|
|
105
|
+
const lines = [];
|
|
106
|
+
for (const r of input.reviewed) {
|
|
107
|
+
lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)`);
|
|
108
|
+
}
|
|
109
|
+
// A verdict file that EXISTS but is unreadable as a verdict is called out here. Without it this
|
|
110
|
+
// reports the checklist as simply owed, and the AI re-runs a reviewer that already ran instead of
|
|
111
|
+
// correcting the file sitting right there.
|
|
112
|
+
for (const e of input.formatErrors)
|
|
113
|
+
lines.push(` ⛔ ${e}`);
|
|
114
|
+
if (owed.length === 0) {
|
|
115
|
+
lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');
|
|
116
|
+
return lines.join('\n') + '\n';
|
|
117
|
+
}
|
|
118
|
+
lines.push('', `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`, 'work, and you may NOT write a reviewer\'s verdict file on its behalf.', '');
|
|
119
|
+
for (const b of owed)
|
|
120
|
+
lines.push(...this.oneSpawnBlock(input, b));
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
oneSpawnBlock(input, b) {
|
|
124
|
+
return [
|
|
125
|
+
` ▶ ${b.subagent} — ${this.why(b)}`,
|
|
126
|
+
` subagent_type: ${b.subagent}`,
|
|
127
|
+
' prompt: Read your instructions file FIRST and follow it exactly:',
|
|
128
|
+
` ${this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent)}`,
|
|
129
|
+
'',
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
// Why this one is in scope. A patternless checklist is NOT "matched" — it always runs, over the whole
|
|
133
|
+
// diff, and saying so is what tells a repo its checklist is firing on docs-only PRs by design.
|
|
134
|
+
why(b) {
|
|
135
|
+
if (b.matchedPatterns.length === 0) {
|
|
136
|
+
return `ALWAYS RUNS (no "patterns" configured), whole diff in scope — ${b.myFiles.length} file(s)`;
|
|
137
|
+
}
|
|
138
|
+
return `${b.myFiles.length} file(s) matched ${b.matchedPatterns.map((p) => `"${p}"`).join(', ')}`;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
exports.ReviewReport = ReviewReport;
|
|
142
|
+
exports.ReviewReport = ReviewReport = tslib_1.__decorate([
|
|
143
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
144
|
+
tslib_1.__metadata("design:paramtypes", [checklist_notice_1.ChecklistNotice,
|
|
145
|
+
rules_config_1.ReviewerInstructionsService])
|
|
146
|
+
], ReviewReport);
|
|
147
|
+
//# sourceMappingURL=review-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 { reviewJsonSchemaHint, RequiredChecklist, ReviewerBriefing, ReviewerInstructionsService } from '@webpieces/rules-config';\nimport { ChecklistNotice } from './checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\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\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 }\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.\n * 3. With zero checklists the all-clear precedes any configuration guidance.\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'\n + this.checklistSection(input)\n + this.nextSteps(input.reviewPath);\n }\n\n // Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo\n // with none is not, and promising subagents it does not have is the same kind of noise as explaining\n // checklist configuration to a repo that configured none.\n private header(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return '② Review, then finish\\n';\n return '② Spawn Subagent Reviews, then finish\\n';\n }\n\n // Either \"nothing applies here\" or the copy-paste spawn blocks — never both, and neither one ends with\n // a command to run: the single next step lives in nextSteps() below.\n private checklistSection(input: ReviewReportInput): string {\n if (input.applicableCount === 0) return this.checklistNotice.build(input.definedCount);\n return this.spawnBlocks(input);\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 * 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(reviewPath: string): string {\n return (\n '\\n' + SEP +\n '▶ NEXT — two steps, in this order. Step 1 is NOT optional:\\n' + SEP + '\\n' +\n 'STEP 1 — review your own changes, then write the review file (finish REFUSES without it).\\n' +\n ' Do this WHILE any reviewer subagents above are still running:\\n\\n' +\n reviewJsonSchemaHint(reviewPath) + '\\n\\n' +\n 'STEP 2 — only once that file exists, 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 /**\n * One copy-paste block per owed reviewer. The prompt is deliberately a POINTER and nothing else: the\n * generated instructions file is the contract, so anything restated here is a second copy that can go\n * stale — which is exactly how a removed `success` field outlived its own removal in print.\n */\n private spawnBlocks(input: ReviewReportInput): string {\n const reviewedIds = new Set(input.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = input.briefings.filter((b: ReviewerBriefing): boolean => !reviewedIds.has(b.checklistId));\n const lines: string[] = [];\n for (const r of input.reviewed) {\n lines.push(` ✓ ${r.subagent} — already reviewed on this branch (reusing its 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 if (owed.length === 0) {\n lines.push('', '✅ Every checklist that applies is already reviewed — nothing to spawn.');\n return lines.join('\\n') + '\\n';\n }\n lines.push('',\n `Spawn these ${owed.length} reviewer subagent(s) — a SEPARATE one each. You may NOT review your own`,\n 'work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n '');\n for (const b of owed) lines.push(...this.oneSpawnBlock(input, b));\n return lines.join('\\n');\n }\n\n private oneSpawnBlock(input: ReviewReportInput, b: ReviewerBriefing): string[] {\n return [\n ` ▶ ${b.subagent} — ${this.why(b)}`,\n ` subagent_type: ${b.subagent}`,\n ' prompt: Read your instructions file FIRST and follow it exactly:',\n ` ${this.reviewerInstructions.pathFor(input.repoRoot, input.featureName, b.subagent)}`,\n '',\n ];\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"]}
|