@webpieces/rules-config 0.4.597 → 0.4.598

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.597",
3
+ "version": "0.4.598",
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",
@@ -17,7 +17,7 @@ exports.VERDICT_GREEN = 'green';
17
17
  exports.VERDICT_YELLOW = 'yellow';
18
18
  exports.VERDICT_RED = 'red';
19
19
  exports.VERDICT_STATUSES = [exports.VERDICT_GREEN, exports.VERDICT_YELLOW, exports.VERDICT_RED];
20
- // The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per
20
+ // The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<featureSlug>/review-<id>.json`, one per
21
21
  // matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared
22
22
  // file. It records the OUTCOME:
23
23
  // status:'green' → PASS
@@ -156,7 +156,7 @@ class ChecklistVerdict {
156
156
  }
157
157
  }
158
158
  exports.ChecklistVerdict = ChecklistVerdict;
159
- // The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`
159
+ // The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<featureSlug>/pr-context.json`
160
160
  // so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then
161
161
  // reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match
162
162
  // coarsely by path (in the config) while the subagent makes the fine, content-level judgment. Data-only.
@@ -1 +1 @@
1
- {"version":3,"file":"review-json-data.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA-ONLY classes and status constants of the PR review system — the verdict a reviewer writes, the\n * checklist that demanded it, the resolved outcome, and the diff context handed to reviewers.\n *\n * Split out of review-json.ts, which holds the SERVICE that reads and writes them. The split is purely by\n * kind (data vs behaviour): review-json.ts re-exports every name below, so `from './review-json'` and\n * `from '@webpieces/rules-config'` keep resolving exactly as before and no consumer changes.\n */\n\n// The three colors a reviewer subagent may report in `review-<id>.json`. A TRI-state, not a boolean,\n// because the boolean it replaced gave a reviewer no way to say \"this passes, but a human should look at\n// X\" — the only way to raise a concern was to FAIL the PR and then override your own failure, which reads\n// on the dashboard as a deliberately-accepted defect rather than as a note.\nexport const VERDICT_GREEN = 'green';\nexport const VERDICT_YELLOW = 'yellow';\nexport const VERDICT_RED = 'red';\nexport const VERDICT_STATUSES = [VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED] as const;\n\n// The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per\n// matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared\n// file. It records the OUTCOME:\n// status:'green' → PASS\n// status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)\n// status:'red' + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// status:'red' + no override → FAIL (refuse; `output` is printed verbatim)\n// `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated\n// in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).\nexport class ChecklistResult {\n id: string;\n status: string; // one of VERDICT_STATUSES; anything else is reported via `problem`\n output: string; // what the reviewer found; printed verbatim when the checklist fails\n override: string; // '' = no override; non-empty = ship-anyway justification (renders 🟠 overridden)\n // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ\n // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the\n // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a\n // legacy file is never silently mistaken for a missing one.\n problem: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, status: string, output: string, override: string, problem = '') {\n this.id = id;\n this.status = status;\n this.output = output;\n this.override = override;\n this.problem = problem;\n }\n}\n\n/**\n * What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the\n * diff, so its reviewer subagent is in scope). Drives review-<id>.json enforcement, provenance, the schema\n * hint, and the dashboard. Data-only.\n *\n * NAME NOTE: \"Required\" here means MATCHED, not mandatory — it predates `required` by a long way and is\n * the shared shape across review-json, the detector, the briefing builder, provenance and the dashboard.\n * Whether the reviewer must actually run is the {@link RequiredChecklist.required} field below.\n */\nexport class RequiredChecklist {\n id: string; // = subagent name; keys review-<id>.json\n subagent: string; // reviewer agent that must run (agentType the harness stamps)\n doc: string; // REPO-RELATIVE guidance doc the reviewer reads ('' → it just reads the diff)\n matchedFiles: string[]; // the changed files that matched it (for the dashboard + hint)\n // Which of the checklist's OWN globs actually fired. Printed so a reviewer can judge how coarse the\n // match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the\n // template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).\n matchedPatterns: string[];\n // Straight from the checklist's config `required`. true = blocking; false = the human is offered it and\n // may decline. Carried on the MATCH rather than looked up from config downstream so the set that gates\n // and the set that is reported cannot disagree about which of the two a checklist is.\n required: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns: string[] = [],\n // Defaulted to the BLOCKING value so any construction that forgets it fails closed — a test or a\n // future call site that silently produced an optional checklist would be a hole in the gate.\n required = true,\n ) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.matchedFiles = matchedFiles;\n this.matchedPatterns = matchedPatterns;\n this.required = required;\n }\n}\n\n/**\n * The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base\n * sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in\n * a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand\n * them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.\n */\nexport class ChecklistReviewContext {\n baseSha: string; // the 3-point merge-base sha\n prContextPath: string; // path of pr-context.json — the AUTHORITATIVE full changed-file set\n /**\n * The exact command that reproduces ONE file's diff, with a `-- <file>` tail — NOT assembled by the\n * caller. This used to be hardcoded as `git diff <baseSha> HEAD -- <file>`, which returns NOTHING on a\n * dirty tree because the changed-file set is computed base→working-tree. See DiffBasis, which derives\n * this string from the same range the file set came from.\n */\n fileDiffCommand: string;\n diffDir: string; // dir of the MATERIALIZED diff (diff/ALL.diff + diff/files/…); '' when not written\n dirty: boolean; // true ⇒ the range includes uncommitted + untracked work, and must be said out loud\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(baseSha = '', prContextPath = '', fileDiffCommand = '', diffDir = '', dirty = false) {\n this.baseSha = baseSha;\n this.prContextPath = prContextPath;\n this.fileDiffCommand = fileDiffCommand;\n this.diffDir = diffDir;\n this.dirty = dirty;\n }\n}\n\n// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist\n// review-<id>.json files. Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n results: ChecklistResult[]; // resolved per-checklist verdicts (from review-<id>.json); [] when none\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n results: ChecklistResult[] = [],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n this.results = results;\n }\n}\n\n// A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.\n// PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.\nexport const CK_PASS = 'pass'; // review-<id>.json status:'green'\nexport const CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠\nexport const CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\nexport const CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT\n detail: string; // reviewer output / override justification / format complaint (dashboard + errors)\n\n constructor(id: string, status: string, detail: string) {\n this.id = id;\n this.status = status;\n this.detail = detail;\n }\n}\n\n// The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`\n// so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then\n// reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match\n// coarsely by path (in the config) while the subagent makes the fine, content-level judgment. Data-only.\nexport class PrContext {\n base: string; // the 3-point merge-base sha the gate diffs against\n /**\n * The real HEAD sha. This was once the literal string 'HEAD', which is not a fact — it cannot be\n * compared later to detect that the tree moved under a review, and it reads as a range that was never\n * actually diffed. Its only reader (reviewContextFor) takes `base`, so recording the sha is free.\n */\n head: string;\n changedFiles: string[]; // every file changed in the range (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n dirty: boolean; // true ⇒ changedFiles includes uncommitted + untracked work\n dirtyFiles: string[]; // exactly which paths are uncommitted/untracked — why `dirty` is true\n diffCommand: string; // the command that reproduces the WHOLE diff (see DiffBasis; correct when dirty)\n diffDir: string; // dir holding the materialized per-file diffs + ALL.diff; '' when not materialized\n generatedAt: string; // ISO timestamp, so a stale context is detectable rather than silently trusted\n /**\n * Main's head as this clone last saw it — the THIRD hash point, matching the trio the 3-point merge\n * records in `merge-info/<branch>/updatemain-hashes.json`. `base`/`head` above are points A and B\n * under the review side's older names.\n *\n * The review side used to record only A and B, so nothing could answer \"did main move while this was\n * under review?\" — the question you most want answered when a review looks stale. '' when origin/main\n * is unresolvable. Purely informational; nothing gates on it.\n */\n hashMainHead: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, changedFiles: string[],\n dirty = false, dirtyFiles: string[] = [], diffCommand = '', diffDir = '', generatedAt = '',\n hashMainHead = '',\n ) {\n this.hashMainHead = hashMainHead;\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n this.dirty = dirty;\n this.dirtyFiles = dirtyFiles;\n this.diffCommand = diffCommand;\n this.diffDir = diffDir;\n this.generatedAt = generatedAt;\n }\n}\n"]}
1
+ {"version":3,"file":"review-json-data.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA-ONLY classes and status constants of the PR review system — the verdict a reviewer writes, the\n * checklist that demanded it, the resolved outcome, and the diff context handed to reviewers.\n *\n * Split out of review-json.ts, which holds the SERVICE that reads and writes them. The split is purely by\n * kind (data vs behaviour): review-json.ts re-exports every name below, so `from './review-json'` and\n * `from '@webpieces/rules-config'` keep resolving exactly as before and no consumer changes.\n */\n\n// The three colors a reviewer subagent may report in `review-<id>.json`. A TRI-state, not a boolean,\n// because the boolean it replaced gave a reviewer no way to say \"this passes, but a human should look at\n// X\" — the only way to raise a concern was to FAIL the PR and then override your own failure, which reads\n// on the dashboard as a deliberately-accepted defect rather than as a note.\nexport const VERDICT_GREEN = 'green';\nexport const VERDICT_YELLOW = 'yellow';\nexport const VERDICT_RED = 'red';\nexport const VERDICT_STATUSES = [VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED] as const;\n\n// The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<featureSlug>/review-<id>.json`, one per\n// matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared\n// file. It records the OUTCOME:\n// status:'green' → PASS\n// status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)\n// status:'red' + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// status:'red' + no override → FAIL (refuse; `output` is printed verbatim)\n// `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated\n// in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).\nexport class ChecklistResult {\n id: string;\n status: string; // one of VERDICT_STATUSES; anything else is reported via `problem`\n output: string; // what the reviewer found; printed verbatim when the checklist fails\n override: string; // '' = no override; non-empty = ship-anyway justification (renders 🟠 overridden)\n // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ\n // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the\n // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a\n // legacy file is never silently mistaken for a missing one.\n problem: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, status: string, output: string, override: string, problem = '') {\n this.id = id;\n this.status = status;\n this.output = output;\n this.override = override;\n this.problem = problem;\n }\n}\n\n/**\n * What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the\n * diff, so its reviewer subagent is in scope). Drives review-<id>.json enforcement, provenance, the schema\n * hint, and the dashboard. Data-only.\n *\n * NAME NOTE: \"Required\" here means MATCHED, not mandatory — it predates `required` by a long way and is\n * the shared shape across review-json, the detector, the briefing builder, provenance and the dashboard.\n * Whether the reviewer must actually run is the {@link RequiredChecklist.required} field below.\n */\nexport class RequiredChecklist {\n id: string; // = subagent name; keys review-<id>.json\n subagent: string; // reviewer agent that must run (agentType the harness stamps)\n doc: string; // REPO-RELATIVE guidance doc the reviewer reads ('' → it just reads the diff)\n matchedFiles: string[]; // the changed files that matched it (for the dashboard + hint)\n // Which of the checklist's OWN globs actually fired. Printed so a reviewer can judge how coarse the\n // match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the\n // template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).\n matchedPatterns: string[];\n // Straight from the checklist's config `required`. true = blocking; false = the human is offered it and\n // may decline. Carried on the MATCH rather than looked up from config downstream so the set that gates\n // and the set that is reported cannot disagree about which of the two a checklist is.\n required: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns: string[] = [],\n // Defaulted to the BLOCKING value so any construction that forgets it fails closed — a test or a\n // future call site that silently produced an optional checklist would be a hole in the gate.\n required = true,\n ) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.matchedFiles = matchedFiles;\n this.matchedPatterns = matchedPatterns;\n this.required = required;\n }\n}\n\n/**\n * The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base\n * sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in\n * a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand\n * them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.\n */\nexport class ChecklistReviewContext {\n baseSha: string; // the 3-point merge-base sha\n prContextPath: string; // path of pr-context.json — the AUTHORITATIVE full changed-file set\n /**\n * The exact command that reproduces ONE file's diff, with a `-- <file>` tail — NOT assembled by the\n * caller. This used to be hardcoded as `git diff <baseSha> HEAD -- <file>`, which returns NOTHING on a\n * dirty tree because the changed-file set is computed base→working-tree. See DiffBasis, which derives\n * this string from the same range the file set came from.\n */\n fileDiffCommand: string;\n diffDir: string; // dir of the MATERIALIZED diff (diff/ALL.diff + diff/files/…); '' when not written\n dirty: boolean; // true ⇒ the range includes uncommitted + untracked work, and must be said out loud\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(baseSha = '', prContextPath = '', fileDiffCommand = '', diffDir = '', dirty = false) {\n this.baseSha = baseSha;\n this.prContextPath = prContextPath;\n this.fileDiffCommand = fileDiffCommand;\n this.diffDir = diffDir;\n this.dirty = dirty;\n }\n}\n\n// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist\n// review-<id>.json files. Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n results: ChecklistResult[]; // resolved per-checklist verdicts (from review-<id>.json); [] when none\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n results: ChecklistResult[] = [],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n this.results = results;\n }\n}\n\n// A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.\n// PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.\nexport const CK_PASS = 'pass'; // review-<id>.json status:'green'\nexport const CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠\nexport const CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\nexport const CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT\n detail: string; // reviewer output / override justification / format complaint (dashboard + errors)\n\n constructor(id: string, status: string, detail: string) {\n this.id = id;\n this.status = status;\n this.detail = detail;\n }\n}\n\n// The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<featureSlug>/pr-context.json`\n// so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then\n// reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match\n// coarsely by path (in the config) while the subagent makes the fine, content-level judgment. Data-only.\nexport class PrContext {\n base: string; // the 3-point merge-base sha the gate diffs against\n /**\n * The real HEAD sha. This was once the literal string 'HEAD', which is not a fact — it cannot be\n * compared later to detect that the tree moved under a review, and it reads as a range that was never\n * actually diffed. Its only reader (reviewContextFor) takes `base`, so recording the sha is free.\n */\n head: string;\n changedFiles: string[]; // every file changed in the range (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n dirty: boolean; // true ⇒ changedFiles includes uncommitted + untracked work\n dirtyFiles: string[]; // exactly which paths are uncommitted/untracked — why `dirty` is true\n diffCommand: string; // the command that reproduces the WHOLE diff (see DiffBasis; correct when dirty)\n diffDir: string; // dir holding the materialized per-file diffs + ALL.diff; '' when not materialized\n generatedAt: string; // ISO timestamp, so a stale context is detectable rather than silently trusted\n /**\n * Main's head as this clone last saw it — the THIRD hash point, matching the trio the 3-point merge\n * records in `merge-info/<branch>/updatemain-hashes.json`. `base`/`head` above are points A and B\n * under the review side's older names.\n *\n * The review side used to record only A and B, so nothing could answer \"did main move while this was\n * under review?\" — the question you most want answered when a review looks stale. '' when origin/main\n * is unresolvable. Purely informational; nothing gates on it.\n */\n hashMainHead: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, changedFiles: string[],\n dirty = false, dirtyFiles: string[] = [], diffCommand = '', diffDir = '', generatedAt = '',\n hashMainHead = '',\n ) {\n this.hashMainHead = hashMainHead;\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n this.dirty = dirty;\n this.dirtyFiles = dirtyFiles;\n this.diffCommand = diffCommand;\n this.diffDir = diffDir;\n this.generatedAt = generatedAt;\n }\n}\n"]}
@@ -56,12 +56,23 @@ export declare class OfferedContext {
56
56
  */
57
57
  export declare class ProvenanceWriteRequest {
58
58
  prDir: string;
59
- branch: string;
59
+ /**
60
+ * The dash-sanitized FEATURE SLUG, not a git branch — `AiBranchName.getFeatureName()`, which is
61
+ * `baseBranchName(branch).replace(/\//g, '-')`. It names the on-disk `pr-review/<slug>/` dir.
62
+ *
63
+ * It was called `branch`, and that mislabel cost a real investigation: a bug report against the
64
+ * provenance gate reasoned from `provenance.json`'s dash-form `branch` that `sidechainOnBranch` must
65
+ * be comparing a dash-form name against a slash-form git branch, and went looking for a missing
66
+ * normalization. There is none — both sides of that comparison are slash-form
67
+ * (`git branch --show-current`) and this field never reaches it. A field called `branch` that never
68
+ * holds a branch is what sent the reader down that path; the name is now what it holds.
69
+ */
70
+ featureSlug: string;
60
71
  headSha: string;
61
72
  provenanceStatus: string;
62
73
  offered: OfferedContext;
63
74
  reviewers: ReviewerTranscript[];
64
- constructor(prDir: string, branch: string, headSha: string, provenanceStatus: string);
75
+ constructor(prDir: string, featureSlug: string, headSha: string, provenanceStatus: string);
65
76
  }
66
77
  /**
67
78
  * The written record. Field names are the JSON keys, in this order — `_WHAT_THIS_IS` is declared FIRST so
@@ -72,7 +83,7 @@ export declare class ReviewProvenance {
72
83
  _WHAT_THIS_IS: string;
73
84
  sessionId: string;
74
85
  mainTranscript: string;
75
- branch: string;
86
+ featureSlug: string;
76
87
  headSha: string;
77
88
  stampedAt: string;
78
89
  transcriptRetentionDays: number;
@@ -97,15 +97,26 @@ exports.OfferedContext = OfferedContext;
97
97
  */
98
98
  class ProvenanceWriteRequest {
99
99
  prDir;
100
- branch;
100
+ /**
101
+ * The dash-sanitized FEATURE SLUG, not a git branch — `AiBranchName.getFeatureName()`, which is
102
+ * `baseBranchName(branch).replace(/\//g, '-')`. It names the on-disk `pr-review/<slug>/` dir.
103
+ *
104
+ * It was called `branch`, and that mislabel cost a real investigation: a bug report against the
105
+ * provenance gate reasoned from `provenance.json`'s dash-form `branch` that `sidechainOnBranch` must
106
+ * be comparing a dash-form name against a slash-form git branch, and went looking for a missing
107
+ * normalization. There is none — both sides of that comparison are slash-form
108
+ * (`git branch --show-current`) and this field never reaches it. A field called `branch` that never
109
+ * holds a branch is what sent the reader down that path; the name is now what it holds.
110
+ */
111
+ featureSlug;
101
112
  headSha;
102
113
  provenanceStatus; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED
103
114
  offered = new OfferedContext('', '');
104
115
  reviewers = [];
105
116
  // eslint-disable-next-line @typescript-eslint/max-params
106
- constructor(prDir, branch, headSha, provenanceStatus) {
117
+ constructor(prDir, featureSlug, headSha, provenanceStatus) {
107
118
  this.prDir = prDir;
108
- this.branch = branch;
119
+ this.featureSlug = featureSlug;
109
120
  this.headSha = headSha;
110
121
  this.provenanceStatus = provenanceStatus;
111
122
  }
@@ -121,7 +132,9 @@ class ReviewProvenance {
121
132
  _WHAT_THIS_IS = WHAT_THIS_IS;
122
133
  sessionId;
123
134
  mainTranscript;
124
- branch;
135
+ // The JSON key too: a reader of provenance.json sees `featureSlug`, matching the `pr-review/<slug>/`
136
+ // dir it sits in. See ProvenanceWriteRequest.featureSlug for why the old `branch` key was a defect.
137
+ featureSlug;
125
138
  headSha;
126
139
  stampedAt;
127
140
  transcriptRetentionDays;
@@ -132,7 +145,7 @@ class ReviewProvenance {
132
145
  constructor(request) {
133
146
  this.sessionId = '';
134
147
  this.mainTranscript = '';
135
- this.branch = request.branch;
148
+ this.featureSlug = request.featureSlug;
136
149
  this.headSha = request.headSha;
137
150
  this.stampedAt = '';
138
151
  this.transcriptRetentionDays = exports.DEFAULT_RETENTION_DAYS;
@@ -1 +1 @@
1
- {"version":3,"file":"review-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-provenance.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyB;AACzB,yCAA2D;AAC3D,yCAAqC;AAGrC,4GAA4G;AAC5G,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAC1C,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAElD;;;;GAIG;AACU,QAAA,sBAAsB,GAAG,EAAE,CAAC;AAEzC,MAAM,YAAY,GACd,uGAAuG;IACvG,yGAAyG;IACzG,qGAAqG;IACrG,yGAAyG;IACzG,yGAAyG;IACzG,kFAAkF,CAAC;AAEvF,sFAAsF;AACtF,MAAa,aAAa;IACtB,WAAW,CAAS,CAAO,uDAAuD;IAClF,gBAAgB,CAAS,CAAE,2CAA2C;IACtE,OAAO,CAAS,CAAW,kEAAkE;IAE7F,YAAY,WAAmB,EAAE,gBAAwB,EAAE,OAAe;QACtE,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QACzC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAVD,sCAUC;AAED;;;;;;;GAOG;AACH,MAAa,kBAAkB;IAC3B,EAAE,CAAS;IACX,SAAS,CAAS;IAClB,OAAO,CAAS;IAChB,UAAU,CAAS,CAAO,sEAAsE;IAChG,gBAAgB,CAAU;IAC1B,WAAW,CAAS;IACpB,gBAAgB,CAAS;IACzB,OAAO,CAAS;IAChB,QAAQ,CAAU;IAClB,OAAO,CAAU;IACjB,aAAa,CAAS;IACtB,eAAe,CAAS;IACxB;;;;;;OAMG;IACH,MAAM,CAAW;IAEjB,YAAY,QAA0B,EAAE,KAAoB;QACxD,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,kEAAkE;QAChG,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,cAAc,CAAC;QAC1C,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC,cAAc,KAAK,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;QACjG,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,CAAC;QAC/C,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAClC,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;QAC5C,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;QAChD,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAClC,CAAC;CACJ;AArCD,gDAqCC;AAED,uFAAuF;AACvF,MAAa,cAAc;IACvB,OAAO,CAAS;IAChB,eAAe,CAAS;IAExB,YAAY,OAAe,EAAE,eAAuB;QAChD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAC3C,CAAC;CACJ;AARD,wCAQC;AAED;;;;GAIG;AACH,MAAa,sBAAsB;IAC/B,KAAK,CAAS;IACd,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,gBAAgB,CAAS,CAAC,0DAA0D;IACpF,OAAO,GAAmB,IAAI,cAAc,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACrD,SAAS,GAAyB,EAAE,CAAC;IAErC,yDAAyD;IACzD,YAAY,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,gBAAwB;QAChF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IAC7C,CAAC;CACJ;AAfD,wDAeC;AAED;;;;GAIG;AACH,MAAa,gBAAgB;IACzB,yGAAyG;IACzG,aAAa,GAAG,YAAY,CAAC;IAC7B,SAAS,CAAS;IAClB,cAAc,CAAS;IACvB,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,SAAS,CAAS;IAClB,uBAAuB,CAAS;IAChC,mBAAmB,CAAS;IAC5B,gBAAgB,CAAS;IACzB,OAAO,CAAiB;IACxB,SAAS,CAAuB;IAEhC,YAAY,OAA+B;QACvC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,cAAc,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,uBAAuB,GAAG,8BAAsB,CAAC;QACtD,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAC;QAC9B,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACjD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACvC,CAAC;CACJ;AA1BD,4CA0BC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAChC,iEAAiE;IACjE,cAAc,CAAC,KAAa;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAC7C,CAAC;IAED,mGAAmG;IACnG,+FAA+F;IAC/F,iBAAiB,CAAC,KAAa;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;IACjD,CAAC;IAED,iGAAiG;IACjG,SAAS;QACL,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAChE,CAAC;IAED;;;;;OAKG;IACH,cAAc;QACV,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAChE,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,CAAC,CAAC;YAChE,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QACnD,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,aAAa;QACT,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;QAC/C,KAAK,MAAM,IAAI,IAAI,CAAC,eAAe,EAAE,qBAAqB,CAAC,EAAE,CAAC;YAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;YACrD,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC,mBAAmB,CAAC,CAAC;YAC7C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC;QACnF,CAAC;QACD,OAAO,8BAAsB,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,WAA8B,EAAE,aAAqB;QAC3D,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,GAAG,MAAM,CAAC;gBAAE,MAAM,GAAG,KAAK,CAAC;QACxE,CAAC;QACD,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,GAAG,aAAa,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACtE,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAA+B;QACjC,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAClD,6GAA6G;QAC7G,8DAA8D;QAC9D,IAAI,CAAC;YACD,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YACrE,OAAO,MAAM,CAAC;QAClB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;IACL,CAAC;IAED,+FAA+F;IAC/F,sGAAsG;IACtG,uGAAuG;IACvG,OAAO,CAAC,KAAa;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,OAAO,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC7C,sGAAsG;QACtG,8DAA8D;QAC9D,IAAI,CAAC;YACD,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAChC,OAAO,MAAM,CAAC;QAClB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;IACL,CAAC;IAED,sGAAsG;IAC9F,KAAK,CAAC,OAA+B;QACzC,MAAM,UAAU,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACjD,UAAU,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACxC,UAAU,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QAClD,UAAU,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAChD,UAAU,CAAC,uBAAuB,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,CAAC,UAAU,CAAC,cAAc,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,GAAG,CAC/D,CAAC,CAAqB,EAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QAC/F,UAAU,CAAC,mBAAmB,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,uBAAuB,CAAC,CAAC;QAC5F,OAAO,UAAU,CAAC;IACtB,CAAC;IAED,+DAA+D;IACvD,OAAO,CAAC,QAAgB;QAC5B,2GAA2G;QAC3G,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACjD,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,CAAC,CAAC;QACb,CAAC;IACL,CAAC;IAEO,OAAO,CAAC,GAAW;QACvB,qGAAqG;QACrG,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,EAAE,CAAC;QACd,CAAC;IACL,CAAC;IAED,yFAAyF;IACjF,QAAQ,CAAC,QAAgB;QAC7B,mHAAmH;QACnH,8DAA8D;QAC9D,IAAI,CAAC;YACD,yFAAyF;YACzF,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA4B,CAAC;QACpF,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;YACX,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;CACJ,CAAA;AA9JY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,uBAAuB,CA8JnC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\nimport { ReviewerEvidence } from './subagent-provenance';\n\n// The audit record `wp-finish-upsert-pr` writes beside review.json, and where a consumed one is retired to.\nconst PROVENANCE_FILE = 'provenance.json';\nconst OLD_PROVENANCE_FILE = 'old-provenance.json';\n\n/**\n * Claude Code deletes transcripts after `cleanupPeriodDays`; 30 is its default and what applies when the\n * setting is absent, which is the common case. Recorded in every provenance file so a reader knows how\n * long the links it is holding remain resolvable rather than discovering it by following a dead path.\n */\nexport const DEFAULT_RETENTION_DAYS = 30;\n\nconst WHAT_THIS_IS =\n 'AUDIT RECORD — written by wp-finish-upsert-pr, never by an AI. It links each reviewer verdict to the ' +\n 'transcript of the subagent that produced it, and records what that reviewer was OFFERED versus what it ' +\n 'demonstrably READ. Open it to audit the review process itself: whether a verdict was written by an ' +\n 'agent that actually opened the diff and its checklist doc. Do not hand-edit it, and do not treat it as ' +\n 'a review — it says nothing about whether the code is good, only about how it was looked at. The linked ' +\n 'transcripts expire (see transcriptsExpireOn); the counters recorded here do not.';\n\n/** Where ONE reviewer's inputs and output live on disk. Data-only (per CLAUDE.md). */\nexport class ReviewerPaths {\n verdictFile: string; // the review-<id>.json this reviewer was told to write\n instructionsFile: string; // its generated <subagent>.instructions.md\n docPath: string; // its checklist's guidance doc ('' when the checklist names none)\n\n constructor(verdictFile: string, instructionsFile: string, docPath: string) {\n this.verdictFile = verdictFile;\n this.instructionsFile = instructionsFile;\n this.docPath = docPath;\n }\n}\n\n/**\n * ONE reviewer's provenance row: which agent ran, where its transcript is, and the evidence counters read\n * out of that transcript. Data-only, and its FIELD NAMES ARE THE JSON KEYS — the file is written by\n * serializing these objects directly, so renaming a field renames it in every consumer's audit record.\n *\n * The counters are copied here rather than left to be re-derived because the transcript they came from is a\n * wasting asset (~30 days). After it expires this row still answers \"did that reviewer read the diff?\".\n */\nexport class ReviewerTranscript {\n id: string;\n agentType: string;\n agentId: string;\n transcript: string; // absolute path to agent-<id>.jsonl, '' when it could not be resolved\n transcriptExists: boolean;\n verdictFile: string;\n instructionsFile: string;\n docPath: string;\n readDiff: boolean;\n readDoc: boolean;\n toolCallCount: number;\n offRepoSearches: number;\n /**\n * The model(s) that ACTUALLY served this reviewer, observed in its transcript — not the `model:` its\n * agent file requested, which can silently disagree, and not anything the reviewer claimed.\n *\n * Copied here for the reason the whole class exists: the transcript is a wasting asset (~30 days),\n * and after it expires this row is the only surviving answer to \"which model reviewed this?\".\n */\n models: string[];\n\n constructor(evidence: ReviewerEvidence, paths: ReviewerPaths) {\n this.id = evidence.agentType; // a checklist's id IS its subagent name (see ChecklistDefinition)\n this.agentType = evidence.agentType;\n this.agentId = evidence.agentId;\n this.transcript = evidence.transcriptPath;\n this.transcriptExists = evidence.transcriptPath !== '' && fs.existsSync(evidence.transcriptPath);\n this.verdictFile = paths.verdictFile;\n this.instructionsFile = paths.instructionsFile;\n this.docPath = paths.docPath;\n this.readDiff = evidence.readDiff;\n this.readDoc = evidence.readDoc;\n this.toolCallCount = evidence.toolCallCount;\n this.offRepoSearches = evidence.offRepoSearches;\n this.models = evidence.models;\n }\n}\n\n/** What the reviewers were handed, whether or not any of them opened it. Data-only. */\nexport class OfferedContext {\n diffDir: string;\n instructionsDir: string;\n\n constructor(diffDir: string, instructionsDir: string) {\n this.diffDir = diffDir;\n this.instructionsDir = instructionsDir;\n }\n}\n\n/**\n * What {@link ReviewProvenanceService.write} is asked to record. Data-only; `offered` and `reviewers` are\n * assigned after construction (the same shape ChecklistCommentRow uses) so this never becomes a 7-param\n * constructor.\n */\nexport class ProvenanceWriteRequest {\n prDir: string;\n branch: string;\n headSha: string;\n provenanceStatus: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n offered: OfferedContext = new OfferedContext('', '');\n reviewers: ReviewerTranscript[] = [];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prDir: string, branch: string, headSha: string, provenanceStatus: string) {\n this.prDir = prDir;\n this.branch = branch;\n this.headSha = headSha;\n this.provenanceStatus = provenanceStatus;\n }\n}\n\n/**\n * The written record. Field names are the JSON keys, in this order — `_WHAT_THIS_IS` is declared FIRST so\n * anything that opens the file reads what it is before it reads anything it might act on, exactly as\n * ReviewJsonService.archiveReviewJson stamps its note first.\n */\nexport class ReviewProvenance {\n // webpieces-disable naming-convention -- the leading underscore marks a note-to-the-reader key, not data\n _WHAT_THIS_IS = WHAT_THIS_IS;\n sessionId: string;\n mainTranscript: string;\n branch: string;\n headSha: string;\n stampedAt: string;\n transcriptRetentionDays: number;\n transcriptsExpireOn: string;\n provenanceStatus: string;\n offered: OfferedContext;\n reviewers: ReviewerTranscript[];\n\n constructor(request: ProvenanceWriteRequest) {\n this.sessionId = '';\n this.mainTranscript = '';\n this.branch = request.branch;\n this.headSha = request.headSha;\n this.stampedAt = '';\n this.transcriptRetentionDays = DEFAULT_RETENTION_DAYS;\n this.transcriptsExpireOn = '';\n this.provenanceStatus = request.provenanceStatus;\n this.offered = request.offered;\n this.reviewers = request.reviewers;\n }\n}\n\n/**\n * Records WHICH transcript produced which verdict, so the review process itself can be audited later.\n *\n * Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The\n * environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported\n * link would be invented. Every path here is derived from the harness's own artifacts:\n * ~/.claude/projects/&#42;/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/&#42;/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript\n * The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and\n * adds the session-level facts (which session, how long the links live).\n *\n * Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay\n * byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.\n *\n * Best-effort throughout — an unreadable ~/.claude degrades the record to empty links, never fails a PR.\n * Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format\n * change must not wedge a consumer's PR.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewProvenanceService {\n // Where the audit record for a branch lives, beside review.json.\n provenancePath(prDir: string): string {\n return path.join(prDir, PROVENANCE_FILE);\n }\n\n // Where a consumed record is retired to — the mirror of ReviewJsonService.oldReviewJsonPath, so an\n // archived old-review.json keeps the transcript links belonging to the round that produced it.\n oldProvenancePath(prDir: string): string {\n return path.join(prDir, OLD_PROVENANCE_FILE);\n }\n\n // The current Claude Code session id, or '' outside a Claude Code session (plain terminal / CI).\n sessionId(): string {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();\n }\n\n /**\n * The main agent's own transcript: `~/.claude/projects/<cwd-slug>/<sessionId>.jsonl`. Located by\n * scanning every project dir for the file named after the session, so the cwd-slug — which is a\n * mangling of the working directory we would otherwise have to reproduce exactly — never has to be\n * derived. '' when there is no session or the file is not there.\n */\n mainTranscript(): string {\n const session = this.sessionId();\n if (session === '') return '';\n const projects = path.join(os.homedir(), '.claude', 'projects');\n for (const proj of this.readDir(projects)) {\n const candidate = path.join(projects, proj, `${session}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * How many days Claude Code keeps transcripts: `cleanupPeriodDays` from ~/.claude/settings.json (then\n * settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent, which is\n * why the default is documented rather than left implicit.\n */\n retentionDays(): number {\n const dir = path.join(os.homedir(), '.claude');\n for (const file of ['settings.json', 'settings.local.json']) {\n const settings = this.readJson(path.join(dir, file));\n const days = settings?.['cleanupPeriodDays'];\n if (typeof days === 'number' && Number.isFinite(days) && days > 0) return days;\n }\n return DEFAULT_RETENTION_DAYS;\n }\n\n /**\n * The date the FIRST of these transcripts becomes unreadable: the oldest one's mtime + retentionDays,\n * as an ISO date. The oldest rather than the newest because that is when the audit trail starts losing\n * links, and a reader planning to follow them needs the pessimistic answer. '' when none exist.\n */\n expiresOn(transcripts: readonly string[], retentionDays: number): string {\n let oldest = 0;\n for (const file of transcripts) {\n const mtime = this.mtimeOf(file);\n if (mtime !== 0 && (oldest === 0 || mtime < oldest)) oldest = mtime;\n }\n if (oldest === 0) return '';\n const expiry = new Date(oldest + retentionDays * 24 * 60 * 60 * 1000);\n return expiry.toISOString().slice(0, 10);\n }\n\n /**\n * Write the record to `<prDir>/provenance.json` and return its path ('' if it could not be written).\n *\n * Written on EVERY finish, including one that refuses for a missing reviewer: a refused round is\n * precisely the one worth auditing, and a record that only ever appears on success cannot answer \"what\n * did the reviewers do the time this was rejected?\".\n */\n write(request: ProvenanceWriteRequest): string {\n const provenance = this.build(request);\n const target = this.provenancePath(request.prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the audit record is never worth failing a PR over\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(request.prDir, { recursive: true });\n fs.writeFileSync(target, JSON.stringify(provenance, null, 2) + '\\n');\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Retire the record for the round that just shipped: copy it to old-provenance.json beside the\n // old-review.json it belongs to. A COPY, not a move — unlike review.json this file is not an input to\n // anything, so leaving it in place cannot mislead a later reviewer, and the next finish overwrites it.\n archive(prDir: string): string {\n const source = this.provenancePath(prDir);\n if (!fs.existsSync(source)) return '';\n const target = this.oldProvenancePath(prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.copyFileSync(source, target);\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Fill in the session-level facts the caller cannot know: which session, which transcripts, how long.\n private build(request: ProvenanceWriteRequest): ReviewProvenance {\n const provenance = new ReviewProvenance(request);\n provenance.sessionId = this.sessionId();\n provenance.mainTranscript = this.mainTranscript();\n provenance.stampedAt = new Date().toISOString();\n provenance.transcriptRetentionDays = this.retentionDays();\n const linked = [provenance.mainTranscript, ...request.reviewers.map(\n (r: ReviewerTranscript): string => r.transcript)].filter((p: string): boolean => p !== '');\n provenance.transcriptsExpireOn = this.expiresOn(linked, provenance.transcriptRetentionDays);\n return provenance;\n }\n\n // Epoch millis of a file's mtime, or 0 when it cannot be read.\n private mtimeOf(filePath: string): number {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unstattable transcript contributes no expiry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(filePath).mtime.getTime();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 0;\n }\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed settings → null (fall through to the default)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"review-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\nimport { ReviewerEvidence } from './subagent-provenance';\n\n// The audit record `wp-finish-upsert-pr` writes beside review.json, and where a consumed one is retired to.\nconst PROVENANCE_FILE = 'provenance.json';\nconst OLD_PROVENANCE_FILE = 'old-provenance.json';\n\n/**\n * Claude Code deletes transcripts after `cleanupPeriodDays`; 30 is its default and what applies when the\n * setting is absent, which is the common case. Recorded in every provenance file so a reader knows how\n * long the links it is holding remain resolvable rather than discovering it by following a dead path.\n */\nexport const DEFAULT_RETENTION_DAYS = 30;\n\nconst WHAT_THIS_IS =\n 'AUDIT RECORD — written by wp-finish-upsert-pr, never by an AI. It links each reviewer verdict to the ' +\n 'transcript of the subagent that produced it, and records what that reviewer was OFFERED versus what it ' +\n 'demonstrably READ. Open it to audit the review process itself: whether a verdict was written by an ' +\n 'agent that actually opened the diff and its checklist doc. Do not hand-edit it, and do not treat it as ' +\n 'a review — it says nothing about whether the code is good, only about how it was looked at. The linked ' +\n 'transcripts expire (see transcriptsExpireOn); the counters recorded here do not.';\n\n/** Where ONE reviewer's inputs and output live on disk. Data-only (per CLAUDE.md). */\nexport class ReviewerPaths {\n verdictFile: string; // the review-<id>.json this reviewer was told to write\n instructionsFile: string; // its generated <subagent>.instructions.md\n docPath: string; // its checklist's guidance doc ('' when the checklist names none)\n\n constructor(verdictFile: string, instructionsFile: string, docPath: string) {\n this.verdictFile = verdictFile;\n this.instructionsFile = instructionsFile;\n this.docPath = docPath;\n }\n}\n\n/**\n * ONE reviewer's provenance row: which agent ran, where its transcript is, and the evidence counters read\n * out of that transcript. Data-only, and its FIELD NAMES ARE THE JSON KEYS — the file is written by\n * serializing these objects directly, so renaming a field renames it in every consumer's audit record.\n *\n * The counters are copied here rather than left to be re-derived because the transcript they came from is a\n * wasting asset (~30 days). After it expires this row still answers \"did that reviewer read the diff?\".\n */\nexport class ReviewerTranscript {\n id: string;\n agentType: string;\n agentId: string;\n transcript: string; // absolute path to agent-<id>.jsonl, '' when it could not be resolved\n transcriptExists: boolean;\n verdictFile: string;\n instructionsFile: string;\n docPath: string;\n readDiff: boolean;\n readDoc: boolean;\n toolCallCount: number;\n offRepoSearches: number;\n /**\n * The model(s) that ACTUALLY served this reviewer, observed in its transcript — not the `model:` its\n * agent file requested, which can silently disagree, and not anything the reviewer claimed.\n *\n * Copied here for the reason the whole class exists: the transcript is a wasting asset (~30 days),\n * and after it expires this row is the only surviving answer to \"which model reviewed this?\".\n */\n models: string[];\n\n constructor(evidence: ReviewerEvidence, paths: ReviewerPaths) {\n this.id = evidence.agentType; // a checklist's id IS its subagent name (see ChecklistDefinition)\n this.agentType = evidence.agentType;\n this.agentId = evidence.agentId;\n this.transcript = evidence.transcriptPath;\n this.transcriptExists = evidence.transcriptPath !== '' && fs.existsSync(evidence.transcriptPath);\n this.verdictFile = paths.verdictFile;\n this.instructionsFile = paths.instructionsFile;\n this.docPath = paths.docPath;\n this.readDiff = evidence.readDiff;\n this.readDoc = evidence.readDoc;\n this.toolCallCount = evidence.toolCallCount;\n this.offRepoSearches = evidence.offRepoSearches;\n this.models = evidence.models;\n }\n}\n\n/** What the reviewers were handed, whether or not any of them opened it. Data-only. */\nexport class OfferedContext {\n diffDir: string;\n instructionsDir: string;\n\n constructor(diffDir: string, instructionsDir: string) {\n this.diffDir = diffDir;\n this.instructionsDir = instructionsDir;\n }\n}\n\n/**\n * What {@link ReviewProvenanceService.write} is asked to record. Data-only; `offered` and `reviewers` are\n * assigned after construction (the same shape ChecklistCommentRow uses) so this never becomes a 7-param\n * constructor.\n */\nexport class ProvenanceWriteRequest {\n prDir: string;\n /**\n * The dash-sanitized FEATURE SLUG, not a git branch — `AiBranchName.getFeatureName()`, which is\n * `baseBranchName(branch).replace(/\\//g, '-')`. It names the on-disk `pr-review/<slug>/` dir.\n *\n * It was called `branch`, and that mislabel cost a real investigation: a bug report against the\n * provenance gate reasoned from `provenance.json`'s dash-form `branch` that `sidechainOnBranch` must\n * be comparing a dash-form name against a slash-form git branch, and went looking for a missing\n * normalization. There is none — both sides of that comparison are slash-form\n * (`git branch --show-current`) and this field never reaches it. A field called `branch` that never\n * holds a branch is what sent the reader down that path; the name is now what it holds.\n */\n featureSlug: string;\n headSha: string;\n provenanceStatus: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n offered: OfferedContext = new OfferedContext('', '');\n reviewers: ReviewerTranscript[] = [];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(prDir: string, featureSlug: string, headSha: string, provenanceStatus: string) {\n this.prDir = prDir;\n this.featureSlug = featureSlug;\n this.headSha = headSha;\n this.provenanceStatus = provenanceStatus;\n }\n}\n\n/**\n * The written record. Field names are the JSON keys, in this order — `_WHAT_THIS_IS` is declared FIRST so\n * anything that opens the file reads what it is before it reads anything it might act on, exactly as\n * ReviewJsonService.archiveReviewJson stamps its note first.\n */\nexport class ReviewProvenance {\n // webpieces-disable naming-convention -- the leading underscore marks a note-to-the-reader key, not data\n _WHAT_THIS_IS = WHAT_THIS_IS;\n sessionId: string;\n mainTranscript: string;\n // The JSON key too: a reader of provenance.json sees `featureSlug`, matching the `pr-review/<slug>/`\n // dir it sits in. See ProvenanceWriteRequest.featureSlug for why the old `branch` key was a defect.\n featureSlug: string;\n headSha: string;\n stampedAt: string;\n transcriptRetentionDays: number;\n transcriptsExpireOn: string;\n provenanceStatus: string;\n offered: OfferedContext;\n reviewers: ReviewerTranscript[];\n\n constructor(request: ProvenanceWriteRequest) {\n this.sessionId = '';\n this.mainTranscript = '';\n this.featureSlug = request.featureSlug;\n this.headSha = request.headSha;\n this.stampedAt = '';\n this.transcriptRetentionDays = DEFAULT_RETENTION_DAYS;\n this.transcriptsExpireOn = '';\n this.provenanceStatus = request.provenanceStatus;\n this.offered = request.offered;\n this.reviewers = request.reviewers;\n }\n}\n\n/**\n * Records WHICH transcript produced which verdict, so the review process itself can be audited later.\n *\n * Why a service and not a field the AI writes: a reviewer subagent CANNOT know its own transcript path. The\n * environment exposes `CLAUDE_CODE_SESSION_ID` — the PARENT session — and no agent id, so a self-reported\n * link would be invented. Every path here is derived from the harness's own artifacts:\n * ~/.claude/projects/&#42;/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/&#42;/<sessionId>/subagents/agent-<id>.jsonl → one reviewer's transcript\n * The subagent half is already resolved by {@link SubagentProvenanceService}; this carries it to disk and\n * adds the session-level facts (which session, how long the links live).\n *\n * Deliberately a SEPARATE file from review.json / review-<id>.json: those are AI-authored and stay\n * byte-untouched, so nothing here can be confused for something a reviewer claimed about itself.\n *\n * Best-effort throughout — an unreadable ~/.claude degrades the record to empty links, never fails a PR.\n * Same reasoning as SubagentProvenanceService: this reads undocumented Claude Code internals, and a format\n * change must not wedge a consumer's PR.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewProvenanceService {\n // Where the audit record for a branch lives, beside review.json.\n provenancePath(prDir: string): string {\n return path.join(prDir, PROVENANCE_FILE);\n }\n\n // Where a consumed record is retired to — the mirror of ReviewJsonService.oldReviewJsonPath, so an\n // archived old-review.json keeps the transcript links belonging to the round that produced it.\n oldProvenancePath(prDir: string): string {\n return path.join(prDir, OLD_PROVENANCE_FILE);\n }\n\n // The current Claude Code session id, or '' outside a Claude Code session (plain terminal / CI).\n sessionId(): string {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim();\n }\n\n /**\n * The main agent's own transcript: `~/.claude/projects/<cwd-slug>/<sessionId>.jsonl`. Located by\n * scanning every project dir for the file named after the session, so the cwd-slug — which is a\n * mangling of the working directory we would otherwise have to reproduce exactly — never has to be\n * derived. '' when there is no session or the file is not there.\n */\n mainTranscript(): string {\n const session = this.sessionId();\n if (session === '') return '';\n const projects = path.join(os.homedir(), '.claude', 'projects');\n for (const proj of this.readDir(projects)) {\n const candidate = path.join(projects, proj, `${session}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * How many days Claude Code keeps transcripts: `cleanupPeriodDays` from ~/.claude/settings.json (then\n * settings.local.json), else {@link DEFAULT_RETENTION_DAYS}. The setting is usually absent, which is\n * why the default is documented rather than left implicit.\n */\n retentionDays(): number {\n const dir = path.join(os.homedir(), '.claude');\n for (const file of ['settings.json', 'settings.local.json']) {\n const settings = this.readJson(path.join(dir, file));\n const days = settings?.['cleanupPeriodDays'];\n if (typeof days === 'number' && Number.isFinite(days) && days > 0) return days;\n }\n return DEFAULT_RETENTION_DAYS;\n }\n\n /**\n * The date the FIRST of these transcripts becomes unreadable: the oldest one's mtime + retentionDays,\n * as an ISO date. The oldest rather than the newest because that is when the audit trail starts losing\n * links, and a reader planning to follow them needs the pessimistic answer. '' when none exist.\n */\n expiresOn(transcripts: readonly string[], retentionDays: number): string {\n let oldest = 0;\n for (const file of transcripts) {\n const mtime = this.mtimeOf(file);\n if (mtime !== 0 && (oldest === 0 || mtime < oldest)) oldest = mtime;\n }\n if (oldest === 0) return '';\n const expiry = new Date(oldest + retentionDays * 24 * 60 * 60 * 1000);\n return expiry.toISOString().slice(0, 10);\n }\n\n /**\n * Write the record to `<prDir>/provenance.json` and return its path ('' if it could not be written).\n *\n * Written on EVERY finish, including one that refuses for a missing reviewer: a refused round is\n * precisely the one worth auditing, and a record that only ever appears on success cannot answer \"what\n * did the reviewers do the time this was rejected?\".\n */\n write(request: ProvenanceWriteRequest): string {\n const provenance = this.build(request);\n const target = this.provenancePath(request.prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the audit record is never worth failing a PR over\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(request.prDir, { recursive: true });\n fs.writeFileSync(target, JSON.stringify(provenance, null, 2) + '\\n');\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Retire the record for the round that just shipped: copy it to old-provenance.json beside the\n // old-review.json it belongs to. A COPY, not a move — unlike review.json this file is not an input to\n // anything, so leaving it in place cannot mislead a later reviewer, and the next finish overwrites it.\n archive(prDir: string): string {\n const source = this.provenancePath(prDir);\n if (!fs.existsSync(source)) return '';\n const target = this.oldProvenancePath(prDir);\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.copyFileSync(source, target);\n return target;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // Fill in the session-level facts the caller cannot know: which session, which transcripts, how long.\n private build(request: ProvenanceWriteRequest): ReviewProvenance {\n const provenance = new ReviewProvenance(request);\n provenance.sessionId = this.sessionId();\n provenance.mainTranscript = this.mainTranscript();\n provenance.stampedAt = new Date().toISOString();\n provenance.transcriptRetentionDays = this.retentionDays();\n const linked = [provenance.mainTranscript, ...request.reviewers.map(\n (r: ReviewerTranscript): string => r.transcript)].filter((p: string): boolean => p !== '');\n provenance.transcriptsExpireOn = this.expiresOn(linked, provenance.transcriptRetentionDays);\n return provenance;\n }\n\n // Epoch millis of a file's mtime, or 0 when it cannot be read.\n private mtimeOf(filePath: string): number {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unstattable transcript contributes no expiry\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(filePath).mtime.getTime();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 0;\n }\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed settings → null (fall through to the default)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
@@ -87,6 +87,7 @@ export declare class EvidenceRequest {
87
87
  * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
88
88
  */
89
89
  export declare class SubagentProvenanceService {
90
+ private readonly branchByCwd;
90
91
  verify(expectedAgentType: string, branch: string): ProvenanceResult;
91
92
  verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult;
92
93
  /**
@@ -139,7 +140,63 @@ export declare class SubagentProvenanceService {
139
140
  private allSubagentsDirs;
140
141
  private metaFiles;
141
142
  private agentIdOf;
143
+ /**
144
+ * Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main
145
+ * loop) and the same branch. Lenient on branch drift (a leftover …wpN suffix from a mid-flight
146
+ * branch rename) and on a missing jsonl (meta already matched — accept, best-effort).
147
+ *
148
+ * WHEN `gitBranch` DISAGREES, A PINNED `cwd` DECIDES. Record 0 carries both fields, and the harness
149
+ * can write a `gitBranch` that CONTRADICTS the record's own `cwd`: a reviewer subagent spawned into
150
+ * a worktree on `dean/one-2406-…` was stamped `worktree-agent-a54887200e40eb956` — an unrelated,
151
+ * still-live worktree's scaffold branch — deterministically, on four consecutive attempts, blocking
152
+ * a PR whose required reviewer genuinely ran and genuinely passed. `stripWp` cannot bridge that, so
153
+ * the reviewer was never credited and `wp-finish-upsert-pr` refused a legitimately-reviewed PR with
154
+ * no in-flow recovery. A sweep of 961 record-0s on one machine found the mis-stamp is the MAJORITY
155
+ * case among subagents in dedicated worktrees, so trusting `gitBranch` alone can never be correct.
156
+ *
157
+ * ─── Why ONLY a linked worktree, and never the primary clone ──────────────────────────────────
158
+ * "Ask git what branch the cwd is on" is correct only when that cwd CANNOT have moved since the
159
+ * reviewer ran. A LINKED WORKTREE is 1:1 with the agent that owns it and nothing re-checks it out;
160
+ * the PRIMARY CLONE gets checked out constantly. Falling back on a primary-clone cwd would credit a
161
+ * STALE reviewer, and by construction rather than by accident:
162
+ *
163
+ * 1. a reviewer runs in the primary clone while it is on branch A
164
+ * 2. the clone is later checked out to branch B
165
+ * 3. `wp-finish-upsert-pr` runs on B — `gitBranch` (stamped A) mismatches
166
+ * 4. `git -C <clone>` now answers B, because the CLONE moved → the reviewer is credited for B
167
+ *
168
+ * It reviewed A. Every past reviewer run in that clone would be credited for whatever branch is
169
+ * checked out at finish time — which in a repo with a required reviewer on every PR is not an
170
+ * exotic sequence, it is the normal one. So the fallback is gated on `dotWebpieces.isLinkedWorktree`
171
+ * (git's own `--git-dir != --git-common-dir` test, reused rather than re-derived), and a primary
172
+ * clone falls through to a refusal exactly as it does today.
173
+ *
174
+ * That gate keeps the anti-forgery property intact: `cwd` is harness-written in the same record by
175
+ * the same writer as `gitBranch`, so it carries the same weight — and inside a pinned worktree it is
176
+ * the field that is CORRECT. There is deliberately no config flag and no second comparison mode:
177
+ * ONE rule, with `gitBranch` only ever a cheap way to skip the subprocess when it already agrees.
178
+ *
179
+ * An UNRESOLVABLE cwd does not credit either. A reaped worktree, a deleted directory, a non-repo
180
+ * path or a detached HEAD yields '' and falls through to a refusal. Leniency here is bounded by
181
+ * "git can still prove it", never by "we could not check, so assume yes".
182
+ */
142
183
  private sidechainOnBranch;
184
+ /**
185
+ * The branch record-0's own `cwd` is PINNED to, '' when that cannot be established or is not pinned.
186
+ *
187
+ * Cached per cwd because `findMatchingAgentId` walks every subagent of every session on the machine
188
+ * and several of them share a worktree — one `git` process per RECORD would put dozens of spawns on
189
+ * the path that opens a PR.
190
+ */
191
+ private branchOfCwd;
192
+ /**
193
+ * The branch of `cwd` when — and only when — `cwd` is a LINKED WORKTREE. '' otherwise.
194
+ *
195
+ * '' for the primary clone (it moves; see sidechainOnBranch for the false-accept that closes), for a
196
+ * reaped or deleted directory, for a non-repo path, when git is unavailable, and for a detached HEAD,
197
+ * which `--abbrev-ref` prints as the literal `HEAD` and which names no branch.
198
+ */
199
+ private pinnedBranch;
143
200
  private stripWp;
144
201
  private readDir;
145
202
  private firstNonEmptyLine;
@@ -2,11 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SubagentProvenanceService = exports.EvidenceRequest = exports.TranscriptScan = exports.ReviewerEvidence = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
5
6
  const fs = tslib_1.__importStar(require("fs"));
6
7
  const path = tslib_1.__importStar(require("path"));
7
8
  const os = tslib_1.__importStar(require("os"));
8
9
  const inversify_1 = require("inversify");
9
10
  const to_error_1 = require("./to-error");
11
+ const state_dir_1 = require("./state-dir");
10
12
  // Outcome of a provenance check.
11
13
  exports.PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch
12
14
  exports.PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)
@@ -129,6 +131,9 @@ exports.EvidenceRequest = EvidenceRequest;
129
131
  * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
130
132
  */
131
133
  let SubagentProvenanceService = class SubagentProvenanceService {
134
+ // cwd → the branch it is PINNED to ('' when it is not a linked worktree, or is unresolvable).
135
+ // See branchOfCwd for why it is cached and pinnedBranch for what "pinned" buys.
136
+ branchByCwd = new Map();
132
137
  // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain
133
138
  // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.
134
139
  verify(expectedAgentType, branch) {
@@ -338,9 +343,46 @@ let SubagentProvenanceService = class SubagentProvenanceService {
338
343
  agentIdOf(metaFile) {
339
344
  return metaFile.replace(/^agent-/, '').replace(/\.meta\.json$/, '');
340
345
  }
341
- // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main
342
- // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a
343
- // mid-flight branch rename) and on a missing jsonl (meta already matched accept, best-effort).
346
+ /**
347
+ * Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main
348
+ * loop) and the same branch. Lenient on branch drift (a leftover …wpN suffix from a mid-flight
349
+ * branch rename) and on a missing jsonl (meta already matched — accept, best-effort).
350
+ *
351
+ * WHEN `gitBranch` DISAGREES, A PINNED `cwd` DECIDES. Record 0 carries both fields, and the harness
352
+ * can write a `gitBranch` that CONTRADICTS the record's own `cwd`: a reviewer subagent spawned into
353
+ * a worktree on `dean/one-2406-…` was stamped `worktree-agent-a54887200e40eb956` — an unrelated,
354
+ * still-live worktree's scaffold branch — deterministically, on four consecutive attempts, blocking
355
+ * a PR whose required reviewer genuinely ran and genuinely passed. `stripWp` cannot bridge that, so
356
+ * the reviewer was never credited and `wp-finish-upsert-pr` refused a legitimately-reviewed PR with
357
+ * no in-flow recovery. A sweep of 961 record-0s on one machine found the mis-stamp is the MAJORITY
358
+ * case among subagents in dedicated worktrees, so trusting `gitBranch` alone can never be correct.
359
+ *
360
+ * ─── Why ONLY a linked worktree, and never the primary clone ──────────────────────────────────
361
+ * "Ask git what branch the cwd is on" is correct only when that cwd CANNOT have moved since the
362
+ * reviewer ran. A LINKED WORKTREE is 1:1 with the agent that owns it and nothing re-checks it out;
363
+ * the PRIMARY CLONE gets checked out constantly. Falling back on a primary-clone cwd would credit a
364
+ * STALE reviewer, and by construction rather than by accident:
365
+ *
366
+ * 1. a reviewer runs in the primary clone while it is on branch A
367
+ * 2. the clone is later checked out to branch B
368
+ * 3. `wp-finish-upsert-pr` runs on B — `gitBranch` (stamped A) mismatches
369
+ * 4. `git -C <clone>` now answers B, because the CLONE moved → the reviewer is credited for B
370
+ *
371
+ * It reviewed A. Every past reviewer run in that clone would be credited for whatever branch is
372
+ * checked out at finish time — which in a repo with a required reviewer on every PR is not an
373
+ * exotic sequence, it is the normal one. So the fallback is gated on `dotWebpieces.isLinkedWorktree`
374
+ * (git's own `--git-dir != --git-common-dir` test, reused rather than re-derived), and a primary
375
+ * clone falls through to a refusal exactly as it does today.
376
+ *
377
+ * That gate keeps the anti-forgery property intact: `cwd` is harness-written in the same record by
378
+ * the same writer as `gitBranch`, so it carries the same weight — and inside a pinned worktree it is
379
+ * the field that is CORRECT. There is deliberately no config flag and no second comparison mode:
380
+ * ONE rule, with `gitBranch` only ever a cheap way to skip the subprocess when it already agrees.
381
+ *
382
+ * An UNRESOLVABLE cwd does not credit either. A reaped worktree, a deleted directory, a non-repo
383
+ * path or a detached HEAD yields '' and falls through to a refusal. Leniency here is bounded by
384
+ * "git can still prove it", never by "we could not check, so assume yes".
385
+ */
344
386
  sidechainOnBranch(dir, agentId, branch) {
345
387
  const jsonl = path.join(dir, `agent-${agentId}.jsonl`);
346
388
  if (!fs.existsSync(jsonl))
@@ -354,9 +396,49 @@ let SubagentProvenanceService = class SubagentProvenanceService {
354
396
  if (rec['isSidechain'] !== true)
355
397
  return false;
356
398
  const gitBranch = rec['gitBranch'];
399
+ // Not recorded at all → accept, as before: nothing to contradict and nothing to verify against.
357
400
  if (typeof gitBranch !== 'string' || gitBranch === '')
358
401
  return true;
359
- return this.stripWp(gitBranch) === this.stripWp(branch);
402
+ const want = this.stripWp(branch);
403
+ if (this.stripWp(gitBranch) === want)
404
+ return true;
405
+ return want !== '' && this.stripWp(this.branchOfCwd(rec['cwd'])) === want;
406
+ }
407
+ /**
408
+ * The branch record-0's own `cwd` is PINNED to, '' when that cannot be established or is not pinned.
409
+ *
410
+ * Cached per cwd because `findMatchingAgentId` walks every subagent of every session on the machine
411
+ * and several of them share a worktree — one `git` process per RECORD would put dozens of spawns on
412
+ * the path that opens a PR.
413
+ */
414
+ // webpieces-disable no-any-unknown -- one opaque record field, type-guarded on the next line
415
+ branchOfCwd(cwd) {
416
+ if (typeof cwd !== 'string' || cwd === '')
417
+ return '';
418
+ const cached = this.branchByCwd.get(cwd);
419
+ if (cached !== undefined)
420
+ return cached;
421
+ const resolved = this.pinnedBranch(cwd);
422
+ this.branchByCwd.set(cwd, resolved);
423
+ return resolved;
424
+ }
425
+ /**
426
+ * The branch of `cwd` when — and only when — `cwd` is a LINKED WORKTREE. '' otherwise.
427
+ *
428
+ * '' for the primary clone (it moves; see sidechainOnBranch for the false-accept that closes), for a
429
+ * reaped or deleted directory, for a non-repo path, when git is unavailable, and for a detached HEAD,
430
+ * which `--abbrev-ref` prints as the literal `HEAD` and which names no branch.
431
+ */
432
+ pinnedBranch(cwd) {
433
+ if (!fs.existsSync(cwd))
434
+ return '';
435
+ if (!state_dir_1.dotWebpieces.isLinkedWorktree(cwd))
436
+ return '';
437
+ const result = (0, child_process_1.spawnSync)('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' });
438
+ if (result.status !== 0)
439
+ return '';
440
+ const printed = (result.stdout ?? '').trim();
441
+ return printed === 'HEAD' ? '' : printed;
360
442
  }
361
443
  stripWp(branch) {
362
444
  return branch.replace(/-?wp\d+$/i, '');
@@ -1 +1 @@
1
- {"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n // agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.\n agentIds: Record<string, string>;\n\n constructor(status: string, detail: string, agentIds: Record<string, string> = {}) {\n this.status = status;\n this.detail = detail;\n this.agentIds = agentIds;\n }\n}\n\n/**\n * What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).\n *\n * `verifyDistinct` answers \"did a reviewer of this type run?\" — an INTEGRITY question, and it blocks. This\n * answers \"did it look at the change?\" — a QUALITY question, and it only warns. The distinction is\n * deliberate; see {@link SubagentProvenanceService.evidenceFor}.\n */\nexport class ReviewerEvidence {\n agentType: string;\n agentId: string;\n readDiff: boolean; // opened the materialized diff dir (or its own instructions file)\n readDoc: boolean; // opened its checklist's guidance doc\n toolCallCount: number;\n offRepoSearches: number; // tool calls that reached into node_modules — the archaeology signal\n /**\n * Distinct `message.model` values OBSERVED in the transcript, in first-seen order.\n *\n * The model that actually ran, never the one that was configured — and never anything the reviewer\n * says about itself. A checklist's `.claude/agents/<subagent>.md` declares `model:`, but that is the\n * REQUESTED value and it can silently disagree with what served the request: a repo was measured\n * running opus for a reviewer whose agent file said `model: sonnet`. Asking the model instead would\n * be worse — a model is unreliable about its own identity, naming a family instead of a version or\n * repeating whatever the system prompt implied, so a self-reported field is usually right and\n * silently wrong, which is the worst kind of telemetry because it looks authoritative.\n * `message.model` is written by the harness, so this inherits the anti-forgery property the rest of\n * this class documents.\n *\n * An ARRAY, not a string: one reviewer run can span more than one model (a fallback after a refusal,\n * a mid-run config change). Collapsing to one value forces a lossy choice at read time; recording\n * what happened lets the renderer decide.\n *\n * Token counts and cost are deliberately NOT here. Prices change, vary by platform and differ under\n * intro pricing, so a rate table in this package would rot silently — and the question this answers\n * is \"which model reviewed this?\", not \"what did it cost?\".\n */\n models: string[];\n // Absolute path of the transcript these counters were read out of, '' when it could not be resolved.\n // Carried out rather than recomputed because a reviewer subagent cannot learn its own transcript path\n // (the environment exposes the PARENT session id and no agent id), so this is the only place it is\n // known — see ReviewProvenanceService, which records it as the audit link for the verdict.\n transcriptPath: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(agentType: string, agentId: string, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0, transcriptPath = '', models: string[] = []) {\n this.agentType = agentType;\n this.agentId = agentId;\n this.readDiff = readDiff;\n this.readDoc = readDoc;\n this.toolCallCount = toolCallCount;\n this.offRepoSearches = offRepoSearches;\n this.transcriptPath = transcriptPath;\n this.models = models;\n }\n}\n\n/**\n * One pass over one transcript: the tool inputs AND the models that served it. Data-only.\n *\n * They are gathered together rather than in two passes because they come from the SAME records — the\n * inputs from `message.content[].input`, the model from `message.model` one level up — and a transcript\n * is the largest file this service opens. Two reads of it to answer two questions about the same lines\n * would be the duplicate-work trap the diff materializer documents at length.\n */\nexport class TranscriptScan {\n inputs: string[]; // every tool_use input, JSON-stringified\n models: string[]; // distinct message.model values, first-seen order\n\n constructor(inputs: string[] = [], models: string[] = []) {\n this.inputs = inputs;\n this.models = models;\n }\n}\n\n/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */\nexport class EvidenceRequest {\n branch: string;\n agentIds: Record<string, string>; // agentType → agentId, straight from ProvenanceResult\n diffDir: string; // '' when nothing was materialized\n docPaths: Record<string, string>; // agentType → its checklist doc path ('' when none)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(branch: string, agentIds: Record<string, string>, diffDir = '', docPaths: Record<string, string> = {}) {\n this.branch = branch;\n this.agentIds = agentIds;\n this.diffDir = diffDir;\n this.docPaths = docPaths;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n if (!this.inClaudeSession()) return this.skipped(`the \"${expectedAgentType}\" reviewer subagent`);\n const dirs = this.allSubagentsDirs();\n const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);\n return agentId !== ''\n ? new ProvenanceResult(PROVENANCE_OK, `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `No \"${expectedAgentType}\" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);\n }\n\n // Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not\n // self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.\n //\n // Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in\n // any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.\n // That is what keeps \"review once per branch\" true across sessions. A PR opened outside the gated flow\n // still has no review-<id>.json, so wp-finish forces the review regardless of provenance.\n verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\n if (!this.inClaudeSession()) return this.skipped('reviewer subagents');\n const dirs = this.allSubagentsDirs();\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n const credited: Record<string, string> = {};\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else {\n usedAgentIds.add(agentId);\n credited[type] = agentId;\n }\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`,\n credited);\n }\n\n /**\n * What each credited reviewer actually READ, from its own transcript.\n *\n * `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff\n * and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and\n * the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because\n * nothing had told it where anything was, which `offRepoSearches` now makes visible.\n *\n * WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least\n * one repo has watched it:\n * 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every\n * consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly\n * this reason, and blocking on a richer read of the same files would be less safe, not more.\n * 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).\n * 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating\n * them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.\n *\n * Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.\n */\n evidenceFor(request: EvidenceRequest): ReviewerEvidence[] {\n if (!this.inClaudeSession()) return [];\n const dirs = this.allSubagentsDirs();\n const out: ReviewerEvidence[] = [];\n for (const agentType of Object.keys(request.agentIds)) {\n const agentId = request.agentIds[agentType];\n const jsonl = this.transcriptPath(dirs, agentId);\n if (jsonl === '') {\n out.push(new ReviewerEvidence(agentType, agentId));\n continue;\n }\n out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));\n }\n return out;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private evidenceFromTranscript(agentType: string, agentId: string, jsonl: string, request: EvidenceRequest): ReviewerEvidence {\n const scan = this.scanTranscript(jsonl);\n const inputs = scan.inputs;\n const docPath = request.docPaths[agentType] ?? '';\n // The instructions file counts as \"read the diff\": it lives in the same per-branch dir, it is what\n // the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and\n // then its own diff files is the intended path; treating only the diff dir as proof would flag it.\n const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);\n return new ReviewerEvidence(\n agentType, agentId, readDiff,\n docPath !== '' && this.mentions(inputs, docPath),\n inputs.length,\n inputs.filter((i: string): boolean => i.includes('node_modules')).length,\n jsonl,\n scan.models,\n );\n }\n\n /**\n * Record this line's `message.model`, deduped, in first-seen order.\n *\n * `<synthetic>` records are SKIPPED. The harness writes them for messages no model produced (tool\n * results, injected notices); counting one would put a non-model in a field whose entire purpose is\n * \"which model reviewed this\", and it would show up on every single reviewer.\n */\n // webpieces-disable no-any-unknown -- opaque transcript record, only `model` is read and it is type-guarded below\n private collectModel(message: Record<string, unknown>, models: string[]): void {\n const model = message['model'];\n if (typeof model !== 'string' || model === '' || model === '<synthetic>') return;\n if (!models.includes(model)) models.push(model);\n }\n\n // The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.\n private transcriptPath(dirs: readonly string[], agentId: string): string {\n for (const dir of dirs) {\n const candidate = path.join(dir, `agent-${agentId}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * ONE pass over the transcript for both answers (see {@link TranscriptScan}).\n *\n * Tool inputs are JSON-stringified rather than walked field-by-field because the shape differs per\n * tool (Read takes file_path, Bash takes command, Grep takes path) and a substring test over the\n * serialized input is both simpler and robust to a tool we have not seen.\n *\n * Best-effort, like everything else that parses this file: a format change must never wedge the\n * gate, so an unreadable transcript yields empty rather than throwing. That matters more for the\n * models than for the counters — this is quality telemetry, not an integrity check, and a missing\n * `model` field must not be able to block a PR.\n */\n private scanTranscript(jsonl: string): TranscriptScan {\n const scan = new TranscriptScan();\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(jsonl, 'utf8').split('\\n')) {\n if (line.trim() === '') continue;\n const rec = this.parseLine(line);\n const message = rec?.['message'];\n if (typeof message !== 'object' || message === null) continue;\n // webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above\n const msg = message as Record<string, unknown>;\n this.collectModel(msg, scan.models);\n const content = msg['content'];\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n // webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read\n const b = block as Record<string, unknown>;\n if (b['type'] === 'tool_use') scan.inputs.push(JSON.stringify(b['input'] ?? {}));\n }\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n return scan;\n }\n\n private mentions(inputs: readonly string[], needle: string): boolean {\n return inputs.some((i: string): boolean => i.includes(needle));\n }\n\n // Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)\n private inClaudeSession(): boolean {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';\n }\n\n private skipped(what: string): ProvenanceResult {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);\n }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'\n // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`\n // skips agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dirs: readonly string[], agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\n for (const dir of dirs) {\n for (const metaFile of this.metaFiles(dir)) {\n const agentId = this.agentIdOf(metaFile);\n if (exclude.has(agentId)) continue;\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta || meta['agentType'] !== agentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n if (this.sidechainOnBranch(dir, agentId, branch)) return agentId;\n }\n }\n return '';\n }\n\n // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by\n // session id) is what makes provenance survive across sessions.\n private allSubagentsDirs(): string[] {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return [];\n const out: string[] = [];\n for (const proj of this.readDir(projects)) {\n const projDir = path.join(projects, proj);\n for (const session of this.readDir(projDir)) {\n const candidate = path.join(projDir, session, 'subagents');\n if (fs.existsSync(candidate)) out.push(candidate);\n }\n }\n return out;\n }\n\n private metaFiles(dir: string): string[] {\n return this.readDir(dir).filter((f: string): boolean => f.endsWith('.meta.json'));\n }\n\n // agent-<id>.meta.json → <id>\n private agentIdOf(metaFile: string): string {\n return metaFile.replace(/^agent-/, '').replace(/\\.meta\\.json$/, '');\n }\n\n // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main\n // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a\n // mid-flight branch rename) and on a missing jsonl (meta already matched — accept, best-effort).\n private sidechainOnBranch(dir: string, agentId: string, branch: string): boolean {\n const jsonl = path.join(dir, `agent-${agentId}.jsonl`);\n if (!fs.existsSync(jsonl)) return true;\n const first = this.firstNonEmptyLine(jsonl);\n if (first === '') return true;\n const rec = this.parseLine(first);\n if (!rec) return true;\n if (rec['isSidechain'] !== true) return false;\n const gitBranch = rec['gitBranch'];\n if (typeof gitBranch !== 'string' || gitBranch === '') return true;\n return this.stripWp(gitBranch) === this.stripWp(branch);\n }\n\n private stripWp(branch: string): string {\n return branch.replace(/-?wp\\d+$/i, '');\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private firstNonEmptyLine(filePath: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(filePath, 'utf8').split('\\n')) {\n if (line.trim() !== '') return line;\n }\n return '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller\n private parseLine(line: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(line) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\nimport { dotWebpieces } from './state-dir';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n // agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.\n agentIds: Record<string, string>;\n\n constructor(status: string, detail: string, agentIds: Record<string, string> = {}) {\n this.status = status;\n this.detail = detail;\n this.agentIds = agentIds;\n }\n}\n\n/**\n * What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).\n *\n * `verifyDistinct` answers \"did a reviewer of this type run?\" — an INTEGRITY question, and it blocks. This\n * answers \"did it look at the change?\" — a QUALITY question, and it only warns. The distinction is\n * deliberate; see {@link SubagentProvenanceService.evidenceFor}.\n */\nexport class ReviewerEvidence {\n agentType: string;\n agentId: string;\n readDiff: boolean; // opened the materialized diff dir (or its own instructions file)\n readDoc: boolean; // opened its checklist's guidance doc\n toolCallCount: number;\n offRepoSearches: number; // tool calls that reached into node_modules — the archaeology signal\n /**\n * Distinct `message.model` values OBSERVED in the transcript, in first-seen order.\n *\n * The model that actually ran, never the one that was configured — and never anything the reviewer\n * says about itself. A checklist's `.claude/agents/<subagent>.md` declares `model:`, but that is the\n * REQUESTED value and it can silently disagree with what served the request: a repo was measured\n * running opus for a reviewer whose agent file said `model: sonnet`. Asking the model instead would\n * be worse — a model is unreliable about its own identity, naming a family instead of a version or\n * repeating whatever the system prompt implied, so a self-reported field is usually right and\n * silently wrong, which is the worst kind of telemetry because it looks authoritative.\n * `message.model` is written by the harness, so this inherits the anti-forgery property the rest of\n * this class documents.\n *\n * An ARRAY, not a string: one reviewer run can span more than one model (a fallback after a refusal,\n * a mid-run config change). Collapsing to one value forces a lossy choice at read time; recording\n * what happened lets the renderer decide.\n *\n * Token counts and cost are deliberately NOT here. Prices change, vary by platform and differ under\n * intro pricing, so a rate table in this package would rot silently — and the question this answers\n * is \"which model reviewed this?\", not \"what did it cost?\".\n */\n models: string[];\n // Absolute path of the transcript these counters were read out of, '' when it could not be resolved.\n // Carried out rather than recomputed because a reviewer subagent cannot learn its own transcript path\n // (the environment exposes the PARENT session id and no agent id), so this is the only place it is\n // known — see ReviewProvenanceService, which records it as the audit link for the verdict.\n transcriptPath: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(agentType: string, agentId: string, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0, transcriptPath = '', models: string[] = []) {\n this.agentType = agentType;\n this.agentId = agentId;\n this.readDiff = readDiff;\n this.readDoc = readDoc;\n this.toolCallCount = toolCallCount;\n this.offRepoSearches = offRepoSearches;\n this.transcriptPath = transcriptPath;\n this.models = models;\n }\n}\n\n/**\n * One pass over one transcript: the tool inputs AND the models that served it. Data-only.\n *\n * They are gathered together rather than in two passes because they come from the SAME records — the\n * inputs from `message.content[].input`, the model from `message.model` one level up — and a transcript\n * is the largest file this service opens. Two reads of it to answer two questions about the same lines\n * would be the duplicate-work trap the diff materializer documents at length.\n */\nexport class TranscriptScan {\n inputs: string[]; // every tool_use input, JSON-stringified\n models: string[]; // distinct message.model values, first-seen order\n\n constructor(inputs: string[] = [], models: string[] = []) {\n this.inputs = inputs;\n this.models = models;\n }\n}\n\n/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */\nexport class EvidenceRequest {\n branch: string;\n agentIds: Record<string, string>; // agentType → agentId, straight from ProvenanceResult\n diffDir: string; // '' when nothing was materialized\n docPaths: Record<string, string>; // agentType → its checklist doc path ('' when none)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(branch: string, agentIds: Record<string, string>, diffDir = '', docPaths: Record<string, string> = {}) {\n this.branch = branch;\n this.agentIds = agentIds;\n this.diffDir = diffDir;\n this.docPaths = docPaths;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // cwd → the branch it is PINNED to ('' when it is not a linked worktree, or is unresolvable).\n // See branchOfCwd for why it is cached and pinnedBranch for what \"pinned\" buys.\n private readonly branchByCwd = new Map<string, string>();\n\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n if (!this.inClaudeSession()) return this.skipped(`the \"${expectedAgentType}\" reviewer subagent`);\n const dirs = this.allSubagentsDirs();\n const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);\n return agentId !== ''\n ? new ProvenanceResult(PROVENANCE_OK, `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `No \"${expectedAgentType}\" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);\n }\n\n // Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not\n // self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.\n //\n // Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in\n // any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.\n // That is what keeps \"review once per branch\" true across sessions. A PR opened outside the gated flow\n // still has no review-<id>.json, so wp-finish forces the review regardless of provenance.\n verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\n if (!this.inClaudeSession()) return this.skipped('reviewer subagents');\n const dirs = this.allSubagentsDirs();\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n const credited: Record<string, string> = {};\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else {\n usedAgentIds.add(agentId);\n credited[type] = agentId;\n }\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`,\n credited);\n }\n\n /**\n * What each credited reviewer actually READ, from its own transcript.\n *\n * `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff\n * and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and\n * the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because\n * nothing had told it where anything was, which `offRepoSearches` now makes visible.\n *\n * WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least\n * one repo has watched it:\n * 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every\n * consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly\n * this reason, and blocking on a richer read of the same files would be less safe, not more.\n * 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).\n * 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating\n * them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.\n *\n * Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.\n */\n evidenceFor(request: EvidenceRequest): ReviewerEvidence[] {\n if (!this.inClaudeSession()) return [];\n const dirs = this.allSubagentsDirs();\n const out: ReviewerEvidence[] = [];\n for (const agentType of Object.keys(request.agentIds)) {\n const agentId = request.agentIds[agentType];\n const jsonl = this.transcriptPath(dirs, agentId);\n if (jsonl === '') {\n out.push(new ReviewerEvidence(agentType, agentId));\n continue;\n }\n out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));\n }\n return out;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private evidenceFromTranscript(agentType: string, agentId: string, jsonl: string, request: EvidenceRequest): ReviewerEvidence {\n const scan = this.scanTranscript(jsonl);\n const inputs = scan.inputs;\n const docPath = request.docPaths[agentType] ?? '';\n // The instructions file counts as \"read the diff\": it lives in the same per-branch dir, it is what\n // the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and\n // then its own diff files is the intended path; treating only the diff dir as proof would flag it.\n const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);\n return new ReviewerEvidence(\n agentType, agentId, readDiff,\n docPath !== '' && this.mentions(inputs, docPath),\n inputs.length,\n inputs.filter((i: string): boolean => i.includes('node_modules')).length,\n jsonl,\n scan.models,\n );\n }\n\n /**\n * Record this line's `message.model`, deduped, in first-seen order.\n *\n * `<synthetic>` records are SKIPPED. The harness writes them for messages no model produced (tool\n * results, injected notices); counting one would put a non-model in a field whose entire purpose is\n * \"which model reviewed this\", and it would show up on every single reviewer.\n */\n // webpieces-disable no-any-unknown -- opaque transcript record, only `model` is read and it is type-guarded below\n private collectModel(message: Record<string, unknown>, models: string[]): void {\n const model = message['model'];\n if (typeof model !== 'string' || model === '' || model === '<synthetic>') return;\n if (!models.includes(model)) models.push(model);\n }\n\n // The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.\n private transcriptPath(dirs: readonly string[], agentId: string): string {\n for (const dir of dirs) {\n const candidate = path.join(dir, `agent-${agentId}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * ONE pass over the transcript for both answers (see {@link TranscriptScan}).\n *\n * Tool inputs are JSON-stringified rather than walked field-by-field because the shape differs per\n * tool (Read takes file_path, Bash takes command, Grep takes path) and a substring test over the\n * serialized input is both simpler and robust to a tool we have not seen.\n *\n * Best-effort, like everything else that parses this file: a format change must never wedge the\n * gate, so an unreadable transcript yields empty rather than throwing. That matters more for the\n * models than for the counters — this is quality telemetry, not an integrity check, and a missing\n * `model` field must not be able to block a PR.\n */\n private scanTranscript(jsonl: string): TranscriptScan {\n const scan = new TranscriptScan();\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(jsonl, 'utf8').split('\\n')) {\n if (line.trim() === '') continue;\n const rec = this.parseLine(line);\n const message = rec?.['message'];\n if (typeof message !== 'object' || message === null) continue;\n // webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above\n const msg = message as Record<string, unknown>;\n this.collectModel(msg, scan.models);\n const content = msg['content'];\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n // webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read\n const b = block as Record<string, unknown>;\n if (b['type'] === 'tool_use') scan.inputs.push(JSON.stringify(b['input'] ?? {}));\n }\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n return scan;\n }\n\n private mentions(inputs: readonly string[], needle: string): boolean {\n return inputs.some((i: string): boolean => i.includes(needle));\n }\n\n // Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)\n private inClaudeSession(): boolean {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';\n }\n\n private skipped(what: string): ProvenanceResult {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);\n }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'\n // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`\n // skips agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dirs: readonly string[], agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\n for (const dir of dirs) {\n for (const metaFile of this.metaFiles(dir)) {\n const agentId = this.agentIdOf(metaFile);\n if (exclude.has(agentId)) continue;\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta || meta['agentType'] !== agentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n if (this.sidechainOnBranch(dir, agentId, branch)) return agentId;\n }\n }\n return '';\n }\n\n // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by\n // session id) is what makes provenance survive across sessions.\n private allSubagentsDirs(): string[] {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return [];\n const out: string[] = [];\n for (const proj of this.readDir(projects)) {\n const projDir = path.join(projects, proj);\n for (const session of this.readDir(projDir)) {\n const candidate = path.join(projDir, session, 'subagents');\n if (fs.existsSync(candidate)) out.push(candidate);\n }\n }\n return out;\n }\n\n private metaFiles(dir: string): string[] {\n return this.readDir(dir).filter((f: string): boolean => f.endsWith('.meta.json'));\n }\n\n // agent-<id>.meta.json → <id>\n private agentIdOf(metaFile: string): string {\n return metaFile.replace(/^agent-/, '').replace(/\\.meta\\.json$/, '');\n }\n\n /**\n * Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main\n * loop) and the same branch. Lenient on branch drift (a leftover …wpN suffix from a mid-flight\n * branch rename) and on a missing jsonl (meta already matched — accept, best-effort).\n *\n * WHEN `gitBranch` DISAGREES, A PINNED `cwd` DECIDES. Record 0 carries both fields, and the harness\n * can write a `gitBranch` that CONTRADICTS the record's own `cwd`: a reviewer subagent spawned into\n * a worktree on `dean/one-2406-…` was stamped `worktree-agent-a54887200e40eb956` — an unrelated,\n * still-live worktree's scaffold branch — deterministically, on four consecutive attempts, blocking\n * a PR whose required reviewer genuinely ran and genuinely passed. `stripWp` cannot bridge that, so\n * the reviewer was never credited and `wp-finish-upsert-pr` refused a legitimately-reviewed PR with\n * no in-flow recovery. A sweep of 961 record-0s on one machine found the mis-stamp is the MAJORITY\n * case among subagents in dedicated worktrees, so trusting `gitBranch` alone can never be correct.\n *\n * ─── Why ONLY a linked worktree, and never the primary clone ──────────────────────────────────\n * \"Ask git what branch the cwd is on\" is correct only when that cwd CANNOT have moved since the\n * reviewer ran. A LINKED WORKTREE is 1:1 with the agent that owns it and nothing re-checks it out;\n * the PRIMARY CLONE gets checked out constantly. Falling back on a primary-clone cwd would credit a\n * STALE reviewer, and by construction rather than by accident:\n *\n * 1. a reviewer runs in the primary clone while it is on branch A\n * 2. the clone is later checked out to branch B\n * 3. `wp-finish-upsert-pr` runs on B — `gitBranch` (stamped A) mismatches\n * 4. `git -C <clone>` now answers B, because the CLONE moved → the reviewer is credited for B\n *\n * It reviewed A. Every past reviewer run in that clone would be credited for whatever branch is\n * checked out at finish time — which in a repo with a required reviewer on every PR is not an\n * exotic sequence, it is the normal one. So the fallback is gated on `dotWebpieces.isLinkedWorktree`\n * (git's own `--git-dir != --git-common-dir` test, reused rather than re-derived), and a primary\n * clone falls through to a refusal exactly as it does today.\n *\n * That gate keeps the anti-forgery property intact: `cwd` is harness-written in the same record by\n * the same writer as `gitBranch`, so it carries the same weight — and inside a pinned worktree it is\n * the field that is CORRECT. There is deliberately no config flag and no second comparison mode:\n * ONE rule, with `gitBranch` only ever a cheap way to skip the subprocess when it already agrees.\n *\n * An UNRESOLVABLE cwd does not credit either. A reaped worktree, a deleted directory, a non-repo\n * path or a detached HEAD yields '' and falls through to a refusal. Leniency here is bounded by\n * \"git can still prove it\", never by \"we could not check, so assume yes\".\n */\n private sidechainOnBranch(dir: string, agentId: string, branch: string): boolean {\n const jsonl = path.join(dir, `agent-${agentId}.jsonl`);\n if (!fs.existsSync(jsonl)) return true;\n const first = this.firstNonEmptyLine(jsonl);\n if (first === '') return true;\n const rec = this.parseLine(first);\n if (!rec) return true;\n if (rec['isSidechain'] !== true) return false;\n const gitBranch = rec['gitBranch'];\n // Not recorded at all → accept, as before: nothing to contradict and nothing to verify against.\n if (typeof gitBranch !== 'string' || gitBranch === '') return true;\n const want = this.stripWp(branch);\n if (this.stripWp(gitBranch) === want) return true;\n return want !== '' && this.stripWp(this.branchOfCwd(rec['cwd'])) === want;\n }\n\n /**\n * The branch record-0's own `cwd` is PINNED to, '' when that cannot be established or is not pinned.\n *\n * Cached per cwd because `findMatchingAgentId` walks every subagent of every session on the machine\n * and several of them share a worktree — one `git` process per RECORD would put dozens of spawns on\n * the path that opens a PR.\n */\n // webpieces-disable no-any-unknown -- one opaque record field, type-guarded on the next line\n private branchOfCwd(cwd: unknown): string {\n if (typeof cwd !== 'string' || cwd === '') return '';\n const cached = this.branchByCwd.get(cwd);\n if (cached !== undefined) return cached;\n const resolved = this.pinnedBranch(cwd);\n this.branchByCwd.set(cwd, resolved);\n return resolved;\n }\n\n /**\n * The branch of `cwd` when — and only when — `cwd` is a LINKED WORKTREE. '' otherwise.\n *\n * '' for the primary clone (it moves; see sidechainOnBranch for the false-accept that closes), for a\n * reaped or deleted directory, for a non-repo path, when git is unavailable, and for a detached HEAD,\n * which `--abbrev-ref` prints as the literal `HEAD` and which names no branch.\n */\n private pinnedBranch(cwd: string): string {\n if (!fs.existsSync(cwd)) return '';\n if (!dotWebpieces.isLinkedWorktree(cwd)) return '';\n const result = spawnSync('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n const printed = (result.stdout ?? '').trim();\n return printed === 'HEAD' ? '' : printed;\n }\n\n private stripWp(branch: string): string {\n return branch.replace(/-?wp\\d+$/i, '');\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private firstNonEmptyLine(filePath: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(filePath, 'utf8').split('\\n')) {\n if (line.trim() !== '') return line;\n }\n return '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller\n private parseLine(line: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(line) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
@@ -185,7 +185,7 @@ flow are not interchangeable.
185
185
  `pnpm wp-finish-update`, and no longer `pnpm wp-finish-upsert-pr`).
186
186
  4. **`pnpm wp-review-upsert-pr`** — STAGE ②, and the one that verifies before anyone reviews. It validates
187
187
  and commits any in-progress 3-point merge, asserts a clean tree, runs the **build gate**, extracts this
188
- branch's diff to `.webpieces/pr-review/<branch>/diff/`, writes a per-reviewer instructions file, and
188
+ branch's diff to `.webpieces/pr-review/<featureSlug>/diff/`, writes a per-reviewer instructions file, and
189
189
  prints exactly what to spawn plus the `review.json` schema. It CAN fail — and when it does, no reviewer
190
190
  has been spawned yet, so a broken branch costs no review effort.
191
191
  5. **`pnpm wp-finish-upsert-pr`** — STAGE ③: requires stage ②'s receipt, your `review.json` (its `title`
@@ -66,7 +66,7 @@ pnpm wp-review-upsert-pr
66
66
 
67
67
  It validates and commits any in-progress 3-point merge, runs the build gate, then **extracts this branch's
68
68
  diff to disk** and writes one instructions file per reviewer under
69
- `.webpieces/pr-review/<branch>/instructions/`. It prints a copy-paste spawn block per reviewer whose prompt
69
+ `.webpieces/pr-review/<featureSlug>/instructions/`. It prints a copy-paste spawn block per reviewer whose prompt
70
70
  is a POINTER to that file and nothing else.
71
71
 
72
72
  That indirection is the design. Everything volatile — the diff, the matched files, the verdict schema, the
@@ -95,7 +95,7 @@ Where patterns DO apply, matching is deliberately **coarse** — the reviewer su
95
95
  content-level judgment by reading the actual diff.
96
96
 
97
97
  The changed files + the exact base sha the gate uses are in
98
- `.webpieces/pr-review/<branch>/pr-context.json`:
98
+ `.webpieces/pr-review/<featureSlug>/pr-context.json`:
99
99
 
100
100
  ```json
101
101
  { "base": "<merge-base sha>", "head": "<HEAD sha>", "changedFiles": ["path/a.ts", "db/003.sql", ...] }
@@ -110,7 +110,7 @@ For each matched **required** checklist — and each **optional** one the human
110
110
  `git diff <base> HEAD -- <file>` (base is in `pr-context.json`) — and decide whether the change
111
111
  satisfies the checklist. (A path-coarse checklist like "new API/queues" simply reports
112
112
  `"status": "green"` when the diffs add no new route/queue.)
113
- 3. Have it **write its verdict** to `.webpieces/pr-review/<branch>/review-<id>.json` (one file per
113
+ 3. Have it **write its verdict** to `.webpieces/pr-review/<featureSlug>/review-<id>.json` (one file per
114
114
  checklist, so concurrent reviewers never clobber each other). `<id>` is the subagent name.
115
115
 
116
116
  **How to ask about the optional ones.** `wp-review-upsert-pr` prints them as their own step, listing each
@@ -188,7 +188,7 @@ only then opens/updates the PR.
188
188
 
189
189
  ## `provenance.json` (the tooling writes this; you never do)
190
190
 
191
- `wp-finish-upsert-pr` writes `.webpieces/pr-review/<branch>/provenance.json` — the audit record of **how
191
+ `wp-finish-upsert-pr` writes `.webpieces/pr-review/<featureSlug>/provenance.json` — the audit record of **how
192
192
  the review was done**, as opposed to what it concluded. Per reviewer it links the verdict to the
193
193
  transcript of the subagent that produced it, plus what that reviewer was *offered* (its instructions file,
194
194
  the diff dir, its checklist doc) against what it demonstrably *read*, and its tool-call counts.