1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//! Transfer a worktree's ownership away from a session or agent that is
//! provably dead (#6497).
//!
//! Why: a session can die holding an agent's worktree that still contains
//! uncommitted work, and until #6497 there was no compliant way for a successor
//! to finish that work. Every write route into the tree is refused — the
//! ownership sentinel still names the dead owner, so `worktree_reclaim`'s gate 4
//! spares it and the write-boundary guard keeps a successor out — while the
//! serialize-instead route is refused too, because the daemon's delegation
//! records still name the dead session's agents as running in the shared
//! checkout. The recorded workaround was to rebuild the branch by hand in a new
//! tree, which loses the original tree's identity and leaves both stale claims
//! in place.
//!
//! What: [`evaluate_adoption`] — the pure policy, which permits a transfer ONLY
//! when the current owner is positively known to be gone — and
//! [`adopt_worktree`], which rewrites the sentinel to name the adopting managed
//! session. Adoption is an EXPLICIT operator verb
//! (`tm session adopt-worktree <path> --as <session>`), never automatic: a tree
//! that changes hands on its own is indistinguishable from one that was taken
//! from a working agent.
//!
//! FAIL-CLOSED, in the same direction as every other worktree gate here: the
//! only permitting arm requires positive evidence of death. An owner the
//! registry has merely never heard of is UNDETERMINABLE, not absent (ADR-0045),
//! and is refused — a delegation map is rebuilt empty at every daemon boot, so
//! its silence proves nothing about an agent that is still working.
//!
//! #7974 does not relax that: it supplies the missing EVIDENCE from a second,
//! daemon-independent source. [`liveness_with_lock_fallback`] reads the git
//! worktree lock the harness itself wrote, and upgrades `Undeterminable` to
//! `Dead` only when that lock names a pid the kernel reports does not exist.
//! An `Alive` answer is never touched, and a lock that cannot answer leaves the
//! refusal exactly where it was.
//!
//! What adoption does NOT do, stated rather than hidden: it changes the
//! sentinel, so it changes what the reclaim and prune gates believe about the
//! tree. It does not move the harness's own worktree isolation — a subagent
//! confined to a different tree still cannot reach into this one — and it does
//! not commit, push, or merge anything.
//! Test: `worktree_adopt_tests`.
use Path;
use ManagedSessionId;
use ;
/// What the caller was able to establish about the CURRENT owner's liveness
/// (#6497).
///
/// Why: the policy must not go looking for this itself. The two owner shapes
/// resolve through completely different registries — a managed session through
/// the session-record store, a dispatched agent through the delegation map —
/// and only the daemon holds both. Passing the answer in keeps the policy a
/// pure function and forces every caller to say which of the three answers it
/// actually has, rather than collapsing "not found" into "gone".
/// What: [`Dead`](Self::Dead) — the owner is positively known to have ended;
/// [`Alive`](Self::Alive) — positively known to be running;
/// [`Undeterminable`](Self::Undeterminable) — the registry could not answer,
/// which refuses.
/// Test: `adoption_refuses_a_live_owner`, `adoption_takes_a_dead_owners_tree`,
/// `adoption_refuses_an_owner_the_registry_never_heard_of`.
pub
/// What the git worktree lock says about the pid it was written for (#7974).
///
/// Why a second source at all: the delegation registry is rebuilt empty at
/// every daemon boot, so after a restart it answers
/// [`OwnerLiveness::Undeterminable`] for every dispatched agent — including one
/// that died days ago holding uncommitted work. That is the moment adoption
/// exists for, and it was exactly the moment it refused. The harness's own
/// worktree lock does not have that defect: no daemon writes it, so it survives
/// a daemon restart, and its reason names the process that was dispatched into
/// the tree.
/// What: the three answers a pid probe can give, kept apart rather than folded
/// into a bool so an unanswerable probe cannot read as death (ADR-0045).
/// Test: `lock_fallback_takes_a_dead_pids_tree`,
/// `lock_fallback_refuses_a_running_pid`,
/// `lock_fallback_refuses_when_the_lock_says_nothing`.
pub
/// Refine an UNDETERMINABLE registry answer with the worktree lock (#7974).
///
/// Why this is not a weakening of the gate: the refinement runs in ONE
/// direction and from ONE starting point. [`OwnerLiveness::Alive`] and
/// [`OwnerLiveness::Dead`] pass through untouched, so no lock state can take a
/// tree from an owner the registry says is working; only the answer that meant
/// "I cannot tell" can be upgraded, and only by POSITIVE evidence — a pid the
/// kernel reports does not exist. Everything else stays `Undeterminable` and
/// still refuses, including a running pid: pid reuse means a live pid is
/// evidence about a process, not about the agent, and the refusal is the same
/// either way.
/// What: `(liveness, evidence)`. The second element is `Some` exactly when the
/// lock changed the answer, and carries the sentence the caller logs — an
/// adoption that succeeded on this path must say what made it succeed, because
/// nothing else in the record explains why a refusal became a transfer.
/// Test: `lock_fallback_takes_a_dead_pids_tree`,
/// `lock_fallback_refuses_a_running_pid`,
/// `lock_fallback_refuses_when_the_lock_says_nothing`,
/// `lock_fallback_never_revives_a_live_owner`,
/// `lock_fallback_leaves_a_dead_owner_dead`.
pub
/// Whether a worktree may change hands, and if not, why not (#6497).
///
/// Test: every test in `worktree_adopt_tests`.
pub
/// Decide whether `owner`'s claim on a worktree may be transferred (#6497).
///
/// Why: this is the whole safety argument for the verb, written so `Adopt` is
/// reachable only by falling off the end. Both conditions the issue asks for are
/// gates here, in this order, and each one `return`s.
/// What: three gates.
/// 1. **A readable claim.** [`SentinelOwner::Unknown`] means the sentinel is
/// absent, empty or unparsable, so there is no claim to transfer and nothing
/// to prove dead. Refused.
/// 2. **The owner is provably gone.** Only [`OwnerLiveness::Dead`] proceeds;
/// `Alive` refuses because taking a working owner's tree is the harm this
/// verb must never cause, and `Undeterminable` refuses because an
/// unanswerable liveness question is not an absent one (ADR-0045).
/// 3. **Nothing references the tree since the claim.** `live_claimants` is the
/// set of agents or sessions the caller found still working IN this
/// directory. A non-empty set refuses whatever the sentinel says: the
/// sentinel names who provisioned the tree, not everyone who is in it.
///
/// Test: `adoption_refuses_an_unreadable_claim`, `adoption_refuses_a_live_owner`,
/// `adoption_refuses_an_owner_the_registry_never_heard_of`,
/// `adoption_refuses_a_tree_something_still_works_in`,
/// `adoption_takes_a_dead_owners_tree`.
pub
/// Name the current owner for a refusal message.
///
/// Why: "owned by a dead session" without the id sends the reader to the wrong
/// registry. One line each, because the whole message is already long.
/// Rewrite `path`'s ownership sentinel to name `new_owner` (#6497).
///
/// Why: the sentinel IS the ownership record every gate reads, so a transfer is
/// exactly this write and nothing else — no new state, no second registry, and
/// no new expiry to keep in step with the first. Writing the SESSION shape
/// rather than a fresh agent claim is deliberate: a managed session's liveness
/// resolves through the session-record store, which has a real terminal state, so
/// the adopted tree re-enters the ordinary reclaim lifecycle instead of becoming
/// permanently unreclaimable.
/// What: the ownership marker (git admin dir, #8511) replaced with a payload naming
/// `new_owner`, timestamped now. Refuses when `path` is not a directory, because
/// writing a sentinel into a path that is not a worktree would manufacture a
/// claim on nothing.
/// Test: `adopt_worktree_rewrites_the_sentinel`,
/// `adopt_worktree_refuses_a_path_that_is_not_a_directory`.
pub