@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.
- package/package.json +1 -1
- package/src/agent-worktree-lock.d.ts +4 -16
- package/src/agent-worktree-lock.js +16 -38
- package/src/agent-worktree-lock.js.map +1 -1
- package/src/branch-reaper.js +3 -1
- package/src/branch-reaper.js.map +1 -1
- package/src/builds-log.d.ts +8 -3
- package/src/builds-log.js +8 -3
- package/src/builds-log.js.map +1 -1
- package/src/harness-agent-activity.d.ts +152 -0
- package/src/harness-agent-activity.js +345 -0
- package/src/harness-agent-activity.js.map +1 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +12 -1
- package/src/index.js.map +1 -1
- package/src/merged-branches.d.ts +13 -7
- package/src/merged-branches.js +29 -19
- package/src/merged-branches.js.map +1 -1
- package/src/worktree-lock-verdicts.d.ts +128 -25
- package/src/worktree-lock-verdicts.js +184 -39
- package/src/worktree-lock-verdicts.js.map +1 -1
|
@@ -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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
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
|
-
|
|
99
|
+
private readonly harness;
|
|
100
|
+
private readonly worktrees;
|
|
101
|
+
constructor(agentLocks?: AgentWorktreeLockReader, harness?: HarnessAgentActivityReader, worktrees?: WorktreeService);
|
|
15
102
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
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
|
-
|
|
113
|
+
gather(tree: Worktree, branchDeletable: boolean, ignoreStaleAgentLocks: boolean): LockEvidence;
|
|
24
114
|
/**
|
|
25
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
|
|
140
|
+
private unverifiableReason;
|
|
38
141
|
/**
|
|
39
|
-
* Stamp
|
|
40
|
-
*
|
|
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,
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the
|
|
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
|
-
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
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
|
-
|
|
138
|
+
gather(tree, branchDeletable, ignoreStaleAgentLocks) {
|
|
29
139
|
const lock = this.agentLocks.parse(tree.lockReason);
|
|
30
|
-
if (lock === null)
|
|
31
|
-
return
|
|
32
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
48
|
-
const lock = this.agentLocks.parse(
|
|
49
|
-
if (lock
|
|
50
|
-
return
|
|
51
|
-
|
|
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 (
|
|
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 "${
|
|
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
|
-
*
|
|
61
|
-
*
|
|
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,
|
|
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};
|
|
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"]}
|