@webpieces/rules-config 0.4.476 → 0.4.478
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.478",
|
|
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",
|
|
@@ -28,8 +28,10 @@ export declare class ProvenanceResult {
|
|
|
28
28
|
export declare class SubagentProvenanceService {
|
|
29
29
|
verify(expectedAgentType: string, branch: string): ProvenanceResult;
|
|
30
30
|
verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult;
|
|
31
|
+
private inClaudeSession;
|
|
32
|
+
private skipped;
|
|
31
33
|
private findMatchingAgentId;
|
|
32
|
-
private
|
|
34
|
+
private allSubagentsDirs;
|
|
33
35
|
private metaFiles;
|
|
34
36
|
private agentIdOf;
|
|
35
37
|
private sidechainOnBranch;
|
|
@@ -43,36 +43,31 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
43
43
|
// Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain
|
|
44
44
|
// terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.
|
|
45
45
|
verify(expectedAgentType, branch) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
const agentId = this.findMatchingAgentId(dir, expectedAgentType, branch);
|
|
46
|
+
if (!this.inClaudeSession())
|
|
47
|
+
return this.skipped(`the "${expectedAgentType}" reviewer subagent`);
|
|
48
|
+
const dirs = this.allSubagentsDirs();
|
|
49
|
+
const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);
|
|
55
50
|
return agentId !== ''
|
|
56
51
|
? new ProvenanceResult(exports.PROVENANCE_OK, `verified "${expectedAgentType}" reviewer subagent ran (agent ${agentId}).`)
|
|
57
52
|
: new ProvenanceResult(exports.PROVENANCE_MISSING, `No "${expectedAgentType}" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);
|
|
58
53
|
}
|
|
59
54
|
// Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not
|
|
60
55
|
// self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.
|
|
56
|
+
//
|
|
57
|
+
// Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in
|
|
58
|
+
// any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.
|
|
59
|
+
// That is what keeps "review once per branch" true across sessions. A PR opened outside the gated flow
|
|
60
|
+
// still has no review-<id>.json, so wp-finish forces the review regardless of provenance.
|
|
61
61
|
verifyDistinct(expectedAgentTypes, branch) {
|
|
62
62
|
if (expectedAgentTypes.length === 0)
|
|
63
63
|
return new ProvenanceResult(exports.PROVENANCE_OK, 'no reviewer subagents required');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
const dir = this.findSubagentsDir(sessionId);
|
|
69
|
-
if (dir === '') {
|
|
70
|
-
return new ProvenanceResult(exports.PROVENANCE_MISSING, `No subagents recorded for this session — each checklist must be reviewed by its own subagent: ${expectedAgentTypes.join(', ')}.`);
|
|
71
|
-
}
|
|
64
|
+
if (!this.inClaudeSession())
|
|
65
|
+
return this.skipped('reviewer subagents');
|
|
66
|
+
const dirs = this.allSubagentsDirs();
|
|
72
67
|
const missing = [];
|
|
73
68
|
const usedAgentIds = new Set();
|
|
74
69
|
for (const type of expectedAgentTypes) {
|
|
75
|
-
const agentId = this.findMatchingAgentId(
|
|
70
|
+
const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);
|
|
76
71
|
if (agentId === '')
|
|
77
72
|
missing.push(type);
|
|
78
73
|
else
|
|
@@ -82,35 +77,50 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
82
77
|
? new ProvenanceResult(exports.PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`)
|
|
83
78
|
: new ProvenanceResult(exports.PROVENANCE_MISSING, `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`);
|
|
84
79
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
// Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)
|
|
81
|
+
inClaudeSession() {
|
|
82
|
+
return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';
|
|
83
|
+
}
|
|
84
|
+
skipped(what) {
|
|
85
|
+
return new ProvenanceResult(exports.PROVENANCE_SKIPPED, `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);
|
|
86
|
+
}
|
|
87
|
+
// The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'
|
|
88
|
+
// subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`
|
|
89
|
+
// skips agentIds already credited to another checklist so one run can't satisfy two.
|
|
90
|
+
findMatchingAgentId(dirs, agentType, branch, exclude = new Set()) {
|
|
91
|
+
for (const dir of dirs) {
|
|
92
|
+
for (const metaFile of this.metaFiles(dir)) {
|
|
93
|
+
const agentId = this.agentIdOf(metaFile);
|
|
94
|
+
if (exclude.has(agentId))
|
|
95
|
+
continue;
|
|
96
|
+
const meta = this.readJson(path.join(dir, metaFile));
|
|
97
|
+
if (!meta || meta['agentType'] !== agentType)
|
|
98
|
+
continue;
|
|
99
|
+
const spawnDepth = meta['spawnDepth'];
|
|
100
|
+
if (typeof spawnDepth !== 'number' || spawnDepth < 1)
|
|
101
|
+
continue;
|
|
102
|
+
if (this.sidechainOnBranch(dir, agentId, branch))
|
|
103
|
+
return agentId;
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
return '';
|
|
102
107
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
108
|
+
// Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by
|
|
109
|
+
// session id) is what makes provenance survive across sessions.
|
|
110
|
+
allSubagentsDirs() {
|
|
105
111
|
const projects = path.join(os.homedir(), '.claude', 'projects');
|
|
106
112
|
if (!fs.existsSync(projects))
|
|
107
|
-
return
|
|
113
|
+
return [];
|
|
114
|
+
const out = [];
|
|
108
115
|
for (const proj of this.readDir(projects)) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
const projDir = path.join(projects, proj);
|
|
117
|
+
for (const session of this.readDir(projDir)) {
|
|
118
|
+
const candidate = path.join(projDir, session, 'subagents');
|
|
119
|
+
if (fs.existsSync(candidate))
|
|
120
|
+
out.push(candidate);
|
|
121
|
+
}
|
|
112
122
|
}
|
|
113
|
-
return
|
|
123
|
+
return out;
|
|
114
124
|
}
|
|
115
125
|
metaFiles(dir) {
|
|
116
126
|
return this.readDir(dir).filter((f) => f.endsWith('.meta.json'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-provenance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/subagent-provenance.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { toError } from './to-error';\n\n// Outcome of a provenance check.\nexport const PROVENANCE_OK = 'ok'; // a matching reviewer subagent demonstrably ran on this branch\nexport const PROVENANCE_MISSING = 'missing'; // session known, but no matching subagent artifact found → refuse (BLOCK)\nexport const PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain terminal / CI) → warn + pass\n\nexport class ProvenanceResult {\n status: string; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED\n detail: string; // human-readable explanation for the warning/error/dashboard\n\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/*/<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 const agentId = this.findMatchingAgentId(dir, 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 verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\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 reviewer subagents ran (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 — each checklist must be reviewed by its own subagent: ${expectedAgentTypes.join(', ')}.`);\n }\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dir, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else usedAgentIds.add(agentId);\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`)\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 }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, or '' if none. `exclude` skips\n // agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dir: string, agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\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 return '';\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"]}
|
|
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/*/<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 for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else usedAgentIds.add(agentId);\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`)\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 }\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"]}
|
|
@@ -9,12 +9,17 @@
|
|
|
9
9
|
# TO ENABLE (client repo):
|
|
10
10
|
# 1. Copy this file to .github/workflows/webpieces-pr-gate.yml and commit it.
|
|
11
11
|
# 2. Set a committed "gateSalt" under the pr-gate section of webpieces.config.json (any non-empty string).
|
|
12
|
-
# 3. In branch-protection for `main`, mark
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
12
|
+
# 3. In branch-protection for `main`, mark THIS WORKFLOW'S JOB required:
|
|
13
|
+
# Webpieces PR Gate / Verify webpieces gate token
|
|
14
|
+
# That is the ONE required check — there is deliberately nothing else to pick. (Only a repo admin
|
|
15
|
+
# can set required checks; webpieces cannot do it for you.)
|
|
16
|
+
#
|
|
17
|
+
# NOTE if you are upgrading: earlier versions ALSO posted a `webpieces/pr-gate` commit status, and told
|
|
18
|
+
# you to require THAT instead of the job. It is gone. It only existed to recover from a race where
|
|
19
|
+
# wp-finish-upsert-pr pushed before writing the PR body — the body is now written FIRST, so the race
|
|
20
|
+
# cannot happen and the job is safe to require on its own. If you had `webpieces/pr-gate` marked required
|
|
21
|
+
# in branch protection, REPOINT it at the job above, or that context sits Pending forever and blocks
|
|
22
|
+
# every PR.
|
|
18
23
|
#
|
|
19
24
|
# NOTE: the salt is committed/obscurity-grade — it stops unhooked teammates, but a determined reader can
|
|
20
25
|
# read it and forge. It is deliberately not cryptographically sound; see RESPONSE-pr-gate-ci-enforcement.
|
|
@@ -26,13 +31,16 @@
|
|
|
26
31
|
name: Webpieces PR Gate
|
|
27
32
|
on:
|
|
28
33
|
pull_request:
|
|
29
|
-
# `edited`
|
|
30
|
-
# push
|
|
34
|
+
# `edited` is LOAD-BEARING, not a fallback — do NOT remove it. wp-start-upsert-pr pushes too, and that
|
|
35
|
+
# push fires `synchronize` while the PR body still carries the PREVIOUS run's token, so the check goes
|
|
36
|
+
# red. By the time wp-finish-upsert-pr runs, HEAD usually has not moved, so its push is a no-op that
|
|
37
|
+
# fires NO event — the only thing that re-triggers this job and clears the red is the `edited` event
|
|
38
|
+
# from finish writing the new body. Drop `edited` and every iteration leaves a stale red.
|
|
39
|
+
# `synchronize` is equally required: it is what catches an unhooked teammate pushing over a gated PR.
|
|
31
40
|
types: [opened, edited, synchronize, reopened]
|
|
32
41
|
permissions:
|
|
33
42
|
contents: read
|
|
34
43
|
pull-requests: read
|
|
35
|
-
statuses: write # wp-check-pr posts the webpieces/pr-gate commit status
|
|
36
44
|
jobs:
|
|
37
45
|
webpieces-pr-gate:
|
|
38
46
|
runs-on: ubuntu-latest
|