@webpieces/pr-gate 0.4.485 → 0.4.487
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/checklist-command.d.ts +14 -22
- package/src/scripts/commands/checklist-command.js +47 -54
- package/src/scripts/commands/checklist-command.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +8 -5
- package/src/scripts/commands/finish-upsert-pr-command.js +47 -34
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/commands/start-upsert-pr-command.d.ts +6 -5
- package/src/scripts/commands/start-upsert-pr-command.js +10 -22
- package/src/scripts/commands/start-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/checklist-scanner.d.ts +72 -0
- package/src/scripts/workflow/checklist-scanner.js +125 -0
- package/src/scripts/workflow/checklist-scanner.js.map +1 -0
- package/src/scripts/workflow/git-findForkPoint.d.ts +15 -0
- package/src/scripts/workflow/git-findForkPoint.js +27 -3
- package/src/scripts/workflow/git-findForkPoint.js.map +1 -1
- package/src/scripts/workflow/pr-context-writer.d.ts +29 -0
- package/src/scripts/workflow/pr-context-writer.js +52 -0
- package/src/scripts/workflow/pr-context-writer.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.487",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/tooling/pr-gate"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
28
|
+
"@webpieces/rules-config": "0.4.487",
|
|
29
29
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
30
30
|
"inversify": "7.10.4",
|
|
31
31
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,34 +1,26 @@
|
|
|
1
|
-
import { RepoRootFinder, ChecklistInstructionsService
|
|
2
|
-
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
|
-
import { ChecklistDetector } from '../workflow/checklist-detector';
|
|
1
|
+
import { RepoRootFinder, ChecklistInstructionsService } from '@webpieces/rules-config';
|
|
4
2
|
import { ChecklistNotice } from '../workflow/checklist-notice';
|
|
3
|
+
import { ChecklistScanner } from '../workflow/checklist-scanner';
|
|
5
4
|
/**
|
|
6
|
-
* `wp-checklist` —
|
|
7
|
-
* subagents do I owe on
|
|
5
|
+
* `wp-checklist` — ALWAYS SUCCEEDS, always just lists. It answers the one question the coding agent has at
|
|
6
|
+
* review time: *which reviewer subagents do I owe on this branch, and what exactly do I tell them?*
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* that
|
|
13
|
-
*
|
|
14
|
-
* It NEVER mutates the repo beyond refreshing the AI-facing template, and never blocks: a diff owing no
|
|
15
|
-
* review says so and exits 0. Safe to run any number of times.
|
|
8
|
+
* Safe to run at any time, including mid-work with a dirty tree — uncommitted and untracked files are part
|
|
9
|
+
* of the diff it matches against (see {@link ChecklistScanner}). It exits 0 in every case: zero checklists
|
|
10
|
+
* configured, zero matched, or several still outstanding. Reporting is not gating; `wp-finish-upsert-pr` is
|
|
11
|
+
* the only command that refuses, and it re-uses this exact scan with `filterAlreadyReviewed: true`, so the
|
|
12
|
+
* two can never disagree about what is owed.
|
|
16
13
|
*/
|
|
17
14
|
export declare class ChecklistCommand {
|
|
18
15
|
private readonly repoRootFinder;
|
|
19
|
-
private readonly
|
|
20
|
-
private readonly checklistDetector;
|
|
21
|
-
private readonly reviewJsonService;
|
|
16
|
+
private readonly checklistScanner;
|
|
22
17
|
private readonly instructions;
|
|
23
18
|
private readonly checklistNotice;
|
|
24
|
-
constructor(repoRootFinder: RepoRootFinder,
|
|
19
|
+
constructor(repoRootFinder: RepoRootFinder, checklistScanner: ChecklistScanner, instructions: ChecklistInstructionsService, checklistNotice: ChecklistNotice);
|
|
25
20
|
run(): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* The whole report. Computed in the same order wp-finish-upsert-pr computes it, from the same services,
|
|
28
|
-
* so "wp-checklist said I was done" and "wp-finish let me through" can never disagree.
|
|
29
|
-
*/
|
|
30
21
|
private report;
|
|
31
|
-
private
|
|
32
|
-
private
|
|
22
|
+
private header;
|
|
23
|
+
private rosterLines;
|
|
24
|
+
private why;
|
|
33
25
|
private allDone;
|
|
34
26
|
}
|
|
@@ -4,34 +4,27 @@ exports.ChecklistCommand = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
6
6
|
const inversify_1 = require("inversify");
|
|
7
|
-
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
8
|
-
const checklist_detector_1 = require("../workflow/checklist-detector");
|
|
9
7
|
const checklist_notice_1 = require("../workflow/checklist-notice");
|
|
8
|
+
const checklist_scanner_1 = require("../workflow/checklist-scanner");
|
|
10
9
|
const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
|
11
10
|
/**
|
|
12
|
-
* `wp-checklist` —
|
|
13
|
-
* subagents do I owe on
|
|
11
|
+
* `wp-checklist` — ALWAYS SUCCEEDS, always just lists. It answers the one question the coding agent has at
|
|
12
|
+
* review time: *which reviewer subagents do I owe on this branch, and what exactly do I tell them?*
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* that
|
|
19
|
-
*
|
|
20
|
-
* It NEVER mutates the repo beyond refreshing the AI-facing template, and never blocks: a diff owing no
|
|
21
|
-
* review says so and exits 0. Safe to run any number of times.
|
|
14
|
+
* Safe to run at any time, including mid-work with a dirty tree — uncommitted and untracked files are part
|
|
15
|
+
* of the diff it matches against (see {@link ChecklistScanner}). It exits 0 in every case: zero checklists
|
|
16
|
+
* configured, zero matched, or several still outstanding. Reporting is not gating; `wp-finish-upsert-pr` is
|
|
17
|
+
* the only command that refuses, and it re-uses this exact scan with `filterAlreadyReviewed: true`, so the
|
|
18
|
+
* two can never disagree about what is owed.
|
|
22
19
|
*/
|
|
23
20
|
let ChecklistCommand = class ChecklistCommand {
|
|
24
21
|
repoRootFinder;
|
|
25
|
-
|
|
26
|
-
checklistDetector;
|
|
27
|
-
reviewJsonService;
|
|
22
|
+
checklistScanner;
|
|
28
23
|
instructions;
|
|
29
24
|
checklistNotice;
|
|
30
|
-
constructor(repoRootFinder,
|
|
25
|
+
constructor(repoRootFinder, checklistScanner, instructions, checklistNotice) {
|
|
31
26
|
this.repoRootFinder = repoRootFinder;
|
|
32
|
-
this.
|
|
33
|
-
this.checklistDetector = checklistDetector;
|
|
34
|
-
this.reviewJsonService = reviewJsonService;
|
|
27
|
+
this.checklistScanner = checklistScanner;
|
|
35
28
|
this.instructions = instructions;
|
|
36
29
|
this.checklistNotice = checklistNotice;
|
|
37
30
|
}
|
|
@@ -39,56 +32,56 @@ let ChecklistCommand = class ChecklistCommand {
|
|
|
39
32
|
const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());
|
|
40
33
|
// Refresh the long-form doc so the block below can cite a file that is present and current.
|
|
41
34
|
(0, rules_config_1.writeTemplate)(repoRoot, 'webpieces.review-checklists.md');
|
|
42
|
-
process.stdout.write('\n' + SEP + '📋 Review checklists for this
|
|
43
|
-
|
|
35
|
+
process.stdout.write('\n' + SEP + '📋 Review checklists for this branch\n' + SEP + '\n');
|
|
36
|
+
// filterAlreadyReviewed:false — LIST everything that applies, marking what is already done. Hiding the
|
|
37
|
+
// done ones would make a second run look like it found fewer checklists than the first.
|
|
38
|
+
const defined = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklists;
|
|
39
|
+
process.stdout.write(this.report(this.checklistScanner.scan(repoRoot, defined, new checklist_scanner_1.ChecklistScanOptions(false))));
|
|
44
40
|
return Promise.resolve();
|
|
45
41
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* so "wp-checklist said I was done" and "wp-finish let me through" can never disagree.
|
|
49
|
-
*/
|
|
50
|
-
report(repoRoot) {
|
|
51
|
-
// loadAndValidate is the ONE gate: it rejects a structurally bad `checklists` (including the removed
|
|
52
|
-
// { doc } manifest shape) AND checks that every guidance doc + reviewer agent file exists. So reaching
|
|
53
|
-
// here means the set is valid — there is no "configured but silently broken" state left to report.
|
|
54
|
-
const defs = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklists;
|
|
55
|
-
const triggered = this.checklistDetector.detectForRepo(repoRoot, defs);
|
|
56
|
-
if (triggered.length === 0) {
|
|
42
|
+
report(scan) {
|
|
43
|
+
if (scan.applicable.length === 0) {
|
|
57
44
|
// Zero is a SUPPORTED state, never a blocker (see ChecklistNotice).
|
|
58
|
-
return this.checklistNotice.build(
|
|
45
|
+
return this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr');
|
|
59
46
|
}
|
|
60
|
-
|
|
47
|
+
const reviewedIds = new Set(scan.reviewed.map((r) => r.id));
|
|
48
|
+
const owed = scan.applicable.filter((r) => !reviewedIds.has(r.id));
|
|
49
|
+
return (this.header(scan) +
|
|
50
|
+
this.rosterLines(scan, owed) +
|
|
51
|
+
(owed.length === 0 ? this.allDone() : `\n${this.instructions.render(owed, scan.reviewPath, scan.context)}\n`));
|
|
52
|
+
}
|
|
53
|
+
// State the base out loud: a reader (and a reviewer) needs to know the diff was taken from the fork point
|
|
54
|
+
// of main and that uncommitted work counted, because both change which checklists fired.
|
|
55
|
+
header(scan) {
|
|
56
|
+
const base = scan.forkPoint === '' ? 'no fork point resolved' : `fork point ${scan.forkPoint.slice(0, 8)}`;
|
|
57
|
+
return `${scan.defined.length} checklist(s) configured, ${scan.applicable.length} apply to this branch\n` +
|
|
58
|
+
`(${base}, including uncommitted + untracked work).\n\n`;
|
|
61
59
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const done = required.filter((r) => !pending.includes(r));
|
|
70
|
-
const context = this.reviewJsonService.reviewContextFor(repoRoot, featureName);
|
|
71
|
-
return (`${defs.length} checklist(s) configured, ${required.length} apply to this diff.\n` +
|
|
72
|
-
this.doneLines(done) +
|
|
73
|
-
(pending.length === 0 ? this.allDone() : `\n${this.instructions.render(pending, reviewPath, context)}\n`));
|
|
60
|
+
// REVIEW ONCE, PER SUBAGENT: a checklist with a passing verdict is reused, not re-run. 2 of 4 done means
|
|
61
|
+
// the other 2 still need running, so both groups are shown and only the owed ones become instructions.
|
|
62
|
+
rosterLines(scan, owed) {
|
|
63
|
+
const lines = scan.reviewed.map((r) => ` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)\n`);
|
|
64
|
+
for (const r of owed)
|
|
65
|
+
lines.push(` ▶ ${r.subagent} — ${this.why(r)}\n`);
|
|
66
|
+
return lines.join('');
|
|
74
67
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
return '';
|
|
79
|
-
|
|
68
|
+
// Why this one is in scope. A patternless checklist is NOT "matched" — it always runs, over the whole diff.
|
|
69
|
+
why(req) {
|
|
70
|
+
if (req.matchedPatterns.length === 0)
|
|
71
|
+
return 'ALWAYS RUNS (no patterns), whole diff in scope';
|
|
72
|
+
const globs = req.matchedPatterns.map((p) => `"${p}"`).join(', ');
|
|
73
|
+
return `${req.matchedFiles.length} file(s) matched ${globs}`;
|
|
80
74
|
}
|
|
81
75
|
allDone() {
|
|
82
|
-
return '\n✅ Every checklist that applies is already reviewed — nothing to run.\n
|
|
76
|
+
return '\n✅ Every checklist that applies is already reviewed — nothing to run.\n' +
|
|
77
|
+
' Write review.json and run: pnpm wp-finish-upsert-pr\n';
|
|
83
78
|
}
|
|
84
79
|
};
|
|
85
80
|
exports.ChecklistCommand = ChecklistCommand;
|
|
86
81
|
exports.ChecklistCommand = ChecklistCommand = tslib_1.__decorate([
|
|
87
82
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
88
83
|
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
89
|
-
|
|
90
|
-
checklist_detector_1.ChecklistDetector,
|
|
91
|
-
rules_config_1.ReviewJsonService,
|
|
84
|
+
checklist_scanner_1.ChecklistScanner,
|
|
92
85
|
rules_config_1.ChecklistInstructionsService,
|
|
93
86
|
checklist_notice_1.ChecklistNotice])
|
|
94
87
|
], ChecklistCommand);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checklist-command.ts"],"names":[],"mappings":";;;;AAAA,
|
|
1
|
+
{"version":3,"file":"checklist-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checklist-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { loadAndValidate, writeTemplate, RepoRootFinder, ChecklistInstructionsService, RequiredChecklist } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checklist` — ALWAYS SUCCEEDS, always just lists. It answers the one question the coding agent has at\n * review time: *which reviewer subagents do I owe on this branch, and what exactly do I tell them?*\n *\n * Safe to run at any time, including mid-work with a dirty tree — uncommitted and untracked files are part\n * of the diff it matches against (see {@link ChecklistScanner}). It exits 0 in every case: zero checklists\n * configured, zero matched, or several still outstanding. Reporting is not gating; `wp-finish-upsert-pr` is\n * the only command that refuses, and it re-uses this exact scan with `filterAlreadyReviewed: true`, so the\n * two can never disagree about what is owed.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly checklistScanner: ChecklistScanner,\n private readonly instructions: ChecklistInstructionsService,\n private readonly checklistNotice: ChecklistNotice,\n ) {}\n\n run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the long-form doc so the block below can cite a file that is present and current.\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n process.stdout.write('\\n' + SEP + '📋 Review checklists for this branch\\n' + SEP + '\\n');\n // filterAlreadyReviewed:false — LIST everything that applies, marking what is already done. Hiding the\n // done ones would make a second run look like it found fewer checklists than the first.\n const defined = loadAndValidate(repoRoot).prGate.checklists;\n process.stdout.write(this.report(this.checklistScanner.scan(repoRoot, defined, new ChecklistScanOptions(false))));\n return Promise.resolve();\n }\n\n private report(scan: ChecklistScan): string {\n if (scan.applicable.length === 0) {\n // Zero is a SUPPORTED state, never a blocker (see ChecklistNotice).\n return this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr');\n }\n const reviewedIds = new Set(scan.reviewed.map((r: RequiredChecklist): string => r.id));\n const owed = scan.applicable.filter((r: RequiredChecklist): boolean => !reviewedIds.has(r.id));\n return (\n this.header(scan) +\n this.rosterLines(scan, owed) +\n (owed.length === 0 ? this.allDone() : `\\n${this.instructions.render(owed, scan.reviewPath, scan.context)}\\n`)\n );\n }\n\n // State the base out loud: a reader (and a reviewer) needs to know the diff was taken from the fork point\n // of main and that uncommitted work counted, because both change which checklists fired.\n private header(scan: ChecklistScan): string {\n const base = scan.forkPoint === '' ? 'no fork point resolved' : `fork point ${scan.forkPoint.slice(0, 8)}`;\n return `${scan.defined.length} checklist(s) configured, ${scan.applicable.length} apply to this branch\\n` +\n `(${base}, including uncommitted + untracked work).\\n\\n`;\n }\n\n // REVIEW ONCE, PER SUBAGENT: a checklist with a passing verdict is reused, not re-run. 2 of 4 done means\n // the other 2 still need running, so both groups are shown and only the owed ones become instructions.\n private rosterLines(scan: ChecklistScan, owed: readonly RequiredChecklist[]): string {\n const lines = scan.reviewed.map((r: RequiredChecklist): string =>\n ` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)\\n`);\n for (const r of owed) lines.push(` ▶ ${r.subagent} — ${this.why(r)}\\n`);\n return lines.join('');\n }\n\n // Why this one is in scope. A patternless checklist is NOT \"matched\" — it always runs, over the whole diff.\n private why(req: RequiredChecklist): string {\n if (req.matchedPatterns.length === 0) return 'ALWAYS RUNS (no patterns), whole diff in scope';\n const globs = req.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ');\n return `${req.matchedFiles.length} file(s) matched ${globs}`;\n }\n\n private allDone(): string {\n return '\\n✅ Every checklist that applies is already reviewed — nothing to run.\\n' +\n ' Write review.json and run: pnpm wp-finish-upsert-pr\\n';\n }\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RepoRootFinder, ReviewJsonService, GateTokenService, SubagentProvenanceService, ChecklistInstructionsService } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BranchNaming } from '../workflow/branch-naming';
|
|
4
|
-
import {
|
|
4
|
+
import { ChecklistScanner } from '../workflow/checklist-scanner';
|
|
5
5
|
import { GitExec } from '../workflow/git-exec';
|
|
6
6
|
import { BuildAffected } from '../workflow/build-affected';
|
|
7
7
|
import { MergeState } from '../workflow/merge-state';
|
|
@@ -20,18 +20,21 @@ export declare class FinishUpsertPrCommand {
|
|
|
20
20
|
private readonly prMerger;
|
|
21
21
|
private readonly publisher;
|
|
22
22
|
private readonly dashboard;
|
|
23
|
-
private readonly
|
|
23
|
+
private readonly checklistScanner;
|
|
24
24
|
private readonly reviewJsonService;
|
|
25
25
|
private readonly gateTokenService;
|
|
26
26
|
private readonly provenance;
|
|
27
27
|
private readonly instructions;
|
|
28
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard,
|
|
28
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, mergeEnd: MergeEnd, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistScanner: ChecklistScanner, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, instructions: ChecklistInstructionsService);
|
|
29
29
|
run(): Promise<void>;
|
|
30
|
+
private emitClickableLinkDirective;
|
|
30
31
|
private finalizeAnyInProgressMerge;
|
|
31
32
|
/**
|
|
32
33
|
* Refuse the PR while ANY applicable checklist still owes a verdict, naming exactly those reviewers and
|
|
33
|
-
* exactly what to tell them
|
|
34
|
-
*
|
|
34
|
+
* exactly what to tell them — via the same renderer `wp-checklist` uses, so the AI sees the identical
|
|
35
|
+
* block it would have seen there. Lists ONLY the outstanding ones: re-instructing an already-reviewed
|
|
36
|
+
* checklist invites a redundant second run and reads as though the earlier verdict did not count.
|
|
37
|
+
* No-op for a repo with no applicable checklists.
|
|
35
38
|
*/
|
|
36
39
|
private assertEveryReviewerRan;
|
|
37
40
|
private gitOut;
|
|
@@ -9,7 +9,7 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
9
9
|
const inversify_1 = require("inversify");
|
|
10
10
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
11
11
|
const branch_naming_1 = require("../workflow/branch-naming");
|
|
12
|
-
const
|
|
12
|
+
const checklist_scanner_1 = require("../workflow/checklist-scanner");
|
|
13
13
|
const git_exec_1 = require("../workflow/git-exec");
|
|
14
14
|
const build_affected_1 = require("../workflow/build-affected");
|
|
15
15
|
const merge_state_1 = require("../workflow/merge-state");
|
|
@@ -28,13 +28,16 @@ class PrRef {
|
|
|
28
28
|
this.url = url;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// The outcome of the whole upsert: the PR number ('' when
|
|
32
|
-
// happened to the merge, so the final summary reports the REAL result rather than assuming success.
|
|
31
|
+
// The outcome of the whole upsert: the PR number + web URL ('' each when unresolved) plus what actually
|
|
32
|
+
// happened to the merge, so the final summary reports the REAL result rather than assuming success. The
|
|
33
|
+
// URL is carried so the closing block can hand the AI a clickable link to relay to the user verbatim.
|
|
33
34
|
class UpsertResult {
|
|
34
35
|
prNumber;
|
|
36
|
+
prUrl;
|
|
35
37
|
merge;
|
|
36
|
-
constructor(prNumber, merge) {
|
|
38
|
+
constructor(prNumber, prUrl, merge) {
|
|
37
39
|
this.prNumber = prNumber;
|
|
40
|
+
this.prUrl = prUrl;
|
|
38
41
|
this.merge = merge;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -53,12 +56,12 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
53
56
|
prMerger;
|
|
54
57
|
publisher;
|
|
55
58
|
dashboard;
|
|
56
|
-
|
|
59
|
+
checklistScanner;
|
|
57
60
|
reviewJsonService;
|
|
58
61
|
gateTokenService;
|
|
59
62
|
provenance;
|
|
60
63
|
instructions;
|
|
61
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, prMerger, publisher, dashboard,
|
|
64
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, mergeEnd, prMerger, publisher, dashboard, checklistScanner, reviewJsonService, gateTokenService, provenance, instructions) {
|
|
62
65
|
this.repoRootFinder = repoRootFinder;
|
|
63
66
|
this.aiBranchName = aiBranchName;
|
|
64
67
|
this.branchNaming = branchNaming;
|
|
@@ -69,7 +72,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
69
72
|
this.prMerger = prMerger;
|
|
70
73
|
this.publisher = publisher;
|
|
71
74
|
this.dashboard = dashboard;
|
|
72
|
-
this.
|
|
75
|
+
this.checklistScanner = checklistScanner;
|
|
73
76
|
this.reviewJsonService = reviewJsonService;
|
|
74
77
|
this.gateTokenService = gateTokenService;
|
|
75
78
|
this.provenance = provenance;
|
|
@@ -84,18 +87,19 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
84
87
|
// 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).
|
|
85
88
|
// Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws
|
|
86
89
|
// here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
// EXISTING verdicts against the (possibly changed)
|
|
91
|
-
// needs no re-review, a newly-
|
|
92
|
-
//
|
|
93
|
-
// FAIL FAST on missing reviewers, BEFORE review.json is even parsed. A missing reviewer is not a
|
|
94
|
-
// review.json defect and folding it into that error made the AI fix the wrong thing; and the message
|
|
95
|
-
// lists ONLY the subagents that still owe a verdict, so an already-reviewed checklist is never
|
|
96
|
-
// re-instructed. Same computation + same renderer as `wp-checklist`, so the two cannot disagree.
|
|
90
|
+
// The SAME scan wp-checklist runs, with filterAlreadyReviewed:true so `outstanding` is exactly what
|
|
91
|
+
// still owes a verdict (Z of N of X). Sharing the computation is the point: the command that REPORTS
|
|
92
|
+
// and the command that GATES must not be able to disagree about what is owed. review-<id>.json files
|
|
93
|
+
// persist locally, so a re-run re-validates the EXISTING verdicts against the (possibly changed)
|
|
94
|
+
// applicable set for free — an unchanged checklist needs no re-review, a newly-applicable one refuses
|
|
95
|
+
// until its file is written.
|
|
97
96
|
const featureName = this.aiBranchName.getFeatureName();
|
|
98
|
-
this.
|
|
97
|
+
const scan = this.checklistScanner.scan(repoRoot, (0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklists, new checklist_scanner_1.ChecklistScanOptions(true));
|
|
98
|
+
const required = scan.applicable;
|
|
99
|
+
// FAIL FAST on missing reviewers, BEFORE review.json is parsed and before the build gate runs. A
|
|
100
|
+
// missing reviewer is not a review.json defect (folding it in made the AI fix the wrong thing), and
|
|
101
|
+
// nobody should wait on a build to be told a reviewer never ran.
|
|
102
|
+
this.assertEveryReviewerRan(scan);
|
|
99
103
|
const review = this.reviewJsonService.loadReviewJson((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), required);
|
|
100
104
|
// 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own
|
|
101
105
|
// artifacts) that such a subagent actually ran on this branch — the coding agent may not
|
|
@@ -129,6 +133,19 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
129
133
|
` 3. force-pushed your work to origin/${base} (after the body, so CI reads the right token)\n` +
|
|
130
134
|
` 4. ${result.merge.message}\n` +
|
|
131
135
|
` You are on ${base} — same name as the remote branch and the PR head.\n\n`);
|
|
136
|
+
// Close by handing the AI a clickable link + directive to end its summary with it (see helper).
|
|
137
|
+
this.emitClickableLinkDirective(prNum, result.prUrl, title);
|
|
138
|
+
}
|
|
139
|
+
// Print the closing AI directive: the resolved PR's number/title/URL and an instruction to end the
|
|
140
|
+
// user-facing summary with EXACTLY `[#N title](url)` as the final line, nothing after it. No-op when
|
|
141
|
+
// the PR could not be resolved — better to say nothing than to point the user at a link that isn't real.
|
|
142
|
+
emitClickableLinkDirective(prNum, prUrl, title) {
|
|
143
|
+
if (prNum === '' || prUrl === '')
|
|
144
|
+
return;
|
|
145
|
+
process.stdout.write(SEP + `🔗 PR #${prNum}: ${title}\n ${prUrl}\n\n` + SEP +
|
|
146
|
+
`AI — the LAST line of your summary to the user MUST be EXACTLY this clickable link, with\n` +
|
|
147
|
+
`NOTHING printed after it (no sign-off, no next steps, no blank commentary):\n\n` +
|
|
148
|
+
` [#${prNum} ${title}](${prUrl})\n\n`);
|
|
132
149
|
}
|
|
133
150
|
// Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here
|
|
134
151
|
// does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only
|
|
@@ -143,21 +160,17 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
143
160
|
}
|
|
144
161
|
/**
|
|
145
162
|
* Refuse the PR while ANY applicable checklist still owes a verdict, naming exactly those reviewers and
|
|
146
|
-
* exactly what to tell them
|
|
147
|
-
*
|
|
163
|
+
* exactly what to tell them — via the same renderer `wp-checklist` uses, so the AI sees the identical
|
|
164
|
+
* block it would have seen there. Lists ONLY the outstanding ones: re-instructing an already-reviewed
|
|
165
|
+
* checklist invites a redundant second run and reads as though the earlier verdict did not count.
|
|
166
|
+
* No-op for a repo with no applicable checklists.
|
|
148
167
|
*/
|
|
149
|
-
assertEveryReviewerRan(
|
|
150
|
-
if (
|
|
151
|
-
return;
|
|
152
|
-
const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, featureName);
|
|
153
|
-
const results = this.reviewJsonService.loadChecklistResults(reviewPath, required);
|
|
154
|
-
const pending = this.reviewJsonService.pendingChecklists(required, results);
|
|
155
|
-
if (pending.length === 0)
|
|
168
|
+
assertEveryReviewerRan(scan) {
|
|
169
|
+
if (scan.outstanding.length === 0)
|
|
156
170
|
return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`${this.instructions.render(pending, reviewPath, context)}\n\n` +
|
|
171
|
+
throw new rules_config_1.InformAiError(`⛔ NO PR — ${scan.outstanding.length} of ${scan.applicable.length} review checklist(s) that apply to this ` +
|
|
172
|
+
`branch have no passing verdict yet: ${this.instructions.names(scan.outstanding)}\n\n` +
|
|
173
|
+
`${this.instructions.render(scan.outstanding, scan.reviewPath, scan.context)}\n\n` +
|
|
161
174
|
`Then re-run: pnpm wp-finish-upsert-pr (or check anytime with: pnpm wp-checklist)`);
|
|
162
175
|
}
|
|
163
176
|
gitOut(args) {
|
|
@@ -270,7 +283,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
270
283
|
const published = this.publisher.publish(baseBranch, title, bodyFile);
|
|
271
284
|
if (published.createFailed) {
|
|
272
285
|
process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\n ' + bodyFile + '\n');
|
|
273
|
-
return new UpsertResult('', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
|
|
286
|
+
return new UpsertResult('', '', new pr_merger_1.MergeOutcome(false, false, '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)'));
|
|
274
287
|
}
|
|
275
288
|
const num = published.number;
|
|
276
289
|
// Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an
|
|
@@ -287,7 +300,7 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
287
300
|
// rules-config that has no such field) reaches PrMerger as '' and is treated as "do not merge".
|
|
288
301
|
const mergeMode = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.mergeMode ?? '';
|
|
289
302
|
const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);
|
|
290
|
-
return new UpsertResult(ref.number !== '' ? ref.number : num, outcome);
|
|
303
|
+
return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);
|
|
291
304
|
}
|
|
292
305
|
// The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''
|
|
293
306
|
// if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.
|
|
@@ -313,7 +326,7 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
|
|
|
313
326
|
pr_merger_1.PrMerger,
|
|
314
327
|
gated_pr_publisher_1.GatedPrPublisher,
|
|
315
328
|
dashboard_1.Dashboard,
|
|
316
|
-
|
|
329
|
+
checklist_scanner_1.ChecklistScanner,
|
|
317
330
|
rules_config_1.ReviewJsonService,
|
|
318
331
|
rules_config_1.GateTokenService,
|
|
319
332
|
rules_config_1.SubagentProvenanceService,
|