@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.
- package/package.json +1 -1
- package/src/aged-tree-sweep.d.ts +4 -1
- package/src/aged-tree-sweep.js +4 -1
- package/src/aged-tree-sweep.js.map +1 -1
- package/src/claude-config-dir.d.ts +62 -0
- package/src/claude-config-dir.js +87 -0
- package/src/claude-config-dir.js.map +1 -0
- package/src/harness-agent-activity.d.ts +5 -3
- package/src/harness-agent-activity.js +15 -14
- package/src/harness-agent-activity.js.map +1 -1
- package/src/index.d.ts +4 -0
- package/src/index.js +18 -4
- package/src/index.js.map +1 -1
- package/src/repo-scratch-dirs.d.ts +20 -0
- package/src/repo-scratch-dirs.js +54 -0
- package/src/repo-scratch-dirs.js.map +1 -0
- package/src/review-provenance.d.ts +22 -10
- package/src/review-provenance.js +36 -22
- package/src/review-provenance.js.map +1 -1
- package/src/spec-temp-dirs.d.ts +37 -0
- package/src/spec-temp-dirs.js +112 -0
- package/src/spec-temp-dirs.js.map +1 -0
- package/src/subagent-provenance.d.ts +49 -10
- package/src/subagent-provenance.js +67 -31
- package/src/subagent-provenance.js.map +1 -1
- package/src/tmp-scratch-sweep.d.ts +32 -0
- package/src/tmp-scratch-sweep.js +99 -0
- package/src/tmp-scratch-sweep.js.map +1 -0
|
@@ -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,
|
|
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
|
|
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
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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/*/<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,
|
|
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
|
|
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
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* `
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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
|
|
392
|
-
//
|
|
393
|
-
//
|
|
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
|
-
//
|
|
396
|
-
// checklist's verdict file wins; the first
|
|
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
|
|
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
|
-
|
|
416
|
-
|
|
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
|
|
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
|
-
|
|
433
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Every `<config>/projects/*/<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
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
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;
|