@webpieces/repo-workflow-core 0.0.1
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 +31 -0
- package/src/agent-worktree-lock.d.ts +21 -0
- package/src/agent-worktree-lock.js +77 -0
- package/src/agent-worktree-lock.js.map +1 -0
- package/src/branch-archiver.d.ts +70 -0
- package/src/branch-archiver.js +157 -0
- package/src/branch-archiver.js.map +1 -0
- package/src/branch-identity.d.ts +6 -0
- package/src/branch-identity.js +59 -0
- package/src/branch-identity.js.map +1 -0
- package/src/branch-mutation-log.d.ts +77 -0
- package/src/branch-mutation-log.js +221 -0
- package/src/branch-mutation-log.js.map +1 -0
- package/src/branch-reaper.d.ts +122 -0
- package/src/branch-reaper.js +261 -0
- package/src/branch-reaper.js.map +1 -0
- package/src/harness-agent-activity.d.ts +154 -0
- package/src/harness-agent-activity.js +346 -0
- package/src/harness-agent-activity.js.map +1 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/main-sync-file.d.ts +121 -0
- package/src/main-sync-file.js +281 -0
- package/src/main-sync-file.js.map +1 -0
- package/src/main-sync-status.d.ts +197 -0
- package/src/main-sync-status.js +555 -0
- package/src/main-sync-status.js.map +1 -0
- package/src/merged-branch-verdicts.d.ts +189 -0
- package/src/merged-branch-verdicts.js +251 -0
- package/src/merged-branch-verdicts.js.map +1 -0
- package/src/merged-branches.d.ts +140 -0
- package/src/merged-branches.js +521 -0
- package/src/merged-branches.js.map +1 -0
- package/src/review-identity-stamp.d.ts +82 -0
- package/src/review-identity-stamp.js +172 -0
- package/src/review-identity-stamp.js.map +1 -0
- package/src/worktree-lock-verdicts.d.ts +149 -0
- package/src/worktree-lock-verdicts.js +225 -0
- package/src/worktree-lock-verdicts.js.map +1 -0
- package/src/worktree-reaper.d.ts +116 -0
- package/src/worktree-reaper.js +318 -0
- package/src/worktree-reaper.js.map +1 -0
- package/src/worktrees.d.ts +113 -0
- package/src/worktrees.js +260 -0
- package/src/worktrees.js.map +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AtomicFile } from '@webpieces/tooling-common';
|
|
2
|
+
import { DotWebpieces } from '@webpieces/tooling-common';
|
|
3
|
+
/** The ONE sanctioned way a reviewer submits a checklist verdict (issue #863). */
|
|
4
|
+
export declare const WRITE_REVIEW_BIN = "wp-write-review";
|
|
5
|
+
/** The flag that names the checklist a `wp-write-review` call submits for. */
|
|
6
|
+
export declare const WRITE_REVIEW_CHECKLIST_FLAG = "--checklist";
|
|
7
|
+
/** Where the stamps live, beneath this worktree's tooling-owned state dir (never the AI-writable one). */
|
|
8
|
+
export declare const REVIEW_STAMP_DIR = "review-stamps";
|
|
9
|
+
/**
|
|
10
|
+
* How long a stamp stays usable. The hook writes it a few milliseconds before the harness starts the very
|
|
11
|
+
* Bash command it stamped, so anything older than this is left over from a call that never reached the bin
|
|
12
|
+
* — and must not be available to lend its identity to a later caller.
|
|
13
|
+
*/
|
|
14
|
+
export declare const REVIEW_STAMP_MAX_AGE_MS = 120000;
|
|
15
|
+
/**
|
|
16
|
+
* WHO is running a `pnpm wp-write-review` Bash call, as the HARNESS reported it to the PreToolUse hook.
|
|
17
|
+
* Data-only (per CLAUDE.md), and its field names are the JSON keys on disk.
|
|
18
|
+
*
|
|
19
|
+
* WHY a hook stamp and not the environment (issue #863). A Bash-invoked bin sees only its env, and the env
|
|
20
|
+
* cannot answer "is this a subagent?" in either harness:
|
|
21
|
+
* - Claude Code exports `CLAUDE_CODE_SESSION_ID`, which is the PARENT session's id in a subagent too, and
|
|
22
|
+
* no agent id at all — a reviewer and its coordinator have byte-identical identity env.
|
|
23
|
+
* - Codex exports `CODEX_SESSION_ID` / `CODEX_THREAD_ID`, and nothing documents (or has been measured to
|
|
24
|
+
* show) that a spawned subagent gets a thread id of its own rather than inheriting the coordinator's.
|
|
25
|
+
* The hook payload DOES carry the answer, in both harnesses, on the same key: `agent_id` is empty for the
|
|
26
|
+
* main loop and populated for a subagent. So the hook — which sees every Bash call before it runs — writes
|
|
27
|
+
* that identity here for each checklist the command names, and the bin consumes it.
|
|
28
|
+
*
|
|
29
|
+
* NOT tamper-proof, and not claimed to be: an agent can write this file by hand. It moves forging a verdict
|
|
30
|
+
* from "write one JSON file" (what a coordinator did on two merged PRs) to a deliberate, auditable forgery of
|
|
31
|
+
* tooling state outside the AI-writable dir — the same bar SubagentProvenanceService sets.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ReviewIdentityStamp {
|
|
34
|
+
checklistId: string;
|
|
35
|
+
aiType: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
agentId: string;
|
|
38
|
+
agentType: string;
|
|
39
|
+
cwd: string;
|
|
40
|
+
stampedAt: string;
|
|
41
|
+
constructor(checklistId: string, aiType: string, sessionId: string, agentId: string, agentType: string, cwd: string, stampedAt: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.
|
|
45
|
+
*
|
|
46
|
+
* Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is
|
|
47
|
+
* keyed by git's answer for that tree — so the hook's payload cwd and the bin's process cwd land on the same
|
|
48
|
+
* file for the same worktree, and two worktrees never share a stamp.
|
|
49
|
+
*
|
|
50
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the
|
|
51
|
+
* instance the dependency-free hook adapter uses.
|
|
52
|
+
*/
|
|
53
|
+
export declare class ReviewIdentityStampService {
|
|
54
|
+
private readonly dotDir;
|
|
55
|
+
private readonly atomicFile;
|
|
56
|
+
constructor(dotDir?: DotWebpieces, atomicFile?: AtomicFile);
|
|
57
|
+
/**
|
|
58
|
+
* The checklist ids a Bash command submits verdicts for, or [] when it does not invoke the bin at all.
|
|
59
|
+
* Every `--checklist <id>` / `--checklist=<id>` is returned, because one reviewer covering several
|
|
60
|
+
* checklists may chain several submissions into one command.
|
|
61
|
+
*/
|
|
62
|
+
invokedChecklists(command: string): string[];
|
|
63
|
+
/**
|
|
64
|
+
* True when the stamp names the COORDINATING agent rather than a subagent: no agent id, or an agent id
|
|
65
|
+
* equal to the session id (a harness that fills the main loop's agent id with the session id instead of
|
|
66
|
+
* leaving it empty). The coordinator may never submit a reviewer's verdict — that is the whole point.
|
|
67
|
+
*/
|
|
68
|
+
isCoordinator(stamp: ReviewIdentityStamp): boolean;
|
|
69
|
+
stampPath(startDir: string, checklistId: string): string;
|
|
70
|
+
/** Best-effort: a stamp that cannot be written makes the bin refuse, which is the safe direction. */
|
|
71
|
+
write(startDir: string, stamp: ReviewIdentityStamp): void;
|
|
72
|
+
/**
|
|
73
|
+
* Read AND delete the stamp for one checklist. null when there is none, it is unreadable, or it is
|
|
74
|
+
* older than {@link REVIEW_STAMP_MAX_AGE_MS}. Deleted on read so one hook call vouches for one
|
|
75
|
+
* submission; a delete that fails (a sandbox that denies it) is harmless, because age bounds it anyway.
|
|
76
|
+
*/
|
|
77
|
+
take(startDir: string, checklistId: string, nowMs?: number): ReviewIdentityStamp | null;
|
|
78
|
+
private read;
|
|
79
|
+
private remove;
|
|
80
|
+
}
|
|
81
|
+
/** The instance the hook adapter uses — it runs without a DI container (see hook-core.ts). */
|
|
82
|
+
export declare const reviewIdentityStamps: ReviewIdentityStampService;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reviewIdentityStamps = exports.ReviewIdentityStampService = exports.ReviewIdentityStamp = exports.REVIEW_STAMP_MAX_AGE_MS = exports.REVIEW_STAMP_DIR = exports.WRITE_REVIEW_CHECKLIST_FLAG = exports.WRITE_REVIEW_BIN = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const inversify_1 = require("inversify");
|
|
7
|
+
const tooling_common_1 = require("@webpieces/tooling-common");
|
|
8
|
+
const tooling_common_2 = require("@webpieces/tooling-common");
|
|
9
|
+
const to_error_1 = require("@webpieces/tooling-common/to-error");
|
|
10
|
+
/** The ONE sanctioned way a reviewer submits a checklist verdict (issue #863). */
|
|
11
|
+
exports.WRITE_REVIEW_BIN = 'wp-write-review';
|
|
12
|
+
/** The flag that names the checklist a `wp-write-review` call submits for. */
|
|
13
|
+
exports.WRITE_REVIEW_CHECKLIST_FLAG = '--checklist';
|
|
14
|
+
/** Where the stamps live, beneath this worktree's tooling-owned state dir (never the AI-writable one). */
|
|
15
|
+
exports.REVIEW_STAMP_DIR = 'review-stamps';
|
|
16
|
+
/**
|
|
17
|
+
* How long a stamp stays usable. The hook writes it a few milliseconds before the harness starts the very
|
|
18
|
+
* Bash command it stamped, so anything older than this is left over from a call that never reached the bin
|
|
19
|
+
* — and must not be available to lend its identity to a later caller.
|
|
20
|
+
*/
|
|
21
|
+
exports.REVIEW_STAMP_MAX_AGE_MS = 120_000;
|
|
22
|
+
/**
|
|
23
|
+
* WHO is running a `pnpm wp-write-review` Bash call, as the HARNESS reported it to the PreToolUse hook.
|
|
24
|
+
* Data-only (per CLAUDE.md), and its field names are the JSON keys on disk.
|
|
25
|
+
*
|
|
26
|
+
* WHY a hook stamp and not the environment (issue #863). A Bash-invoked bin sees only its env, and the env
|
|
27
|
+
* cannot answer "is this a subagent?" in either harness:
|
|
28
|
+
* - Claude Code exports `CLAUDE_CODE_SESSION_ID`, which is the PARENT session's id in a subagent too, and
|
|
29
|
+
* no agent id at all — a reviewer and its coordinator have byte-identical identity env.
|
|
30
|
+
* - Codex exports `CODEX_SESSION_ID` / `CODEX_THREAD_ID`, and nothing documents (or has been measured to
|
|
31
|
+
* show) that a spawned subagent gets a thread id of its own rather than inheriting the coordinator's.
|
|
32
|
+
* The hook payload DOES carry the answer, in both harnesses, on the same key: `agent_id` is empty for the
|
|
33
|
+
* main loop and populated for a subagent. So the hook — which sees every Bash call before it runs — writes
|
|
34
|
+
* that identity here for each checklist the command names, and the bin consumes it.
|
|
35
|
+
*
|
|
36
|
+
* NOT tamper-proof, and not claimed to be: an agent can write this file by hand. It moves forging a verdict
|
|
37
|
+
* from "write one JSON file" (what a coordinator did on two merged PRs) to a deliberate, auditable forgery of
|
|
38
|
+
* tooling state outside the AI-writable dir — the same bar SubagentProvenanceService sets.
|
|
39
|
+
*/
|
|
40
|
+
class ReviewIdentityStamp {
|
|
41
|
+
checklistId;
|
|
42
|
+
aiType; // 'claude-code' | 'codex' — the adapter's answer, never the model's
|
|
43
|
+
sessionId;
|
|
44
|
+
agentId; // '' ⇒ the MAIN loop, i.e. the coordinating agent
|
|
45
|
+
agentType;
|
|
46
|
+
cwd;
|
|
47
|
+
stampedAt; // ISO
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
49
|
+
constructor(checklistId, aiType, sessionId, agentId, agentType, cwd, stampedAt) {
|
|
50
|
+
this.checklistId = checklistId;
|
|
51
|
+
this.aiType = aiType;
|
|
52
|
+
this.sessionId = sessionId;
|
|
53
|
+
this.agentId = agentId;
|
|
54
|
+
this.agentType = agentType;
|
|
55
|
+
this.cwd = cwd;
|
|
56
|
+
this.stampedAt = stampedAt;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.ReviewIdentityStamp = ReviewIdentityStamp;
|
|
60
|
+
/**
|
|
61
|
+
* Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.
|
|
62
|
+
*
|
|
63
|
+
* Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is
|
|
64
|
+
* keyed by git's answer for that tree — so the hook's payload cwd and the bin's process cwd land on the same
|
|
65
|
+
* file for the same worktree, and two worktrees never share a stamp.
|
|
66
|
+
*
|
|
67
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the
|
|
68
|
+
* instance the dependency-free hook adapter uses.
|
|
69
|
+
*/
|
|
70
|
+
let ReviewIdentityStampService = class ReviewIdentityStampService {
|
|
71
|
+
dotDir;
|
|
72
|
+
atomicFile;
|
|
73
|
+
constructor(dotDir = tooling_common_2.dotWebpieces, atomicFile = new tooling_common_1.AtomicFile()) {
|
|
74
|
+
this.dotDir = dotDir;
|
|
75
|
+
this.atomicFile = atomicFile;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The checklist ids a Bash command submits verdicts for, or [] when it does not invoke the bin at all.
|
|
79
|
+
* Every `--checklist <id>` / `--checklist=<id>` is returned, because one reviewer covering several
|
|
80
|
+
* checklists may chain several submissions into one command.
|
|
81
|
+
*/
|
|
82
|
+
invokedChecklists(command) {
|
|
83
|
+
if (!new RegExp(`\\b${exports.WRITE_REVIEW_BIN}\\b`).test(command))
|
|
84
|
+
return [];
|
|
85
|
+
const ids = [];
|
|
86
|
+
const flag = /--checklist(?:=|\s+)(["']?)([A-Za-z0-9._-]+)\1/g;
|
|
87
|
+
let match = flag.exec(command);
|
|
88
|
+
while (match !== null) {
|
|
89
|
+
if (!ids.includes(match[2]))
|
|
90
|
+
ids.push(match[2]);
|
|
91
|
+
match = flag.exec(command);
|
|
92
|
+
}
|
|
93
|
+
return ids;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* True when the stamp names the COORDINATING agent rather than a subagent: no agent id, or an agent id
|
|
97
|
+
* equal to the session id (a harness that fills the main loop's agent id with the session id instead of
|
|
98
|
+
* leaving it empty). The coordinator may never submit a reviewer's verdict — that is the whole point.
|
|
99
|
+
*/
|
|
100
|
+
isCoordinator(stamp) {
|
|
101
|
+
return stamp.agentId.trim() === '' || stamp.agentId === stamp.sessionId;
|
|
102
|
+
}
|
|
103
|
+
stampPath(startDir, checklistId) {
|
|
104
|
+
return this.dotDir.localFile(startDir, exports.REVIEW_STAMP_DIR, `${checklistId.replace(/[^A-Za-z0-9._-]/g, '_')}.json`);
|
|
105
|
+
}
|
|
106
|
+
/** Best-effort: a stamp that cannot be written makes the bin refuse, which is the safe direction. */
|
|
107
|
+
write(startDir, stamp) {
|
|
108
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: the hook must never fail a tool call over its own bookkeeping
|
|
109
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
110
|
+
try {
|
|
111
|
+
this.atomicFile.writeJsonAtomic(this.stampPath(startDir, stamp.checklistId), stamp);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const error = (0, to_error_1.toError)(err);
|
|
115
|
+
void error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read AND delete the stamp for one checklist. null when there is none, it is unreadable, or it is
|
|
120
|
+
* older than {@link REVIEW_STAMP_MAX_AGE_MS}. Deleted on read so one hook call vouches for one
|
|
121
|
+
* submission; a delete that fails (a sandbox that denies it) is harmless, because age bounds it anyway.
|
|
122
|
+
*/
|
|
123
|
+
take(startDir, checklistId, nowMs = Date.now()) {
|
|
124
|
+
const file = this.stampPath(startDir, checklistId);
|
|
125
|
+
const stamp = this.read(file);
|
|
126
|
+
this.remove(file);
|
|
127
|
+
if (stamp === null || stamp.checklistId !== checklistId)
|
|
128
|
+
return null;
|
|
129
|
+
const age = nowMs - Date.parse(stamp.stampedAt);
|
|
130
|
+
if (!Number.isFinite(age) || age < 0 || age > exports.REVIEW_STAMP_MAX_AGE_MS)
|
|
131
|
+
return null;
|
|
132
|
+
return stamp;
|
|
133
|
+
}
|
|
134
|
+
read(file) {
|
|
135
|
+
if (!fs.existsSync(file))
|
|
136
|
+
return null;
|
|
137
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable stamp is no stamp
|
|
138
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
139
|
+
try {
|
|
140
|
+
// webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field below
|
|
141
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
142
|
+
// webpieces-disable no-any-unknown -- one opaque JSON value, narrowed to a string
|
|
143
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
144
|
+
return new ReviewIdentityStamp(str(raw['checklistId']), str(raw['aiType']), str(raw['sessionId']), str(raw['agentId']), str(raw['agentType']), str(raw['cwd']), str(raw['stampedAt']));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const error = (0, to_error_1.toError)(err);
|
|
148
|
+
void error;
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
remove(file) {
|
|
153
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: a stamp that cannot be deleted still expires
|
|
154
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
155
|
+
try {
|
|
156
|
+
fs.rmSync(file, { force: true });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const error = (0, to_error_1.toError)(err);
|
|
160
|
+
void error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
exports.ReviewIdentityStampService = ReviewIdentityStampService;
|
|
165
|
+
exports.ReviewIdentityStampService = ReviewIdentityStampService = tslib_1.__decorate([
|
|
166
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
167
|
+
tslib_1.__metadata("design:paramtypes", [tooling_common_2.DotWebpieces,
|
|
168
|
+
tooling_common_1.AtomicFile])
|
|
169
|
+
], ReviewIdentityStampService);
|
|
170
|
+
/** The instance the hook adapter uses — it runs without a DI container (see hook-core.ts). */
|
|
171
|
+
exports.reviewIdentityStamps = new ReviewIdentityStampService();
|
|
172
|
+
//# sourceMappingURL=review-identity-stamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-identity-stamp.js","sourceRoot":"","sources":["../../../../../packages/tooling/repo-workflow-core/src/review-identity-stamp.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AtomicFile } from '@webpieces/tooling-common';\nimport { DotWebpieces, dotWebpieces } from '@webpieces/tooling-common';\nimport { toError } from '@webpieces/tooling-common/to-error';\n\n/** The ONE sanctioned way a reviewer submits a checklist verdict (issue #863). */\nexport const WRITE_REVIEW_BIN = 'wp-write-review';\n\n/** The flag that names the checklist a `wp-write-review` call submits for. */\nexport const WRITE_REVIEW_CHECKLIST_FLAG = '--checklist';\n\n/** Where the stamps live, beneath this worktree's tooling-owned state dir (never the AI-writable one). */\nexport const REVIEW_STAMP_DIR = 'review-stamps';\n\n/**\n * How long a stamp stays usable. The hook writes it a few milliseconds before the harness starts the very\n * Bash command it stamped, so anything older than this is left over from a call that never reached the bin\n * — and must not be available to lend its identity to a later caller.\n */\nexport const REVIEW_STAMP_MAX_AGE_MS = 120_000;\n\n/**\n * WHO is running a `pnpm wp-write-review` Bash call, as the HARNESS reported it to the PreToolUse hook.\n * Data-only (per CLAUDE.md), and its field names are the JSON keys on disk.\n *\n * WHY a hook stamp and not the environment (issue #863). A Bash-invoked bin sees only its env, and the env\n * cannot answer \"is this a subagent?\" in either harness:\n * - Claude Code exports `CLAUDE_CODE_SESSION_ID`, which is the PARENT session's id in a subagent too, and\n * no agent id at all — a reviewer and its coordinator have byte-identical identity env.\n * - Codex exports `CODEX_SESSION_ID` / `CODEX_THREAD_ID`, and nothing documents (or has been measured to\n * show) that a spawned subagent gets a thread id of its own rather than inheriting the coordinator's.\n * The hook payload DOES carry the answer, in both harnesses, on the same key: `agent_id` is empty for the\n * main loop and populated for a subagent. So the hook — which sees every Bash call before it runs — writes\n * that identity here for each checklist the command names, and the bin consumes it.\n *\n * NOT tamper-proof, and not claimed to be: an agent can write this file by hand. It moves forging a verdict\n * from \"write one JSON file\" (what a coordinator did on two merged PRs) to a deliberate, auditable forgery of\n * tooling state outside the AI-writable dir — the same bar SubagentProvenanceService sets.\n */\nexport class ReviewIdentityStamp {\n checklistId: string;\n aiType: string; // 'claude-code' | 'codex' — the adapter's answer, never the model's\n sessionId: string;\n agentId: string; // '' ⇒ the MAIN loop, i.e. the coordinating agent\n agentType: string;\n cwd: string;\n stampedAt: string; // ISO\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(checklistId: string, aiType: string, sessionId: string, agentId: string, agentType: string, cwd: string, stampedAt: string) {\n this.checklistId = checklistId;\n this.aiType = aiType;\n this.sessionId = sessionId;\n this.agentId = agentId;\n this.agentType = agentType;\n this.cwd = cwd;\n this.stampedAt = stampedAt;\n }\n}\n\n/**\n * Writes (from the PreToolUse hook) and consumes (from `wp-write-review`) {@link ReviewIdentityStamp}s.\n *\n * Both ends resolve the path from their OWN working directory through {@link DotWebpieces.local}, which is\n * keyed by git's answer for that tree — so the hook's payload cwd and the bin's process cwd land on the same\n * file for the same worktree, and two worktrees never share a stamp.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type; {@link reviewIdentityStamps} is the\n * instance the dependency-free hook adapter uses.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewIdentityStampService {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n ) {}\n\n /**\n * The checklist ids a Bash command submits verdicts for, or [] when it does not invoke the bin at all.\n * Every `--checklist <id>` / `--checklist=<id>` is returned, because one reviewer covering several\n * checklists may chain several submissions into one command.\n */\n invokedChecklists(command: string): string[] {\n if (!new RegExp(`\\\\b${WRITE_REVIEW_BIN}\\\\b`).test(command)) return [];\n const ids: string[] = [];\n const flag = /--checklist(?:=|\\s+)([\"']?)([A-Za-z0-9._-]+)\\1/g;\n let match = flag.exec(command);\n while (match !== null) {\n if (!ids.includes(match[2])) ids.push(match[2]);\n match = flag.exec(command);\n }\n return ids;\n }\n\n /**\n * True when the stamp names the COORDINATING agent rather than a subagent: no agent id, or an agent id\n * equal to the session id (a harness that fills the main loop's agent id with the session id instead of\n * leaving it empty). The coordinator may never submit a reviewer's verdict — that is the whole point.\n */\n isCoordinator(stamp: ReviewIdentityStamp): boolean {\n return stamp.agentId.trim() === '' || stamp.agentId === stamp.sessionId;\n }\n\n stampPath(startDir: string, checklistId: string): string {\n return this.dotDir.localFile(startDir, REVIEW_STAMP_DIR, `${checklistId.replace(/[^A-Za-z0-9._-]/g, '_')}.json`);\n }\n\n /** Best-effort: a stamp that cannot be written makes the bin refuse, which is the safe direction. */\n write(startDir: string, stamp: ReviewIdentityStamp): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the hook must never fail a tool call over its own bookkeeping\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n this.atomicFile.writeJsonAtomic(this.stampPath(startDir, stamp.checklistId), stamp);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Read AND delete the stamp for one checklist. null when there is none, it is unreadable, or it is\n * older than {@link REVIEW_STAMP_MAX_AGE_MS}. Deleted on read so one hook call vouches for one\n * submission; a delete that fails (a sandbox that denies it) is harmless, because age bounds it anyway.\n */\n take(startDir: string, checklistId: string, nowMs: number = Date.now()): ReviewIdentityStamp | null {\n const file = this.stampPath(startDir, checklistId);\n const stamp = this.read(file);\n this.remove(file);\n if (stamp === null || stamp.checklistId !== checklistId) return null;\n const age = nowMs - Date.parse(stamp.stampedAt);\n if (!Number.isFinite(age) || age < 0 || age > REVIEW_STAMP_MAX_AGE_MS) return null;\n return stamp;\n }\n\n private read(file: string): ReviewIdentityStamp | null {\n if (!fs.existsSync(file)) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable stamp is no stamp\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field below\n const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Record<string, unknown>;\n // webpieces-disable no-any-unknown -- one opaque JSON value, narrowed to a string\n const str = (v: unknown): string => (typeof v === 'string' ? v : '');\n return new ReviewIdentityStamp(\n str(raw['checklistId']), str(raw['aiType']), str(raw['sessionId']), str(raw['agentId']),\n str(raw['agentType']), str(raw['cwd']), str(raw['stampedAt']));\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n private remove(file: string): void {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a stamp that cannot be deleted still expires\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(file, { force: true });\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n/** The instance the hook adapter uses — it runs without a DI container (see hook-core.ts). */\nexport const reviewIdentityStamps = new ReviewIdentityStampService();\n"]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { AgentWorktreeLock, AgentWorktreeLockReader } from './agent-worktree-lock';
|
|
2
|
+
import { AgentActivity, HarnessAgentActivityReader } from './harness-agent-activity';
|
|
3
|
+
import { DeletableWorktree } from './merged-branch-verdicts';
|
|
4
|
+
import { Worktree, WorktreeService } from './worktrees';
|
|
5
|
+
/**
|
|
6
|
+
* What a `git worktree lock` DOES to a worktree's cleanup verdict.
|
|
7
|
+
*
|
|
8
|
+
* Separate from agent-worktree-lock.ts (which only parses a lock reason) and from
|
|
9
|
+
* harness-agent-activity.ts (which only asks the harness about an agent): this is the POLICY built on
|
|
10
|
+
* top of that evidence — what stops a reap, what a spared lock is allowed to CLAIM about who took it,
|
|
11
|
+
* and what an overridden one has to leave on the verdict so the reaper knows to clear it.
|
|
12
|
+
*
|
|
13
|
+
* ─── WHY THE POLICY CHANGED ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
* It used to be one question, asked of the kernel: is the pid in the lock reason still running? That
|
|
15
|
+
* pid is the Claude Code SESSION process, shared by every subagent, so the answer was yes for the
|
|
16
|
+
* whole life of a session and every agent worktree was spared forever — nine against a cap of five,
|
|
17
|
+
* with one agent running, each carrying the sentence "that agent is working in here" about agents
|
|
18
|
+
* whose PRs had already merged. Both halves were defects: the reap never happened, and the message
|
|
19
|
+
* asserted something nobody knew.
|
|
20
|
+
*
|
|
21
|
+
* ─── THE DECISION TABLE, AND THE ONE RULE BEHIND IT ──────────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* HARNESS STATE MAY ONLY VETO A REAP. IT MAY NEVER LICENSE ONE.
|
|
24
|
+
*
|
|
25
|
+
* The licence comes solely from the branch/commit evidence wp-cleanup already computes and already
|
|
26
|
+
* gets right. So, for a lock this repo can read as the harness's own:
|
|
27
|
+
*
|
|
28
|
+
* branch evidence does NOT say deletable → SPARE. The harness is not even consulted; it
|
|
29
|
+
* cannot promote a spare into a reap.
|
|
30
|
+
* deletable, but the worktree is DIRTY → SPARE, unconditionally. Uncommitted or
|
|
31
|
+
* untracked files are work no archive tag can
|
|
32
|
+
* bring back. This one overrides everything,
|
|
33
|
+
* including the explicit flag.
|
|
34
|
+
* deletable, clean, harness says the agent is
|
|
35
|
+
* mid-tool-call and writing → SPARE. The veto.
|
|
36
|
+
* deletable, clean, harness says returned /
|
|
37
|
+
* long-stale / cannot tell → judge it on its branch like any unlocked tree,
|
|
38
|
+
* clearing the lock on the way out.
|
|
39
|
+
*
|
|
40
|
+
* `--ignore-stale-locks` moves ONE line of that table: it lets a locked worktree whose branch is not
|
|
41
|
+
* provably dead be classified on its real branch and commit state anyway (so a zero-commit husk gets
|
|
42
|
+
* reaped and a worktree holding unique commits reaches the numbered block a human can answer). It
|
|
43
|
+
* does not touch the dirty rail or the live-agent veto, because neither of those is what the caller
|
|
44
|
+
* is complaining about when they pass it.
|
|
45
|
+
*
|
|
46
|
+
* ─── SCOPE: AGENT LOCKS ONLY ─────────────────────────────────────────────────────────────────────
|
|
47
|
+
* Every override applies ONLY to a lock this file can read as the harness's own. A lock reason naming
|
|
48
|
+
* anything else — a human's "dean is debugging", or nothing at all — is still an instruction from
|
|
49
|
+
* somebody, and wp-cleanup still obeys it.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* The exact phrase a spared "we cannot tell" verdict carries, exported so wp-cleanup can spot those
|
|
53
|
+
* worktrees and offer the flag that judges them on their branch instead.
|
|
54
|
+
*
|
|
55
|
+
* ONE spelling, shared: the alternative is the caller re-deriving the same condition from a second
|
|
56
|
+
* copy of the sentence, which is how a message and the code that keys off it drift apart.
|
|
57
|
+
*/
|
|
58
|
+
export declare const LOCK_LIVENESS_UNVERIFIABLE = "whether that agent is still running cannot be verified";
|
|
59
|
+
/**
|
|
60
|
+
* The stand-in for "the harness was never asked", used on the paths where asking could not change the
|
|
61
|
+
* answer. It is UNKNOWN, so a caller that consults it anyway still fails safe.
|
|
62
|
+
*/
|
|
63
|
+
export declare const HARNESS_NOT_CONSULTED: AgentActivity;
|
|
64
|
+
export declare class LockEvidence {
|
|
65
|
+
/** The lock reason verbatim, straight out of `git worktree list --porcelain`. */
|
|
66
|
+
lockReason: string;
|
|
67
|
+
/** What the harness says about the agent named in that reason, or HARNESS_NOT_CONSULTED. */
|
|
68
|
+
activity: AgentActivity;
|
|
69
|
+
/** Does `git status --porcelain` report anything, or could it not answer? Either way: held. */
|
|
70
|
+
workInFlight: boolean;
|
|
71
|
+
/** Why it is held, in git's terms — printed verbatim, because the two reasons differ for a human. */
|
|
72
|
+
workInFlightReason: string;
|
|
73
|
+
/** Is the branch this worktree holds ALREADY provably dead by the ordinary verdicts? */
|
|
74
|
+
branchDeletable: boolean;
|
|
75
|
+
/** The caller said to treat a standing agent lock as no evidence at all. */
|
|
76
|
+
ignoreStaleAgentLocks: boolean;
|
|
77
|
+
constructor(lockReason: string, activity: AgentActivity, workInFlight: boolean, workInFlightReason: string, branchDeletable: boolean, ignoreStaleAgentLocks: boolean);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Data-only. The one verdict this file produces: spare or override, the sentence that says WHY, and
|
|
81
|
+
* the agent lock the reaper will have to clear if the worktree goes.
|
|
82
|
+
*
|
|
83
|
+
* ONE class rather than a spare-path method and an override-path method, because the two answers are
|
|
84
|
+
* mutually exclusive readings of the same evidence, and two methods is how they drift apart — which
|
|
85
|
+
* is precisely what happened before: the old spared message and the old stale-lock test were computed
|
|
86
|
+
* from the same pid by different code, and only one of them was ever corrected.
|
|
87
|
+
*/
|
|
88
|
+
export declare class LockDecision {
|
|
89
|
+
/** True: leave this worktree alone, and `reason` is the whole story. */
|
|
90
|
+
spare: boolean;
|
|
91
|
+
/** Spared: why it was left. Overridden: the clause appended to the branch's own verdict. */
|
|
92
|
+
reason: string;
|
|
93
|
+
/** The agent lock to clear on the way out — null when nothing may be cleared. */
|
|
94
|
+
lock: AgentWorktreeLock | null;
|
|
95
|
+
constructor(spare: boolean, reason: string, lock: AgentWorktreeLock | null);
|
|
96
|
+
}
|
|
97
|
+
export declare class WorktreeLockVerdicts {
|
|
98
|
+
private readonly agentLocks;
|
|
99
|
+
private readonly harness;
|
|
100
|
+
private readonly worktrees;
|
|
101
|
+
constructor(agentLocks?: AgentWorktreeLockReader, harness?: HarnessAgentActivityReader, worktrees?: WorktreeService);
|
|
102
|
+
/**
|
|
103
|
+
* Collect what `decide` rules on — and, just as deliberately, DECLINE to collect the two
|
|
104
|
+
* expensive facts when they cannot change the answer.
|
|
105
|
+
*
|
|
106
|
+
* `git status --porcelain` and a walk of the harness's state tree are gathered only when a reap
|
|
107
|
+
* is otherwise on the table: the lock is one this repo can read as the harness's own, and either
|
|
108
|
+
* the branch is already provably dead or the caller asked for locks to be ignored. That is not an
|
|
109
|
+
* optimisation dressed up as a rule — harness state may only VETO a reap, never license one, so on
|
|
110
|
+
* a path with no reap to veto there is nothing to ask, and asking would invite a later reader to
|
|
111
|
+
* let the answer decide something.
|
|
112
|
+
*/
|
|
113
|
+
gather(tree: Worktree, branchDeletable: boolean, ignoreStaleAgentLocks: boolean): LockEvidence;
|
|
114
|
+
/**
|
|
115
|
+
* Spare this locked worktree, or judge it on its branch like any unlocked one — with the sentence
|
|
116
|
+
* that says which piece of evidence decided it.
|
|
117
|
+
*
|
|
118
|
+
* The order below IS the decision table in the class header, in the same order, for the same
|
|
119
|
+
* reasons. Read it there.
|
|
120
|
+
*/
|
|
121
|
+
decide(evidence: LockEvidence): LockDecision;
|
|
122
|
+
/**
|
|
123
|
+
* WHY a locked worktree was left alone when the lock is NOT the harness's — reporting what the
|
|
124
|
+
* reason SAYS, never who we imagine wrote it.
|
|
125
|
+
*
|
|
126
|
+
* This used to read `locked by a human — do not touch` for every lock in existence, which named an
|
|
127
|
+
* actor nothing in the evidence identifies: the lock reason is the ONLY thing we have, a human may
|
|
128
|
+
* have locked it, so may some other tool, and asserting either is the same defect whichever way it
|
|
129
|
+
* lands.
|
|
130
|
+
*/
|
|
131
|
+
private foreignLockReason;
|
|
132
|
+
/**
|
|
133
|
+
* The sentence that replaced "pid N still running — that agent is working in here".
|
|
134
|
+
*
|
|
135
|
+
* It says only what is known: which agent the reason names, that the pid in it cannot answer the
|
|
136
|
+
* question because it is the shared session process, and that nothing else made the worktree
|
|
137
|
+
* provably dead either. No claim about anybody working — and no claim about the harness, which on
|
|
138
|
+
* this path was deliberately never asked, because its answer could not have changed the outcome.
|
|
139
|
+
*/
|
|
140
|
+
private unverifiableReason;
|
|
141
|
+
/**
|
|
142
|
+
* Stamp an overridden verdict with the lock the reap must CLEAR: git refuses to remove a locked
|
|
143
|
+
* worktree, and the reason has to say why a lock is being overridden at all.
|
|
144
|
+
*
|
|
145
|
+
* Applied to every verdict the fall-through can produce, not just the deletable one, because the
|
|
146
|
+
* spared cases are exactly where a human reads the reason and asks "wasn't that locked?".
|
|
147
|
+
*/
|
|
148
|
+
annotate(verdict: DeletableWorktree, decision: LockDecision | null): DeletableWorktree;
|
|
149
|
+
}
|