@webpieces/rules-config 0.4.786 → 0.4.788

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.
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.specTempDirs = exports.SpecTempDirs = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const os = tslib_1.__importStar(require("os"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const to_error_1 = require("./to-error");
9
+ // ---------------------------------------------------------------------------
10
+ // ONE OWNER FOR EVERY `$TMPDIR` SCRATCH TREE THE TOOLING CREATES.
11
+ //
12
+ // THE DEFECT. Every `packages/tooling/**` spec builds its fixture with
13
+ // `specTempDirs.make('wp-<something>-')` and NONE of them removed it afterwards.
14
+ // Measured on one developer machine: 270,200 abandoned `wp-*` directories out of 286,270 total entries
15
+ // in `$TMPDIR` — 64,828 of them from `vitest.setup.mts` alone, which mints one isolated `$HOME` per test
16
+ // FILE per run. The cost is not disk (every `wp-*` prefix together is under 1 GiB; the `$HOME` dirs are
17
+ // empty by design) — it is INODES and the fact that `$TMPDIR` stops being inspectable: `ls` takes ~30s
18
+ // and `du` takes minutes, which is how this went unnoticed for as long as it did.
19
+ //
20
+ // WHY `$TMPDIR` AND NOT `.webpieces/`, WHICH IS WHERE EVERYTHING ELSE GOES. Because for a TEST fixture
21
+ // the whole point is to be outside the repo, and two separate mechanisms depend on that:
22
+ // - `vitest.setup.mts` mints a throwaway `$HOME` so a spec cannot read the developer's real
23
+ // `~/.webpieces/config.json`. A fake HOME inside the repo defeats its own isolation.
24
+ // - The fixtures `git init` and `git worktree add` INSIDE themselves. A nested git repo under the real
25
+ // repo root is what breaks the nx graph — the same hazard that makes `.claude/worktrees/` a
26
+ // mandatory gitignore entry.
27
+ // So the location was never the bug. The missing cleanup was.
28
+ //
29
+ // WHO ACTUALLY CALLS `reapAll`, AND WHY IT IS NOT A LINE IN EACH SPEC. An `afterAll` written into each
30
+ // of the 212 call sites is a line somebody has to remember, forever, in every new spec — precisely the
31
+ // discipline already demonstrated not to hold here. Instead `vitest.setup.mts` calls `reapAll()` in ONE
32
+ // global `afterAll`, so a call site swaps `fs.mkdtempSync(...)` for `specTempDirs.make(...)` and there
33
+ // is no other half to forget.
34
+ //
35
+ // THE `exit` REAPER BELOW IS THE SECONDARY BELT, NOT THE PRIMARY ONE — and the first cut of this file
36
+ // had that backwards. It relied on `process.on('exit')` alone, and a 145-file tooling run then left
37
+ // exactly 145 `wp-vitest-home-` directories behind: a 100% miss. The cause is `pool: 'forks'` (see
38
+ // vitest.config.mts) — vitest KILLS its workers rather than letting them exit, and a killed process runs
39
+ // no exit handler. The handler stays because it costs one listener and does fire for the non-vitest
40
+ // callers (testkits driven from plain node scripts), but nothing here should depend on it.
41
+ //
42
+ // SO CLEANUP IS BEST-EFFORT BY CONSTRUCTION, at two levels: a worker killed mid-file skips the
43
+ // `afterAll` too. That residue is why `CleanTmp` sweeps aged `wp-*` out of `$TMPDIR` via
44
+ // `TmpScratchSweeper` — belt, braces, and a sweep for what both of them miss.
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Creates `$TMPDIR` scratch directories for specs and testkits, and reaps every one of them when the
48
+ * process exits.
49
+ *
50
+ * Use the shared `specTempDirs` instance — the reaper is registered per-instance, so a caller that
51
+ * constructs its own gets a second `exit` listener and no benefit.
52
+ */
53
+ class SpecTempDirs {
54
+ created = [];
55
+ reaperRegistered = false;
56
+ /**
57
+ * `fs.mkdtempSync` under `os.tmpdir()`, with the path remembered for cleanup.
58
+ *
59
+ * `prefix` keeps the existing `wp-<area>-` convention so a leaked tree still names its creator; the
60
+ * trailing dash matters because mkdtemp appends six random characters directly onto it.
61
+ */
62
+ make(prefix) {
63
+ this.registerReaper();
64
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
65
+ this.created.push(dir);
66
+ return dir;
67
+ }
68
+ /**
69
+ * `make`, with symlinks resolved.
70
+ *
71
+ * macOS hands back `/var/folders/...` from `os.tmpdir()` while `/var` is a symlink to `/private/var`,
72
+ * so a fixture that compares a path it was given against one the code under test computed sees two
73
+ * different strings for the same directory. Specs that do that comparison call this instead.
74
+ */
75
+ makeReal(prefix) {
76
+ return fs.realpathSync(this.make(prefix));
77
+ }
78
+ /**
79
+ * Removes every directory this instance created, then forgets them.
80
+ *
81
+ * Deliberately never throws: it runs from an `exit` handler where a throw would replace a passing
82
+ * test run's exit code with a crash, and a scratch directory that cannot be removed is a leak, not a
83
+ * failure. Callers may also invoke it directly — a long suite that wants its fixtures gone before the
84
+ * end of the run.
85
+ */
86
+ reapAll() {
87
+ const dirs = this.created.splice(0, this.created.length);
88
+ for (const dir of dirs) {
89
+ // webpieces-disable no-unmanaged-exceptions -- chokepoint: this runs from vitest's afterAll and
90
+ // from an exit handler, where a throw would turn a passing run into a crash over a scratch dir
91
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
92
+ try {
93
+ fs.rmSync(dir, { recursive: true, force: true });
94
+ }
95
+ catch (err) {
96
+ const error = (0, to_error_1.toError)(err);
97
+ process.stderr.write(`SpecTempDirs: could not remove ${dir}: ${error.message}\n`);
98
+ }
99
+ }
100
+ }
101
+ // One listener per instance, added lazily so a process that never makes a fixture never registers.
102
+ registerReaper() {
103
+ if (this.reaperRegistered)
104
+ return;
105
+ this.reaperRegistered = true;
106
+ process.on('exit', () => this.reapAll());
107
+ }
108
+ }
109
+ exports.SpecTempDirs = SpecTempDirs;
110
+ // The shared instance — the reaper and the created-list are per-instance, so every caller must use this.
111
+ exports.specTempDirs = new SpecTempDirs();
112
+ //# sourceMappingURL=spec-temp-dirs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-temp-dirs.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/spec-temp-dirs.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,+CAAyB;AACzB,mDAA6B;AAC7B,yCAAqC;AAErC,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,iFAAiF;AACjF,uGAAuG;AACvG,yGAAyG;AACzG,wGAAwG;AACxG,uGAAuG;AACvG,kFAAkF;AAClF,EAAE;AACF,uGAAuG;AACvG,yFAAyF;AACzF,8FAA8F;AAC9F,yFAAyF;AACzF,yGAAyG;AACzG,gGAAgG;AAChG,iCAAiC;AACjC,8DAA8D;AAC9D,EAAE;AACF,uGAAuG;AACvG,uGAAuG;AACvG,wGAAwG;AACxG,uGAAuG;AACvG,8BAA8B;AAC9B,EAAE;AACF,sGAAsG;AACtG,oGAAoG;AACpG,mGAAmG;AACnG,yGAAyG;AACzG,oGAAoG;AACpG,2FAA2F;AAC3F,EAAE;AACF,+FAA+F;AAC/F,yFAAyF;AACzF,8EAA8E;AAC9E,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAa,YAAY;IACJ,OAAO,GAAa,EAAE,CAAC;IAChC,gBAAgB,GAAG,KAAK,CAAC;IAEjC;;;;;OAKG;IACH,IAAI,CAAC,MAAc;QACf,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,MAAc;QACnB,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;OAOG;IACH,OAAO;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACzD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,gGAAgG;YAChG,+FAA+F;YAC/F,8DAA8D;YAC9D,IAAI,CAAC;gBACD,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;gBAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,GAAG,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YACtF,CAAC;QACL,CAAC;IACL,CAAC;IAED,mGAAmG;IAC3F,cAAc;QAClB,IAAI,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAClC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAS,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;CACJ;AAzDD,oCAyDC;AAED,yGAAyG;AAC5F,QAAA,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// ONE OWNER FOR EVERY `$TMPDIR` SCRATCH TREE THE TOOLING CREATES.\n//\n// THE DEFECT. Every `packages/tooling/**` spec builds its fixture with\n// `specTempDirs.make('wp-<something>-')` and NONE of them removed it afterwards.\n// Measured on one developer machine: 270,200 abandoned `wp-*` directories out of 286,270 total entries\n// in `$TMPDIR` — 64,828 of them from `vitest.setup.mts` alone, which mints one isolated `$HOME` per test\n// FILE per run. The cost is not disk (every `wp-*` prefix together is under 1 GiB; the `$HOME` dirs are\n// empty by design) — it is INODES and the fact that `$TMPDIR` stops being inspectable: `ls` takes ~30s\n// and `du` takes minutes, which is how this went unnoticed for as long as it did.\n//\n// WHY `$TMPDIR` AND NOT `.webpieces/`, WHICH IS WHERE EVERYTHING ELSE GOES. Because for a TEST fixture\n// the whole point is to be outside the repo, and two separate mechanisms depend on that:\n// - `vitest.setup.mts` mints a throwaway `$HOME` so a spec cannot read the developer's real\n// `~/.webpieces/config.json`. A fake HOME inside the repo defeats its own isolation.\n// - The fixtures `git init` and `git worktree add` INSIDE themselves. A nested git repo under the real\n// repo root is what breaks the nx graph — the same hazard that makes `.claude/worktrees/` a\n// mandatory gitignore entry.\n// So the location was never the bug. The missing cleanup was.\n//\n// WHO ACTUALLY CALLS `reapAll`, AND WHY IT IS NOT A LINE IN EACH SPEC. An `afterAll` written into each\n// of the 212 call sites is a line somebody has to remember, forever, in every new spec — precisely the\n// discipline already demonstrated not to hold here. Instead `vitest.setup.mts` calls `reapAll()` in ONE\n// global `afterAll`, so a call site swaps `fs.mkdtempSync(...)` for `specTempDirs.make(...)` and there\n// is no other half to forget.\n//\n// THE `exit` REAPER BELOW IS THE SECONDARY BELT, NOT THE PRIMARY ONE — and the first cut of this file\n// had that backwards. It relied on `process.on('exit')` alone, and a 145-file tooling run then left\n// exactly 145 `wp-vitest-home-` directories behind: a 100% miss. The cause is `pool: 'forks'` (see\n// vitest.config.mts) — vitest KILLS its workers rather than letting them exit, and a killed process runs\n// no exit handler. The handler stays because it costs one listener and does fire for the non-vitest\n// callers (testkits driven from plain node scripts), but nothing here should depend on it.\n//\n// SO CLEANUP IS BEST-EFFORT BY CONSTRUCTION, at two levels: a worker killed mid-file skips the\n// `afterAll` too. That residue is why `CleanTmp` sweeps aged `wp-*` out of `$TMPDIR` via\n// `TmpScratchSweeper` — belt, braces, and a sweep for what both of them miss.\n// ---------------------------------------------------------------------------\n\n/**\n * Creates `$TMPDIR` scratch directories for specs and testkits, and reaps every one of them when the\n * process exits.\n *\n * Use the shared `specTempDirs` instance — the reaper is registered per-instance, so a caller that\n * constructs its own gets a second `exit` listener and no benefit.\n */\nexport class SpecTempDirs {\n private readonly created: string[] = [];\n private reaperRegistered = false;\n\n /**\n * `fs.mkdtempSync` under `os.tmpdir()`, with the path remembered for cleanup.\n *\n * `prefix` keeps the existing `wp-<area>-` convention so a leaked tree still names its creator; the\n * trailing dash matters because mkdtemp appends six random characters directly onto it.\n */\n make(prefix: string): string {\n this.registerReaper();\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n this.created.push(dir);\n return dir;\n }\n\n /**\n * `make`, with symlinks resolved.\n *\n * macOS hands back `/var/folders/...` from `os.tmpdir()` while `/var` is a symlink to `/private/var`,\n * so a fixture that compares a path it was given against one the code under test computed sees two\n * different strings for the same directory. Specs that do that comparison call this instead.\n */\n makeReal(prefix: string): string {\n return fs.realpathSync(this.make(prefix));\n }\n\n /**\n * Removes every directory this instance created, then forgets them.\n *\n * Deliberately never throws: it runs from an `exit` handler where a throw would replace a passing\n * test run's exit code with a crash, and a scratch directory that cannot be removed is a leak, not a\n * failure. Callers may also invoke it directly — a long suite that wants its fixtures gone before the\n * end of the run.\n */\n reapAll(): void {\n const dirs = this.created.splice(0, this.created.length);\n for (const dir of dirs) {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: this runs from vitest's afterAll and\n // from an exit handler, where a throw would turn a passing run into a crash over a scratch dir\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(dir, { recursive: true, force: true });\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`SpecTempDirs: could not remove ${dir}: ${error.message}\\n`);\n }\n }\n }\n\n // One listener per instance, added lazily so a process that never makes a fixture never registers.\n private registerReaper(): void {\n if (this.reaperRegistered) return;\n this.reaperRegistered = true;\n process.on('exit', (): void => this.reapAll());\n }\n}\n\n// The shared instance — the reaper and the created-list are per-instance, so every caller must use this.\nexport const specTempDirs = new SpecTempDirs();\n"]}
@@ -113,11 +113,16 @@ export declare class ReviewerContext {
113
113
  constructor(branch: string, diffDir?: string, docPaths?: Record<string, string>, verdictPaths?: Record<string, string>);
114
114
  }
115
115
  /**
116
- * One checklist that must be credited to a reviewer run, and the agent type that run must carry. Data-only.
116
+ * One checklist that must be credited to a reviewer run, plus the agent type the BRIEFING named. Data-only.
117
117
  *
118
118
  * The two used to be one string — a checklist's id WAS its agent type — and provenance keyed everything by
119
119
  * it. Now every checklist is reviewed by the same repo-wide agent type, so the id and the type are
120
- * separate facts and a run is matched on the type but credited to the id.
120
+ * separate facts.
121
+ *
122
+ * `agentType` is NOT a provenance input: nothing here matches a run on it (issue #964, see
123
+ * {@link SubagentProvenanceService.isReviewerRun}). It is carried because it is what the BRIEFING told
124
+ * the agent to spawn and what {@link ReviewerEvidence} reports, which are both worth recording even
125
+ * though neither is worth gating on.
121
126
  */
122
127
  export declare class ExpectedReviewer {
123
128
  checklistId: string;
@@ -126,16 +131,18 @@ export declare class ExpectedReviewer {
126
131
  }
127
132
  /**
128
133
  * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
129
- * a subagent of a given `agentType` actually ran during the current session on the current branch. Used
130
- * to enforce that every checklist was reviewed by a subagent of the repo reviewer-agent type: that an INDEPENDENT
131
- * reviewer looked, rather than the coding agent self-certifying.
134
+ * a real SUBAGENT actually ran on the current branch and did this checklist's work. Used to enforce that
135
+ * every checklist was reviewed by an INDEPENDENT reviewer, rather than by the coding agent
136
+ * self-certifying.
132
137
  *
133
138
  * The harness writes, beside each subagent transcript:
134
- * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
135
- * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
136
- * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir
137
- * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be
138
- * derived/guessed.
139
+ * <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
140
+ * <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
141
+ * `spawnDepth`/`isSidechain` are written by Claude Code, not by the model, and they are what carries the
142
+ * anti-self-certification property. `agentType` is written there too and is deliberately NOT checked
143
+ * see {@link SubagentProvenanceService.isReviewerRun}. `<config>` is `$CLAUDE_CONFIG_DIR` when set, else
144
+ * `~/.claude`; see {@link ClaudeConfigDir}, and never re-derive it here. Dirs are found by walking
145
+ * every session, so the cwd-slug never has to be derived/guessed.
139
146
  *
140
147
  * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
141
148
  * raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
@@ -201,8 +208,40 @@ export declare class SubagentProvenanceService {
201
208
  private inClaudeSession;
202
209
  private skipped;
203
210
  private findMatchingAgentId;
211
+ /**
212
+ * A harness-written meta for a REAL SUBAGENT — `spawnDepth >= 1`, and nothing about its NAME.
213
+ *
214
+ * The agent TYPE used to be compared against the configured reviewer agent, and that check is
215
+ * gone (issue #964). It proved nothing: `agentType` is a label on a prompt, and it does not show
216
+ * the agent read the checklist, opened the diff, or looked at the right branch. The gates that DO
217
+ * show those are untouched — `spawnDepth >= 1` plus `isSidechain === true` (see
218
+ * {@link sidechainOnBranch}) are harness-written, so the main loop still cannot self-certify;
219
+ * {@link creditedByWhatItTouched} proves the branch and the actual work; `requireDiffEvidence`
220
+ * proves the diff was opened.
221
+ *
222
+ * It was also the ONLY gate with an unfixable failure mode. Claude Code snapshots agent
223
+ * definitions per session and the refresh does not reach an already-running subagent, so a repo
224
+ * that renames its reviewer agent mid-session leaves that subagent's registry offering only the
225
+ * OLD names: the spawn is rejected, the agent falls back to `general-purpose`, does the real
226
+ * review, writes every verdict — and is refused as unattributable, with no cwd, branch or respawn
227
+ * able to change it. Measured: six checklists, six verdict files on disk, PR blocked.
228
+ *
229
+ * Accepted trade-off: without the name check the implementer's own subagent could in principle be
230
+ * credited, since it touches the diff dir while running the gate. That is deliberate. The rule is
231
+ * "if the verdict file is there, a subagent wrote it", and this gate's job is to catch "no
232
+ * subagent ran at all" — not to adjudicate which subagent it was.
233
+ */
204
234
  private isReviewerRun;
205
235
  private namedVerdict;
236
+ /**
237
+ * Every `<config>/projects/&#42;/<session>/subagents` dir that exists — matching by the recorded
238
+ * gitBranch (not by session id) is what makes provenance survive across sessions.
239
+ *
240
+ * `<config>` comes from {@link ClaudeConfigDir.projectsRoots}, which is BOTH the configured
241
+ * `$CLAUDE_CONFIG_DIR` tree and `~/.claude`. This used to be a hardcoded `~/.claude`, and on a
242
+ * machine with a relocated config dir that found nothing at all — so every PR was refused with a
243
+ * message about the reviewers' cwd, which is not what was wrong and which no agent could act on.
244
+ */
206
245
  private allSubagentsDirs;
207
246
  private metaFiles;
208
247
  private agentIdOf;
@@ -5,8 +5,8 @@ const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const fs = tslib_1.__importStar(require("fs"));
7
7
  const path = tslib_1.__importStar(require("path"));
8
- const os = tslib_1.__importStar(require("os"));
9
8
  const inversify_1 = require("inversify");
9
+ const claude_config_dir_1 = require("./claude-config-dir");
10
10
  const to_error_1 = require("./to-error");
11
11
  const state_dir_1 = require("./state-dir");
12
12
  // Outcome of a provenance check.
@@ -162,11 +162,16 @@ class ReviewerContext {
162
162
  }
163
163
  exports.ReviewerContext = ReviewerContext;
164
164
  /**
165
- * One checklist that must be credited to a reviewer run, and the agent type that run must carry. Data-only.
165
+ * One checklist that must be credited to a reviewer run, plus the agent type the BRIEFING named. Data-only.
166
166
  *
167
167
  * The two used to be one string — a checklist's id WAS its agent type — and provenance keyed everything by
168
168
  * it. Now every checklist is reviewed by the same repo-wide agent type, so the id and the type are
169
- * separate facts and a run is matched on the type but credited to the id.
169
+ * separate facts.
170
+ *
171
+ * `agentType` is NOT a provenance input: nothing here matches a run on it (issue #964, see
172
+ * {@link SubagentProvenanceService.isReviewerRun}). It is carried because it is what the BRIEFING told
173
+ * the agent to spawn and what {@link ReviewerEvidence} reports, which are both worth recording even
174
+ * though neither is worth gating on.
170
175
  */
171
176
  class ExpectedReviewer {
172
177
  checklistId;
@@ -179,16 +184,18 @@ class ExpectedReviewer {
179
184
  exports.ExpectedReviewer = ExpectedReviewer;
180
185
  /**
181
186
  * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
182
- * a subagent of a given `agentType` actually ran during the current session on the current branch. Used
183
- * to enforce that every checklist was reviewed by a subagent of the repo reviewer-agent type: that an INDEPENDENT
184
- * reviewer looked, rather than the coding agent self-certifying.
187
+ * a real SUBAGENT actually ran on the current branch and did this checklist's work. Used to enforce that
188
+ * every checklist was reviewed by an INDEPENDENT reviewer, rather than by the coding agent
189
+ * self-certifying.
185
190
  *
186
191
  * The harness writes, beside each subagent transcript:
187
- * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
188
- * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
189
- * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir
190
- * by the unique sessionId (globbing `projects/&#42;/<sessionId>/subagents`), so the cwd-slug never has to be
191
- * derived/guessed.
192
+ * <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }
193
+ * <config>/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }
194
+ * `spawnDepth`/`isSidechain` are written by Claude Code, not by the model, and they are what carries the
195
+ * anti-self-certification property. `agentType` is written there too and is deliberately NOT checked
196
+ * see {@link SubagentProvenanceService.isReviewerRun}. `<config>` is `$CLAUDE_CONFIG_DIR` when set, else
197
+ * `~/.claude`; see {@link ClaudeConfigDir}, and never re-derive it here. Dirs are found by walking
198
+ * every session, so the cwd-slug never has to be derived/guessed.
192
199
  *
193
200
  * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This
194
201
  * raises the bar from "trust the model's word" to "deliberate, auditable forgery outside the repo"; it
@@ -388,19 +395,20 @@ let SubagentProvenanceService = class SubagentProvenanceService {
388
395
  skipped(what) {
389
396
  return new ProvenanceResult(exports.PROVENANCE_SKIPPED, `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`, {}, []);
390
397
  }
391
- // The agentId of a matching subagent run for `want.agentType` on `branch`, searched across ALL sessions'
392
- // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`
393
- // skips agentIds already credited to another checklist so one run can't satisfy two.
398
+ // The agentId of a subagent run on `branch`, searched across ALL sessions' subagent dirs
399
+ // (branch-scoped, so a run from a prior session still counts). '' if none. `exclude` skips agentIds
400
+ // already credited to another checklist so one run can't satisfy two.
394
401
  //
395
- // Now that every checklist shares ONE agent type, several runs match, so the one that NAMED this
396
- // checklist's verdict file wins; the first branch-matching run is the fallback, as before.
402
+ // The run is NOT matched on its agent type see isReviewerRun for why that check was removed — so
403
+ // several runs match and the one that NAMED this checklist's verdict file wins; the first
404
+ // branch-matching run is the fallback, as before.
397
405
  // eslint-disable-next-line @typescript-eslint/max-params
398
406
  findMatchingAgentId(dirs, want, context, exclude) {
399
407
  let fallback = '';
400
408
  for (const dir of dirs) {
401
409
  for (const metaFile of this.metaFiles(dir)) {
402
410
  const agentId = this.agentIdOf(metaFile);
403
- if (exclude.has(agentId) || !this.isReviewerRun(dir, metaFile, want.agentType))
411
+ if (exclude.has(agentId) || !this.isReviewerRun(dir, metaFile))
404
412
  continue;
405
413
  if (!this.sidechainOnBranch(dir, agentId, want.checklistId, context))
406
414
  continue;
@@ -412,10 +420,32 @@ let SubagentProvenanceService = class SubagentProvenanceService {
412
420
  }
413
421
  return fallback;
414
422
  }
415
- // A harness-written meta for a real subagent (spawnDepth >= 1) of the wanted type.
416
- isReviewerRun(dir, metaFile, agentType) {
423
+ /**
424
+ * A harness-written meta for a REAL SUBAGENT — `spawnDepth >= 1`, and nothing about its NAME.
425
+ *
426
+ * The agent TYPE used to be compared against the configured reviewer agent, and that check is
427
+ * gone (issue #964). It proved nothing: `agentType` is a label on a prompt, and it does not show
428
+ * the agent read the checklist, opened the diff, or looked at the right branch. The gates that DO
429
+ * show those are untouched — `spawnDepth >= 1` plus `isSidechain === true` (see
430
+ * {@link sidechainOnBranch}) are harness-written, so the main loop still cannot self-certify;
431
+ * {@link creditedByWhatItTouched} proves the branch and the actual work; `requireDiffEvidence`
432
+ * proves the diff was opened.
433
+ *
434
+ * It was also the ONLY gate with an unfixable failure mode. Claude Code snapshots agent
435
+ * definitions per session and the refresh does not reach an already-running subagent, so a repo
436
+ * that renames its reviewer agent mid-session leaves that subagent's registry offering only the
437
+ * OLD names: the spawn is rejected, the agent falls back to `general-purpose`, does the real
438
+ * review, writes every verdict — and is refused as unattributable, with no cwd, branch or respawn
439
+ * able to change it. Measured: six checklists, six verdict files on disk, PR blocked.
440
+ *
441
+ * Accepted trade-off: without the name check the implementer's own subagent could in principle be
442
+ * credited, since it touches the diff dir while running the gate. That is deliberate. The rule is
443
+ * "if the verdict file is there, a subagent wrote it", and this gate's job is to catch "no
444
+ * subagent ran at all" — not to adjudicate which subagent it was.
445
+ */
446
+ isReviewerRun(dir, metaFile) {
417
447
  const meta = this.readJson(path.join(dir, metaFile));
418
- if (!meta || meta['agentType'] !== agentType)
448
+ if (!meta)
419
449
  return false;
420
450
  const spawnDepth = meta['spawnDepth'];
421
451
  return typeof spawnDepth === 'number' && spawnDepth >= 1;
@@ -429,19 +459,25 @@ let SubagentProvenanceService = class SubagentProvenanceService {
429
459
  return false;
430
460
  return this.mentions(this.scanTranscript(jsonl).inputs, verdictPath);
431
461
  }
432
- // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by
433
- // session id) is what makes provenance survive across sessions.
462
+ /**
463
+ * Every `<config>/projects/&#42;/<session>/subagents` dir that exists matching by the recorded
464
+ * gitBranch (not by session id) is what makes provenance survive across sessions.
465
+ *
466
+ * `<config>` comes from {@link ClaudeConfigDir.projectsRoots}, which is BOTH the configured
467
+ * `$CLAUDE_CONFIG_DIR` tree and `~/.claude`. This used to be a hardcoded `~/.claude`, and on a
468
+ * machine with a relocated config dir that found nothing at all — so every PR was refused with a
469
+ * message about the reviewers' cwd, which is not what was wrong and which no agent could act on.
470
+ */
434
471
  allSubagentsDirs() {
435
- const projects = path.join(os.homedir(), '.claude', 'projects');
436
- if (!fs.existsSync(projects))
437
- return [];
438
472
  const out = [];
439
- for (const proj of this.readDir(projects)) {
440
- const projDir = path.join(projects, proj);
441
- for (const session of this.readDir(projDir)) {
442
- const candidate = path.join(projDir, session, 'subagents');
443
- if (fs.existsSync(candidate))
444
- out.push(candidate);
473
+ for (const projects of claude_config_dir_1.claudeConfigDir.projectsRoots()) {
474
+ for (const proj of this.readDir(projects)) {
475
+ const projDir = path.join(projects, proj);
476
+ for (const session of this.readDir(projDir)) {
477
+ const candidate = path.join(projDir, session, 'subagents');
478
+ if (fs.existsSync(candidate))
479
+ out.push(candidate);
480
+ }
445
481
  }
446
482
  }
447
483
  return out;