@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.485",
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.485",
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, ReviewJsonService } from '@webpieces/rules-config';
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` — READ-ONLY. Answers the one question the coding agent has at review time: *which reviewer
7
- * subagents do I owe on THIS diff, and what exactly do I tell them?*
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
- * It exists because that answer used to be smeared across `wp-start-upsert-pr`'s output in two partly
10
- * duplicated blocks, which made the instruction long, drift-prone, and easy to skim past. Now `wp-start`
11
- * points here, `wp-finish` fails fast against the same computation, and this command is the single place
12
- * that renders it via {@link ChecklistInstructionsService}, shared with both.
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 aiBranchName;
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, aiBranchName: AiBranchName, checklistDetector: ChecklistDetector, reviewJsonService: ReviewJsonService, instructions: ChecklistInstructionsService, checklistNotice: ChecklistNotice);
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 plan;
32
- private doneLines;
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` — READ-ONLY. Answers the one question the coding agent has at review time: *which reviewer
13
- * subagents do I owe on THIS diff, and what exactly do I tell them?*
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
- * It exists because that answer used to be smeared across `wp-start-upsert-pr`'s output in two partly
16
- * duplicated blocks, which made the instruction long, drift-prone, and easy to skim past. Now `wp-start`
17
- * points here, `wp-finish` fails fast against the same computation, and this command is the single place
18
- * that renders it via {@link ChecklistInstructionsService}, shared with both.
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
- aiBranchName;
26
- checklistDetector;
27
- reviewJsonService;
22
+ checklistScanner;
28
23
  instructions;
29
24
  checklistNotice;
30
- constructor(repoRootFinder, aiBranchName, checklistDetector, reviewJsonService, instructions, checklistNotice) {
25
+ constructor(repoRootFinder, checklistScanner, instructions, checklistNotice) {
31
26
  this.repoRootFinder = repoRootFinder;
32
- this.aiBranchName = aiBranchName;
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 diff\n' + SEP + '\n');
43
- process.stdout.write(this.report(repoRoot));
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
- * The whole report. Computed in the same order wp-finish-upsert-pr computes it, from the same services,
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(defs.length, 'wp-finish-upsert-pr');
45
+ return this.checklistNotice.build(scan.defined.length, 'wp-finish-upsert-pr');
59
46
  }
60
- return this.plan(repoRoot, defs, triggered);
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
- // The triggered set split into "already reviewed on this branch" (reused, not re-run) and "still owed".
63
- plan(repoRoot, defs, triggered) {
64
- const featureName = this.aiBranchName.getFeatureName();
65
- const reviewPath = (0, rules_config_1.reviewJsonPath)(repoRoot, featureName);
66
- const required = this.checklistDetector.toRequired(triggered);
67
- const results = this.reviewJsonService.loadChecklistResults(reviewPath, required);
68
- const pending = this.reviewJsonService.pendingChecklists(required, results);
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
- // REVIEW ONCE PER BRANCH: verdict files persist in .webpieces, so a second cycle re-instructs nothing.
76
- doneLines(done) {
77
- if (done.length === 0)
78
- return '';
79
- return done.map((r) => `${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)\n`).join('');
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 Write review.json and run: pnpm wp-finish-upsert-pr\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
- git_readAiBranchName_1.AiBranchName,
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":"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","sourcesContent":["import {\n loadAndValidate, reviewJsonPath, writeTemplate, RepoRootFinder,\n ChecklistDefinition, ChecklistInstructionsService, ReviewJsonService, RequiredChecklist,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { ChecklistDetector, TriggeredChecklist } from '../workflow/checklist-detector';\nimport { ChecklistNotice } from '../workflow/checklist-notice';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-checklist` — READ-ONLY. Answers the one question the coding agent has at review time: *which reviewer\n * subagents do I owe on THIS diff, and what exactly do I tell them?*\n *\n * It exists because that answer used to be smeared across `wp-start-upsert-pr`'s output in two partly\n * duplicated blocks, which made the instruction long, drift-prone, and easy to skim past. Now `wp-start`\n * points here, `wp-finish` fails fast against the same computation, and this command is the single place\n * that renders it via {@link ChecklistInstructionsService}, shared with both.\n *\n * It NEVER mutates the repo beyond refreshing the AI-facing template, and never blocks: a diff owing no\n * review says so and exits 0. Safe to run any number of times.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly checklistDetector: ChecklistDetector,\n private readonly reviewJsonService: ReviewJsonService,\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 diff\\n' + SEP + '\\n');\n process.stdout.write(this.report(repoRoot));\n return Promise.resolve();\n }\n\n /**\n * The whole report. Computed in the same order wp-finish-upsert-pr computes it, from the same services,\n * so \"wp-checklist said I was done\" and \"wp-finish let me through\" can never disagree.\n */\n private report(repoRoot: string): string {\n // loadAndValidate is the ONE gate: it rejects a structurally bad `checklists` (including the removed\n // { doc } manifest shape) AND checks that every guidance doc + reviewer agent file exists. So reaching\n // here means the set is valid — there is no \"configured but silently broken\" state left to report.\n const defs = loadAndValidate(repoRoot).prGate.checklists;\n const triggered = this.checklistDetector.detectForRepo(repoRoot, defs);\n if (triggered.length === 0) {\n // Zero is a SUPPORTED state, never a blocker (see ChecklistNotice).\n return this.checklistNotice.build(defs.length, 'wp-finish-upsert-pr');\n }\n return this.plan(repoRoot, defs, triggered);\n }\n\n // The triggered set split into \"already reviewed on this branch\" (reused, not re-run) and \"still owed\".\n private plan(repoRoot: string, defs: readonly ChecklistDefinition[], triggered: readonly TriggeredChecklist[]): string {\n const featureName = this.aiBranchName.getFeatureName();\n const reviewPath = reviewJsonPath(repoRoot, featureName);\n const required = this.checklistDetector.toRequired(triggered);\n const results = this.reviewJsonService.loadChecklistResults(reviewPath, required);\n const pending = this.reviewJsonService.pendingChecklists(required, results);\n const done = required.filter((r: RequiredChecklist): boolean => !pending.includes(r));\n const context = this.reviewJsonService.reviewContextFor(repoRoot, featureName);\n return (\n `${defs.length} checklist(s) configured, ${required.length} apply to this diff.\\n` +\n this.doneLines(done) +\n (pending.length === 0 ? this.allDone() : `\\n${this.instructions.render(pending, reviewPath, context)}\\n`)\n );\n }\n\n // REVIEW ONCE PER BRANCH: verdict files persist in .webpieces, so a second cycle re-instructs nothing.\n private doneLines(done: readonly RequiredChecklist[]): string {\n if (done.length === 0) return '';\n return done.map((r: RequiredChecklist): string =>\n ` ✓ ${r.subagent} — already reviewed on this branch (reusing its review-${r.id}.json)\\n`).join('');\n }\n\n private allDone(): string {\n return '\\n✅ Every checklist that applies is already reviewed — nothing to run.\\n Write review.json and run: pnpm wp-finish-upsert-pr\\n';\n }\n}\n"]}
1
+ {"version":3,"file":"checklist-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/checklist-command.ts"],"names":[],"mappings":";;;;AAAA,0DAA0I;AAC1I,yCAA2D;AAC3D,mEAA+D;AAC/D,qEAAsG;AAEtG,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE;;;;;;;;;GASG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAEJ;IACA;IACA;IACA;IAJrB,YACqB,cAA8B,EAC9B,gBAAkC,EAClC,YAA0C,EAC1C,eAAgC;QAHhC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,iBAAY,GAAZ,YAAY,CAA8B;QAC1C,oBAAe,GAAf,eAAe,CAAiB;IAClD,CAAC;IAEJ,GAAG;QACC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,4FAA4F;QAC5F,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,wCAAwC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACzF,uGAAuG;QACvG,wFAAwF;QACxF,MAAM,OAAO,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;QAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,wCAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClH,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;IAEO,MAAM,CAAC,IAAmB;QAC9B,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,oEAAoE;YACpE,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;QAClF,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACvF,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAoB,EAAW,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/F,OAAO,CACH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YACjB,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC;YAC5B,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAChH,CAAC;IACN,CAAC;IAED,0GAA0G;IAC1G,yFAAyF;IACjF,MAAM,CAAC,IAAmB;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QAC3G,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,6BAA6B,IAAI,CAAC,UAAU,CAAC,MAAM,yBAAyB;YACrG,IAAI,IAAI,gDAAgD,CAAC;IACjE,CAAC;IAED,yGAAyG;IACzG,uGAAuG;IAC/F,WAAW,CAAC,IAAmB,EAAE,IAAkC;QACvE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAoB,EAAU,EAAE,CAC7D,OAAO,CAAC,CAAC,QAAQ,0DAA0D,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC/F,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1B,CAAC;IAED,4GAA4G;IACpG,GAAG,CAAC,GAAsB;QAC9B,IAAI,GAAG,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,gDAAgD,CAAC;QAC9F,MAAM,KAAK,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClF,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,oBAAoB,KAAK,EAAE,CAAC;IACjE,CAAC;IAEO,OAAO;QACX,OAAO,0EAA0E;YAC7E,2DAA2D,CAAC;IACpE,CAAC;CACJ,CAAA;AA9DY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QACZ,oCAAgB;QACpB,2CAA4B;QACzB,kCAAe;GAL5C,gBAAgB,CA8D5B","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 { ChecklistDetector } from '../workflow/checklist-detector';
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 checklistDetector;
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, checklistDetector: ChecklistDetector, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, instructions: ChecklistInstructionsService);
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. Throws InformAiError so the AI gets one actionable message; passes silently
34
- * when nothing is pending, and is a no-op for a repo with no checklists.
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 checklist_detector_1 = require("../workflow/checklist-detector");
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 it could not be resolved) plus what actually
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
- checklistDetector;
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, checklistDetector, reviewJsonService, gateTokenService, provenance, instructions) {
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.checklistDetector = checklistDetector;
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
- const defs = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.checklists;
88
- const required = this.checklistDetector.toRequired(this.checklistDetector.detectForRepo(repoRoot, defs));
89
- // review-<id>.json files persist locally between runs, so a re-run after a push re-validates the
90
- // EXISTING verdicts against the (possibly changed) triggered set for free: an unchanged checklist
91
- // needs no re-review, a newly-triggered one refuses until its file is written. That is the
92
- // "full review only when the checklist surface changes" behavior — no special-casing here.
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.assertEveryReviewerRan(repoRoot, featureName, required);
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. Throws InformAiError so the AI gets one actionable message; passes silently
147
- * when nothing is pending, and is a no-op for a repo with no checklists.
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(repoRoot, featureName, required) {
150
- if (required.length === 0)
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
- const context = this.reviewJsonService.reviewContextFor(repoRoot, featureName);
158
- throw new rules_config_1.InformAiError(`⛔ NO PR ${pending.length} of ${required.length} review checklist(s) that apply to this diff have ` +
159
- `no passing verdict yet: ${this.instructions.names(pending)}\n\n` +
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
- checklist_detector_1.ChecklistDetector,
329
+ checklist_scanner_1.ChecklistScanner,
317
330
  rules_config_1.ReviewJsonService,
318
331
  rules_config_1.GateTokenService,
319
332
  rules_config_1.SubagentProvenanceService,