@webpieces/rules-config 0.4.774 → 0.4.776
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 +1 -1
- package/src/checklist-config.d.ts +37 -6
- package/src/checklist-config.js +55 -19
- package/src/checklist-config.js.map +1 -1
- package/src/checklist-docs-validator.d.ts +2 -2
- package/src/checklist-docs-validator.js +2 -2
- package/src/checklist-docs-validator.js.map +1 -1
- package/src/checklist-instructions.d.ts +11 -0
- package/src/checklist-instructions.js +43 -11
- package/src/checklist-instructions.js.map +1 -1
- package/src/checklist-validator.d.ts +13 -12
- package/src/checklist-validator.js +46 -34
- package/src/checklist-validator.js.map +1 -1
- package/src/constants.d.ts +6 -0
- package/src/constants.js +7 -1
- package/src/constants.js.map +1 -1
- package/src/index.d.ts +3 -3
- package/src/index.js +11 -6
- package/src/index.js.map +1 -1
- package/src/pr-gate-config.d.ts +9 -1
- package/src/pr-gate-config.js +15 -4
- package/src/pr-gate-config.js.map +1 -1
- package/src/pr-gate-section-validators.d.ts +17 -3
- package/src/pr-gate-section-validators.js +91 -30
- package/src/pr-gate-section-validators.js.map +1 -1
- package/src/retired-config-keys.js +8 -0
- package/src/retired-config-keys.js.map +1 -1
- package/src/review-json-data.d.ts +3 -2
- package/src/review-json-data.js +4 -4
- package/src/review-json-data.js.map +1 -1
- package/src/review-json.js +3 -3
- package/src/review-json.js.map +1 -1
- package/src/review-provenance.js +2 -2
- package/src/review-provenance.js.map +1 -1
- package/src/reviewer-instructions.d.ts +13 -9
- package/src/reviewer-instructions.js +27 -16
- package/src/reviewer-instructions.js.map +1 -1
- package/src/subagent-provenance.d.ts +30 -15
- package/src/subagent-provenance.js +100 -50
- package/src/subagent-provenance.js.map +1 -1
- package/src/validate-config.js +5 -2
- package/src/validate-config.js.map +1 -1
- package/templates/webpieces.review-checklists.md +50 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.776",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -1,6 +1,38 @@
|
|
|
1
|
+
/** The {@link ReviewerAgentPolicy.maxAgents} value meaning "`reviewerAgents` is not configured". */
|
|
2
|
+
export declare const REVIEWER_AGENTS_ONE_PER_CHECKLIST = 0;
|
|
3
|
+
/** The generic reviewer agent webpieces ships, and the value the config error tells a repo to add. */
|
|
4
|
+
export declare const DEFAULT_REVIEWER_AGENT_NAME = "webpieces-reviewer";
|
|
5
|
+
/**
|
|
6
|
+
* WHICH reviewer agent the gate briefs, and HOW MANY of them one round may use — straight from
|
|
7
|
+
* `commands.pr-gate.reviewerAgentName` / `commands.pr-gate.reviewerAgents`. Data-only.
|
|
8
|
+
*
|
|
9
|
+
* One instance per loaded config, shared by every {@link ChecklistDefinition} and copied onto every matched
|
|
10
|
+
* checklist, because the answer is repo-wide: a checklist no longer names an agent type of its own. It used
|
|
11
|
+
* to — a per-entry key pointing at its own `.claude/agents/<name>.md` — and those files added almost nothing
|
|
12
|
+
* over the checklist `doc` while forcing one subagent per checklist, so a PR matching four checklists paid
|
|
13
|
+
* for four agents re-reading the same diff (issue #938).
|
|
14
|
+
*/
|
|
15
|
+
export declare class ReviewerAgentPolicy {
|
|
16
|
+
/**
|
|
17
|
+
* The `subagent_type` every reviewer briefing names, e.g. `webpieces-reviewer` — the generic,
|
|
18
|
+
* checklist-agnostic agent webpieces ships (`.claude/agents/webpieces-reviewer.md`, written by
|
|
19
|
+
* `wp-install-ai-hooks` / `wp-upgrade-shim`). A repo may point it at its own agent instead.
|
|
20
|
+
*/
|
|
21
|
+
agentName: string;
|
|
22
|
+
/**
|
|
23
|
+
* The most reviewer subagents one stage-② round may use, or {@link REVIEWER_AGENTS_ONE_PER_CHECKLIST}
|
|
24
|
+
* when `reviewerAgents` is not configured — which keeps the original contract: one separate subagent per
|
|
25
|
+
* checklist. When set, the main AI groups the owed checklists across at most this many subagents, and
|
|
26
|
+
* each subagent still writes one verdict file per checklist it covers.
|
|
27
|
+
*/
|
|
28
|
+
maxAgents: number;
|
|
29
|
+
constructor(agentName: string, maxAgents: number);
|
|
30
|
+
/** true when `reviewerAgents` is configured, i.e. one subagent may cover several checklists. */
|
|
31
|
+
grouped(): boolean;
|
|
32
|
+
}
|
|
1
33
|
export declare class ChecklistDefinition {
|
|
2
34
|
id: string;
|
|
3
|
-
|
|
35
|
+
reviewer: ReviewerAgentPolicy;
|
|
4
36
|
doc: string;
|
|
5
37
|
patterns: string[];
|
|
6
38
|
/**
|
|
@@ -20,24 +52,23 @@ export declare class ChecklistDefinition {
|
|
|
20
52
|
* that the coding agent reading the error applies in a single pass.
|
|
21
53
|
*/
|
|
22
54
|
required: boolean;
|
|
23
|
-
constructor(id: string,
|
|
55
|
+
constructor(id: string, reviewer: ReviewerAgentPolicy, doc: string, patterns: string[], required: boolean);
|
|
24
56
|
}
|
|
25
57
|
export interface RawChecklistItem {
|
|
26
|
-
|
|
58
|
+
id?: string;
|
|
27
59
|
doc?: string;
|
|
28
60
|
patterns?: string[];
|
|
29
61
|
required?: boolean;
|
|
30
62
|
}
|
|
31
63
|
/**
|
|
32
|
-
* Build a ChecklistDefinition from
|
|
33
|
-
* only stable, human-meaningful key we have).
|
|
64
|
+
* Build a ChecklistDefinition from a raw entry, bound to the repo's reviewer policy.
|
|
34
65
|
*
|
|
35
66
|
* `required` is coerced with `=== true` rather than defaulted, and that is not a default in disguise:
|
|
36
67
|
* `validateChecklistArray` has already REJECTED any entry that omitted it or gave a non-boolean, so the
|
|
37
68
|
* only values that reach here are real booleans. The coercion exists so a validator that runs without a
|
|
38
69
|
* repoRoot (structure-only) still produces a well-typed def instead of `undefined` leaking through.
|
|
39
70
|
*/
|
|
40
|
-
export declare function toChecklist(raw: RawChecklistItem): ChecklistDefinition;
|
|
71
|
+
export declare function toChecklist(raw: RawChecklistItem, reviewer: ReviewerAgentPolicy): ChecklistDefinition;
|
|
41
72
|
/** Normalize a checklist entry's repo-relative `doc` to a POSIX path, so every printed path matches. */
|
|
42
73
|
export declare function normalizeChecklistDoc(doc: string): string;
|
|
43
74
|
export declare const MATCHED_FILES_CAP = 6;
|
package/src/checklist-config.js
CHANGED
|
@@ -1,28 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MATCHED_FILES_CAP = exports.ChecklistDefinition = void 0;
|
|
3
|
+
exports.MATCHED_FILES_CAP = exports.ChecklistDefinition = exports.ReviewerAgentPolicy = exports.DEFAULT_REVIEWER_AGENT_NAME = exports.REVIEWER_AGENTS_ONE_PER_CHECKLIST = void 0;
|
|
4
4
|
exports.toChecklist = toChecklist;
|
|
5
5
|
exports.normalizeChecklistDoc = normalizeChecklistDoc;
|
|
6
6
|
exports.formatFileList = formatFileList;
|
|
7
7
|
const tslib_1 = require("tslib");
|
|
8
8
|
const path = tslib_1.__importStar(require("path"));
|
|
9
|
+
/** The {@link ReviewerAgentPolicy.maxAgents} value meaning "`reviewerAgents` is not configured". */
|
|
10
|
+
exports.REVIEWER_AGENTS_ONE_PER_CHECKLIST = 0;
|
|
11
|
+
/** The generic reviewer agent webpieces ships, and the value the config error tells a repo to add. */
|
|
12
|
+
exports.DEFAULT_REVIEWER_AGENT_NAME = 'webpieces-reviewer';
|
|
13
|
+
/**
|
|
14
|
+
* WHICH reviewer agent the gate briefs, and HOW MANY of them one round may use — straight from
|
|
15
|
+
* `commands.pr-gate.reviewerAgentName` / `commands.pr-gate.reviewerAgents`. Data-only.
|
|
16
|
+
*
|
|
17
|
+
* One instance per loaded config, shared by every {@link ChecklistDefinition} and copied onto every matched
|
|
18
|
+
* checklist, because the answer is repo-wide: a checklist no longer names an agent type of its own. It used
|
|
19
|
+
* to — a per-entry key pointing at its own `.claude/agents/<name>.md` — and those files added almost nothing
|
|
20
|
+
* over the checklist `doc` while forcing one subagent per checklist, so a PR matching four checklists paid
|
|
21
|
+
* for four agents re-reading the same diff (issue #938).
|
|
22
|
+
*/
|
|
23
|
+
class ReviewerAgentPolicy {
|
|
24
|
+
/**
|
|
25
|
+
* The `subagent_type` every reviewer briefing names, e.g. `webpieces-reviewer` — the generic,
|
|
26
|
+
* checklist-agnostic agent webpieces ships (`.claude/agents/webpieces-reviewer.md`, written by
|
|
27
|
+
* `wp-install-ai-hooks` / `wp-upgrade-shim`). A repo may point it at its own agent instead.
|
|
28
|
+
*/
|
|
29
|
+
agentName;
|
|
30
|
+
/**
|
|
31
|
+
* The most reviewer subagents one stage-② round may use, or {@link REVIEWER_AGENTS_ONE_PER_CHECKLIST}
|
|
32
|
+
* when `reviewerAgents` is not configured — which keeps the original contract: one separate subagent per
|
|
33
|
+
* checklist. When set, the main AI groups the owed checklists across at most this many subagents, and
|
|
34
|
+
* each subagent still writes one verdict file per checklist it covers.
|
|
35
|
+
*/
|
|
36
|
+
maxAgents;
|
|
37
|
+
constructor(agentName, maxAgents) {
|
|
38
|
+
this.agentName = agentName;
|
|
39
|
+
this.maxAgents = maxAgents;
|
|
40
|
+
}
|
|
41
|
+
/** true when `reviewerAgents` is configured, i.e. one subagent may cover several checklists. */
|
|
42
|
+
grouped() {
|
|
43
|
+
return this.maxAgents !== exports.REVIEWER_AGENTS_ONE_PER_CHECKLIST;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.ReviewerAgentPolicy = ReviewerAgentPolicy;
|
|
9
47
|
// A company review checklist: a diff-triggered extension point that lets a CONSUMER inject its own
|
|
10
|
-
// PR-time review process into the webpieces gated flow WITHOUT forking the tooling. Each checklist
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json
|
|
14
|
-
//
|
|
48
|
+
// PR-time review process into the webpieces gated flow WITHOUT forking the tooling. Each checklist has an
|
|
49
|
+
// `id` and the doc its reviewer reads; when the diff matches the checklist's `patterns`,
|
|
50
|
+
// wp-review-upsert-pr tells the AI to spawn the repo's reviewer agent (`commands.pr-gate.reviewerAgentName`)
|
|
51
|
+
// over it, and wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json
|
|
52
|
+
// exists AND a reviewer subagent is proven (from the harness's own artifacts) to have actually run.
|
|
15
53
|
//
|
|
16
54
|
// Checklists are configured as an ARRAY in `pr-gate.checklists` in webpieces.config.json — the ONLY
|
|
17
|
-
// accepted shape. `patterns` is a path-glob dispatch table and `
|
|
55
|
+
// accepted shape. `patterns` is a path-glob dispatch table and `id` is the checklist's name: both are
|
|
18
56
|
// config, so they live where every tool that reads webpieces.config.json can see, grep and schema them.
|
|
19
57
|
// Data-only.
|
|
20
58
|
class ChecklistDefinition {
|
|
21
|
-
id; //
|
|
22
|
-
|
|
23
|
-
// REPO-RELATIVE guidance doc the reviewer reads
|
|
24
|
-
// because this value is printed verbatim to a reviewer subagent as
|
|
25
|
-
// relative to anything else is unresolvable from where
|
|
59
|
+
id; // keys review-<id>.json, its instructions file and its dashboard row
|
|
60
|
+
reviewer; // repo-wide: which agent type reviews it, and the per-round cap
|
|
61
|
+
// REPO-RELATIVE guidance doc the reviewer reads. REQUIRED: with one generic reviewer agent the doc is
|
|
62
|
+
// the whole checklist. Repo-relative because this value is printed verbatim to a reviewer subagent as
|
|
63
|
+
// "the file to open", and a path relative to anything else is unresolvable from where it stands.
|
|
26
64
|
doc;
|
|
27
65
|
patterns; // path globs (isPathExcluded semantics); [] = matches any changed file (always runs)
|
|
28
66
|
/**
|
|
@@ -43,9 +81,9 @@ class ChecklistDefinition {
|
|
|
43
81
|
*/
|
|
44
82
|
required;
|
|
45
83
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
46
|
-
constructor(id,
|
|
84
|
+
constructor(id, reviewer, doc, patterns, required) {
|
|
47
85
|
this.id = id;
|
|
48
|
-
this.
|
|
86
|
+
this.reviewer = reviewer;
|
|
49
87
|
this.doc = doc;
|
|
50
88
|
this.patterns = patterns;
|
|
51
89
|
this.required = required;
|
|
@@ -53,8 +91,7 @@ class ChecklistDefinition {
|
|
|
53
91
|
}
|
|
54
92
|
exports.ChecklistDefinition = ChecklistDefinition;
|
|
55
93
|
/**
|
|
56
|
-
* Build a ChecklistDefinition from
|
|
57
|
-
* only stable, human-meaningful key we have).
|
|
94
|
+
* Build a ChecklistDefinition from a raw entry, bound to the repo's reviewer policy.
|
|
58
95
|
*
|
|
59
96
|
* `required` is coerced with `=== true` rather than defaulted, and that is not a default in disguise:
|
|
60
97
|
* `validateChecklistArray` has already REJECTED any entry that omitted it or gave a non-boolean, so the
|
|
@@ -62,9 +99,8 @@ exports.ChecklistDefinition = ChecklistDefinition;
|
|
|
62
99
|
* repoRoot (structure-only) still produces a well-typed def instead of `undefined` leaking through.
|
|
63
100
|
*/
|
|
64
101
|
// webpieces-disable no-function-outside-class -- pure config transform beside its data class
|
|
65
|
-
function toChecklist(raw) {
|
|
66
|
-
|
|
67
|
-
return new ChecklistDefinition(subagent, subagent, normalizeChecklistDoc(raw.doc ?? ''), raw.patterns ?? [], raw.required === true);
|
|
102
|
+
function toChecklist(raw, reviewer) {
|
|
103
|
+
return new ChecklistDefinition((raw.id ?? '').trim(), reviewer, normalizeChecklistDoc(raw.doc ?? ''), raw.patterns ?? [], raw.required === true);
|
|
68
104
|
}
|
|
69
105
|
/** Normalize a checklist entry's repo-relative `doc` to a POSIX path, so every printed path matches. */
|
|
70
106
|
// webpieces-disable no-function-outside-class -- pure path transform beside its data class
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-config.ts"],"names":[],"mappings":";;;AAmEA,kCAIC;AAID,sDAIC;AAYD,wCAIC;;AA/FD,mDAA6B;AAE7B,mGAAmG;AACnG,yGAAyG;AACzG,wGAAwG;AACxG,gHAAgH;AAChH,2GAA2G;AAC3G,oFAAoF;AACpF,EAAE;AACF,oGAAoG;AACpG,sGAAsG;AACtG,wGAAwG;AACxG,aAAa;AACb,MAAa,mBAAmB;IAC5B,EAAE,CAAS,CAAS,mEAAmE;IACvF,QAAQ,CAAS,CAAG,uFAAuF;IAC3G,yGAAyG;IACzG,kGAAkG;IAClG,6EAA6E;IAC7E,GAAG,CAAS;IACZ,QAAQ,CAAW,CAAC,qFAAqF;IACzG;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAU;IAElB,yDAAyD;IACzD,YAAY,EAAU,EAAE,QAAgB,EAAE,GAAW,EAAE,QAAkB,EAAE,QAAiB;QACxF,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAlCD,kDAkCC;AAUD;;;;;;;;GAQG;AACH,6FAA6F;AAC7F,SAAgB,WAAW,CAAC,GAAqB;IAC7C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;IACpC,OAAO,IAAI,mBAAmB,CAC1B,QAAQ,EAAE,QAAQ,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;AAC7G,CAAC;AAED,wGAAwG;AACxG,2FAA2F;AAC3F,SAAgB,qBAAqB,CAAC,GAAW;IAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,0GAA0G;AAC1G,+FAA+F;AAClF,QAAA,iBAAiB,GAAG,CAAC,CAAC;AAEnC;;;;GAIG;AACH,mGAAmG;AACnG,SAAgB,cAAc,CAAC,KAAwB,EAAE,MAAc,yBAAiB;IACpF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,GAAG,UAAU,KAAK,CAAC,MAAM,SAAS,CAAC;AACpG,CAAC","sourcesContent":["import * as path from 'path';\n\n// A company review checklist: a diff-triggered extension point that lets a CONSUMER inject its own\n// PR-time review process into the webpieces gated flow WITHOUT forking the tooling. Each checklist names\n// a reviewer SUBAGENT (a `.claude/agents/<subagent>.md`) and the doc that reviewer reads; when the diff\n// matches the checklist's `patterns`, wp-review-upsert-pr tells the AI to spawn that subagent to review it, and\n// wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json exists AND that\n// named subagent is proven (from the harness's own artifacts) to have actually run.\n//\n// Checklists are configured as an ARRAY in `pr-gate.checklists` in webpieces.config.json — the ONLY\n// accepted shape. `patterns` is a path-glob dispatch table and `subagent` is a name binding: both are\n// config, so they live where every tool that reads webpieces.config.json can see, grep and schema them.\n// Data-only.\nexport class ChecklistDefinition {\n id: string; // = the subagent name; keys review-<id>.json and the dashboard row\n subagent: string; // reviewer agent name → .claude/agents/<subagent>.md; the agentType the harness stamps\n // REPO-RELATIVE guidance doc the reviewer reads (may be '' — then it just reads the diff). Repo-relative\n // because this value is printed verbatim to a reviewer subagent as \"the file to open\", and a path\n // relative to anything else is unresolvable from where that subagent stands.\n doc: string;\n patterns: string[]; // path globs (isPathExcluded semantics); [] = matches any changed file (always runs)\n /**\n * true — BLOCKING. `wp-finish-upsert-pr` refuses the PR until this checklist has a passing verdict.\n * false — OPTIONAL. When it matches, stage ② OFFERS it: the AI asks the human which optional reviewers\n * to run, and the human may decline every one of them. A declined optional checklist never\n * blocks.\n *\n * This governs whether the reviewer must RUN — NOT whether its verdict counts. An optional reviewer\n * that is actually spawned and comes back red blocks finish exactly like a required one; otherwise\n * running it would be theater.\n *\n * There is deliberately NO default (see the validator): a review process is the last thing that should\n * acquire a scope silently, in either direction. Defaulting to true would leave the all-blocking status\n * quo in place for anyone who did not read the release note; defaulting to false would quietly\n * DOWNGRADE every existing consumer's gate on upgrade. Both are worse than one mechanical config edit\n * that the coding agent reading the error applies in a single pass.\n */\n required: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, subagent: string, doc: string, patterns: string[], required: boolean) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.patterns = patterns;\n this.required = required;\n }\n}\n\n// One config entry straight from JSON, before it is validated + narrowed into a class.\nexport interface RawChecklistItem {\n subagent?: string;\n doc?: string;\n patterns?: string[];\n required?: boolean;\n}\n\n/**\n * Build a ChecklistDefinition from an omitting-friendly raw entry. `id` defaults to the subagent name (the\n * only stable, human-meaningful key we have).\n *\n * `required` is coerced with `=== true` rather than defaulted, and that is not a default in disguise:\n * `validateChecklistArray` has already REJECTED any entry that omitted it or gave a non-boolean, so the\n * only values that reach here are real booleans. The coercion exists so a validator that runs without a\n * repoRoot (structure-only) still produces a well-typed def instead of `undefined` leaking through.\n */\n// webpieces-disable no-function-outside-class -- pure config transform beside its data class\nexport function toChecklist(raw: RawChecklistItem): ChecklistDefinition {\n const subagent = raw.subagent ?? '';\n return new ChecklistDefinition(\n subagent, subagent, normalizeChecklistDoc(raw.doc ?? ''), raw.patterns ?? [], raw.required === true);\n}\n\n/** Normalize a checklist entry's repo-relative `doc` to a POSIX path, so every printed path matches. */\n// webpieces-disable no-function-outside-class -- pure path transform beside its data class\nexport function normalizeChecklistDoc(doc: string): string {\n const trimmed = doc.trim();\n if (trimmed === '') return '';\n return path.posix.normalize(trimmed.split(path.sep).join('/'));\n}\n\n// The ONE cap for every printed matched-file list. Two print sites once used to slice to 4 and to 5 — two\n// different caps for the same list, neither chosen deliberately, and both without an ellipsis.\nexport const MATCHED_FILES_CAP = 6;\n\n/**\n * Render a file list for a message, NEVER silently. A truncated list that looks complete is how a reviewer\n * gets pointed at 4 of 40 changed files and reports success having reviewed a tenth of the diff, so the\n * dropped count is always stated and the caller is expected to name the file holding the full set.\n */\n// webpieces-disable no-function-outside-class -- pure display formatter beside the data it formats\nexport function formatFileList(files: readonly string[], cap: number = MATCHED_FILES_CAP): string {\n if (files.length === 0) return '(none)';\n if (files.length <= cap) return files.join(', ');\n return `${files.slice(0, cap).join(', ')}, +${files.length - cap} more (${files.length} total)`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-config.ts"],"names":[],"mappings":";;;AA4GA,kCAGC;AAID,sDAIC;AAYD,wCAIC;;AAvID,mDAA6B;AAE7B,oGAAoG;AACvF,QAAA,iCAAiC,GAAG,CAAC,CAAC;AAEnD,sGAAsG;AACzF,QAAA,2BAA2B,GAAG,oBAAoB,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAa,mBAAmB;IAC5B;;;;OAIG;IACH,SAAS,CAAS;IAClB;;;;;OAKG;IACH,SAAS,CAAS;IAElB,YAAY,SAAiB,EAAE,SAAiB;QAC5C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,gGAAgG;IAChG,OAAO;QACH,OAAO,IAAI,CAAC,SAAS,KAAK,yCAAiC,CAAC;IAChE,CAAC;CACJ;AAxBD,kDAwBC;AAED,mGAAmG;AACnG,0GAA0G;AAC1G,yFAAyF;AACzF,6GAA6G;AAC7G,wGAAwG;AACxG,oGAAoG;AACpG,EAAE;AACF,oGAAoG;AACpG,sGAAsG;AACtG,wGAAwG;AACxG,aAAa;AACb,MAAa,mBAAmB;IAC5B,EAAE,CAAS,CAAoB,qEAAqE;IACpG,QAAQ,CAAsB,CAAC,gEAAgE;IAC/F,sGAAsG;IACtG,sGAAsG;IACtG,iGAAiG;IACjG,GAAG,CAAS;IACZ,QAAQ,CAAW,CAAC,qFAAqF;IACzG;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAU;IAElB,yDAAyD;IACzD,YAAY,EAAU,EAAE,QAA6B,EAAE,GAAW,EAAE,QAAkB,EAAE,QAAiB;QACrG,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAlCD,kDAkCC;AAUD;;;;;;;GAOG;AACH,6FAA6F;AAC7F,SAAgB,WAAW,CAAC,GAAqB,EAAE,QAA6B;IAC5E,OAAO,IAAI,mBAAmB,CAC1B,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,EAAE,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;AAC1H,CAAC;AAED,wGAAwG;AACxG,2FAA2F;AAC3F,SAAgB,qBAAqB,CAAC,GAAW;IAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED,0GAA0G;AAC1G,+FAA+F;AAClF,QAAA,iBAAiB,GAAG,CAAC,CAAC;AAEnC;;;;GAIG;AACH,mGAAmG;AACnG,SAAgB,cAAc,CAAC,KAAwB,EAAE,MAAc,yBAAiB;IACpF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,GAAG,GAAG,UAAU,KAAK,CAAC,MAAM,SAAS,CAAC;AACpG,CAAC","sourcesContent":["import * as path from 'path';\n\n/** The {@link ReviewerAgentPolicy.maxAgents} value meaning \"`reviewerAgents` is not configured\". */\nexport const REVIEWER_AGENTS_ONE_PER_CHECKLIST = 0;\n\n/** The generic reviewer agent webpieces ships, and the value the config error tells a repo to add. */\nexport const DEFAULT_REVIEWER_AGENT_NAME = 'webpieces-reviewer';\n\n/**\n * WHICH reviewer agent the gate briefs, and HOW MANY of them one round may use — straight from\n * `commands.pr-gate.reviewerAgentName` / `commands.pr-gate.reviewerAgents`. Data-only.\n *\n * One instance per loaded config, shared by every {@link ChecklistDefinition} and copied onto every matched\n * checklist, because the answer is repo-wide: a checklist no longer names an agent type of its own. It used\n * to — a per-entry key pointing at its own `.claude/agents/<name>.md` — and those files added almost nothing\n * over the checklist `doc` while forcing one subagent per checklist, so a PR matching four checklists paid\n * for four agents re-reading the same diff (issue #938).\n */\nexport class ReviewerAgentPolicy {\n /**\n * The `subagent_type` every reviewer briefing names, e.g. `webpieces-reviewer` — the generic,\n * checklist-agnostic agent webpieces ships (`.claude/agents/webpieces-reviewer.md`, written by\n * `wp-install-ai-hooks` / `wp-upgrade-shim`). A repo may point it at its own agent instead.\n */\n agentName: string;\n /**\n * The most reviewer subagents one stage-② round may use, or {@link REVIEWER_AGENTS_ONE_PER_CHECKLIST}\n * when `reviewerAgents` is not configured — which keeps the original contract: one separate subagent per\n * checklist. When set, the main AI groups the owed checklists across at most this many subagents, and\n * each subagent still writes one verdict file per checklist it covers.\n */\n maxAgents: number;\n\n constructor(agentName: string, maxAgents: number) {\n this.agentName = agentName;\n this.maxAgents = maxAgents;\n }\n\n /** true when `reviewerAgents` is configured, i.e. one subagent may cover several checklists. */\n grouped(): boolean {\n return this.maxAgents !== REVIEWER_AGENTS_ONE_PER_CHECKLIST;\n }\n}\n\n// A company review checklist: a diff-triggered extension point that lets a CONSUMER inject its own\n// PR-time review process into the webpieces gated flow WITHOUT forking the tooling. Each checklist has an\n// `id` and the doc its reviewer reads; when the diff matches the checklist's `patterns`,\n// wp-review-upsert-pr tells the AI to spawn the repo's reviewer agent (`commands.pr-gate.reviewerAgentName`)\n// over it, and wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json\n// exists AND a reviewer subagent is proven (from the harness's own artifacts) to have actually run.\n//\n// Checklists are configured as an ARRAY in `pr-gate.checklists` in webpieces.config.json — the ONLY\n// accepted shape. `patterns` is a path-glob dispatch table and `id` is the checklist's name: both are\n// config, so they live where every tool that reads webpieces.config.json can see, grep and schema them.\n// Data-only.\nexport class ChecklistDefinition {\n id: string; // keys review-<id>.json, its instructions file and its dashboard row\n reviewer: ReviewerAgentPolicy; // repo-wide: which agent type reviews it, and the per-round cap\n // REPO-RELATIVE guidance doc the reviewer reads. REQUIRED: with one generic reviewer agent the doc is\n // the whole checklist. Repo-relative because this value is printed verbatim to a reviewer subagent as\n // \"the file to open\", and a path relative to anything else is unresolvable from where it stands.\n doc: string;\n patterns: string[]; // path globs (isPathExcluded semantics); [] = matches any changed file (always runs)\n /**\n * true — BLOCKING. `wp-finish-upsert-pr` refuses the PR until this checklist has a passing verdict.\n * false — OPTIONAL. When it matches, stage ② OFFERS it: the AI asks the human which optional reviewers\n * to run, and the human may decline every one of them. A declined optional checklist never\n * blocks.\n *\n * This governs whether the reviewer must RUN — NOT whether its verdict counts. An optional reviewer\n * that is actually spawned and comes back red blocks finish exactly like a required one; otherwise\n * running it would be theater.\n *\n * There is deliberately NO default (see the validator): a review process is the last thing that should\n * acquire a scope silently, in either direction. Defaulting to true would leave the all-blocking status\n * quo in place for anyone who did not read the release note; defaulting to false would quietly\n * DOWNGRADE every existing consumer's gate on upgrade. Both are worse than one mechanical config edit\n * that the coding agent reading the error applies in a single pass.\n */\n required: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, reviewer: ReviewerAgentPolicy, doc: string, patterns: string[], required: boolean) {\n this.id = id;\n this.reviewer = reviewer;\n this.doc = doc;\n this.patterns = patterns;\n this.required = required;\n }\n}\n\n// One config entry straight from JSON, before it is validated + narrowed into a class.\nexport interface RawChecklistItem {\n id?: string;\n doc?: string;\n patterns?: string[];\n required?: boolean;\n}\n\n/**\n * Build a ChecklistDefinition from a raw entry, bound to the repo's reviewer policy.\n *\n * `required` is coerced with `=== true` rather than defaulted, and that is not a default in disguise:\n * `validateChecklistArray` has already REJECTED any entry that omitted it or gave a non-boolean, so the\n * only values that reach here are real booleans. The coercion exists so a validator that runs without a\n * repoRoot (structure-only) still produces a well-typed def instead of `undefined` leaking through.\n */\n// webpieces-disable no-function-outside-class -- pure config transform beside its data class\nexport function toChecklist(raw: RawChecklistItem, reviewer: ReviewerAgentPolicy): ChecklistDefinition {\n return new ChecklistDefinition(\n (raw.id ?? '').trim(), reviewer, normalizeChecklistDoc(raw.doc ?? ''), raw.patterns ?? [], raw.required === true);\n}\n\n/** Normalize a checklist entry's repo-relative `doc` to a POSIX path, so every printed path matches. */\n// webpieces-disable no-function-outside-class -- pure path transform beside its data class\nexport function normalizeChecklistDoc(doc: string): string {\n const trimmed = doc.trim();\n if (trimmed === '') return '';\n return path.posix.normalize(trimmed.split(path.sep).join('/'));\n}\n\n// The ONE cap for every printed matched-file list. Two print sites once used to slice to 4 and to 5 — two\n// different caps for the same list, neither chosen deliberately, and both without an ellipsis.\nexport const MATCHED_FILES_CAP = 6;\n\n/**\n * Render a file list for a message, NEVER silently. A truncated list that looks complete is how a reviewer\n * gets pointed at 4 of 40 changed files and reports success having reviewed a tenth of the diff, so the\n * dropped count is always stated and the caller is expected to name the file holding the full set.\n */\n// webpieces-disable no-function-outside-class -- pure display formatter beside the data it formats\nexport function formatFileList(files: readonly string[], cap: number = MATCHED_FILES_CAP): string {\n if (files.length === 0) return '(none)';\n if (files.length <= cap) return files.join(', ');\n return `${files.slice(0, cap).join(', ')}, +${files.length - cap} more (${files.length} total)`;\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validate ONLY the pr-gate `checklists` array, in isolation — that it IS an array (the removed `{ doc }`
|
|
3
|
-
* manifest shape is rejected with its migration steps), each entry's
|
|
4
|
-
*
|
|
3
|
+
* manifest shape is rejected with its migration steps), each entry's id is present, distinct and
|
|
4
|
+
* file-safe, each entry's repo-relative doc exists, a retired `subagent` key is rejected, and patterns are
|
|
5
5
|
* string[]. Same logic loadAndValidate runs, but callable directly so broken checklists fail as their OWN
|
|
6
6
|
* `validate-checklist-docs` check (clear owner) instead of surfacing as an unrelated validator's banner.
|
|
7
7
|
* Returns errors; never throws.
|
|
@@ -9,8 +9,8 @@ const validate_config_1 = require("./validate-config");
|
|
|
9
9
|
const to_error_1 = require("./to-error");
|
|
10
10
|
/**
|
|
11
11
|
* Validate ONLY the pr-gate `checklists` array, in isolation — that it IS an array (the removed `{ doc }`
|
|
12
|
-
* manifest shape is rejected with its migration steps), each entry's
|
|
13
|
-
*
|
|
12
|
+
* manifest shape is rejected with its migration steps), each entry's id is present, distinct and
|
|
13
|
+
* file-safe, each entry's repo-relative doc exists, a retired `subagent` key is rejected, and patterns are
|
|
14
14
|
* string[]. Same logic loadAndValidate runs, but callable directly so broken checklists fail as their OWN
|
|
15
15
|
* `validate-checklist-docs` check (clear owner) instead of surfacing as an unrelated validator's banner.
|
|
16
16
|
* Returns errors; never throws.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-docs-validator.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-docs-validator.ts"],"names":[],"mappings":";;AAeA,sDAwBC;;AAvCD,+CAAyB;AACzB,mDAA6B;AAC7B,+CAA+C;AAC/C,uDAA8D;AAC9D,yCAAqC;AAErC;;;;;;;GAOG;AACH,2GAA2G;AAC3G,SAAgB,qBAAqB,CAAC,GAAW;IAC7C,MAAM,UAAU,GAAG,IAAA,4BAAc,EAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAC3B,wFAAwF;IACxF,IAAI,GAA4B,CAAC;IACjC,6GAA6G;IAC7G,8DAA8D;IAC9D,IAAI,CAAC;QACD,wFAAwF;QACxF,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAA4B,CAAC;IACrF,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,uBAAuB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1C,sGAAsG;IACtG,yGAAyG;IACzG,wGAAwG;IACxG,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAwC,CAAC;IACxE,2EAA2E;IAC3E,MAAM,MAAM,GAAG,QAAQ,EAAE,CAAC,SAAS,CAAwC,CAAC;IAC5E,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,YAAY,IAAI,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACpD,OAAO,IAAA,2CAAyB,EAAC,MAAM,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,CAAC;AACrE,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { findConfigFile } from './config-file';\nimport { validateChecklistsSection } from './validate-config';\nimport { toError } from './to-error';\n\n/**\n * Validate ONLY the pr-gate `checklists` array, in isolation — that it IS an array (the removed `{ doc }`\n * manifest shape is rejected with its migration steps), each entry's
|
|
1
|
+
{"version":3,"file":"checklist-docs-validator.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-docs-validator.ts"],"names":[],"mappings":";;AAeA,sDAwBC;;AAvCD,+CAAyB;AACzB,mDAA6B;AAC7B,+CAA+C;AAC/C,uDAA8D;AAC9D,yCAAqC;AAErC;;;;;;;GAOG;AACH,2GAA2G;AAC3G,SAAgB,qBAAqB,CAAC,GAAW;IAC7C,MAAM,UAAU,GAAG,IAAA,4BAAc,EAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAC3B,wFAAwF;IACxF,IAAI,GAA4B,CAAC;IACjC,6GAA6G;IAC7G,8DAA8D;IAC9D,IAAI,CAAC;QACD,wFAAwF;QACxF,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAA4B,CAAC;IACrF,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,uBAAuB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC1C,sGAAsG;IACtG,yGAAyG;IACzG,wGAAwG;IACxG,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAwC,CAAC;IACxE,2EAA2E;IAC3E,MAAM,MAAM,GAAG,QAAQ,EAAE,CAAC,SAAS,CAAwC,CAAC;IAC5E,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,YAAY,IAAI,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IACpD,OAAO,IAAA,2CAAyB,EAAC,MAAM,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,CAAC;AACrE,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { findConfigFile } from './config-file';\nimport { validateChecklistsSection } from './validate-config';\nimport { toError } from './to-error';\n\n/**\n * Validate ONLY the pr-gate `checklists` array, in isolation — that it IS an array (the removed `{ doc }`\n * manifest shape is rejected with its migration steps), each entry's id is present, distinct and\n * file-safe, each entry's repo-relative doc exists, a retired `subagent` key is rejected, and patterns are\n * string[]. Same logic loadAndValidate runs, but callable directly so broken checklists fail as their OWN\n * `validate-checklist-docs` check (clear owner) instead of surfacing as an unrelated validator's banner.\n * Returns errors; never throws.\n */\n// webpieces-disable no-function-outside-class -- module-level config validator, matches validate-config.ts\nexport function validateChecklistDocs(cwd: string): string[] {\n const configPath = findConfigFile(cwd);\n if (!configPath) return [];\n // webpieces-disable no-any-unknown -- parsed config JSON is opaque until narrowed below\n let raw: Record<string, unknown>;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed config surfaces as one readable error\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed config JSON is opaque until narrowed below\n raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n return [`[pr-gate] ${path.basename(configPath)} is not valid JSON: ${error.message}`];\n }\n const repoRoot = path.dirname(configPath);\n // ONE location: commands[\"pr-gate\"]. This function parses the config itself rather than going through\n // loadAndValidate, so it used to also read the retired top-level `pr-gate` — which meant a config on the\n // dead layout validated its checklists silently here while loadAndValidate rejected it everywhere else.\n // webpieces-disable no-any-unknown -- narrowing the opaque command section\n const commands = raw['commands'] as Record<string, unknown> | undefined;\n // webpieces-disable no-any-unknown -- narrowing the opaque pr-gate section\n const prGate = commands?.['pr-gate'] as Record<string, unknown> | undefined;\n if (!prGate || !('checklists' in prGate)) return [];\n return validateChecklistsSection(prGate['checklists'], repoRoot);\n}\n"]}
|
|
@@ -22,6 +22,17 @@ export declare class ChecklistInstructionsService {
|
|
|
22
22
|
* `reviewPath` is the branch's review.json — each verdict file sits beside it as review-<id>.json.
|
|
23
23
|
*/
|
|
24
24
|
render(pending: readonly RequiredChecklist[], reviewPath: string, context: ChecklistReviewContext): string;
|
|
25
|
+
/**
|
|
26
|
+
* HOW MANY subagents, of WHICH type. Without `reviewerAgents` that is one separate subagent per checklist;
|
|
27
|
+
* with it, at most N subagents with the checklists grouped across them as the AI judges best. Either way
|
|
28
|
+
* every subagent is the repo's ONE reviewer agent type, and every checklist still gets its own verdict.
|
|
29
|
+
*/
|
|
30
|
+
spawnRule(pending: readonly RequiredChecklist[]): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The grouping sentence, with an example that fits THIS round's numbers — a cap of 1 has no choice to
|
|
33
|
+
* make, and saying "group them as you judge best" there would invite a second subagent.
|
|
34
|
+
*/
|
|
35
|
+
groupingHint(count: number, cap: number): string;
|
|
25
36
|
names(pending: readonly RequiredChecklist[]): string;
|
|
26
37
|
private oneReviewer;
|
|
27
38
|
/**
|
|
@@ -36,24 +36,56 @@ let ChecklistInstructionsService = class ChecklistInstructionsService {
|
|
|
36
36
|
render(pending, reviewPath, context) {
|
|
37
37
|
if (pending.length === 0)
|
|
38
38
|
return '';
|
|
39
|
-
const lines = [
|
|
40
|
-
`You MUST run these ${pending.length} reviewer subagent(s) — a SEPARATE one for each. You may NOT review`,
|
|
41
|
-
`your own work, and you may NOT write a reviewer's verdict file on its behalf.`,
|
|
42
|
-
'',
|
|
43
|
-
];
|
|
39
|
+
const lines = [...this.spawnRule(pending), ''];
|
|
44
40
|
for (const req of pending)
|
|
45
41
|
lines.push(...this.oneReviewer(req, reviewPath));
|
|
46
42
|
lines.push('', ...this.verdictFormat());
|
|
47
43
|
lines.push('', ...this.diffLines(context));
|
|
48
44
|
return lines.join('\n');
|
|
49
45
|
}
|
|
50
|
-
|
|
46
|
+
/**
|
|
47
|
+
* HOW MANY subagents, of WHICH type. Without `reviewerAgents` that is one separate subagent per checklist;
|
|
48
|
+
* with it, at most N subagents with the checklists grouped across them as the AI judges best. Either way
|
|
49
|
+
* every subagent is the repo's ONE reviewer agent type, and every checklist still gets its own verdict.
|
|
50
|
+
*/
|
|
51
|
+
spawnRule(pending) {
|
|
52
|
+
const reviewer = pending[0].reviewer;
|
|
53
|
+
const own = [
|
|
54
|
+
'You may NOT review your own work, and you may NOT write a reviewer\'s verdict file on its behalf.',
|
|
55
|
+
];
|
|
56
|
+
if (!reviewer.grouped()) {
|
|
57
|
+
return [
|
|
58
|
+
`You MUST run these ${pending.length} checklist review(s) — a SEPARATE \`${reviewer.agentName}\` subagent for each.`,
|
|
59
|
+
...own,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
const cap = Math.min(reviewer.maxAgents, pending.length);
|
|
63
|
+
return [
|
|
64
|
+
`You MUST run these ${pending.length} checklist review(s) using AT MOST ${cap} \`${reviewer.agentName}\` subagent(s)`,
|
|
65
|
+
`(commands.pr-gate.reviewerAgents = ${reviewer.maxAgents}). ${this.groupingHint(pending.length, cap)}`,
|
|
66
|
+
'Give each subagent every checklist it covers (doc, in-scope files, verdict path); it writes ONE verdict',
|
|
67
|
+
'file per checklist it covers, and never one for a checklist it was not given.',
|
|
68
|
+
...own,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The grouping sentence, with an example that fits THIS round's numbers — a cap of 1 has no choice to
|
|
73
|
+
* make, and saying "group them as you judge best" there would invite a second subagent.
|
|
74
|
+
*/
|
|
75
|
+
groupingHint(count, cap) {
|
|
76
|
+
if (cap <= 1)
|
|
77
|
+
return 'Use ONE subagent for all of them.';
|
|
78
|
+
const per = Math.ceil(count / cap);
|
|
79
|
+
return `Group the checklists across them as you judge best — e.g. one subagent for all of them, or ${cap} with about ${per} each.`;
|
|
80
|
+
}
|
|
81
|
+
// Just the checklist IDS, for a caller that wants a one-line summary rather than the whole block.
|
|
51
82
|
names(pending) {
|
|
52
|
-
return pending.map((r) => r.
|
|
83
|
+
return pending.map((r) => r.id).join(', ');
|
|
53
84
|
}
|
|
54
|
-
// What
|
|
85
|
+
// What the subagent reviewing ONE checklist must be given: its doc, why it is running + over what, and
|
|
86
|
+
// the file it must write.
|
|
55
87
|
oneReviewer(req, reviewPath) {
|
|
56
|
-
const lines = [` • ${req.
|
|
88
|
+
const lines = [` • checklist ${req.id}`];
|
|
57
89
|
// The doc is REPO-relative by the time it reaches here (see ChecklistDefinition.doc), so a subagent
|
|
58
90
|
// handed this string can actually open it. Printing the raw config value would not resolve.
|
|
59
91
|
if (req.doc.trim() !== '')
|
|
@@ -88,9 +120,9 @@ let ChecklistInstructionsService = class ChecklistInstructionsService {
|
|
|
88
120
|
// hand-written agent .md.)
|
|
89
121
|
verdictFormat() {
|
|
90
122
|
return [
|
|
91
|
-
'TELL EACH subagent to write
|
|
123
|
+
'TELL EACH subagent to write each of its verdict files with EXACTLY this format (there is NO "success" field —',
|
|
92
124
|
'it was removed; "status" is a tri-state so a reviewer can pass a change AND still raise a concern):',
|
|
93
|
-
...this.reviewJsonService.verdictSchemaFor('<
|
|
125
|
+
...this.reviewJsonService.verdictSchemaFor('<the checklist id>', '', ' ').split('\n'),
|
|
94
126
|
];
|
|
95
127
|
}
|
|
96
128
|
// The diff every reviewer judges. Stated once, here, because path matching is deliberately coarse and a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist-instructions.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-instructions.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { formatFileList } from './checklist-config';\nimport {\n ChecklistReviewContext, RequiredChecklist, ReviewJsonService,\n} from './review-json';\n\n/**\n * Renders the ONE block that tells the coding agent which reviewer subagents it must run and exactly what\n * to tell them. There is a single renderer because three callers need the identical text and any drift\n * between them is a correctness bug, not a cosmetic one:\n *\n * - `wp-review-upsert-pr` — the AI asks \"what review do I owe on this diff?\"\n * - `wp-finish-upsert-pr` — fails fast, listing ONLY the reviewers that still have not run\n * - `ReviewJsonService` — the same list appended to a review.json validation failure\n *\n * Callers pass ONLY the checklists still needing a verdict. A checklist already reviewed on this branch is\n * never re-listed — re-instructing it invites a redundant second run, and (worse) reads as though the\n * earlier verdict did not count.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistInstructionsService {\n // Injected so the verdict path comes from the ONE place that knows it. This class used to hand-roll\n // dirname with a regex and a hardcoded '/', a second implementation of ReviewJsonService's\n // checklistResultPath that agreed on POSIX and disagreed on Windows — the same duplicate-logic trap as\n // the two different matched-file truncation caps.\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /**\n * The full instruction block, or '' when nothing is pending (so a caller can concatenate it blindly).\n * `reviewPath` is the branch's review.json — each verdict file sits beside it as review-<id>.json.\n */\n render(pending: readonly RequiredChecklist[], reviewPath: string, context: ChecklistReviewContext): string {\n if (pending.length === 0) return '';\n const lines: string[] = [\n `You MUST run these ${pending.length} reviewer subagent(s) — a SEPARATE one for each. You may NOT review`,\n `your own work, and you may NOT write a reviewer's verdict file on its behalf.`,\n '',\n ];\n for (const req of pending) lines.push(...this.oneReviewer(req, reviewPath));\n lines.push('', ...this.verdictFormat());\n lines.push('', ...this.diffLines(context));\n return lines.join('\\n');\n }\n\n // Just the reviewer NAMES, for a caller that wants a one-line summary rather than the whole block.\n names(pending: readonly RequiredChecklist[]): string {\n return pending.map((r: RequiredChecklist): string => r.subagent).join(', ');\n }\n\n // What ONE subagent must be given: its doc, why it is running + over what, and the file it must write.\n private oneReviewer(req: RequiredChecklist, reviewPath: string): string[] {\n const lines = [` • ${req.subagent}`];\n // The doc is REPO-relative by the time it reaches here (see ChecklistDefinition.doc), so a subagent\n // handed this string can actually open it. Printing the raw config value would not resolve.\n if (req.doc.trim() !== '') lines.push(` doc to read: ${req.doc}`);\n for (const scopeLine of this.scope(req)) lines.push(` ${scopeLine}`);\n lines.push(` must write: ${this.reviewJsonService.checklistResultPath(reviewPath, req.id)}`);\n return lines;\n }\n\n /**\n * WHY this reviewer is running, and over what. NOT every checklist is pattern-matched: one with no\n * `patterns` runs on EVERY PR, and calling its file list \"matched\" implies the list is a narrow,\n * pre-filtered slice of the diff when it is in fact the whole thing. When patterns DID fire, they are\n * named — the reviewer cannot otherwise tell a precise migrations-only glob from a blanket match-all one.\n */\n private scope(req: RequiredChecklist): string[] {\n if (req.matchedPatterns.length === 0) {\n return [\n `in scope: ALWAYS RUNS — this checklist has no patterns, so the WHOLE diff is in scope`,\n ` all ${req.matchedFiles.length} changed file(s): ${formatFileList(req.matchedFiles)}`,\n ];\n }\n const globs = req.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ');\n return [\n `in scope: ${req.matchedFiles.length} file(s) matched ${globs}`,\n ` ${formatFileList(req.matchedFiles)}`,\n ];\n }\n\n // ONE shared format block for every reviewer, rather than repeating the schema under each name — and\n // the schema itself comes from ReviewJsonService, the single renderer, so a change to the verdict shape\n // cannot leave a stale copy in print. (It did once: `success` outlived its own removal in every\n // hand-written agent .md.)\n private verdictFormat(): string[] {\n return [\n 'TELL EACH subagent to write that file with EXACTLY this format (there is NO \"success\" field —',\n 'it was removed; \"status\" is a tri-state so a reviewer can pass a change AND still raise a concern):',\n ...this.reviewJsonService.verdictSchemaFor('<its own subagent name>', '', ' ').split('\\n'),\n ];\n }\n\n // The diff every reviewer judges. Stated once, here, because path matching is deliberately coarse and a\n // reviewer that only sees filenames cannot make the content-level call the checklist is asking for.\n private diffLines(context: ChecklistReviewContext): string[] {\n // NEVER omit these lines quietly. A reviewer given filenames but no way to read the change cannot make\n // the content-level judgment the checklist asks for, and a silently shorter instruction block is\n // indistinguishable from a complete one — so an unresolvable base is stated as the problem it is.\n if (context.baseSha.trim() === '') {\n return [\n '⚠️ No diff base resolved for this branch, so the exact `git diff` command could not be given.',\n ' Tell each reviewer to diff against main itself (`git diff $(git merge-base origin/main HEAD) HEAD -- <file>`)',\n ' and note that path matching is COARSE — judge the real change, not the path.',\n ];\n }\n const lines = ['Also give EACH one the real diff — path matching is COARSE, so judge the change, not the path:'];\n // The MATERIALIZED diff when it exists: it cannot go empty the way a hand-assembled range can. The\n // MANIFEST leads, and ALL.diff follows it, for the same reason the per-reviewer instructions were\n // reordered: whichever one is printed first and framed as \"everything\" is the only one that gets\n // opened — measured 4/4 — and ALL.diff is the one that can be silently incomplete.\n if (context.diffDir.trim() !== '') {\n lines.push(` path map, AUTHORITATIVE: ${context.diffDir}/manifest.json`);\n lines.push(' per file: status, bytes, absolute source + diff paths,');\n lines.push(' plus what was truncated or excluded');\n lines.push(` per-file diffs: ${context.diffDir}/files/`);\n lines.push(` combined view (may be capped): ${context.diffDir}/ALL.diff`);\n }\n // NEVER hand-assemble `<baseSha> HEAD` here. On a dirty tree the changed-file set is base→working\n // tree, so that range is empty and the reviewer is handed a command that shows it nothing — the\n // failure this whole class of field exists to prevent. The command is derived from the same basis\n // the file set was; when it is absent (an older pr-gate wrote the context), say so instead.\n if (context.fileDiffCommand.trim() !== '') {\n lines.push(` reproduce one file: ${context.fileDiffCommand}`);\n } else {\n lines.push(` ⚠️ no reproduce command recorded — derive it yourself from base ${context.baseSha},`);\n lines.push(' and check `git status` first: uncommitted work needs `git diff <base>` with NO head.');\n }\n if (context.dirty) {\n lines.push(' ⚠️ this diff INCLUDES uncommitted + untracked work, so it is not what a commit-to-commit');\n lines.push(' range would show. Judge it anyway — it is what the checklist matched against.');\n }\n if (context.prContextPath.trim() !== '') {\n lines.push(` full changed-file set + base/head sha: ${context.prContextPath}`);\n }\n return lines;\n }\n\n}\n"]}
|
|
1
|
+
{"version":3,"file":"checklist-instructions.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-instructions.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\nimport { formatFileList } from './checklist-config';\nimport {\n ChecklistReviewContext, RequiredChecklist, ReviewJsonService,\n} from './review-json';\n\n/**\n * Renders the ONE block that tells the coding agent which reviewer subagents it must run and exactly what\n * to tell them. There is a single renderer because three callers need the identical text and any drift\n * between them is a correctness bug, not a cosmetic one:\n *\n * - `wp-review-upsert-pr` — the AI asks \"what review do I owe on this diff?\"\n * - `wp-finish-upsert-pr` — fails fast, listing ONLY the reviewers that still have not run\n * - `ReviewJsonService` — the same list appended to a review.json validation failure\n *\n * Callers pass ONLY the checklists still needing a verdict. A checklist already reviewed on this branch is\n * never re-listed — re-instructing it invites a redundant second run, and (worse) reads as though the\n * earlier verdict did not count.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistInstructionsService {\n // Injected so the verdict path comes from the ONE place that knows it. This class used to hand-roll\n // dirname with a regex and a hardcoded '/', a second implementation of ReviewJsonService's\n // checklistResultPath that agreed on POSIX and disagreed on Windows — the same duplicate-logic trap as\n // the two different matched-file truncation caps.\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /**\n * The full instruction block, or '' when nothing is pending (so a caller can concatenate it blindly).\n * `reviewPath` is the branch's review.json — each verdict file sits beside it as review-<id>.json.\n */\n render(pending: readonly RequiredChecklist[], reviewPath: string, context: ChecklistReviewContext): string {\n if (pending.length === 0) return '';\n const lines: string[] = [...this.spawnRule(pending), ''];\n for (const req of pending) lines.push(...this.oneReviewer(req, reviewPath));\n lines.push('', ...this.verdictFormat());\n lines.push('', ...this.diffLines(context));\n return lines.join('\\n');\n }\n\n /**\n * HOW MANY subagents, of WHICH type. Without `reviewerAgents` that is one separate subagent per checklist;\n * with it, at most N subagents with the checklists grouped across them as the AI judges best. Either way\n * every subagent is the repo's ONE reviewer agent type, and every checklist still gets its own verdict.\n */\n spawnRule(pending: readonly RequiredChecklist[]): string[] {\n const reviewer = pending[0].reviewer;\n const own = [\n 'You may NOT review your own work, and you may NOT write a reviewer\\'s verdict file on its behalf.',\n ];\n if (!reviewer.grouped()) {\n return [\n `You MUST run these ${pending.length} checklist review(s) — a SEPARATE \\`${reviewer.agentName}\\` subagent for each.`,\n ...own,\n ];\n }\n const cap = Math.min(reviewer.maxAgents, pending.length);\n return [\n `You MUST run these ${pending.length} checklist review(s) using AT MOST ${cap} \\`${reviewer.agentName}\\` subagent(s)`,\n `(commands.pr-gate.reviewerAgents = ${reviewer.maxAgents}). ${this.groupingHint(pending.length, cap)}`,\n 'Give each subagent every checklist it covers (doc, in-scope files, verdict path); it writes ONE verdict',\n 'file per checklist it covers, and never one for a checklist it was not given.',\n ...own,\n ];\n }\n\n /**\n * The grouping sentence, with an example that fits THIS round's numbers — a cap of 1 has no choice to\n * make, and saying \"group them as you judge best\" there would invite a second subagent.\n */\n groupingHint(count: number, cap: number): string {\n if (cap <= 1) return 'Use ONE subagent for all of them.';\n const per = Math.ceil(count / cap);\n return `Group the checklists across them as you judge best — e.g. one subagent for all of them, or ${cap} with about ${per} each.`;\n }\n\n // Just the checklist IDS, for a caller that wants a one-line summary rather than the whole block.\n names(pending: readonly RequiredChecklist[]): string {\n return pending.map((r: RequiredChecklist): string => r.id).join(', ');\n }\n\n // What the subagent reviewing ONE checklist must be given: its doc, why it is running + over what, and\n // the file it must write.\n private oneReviewer(req: RequiredChecklist, reviewPath: string): string[] {\n const lines = [` • checklist ${req.id}`];\n // The doc is REPO-relative by the time it reaches here (see ChecklistDefinition.doc), so a subagent\n // handed this string can actually open it. Printing the raw config value would not resolve.\n if (req.doc.trim() !== '') lines.push(` doc to read: ${req.doc}`);\n for (const scopeLine of this.scope(req)) lines.push(` ${scopeLine}`);\n lines.push(` must write: ${this.reviewJsonService.checklistResultPath(reviewPath, req.id)}`);\n return lines;\n }\n\n /**\n * WHY this reviewer is running, and over what. NOT every checklist is pattern-matched: one with no\n * `patterns` runs on EVERY PR, and calling its file list \"matched\" implies the list is a narrow,\n * pre-filtered slice of the diff when it is in fact the whole thing. When patterns DID fire, they are\n * named — the reviewer cannot otherwise tell a precise migrations-only glob from a blanket match-all one.\n */\n private scope(req: RequiredChecklist): string[] {\n if (req.matchedPatterns.length === 0) {\n return [\n `in scope: ALWAYS RUNS — this checklist has no patterns, so the WHOLE diff is in scope`,\n ` all ${req.matchedFiles.length} changed file(s): ${formatFileList(req.matchedFiles)}`,\n ];\n }\n const globs = req.matchedPatterns.map((p: string): string => `\"${p}\"`).join(', ');\n return [\n `in scope: ${req.matchedFiles.length} file(s) matched ${globs}`,\n ` ${formatFileList(req.matchedFiles)}`,\n ];\n }\n\n // ONE shared format block for every reviewer, rather than repeating the schema under each name — and\n // the schema itself comes from ReviewJsonService, the single renderer, so a change to the verdict shape\n // cannot leave a stale copy in print. (It did once: `success` outlived its own removal in every\n // hand-written agent .md.)\n private verdictFormat(): string[] {\n return [\n 'TELL EACH subagent to write each of its verdict files with EXACTLY this format (there is NO \"success\" field —',\n 'it was removed; \"status\" is a tri-state so a reviewer can pass a change AND still raise a concern):',\n ...this.reviewJsonService.verdictSchemaFor('<the checklist id>', '', ' ').split('\\n'),\n ];\n }\n\n // The diff every reviewer judges. Stated once, here, because path matching is deliberately coarse and a\n // reviewer that only sees filenames cannot make the content-level call the checklist is asking for.\n private diffLines(context: ChecklistReviewContext): string[] {\n // NEVER omit these lines quietly. A reviewer given filenames but no way to read the change cannot make\n // the content-level judgment the checklist asks for, and a silently shorter instruction block is\n // indistinguishable from a complete one — so an unresolvable base is stated as the problem it is.\n if (context.baseSha.trim() === '') {\n return [\n '⚠️ No diff base resolved for this branch, so the exact `git diff` command could not be given.',\n ' Tell each reviewer to diff against main itself (`git diff $(git merge-base origin/main HEAD) HEAD -- <file>`)',\n ' and note that path matching is COARSE — judge the real change, not the path.',\n ];\n }\n const lines = ['Also give EACH one the real diff — path matching is COARSE, so judge the change, not the path:'];\n // The MATERIALIZED diff when it exists: it cannot go empty the way a hand-assembled range can. The\n // MANIFEST leads, and ALL.diff follows it, for the same reason the per-reviewer instructions were\n // reordered: whichever one is printed first and framed as \"everything\" is the only one that gets\n // opened — measured 4/4 — and ALL.diff is the one that can be silently incomplete.\n if (context.diffDir.trim() !== '') {\n lines.push(` path map, AUTHORITATIVE: ${context.diffDir}/manifest.json`);\n lines.push(' per file: status, bytes, absolute source + diff paths,');\n lines.push(' plus what was truncated or excluded');\n lines.push(` per-file diffs: ${context.diffDir}/files/`);\n lines.push(` combined view (may be capped): ${context.diffDir}/ALL.diff`);\n }\n // NEVER hand-assemble `<baseSha> HEAD` here. On a dirty tree the changed-file set is base→working\n // tree, so that range is empty and the reviewer is handed a command that shows it nothing — the\n // failure this whole class of field exists to prevent. The command is derived from the same basis\n // the file set was; when it is absent (an older pr-gate wrote the context), say so instead.\n if (context.fileDiffCommand.trim() !== '') {\n lines.push(` reproduce one file: ${context.fileDiffCommand}`);\n } else {\n lines.push(` ⚠️ no reproduce command recorded — derive it yourself from base ${context.baseSha},`);\n lines.push(' and check `git status` first: uncommitted work needs `git diff <base>` with NO head.');\n }\n if (context.dirty) {\n lines.push(' ⚠️ this diff INCLUDES uncommitted + untracked work, so it is not what a commit-to-commit');\n lines.push(' range would show. Judge it anyway — it is what the checklist matched against.');\n }\n if (context.prContextPath.trim() !== '') {\n lines.push(` full changed-file set + base/head sha: ${context.prContextPath}`);\n }\n return lines;\n }\n\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChecklistDefinition } from './checklist-config';
|
|
1
|
+
import { ChecklistDefinition, ReviewerAgentPolicy } from './checklist-config';
|
|
2
2
|
/**
|
|
3
3
|
* Validates the review checklists declared in `pr-gate.checklists`. The array in webpieces.config.json is
|
|
4
4
|
* the ONLY accepted shape: there is deliberately no fallback, no second location, and no back-compat path
|
|
@@ -7,22 +7,23 @@ import { ChecklistDefinition } from './checklist-config';
|
|
|
7
7
|
* strictly better than carrying two code paths forever so that nobody has to read an error message.
|
|
8
8
|
*
|
|
9
9
|
* Deliberately NOT `@injectable`: config validation runs from module-level functions in validate-config.ts,
|
|
10
|
-
* before any container exists, so this is constructed directly.
|
|
11
|
-
* decorator plus a comment claiming it was "injected by type and drawn in the DI design" — while nothing
|
|
12
|
-
* injected it and it appeared nowhere in design.json. A false claim the next reader would have trusted.
|
|
10
|
+
* before any container exists, so this is constructed directly.
|
|
13
11
|
*/
|
|
14
12
|
export declare class ChecklistValidator {
|
|
15
13
|
/**
|
|
16
14
|
* Human-readable errors for the configured checklists, or [] when they are valid. Never throws.
|
|
17
|
-
* `defs` are already narrowed by buildPrGateConfig; this checks what only the filesystem can
|
|
15
|
+
* `defs` are already narrowed by buildPrGateConfig; this checks ids plus what only the filesystem can
|
|
16
|
+
* answer (each doc exists — skipped when `repoRoot` is undefined, i.e. a structure-only validation).
|
|
18
17
|
*/
|
|
19
|
-
validate(repoRoot: string, defs: readonly ChecklistDefinition[]): string[];
|
|
20
|
-
private validateSubagent;
|
|
18
|
+
validate(repoRoot: string | undefined, defs: readonly ChecklistDefinition[]): string[];
|
|
21
19
|
/**
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* The reviewer agent file named by `commands.pr-gate.reviewerAgentName`, checked ONCE for the repo.
|
|
21
|
+
*
|
|
22
|
+
* Only enforced when this repo HAS a `.claude/agents` dir — a non-Claude-Code consumer that drives the
|
|
23
|
+
* gate some other way must not be broken by a check for a directory it never has. Without it a typo
|
|
24
|
+
* validates clean, gets printed to the coding agent as "spawn this", and the path of least resistance
|
|
25
|
+
* becomes writing the verdicts itself — the self-certification the gate exists to prevent.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
validateReviewerAgent(repoRoot: string, reviewer: ReviewerAgentPolicy): string[];
|
|
28
|
+
private validateId;
|
|
28
29
|
}
|
|
@@ -4,11 +4,16 @@ exports.ChecklistValidator = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const checklist_config_1 = require("./checklist-config");
|
|
8
|
+
const constants_1 = require("./constants");
|
|
7
9
|
// Where a reviewer subagent's definition must live for Claude Code to be able to spawn it.
|
|
8
10
|
const AGENTS_DIR = path.join('.claude', 'agents');
|
|
9
11
|
// Every checklist error names this, so a reader always knows which file and key to open. There is exactly
|
|
10
12
|
// ONE place checklists can be configured, so there is exactly one label.
|
|
11
13
|
const SOURCE = 'pr-gate.checklists in webpieces.config.json';
|
|
14
|
+
// A checklist id becomes a FILE NAME (review-<id>.json, <id>.instructions.md), so it is held to a
|
|
15
|
+
// file-name-safe alphabet rather than trusted to not contain a slash.
|
|
16
|
+
const CHECKLIST_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
12
17
|
/**
|
|
13
18
|
* Validates the review checklists declared in `pr-gate.checklists`. The array in webpieces.config.json is
|
|
14
19
|
* the ONLY accepted shape: there is deliberately no fallback, no second location, and no back-compat path
|
|
@@ -17,61 +22,68 @@ const SOURCE = 'pr-gate.checklists in webpieces.config.json';
|
|
|
17
22
|
* strictly better than carrying two code paths forever so that nobody has to read an error message.
|
|
18
23
|
*
|
|
19
24
|
* Deliberately NOT `@injectable`: config validation runs from module-level functions in validate-config.ts,
|
|
20
|
-
* before any container exists, so this is constructed directly.
|
|
21
|
-
* decorator plus a comment claiming it was "injected by type and drawn in the DI design" — while nothing
|
|
22
|
-
* injected it and it appeared nowhere in design.json. A false claim the next reader would have trusted.
|
|
25
|
+
* before any container exists, so this is constructed directly.
|
|
23
26
|
*/
|
|
24
27
|
class ChecklistValidator {
|
|
25
28
|
/**
|
|
26
29
|
* Human-readable errors for the configured checklists, or [] when they are valid. Never throws.
|
|
27
|
-
* `defs` are already narrowed by buildPrGateConfig; this checks what only the filesystem can
|
|
30
|
+
* `defs` are already narrowed by buildPrGateConfig; this checks ids plus what only the filesystem can
|
|
31
|
+
* answer (each doc exists — skipped when `repoRoot` is undefined, i.e. a structure-only validation).
|
|
28
32
|
*/
|
|
29
33
|
validate(repoRoot, defs) {
|
|
30
34
|
const errors = [];
|
|
31
35
|
const seen = new Set();
|
|
32
|
-
// Only enforce the reviewer-agent file when this repo HAS an agents dir — a non-Claude-Code consumer
|
|
33
|
-
// that drives the gate some other way must not be broken by a check for a directory it never has.
|
|
34
|
-
const agentsDir = path.join(repoRoot, AGENTS_DIR);
|
|
35
|
-
const checkAgents = fs.existsSync(agentsDir);
|
|
36
36
|
defs.forEach((def, i) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (def.doc
|
|
37
|
+
errors.push(...this.validateId(def, i, seen));
|
|
38
|
+
const label = def.id !== '' ? `"${def.id}"` : `checklists[${i}]`;
|
|
39
|
+
if (def.doc === '') {
|
|
40
|
+
errors.push(`[pr-gate] ${SOURCE} ${label}.doc is required — the REPO-relative guidance doc the reviewer ` +
|
|
41
|
+
`reviews against (e.g. ".claude/review/${def.id !== '' ? def.id : 'my-checklist'}.md"). The reviewer ` +
|
|
42
|
+
`agent is generic, so the doc IS the checklist.`);
|
|
43
|
+
}
|
|
44
|
+
else if (repoRoot !== undefined && !fs.existsSync(path.join(repoRoot, def.doc))) {
|
|
40
45
|
errors.push(`[pr-gate] ${SOURCE} ${label}.doc "${def.doc}" does not exist (paths are REPO-relative).`);
|
|
41
46
|
}
|
|
42
47
|
});
|
|
43
48
|
return errors;
|
|
44
49
|
}
|
|
45
|
-
// `subagent` is the ONE required field and the whole distinct-reviewer guarantee rests on it.
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
47
|
-
validateSubagent(def, i, seen, checkAgents, agentsDir) {
|
|
48
|
-
const label = def.subagent !== '' ? `"${def.subagent}"` : `checklists[${i}]`;
|
|
49
|
-
if (def.subagent.trim() === '') {
|
|
50
|
-
return [`[pr-gate] ${SOURCE} checklists[${i}].subagent must be a non-empty string (the reviewer agent name, matching ${AGENTS_DIR}/<subagent>.md).`];
|
|
51
|
-
}
|
|
52
|
-
if (seen.has(def.subagent)) {
|
|
53
|
-
return [`[pr-gate] ${SOURCE} duplicate subagent "${def.subagent}" — each checklist must use a DISTINCT reviewer subagent (that is how independent review is enforced).`];
|
|
54
|
-
}
|
|
55
|
-
seen.add(def.subagent);
|
|
56
|
-
return this.validateSubagentExists(def.subagent, label, checkAgents, agentsDir);
|
|
57
|
-
}
|
|
58
50
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
51
|
+
* The reviewer agent file named by `commands.pr-gate.reviewerAgentName`, checked ONCE for the repo.
|
|
52
|
+
*
|
|
53
|
+
* Only enforced when this repo HAS a `.claude/agents` dir — a non-Claude-Code consumer that drives the
|
|
54
|
+
* gate some other way must not be broken by a check for a directory it never has. Without it a typo
|
|
55
|
+
* validates clean, gets printed to the coding agent as "spawn this", and the path of least resistance
|
|
56
|
+
* becomes writing the verdicts itself — the self-certification the gate exists to prevent.
|
|
63
57
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
validateReviewerAgent(repoRoot, reviewer) {
|
|
59
|
+
const agentsDir = path.join(repoRoot, AGENTS_DIR);
|
|
60
|
+
if (reviewer.agentName === '' || !fs.existsSync(agentsDir))
|
|
66
61
|
return [];
|
|
67
|
-
if (fs.existsSync(path.join(agentsDir, `${
|
|
62
|
+
if (fs.existsSync(path.join(agentsDir, `${reviewer.agentName}.md`)))
|
|
68
63
|
return [];
|
|
64
|
+
const cure = reviewer.agentName === checklist_config_1.DEFAULT_REVIEWER_AGENT_NAME
|
|
65
|
+
? `Run \`${constants_1.UPGRADE_SHIM_COMMAND}\` — it writes that webpieces-owned file.`
|
|
66
|
+
: 'Create that agent file, or set "reviewerAgentName" to an agent that exists.';
|
|
69
67
|
return [
|
|
70
|
-
`[pr-gate]
|
|
71
|
-
|
|
72
|
-
`
|
|
68
|
+
`[pr-gate] commands.pr-gate.reviewerAgentName "${reviewer.agentName}" names no reviewer — ` +
|
|
69
|
+
`${AGENTS_DIR}/${reviewer.agentName}.md does not exist, so nothing can spawn it and wp-finish-upsert-pr ` +
|
|
70
|
+
`would block forever on verdicts no reviewer can write. ${cure}`,
|
|
73
71
|
];
|
|
74
72
|
}
|
|
73
|
+
// `id` keys every per-checklist file and the dashboard row, so it must be present, unique and file-safe.
|
|
74
|
+
validateId(def, i, seen) {
|
|
75
|
+
if (def.id === '') {
|
|
76
|
+
return [`[pr-gate] ${SOURCE} checklists[${i}].id must be a non-empty string — the checklist's name; it keys review-<id>.json.`];
|
|
77
|
+
}
|
|
78
|
+
if (!CHECKLIST_ID.test(def.id)) {
|
|
79
|
+
return [`[pr-gate] ${SOURCE} checklists[${i}].id "${def.id}" must use only letters, digits, ".", "_" and "-" (it becomes a file name).`];
|
|
80
|
+
}
|
|
81
|
+
if (seen.has(def.id)) {
|
|
82
|
+
return [`[pr-gate] ${SOURCE} duplicate id "${def.id}" — every checklist needs its own id, because each writes its own review-<id>.json.`];
|
|
83
|
+
}
|
|
84
|
+
seen.add(def.id);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
exports.ChecklistValidator = ChecklistValidator;
|
|
77
89
|
//# sourceMappingURL=checklist-validator.js.map
|