@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 +1 -1
- package/src/review-json-data.js +2 -2
- package/src/review-json-data.js.map +1 -1
- package/src/review-provenance.d.ts +14 -3
- package/src/review-provenance.js +18 -5
- package/src/review-provenance.js.map +1 -1
- package/src/subagent-provenance.d.ts +57 -0
- package/src/subagent-provenance.js +86 -4
- package/src/subagent-provenance.js.map +1 -1
- package/templates/webpieces.git-workflow.md +1 -1
- package/templates/webpieces.review-checklists.md +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
package/src/review-json-data.js
CHANGED
|
@@ -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/<
|
|
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/<
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
86
|
+
featureSlug: string;
|
|
76
87
|
headSha: string;
|
|
77
88
|
stampedAt: string;
|
|
78
89
|
transcriptRetentionDays: number;
|
package/src/review-provenance.js
CHANGED
|
@@ -97,15 +97,26 @@ exports.OfferedContext = OfferedContext;
|
|
|
97
97
|
*/
|
|
98
98
|
class ProvenanceWriteRequest {
|
|
99
99
|
prDir;
|
|
100
|
-
|
|
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,
|
|
117
|
+
constructor(prDir, featureSlug, headSha, provenanceStatus) {
|
|
107
118
|
this.prDir = prDir;
|
|
108
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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":"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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 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/*/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/*/<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/*/<sessionId>.jsonl → the main agent's transcript\n * ~/.claude/projects/*/<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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,+CAAyB;AACzB,yCAA2D;AAC3D,yCAAqC;AAErC,iCAAiC;AACpB,QAAA,aAAa,GAAG,IAAI,CAAC,CAAW,+DAA+D;AAC/F,QAAA,kBAAkB,GAAG,SAAS,CAAC,CAAC,0EAA0E;AAC1G,QAAA,kBAAkB,GAAG,SAAS,CAAC,CAAC,gEAAgE;AAE7G,MAAa,gBAAgB;IACzB,MAAM,CAAS,CAAC,0DAA0D;IAC1E,MAAM,CAAS,CAAC,6DAA6D;IAC7E,kGAAkG;IAClG,QAAQ,CAAyB;IAEjC,YAAY,MAAc,EAAE,MAAc,EAAE,WAAmC,EAAE;QAC7E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAXD,4CAWC;AAED;;;;;;GAMG;AACH,MAAa,gBAAgB;IACzB,SAAS,CAAS;IAClB,OAAO,CAAS;IAChB,QAAQ,CAAU,CAAO,kEAAkE;IAC3F,OAAO,CAAU,CAAQ,sCAAsC;IAC/D,aAAa,CAAS;IACtB,eAAe,CAAS,CAAC,qEAAqE;IAC9F;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAW;IACjB,qGAAqG;IACrG,sGAAsG;IACtG,mGAAmG;IACnG,2FAA2F;IAC3F,cAAc,CAAS;IAEvB,yDAAyD;IACzD,YAAY,SAAiB,EAAE,OAAe,EAAE,QAAQ,GAAG,KAAK,EAAE,OAAO,GAAG,KAAK,EAAE,aAAa,GAAG,CAAC,EAAE,eAAe,GAAG,CAAC,EAAE,cAAc,GAAG,EAAE,EAAE,SAAmB,EAAE;QACjK,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AA9CD,4CA8CC;AAED;;;;;;;GAOG;AACH,MAAa,cAAc;IACvB,MAAM,CAAW,CAAG,yCAAyC;IAC7D,MAAM,CAAW,CAAG,kDAAkD;IAEtE,YAAY,SAAmB,EAAE,EAAE,SAAmB,EAAE;QACpD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AARD,wCAQC;AAED,+FAA+F;AAC/F,MAAa,eAAe;IACxB,MAAM,CAAS;IACf,QAAQ,CAAyB,CAAC,sDAAsD;IACxF,OAAO,CAAS,CAAkB,mCAAmC;IACrE,QAAQ,CAAyB,CAAC,oDAAoD;IAEtF,yDAAyD;IACzD,YAAY,MAAc,EAAE,QAAgC,EAAE,OAAO,GAAG,EAAE,EAAE,WAAmC,EAAE;QAC7G,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAbD,0CAaC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,yBAAyB,GAA/B,MAAM,yBAAyB;IAClC,iGAAiG;IACjG,yFAAyF;IACzF,MAAM,CAAC,iBAAyB,EAAE,MAAc;QAC5C,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,iBAAiB,qBAAqB,CAAC,CAAC;QACjG,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC;QAC1E,OAAO,OAAO,KAAK,EAAE;YACjB,CAAC,CAAC,IAAI,gBAAgB,CAAC,qBAAa,EAAE,aAAa,iBAAiB,kCAAkC,OAAO,IAAI,CAAC;YAClH,CAAC,CAAC,IAAI,gBAAgB,CAAC,0BAAkB,EACrC,OAAO,iBAAiB,uGAAuG,CAAC,CAAC;IAC7I,CAAC;IAED,uGAAuG;IACvG,oGAAoG;IACpG,EAAE;IACF,wGAAwG;IACxG,uGAAuG;IACvG,uGAAuG;IACvG,0FAA0F;IAC1F,cAAc,CAAC,kBAAqC,EAAE,MAAc;QAChE,IAAI,kBAAkB,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,gBAAgB,CAAC,qBAAa,EAAE,gCAAgC,CAAC,CAAC;QAClH,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACvE,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACrC,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;QACvC,MAAM,QAAQ,GAA2B,EAAE,CAAC;QAC5C,KAAK,MAAM,IAAI,IAAI,kBAAkB,EAAE,CAAC;YACpC,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;YAC3E,IAAI,OAAO,KAAK,EAAE;gBAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;iBAClC,CAAC;gBACF,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;YAC7B,CAAC;QACL,CAAC;QACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC;YACvB,CAAC,CAAC,IAAI,gBAAgB,CAAC,qBAAa,EAAE,YAAY,kBAAkB,CAAC,MAAM,oCAAoC,EAAE,QAAQ,CAAC;YAC1H,CAAC,CAAC,IAAI,gBAAgB,CAAC,0BAAkB,EACrC,+GAA+G,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EACnI,QAAQ,CAAC,CAAC;IACtB,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,WAAW,CAAC,OAAwB;QAChC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE;YAAE,OAAO,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACrC,MAAM,GAAG,GAAuB,EAAE,CAAC;QACnC,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpD,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACjD,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACf,GAAG,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;gBACnD,SAAS;YACb,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,sBAAsB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,yDAAyD;IACjD,sBAAsB,CAAC,SAAiB,EAAE,OAAe,EAAE,KAAa,EAAE,OAAwB;QACtG,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAClD,mGAAmG;QACnG,mGAAmG;QACnG,mGAAmG;QACnG,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAClF,OAAO,IAAI,gBAAgB,CACvB,SAAS,EAAE,OAAO,EAAE,QAAQ,EAC5B,OAAO,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,EAChD,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,EACxE,KAAK,EACL,IAAI,CAAC,MAAM,CACd,CAAC;IACN,CAAC;IAED;;;;;;OAMG;IACH,kHAAkH;IAC1G,YAAY,CAAC,OAAgC,EAAE,MAAgB;QACnE,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,IAAI,KAAK,KAAK,aAAa;YAAE,OAAO;QACjF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC;IAED,2FAA2F;IACnF,cAAc,CAAC,IAAuB,EAAE,OAAe;QAC3D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,OAAO,QAAQ,CAAC,CAAC;YAC3D,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QACnD,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAED;;;;;;;;;;;OAWG;IACK,cAAc,CAAC,KAAa;QAChC,MAAM,IAAI,GAAG,IAAI,cAAc,EAAE,CAAC;QAClC,sHAAsH;QACtH,8DAA8D;QAC9D,IAAI,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5D,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;oBAAE,SAAS;gBACjC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;gBACjC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;oBAAE,SAAS;gBAC9D,4FAA4F;gBAC5F,MAAM,GAAG,GAAG,OAAkC,CAAC;gBAC/C,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;oBAAE,SAAS;gBACtC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;oBAC1B,yFAAyF;oBACzF,MAAM,CAAC,GAAG,KAAgC,CAAC;oBAC3C,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,UAAU;wBAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBACrF,CAAC;YACL,CAAC;QACL,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,KAAK,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,QAAQ,CAAC,MAAyB,EAAE,MAAc;QACtD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,sGAAsG;IAC9F,eAAe;QACnB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IACvE,CAAC;IAEO,OAAO,CAAC,IAAY;QACxB,OAAO,IAAI,gBAAgB,CAAC,0BAAkB,EAC1C,kDAAkD,IAAI,4DAA4D,CAAC,CAAC;IAC5H,CAAC;IAED,oGAAoG;IACpG,mGAAmG;IACnG,qFAAqF;IAC7E,mBAAmB,CAAC,IAAuB,EAAE,SAAiB,EAAE,MAAc,EAAE,UAA+B,IAAI,GAAG,EAAE;QAC5H,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;gBACzC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;oBAAE,SAAS;gBACnC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;gBACrD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,SAAS;oBAAE,SAAS;gBACvD,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;gBACtC,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,CAAC;oBAAE,SAAS;gBAC/D,IAAI,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC;oBAAE,OAAO,OAAO,CAAC;YACrE,CAAC;QACL,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAED,sGAAsG;IACtG,gEAAgE;IACxD,gBAAgB;QACpB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAChE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACxC,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAC1C,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;gBAC3D,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtD,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAEO,SAAS,CAAC,GAAW;QACzB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACtF,CAAC;IAED,8BAA8B;IACtB,SAAS,CAAC,QAAgB;QAC9B,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,iGAAiG;IACjG,oGAAoG;IACpG,iGAAiG;IACzF,iBAAiB,CAAC,GAAW,EAAE,OAAe,EAAE,MAAc;QAClE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,OAAO,QAAQ,CAAC,CAAC;QACvD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,IAAI,GAAG,CAAC,aAAa,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC9C,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;QACnC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QACnE,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IAEO,OAAO,CAAC,MAAc;QAC1B,OAAO,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IAC3C,CAAC;IAEO,OAAO,CAAC,GAAW;QACvB,gHAAgH;QAChH,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;IAEO,iBAAiB,CAAC,QAAgB;QACtC,oGAAoG;QACpG,8DAA8D;QAC9D,IAAI,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/D,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;oBAAE,OAAO,IAAI,CAAC;YACxC,CAAC;YACD,OAAO,EAAE,CAAC;QACd,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,gGAAgG;QAChG,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;IAED,uFAAuF;IAC/E,SAAS,CAAC,IAAY;QAC1B,yFAAyF;QACzF,8DAA8D;QAC9D,IAAI,CAAC;YACD,yFAAyF;YACzF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QACvD,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;AAlSY,8DAAyB;oCAAzB,yBAAyB;IADrC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;GAC5B,yBAAyB,CAkSrC","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/*/<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/*/<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/<
|
|
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/<
|
|
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/<
|
|
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/<
|
|
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/<
|
|
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.
|