@webpieces/rules-config 0.4.468 → 0.4.470

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.
@@ -1 +1 @@
1
- {"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { CHECKLIST_BLOCK } from './checklist-config';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// One entry the AI writes into review.json's `checklists[]`: \"I read the doc for <id> and walked it\".\n// acknowledged: true is the AI attesting it did the walk — a SURFACING/AUDIT signal, not authorization.\nexport class ChecklistAck {\n id: string;\n acknowledged: boolean;\n notes: string[]; // per-item findings the AI chose to record (optional; [] when none)\n\n constructor(id: string, acknowledged: boolean, notes: string[]) {\n this.id = id;\n this.acknowledged = acknowledged;\n this.notes = notes;\n }\n}\n\n// What the CALLER (the pr-gate command) computed from the diff: the checklists this branch triggered.\n// Drives BOTH review.json validation (BLOCK must be acknowledged) AND the printed schema hint (so the\n// AI is told, at the moment it writes review.json, exactly which docs to read). Data-only.\nexport class RequiredChecklist {\n id: string;\n title: string;\n severity: string; // 'BLOCK' | 'WARN'\n docs: string[];\n blockMessage: string;\n matchedFiles: string[]; // the changed files that triggered it (for the dashboard + hint)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, title: string, severity: string, docs: string[], blockMessage: string, matchedFiles: string[]) {\n this.id = id;\n this.title = title;\n this.severity = severity;\n this.docs = docs;\n this.blockMessage = blockMessage;\n this.matchedFiles = matchedFiles;\n }\n}\n\n// The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). 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 checklists: ChecklistAck[]; // consumer-checklist acknowledgments; [] when no checklists were required\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 checklists: ChecklistAck[] = [],\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.checklists = checklists;\n }\n}\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Copy-paste schema both commands print (write it / fix it). `required` is the set of consumer\n // checklists the diff triggered; when it is empty the output is byte-identical to before this\n // feature existed (non-adopting repos see no change). When non-empty it grows a `checklists` line\n // in the JSON shape PLUS an instruction block naming the docs to read — diff-derived instructions\n // injected at exactly the moment the AI writes review.json.\n reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n const checklistLine = required.length > 0\n ? `,\\n \"checklists\": [{ \"id\": \"<id from the list below>\", \"acknowledged\": true, \"notes\": [\"what you checked\"] }]\\n`\n : `\\n`;\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]` +\n checklistLine +\n `}` +\n this.requiredChecklistHint(required)\n );\n }\n\n // The diff-triggered instruction block, appended ONLY when the branch triggered a checklist. This\n // is the consumer's review process, re-injected: read doc Y because the diff touched X. BLOCK\n // entries must be acknowledged in `checklists[]` or wp-finish-upsert-pr refuses to open the PR.\n private requiredChecklistHint(required: readonly RequiredChecklist[]): string {\n if (required.length === 0) return '';\n const lines: string[] = ['', '', 'This branch triggered company review checklist(s). BEFORE writing review.json, READ each'];\n lines.push('doc, walk its items against your diff, then add a `checklists[]` entry acknowledging it:');\n lines.push('');\n for (const req of required) {\n const gate = req.severity === CHECKLIST_BLOCK\n ? 'BLOCK — the PR will NOT open until you acknowledge it'\n : 'WARN — acknowledge if it applies (never blocks)';\n lines.push(` • [${req.id}] ${req.title} (${gate})`);\n lines.push(` docs to read: ${req.docs.join(', ')}`);\n if (req.blockMessage.trim() !== '') lines.push(` ${req.blockMessage.trim()}`);\n if (req.matchedFiles.length > 0) lines.push(` triggered by: ${req.matchedFiles.slice(0, 5).join(', ')}`);\n }\n return lines.join('\\n');\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.\n *\n * `required` is the set of consumer checklists the diff triggered (empty for non-adopting repos, in\n * which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in\n * review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside\n * the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in\n * `checklists[]` are ignored (forward-compat).\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const acks = this.parseChecklistAcks(raw['checklists']);\n for (const err of this.requiredChecklistErrors(required, acks)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n acks,\n );\n }\n\n // Parse the AI-authored `checklists[]` into typed ChecklistAck[]. Tolerant of a missing/garbage\n // field (→ []) and of non-object entries (skipped) — malformed acks simply fail to satisfy a BLOCK\n // requirement rather than crashing the load.\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed here\n private parseChecklistAcks(value: unknown): ChecklistAck[] {\n if (!Array.isArray(value)) return [];\n const acks: ChecklistAck[] = [];\n for (const entry of value) {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue;\n // webpieces-disable no-any-unknown -- one opaque ack entry, narrowed field-by-field\n const e = entry as Record<string, unknown>;\n const id = typeof e['id'] === 'string' ? (e['id'] as string) : '';\n if (id === '') continue;\n acks.push(new ChecklistAck(id, e['acknowledged'] === true, this.asStringArray(e['notes'])));\n }\n return acks;\n }\n\n // BLOCK requirements that are not acknowledged → one error each, using the consumer's blockMessage\n // verbatim so the consumer owns the wording and webpieces owns the mechanism.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], acks: readonly ChecklistAck[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n if (req.severity !== CHECKLIST_BLOCK) continue;\n const ack = acks.find((a: ChecklistAck): boolean => a.id === req.id);\n if (!ack || !ack.acknowledged) {\n const docs = req.docs.length > 0 ? ` Read: ${req.docs.join(', ')}.` : '';\n const msg = req.blockMessage.trim() !== '' ? `${req.blockMessage.trim()} ` : '';\n errors.push(\n `Checklist \"${req.id}\" (${req.title}) is REQUIRED for this diff but not acknowledged. ${msg}` +\n `Add {\"id\":\"${req.id}\",\"acknowledged\":true,\"notes\":[...]} to \"checklists\" once you have walked it.${docs}`,\n );\n }\n }\n return errors;\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\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(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath, required);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
1
+ {"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { CHECKLIST_BLOCK } from './checklist-config';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// One entry the AI writes into review.json's `checklists[]`: \"I read the doc for <id> and walked it\".\n// acknowledged: true is the AI attesting it did the walk — a SURFACING/AUDIT signal, not authorization.\nexport class ChecklistAck {\n id: string;\n acknowledged: boolean;\n notes: string[]; // per-item findings the AI chose to record (optional; [] when none)\n\n constructor(id: string, acknowledged: boolean, notes: string[]) {\n this.id = id;\n this.acknowledged = acknowledged;\n this.notes = notes;\n }\n}\n\n// The verdict a reviewer wrote into a PER-CHECKLIST file `.webpieces/pr-review/<branch>/review-<id>.json`,\n// one per triggered checklist. Replaces the single shared `checklists[]` array so concurrent reviewer\n// subagents never clobber one file. Unlike ChecklistAck's bare boolean, this records the OUTCOME:\n// success:true → PASS\n// success:false + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// success:false + no override → FAIL (refuse; `output` is printed verbatim)\n// `override` is deliberately free text, not a boolean — it forces the bypass to be stated in words and\n// surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).\nexport class ChecklistResult {\n id: string;\n success: boolean;\n output: string; // what the reviewer found; printed verbatim when a BLOCK checklist fails\n override: string; // '' = no override; non-empty = ship-anyway justification (renders 🟡 overridden)\n\n constructor(id: string, success: boolean, output: string, override: string) {\n this.id = id;\n this.success = success;\n this.output = output;\n this.override = override;\n }\n}\n\n// What the CALLER (the pr-gate command) computed from the diff: the checklists this branch triggered.\n// Drives BOTH review.json validation (BLOCK must be acknowledged) AND the printed schema hint (so the\n// AI is told, at the moment it writes review.json, exactly which docs to read). Data-only.\nexport class RequiredChecklist {\n id: string;\n title: string;\n severity: string; // 'BLOCK' | 'WARN'\n docs: string[];\n blockMessage: string;\n matchedFiles: string[]; // the changed files that triggered it (for the dashboard + hint)\n subagent: string; // expected reviewer agentType ('' = no provenance requirement); enforced by finish\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, title: string, severity: string, docs: string[], blockMessage: string, matchedFiles: string[], subagent = '') {\n this.id = id;\n this.title = title;\n this.severity = severity;\n this.docs = docs;\n this.blockMessage = blockMessage;\n this.matchedFiles = matchedFiles;\n this.subagent = subagent;\n }\n}\n\n// The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). 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 checklists: ChecklistAck[]; // legacy inline acknowledgments (back-compat); [] when none written\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 checklists: ChecklistAck[] = [],\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.checklists = checklists;\n this.results = results;\n }\n}\n\n// A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.\nexport const CK_PASS = 'pass'; // review-<id>.json success:true\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json success:false + non-empty override → 🟡\nexport const CK_FAIL = 'fail'; // review-<id>.json success:false + no override → refuse\nexport const CK_MISSING = 'missing'; // no verdict file and no inline ack → refuse (BLOCK)\nexport const CK_ACKED = 'acknowledged'; // legacy inline checklists[] ack satisfied (back-compat)\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_ACKED\n detail: string; // reviewer output / override justification / ack notes (for the 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\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Copy-paste schema both commands print (write it / fix it). `required` is the set of consumer\n // checklists the diff triggered; when it is empty the output is byte-identical to before this\n // feature existed (non-adopting repos see no change). When non-empty it grows a `checklists` line\n // in the JSON shape PLUS an instruction block naming the docs to read — diff-derived instructions\n // injected at exactly the moment the AI writes review.json.\n reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}` +\n this.requiredChecklistHint(filePath, required)\n );\n }\n\n // The per-checklist review file path that sits beside review.json: review-<id>.json.\n checklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);\n }\n\n // The diff-triggered instruction block, appended ONLY when the branch triggered a checklist. This is\n // the consumer's review process, re-injected: read doc Y because the diff touched X, then write ONE\n // file per checklist — `review-<id>.json` — so concurrent reviewer subagents never clobber a shared\n // file. A BLOCK checklist with success:false and no override refuses to open the PR.\n private requiredChecklistHint(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): string {\n if (required.length === 0) return '';\n const lines: string[] = ['', '', 'This branch triggered company review checklist(s). For EACH one below: READ its docs, walk'];\n lines.push('the items against your diff, then write a SEPARATE file `review-<id>.json` beside review.json');\n lines.push('with this shape (override optional — a free-text ship-anyway justification):');\n lines.push('');\n lines.push(' { \"success\": true, \"output\": \"what you checked / what you found\", \"override\": \"\" }');\n lines.push('');\n for (const req of required) {\n const gate = req.severity === CHECKLIST_BLOCK\n ? 'BLOCK — success:false with no \"override\" will NOT open the PR'\n : 'WARN — recorded on the dashboard; never blocks';\n lines.push(` • [${req.id}] ${req.title} (${gate})`);\n lines.push(` write: ${this.checklistResultPath(reviewJsonFilePath, req.id)}`);\n lines.push(` docs to read: ${req.docs.join(', ')}`);\n if (req.subagent.trim() !== '') lines.push(` must be reviewed by the \"${req.subagent.trim()}\" subagent (its independent run is verified from the harness).`);\n if (req.blockMessage.trim() !== '') lines.push(` ${req.blockMessage.trim()}`);\n if (req.matchedFiles.length > 0) lines.push(` triggered by: ${req.matchedFiles.slice(0, 5).join(', ')}`);\n }\n return lines.join('\\n');\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.\n *\n * `required` is the set of consumer checklists the diff triggered (empty for non-adopting repos, in\n * which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in\n * review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside\n * the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in\n * `checklists[]` are ignored (forward-compat).\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath, required);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const acks = this.parseChecklistAcks(raw['checklists']);\n const results = this.loadChecklistResults(filePath, required);\n for (const err of this.requiredChecklistErrors(required, acks, results)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n acks,\n results,\n );\n }\n\n // Parse the AI-authored `checklists[]` into typed ChecklistAck[]. Tolerant of a missing/garbage\n // field (→ []) and of non-object entries (skipped) — malformed acks simply fail to satisfy a BLOCK\n // requirement rather than crashing the load.\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed here\n private parseChecklistAcks(value: unknown): ChecklistAck[] {\n if (!Array.isArray(value)) return [];\n const acks: ChecklistAck[] = [];\n for (const entry of value) {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue;\n // webpieces-disable no-any-unknown -- one opaque ack entry, narrowed field-by-field\n const e = entry as Record<string, unknown>;\n const id = typeof e['id'] === 'string' ? (e['id'] as string) : '';\n if (id === '') continue;\n acks.push(new ChecklistAck(id, e['acknowledged'] === true, this.asStringArray(e['notes'])));\n }\n return acks;\n }\n\n // Read the per-checklist verdict files `review-<id>.json` that sit beside review.json — one per\n // triggered checklist. Tolerant like parseChecklistAcks: a missing file is simply absent from the\n // result (→ falls back to the inline ack / counts as unmet for a BLOCK), and a malformed one is\n // skipped (an unknown/stale review-<id>.json never wedges the branch).\n loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[] {\n const results: ChecklistResult[] = [];\n for (const req of required) {\n const p = this.checklistResultPath(reviewJsonFilePath, req.id);\n if (!fs.existsSync(p)) continue;\n const parsed = this.parseChecklistResult(p, req.id);\n if (parsed) results.push(parsed);\n }\n return results;\n }\n\n // Resolve ONE checklist's verdict from its per-file result (preferred) or its legacy inline ack.\n // Central so review.json enforcement AND the finish-command dashboard agree on the outcome.\n resolveVerdict(req: RequiredChecklist, acks: readonly ChecklistAck[], results: readonly ChecklistResult[]): ChecklistVerdict {\n const result = results.find((r: ChecklistResult): boolean => r.id === req.id);\n if (result) {\n if (result.success) return new ChecklistVerdict(req.id, CK_PASS, result.output);\n if (result.override.trim() !== '') return new ChecklistVerdict(req.id, CK_OVERRIDDEN, result.override.trim());\n return new ChecklistVerdict(req.id, CK_FAIL, result.output);\n }\n const ack = acks.find((a: ChecklistAck): boolean => a.id === req.id);\n if (ack && ack.acknowledged) return new ChecklistVerdict(req.id, CK_ACKED, ack.notes.join('; '));\n return new ChecklistVerdict(req.id, CK_MISSING, '');\n }\n\n // BLOCK requirements whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no\n // verdict written at all) → one error each. WARN never blocks. Uses the consumer's blockMessage /\n // the reviewer's `output` verbatim so the consumer owns the wording and webpieces owns the mechanism.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], acks: readonly ChecklistAck[], results: readonly ChecklistResult[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n if (req.severity !== CHECKLIST_BLOCK) continue;\n const verdict = this.resolveVerdict(req, acks, results);\n if (verdict.status === CK_FAIL) {\n const msg = req.blockMessage.trim() !== '' ? `${req.blockMessage.trim()} ` : '';\n errors.push(\n `Checklist \"${req.id}\" (${req.title}) FAILED review. ${msg}` +\n `The reviewer wrote:\\n ${verdict.detail.split('\\n').join('\\n ')}\\n` +\n ` Fix it, then re-run; or set a non-empty \"override\" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`,\n );\n } else if (verdict.status === CK_MISSING) {\n const docs = req.docs.length > 0 ? ` Read: ${req.docs.join(', ')}.` : '';\n const msg = req.blockMessage.trim() !== '' ? `${req.blockMessage.trim()} ` : '';\n errors.push(\n `Checklist \"${req.id}\" (${req.title}) is REQUIRED for this diff but has no verdict. ${msg}` +\n `Write ${this.checklistFileName(req.id)} with {\"success\":true,\"output\":\"…\"} once you have walked it.${docs}`,\n );\n }\n }\n return errors;\n }\n\n private checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n\n // Parse one review-<id>.json into a ChecklistResult, or null when malformed. Tolerant: missing\n // `success` counts as false (fail-closed), `output`/`override` default to ''.\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field\n private parseChecklistResult(filePath: string, id: string): ChecklistResult | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed per-checklist file is skipped, not fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const success = raw['success'] === true;\n const output = typeof raw['output'] === 'string' ? (raw['output'] as string) : '';\n const override = typeof raw['override'] === 'string' ? (raw['override'] as string) : '';\n return new ChecklistResult(id, success, output, override);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError. `required`\n // is threaded through so a JSON syntax error still prints the checklist instructions the AI needs —\n // exactly when it most needs them — rather than the bare schema.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string, required: readonly RequiredChecklist[]): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\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(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath, required)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath, required);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
@@ -0,0 +1,39 @@
1
+ export declare const PROVENANCE_OK = "ok";
2
+ export declare const PROVENANCE_MISSING = "missing";
3
+ export declare const PROVENANCE_SKIPPED = "skipped";
4
+ export declare class ProvenanceResult {
5
+ status: string;
6
+ detail: string;
7
+ constructor(status: string, detail: string);
8
+ }
9
+ /**
10
+ * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
11
+ * a subagent of a given `agentType` actually ran during the current session on the current branch. Used
12
+ * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than
13
+ * the coding agent self-certifying.
14
+ *
15
+ * The harness writes, beside each subagent transcript:
16
+ * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
17
+ * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
18
+ * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir
19
+ * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be
20
+ * derived/guessed.
21
+ *
22
+ * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
23
+ * raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
24
+ * is not cryptographic. Say so wherever `subagent:` is documented.
25
+ *
26
+ * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
27
+ */
28
+ export declare class SubagentProvenanceService {
29
+ verify(expectedAgentType: string, branch: string): ProvenanceResult;
30
+ private findSubagentsDir;
31
+ private metaFiles;
32
+ private agentIdOf;
33
+ private sidechainOnBranch;
34
+ private stripWp;
35
+ private readDir;
36
+ private firstNonEmptyLine;
37
+ private readJson;
38
+ private parseLine;
39
+ }
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SubagentProvenanceService = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const os = tslib_1.__importStar(require("os"));
8
+ const inversify_1 = require("inversify");
9
+ const to_error_1 = require("./to-error");
10
+ // Outcome of a provenance check.
11
+ exports.PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch
12
+ exports.PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)
13
+ exports.PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass
14
+ class ProvenanceResult {
15
+ status; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED
16
+ detail; // human-readable explanation for the warning/error/dashboard
17
+ constructor(status, detail) {
18
+ this.status = status;
19
+ this.detail = detail;
20
+ }
21
+ }
22
+ exports.ProvenanceResult = ProvenanceResult;
23
+ /**
24
+ * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
25
+ * a subagent of a given `agentType` actually ran during the current session on the current branch. Used
26
+ * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than
27
+ * the coding agent self-certifying.
28
+ *
29
+ * The harness writes, beside each subagent transcript:
30
+ * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
31
+ * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
32
+ * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir
33
+ * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be
34
+ * derived/guessed.
35
+ *
36
+ * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
37
+ * raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
38
+ * is not cryptographic. Say so wherever `subagent:` is documented.
39
+ *
40
+ * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
41
+ */
42
+ let SubagentProvenanceService = class SubagentProvenanceService {
43
+ // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain
44
+ // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.
45
+ verify(expectedAgentType, branch) {
46
+ const sessionId = process.env['CLAUDE_CODE_SESSION_ID'] ?? '';
47
+ if (sessionId.trim() === '') {
48
+ return new ProvenanceResult(exports.PROVENANCE_SKIPPED, `CLAUDE_CODE_SESSION_ID not set — cannot verify the "${expectedAgentType}" reviewer subagent (plain terminal / CI). Skipping the provenance check.`);
49
+ }
50
+ const dir = this.findSubagentsDir(sessionId);
51
+ if (dir === '') {
52
+ return new ProvenanceResult(exports.PROVENANCE_MISSING, `No subagents recorded for this session — the "${expectedAgentType}" reviewer subagent must run on this branch before the PR can open.`);
53
+ }
54
+ for (const metaFile of this.metaFiles(dir)) {
55
+ const meta = this.readJson(path.join(dir, metaFile));
56
+ if (!meta)
57
+ continue;
58
+ if (meta['agentType'] !== expectedAgentType)
59
+ continue;
60
+ const spawnDepth = meta['spawnDepth'];
61
+ if (typeof spawnDepth !== 'number' || spawnDepth < 1)
62
+ continue;
63
+ const agentId = this.agentIdOf(metaFile);
64
+ if (this.sidechainOnBranch(dir, agentId, branch)) {
65
+ return new ProvenanceResult(exports.PROVENANCE_OK, `verified "${expectedAgentType}" reviewer subagent ran (agent ${agentId}).`);
66
+ }
67
+ }
68
+ return new ProvenanceResult(exports.PROVENANCE_MISSING, `No "${expectedAgentType}" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);
69
+ }
70
+ // Find the subagents dir for `sessionId` by its UNIQUE id, so the cwd-slug never has to be guessed.
71
+ findSubagentsDir(sessionId) {
72
+ const projects = path.join(os.homedir(), '.claude', 'projects');
73
+ if (!fs.existsSync(projects))
74
+ return '';
75
+ for (const proj of this.readDir(projects)) {
76
+ const candidate = path.join(projects, proj, sessionId, 'subagents');
77
+ if (fs.existsSync(candidate))
78
+ return candidate;
79
+ }
80
+ return '';
81
+ }
82
+ metaFiles(dir) {
83
+ return this.readDir(dir).filter((f) => f.endsWith('.meta.json'));
84
+ }
85
+ // agent-<id>.meta.json → <id>
86
+ agentIdOf(metaFile) {
87
+ return metaFile.replace(/^agent-/, '').replace(/\.meta\.json$/, '');
88
+ }
89
+ // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main
90
+ // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a
91
+ // mid-flight branch rename) and on a missing jsonl (meta already matched — accept, best-effort).
92
+ sidechainOnBranch(dir, agentId, branch) {
93
+ const jsonl = path.join(dir, `agent-${agentId}.jsonl`);
94
+ if (!fs.existsSync(jsonl))
95
+ return true;
96
+ const first = this.firstNonEmptyLine(jsonl);
97
+ if (first === '')
98
+ return true;
99
+ const rec = this.parseLine(first);
100
+ if (!rec)
101
+ return true;
102
+ if (rec['isSidechain'] !== true)
103
+ return false;
104
+ const gitBranch = rec['gitBranch'];
105
+ if (typeof gitBranch !== 'string' || gitBranch === '')
106
+ return true;
107
+ return this.stripWp(gitBranch) === this.stripWp(branch);
108
+ }
109
+ stripWp(branch) {
110
+ return branch.replace(/-?wp\d+$/i, '');
111
+ }
112
+ readDir(dir) {
113
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)
114
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
115
+ try {
116
+ return fs.readdirSync(dir);
117
+ }
118
+ catch (err) {
119
+ const error = (0, to_error_1.toError)(err);
120
+ void error;
121
+ return [];
122
+ }
123
+ }
124
+ firstNonEmptyLine(filePath) {
125
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)
126
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
127
+ try {
128
+ for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
129
+ if (line.trim() !== '')
130
+ return line;
131
+ }
132
+ return '';
133
+ }
134
+ catch (err) {
135
+ const error = (0, to_error_1.toError)(err);
136
+ void error;
137
+ return '';
138
+ }
139
+ }
140
+ // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller
141
+ readJson(filePath) {
142
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)
143
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
144
+ try {
145
+ // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller
146
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
147
+ }
148
+ catch (err) {
149
+ const error = (0, to_error_1.toError)(err);
150
+ void error;
151
+ return null;
152
+ }
153
+ }
154
+ // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller
155
+ parseLine(line) {
156
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null
157
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
158
+ try {
159
+ // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller
160
+ return JSON.parse(line);
161
+ }
162
+ catch (err) {
163
+ const error = (0, to_error_1.toError)(err);
164
+ void error;
165
+ return null;
166
+ }
167
+ }
168
+ };
169
+ exports.SubagentProvenanceService = SubagentProvenanceService;
170
+ exports.SubagentProvenanceService = SubagentProvenanceService = tslib_1.__decorate([
171
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
172
+ ], SubagentProvenanceService);
173
+ //# sourceMappingURL=subagent-provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n\n constructor(status: string, detail: string) {\n this.status = status;\n this.detail = detail;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n const sessionId = process.env['CLAUDE_CODE_SESSION_ID'] ?? '';\n if (sessionId.trim() === '') {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify the \"${expectedAgentType}\" reviewer subagent (plain terminal / CI). Skipping the provenance check.`);\n }\n const dir = this.findSubagentsDir(sessionId);\n if (dir === '') {\n return new ProvenanceResult(PROVENANCE_MISSING,\n `No subagents recorded for this session — the \"${expectedAgentType}\" reviewer subagent must run on this branch before the PR can open.`);\n }\n for (const metaFile of this.metaFiles(dir)) {\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta) continue;\n if (meta['agentType'] !== expectedAgentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n const agentId = this.agentIdOf(metaFile);\n if (this.sidechainOnBranch(dir, agentId, branch)) {\n return new ProvenanceResult(PROVENANCE_OK,\n `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`);\n }\n }\n return 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 // Find the subagents dir for `sessionId` by its UNIQUE id, so the cwd-slug never has to be guessed.\n private findSubagentsDir(sessionId: string): string {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return '';\n for (const proj of this.readDir(projects)) {\n const candidate = path.join(projects, proj, sessionId, 'subagents');\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\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"]}
@@ -8,6 +8,7 @@ export declare function validateWebpiecesConfig(rawRules: Record<string, Record<
8
8
  * `repoRoot` (when known) lets the `checklists[].docs` existence check run.
9
9
  */
10
10
  export declare function validatePrGateSection(section: unknown, repoRoot?: string): string[];
11
+ export declare function validateChecklists(value: unknown, repoRoot?: string): string[];
11
12
  /**
12
13
  * Validate the REQUIRED top-level `excludePaths` block: two independent glob lists (`rules`,
13
14
  * `guards`) that suppress hook enforcement per file path. Required so every client upgrading is
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.allRuleNames = allRuleNames;
4
4
  exports.validateWebpiecesConfig = validateWebpiecesConfig;
5
5
  exports.validatePrGateSection = validatePrGateSection;
6
+ exports.validateChecklists = validateChecklists;
6
7
  exports.validateExcludePaths = validateExcludePaths;
7
8
  exports.validateMatchRulesSection = validateMatchRulesSection;
8
9
  exports.validateSectionPlacement = validateSectionPlacement;
@@ -16,6 +17,7 @@ const checklist_config_1 = require("./checklist-config");
16
17
  const match_rules_config_1 = require("./match-rules-config");
17
18
  const to_error_1 = require("./to-error");
18
19
  const rule_configs_2 = require("./rule-configs");
20
+ const no_client_creation_config_1 = require("./no-client-creation-config");
19
21
  // Thin lookup table — each entry delegates to the class's own SCHEMA.
20
22
  // No field lists here; all schemas live with their config class.
21
23
  const RULE_SCHEMAS = {
@@ -33,6 +35,7 @@ const RULE_SCHEMAS = {
33
35
  'throw-cause-required': rule_configs_2.ThrowCauseRequiredConfig.SCHEMA,
34
36
  'angular-no-direct-api-in-resolver': rule_configs_2.AngularNoDirectApiInResolverConfig.SCHEMA,
35
37
  'no-symbol-di-tokens': rule_configs_2.NoSymbolDiTokensConfig.SCHEMA,
38
+ 'no-client-creation-outside-server-or-client': no_client_creation_config_1.NoClientCreationOutsideServerOrClientConfig.SCHEMA,
36
39
  'no-custom-css': rule_configs_2.NoCustomCssConfig.SCHEMA,
37
40
  'no-process-exit-outside-main': rule_configs_2.NoProcessExitOutsideMainConfig.SCHEMA,
38
41
  'no-function-outside-class': rule_configs_2.NoFunctionOutsideClassConfig.SCHEMA,
@@ -290,6 +293,8 @@ function validateChecklist(entry, index, repoRoot) {
290
293
  errors.push(`[pr-gate] ${label}.blockMessage must be a string.`);
291
294
  if (c['disabled'] !== undefined && typeof c['disabled'] !== 'boolean')
292
295
  errors.push(`[pr-gate] ${label}.disabled must be a boolean (example/inactive checklist kept in the file).`);
296
+ if (c['subagent'] !== undefined && typeof c['subagent'] !== 'string')
297
+ errors.push(`[pr-gate] ${label}.subagent must be a string (the expected reviewer agentType, e.g. "morpheus-reviewer"; omit for no provenance requirement).`);
293
298
  return errors;
294
299
  }
295
300
  /**
@@ -352,9 +357,18 @@ function validatePrGateSection(section, repoRoot) {
352
357
  // behavior change. Present ⇒ every entry validated field-by-field, ids unique across the array.
353
358
  if ('checklists' in s)
354
359
  errors.push(...validateChecklists(s['checklists'], repoRoot));
360
+ // Optional server-token salt. Absent ⇒ no token minted, CI enforcement is a no-op. Present ⇒ must be
361
+ // a non-empty string (an empty salt would mint a token anyone can forge from a known-empty secret).
362
+ if ('gateSalt' in s) {
363
+ const salt = s['gateSalt'];
364
+ if (typeof salt !== 'string' || salt.trim() === '') {
365
+ errors.push(`[pr-gate] "gateSalt" must be a non-empty string — it is the shared secret the gate token is HMAC'd with. Omit the key entirely to disable server-side token enforcement.`);
366
+ }
367
+ }
355
368
  return errors;
356
369
  }
357
370
  // The `checklists` array of a pr-gate section: each entry validated field-by-field, ids unique.
371
+ // Exported so the isolated validate-checklist-docs target (checklist-docs-validator.ts) reuses it.
358
372
  // webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below
359
373
  // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
360
374
  function validateChecklists(value, repoRoot) {