@webpieces/rules-config 0.4.709 → 0.4.710

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.
@@ -1,46 +1,149 @@
1
1
  import { AgentWorktreeLock, AgentWorktreeLockReader } from './agent-worktree-lock';
2
+ import { AgentActivity, HarnessAgentActivityReader } from './harness-agent-activity';
2
3
  import { DeletableWorktree } from './merged-branch-verdicts';
3
- import { Worktree } from './worktrees';
4
+ import { Worktree, WorktreeService } from './worktrees';
4
5
  /**
5
6
  * What a `git worktree lock` DOES to a worktree's cleanup verdict.
6
7
  *
7
- * Separate from agent-worktree-lock.ts, which only reads a lock reason and asks the kernel about a
8
- * pid: this is the policy built on top of that evidence which locks stop a reap, what a spared lock
9
- * is allowed to claim about who took it, and what a stale one has to leave behind on the verdict so
10
- * the reaper knows a lock is standing in its way.
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.
11
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
+ }
12
97
  export declare class WorktreeLockVerdicts {
13
98
  private readonly agentLocks;
14
- constructor(agentLocks?: AgentWorktreeLockReader);
99
+ private readonly harness;
100
+ private readonly worktrees;
101
+ constructor(agentLocks?: AgentWorktreeLockReader, harness?: HarnessAgentActivityReader, worktrees?: WorktreeService);
15
102
  /**
16
- * The lock on this worktree was taken by a Claude agent whose process is GONE — or null, meaning
17
- * the lock is still standing for something and the worktree must be spared.
103
+ * Collect what `decide` rules on and, just as deliberately, DECLINE to collect the two
104
+ * expensive facts when they cannot change the answer.
18
105
  *
19
- * Null covers three genuinely different situations that all end the same way: a live agent, a
20
- * reason we do not recognise, and no reason at all. `sparedReason` is what tells them apart for
21
- * the reader; here they are one answer, "do not touch".
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.
22
112
  */
23
- staleAgentLock(tree: Worktree): AgentWorktreeLock | null;
113
+ gather(tree: Worktree, branchDeletable: boolean, ignoreStaleAgentLocks: boolean): LockEvidence;
24
114
  /**
25
- * WHY a locked worktree was left alone reporting what the lock SAYS, never who we imagine wrote
26
- * it.
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.
27
117
  *
28
- * This used to read `locked by a human do not touch` for every lock in existence, which was
29
- * wrong twice over. It was factually wrong about the agent harness's own locks, which is what let
30
- * every `/full-cycle` worktree pile up forever. And it named an actor nothing in the evidence
31
- * identifies: the lock reason is the ONLY thing we have, a human may have locked it, so may some
32
- * other tool, and asserting either is the same defect whichever way it lands.
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".
33
134
  *
34
- * So: name the agent and pid when the reason ASSERTS them, and otherwise quote the reason back
35
- * verbatim and say plainly that we cannot tell who locked it.
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.
36
139
  */
37
- sparedReason(tree: Worktree): string;
140
+ private unverifiableReason;
38
141
  /**
39
- * Stamp a verdict with the stale lock on its worktree: the lock has to be CLEARED before git will
40
- * remove the directory, and the reason has to say why a lock is being overridden at all.
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.
41
144
  *
42
145
  * Applied to every verdict the fall-through can produce, not just the deletable one, because the
43
146
  * spared cases are exactly where a human reads the reason and asks "wasn't that locked?".
44
147
  */
45
- annotate(verdict: DeletableWorktree, lock: AgentWorktreeLock | null): DeletableWorktree;
148
+ annotate(verdict: DeletableWorktree, decision: LockDecision | null): DeletableWorktree;
46
149
  }
@@ -1,80 +1,225 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WorktreeLockVerdicts = void 0;
3
+ exports.WorktreeLockVerdicts = exports.LockDecision = exports.LockEvidence = exports.HARNESS_NOT_CONSULTED = exports.LOCK_LIVENESS_UNVERIFIABLE = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const inversify_1 = require("inversify");
6
6
  const agent_worktree_lock_1 = require("./agent-worktree-lock");
7
+ const harness_agent_activity_1 = require("./harness-agent-activity");
8
+ const worktrees_1 = require("./worktrees");
7
9
  /**
8
10
  * What a `git worktree lock` DOES to a worktree's cleanup verdict.
9
11
  *
10
- * Separate from agent-worktree-lock.ts, which only reads a lock reason and asks the kernel about a
11
- * pid: this is the policy built on top of that evidence which locks stop a reap, what a spared lock
12
- * is allowed to claim about who took it, and what a stale one has to leave behind on the verdict so
13
- * the reaper knows a lock is standing in its way.
12
+ * Separate from agent-worktree-lock.ts (which only parses a lock reason) and from
13
+ * harness-agent-activity.ts (which only asks the harness about an agent): this is the POLICY built on
14
+ * top of that evidence what stops a reap, what a spared lock is allowed to CLAIM about who took it,
15
+ * and what an overridden one has to leave on the verdict so the reaper knows to clear it.
16
+ *
17
+ * ─── WHY THE POLICY CHANGED ──────────────────────────────────────────────────────────────────────
18
+ * It used to be one question, asked of the kernel: is the pid in the lock reason still running? That
19
+ * pid is the Claude Code SESSION process, shared by every subagent, so the answer was yes for the
20
+ * whole life of a session and every agent worktree was spared forever — nine against a cap of five,
21
+ * with one agent running, each carrying the sentence "that agent is working in here" about agents
22
+ * whose PRs had already merged. Both halves were defects: the reap never happened, and the message
23
+ * asserted something nobody knew.
24
+ *
25
+ * ─── THE DECISION TABLE, AND THE ONE RULE BEHIND IT ──────────────────────────────────────────────
26
+ *
27
+ * HARNESS STATE MAY ONLY VETO A REAP. IT MAY NEVER LICENSE ONE.
28
+ *
29
+ * The licence comes solely from the branch/commit evidence wp-cleanup already computes and already
30
+ * gets right. So, for a lock this repo can read as the harness's own:
31
+ *
32
+ * branch evidence does NOT say deletable → SPARE. The harness is not even consulted; it
33
+ * cannot promote a spare into a reap.
34
+ * deletable, but the worktree is DIRTY → SPARE, unconditionally. Uncommitted or
35
+ * untracked files are work no archive tag can
36
+ * bring back. This one overrides everything,
37
+ * including the explicit flag.
38
+ * deletable, clean, harness says the agent is
39
+ * mid-tool-call and writing → SPARE. The veto.
40
+ * deletable, clean, harness says returned /
41
+ * long-stale / cannot tell → judge it on its branch like any unlocked tree,
42
+ * clearing the lock on the way out.
43
+ *
44
+ * `--ignore-stale-locks` moves ONE line of that table: it lets a locked worktree whose branch is not
45
+ * provably dead be classified on its real branch and commit state anyway (so a zero-commit husk gets
46
+ * reaped and a worktree holding unique commits reaches the numbered block a human can answer). It
47
+ * does not touch the dirty rail or the live-agent veto, because neither of those is what the caller
48
+ * is complaining about when they pass it.
49
+ *
50
+ * ─── SCOPE: AGENT LOCKS ONLY ─────────────────────────────────────────────────────────────────────
51
+ * Every override applies ONLY to a lock this file can read as the harness's own. A lock reason naming
52
+ * anything else — a human's "dean is debugging", or nothing at all — is still an instruction from
53
+ * somebody, and wp-cleanup still obeys it.
14
54
  */
55
+ /**
56
+ * The exact phrase a spared "we cannot tell" verdict carries, exported so wp-cleanup can spot those
57
+ * worktrees and offer the flag that judges them on their branch instead.
58
+ *
59
+ * ONE spelling, shared: the alternative is the caller re-deriving the same condition from a second
60
+ * copy of the sentence, which is how a message and the code that keys off it drift apart.
61
+ */
62
+ exports.LOCK_LIVENESS_UNVERIFIABLE = 'whether that agent is still running cannot be verified';
63
+ /**
64
+ * The stand-in for "the harness was never asked", used on the paths where asking could not change the
65
+ * answer. It is UNKNOWN, so a caller that consults it anyway still fails safe.
66
+ */
67
+ exports.HARNESS_NOT_CONSULTED = new harness_agent_activity_1.AgentActivity(harness_agent_activity_1.AGENT_ACTIVITY_UNKNOWN, 'the harness was not consulted');
68
+ // Data-only (per CLAUDE.md, classes for data). Everything `decide` rules on, as one value — see
69
+ // `gather`, which is the only thing that builds one outside a test.
70
+ class LockEvidence {
71
+ /** The lock reason verbatim, straight out of `git worktree list --porcelain`. */
72
+ lockReason;
73
+ /** What the harness says about the agent named in that reason, or HARNESS_NOT_CONSULTED. */
74
+ activity;
75
+ /** Does `git status --porcelain` report anything, or could it not answer? Either way: held. */
76
+ workInFlight;
77
+ /** Why it is held, in git's terms — printed verbatim, because the two reasons differ for a human. */
78
+ workInFlightReason;
79
+ /** Is the branch this worktree holds ALREADY provably dead by the ordinary verdicts? */
80
+ branchDeletable;
81
+ /** The caller said to treat a standing agent lock as no evidence at all. */
82
+ ignoreStaleAgentLocks;
83
+ constructor(lockReason, activity, workInFlight, workInFlightReason, branchDeletable, ignoreStaleAgentLocks) {
84
+ this.lockReason = lockReason;
85
+ this.activity = activity;
86
+ this.workInFlight = workInFlight;
87
+ this.workInFlightReason = workInFlightReason;
88
+ this.branchDeletable = branchDeletable;
89
+ this.ignoreStaleAgentLocks = ignoreStaleAgentLocks;
90
+ }
91
+ }
92
+ exports.LockEvidence = LockEvidence;
93
+ /**
94
+ * Data-only. The one verdict this file produces: spare or override, the sentence that says WHY, and
95
+ * the agent lock the reaper will have to clear if the worktree goes.
96
+ *
97
+ * ONE class rather than a spare-path method and an override-path method, because the two answers are
98
+ * mutually exclusive readings of the same evidence, and two methods is how they drift apart — which
99
+ * is precisely what happened before: the old spared message and the old stale-lock test were computed
100
+ * from the same pid by different code, and only one of them was ever corrected.
101
+ */
102
+ class LockDecision {
103
+ /** True: leave this worktree alone, and `reason` is the whole story. */
104
+ spare;
105
+ /** Spared: why it was left. Overridden: the clause appended to the branch's own verdict. */
106
+ reason;
107
+ /** The agent lock to clear on the way out — null when nothing may be cleared. */
108
+ lock;
109
+ constructor(spare, reason, lock) {
110
+ this.spare = spare;
111
+ this.reason = reason;
112
+ this.lock = lock;
113
+ }
114
+ }
115
+ exports.LockDecision = LockDecision;
15
116
  let WorktreeLockVerdicts = class WorktreeLockVerdicts {
16
117
  agentLocks;
17
- constructor(agentLocks = new agent_worktree_lock_1.AgentWorktreeLockReader()) {
118
+ harness;
119
+ worktrees;
120
+ // Defaulted like BranchReaper's collaborators, so the non-DI call sites can just
121
+ // `new WorktreeLockVerdicts()` while inversify still injects the singletons from a container.
122
+ constructor(agentLocks = new agent_worktree_lock_1.AgentWorktreeLockReader(), harness = new harness_agent_activity_1.HarnessAgentActivityReader(), worktrees = new worktrees_1.WorktreeService()) {
18
123
  this.agentLocks = agentLocks;
124
+ this.harness = harness;
125
+ this.worktrees = worktrees;
19
126
  }
20
127
  /**
21
- * The lock on this worktree was taken by a Claude agent whose process is GONE — or null, meaning
22
- * the lock is still standing for something and the worktree must be spared.
128
+ * Collect what `decide` rules on and, just as deliberately, DECLINE to collect the two
129
+ * expensive facts when they cannot change the answer.
23
130
  *
24
- * Null covers three genuinely different situations that all end the same way: a live agent, a
25
- * reason we do not recognise, and no reason at all. `sparedReason` is what tells them apart for
26
- * the reader; here they are one answer, "do not touch".
131
+ * `git status --porcelain` and a walk of the harness's state tree are gathered only when a reap
132
+ * is otherwise on the table: the lock is one this repo can read as the harness's own, and either
133
+ * the branch is already provably dead or the caller asked for locks to be ignored. That is not an
134
+ * optimisation dressed up as a rule — harness state may only VETO a reap, never license one, so on
135
+ * a path with no reap to veto there is nothing to ask, and asking would invite a later reader to
136
+ * let the answer decide something.
27
137
  */
28
- staleAgentLock(tree) {
138
+ gather(tree, branchDeletable, ignoreStaleAgentLocks) {
29
139
  const lock = this.agentLocks.parse(tree.lockReason);
30
- if (lock === null)
31
- return null;
32
- return this.agentLocks.isRunning(lock) ? null : lock;
140
+ if (lock === null || (!branchDeletable && !ignoreStaleAgentLocks)) {
141
+ return new LockEvidence(tree.lockReason, exports.HARNESS_NOT_CONSULTED, false, '', branchDeletable, ignoreStaleAgentLocks);
142
+ }
143
+ const held = this.worktrees.workInFlight(tree.path);
144
+ return new LockEvidence(tree.lockReason, this.harness.activityOf(lock.agent, tree.path), held.held, held.reason, branchDeletable, ignoreStaleAgentLocks);
33
145
  }
34
146
  /**
35
- * WHY a locked worktree was left alone reporting what the lock SAYS, never who we imagine wrote
36
- * it.
37
- *
38
- * This used to read `locked by a human — do not touch` for every lock in existence, which was
39
- * wrong twice over. It was factually wrong about the agent harness's own locks, which is what let
40
- * every `/full-cycle` worktree pile up forever. And it named an actor nothing in the evidence
41
- * identifies: the lock reason is the ONLY thing we have, a human may have locked it, so may some
42
- * other tool, and asserting either is the same defect whichever way it lands.
147
+ * Spare this locked worktree, or judge it on its branch like any unlocked one with the sentence
148
+ * that says which piece of evidence decided it.
43
149
  *
44
- * So: name the agent and pid when the reason ASSERTS them, and otherwise quote the reason back
45
- * verbatim and say plainly that we cannot tell who locked it.
150
+ * The order below IS the decision table in the class header, in the same order, for the same
151
+ * reasons. Read it there.
46
152
  */
47
- sparedReason(tree) {
48
- const lock = this.agentLocks.parse(tree.lockReason);
49
- if (lock !== null) {
50
- return `locked by claude agent ${lock.agent}, pid ${String(lock.pid)} still running — `
51
- + 'that agent is working in here';
153
+ decide(evidence) {
154
+ const lock = this.agentLocks.parse(evidence.lockReason);
155
+ if (lock === null)
156
+ return new LockDecision(true, this.foreignLockReason(evidence.lockReason), null);
157
+ if (!evidence.branchDeletable && !evidence.ignoreStaleAgentLocks) {
158
+ return new LockDecision(true, this.unverifiableReason(lock), null);
52
159
  }
53
- if (tree.lockReason === '') {
160
+ if (evidence.workInFlight) {
161
+ return new LockDecision(true, `locked by claude agent ${lock.agent}, and the worktree ${evidence.workInFlightReason} — `
162
+ + 'nothing archives that, so it is left exactly where it is', null);
163
+ }
164
+ if (evidence.activity.state === harness_agent_activity_1.AGENT_ACTIVITY_LIVE) {
165
+ return new LockDecision(true, `locked by claude agent ${lock.agent}, and the Claude Code harness says `
166
+ + `${evidence.activity.detail} — that agent is working in here`, null);
167
+ }
168
+ if (evidence.branchDeletable) {
169
+ return new LockDecision(false, `the lock from claude agent ${lock.agent} did not stop this — the worktree is clean, its `
170
+ + `branch is already dead, and ${evidence.activity.detail}`, lock);
171
+ }
172
+ return new LockDecision(false, `the lock from claude agent ${lock.agent} was treated as no evidence, as asked`, lock);
173
+ }
174
+ /**
175
+ * WHY a locked worktree was left alone when the lock is NOT the harness's — reporting what the
176
+ * reason SAYS, never who we imagine wrote it.
177
+ *
178
+ * This used to read `locked by a human — do not touch` for every lock in existence, which named an
179
+ * actor nothing in the evidence identifies: the lock reason is the ONLY thing we have, a human may
180
+ * have locked it, so may some other tool, and asserting either is the same defect whichever way it
181
+ * lands.
182
+ */
183
+ foreignLockReason(lockReason) {
184
+ if (lockReason === '') {
54
185
  return 'locked with no reason recorded — nothing says who locked it or why, so it is left alone';
55
186
  }
56
- return `locked, reason "${tree.lockReason}" — that is not a claude agent lock, so who locked it `
187
+ return `locked, reason "${lockReason}" — that is not a claude agent lock, so who locked it `
57
188
  + 'is unknown; left alone';
58
189
  }
59
190
  /**
60
- * Stamp a verdict with the stale lock on its worktree: the lock has to be CLEARED before git will
61
- * remove the directory, and the reason has to say why a lock is being overridden at all.
191
+ * The sentence that replaced "pid N still running that agent is working in here".
192
+ *
193
+ * It says only what is known: which agent the reason names, that the pid in it cannot answer the
194
+ * question because it is the shared session process, and that nothing else made the worktree
195
+ * provably dead either. No claim about anybody working — and no claim about the harness, which on
196
+ * this path was deliberately never asked, because its answer could not have changed the outcome.
197
+ */
198
+ unverifiableReason(lock) {
199
+ return `locked by claude agent ${lock.agent}; ${exports.LOCK_LIVENESS_UNVERIFIABLE} — the recorded pid `
200
+ + `${String(lock.pid)} is the shared Claude Code session process, not the agent's. Its branch `
201
+ + 'is not provably dead either, so it is left alone';
202
+ }
203
+ /**
204
+ * Stamp an overridden verdict with the lock the reap must CLEAR: git refuses to remove a locked
205
+ * worktree, and the reason has to say why a lock is being overridden at all.
62
206
  *
63
207
  * Applied to every verdict the fall-through can produce, not just the deletable one, because the
64
208
  * spared cases are exactly where a human reads the reason and asks "wasn't that locked?".
65
209
  */
66
- annotate(verdict, lock) {
67
- if (lock === null)
210
+ annotate(verdict, decision) {
211
+ if (decision === null || decision.spare || decision.lock === null)
68
212
  return verdict;
69
213
  verdict.unlockBeforeRemove = true;
70
- verdict.reason = `${verdict.reason}; stale lock from claude agent ${lock.agent}, `
71
- + `pid ${String(lock.pid)} is gone`;
214
+ verdict.reason = `${verdict.reason}; ${decision.reason}`;
72
215
  return verdict;
73
216
  }
74
217
  };
75
218
  exports.WorktreeLockVerdicts = WorktreeLockVerdicts;
76
219
  exports.WorktreeLockVerdicts = WorktreeLockVerdicts = tslib_1.__decorate([
77
220
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
78
- tslib_1.__metadata("design:paramtypes", [agent_worktree_lock_1.AgentWorktreeLockReader])
221
+ tslib_1.__metadata("design:paramtypes", [agent_worktree_lock_1.AgentWorktreeLockReader,
222
+ harness_agent_activity_1.HarnessAgentActivityReader,
223
+ worktrees_1.WorktreeService])
79
224
  ], WorktreeLockVerdicts);
80
225
  //# sourceMappingURL=worktree-lock-verdicts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-lock-verdicts.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktree-lock-verdicts.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D,+DAAmF;AAInF;;;;;;;GAOG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACA;IAA7B,YAA6B,aAAsC,IAAI,6CAAuB,EAAE;QAAnE,eAAU,GAAV,UAAU,CAAyD;IAAG,CAAC;IAEpG;;;;;;;OAOG;IACH,cAAc,CAAC,IAAc;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,IAAc;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAChB,OAAO,0BAA0B,IAAI,CAAC,KAAK,SAAS,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,mBAAmB;kBACjF,+BAA+B,CAAC;QAC1C,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,KAAK,EAAE,EAAE,CAAC;YACzB,OAAO,yFAAyF,CAAC;QACrG,CAAC;QACD,OAAO,mBAAmB,IAAI,CAAC,UAAU,wDAAwD;cAC3F,wBAAwB,CAAC;IACnC,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,OAA0B,EAAE,IAA8B;QAC/D,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,OAAO,CAAC;QAClC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAClC,OAAO,CAAC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,kCAAkC,IAAI,CAAC,KAAK,IAAI;cAC5E,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC;QACxC,OAAO,OAAO,CAAC;IACnB,CAAC;CACJ,CAAA;AAzDY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEI,6CAAuB;GADvD,oBAAoB,CAyDhC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { AgentWorktreeLock, AgentWorktreeLockReader } from './agent-worktree-lock';\nimport { DeletableWorktree } from './merged-branch-verdicts';\nimport { Worktree } from './worktrees';\n\n/**\n * What a `git worktree lock` DOES to a worktree's cleanup verdict.\n *\n * Separate from agent-worktree-lock.ts, which only reads a lock reason and asks the kernel about a\n * pid: this is the policy built on top of that evidence — which locks stop a reap, what a spared lock\n * is allowed to claim about who took it, and what a stale one has to leave behind on the verdict so\n * the reaper knows a lock is standing in its way.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeLockVerdicts {\n constructor(private readonly agentLocks: AgentWorktreeLockReader = new AgentWorktreeLockReader()) {}\n\n /**\n * The lock on this worktree was taken by a Claude agent whose process is GONE — or null, meaning\n * the lock is still standing for something and the worktree must be spared.\n *\n * Null covers three genuinely different situations that all end the same way: a live agent, a\n * reason we do not recognise, and no reason at all. `sparedReason` is what tells them apart for\n * the reader; here they are one answer, \"do not touch\".\n */\n staleAgentLock(tree: Worktree): AgentWorktreeLock | null {\n const lock = this.agentLocks.parse(tree.lockReason);\n if (lock === null) return null;\n return this.agentLocks.isRunning(lock) ? null : lock;\n }\n\n /**\n * WHY a locked worktree was left alone — reporting what the lock SAYS, never who we imagine wrote\n * it.\n *\n * This used to read `locked by a human — do not touch` for every lock in existence, which was\n * wrong twice over. It was factually wrong about the agent harness's own locks, which is what let\n * every `/full-cycle` worktree pile up forever. And it named an actor nothing in the evidence\n * identifies: the lock reason is the ONLY thing we have, a human may have locked it, so may some\n * other tool, and asserting either is the same defect whichever way it lands.\n *\n * So: name the agent and pid when the reason ASSERTS them, and otherwise quote the reason back\n * verbatim and say plainly that we cannot tell who locked it.\n */\n sparedReason(tree: Worktree): string {\n const lock = this.agentLocks.parse(tree.lockReason);\n if (lock !== null) {\n return `locked by claude agent ${lock.agent}, pid ${String(lock.pid)} still running — `\n + 'that agent is working in here';\n }\n if (tree.lockReason === '') {\n return 'locked with no reason recorded — nothing says who locked it or why, so it is left alone';\n }\n return `locked, reason \"${tree.lockReason}\" — that is not a claude agent lock, so who locked it `\n + 'is unknown; left alone';\n }\n\n /**\n * Stamp a verdict with the stale lock on its worktree: the lock has to be CLEARED before git will\n * remove the directory, and the reason has to say why a lock is being overridden at all.\n *\n * Applied to every verdict the fall-through can produce, not just the deletable one, because the\n * spared cases are exactly where a human reads the reason and asks \"wasn't that locked?\".\n */\n annotate(verdict: DeletableWorktree, lock: AgentWorktreeLock | null): DeletableWorktree {\n if (lock === null) return verdict;\n verdict.unlockBeforeRemove = true;\n verdict.reason = `${verdict.reason}; stale lock from claude agent ${lock.agent}, `\n + `pid ${String(lock.pid)} is gone`;\n return verdict;\n }\n}\n"]}
1
+ {"version":3,"file":"worktree-lock-verdicts.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktree-lock-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { AgentWorktreeLock, AgentWorktreeLockReader } from './agent-worktree-lock';\nimport {\n AGENT_ACTIVITY_LIVE,\n AGENT_ACTIVITY_UNKNOWN,\n AgentActivity,\n HarnessAgentActivityReader,\n} from './harness-agent-activity';\nimport { DeletableWorktree } from './merged-branch-verdicts';\nimport { Worktree, WorktreeService } from './worktrees';\n\n/**\n * What a `git worktree lock` DOES to a worktree's cleanup verdict.\n *\n * Separate from agent-worktree-lock.ts (which only parses a lock reason) and from\n * harness-agent-activity.ts (which only asks the harness about an agent): this is the POLICY built on\n * top of that evidence — what stops a reap, what a spared lock is allowed to CLAIM about who took it,\n * and what an overridden one has to leave on the verdict so the reaper knows to clear it.\n *\n * ─── WHY THE POLICY CHANGED ──────────────────────────────────────────────────────────────────────\n * It used to be one question, asked of the kernel: is the pid in the lock reason still running? That\n * pid is the Claude Code SESSION process, shared by every subagent, so the answer was yes for the\n * whole life of a session and every agent worktree was spared forever — nine against a cap of five,\n * with one agent running, each carrying the sentence \"that agent is working in here\" about agents\n * whose PRs had already merged. Both halves were defects: the reap never happened, and the message\n * asserted something nobody knew.\n *\n * ─── THE DECISION TABLE, AND THE ONE RULE BEHIND IT ──────────────────────────────────────────────\n *\n * HARNESS STATE MAY ONLY VETO A REAP. IT MAY NEVER LICENSE ONE.\n *\n * The licence comes solely from the branch/commit evidence wp-cleanup already computes and already\n * gets right. So, for a lock this repo can read as the harness's own:\n *\n * branch evidence does NOT say deletable → SPARE. The harness is not even consulted; it\n * cannot promote a spare into a reap.\n * deletable, but the worktree is DIRTY → SPARE, unconditionally. Uncommitted or\n * untracked files are work no archive tag can\n * bring back. This one overrides everything,\n * including the explicit flag.\n * deletable, clean, harness says the agent is\n * mid-tool-call and writing → SPARE. The veto.\n * deletable, clean, harness says returned /\n * long-stale / cannot tell → judge it on its branch like any unlocked tree,\n * clearing the lock on the way out.\n *\n * `--ignore-stale-locks` moves ONE line of that table: it lets a locked worktree whose branch is not\n * provably dead be classified on its real branch and commit state anyway (so a zero-commit husk gets\n * reaped and a worktree holding unique commits reaches the numbered block a human can answer). It\n * does not touch the dirty rail or the live-agent veto, because neither of those is what the caller\n * is complaining about when they pass it.\n *\n * ─── SCOPE: AGENT LOCKS ONLY ─────────────────────────────────────────────────────────────────────\n * Every override applies ONLY to a lock this file can read as the harness's own. A lock reason naming\n * anything else — a human's \"dean is debugging\", or nothing at all — is still an instruction from\n * somebody, and wp-cleanup still obeys it.\n */\n\n/**\n * The exact phrase a spared \"we cannot tell\" verdict carries, exported so wp-cleanup can spot those\n * worktrees and offer the flag that judges them on their branch instead.\n *\n * ONE spelling, shared: the alternative is the caller re-deriving the same condition from a second\n * copy of the sentence, which is how a message and the code that keys off it drift apart.\n */\nexport const LOCK_LIVENESS_UNVERIFIABLE = 'whether that agent is still running cannot be verified';\n\n/**\n * The stand-in for \"the harness was never asked\", used on the paths where asking could not change the\n * answer. It is UNKNOWN, so a caller that consults it anyway still fails safe.\n */\nexport const HARNESS_NOT_CONSULTED = new AgentActivity(\n AGENT_ACTIVITY_UNKNOWN, 'the harness was not consulted');\n\n// Data-only (per CLAUDE.md, classes for data). Everything `decide` rules on, as one value — see\n// `gather`, which is the only thing that builds one outside a test.\nexport class LockEvidence {\n /** The lock reason verbatim, straight out of `git worktree list --porcelain`. */\n lockReason: string;\n /** What the harness says about the agent named in that reason, or HARNESS_NOT_CONSULTED. */\n activity: AgentActivity;\n /** Does `git status --porcelain` report anything, or could it not answer? Either way: held. */\n workInFlight: boolean;\n /** Why it is held, in git's terms — printed verbatim, because the two reasons differ for a human. */\n workInFlightReason: string;\n /** Is the branch this worktree holds ALREADY provably dead by the ordinary verdicts? */\n branchDeletable: boolean;\n /** The caller said to treat a standing agent lock as no evidence at all. */\n ignoreStaleAgentLocks: boolean;\n\n constructor(\n lockReason: string,\n activity: AgentActivity,\n workInFlight: boolean,\n workInFlightReason: string,\n branchDeletable: boolean,\n ignoreStaleAgentLocks: boolean,\n ) {\n this.lockReason = lockReason;\n this.activity = activity;\n this.workInFlight = workInFlight;\n this.workInFlightReason = workInFlightReason;\n this.branchDeletable = branchDeletable;\n this.ignoreStaleAgentLocks = ignoreStaleAgentLocks;\n }\n}\n\n/**\n * Data-only. The one verdict this file produces: spare or override, the sentence that says WHY, and\n * the agent lock the reaper will have to clear if the worktree goes.\n *\n * ONE class rather than a spare-path method and an override-path method, because the two answers are\n * mutually exclusive readings of the same evidence, and two methods is how they drift apart — which\n * is precisely what happened before: the old spared message and the old stale-lock test were computed\n * from the same pid by different code, and only one of them was ever corrected.\n */\nexport class LockDecision {\n /** True: leave this worktree alone, and `reason` is the whole story. */\n spare: boolean;\n /** Spared: why it was left. Overridden: the clause appended to the branch's own verdict. */\n reason: string;\n /** The agent lock to clear on the way out — null when nothing may be cleared. */\n lock: AgentWorktreeLock | null;\n\n constructor(spare: boolean, reason: string, lock: AgentWorktreeLock | null) {\n this.spare = spare;\n this.reason = reason;\n this.lock = lock;\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeLockVerdicts {\n // Defaulted like BranchReaper's collaborators, so the non-DI call sites can just\n // `new WorktreeLockVerdicts()` while inversify still injects the singletons from a container.\n constructor(\n private readonly agentLocks: AgentWorktreeLockReader = new AgentWorktreeLockReader(),\n private readonly harness: HarnessAgentActivityReader = new HarnessAgentActivityReader(),\n private readonly worktrees: WorktreeService = new WorktreeService(),\n ) {}\n\n /**\n * Collect what `decide` rules on — and, just as deliberately, DECLINE to collect the two\n * expensive facts when they cannot change the answer.\n *\n * `git status --porcelain` and a walk of the harness's state tree are gathered only when a reap\n * is otherwise on the table: the lock is one this repo can read as the harness's own, and either\n * the branch is already provably dead or the caller asked for locks to be ignored. That is not an\n * optimisation dressed up as a rule — harness state may only VETO a reap, never license one, so on\n * a path with no reap to veto there is nothing to ask, and asking would invite a later reader to\n * let the answer decide something.\n */\n gather(tree: Worktree, branchDeletable: boolean, ignoreStaleAgentLocks: boolean): LockEvidence {\n const lock = this.agentLocks.parse(tree.lockReason);\n if (lock === null || (!branchDeletable && !ignoreStaleAgentLocks)) {\n return new LockEvidence(tree.lockReason, HARNESS_NOT_CONSULTED, false, '',\n branchDeletable, ignoreStaleAgentLocks);\n }\n const held = this.worktrees.workInFlight(tree.path);\n return new LockEvidence(\n tree.lockReason, this.harness.activityOf(lock.agent, tree.path), held.held, held.reason,\n branchDeletable, ignoreStaleAgentLocks);\n }\n\n /**\n * Spare this locked worktree, or judge it on its branch like any unlocked one — with the sentence\n * that says which piece of evidence decided it.\n *\n * The order below IS the decision table in the class header, in the same order, for the same\n * reasons. Read it there.\n */\n decide(evidence: LockEvidence): LockDecision {\n const lock = this.agentLocks.parse(evidence.lockReason);\n if (lock === null) return new LockDecision(true, this.foreignLockReason(evidence.lockReason), null);\n\n if (!evidence.branchDeletable && !evidence.ignoreStaleAgentLocks) {\n return new LockDecision(true, this.unverifiableReason(lock), null);\n }\n if (evidence.workInFlight) {\n return new LockDecision(true,\n `locked by claude agent ${lock.agent}, and the worktree ${evidence.workInFlightReason} — `\n + 'nothing archives that, so it is left exactly where it is', null);\n }\n if (evidence.activity.state === AGENT_ACTIVITY_LIVE) {\n return new LockDecision(true,\n `locked by claude agent ${lock.agent}, and the Claude Code harness says `\n + `${evidence.activity.detail} — that agent is working in here`, null);\n }\n if (evidence.branchDeletable) {\n return new LockDecision(false,\n `the lock from claude agent ${lock.agent} did not stop this — the worktree is clean, its `\n + `branch is already dead, and ${evidence.activity.detail}`, lock);\n }\n return new LockDecision(false,\n `the lock from claude agent ${lock.agent} was treated as no evidence, as asked`, lock);\n }\n\n /**\n * WHY a locked worktree was left alone when the lock is NOT the harness's — reporting what the\n * reason SAYS, never who we imagine wrote it.\n *\n * This used to read `locked by a human — do not touch` for every lock in existence, which named an\n * actor nothing in the evidence identifies: the lock reason is the ONLY thing we have, a human may\n * have locked it, so may some other tool, and asserting either is the same defect whichever way it\n * lands.\n */\n private foreignLockReason(lockReason: string): string {\n if (lockReason === '') {\n return 'locked with no reason recorded — nothing says who locked it or why, so it is left alone';\n }\n return `locked, reason \"${lockReason}\" — that is not a claude agent lock, so who locked it `\n + 'is unknown; left alone';\n }\n\n /**\n * The sentence that replaced \"pid N still running — that agent is working in here\".\n *\n * It says only what is known: which agent the reason names, that the pid in it cannot answer the\n * question because it is the shared session process, and that nothing else made the worktree\n * provably dead either. No claim about anybody working — and no claim about the harness, which on\n * this path was deliberately never asked, because its answer could not have changed the outcome.\n */\n private unverifiableReason(lock: AgentWorktreeLock): string {\n return `locked by claude agent ${lock.agent}; ${LOCK_LIVENESS_UNVERIFIABLE} — the recorded pid `\n + `${String(lock.pid)} is the shared Claude Code session process, not the agent's. Its branch `\n + 'is not provably dead either, so it is left alone';\n }\n\n /**\n * Stamp an overridden verdict with the lock the reap must CLEAR: git refuses to remove a locked\n * worktree, and the reason has to say why a lock is being overridden at all.\n *\n * Applied to every verdict the fall-through can produce, not just the deletable one, because the\n * spared cases are exactly where a human reads the reason and asks \"wasn't that locked?\".\n */\n annotate(verdict: DeletableWorktree, decision: LockDecision | null): DeletableWorktree {\n if (decision === null || decision.spare || decision.lock === null) return verdict;\n verdict.unlockBeforeRemove = true;\n verdict.reason = `${verdict.reason}; ${decision.reason}`;\n return verdict;\n }\n}\n"]}