@webpieces/rules-config 0.4.497 → 0.4.499
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/branch-mutation-log.d.ts +13 -2
- package/src/branch-mutation-log.js +33 -1
- package/src/branch-mutation-log.js.map +1 -1
- package/src/checklist-config.js +1 -1
- package/src/checklist-config.js.map +1 -1
- package/src/checklist-instructions.d.ts +1 -1
- package/src/checklist-instructions.js +28 -13
- package/src/checklist-instructions.js.map +1 -1
- package/src/diff-scope.d.ts +10 -0
- package/src/diff-scope.js +14 -1
- package/src/diff-scope.js.map +1 -1
- package/src/index.d.ts +5 -3
- package/src/index.js +19 -3
- package/src/index.js.map +1 -1
- package/src/merged-branches.d.ts +24 -1
- package/src/merged-branches.js +36 -8
- package/src/merged-branches.js.map +1 -1
- package/src/pr-gate-config.d.ts +53 -0
- package/src/pr-gate-config.js +66 -4
- package/src/pr-gate-config.js.map +1 -1
- package/src/review-json.d.ts +35 -2
- package/src/review-json.js +73 -14
- package/src/review-json.js.map +1 -1
- package/src/reviewer-instructions.d.ts +86 -0
- package/src/reviewer-instructions.js +236 -0
- package/src/reviewer-instructions.js.map +1 -0
- package/src/subagent-provenance.d.ts +55 -1
- package/src/subagent-provenance.js +143 -5
- package/src/subagent-provenance.js.map +1 -1
- package/src/worktree-reaper.d.ts +116 -0
- package/src/worktree-reaper.js +305 -0
- package/src/worktree-reaper.js.map +1 -0
- package/templates/webpieces.git-workflow.md +13 -7
- package/templates/webpieces.review-checklists.md +19 -8
|
@@ -4,7 +4,32 @@ export declare const PROVENANCE_SKIPPED = "skipped";
|
|
|
4
4
|
export declare class ProvenanceResult {
|
|
5
5
|
status: string;
|
|
6
6
|
detail: string;
|
|
7
|
-
|
|
7
|
+
agentIds: Record<string, string>;
|
|
8
|
+
constructor(status: string, detail: string, agentIds?: Record<string, string>);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).
|
|
12
|
+
*
|
|
13
|
+
* `verifyDistinct` answers "did a reviewer of this type run?" — an INTEGRITY question, and it blocks. This
|
|
14
|
+
* answers "did it look at the change?" — a QUALITY question, and it only warns. The distinction is
|
|
15
|
+
* deliberate; see {@link SubagentProvenanceService.evidenceFor}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ReviewerEvidence {
|
|
18
|
+
agentType: string;
|
|
19
|
+
agentId: string;
|
|
20
|
+
readDiff: boolean;
|
|
21
|
+
readDoc: boolean;
|
|
22
|
+
toolCallCount: number;
|
|
23
|
+
offRepoSearches: number;
|
|
24
|
+
constructor(agentType: string, agentId: string, readDiff?: boolean, readDoc?: boolean, toolCallCount?: number, offRepoSearches?: number);
|
|
25
|
+
}
|
|
26
|
+
/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */
|
|
27
|
+
export declare class EvidenceRequest {
|
|
28
|
+
branch: string;
|
|
29
|
+
agentIds: Record<string, string>;
|
|
30
|
+
diffDir: string;
|
|
31
|
+
docPaths: Record<string, string>;
|
|
32
|
+
constructor(branch: string, agentIds: Record<string, string>, diffDir?: string, docPaths?: Record<string, string>);
|
|
8
33
|
}
|
|
9
34
|
/**
|
|
10
35
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
@@ -28,6 +53,35 @@ export declare class ProvenanceResult {
|
|
|
28
53
|
export declare class SubagentProvenanceService {
|
|
29
54
|
verify(expectedAgentType: string, branch: string): ProvenanceResult;
|
|
30
55
|
verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult;
|
|
56
|
+
/**
|
|
57
|
+
* What each credited reviewer actually READ, from its own transcript.
|
|
58
|
+
*
|
|
59
|
+
* `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff
|
|
60
|
+
* and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and
|
|
61
|
+
* the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because
|
|
62
|
+
* nothing had told it where anything was, which `offRepoSearches` now makes visible.
|
|
63
|
+
*
|
|
64
|
+
* WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least
|
|
65
|
+
* one repo has watched it:
|
|
66
|
+
* 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every
|
|
67
|
+
* consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly
|
|
68
|
+
* this reason, and blocking on a richer read of the same files would be less safe, not more.
|
|
69
|
+
* 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).
|
|
70
|
+
* 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating
|
|
71
|
+
* them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.
|
|
72
|
+
*
|
|
73
|
+
* Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.
|
|
74
|
+
*/
|
|
75
|
+
evidenceFor(request: EvidenceRequest): ReviewerEvidence[];
|
|
76
|
+
private evidenceFromTranscript;
|
|
77
|
+
private transcriptPath;
|
|
78
|
+
/**
|
|
79
|
+
* Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field
|
|
80
|
+
* because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a
|
|
81
|
+
* substring test over the serialized input is both simpler and robust to a tool we have not seen.
|
|
82
|
+
*/
|
|
83
|
+
private toolInputsOf;
|
|
84
|
+
private mentions;
|
|
31
85
|
private inClaudeSession;
|
|
32
86
|
private skipped;
|
|
33
87
|
private findMatchingAgentId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SubagentProvenanceService = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
|
|
3
|
+
exports.SubagentProvenanceService = exports.EvidenceRequest = exports.ReviewerEvidence = exports.ProvenanceResult = exports.PROVENANCE_SKIPPED = exports.PROVENANCE_MISSING = exports.PROVENANCE_OK = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
@@ -14,12 +14,55 @@ exports.PROVENANCE_SKIPPED = 'skipped'; // no CLAUDE_CODE_SESSION_ID (plain term
|
|
|
14
14
|
class ProvenanceResult {
|
|
15
15
|
status; // PROVENANCE_OK | PROVENANCE_MISSING | PROVENANCE_SKIPPED
|
|
16
16
|
detail; // human-readable explanation for the warning/error/dashboard
|
|
17
|
-
|
|
17
|
+
// agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.
|
|
18
|
+
agentIds;
|
|
19
|
+
constructor(status, detail, agentIds = {}) {
|
|
18
20
|
this.status = status;
|
|
19
21
|
this.detail = detail;
|
|
22
|
+
this.agentIds = agentIds;
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
exports.ProvenanceResult = ProvenanceResult;
|
|
26
|
+
/**
|
|
27
|
+
* What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).
|
|
28
|
+
*
|
|
29
|
+
* `verifyDistinct` answers "did a reviewer of this type run?" — an INTEGRITY question, and it blocks. This
|
|
30
|
+
* answers "did it look at the change?" — a QUALITY question, and it only warns. The distinction is
|
|
31
|
+
* deliberate; see {@link SubagentProvenanceService.evidenceFor}.
|
|
32
|
+
*/
|
|
33
|
+
class ReviewerEvidence {
|
|
34
|
+
agentType;
|
|
35
|
+
agentId;
|
|
36
|
+
readDiff; // opened the materialized diff dir (or its own instructions file)
|
|
37
|
+
readDoc; // opened its checklist's guidance doc
|
|
38
|
+
toolCallCount;
|
|
39
|
+
offRepoSearches; // tool calls that reached into node_modules — the archaeology signal
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
41
|
+
constructor(agentType, agentId, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0) {
|
|
42
|
+
this.agentType = agentType;
|
|
43
|
+
this.agentId = agentId;
|
|
44
|
+
this.readDiff = readDiff;
|
|
45
|
+
this.readDoc = readDoc;
|
|
46
|
+
this.toolCallCount = toolCallCount;
|
|
47
|
+
this.offRepoSearches = offRepoSearches;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.ReviewerEvidence = ReviewerEvidence;
|
|
51
|
+
/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */
|
|
52
|
+
class EvidenceRequest {
|
|
53
|
+
branch;
|
|
54
|
+
agentIds; // agentType → agentId, straight from ProvenanceResult
|
|
55
|
+
diffDir; // '' when nothing was materialized
|
|
56
|
+
docPaths; // agentType → its checklist doc path ('' when none)
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
58
|
+
constructor(branch, agentIds, diffDir = '', docPaths = {}) {
|
|
59
|
+
this.branch = branch;
|
|
60
|
+
this.agentIds = agentIds;
|
|
61
|
+
this.diffDir = diffDir;
|
|
62
|
+
this.docPaths = docPaths;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.EvidenceRequest = EvidenceRequest;
|
|
23
66
|
/**
|
|
24
67
|
* Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that
|
|
25
68
|
* a subagent of a given `agentType` actually ran during the current session on the current branch. Used
|
|
@@ -66,16 +109,111 @@ let SubagentProvenanceService = class SubagentProvenanceService {
|
|
|
66
109
|
const dirs = this.allSubagentsDirs();
|
|
67
110
|
const missing = [];
|
|
68
111
|
const usedAgentIds = new Set();
|
|
112
|
+
const credited = {};
|
|
69
113
|
for (const type of expectedAgentTypes) {
|
|
70
114
|
const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);
|
|
71
115
|
if (agentId === '')
|
|
72
116
|
missing.push(type);
|
|
73
|
-
else
|
|
117
|
+
else {
|
|
74
118
|
usedAgentIds.add(agentId);
|
|
119
|
+
credited[type] = agentId;
|
|
120
|
+
}
|
|
75
121
|
}
|
|
76
122
|
return missing.length === 0
|
|
77
|
-
? new ProvenanceResult(exports.PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran
|
|
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(', ')}
|
|
123
|
+
? new ProvenanceResult(exports.PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)
|
|
124
|
+
: 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(', ')}`, credited);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What each credited reviewer actually READ, from its own transcript.
|
|
128
|
+
*
|
|
129
|
+
* `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff
|
|
130
|
+
* and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and
|
|
131
|
+
* the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because
|
|
132
|
+
* nothing had told it where anything was, which `offRepoSearches` now makes visible.
|
|
133
|
+
*
|
|
134
|
+
* WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least
|
|
135
|
+
* one repo has watched it:
|
|
136
|
+
* 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every
|
|
137
|
+
* consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly
|
|
138
|
+
* this reason, and blocking on a richer read of the same files would be less safe, not more.
|
|
139
|
+
* 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).
|
|
140
|
+
* 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating
|
|
141
|
+
* them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.
|
|
142
|
+
*
|
|
143
|
+
* Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.
|
|
144
|
+
*/
|
|
145
|
+
evidenceFor(request) {
|
|
146
|
+
if (!this.inClaudeSession())
|
|
147
|
+
return [];
|
|
148
|
+
const dirs = this.allSubagentsDirs();
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const agentType of Object.keys(request.agentIds)) {
|
|
151
|
+
const agentId = request.agentIds[agentType];
|
|
152
|
+
const jsonl = this.transcriptPath(dirs, agentId);
|
|
153
|
+
if (jsonl === '') {
|
|
154
|
+
out.push(new ReviewerEvidence(agentType, agentId));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
162
|
+
evidenceFromTranscript(agentType, agentId, jsonl, request) {
|
|
163
|
+
const inputs = this.toolInputsOf(jsonl);
|
|
164
|
+
const docPath = request.docPaths[agentType] ?? '';
|
|
165
|
+
// The instructions file counts as "read the diff": it lives in the same per-branch dir, it is what
|
|
166
|
+
// the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and
|
|
167
|
+
// then its own diff files is the intended path; treating only the diff dir as proof would flag it.
|
|
168
|
+
const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);
|
|
169
|
+
return new ReviewerEvidence(agentType, agentId, readDiff, docPath !== '' && this.mentions(inputs, docPath), inputs.length, inputs.filter((i) => i.includes('node_modules')).length);
|
|
170
|
+
}
|
|
171
|
+
// The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.
|
|
172
|
+
transcriptPath(dirs, agentId) {
|
|
173
|
+
for (const dir of dirs) {
|
|
174
|
+
const candidate = path.join(dir, `agent-${agentId}.jsonl`);
|
|
175
|
+
if (fs.existsSync(candidate))
|
|
176
|
+
return candidate;
|
|
177
|
+
}
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field
|
|
182
|
+
* because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a
|
|
183
|
+
* substring test over the serialized input is both simpler and robust to a tool we have not seen.
|
|
184
|
+
*/
|
|
185
|
+
toolInputsOf(jsonl) {
|
|
186
|
+
const out = [];
|
|
187
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash
|
|
188
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
189
|
+
try {
|
|
190
|
+
for (const line of fs.readFileSync(jsonl, 'utf8').split('\n')) {
|
|
191
|
+
if (line.trim() === '')
|
|
192
|
+
continue;
|
|
193
|
+
const rec = this.parseLine(line);
|
|
194
|
+
const message = rec?.['message'];
|
|
195
|
+
if (typeof message !== 'object' || message === null)
|
|
196
|
+
continue;
|
|
197
|
+
// webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above
|
|
198
|
+
const content = message['content'];
|
|
199
|
+
if (!Array.isArray(content))
|
|
200
|
+
continue;
|
|
201
|
+
for (const block of content) {
|
|
202
|
+
// webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read
|
|
203
|
+
const b = block;
|
|
204
|
+
if (b['type'] === 'tool_use')
|
|
205
|
+
out.push(JSON.stringify(b['input'] ?? {}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const error = (0, to_error_1.toError)(err);
|
|
211
|
+
void error;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
mentions(inputs, needle) {
|
|
216
|
+
return inputs.some((i) => i.includes(needle));
|
|
79
217
|
}
|
|
80
218
|
// Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)
|
|
81
219
|
inClaudeSession() {
|
|
@@ -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 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"]}
|
|
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 // agentIds credited, keyed by agentType, so an evidence pass does not re-scan to find them again.\n agentIds: Record<string, string>;\n\n constructor(status: string, detail: string, agentIds: Record<string, string> = {}) {\n this.status = status;\n this.detail = detail;\n this.agentIds = agentIds;\n }\n}\n\n/**\n * What ONE credited reviewer actually DID, read from its own transcript. Data-only (per CLAUDE.md).\n *\n * `verifyDistinct` answers \"did a reviewer of this type run?\" — an INTEGRITY question, and it blocks. This\n * answers \"did it look at the change?\" — a QUALITY question, and it only warns. The distinction is\n * deliberate; see {@link SubagentProvenanceService.evidenceFor}.\n */\nexport class ReviewerEvidence {\n agentType: string;\n agentId: string;\n readDiff: boolean; // opened the materialized diff dir (or its own instructions file)\n readDoc: boolean; // opened its checklist's guidance doc\n toolCallCount: number;\n offRepoSearches: number; // tool calls that reached into node_modules — the archaeology signal\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(agentType: string, agentId: string, readDiff = false, readDoc = false, toolCallCount = 0, offRepoSearches = 0) {\n this.agentType = agentType;\n this.agentId = agentId;\n this.readDiff = readDiff;\n this.readDoc = readDoc;\n this.toolCallCount = toolCallCount;\n this.offRepoSearches = offRepoSearches;\n }\n}\n\n/** What to look for when reading reviewer transcripts. Data-only — avoids a 5-param method. */\nexport class EvidenceRequest {\n branch: string;\n agentIds: Record<string, string>; // agentType → agentId, straight from ProvenanceResult\n diffDir: string; // '' when nothing was materialized\n docPaths: Record<string, string>; // agentType → its checklist doc path ('' when none)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(branch: string, agentIds: Record<string, string>, diffDir = '', docPaths: Record<string, string> = {}) {\n this.branch = branch;\n this.agentIds = agentIds;\n this.diffDir = diffDir;\n this.docPaths = docPaths;\n }\n}\n\n/**\n * Verifies — from the Claude Code harness's OWN artifacts, never from anything the model asserts — that\n * a subagent of a given `agentType` actually ran during the current session on the current branch. Used\n * to enforce a checklist's optional `subagent:` field: that an INDEPENDENT reviewer looked, rather than\n * the coding agent self-certifying.\n *\n * The harness writes, beside each subagent transcript:\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.meta.json → { agentType, spawnDepth, … }\n * ~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<id>.jsonl → record 0 { isSidechain, gitBranch, … }\n * `agentType`/`spawnDepth`/`isSidechain` are written by Claude Code, not by the model. We locate the dir\n * by the unique sessionId (globbing `projects/*/<sessionId>/subagents`), so the cwd-slug never has to be\n * derived/guessed.\n *\n * IMPORTANT — this is NOT tamper-proof. A determined agent can `cat >` a fake agent-*.meta.json. This\n * raises the bar from \"trust the model's word\" to \"deliberate, auditable forgery outside the repo\"; it\n * is not cryptographic. Say so wherever `subagent:` is documented.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class SubagentProvenanceService {\n // Verify a subagent of `expectedAgentType` ran on `branch`. Absent CLAUDE_CODE_SESSION_ID (plain\n // terminal / CI) returns SKIPPED — the feature must not break non-Claude-Code consumers.\n verify(expectedAgentType: string, branch: string): ProvenanceResult {\n if (!this.inClaudeSession()) return this.skipped(`the \"${expectedAgentType}\" reviewer subagent`);\n const dirs = this.allSubagentsDirs();\n const agentId = this.findMatchingAgentId(dirs, expectedAgentType, branch);\n return agentId !== ''\n ? new ProvenanceResult(PROVENANCE_OK, `verified \"${expectedAgentType}\" reviewer subagent ran (agent ${agentId}).`)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `No \"${expectedAgentType}\" reviewer subagent ran on this branch — a separate reviewer of that type must review this checklist.`);\n }\n\n // Verify EVERY expected reviewer subagent ran on `branch` as a DISTINCT run — the coding agent may not\n // self-certify, and one reviewer may not stand in for several. SKIPPED (pass) without a session id.\n //\n // Scoped by BRANCH across ALL sessions (not the current session): once a reviewer ran on this branch in\n // any session, a later re-push in a NEW session still finds it, so the review is NOT forced to re-run.\n // That is what keeps \"review once per branch\" true across sessions. A PR opened outside the gated flow\n // still has no review-<id>.json, so wp-finish forces the review regardless of provenance.\n verifyDistinct(expectedAgentTypes: readonly string[], branch: string): ProvenanceResult {\n if (expectedAgentTypes.length === 0) return new ProvenanceResult(PROVENANCE_OK, 'no reviewer subagents required');\n if (!this.inClaudeSession()) return this.skipped('reviewer subagents');\n const dirs = this.allSubagentsDirs();\n const missing: string[] = [];\n const usedAgentIds = new Set<string>();\n const credited: Record<string, string> = {};\n for (const type of expectedAgentTypes) {\n const agentId = this.findMatchingAgentId(dirs, type, branch, usedAgentIds);\n if (agentId === '') missing.push(type);\n else {\n usedAgentIds.add(agentId);\n credited[type] = agentId;\n }\n }\n return missing.length === 0\n ? new ProvenanceResult(PROVENANCE_OK, `verified ${expectedAgentTypes.length} distinct reviewer subagent(s) ran`, credited)\n : new ProvenanceResult(PROVENANCE_MISSING,\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${missing.join(', ')}`,\n credited);\n }\n\n /**\n * What each credited reviewer actually READ, from its own transcript.\n *\n * `verifyDistinct` proves a reviewer of the right type RAN. It cannot tell a reviewer that read the diff\n * and thought about it from one that wrote a verdict having opened nothing. This closes that gap — and\n * the motivating case is real: one reviewer spent 14 of 26 tool calls grepping `node_modules` because\n * nothing had told it where anything was, which `offRepoSearches` now makes visible.\n *\n * WARNING-ONLY by default, for three reasons, and `requireDiffEvidence` must stay opt-in until at least\n * one repo has watched it:\n * 1. The transcript layout is undocumented Claude Code internals. A format change would wedge every\n * consumer's PR with no self-service recovery — `sidechainOnBranch` is already lenient for exactly\n * this reason, and blocking on a richer read of the same files would be less safe, not more.\n * 2. A reviewer can legitimately receive the diff another way (inlined into its prompt, say).\n * 3. verifyDistinct is the INTEGRITY signal and rightly blocks; this is a QUALITY signal. Conflating\n * them would let a transcript-parsing quirk refuse a PR that a real reviewer really did review.\n *\n * Returns [] outside a Claude Code session, matching verify/verifyDistinct's SKIP behavior.\n */\n evidenceFor(request: EvidenceRequest): ReviewerEvidence[] {\n if (!this.inClaudeSession()) return [];\n const dirs = this.allSubagentsDirs();\n const out: ReviewerEvidence[] = [];\n for (const agentType of Object.keys(request.agentIds)) {\n const agentId = request.agentIds[agentType];\n const jsonl = this.transcriptPath(dirs, agentId);\n if (jsonl === '') {\n out.push(new ReviewerEvidence(agentType, agentId));\n continue;\n }\n out.push(this.evidenceFromTranscript(agentType, agentId, jsonl, request));\n }\n return out;\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private evidenceFromTranscript(agentType: string, agentId: string, jsonl: string, request: EvidenceRequest): ReviewerEvidence {\n const inputs = this.toolInputsOf(jsonl);\n const docPath = request.docPaths[agentType] ?? '';\n // The instructions file counts as \"read the diff\": it lives in the same per-branch dir, it is what\n // the reviewer is told to open first, and it inlines the diff paths. A reviewer that opened it and\n // then its own diff files is the intended path; treating only the diff dir as proof would flag it.\n const readDiff = request.diffDir !== '' && this.mentions(inputs, request.diffDir);\n return new ReviewerEvidence(\n agentType, agentId, readDiff,\n docPath !== '' && this.mentions(inputs, docPath),\n inputs.length,\n inputs.filter((i: string): boolean => i.includes('node_modules')).length,\n );\n }\n\n // The absolute path of agent-<id>.jsonl across all session dirs, or '' if it is not there.\n private transcriptPath(dirs: readonly string[], agentId: string): string {\n for (const dir of dirs) {\n const candidate = path.join(dir, `agent-${agentId}.jsonl`);\n if (fs.existsSync(candidate)) return candidate;\n }\n return '';\n }\n\n /**\n * Every tool_use input in a transcript, JSON-stringified. Stringified rather than walked field-by-field\n * because the shape differs per tool (Read takes file_path, Bash takes command, Grep takes path) and a\n * substring test over the serialized input is both simpler and robust to a tool we have not seen.\n */\n private toolInputsOf(jsonl: string): string[] {\n const out: string[] = [];\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable transcript yields no evidence, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(jsonl, 'utf8').split('\\n')) {\n if (line.trim() === '') continue;\n const rec = this.parseLine(line);\n const message = rec?.['message'];\n if (typeof message !== 'object' || message === null) continue;\n // webpieces-disable no-any-unknown -- opaque transcript record, narrowed by the guard above\n const content = (message as Record<string, unknown>)['content'];\n if (!Array.isArray(content)) continue;\n for (const block of content) {\n // webpieces-disable no-any-unknown -- opaque content block, only `type`/`input` are read\n const b = block as Record<string, unknown>;\n if (b['type'] === 'tool_use') out.push(JSON.stringify(b['input'] ?? {}));\n }\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n return out;\n }\n\n private mentions(inputs: readonly string[], needle: string): boolean {\n return inputs.some((i: string): boolean => i.includes(needle));\n }\n\n // Are we running under Claude Code at all? (CI / plain terminal → provenance is unverifiable → SKIP.)\n private inClaudeSession(): boolean {\n return (process.env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';\n }\n\n private skipped(what: string): ProvenanceResult {\n return new ProvenanceResult(PROVENANCE_SKIPPED,\n `CLAUDE_CODE_SESSION_ID not set — cannot verify ${what} ran (plain terminal / CI). Skipping the provenance check.`);\n }\n\n // The agentId of a matching subagent run for `agentType` on `branch`, searched across ALL sessions'\n // subagent dirs (branch-scoped, so a run from a prior session still counts). '' if none. `exclude`\n // skips agentIds already credited to another checklist so one run can't satisfy two.\n private findMatchingAgentId(dirs: readonly string[], agentType: string, branch: string, exclude: ReadonlySet<string> = new Set()): string {\n for (const dir of dirs) {\n for (const metaFile of this.metaFiles(dir)) {\n const agentId = this.agentIdOf(metaFile);\n if (exclude.has(agentId)) continue;\n const meta = this.readJson(path.join(dir, metaFile));\n if (!meta || meta['agentType'] !== agentType) continue;\n const spawnDepth = meta['spawnDepth'];\n if (typeof spawnDepth !== 'number' || spawnDepth < 1) continue;\n if (this.sidechainOnBranch(dir, agentId, branch)) return agentId;\n }\n }\n return '';\n }\n\n // Every `projects/*/<session>/subagents` dir that exists — matching by the recorded gitBranch (not by\n // session id) is what makes provenance survive across sessions.\n private allSubagentsDirs(): string[] {\n const projects = path.join(os.homedir(), '.claude', 'projects');\n if (!fs.existsSync(projects)) return [];\n const out: string[] = [];\n for (const proj of this.readDir(projects)) {\n const projDir = path.join(projects, proj);\n for (const session of this.readDir(projDir)) {\n const candidate = path.join(projDir, session, 'subagents');\n if (fs.existsSync(candidate)) out.push(candidate);\n }\n }\n return out;\n }\n\n private metaFiles(dir: string): string[] {\n return this.readDir(dir).filter((f: string): boolean => f.endsWith('.meta.json'));\n }\n\n // agent-<id>.meta.json → <id>\n private agentIdOf(metaFile: string): string {\n return metaFile.replace(/^agent-/, '').replace(/\\.meta\\.json$/, '');\n }\n\n // Cross-check the sibling transcript's record 0: isSidechain true (a real subagent, not the main\n // loop) and, when recorded, the same branch. Lenient on branch drift (a leftover …wpN suffix from a\n // mid-flight branch rename) and on a missing jsonl (meta already matched — accept, best-effort).\n private sidechainOnBranch(dir: string, agentId: string, branch: string): boolean {\n const jsonl = path.join(dir, `agent-${agentId}.jsonl`);\n if (!fs.existsSync(jsonl)) return true;\n const first = this.firstNonEmptyLine(jsonl);\n if (first === '') return true;\n const rec = this.parseLine(first);\n if (!rec) return true;\n if (rec['isSidechain'] !== true) return false;\n const gitBranch = rec['gitBranch'];\n if (typeof gitBranch !== 'string' || gitBranch === '') return true;\n return this.stripWp(gitBranch) === this.stripWp(branch);\n }\n\n private stripWp(branch: string): string {\n return branch.replace(/-?wp\\d+$/i, '');\n }\n\n private readDir(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable dir yields [] (best-effort provenance)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return [];\n }\n }\n\n private firstNonEmptyLine(filePath: string): string {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unreadable transcript → '' (best-effort)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n for (const line of fs.readFileSync(filePath, 'utf8').split('\\n')) {\n if (line.trim() !== '') return line;\n }\n return '';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return '';\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON object, keys read by the caller\n private readJson(filePath: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed meta.json → null (skipped)\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSONL record, keys read by the caller\n private parseLine(line: string): Record<string, unknown> | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: malformed JSONL record → null\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(line) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { BranchArchiver } from './branch-archiver';
|
|
2
|
+
import { BranchMutationLog, MutationVerb } from './branch-mutation-log';
|
|
3
|
+
import { DeletableWorktree } from './merged-branches';
|
|
4
|
+
import { WorktreeService } from './worktrees';
|
|
5
|
+
/**
|
|
6
|
+
* The EXECUTOR for the dead-WORKTREE verdicts that merged-branches.ts has been computing all along.
|
|
7
|
+
*
|
|
8
|
+
* WHY this exists: `DeletableWorktree` was designed for reaping — it carries `path` (what
|
|
9
|
+
* `git worktree remove` takes) AND `branch` (what `git branch -D` takes afterwards) precisely because
|
|
10
|
+
* a reap is always those two steps in that order. The reaping was then never wired up. The verdicts
|
|
11
|
+
* had exactly one consumer, branch-creation-guard, which used them only to BLOCK: at the worktree cap
|
|
12
|
+
* it printed the reap commands and refused to create the next worktree. Nothing ever ran them.
|
|
13
|
+
*
|
|
14
|
+
* That composes into a deadlock, not merely a missing feature. A merged worktree HOLDS its branch, so
|
|
15
|
+
* the branch lands in `keep` with "checked out in worktree '<path>' — remove that worktree before
|
|
16
|
+
* deleting the branch". Nothing removes the worktree. Both accumulate forever, and the guard's only
|
|
17
|
+
* remaining advice is to loosen its own cap — which is the failure the cap exists to prevent. Observed
|
|
18
|
+
* twice in one day: a `wp-cleanup` run that spared three worktree-held branches with that exact line,
|
|
19
|
+
* and another that spared seven.
|
|
20
|
+
*
|
|
21
|
+
* THE ORDER IS FIXED AND LOAD-BEARING: archive the branch as a tag → `git worktree remove <path>` →
|
|
22
|
+
* `git branch -D <branch>`.
|
|
23
|
+
* - Archive FIRST, and if it fails nothing is deleted. Same rule BranchArchiver already enforces for
|
|
24
|
+
* branches: a branch we could not tag is a branch whose only copy would be the reflog.
|
|
25
|
+
* - Remove the worktree BEFORE the branch, because git flatly refuses to delete a branch that is
|
|
26
|
+
* still checked out somewhere.
|
|
27
|
+
*
|
|
28
|
+
* AND NEVER `--force`. Git refuses to remove a worktree with uncommitted changes or untracked files,
|
|
29
|
+
* and that refusal is the entire safety property here: a worktree removal deletes real FILES, not just
|
|
30
|
+
* a ref, and an untracked file is by definition something no archive tag captured. A failed removal is
|
|
31
|
+
* reported and moved past, exactly as a failed branch delete already is.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ReapedWorktree {
|
|
34
|
+
path: string;
|
|
35
|
+
branch: string;
|
|
36
|
+
sha: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
pr: number;
|
|
39
|
+
ok: boolean;
|
|
40
|
+
error: string;
|
|
41
|
+
archiveTag: string;
|
|
42
|
+
/**
|
|
43
|
+
* Did the BRANCH delete also succeed? A worktree removal that succeeds while the branch delete
|
|
44
|
+
* fails is a real, reportable half-state — the directory is gone, the branch is still there — and
|
|
45
|
+
* collapsing it into `ok` would hide it. `ok` means the DIRECTORY is gone; this means the pair is.
|
|
46
|
+
*/
|
|
47
|
+
branchDeleted: boolean;
|
|
48
|
+
constructor(treePath: string, branch: string, sha: string, reason: string, pr: number, ok: boolean, error: string);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The outcome of one worktree reap. `spared` carries the worktrees we refused to touch, each with its
|
|
52
|
+
* reason, for the same reason ReapResult does: a cleanup silent about what it did NOT remove reads as
|
|
53
|
+
* "there was nothing else", when those are exactly the ones only a human can rule on.
|
|
54
|
+
*/
|
|
55
|
+
export declare class WorktreeReapResult {
|
|
56
|
+
reaped: ReapedWorktree[];
|
|
57
|
+
failed: ReapedWorktree[];
|
|
58
|
+
spared: DeletableWorktree[];
|
|
59
|
+
constructor(reaped: ReapedWorktree[], failed: ReapedWorktree[], spared: DeletableWorktree[]);
|
|
60
|
+
}
|
|
61
|
+
export declare class WorktreeReaper {
|
|
62
|
+
private readonly worktrees;
|
|
63
|
+
private readonly mutationLog;
|
|
64
|
+
private readonly archiver;
|
|
65
|
+
constructor(worktrees?: WorktreeService, mutationLog?: BranchMutationLog, archiver?: BranchArchiver);
|
|
66
|
+
/**
|
|
67
|
+
* Reap every worktree in `targets`, skipping any the safety rails refuse.
|
|
68
|
+
*
|
|
69
|
+
* `cwd` is passed in rather than read from `process.cwd()` here so the "never remove the tree I am
|
|
70
|
+
* standing in" rule is testable and so a caller running from a subdirectory still gets the right
|
|
71
|
+
* answer — the containing WORKTREE is what matters, not the exact directory.
|
|
72
|
+
*
|
|
73
|
+
* `targets` is caller-chosen on purpose. wp-cleanup passes the provably-dead ones unattended, and
|
|
74
|
+
* passes human-approved probably-dead ones on a second call. The safety rails below apply to both:
|
|
75
|
+
* no answer at a prompt can authorise removing your own cwd or the primary clone.
|
|
76
|
+
*/
|
|
77
|
+
reapWorktrees(repoRoot: string, cwd: string, verb: MutationVerb, targets: DeletableWorktree[], retention?: string): WorktreeReapResult;
|
|
78
|
+
/**
|
|
79
|
+
* The two directories that must never be removed, resolved to absolute paths so a relative
|
|
80
|
+
* `../foo` in a verdict can still be compared against them.
|
|
81
|
+
*
|
|
82
|
+
* - THE PRIMARY CLONE. It owns `.git`; `git worktree remove` cannot take it, and a caller who
|
|
83
|
+
* somehow got it into a target list has a bug we must not execute.
|
|
84
|
+
* - THE TREE WE ARE STANDING IN. Removing your own cwd mid-command deletes the files underneath
|
|
85
|
+
* the running process — including, when the tooling is invoked by an agent, the checkout the
|
|
86
|
+
* agent's next tool call will try to read. merged-branches.ts already declines to mark it
|
|
87
|
+
* deletable; this is the second, independent line, because the caller supplies the list.
|
|
88
|
+
*/
|
|
89
|
+
private protectedPaths;
|
|
90
|
+
private currentTree;
|
|
91
|
+
private refuseReason;
|
|
92
|
+
/**
|
|
93
|
+
* One worktree: ARCHIVE the branch, REMOVE the directory, then DELETE the branch — and stop at the
|
|
94
|
+
* first step that fails.
|
|
95
|
+
*
|
|
96
|
+
* The prunable case is genuinely different and is why the classification token rides along on the
|
|
97
|
+
* verdict: the directory is ALREADY gone, so `git worktree remove` fails on it and the reap is
|
|
98
|
+
* `git worktree prune`. There is also nothing to archive from a directory that no longer exists —
|
|
99
|
+
* the branch itself is still archived, since it may well still hold the only copy of some work.
|
|
100
|
+
*/
|
|
101
|
+
private removeOne;
|
|
102
|
+
private archiveBranch;
|
|
103
|
+
private archiveFailed;
|
|
104
|
+
/**
|
|
105
|
+
* git refused to remove the directory — nearly always because it holds uncommitted changes or
|
|
106
|
+
* untracked files. Reported with git's own words and moved past. We do NOT retry with `--force`:
|
|
107
|
+
* an untracked file is work no archive tag captured, and `--force` is how a cleanup command turns
|
|
108
|
+
* into a data-loss command. The branch is left alone too, since it is still checked out here.
|
|
109
|
+
*/
|
|
110
|
+
private removalFailed;
|
|
111
|
+
private log;
|
|
112
|
+
/** The literal command that puts BOTH the directory and the branch back. */
|
|
113
|
+
restoreCommand(target: ReapedWorktree): string;
|
|
114
|
+
private revParse;
|
|
115
|
+
private capture;
|
|
116
|
+
}
|