@webpieces/rules-config 0.4.481 → 0.4.482

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/rules-config",
3
- "version": "0.4.481",
3
+ "version": "0.4.482",
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",
@@ -5,9 +5,40 @@ export declare class ChecklistDefinition {
5
5
  patterns: string[];
6
6
  constructor(id: string, subagent: string, doc: string, patterns: string[]);
7
7
  }
8
+ /**
9
+ * WHERE a repo's checklists come from. Exactly one of the two is populated (an array in
10
+ * webpieces.config.json wins when both are present, and validation says so), and both are empty for the
11
+ * common case of a repo with no checklists at all. Data-only.
12
+ */
13
+ export declare class ChecklistSource {
14
+ inline: ChecklistDefinition[];
15
+ doc: string;
16
+ constructor(inline?: ChecklistDefinition[], doc?: string);
17
+ isEmpty(): boolean;
18
+ describe(): string;
19
+ }
8
20
  export interface RawChecklistItem {
9
21
  subagent?: string;
10
22
  doc?: string;
11
23
  patterns?: string[];
12
24
  }
13
- export declare function toChecklist(raw: RawChecklistItem): ChecklistDefinition;
25
+ /**
26
+ * Build a ChecklistDefinition from an omitting-friendly raw entry. `id` defaults to the subagent name (the
27
+ * only stable, human-meaningful key we have). `docBaseRel` is the repo-relative DIRECTORY that a relative
28
+ * `raw.doc` resolves against: '' for the array-in-config form (repo root), `dirname(manifestDoc)` for the
29
+ * legacy manifest form. The stored `doc` is always repo-relative — see ChecklistDefinition.doc.
30
+ */
31
+ export declare function toChecklist(raw: RawChecklistItem, docBaseRel?: string): ChecklistDefinition;
32
+ export declare const MATCHED_FILES_CAP = 6;
33
+ /**
34
+ * Render a file list for a message, NEVER silently. A truncated list that looks complete is how a reviewer
35
+ * gets pointed at 4 of 40 changed files and reports success having reviewed a tenth of the diff, so the
36
+ * dropped count is always stated and the caller is expected to name the file holding the full set.
37
+ */
38
+ export declare function formatFileList(files: readonly string[], cap?: number): string;
39
+ /**
40
+ * Resolve a checklist entry's `doc` to a repo-relative POSIX path. This is the fix for a reviewer subagent
41
+ * being handed a bare `deploy-infra.md` that exists nowhere relative to its CWD: the resolution against the
42
+ * manifest doc's directory happens ONCE, here, instead of being re-derived (or forgotten) at each print site.
43
+ */
44
+ export declare function resolveChecklistDoc(doc: string, docBaseRel: string): string;
@@ -1,4 +1,11 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATCHED_FILES_CAP = exports.ChecklistSource = exports.ChecklistDefinition = void 0;
4
+ exports.toChecklist = toChecklist;
5
+ exports.formatFileList = formatFileList;
6
+ exports.resolveChecklistDoc = resolveChecklistDoc;
7
+ const tslib_1 = require("tslib");
8
+ const path = tslib_1.__importStar(require("path"));
2
9
  // A company review checklist: a diff-triggered extension point that lets a CONSUMER inject its own
3
10
  // PR-time review process into the webpieces gated flow WITHOUT forking the tooling. Each checklist names
4
11
  // a reviewer SUBAGENT (a `.claude/agents/<subagent>.md`) and the doc that reviewer reads; when the diff
@@ -6,16 +13,21 @@
6
13
  // it, and wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json exists
7
14
  // AND that named subagent is proven (from the harness's own artifacts) to have actually run.
8
15
  //
9
- // The checklist SET is NOT defined in webpieces.config.json that only points at ONE manifest doc
10
- // (`checklists: { "doc": "..." }`). The manifest lives in the doc so the review process is content, not
11
- // config. This is a SURFACING + AUDIT extension point; webpieces owns only the mechanism. Data-only.
12
- Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.ChecklistDefinition = void 0;
14
- exports.toChecklist = toChecklist;
16
+ // TWO config shapes are supported (see ChecklistSource):
17
+ // PRIMARY — `checklists: [ { subagent, doc?, patterns? } ]` in webpieces.config.json. `patterns` is a
18
+ // path-glob dispatch table and `subagent` is a name binding: both are config, so they live
19
+ // where every tool that reads webpieces.config.json can see, grep, and schema them.
20
+ // LEGACY — `checklists: { "doc": "..." }`, where the array lives in an HTML comment inside that doc.
21
+ // Still loaded (it is shipped and consumers depend on it), but no longer the recommended form.
22
+ // Data-only.
15
23
  class ChecklistDefinition {
16
24
  id; // = the subagent name; keys review-<id>.json and the dashboard row
17
25
  subagent; // reviewer agent name → .claude/agents/<subagent>.md; the agentType the harness stamps
18
- doc; // repo-relative guidance doc the reviewer reads (may be '' — then it reads the manifest doc)
26
+ // REPO-RELATIVE guidance doc the reviewer reads (may be '' — then it reads the manifest doc). Always
27
+ // repo-relative by the time it reaches here, whichever config shape it came from, because this value is
28
+ // printed verbatim to a reviewer subagent as "the file to open" — a path relative to some other file's
29
+ // directory is unresolvable from where that subagent stands.
30
+ doc;
19
31
  patterns; // path globs (isPathExcluded semantics); [] = matches any changed file (always runs)
20
32
  constructor(id, subagent, doc, patterns) {
21
33
  this.id = id;
@@ -25,11 +37,74 @@ class ChecklistDefinition {
25
37
  }
26
38
  }
27
39
  exports.ChecklistDefinition = ChecklistDefinition;
28
- // Build a ChecklistDefinition from an omitting-friendly raw manifest entry. id defaults to the subagent
29
- // name (the only stable, human-meaningful key we have).
40
+ /**
41
+ * WHERE a repo's checklists come from. Exactly one of the two is populated (an array in
42
+ * webpieces.config.json wins when both are present, and validation says so), and both are empty for the
43
+ * common case of a repo with no checklists at all. Data-only.
44
+ */
45
+ class ChecklistSource {
46
+ // The array form straight from `pr-gate.checklists` in webpieces.config.json (PRIMARY). Already
47
+ // narrowed + repo-relative-resolved. [] when the repo uses the legacy `{ doc }` form or has none.
48
+ inline;
49
+ // Repo-relative path of the ONE doc carrying a `<!-- webpieces:checklists [...] -->` manifest (LEGACY).
50
+ // '' when the repo uses the array form or has no checklists.
51
+ doc;
52
+ constructor(inline = [], doc = '') {
53
+ this.inline = inline;
54
+ this.doc = doc;
55
+ }
56
+ // True when this repo configured no checklists at all (neither shape).
57
+ isEmpty() {
58
+ return this.inline.length === 0 && this.doc.trim() === '';
59
+ }
60
+ // How to NAME this source in a message: the config key for the array form, the doc path for the
61
+ // manifest form. Every checklist error/notice cites one of these so a reader knows what file to open.
62
+ describe() {
63
+ if (this.doc.trim() !== '')
64
+ return this.doc;
65
+ return 'pr-gate.checklists in webpieces.config.json';
66
+ }
67
+ }
68
+ exports.ChecklistSource = ChecklistSource;
69
+ /**
70
+ * Build a ChecklistDefinition from an omitting-friendly raw entry. `id` defaults to the subagent name (the
71
+ * only stable, human-meaningful key we have). `docBaseRel` is the repo-relative DIRECTORY that a relative
72
+ * `raw.doc` resolves against: '' for the array-in-config form (repo root), `dirname(manifestDoc)` for the
73
+ * legacy manifest form. The stored `doc` is always repo-relative — see ChecklistDefinition.doc.
74
+ */
30
75
  // webpieces-disable no-function-outside-class -- pure config transform beside its data class
31
- function toChecklist(raw) {
76
+ function toChecklist(raw, docBaseRel = '') {
32
77
  const subagent = raw.subagent ?? '';
33
- return new ChecklistDefinition(subagent, subagent, raw.doc ?? '', raw.patterns ?? []);
78
+ return new ChecklistDefinition(subagent, subagent, resolveChecklistDoc(raw.doc ?? '', docBaseRel), raw.patterns ?? []);
79
+ }
80
+ // The ONE cap for every printed matched-file list. Two print sites used to slice to 4 and to 5 — two
81
+ // different caps for the same list, neither chosen deliberately, and both without an ellipsis.
82
+ exports.MATCHED_FILES_CAP = 6;
83
+ /**
84
+ * Render a file list for a message, NEVER silently. A truncated list that looks complete is how a reviewer
85
+ * gets pointed at 4 of 40 changed files and reports success having reviewed a tenth of the diff, so the
86
+ * dropped count is always stated and the caller is expected to name the file holding the full set.
87
+ */
88
+ // webpieces-disable no-function-outside-class -- pure display formatter beside the data it formats
89
+ function formatFileList(files, cap = exports.MATCHED_FILES_CAP) {
90
+ if (files.length === 0)
91
+ return '(none)';
92
+ if (files.length <= cap)
93
+ return files.join(', ');
94
+ return `${files.slice(0, cap).join(', ')}, +${files.length - cap} more (${files.length} total)`;
95
+ }
96
+ /**
97
+ * Resolve a checklist entry's `doc` to a repo-relative POSIX path. This is the fix for a reviewer subagent
98
+ * being handed a bare `deploy-infra.md` that exists nowhere relative to its CWD: the resolution against the
99
+ * manifest doc's directory happens ONCE, here, instead of being re-derived (or forgotten) at each print site.
100
+ */
101
+ // webpieces-disable no-function-outside-class -- pure path transform beside its data class
102
+ function resolveChecklistDoc(doc, docBaseRel) {
103
+ const trimmed = doc.trim();
104
+ if (trimmed === '')
105
+ return '';
106
+ const base = docBaseRel.trim();
107
+ const joined = base === '' || base === '.' ? trimmed : `${base}/${trimmed}`;
108
+ return path.posix.normalize(joined.split(path.sep).join('/'));
34
109
  }
35
110
  //# sourceMappingURL=checklist-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"checklist-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-config.ts"],"names":[],"mappings":";AAAA,mGAAmG;AACnG,yGAAyG;AACzG,wGAAwG;AACxG,uGAAuG;AACvG,0GAA0G;AAC1G,6FAA6F;AAC7F,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,qGAAqG;;;AA0BrG,kCAGC;AA3BD,MAAa,mBAAmB;IAC5B,EAAE,CAAS,CAAS,mEAAmE;IACvF,QAAQ,CAAS,CAAG,uFAAuF;IAC3G,GAAG,CAAS,CAAQ,6FAA6F;IACjH,QAAQ,CAAW,CAAC,qFAAqF;IAEzG,YAAY,EAAU,EAAE,QAAgB,EAAE,GAAW,EAAE,QAAkB;QACrE,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;IAC7B,CAAC;CACJ;AAZD,kDAYC;AASD,wGAAwG;AACxG,wDAAwD;AACxD,6FAA6F;AAC7F,SAAgB,WAAW,CAAC,GAAqB;IAC7C,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC;IACpC,OAAO,IAAI,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;AAC1F,CAAC","sourcesContent":["// 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-start-upsert-pr tells the AI to spawn that subagent to review\n// it, and wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json exists\n// AND that named subagent is proven (from the harness's own artifacts) to have actually run.\n//\n// The checklist SET is NOT defined in webpieces.config.json that only points at ONE manifest doc\n// (`checklists: { \"doc\": \"...\" }`). The manifest lives in the doc so the review process is content, not\n// config. This is a SURFACING + AUDIT extension point; webpieces owns only the mechanism. Data-only.\n\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 doc: string; // repo-relative guidance doc the reviewer reads (may be '' — then it reads the manifest doc)\n patterns: string[]; // path globs (isPathExcluded semantics); [] = matches any changed file (always runs)\n\n constructor(id: string, subagent: string, doc: string, patterns: string[]) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.patterns = patterns;\n }\n}\n\n// One manifest entry straight from the doc's JSON block, before it is validated + narrowed into a class.\nexport interface RawChecklistItem {\n subagent?: string;\n doc?: string;\n patterns?: string[];\n}\n\n// Build a ChecklistDefinition from an omitting-friendly raw manifest entry. id defaults to the subagent\n// name (the only stable, human-meaningful key we have).\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(subagent, subagent, raw.doc ?? '', raw.patterns ?? []);\n}\n"]}
1
+ {"version":3,"file":"checklist-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-config.ts"],"names":[],"mappings":"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","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-start-upsert-pr tells the AI to spawn that subagent to review\n// it, and wp-finish-upsert-pr refuses to open the PR until a well-formed, passing review-<id>.json exists\n// AND that named subagent is proven (from the harness's own artifacts) to have actually run.\n//\n// TWO config shapes are supported (see ChecklistSource):\n// PRIMARY — `checklists: [ { subagent, doc?, patterns? } ]` in webpieces.config.json. `patterns` is a\n// path-glob dispatch table and `subagent` is a name binding: both are config, so they live\n// where every tool that reads webpieces.config.json can see, grep, and schema them.\n// LEGACY — `checklists: { \"doc\": \"...\" }`, where the array lives in an HTML comment inside that doc.\n// Still loaded (it is shipped and consumers depend on it), but no longer the recommended form.\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 reads the manifest doc). Always\n // repo-relative by the time it reaches here, whichever config shape it came from, because this value is\n // printed verbatim to a reviewer subagent as \"the file to open\" — a path relative to some other file's\n // directory 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 constructor(id: string, subagent: string, doc: string, patterns: string[]) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.patterns = patterns;\n }\n}\n\n/**\n * WHERE a repo's checklists come from. Exactly one of the two is populated (an array in\n * webpieces.config.json wins when both are present, and validation says so), and both are empty for the\n * common case of a repo with no checklists at all. Data-only.\n */\nexport class ChecklistSource {\n // The array form straight from `pr-gate.checklists` in webpieces.config.json (PRIMARY). Already\n // narrowed + repo-relative-resolved. [] when the repo uses the legacy `{ doc }` form or has none.\n inline: ChecklistDefinition[];\n // Repo-relative path of the ONE doc carrying a `<!-- webpieces:checklists [...] -->` manifest (LEGACY).\n // '' when the repo uses the array form or has no checklists.\n doc: string;\n\n constructor(inline: ChecklistDefinition[] = [], doc = '') {\n this.inline = inline;\n this.doc = doc;\n }\n\n // True when this repo configured no checklists at all (neither shape).\n isEmpty(): boolean {\n return this.inline.length === 0 && this.doc.trim() === '';\n }\n\n // How to NAME this source in a message: the config key for the array form, the doc path for the\n // manifest form. Every checklist error/notice cites one of these so a reader knows what file to open.\n describe(): string {\n if (this.doc.trim() !== '') return this.doc;\n return 'pr-gate.checklists in webpieces.config.json';\n }\n}\n\n// One manifest/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}\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). `docBaseRel` is the repo-relative DIRECTORY that a relative\n * `raw.doc` resolves against: '' for the array-in-config form (repo root), `dirname(manifestDoc)` for the\n * legacy manifest form. The stored `doc` is always repo-relative — see ChecklistDefinition.doc.\n */\n// webpieces-disable no-function-outside-class -- pure config transform beside its data class\nexport function toChecklist(raw: RawChecklistItem, docBaseRel = ''): ChecklistDefinition {\n const subagent = raw.subagent ?? '';\n return new ChecklistDefinition(subagent, subagent, resolveChecklistDoc(raw.doc ?? '', docBaseRel), raw.patterns ?? []);\n}\n\n// The ONE cap for every printed matched-file list. Two print sites 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\n/**\n * Resolve a checklist entry's `doc` to a repo-relative POSIX path. This is the fix for a reviewer subagent\n * being handed a bare `deploy-infra.md` that exists nowhere relative to its CWD: the resolution against the\n * manifest doc's directory happens ONCE, here, instead of being re-derived (or forgotten) at each print site.\n */\n// webpieces-disable no-function-outside-class -- pure path transform beside its data class\nexport function resolveChecklistDoc(doc: string, docBaseRel: string): string {\n const trimmed = doc.trim();\n if (trimmed === '') return '';\n const base = docBaseRel.trim();\n const joined = base === '' || base === '.' ? trimmed : `${base}/${trimmed}`;\n return path.posix.normalize(joined.split(path.sep).join('/'));\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import { ChecklistReviewContext, RequiredChecklist } from './review-json';
2
+ /**
3
+ * Renders the ONE block that tells the coding agent which reviewer subagents it must run and exactly what
4
+ * to tell them. There is a single renderer because three callers need the identical text and any drift
5
+ * between them is a correctness bug, not a cosmetic one:
6
+ *
7
+ * - `wp-checklist` — the AI asks "what review do I owe on this diff?"
8
+ * - `wp-finish-upsert-pr` — fails fast, listing ONLY the reviewers that still have not run
9
+ * - `ReviewJsonService` — the same list appended to a review.json validation failure
10
+ *
11
+ * Callers pass ONLY the checklists still needing a verdict. A checklist already reviewed on this branch is
12
+ * never re-listed — re-instructing it invites a redundant second run, and (worse) reads as though the
13
+ * earlier verdict did not count.
14
+ *
15
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
16
+ */
17
+ export declare class ChecklistInstructionsService {
18
+ /**
19
+ * The full instruction block, or '' when nothing is pending (so a caller can concatenate it blindly).
20
+ * `reviewPath` is the branch's review.json — each verdict file sits beside it as review-<id>.json.
21
+ */
22
+ render(pending: readonly RequiredChecklist[], reviewPath: string, context: ChecklistReviewContext): string;
23
+ names(pending: readonly RequiredChecklist[]): string;
24
+ private oneReviewer;
25
+ /**
26
+ * WHY this reviewer is running, and over what. NOT every checklist is pattern-matched: one with no
27
+ * `patterns` runs on EVERY PR, and calling its file list "matched" implies the list is a narrow,
28
+ * pre-filtered slice of the diff when it is in fact the whole thing. When patterns DID fire, they are
29
+ * named — the reviewer cannot otherwise tell a precise migrations-only glob from a blanket match-all one.
30
+ */
31
+ private scope;
32
+ private verdictFormat;
33
+ private diffLines;
34
+ private verdictPath;
35
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChecklistInstructionsService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const inversify_1 = require("inversify");
6
+ const checklist_config_1 = require("./checklist-config");
7
+ /**
8
+ * Renders the ONE block that tells the coding agent which reviewer subagents it must run and exactly what
9
+ * to tell them. There is a single renderer because three callers need the identical text and any drift
10
+ * between them is a correctness bug, not a cosmetic one:
11
+ *
12
+ * - `wp-checklist` — the AI asks "what review do I owe on this diff?"
13
+ * - `wp-finish-upsert-pr` — fails fast, listing ONLY the reviewers that still have not run
14
+ * - `ReviewJsonService` — the same list appended to a review.json validation failure
15
+ *
16
+ * Callers pass ONLY the checklists still needing a verdict. A checklist already reviewed on this branch is
17
+ * never re-listed — re-instructing it invites a redundant second run, and (worse) reads as though the
18
+ * earlier verdict did not count.
19
+ *
20
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.
21
+ */
22
+ let ChecklistInstructionsService = class ChecklistInstructionsService {
23
+ /**
24
+ * The full instruction block, or '' when nothing is pending (so a caller can concatenate it blindly).
25
+ * `reviewPath` is the branch's review.json — each verdict file sits beside it as review-<id>.json.
26
+ */
27
+ render(pending, reviewPath, context) {
28
+ if (pending.length === 0)
29
+ return '';
30
+ const lines = [
31
+ `You MUST run these ${pending.length} reviewer subagent(s) — a SEPARATE one for each. You may NOT review`,
32
+ `your own work, and you may NOT write a reviewer's verdict file on its behalf.`,
33
+ '',
34
+ ];
35
+ for (const req of pending)
36
+ lines.push(...this.oneReviewer(req, reviewPath));
37
+ lines.push('', ...this.verdictFormat());
38
+ lines.push('', ...this.diffLines(context));
39
+ return lines.join('\n');
40
+ }
41
+ // Just the reviewer NAMES, for a caller that wants a one-line summary rather than the whole block.
42
+ names(pending) {
43
+ return pending.map((r) => r.subagent).join(', ');
44
+ }
45
+ // What ONE subagent must be given: its doc, why it is running + over what, and the file it must write.
46
+ oneReviewer(req, reviewPath) {
47
+ const lines = [` • ${req.subagent}`];
48
+ // The doc is REPO-relative by the time it reaches here (see ChecklistDefinition.doc), so a subagent
49
+ // handed this string can actually open it. Printing the raw config value would not resolve.
50
+ if (req.doc.trim() !== '')
51
+ lines.push(` doc to read: ${req.doc}`);
52
+ for (const scopeLine of this.scope(req))
53
+ lines.push(` ${scopeLine}`);
54
+ lines.push(` must write: ${this.verdictPath(reviewPath, req.id)}`);
55
+ return lines;
56
+ }
57
+ /**
58
+ * WHY this reviewer is running, and over what. NOT every checklist is pattern-matched: one with no
59
+ * `patterns` runs on EVERY PR, and calling its file list "matched" implies the list is a narrow,
60
+ * pre-filtered slice of the diff when it is in fact the whole thing. When patterns DID fire, they are
61
+ * named — the reviewer cannot otherwise tell a precise migrations-only glob from a blanket match-all one.
62
+ */
63
+ scope(req) {
64
+ if (req.matchedPatterns.length === 0) {
65
+ return [
66
+ `in scope: ALWAYS RUNS — this checklist has no patterns, so the WHOLE diff is in scope`,
67
+ ` all ${req.matchedFiles.length} changed file(s): ${(0, checklist_config_1.formatFileList)(req.matchedFiles)}`,
68
+ ];
69
+ }
70
+ const globs = req.matchedPatterns.map((p) => `"${p}"`).join(', ');
71
+ return [
72
+ `in scope: ${req.matchedFiles.length} file(s) matched ${globs}`,
73
+ ` ${(0, checklist_config_1.formatFileList)(req.matchedFiles)}`,
74
+ ];
75
+ }
76
+ // ONE shared format block for every reviewer, rather than repeating the schema under each name.
77
+ verdictFormat() {
78
+ return [
79
+ 'TELL EACH subagent to write that file with EXACTLY this format:',
80
+ ' { "id": "<its own subagent name>", "success": true, "output": "what you checked / found", "override": "" }',
81
+ ' success:false + empty "override" → REFUSES the PR; the reviewer\'s "output" is printed verbatim',
82
+ ' success:false + non-empty "override" → ships anyway as 🟡; the justification is published on the PR',
83
+ ];
84
+ }
85
+ // The diff every reviewer judges. Stated once, here, because path matching is deliberately coarse and a
86
+ // reviewer that only sees filenames cannot make the content-level call the checklist is asking for.
87
+ diffLines(context) {
88
+ if (context.baseSha.trim() === '')
89
+ return ['Also tell each one that path matching is COARSE — judge the real change, not the path.'];
90
+ const lines = [
91
+ 'Also give EACH one the real diff — path matching is COARSE, so judge the change, not the path:',
92
+ ` git diff ${context.baseSha} HEAD -- <file>`,
93
+ ];
94
+ if (context.prContextPath.trim() !== '') {
95
+ lines.push(` full changed-file set + base/head sha: ${context.prContextPath}`);
96
+ }
97
+ return lines;
98
+ }
99
+ // review-<id>.json beside the branch's review.json — one file per checklist so N concurrent reviewer
100
+ // subagents never clobber a shared file.
101
+ verdictPath(reviewPath, checklistId) {
102
+ const dir = reviewPath.replace(/[/\\][^/\\]*$/, '');
103
+ return `${dir}/review-${checklistId}.json`;
104
+ }
105
+ };
106
+ exports.ChecklistInstructionsService = ChecklistInstructionsService;
107
+ exports.ChecklistInstructionsService = ChecklistInstructionsService = tslib_1.__decorate([
108
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
109
+ ], ChecklistInstructionsService);
110
+ //# sourceMappingURL=checklist-instructions.js.map
@@ -0,0 +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 { ChecklistReviewContext, RequiredChecklist } 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-checklist` — 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 /**\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.verdictPath(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.\n private verdictFormat(): string[] {\n return [\n 'TELL EACH subagent to write that file with EXACTLY this format:',\n ' { \"id\": \"<its own subagent name>\", \"success\": true, \"output\": \"what you checked / found\", \"override\": \"\" }',\n ' success:false + empty \"override\" → REFUSES the PR; the reviewer\\'s \"output\" is printed verbatim',\n ' success:false + non-empty \"override\" → ships anyway as 🟡; the justification is published on the PR',\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 if (context.baseSha.trim() === '') return ['Also tell each one that path matching is COARSE — judge the real change, not the path.'];\n const lines = [\n 'Also give EACH one the real diff — path matching is COARSE, so judge the change, not the path:',\n ` git diff ${context.baseSha} HEAD -- <file>`,\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 // review-<id>.json beside the branch's review.json — one file per checklist so N concurrent reviewer\n // subagents never clobber a shared file.\n private verdictPath(reviewPath: string, checklistId: string): string {\n const dir = reviewPath.replace(/[/\\\\][^/\\\\]*$/, '');\n return `${dir}/review-${checklistId}.json`;\n }\n}\n"]}
@@ -1,15 +1,34 @@
1
- import { ChecklistDefinition } from './checklist-config';
1
+ import { ChecklistDefinition, ChecklistSource } from './checklist-config';
2
2
  /**
3
- * Loads + validates the checklist manifest embedded in the single doc `pr-gate.checklists.doc` points at.
4
- * The set of checklists lives in the DOC (content), not webpieces.config.json (config) the config only
5
- * carries the doc path. `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in
6
- * the DI design.
3
+ * Loads + validates a repo's review checklists from EITHER shape (see {@link ChecklistSource}): the
4
+ * `pr-gate.checklists` array in webpieces.config.json (primary) or the `<!-- webpieces:checklists -->`
5
+ * manifest embedded in the doc that `pr-gate.checklists.doc` points at (legacy).
6
+ *
7
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in the DI design.
7
8
  */
8
9
  export declare class ChecklistManifestService {
9
10
  manifestDocPath(repoRoot: string, docRel: string): string;
10
- load(repoRoot: string, docRel: string): ChecklistDefinition[];
11
- validate(repoRoot: string, docRel: string): string[];
11
+ /**
12
+ * The repo's checklists, or [] when there are none / the manifest doc is missing / malformed. Callers
13
+ * that need to REPORT those problems use validate(); this one is the tolerant runtime read.
14
+ */
15
+ load(repoRoot: string, source: ChecklistSource): ChecklistDefinition[];
16
+ validate(repoRoot: string, source: ChecklistSource): string[];
12
17
  private validateItems;
18
+ /**
19
+ * The checks that are the SAME for both config shapes, run against already-narrowed definitions:
20
+ * subagent present + distinct + actually resolves to a spawnable agent, and the guidance doc exists.
21
+ */
22
+ private validateResolved;
23
+ /**
24
+ * The check this validator was missing: `subagent` is the ONE required field and the whole
25
+ * distinct-reviewer guarantee rests on it, yet nothing confirmed it names a real agent. A typo used to
26
+ * validate clean, then get printed to the coding agent as "spawn this" — and since wp-finish blocks on
27
+ * `review-<that-typo>.json`, the path of least resistance became writing the reviewer's verdict itself,
28
+ * which is the exact self-certification the distinct-subagent rule exists to prevent. Reject at load.
29
+ */
30
+ private validateSubagentExists;
31
+ private usable;
13
32
  private readItems;
14
33
  private extractManifest;
15
34
  private parse;
@@ -7,38 +7,55 @@ const path = tslib_1.__importStar(require("path"));
7
7
  const inversify_1 = require("inversify");
8
8
  const checklist_config_1 = require("./checklist-config");
9
9
  const to_error_1 = require("./to-error");
10
- // The delimited JSON manifest embedded in the review doc. An HTML comment so it does NOT render in the
11
- // markdown a human reads, but is trivially + robustly parseable (JSON.parse — no YAML dependency, no
12
- // hand-rolled frontmatter parser). Example, at the top of `.claude/review/index.md`:
10
+ // The delimited JSON manifest embedded in the review doc the LEGACY shape, kept working because it is
11
+ // shipped. An HTML comment so it does NOT render in the markdown a human reads, but is trivially +
12
+ // robustly parseable (JSON.parse — no YAML dependency, no hand-rolled frontmatter parser). Example, at the
13
+ // top of `.claude/review/index.md`:
13
14
  //
14
15
  // <!-- webpieces:checklists
15
16
  // [ { "subagent": "morpheus-envvars-reviewer", "doc": "morpheus-envvars.md",
16
17
  // "patterns": ["**/.env*", "**/Dockerfile*"] } ]
17
18
  // -->
19
+ //
20
+ // New repos should put the SAME array directly in `pr-gate.checklists` in webpieces.config.json instead —
21
+ // see ChecklistSource. A JSON array inside an HTML comment is reachable only by this regex: no JSON Schema
22
+ // covers it, no editor completes it, no `jq` reads it.
18
23
  const MANIFEST_RE = /<!--\s*webpieces:checklists\s*([\s\S]*?)-->/;
24
+ // Where a reviewer subagent's definition must live for Claude Code to be able to spawn it.
25
+ const AGENTS_DIR = path.join('.claude', 'agents');
19
26
  /**
20
- * Loads + validates the checklist manifest embedded in the single doc `pr-gate.checklists.doc` points at.
21
- * The set of checklists lives in the DOC (content), not webpieces.config.json (config) the config only
22
- * carries the doc path. `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in
23
- * the DI design.
27
+ * Loads + validates a repo's review checklists from EITHER shape (see {@link ChecklistSource}): the
28
+ * `pr-gate.checklists` array in webpieces.config.json (primary) or the `<!-- webpieces:checklists -->`
29
+ * manifest embedded in the doc that `pr-gate.checklists.doc` points at (legacy).
30
+ *
31
+ * `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in the DI design.
24
32
  */
25
33
  let ChecklistManifestService = class ChecklistManifestService {
26
34
  // Absolute path of the manifest doc under repoRoot.
27
35
  manifestDocPath(repoRoot, docRel) {
28
36
  return path.join(repoRoot, docRel);
29
37
  }
30
- // The parsed checklists, or [] when the doc is missing / has no manifest block / is malformed. Callers
31
- // that need to REPORT those problems use validate(); this one is the tolerant runtime read.
32
- load(repoRoot, docRel) {
33
- if (docRel.trim() === '')
38
+ /**
39
+ * The repo's checklists, or [] when there are none / the manifest doc is missing / malformed. Callers
40
+ * that need to REPORT those problems use validate(); this one is the tolerant runtime read.
41
+ */
42
+ load(repoRoot, source) {
43
+ if (source.inline.length > 0)
44
+ return this.usable(source.inline);
45
+ if (source.doc.trim() === '')
46
+ return [];
47
+ const items = this.readItems(this.manifestDocPath(repoRoot, source.doc));
48
+ if (items === null)
34
49
  return [];
35
- const items = this.readItems(this.manifestDocPath(repoRoot, docRel));
36
- return items === null ? [] : items.map(checklist_config_1.toChecklist).filter((d) => d.subagent !== '');
50
+ return this.usable(items.map((raw) => (0, checklist_config_1.toChecklist)(raw, path.posix.dirname(source.doc))));
37
51
  }
38
- // Human-readable errors for the manifest doc, or [] when it is valid. Never throws.
39
- validate(repoRoot, docRel) {
40
- if (docRel.trim() === '')
52
+ // Human-readable errors for the repo's checklist config, or [] when valid. Never throws.
53
+ validate(repoRoot, source) {
54
+ if (source.inline.length > 0)
55
+ return this.validateResolved(source.inline, repoRoot, source.describe());
56
+ if (source.doc.trim() === '')
41
57
  return [];
58
+ const docRel = source.doc;
42
59
  const docPath = this.manifestDocPath(repoRoot, docRel);
43
60
  if (!fs.existsSync(docPath)) {
44
61
  return [`[pr-gate] checklists.doc "${docRel}" does not exist — it must be a markdown doc carrying a <!-- webpieces:checklists [...] --> manifest.`];
@@ -53,30 +70,73 @@ let ChecklistManifestService = class ChecklistManifestService {
53
70
  }
54
71
  return this.validateItems(parsed, repoRoot, docRel);
55
72
  }
73
+ // Validate raw manifest entries (legacy shape): narrow each to a ChecklistDefinition with its doc
74
+ // resolved against the manifest doc's directory, then run the SAME checks the array form gets.
56
75
  validateItems(items, repoRoot, docRel) {
57
76
  const errors = [];
58
- const seen = new Set();
59
- const docDir = path.dirname(this.manifestDocPath(repoRoot, docRel));
77
+ const docBase = path.posix.dirname(docRel);
60
78
  items.forEach((item, i) => {
61
79
  const label = typeof item.subagent === 'string' && item.subagent !== '' ? `"${item.subagent}"` : `checklists[${i}]`;
62
- if (typeof item.subagent !== 'string' || item.subagent.trim() === '') {
63
- errors.push(`[pr-gate] ${docRel} checklists[${i}].subagent must be a non-empty string (the reviewer agent name, matching .claude/agents/<subagent>.md).`);
80
+ if (item.patterns !== undefined && !this.isStringArray(item.patterns)) {
81
+ errors.push(`[pr-gate] ${docRel} ${label}.patterns must be a string[] of path globs (omit or [] to run on every PR).`);
64
82
  }
65
- else if (seen.has(item.subagent)) {
66
- errors.push(`[pr-gate] ${docRel} duplicate subagent "${item.subagent}" each checklist must use a DISTINCT reviewer subagent (that is how independent review is enforced).`);
83
+ });
84
+ const defs = items.map((item) => (0, checklist_config_1.toChecklist)(item, docBase));
85
+ return [...errors, ...this.validateResolved(defs, repoRoot, docRel)];
86
+ }
87
+ /**
88
+ * The checks that are the SAME for both config shapes, run against already-narrowed definitions:
89
+ * subagent present + distinct + actually resolves to a spawnable agent, and the guidance doc exists.
90
+ */
91
+ validateResolved(defs, repoRoot, sourceLabel) {
92
+ const errors = [];
93
+ const seen = new Set();
94
+ // Only enforce the reviewer-agent file when this repo HAS an agents dir — a non-Claude-Code consumer
95
+ // that drives the gate some other way must not be broken by a check for a directory it never has.
96
+ const agentsDir = path.join(repoRoot, AGENTS_DIR);
97
+ const checkAgents = fs.existsSync(agentsDir);
98
+ defs.forEach((def, i) => {
99
+ const label = def.subagent !== '' ? `"${def.subagent}"` : `checklists[${i}]`;
100
+ if (def.subagent.trim() === '') {
101
+ errors.push(`[pr-gate] ${sourceLabel} checklists[${i}].subagent must be a non-empty string (the reviewer agent name, matching .claude/agents/<subagent>.md).`);
67
102
  }
68
- else {
69
- seen.add(item.subagent);
103
+ else if (seen.has(def.subagent)) {
104
+ errors.push(`[pr-gate] ${sourceLabel} duplicate subagent "${def.subagent}" — each checklist must use a DISTINCT reviewer subagent (that is how independent review is enforced).`);
70
105
  }
71
- if (item.doc !== undefined && item.doc !== '' && !fs.existsSync(path.join(docDir, item.doc))) {
72
- errors.push(`[pr-gate] ${docRel} ${label}.doc "${item.doc}" does not exist (resolved relative to the manifest doc).`);
106
+ else {
107
+ seen.add(def.subagent);
108
+ errors.push(...this.validateSubagentExists(def.subagent, label, checkAgents, agentsDir, sourceLabel));
73
109
  }
74
- if (item.patterns !== undefined && !this.isStringArray(item.patterns)) {
75
- errors.push(`[pr-gate] ${docRel} ${label}.patterns must be a string[] of path globs (omit or [] to run on every PR).`);
110
+ if (def.doc !== '' && !fs.existsSync(path.join(repoRoot, def.doc))) {
111
+ errors.push(`[pr-gate] ${sourceLabel} ${label}.doc "${def.doc}" does not exist (resolved repo-relative).`);
76
112
  }
77
113
  });
78
114
  return errors;
79
115
  }
116
+ /**
117
+ * The check this validator was missing: `subagent` is the ONE required field and the whole
118
+ * distinct-reviewer guarantee rests on it, yet nothing confirmed it names a real agent. A typo used to
119
+ * validate clean, then get printed to the coding agent as "spawn this" — and since wp-finish blocks on
120
+ * `review-<that-typo>.json`, the path of least resistance became writing the reviewer's verdict itself,
121
+ * which is the exact self-certification the distinct-subagent rule exists to prevent. Reject at load.
122
+ */
123
+ // eslint-disable-next-line @typescript-eslint/max-params
124
+ validateSubagentExists(subagent, label, checkAgents, agentsDir, sourceLabel) {
125
+ if (!checkAgents)
126
+ return [];
127
+ if (fs.existsSync(path.join(agentsDir, `${subagent}.md`)))
128
+ return [];
129
+ return [
130
+ `[pr-gate] ${sourceLabel} ${label}.subagent names no reviewer — ${AGENTS_DIR}/${subagent}.md does not exist, ` +
131
+ `so nothing can spawn it and wp-finish-upsert-pr would block forever on a review-${subagent}.json that no reviewer can write. ` +
132
+ `Create that agent file or fix the name.`,
133
+ ];
134
+ }
135
+ // Drop entries with no subagent — they have no id, so they can key neither review-<id>.json nor a
136
+ // dashboard row. validate() reports them; the tolerant load() just skips them.
137
+ usable(defs) {
138
+ return defs.filter((d) => d.subagent !== '');
139
+ }
80
140
  // Read the manifest doc and return its parsed items, or null on any problem (missing/no block/bad JSON).
81
141
  readItems(docPath) {
82
142
  if (!fs.existsSync(docPath))