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
294
295
296
297
298
//! The three seams `tm pr cleanup` runs through: `gh`, `git`, and the session
//! claim store (#7275).
//!
//! Why: post-merge cleanup is a sequence of destructive commands, so the
//! decision to run each one has to be testable without a network, a live `gh`,
//! or a real worktree to delete. Hiding every spawn behind a trait is what lets
//! [`super::plan`] be a pure function of scripted command output. The
//! production implementations here spawn through this workspace's single
//! entry points — `trusty_common::gh::GhCommand` (#5475) and
//! `trusty_common::git::command_in` (#7171) — so cleanup adds no new spawn
//! site, per the repo's common-entry-point rule.
//!
//! What: [`CmdOut`] (one completed subprocess), the [`Gh`] and [`Git`] seams
//! with [`RealGh`] / [`RealGit`] behind them, and [`ClaimEnder`] — the seam
//! that answers which session records claim a worktree and tombstones one.
//!
//! Test: `super::tests` drives scripted fakes for all three.
use Path;
use Context as _;
/// One completed subprocess, as cleanup needs it.
///
/// Why: a `gh pr view` on a missing PR and a `git push --delete` on an absent
/// branch both exit non-zero and want different handling, so the seam hands
/// back the triple rather than deciding.
/// What: exit success, verbatim stdout, verbatim stderr.
/// Test: the fakes in `super::tests` construct these directly.
/// The `gh` seam every cleanup call goes through.
///
/// Test: `FakeGh` in `super::tests`.
/// The `git` seam every cleanup call goes through, scoped to one checkout.
///
/// Test: `FakeGit` in `super::tests`.
/// Who claims a worktree, and how that claim is ended (#7275).
///
/// Why: the owner's ruling is that a MERGED pull request makes its worktree
/// obsolete, claim included — so cleanup ends the claim and proceeds rather
/// than refusing. Ending it must be RECORD-ONLY: cleanup tombstones the
/// session record, and never kills a process it does not own. Both halves are
/// behind this trait so the executor can be tested without a daemon and so the
/// two production callers (the CLI over HTTP, the supervisor over
/// `SessionManager`) share one decision path.
/// What: [`claims_on`](Self::claims_on) names the session ids claiming a path;
/// [`end_claim`](Self::end_claim) tombstones one. An error from either is a
/// step FAILURE, never a silent proceed — see [`super::run`].
/// Test: `FakeClaims` in `super::tests`;
/// `cleanup_ends_a_session_claim_before_removing_the_worktree`.
/// A [`ClaimEnder`] that reports no claims and can end none.
///
/// Why: `--dry-run` never needs to end a claim, and a caller with no session
/// store to consult must be able to say so explicitly rather than by passing
/// something that silently answers "unclaimed".
/// What: `claims_on` returns an error naming `reason`, so the worktree step
/// FAILS rather than proceeding on an unanswerable question.
/// Test: `cleanup_fails_the_worktree_step_when_claims_cannot_be_read`.
/// Production [`Gh`] over `trusty_common::gh::GhCommand`.
///
/// Why: `GhCommand` is this workspace's single `gh` entry point (#5475) — it
/// decides which binary, renders the argv for errors, and never goes through a
/// shell. A second `Command::new("gh")` here would be a defect.
/// What: overlays a caller-supplied GitHub identity (`tm`'s `gh_identity`,
/// #1265) onto the child, removals first (#6668), then runs blocking.
/// Test: exercised live; the logic it feeds is covered against [`Gh`] fakes.
/// Production [`Git`] over `trusty_common::git::command_in`.
///
/// Why: `command_in` is this workspace's single synchronous git entry point
/// (#7171) — it carries `-c maintenance.auto=false -c gc.auto=0`, which is what
/// keeps a sweep over many worktrees from touching off the detached-repack
/// pile-up that has flattened this machine before.
/// What: `git -C <dir> <args>`, run blocking.
/// Test: exercised live; the logic it feeds is covered against [`Git`] fakes.
;
/// Which MERGED pull request carried a worktree's work (#7275 round 3).
///
/// Why: every merge here is a squash, so a landed branch's commits are never
/// ancestors of `main` and an ahead-of-upstream count reads every landed
/// worktree as live work — seven trees were refused as holding "1–3 unpushed
/// commit(s)" on 2026-09-09, which is the population cleanup exists to
/// reclaim. The question that answers it is which merged pull request carried
/// this tree, and `worktree_reclaim_pr_match::resolve_landing` already decides
/// that by exact branch name, round stem, and head-commit ancestry. This seam
/// exists so the engine — which is `pub` and wired from the `tm` binary — can
/// ask through a trait its tests can fake, without exporting the matcher's own
/// `pub(crate)` types.
/// What: [`merged_pr`](Self::merged_pr) names the pull request, or `None` when
/// none matches. FAIL-CLOSED: every failure — no `gh`, a timeout, an
/// unparseable reply — answers `None`, which leaves today's refusal standing.
/// Test: `FakeLanding` in `super::tests` drives the real matcher over #7267's
/// `LandingProbe` fake; `cleanup_removes_a_squash_merged_worktree`.
/// Production [`Landing`] over the #7267 merged-pull-request matcher.
///
/// Why: one implementation of "which merged pull request carried this tree",
/// shared with `tm session prune-worktrees --merged-prs`. A second matcher here
/// would be the defect the repo's common-entry-point rule names.
/// What: seeds `resolve_landing`'s first rung with the caller's `exact` answer
/// when there is one and otherwise asks GitHub about the branch itself, then
/// lets the round-stem and head-commit rungs widen it. Only a `Merged` verdict
/// becomes `Some`.
/// Test: exercised live; the policy it delegates to is unit-tested in
/// `worktree_reclaim_pr_match_tests`.
;
/// [`RealLanding`]'s whole body, over any [`LandingProbe`] (#7275 round 3).
///
/// Why: the seed-then-ladder shape IS the policy, so a test that drove a
/// different shape would prove nothing about what production does. Taking the
/// probe as a parameter lets `super::tests` run this exact code over #7267's
/// own `LandingProbe` fake.
/// What: `exact` becomes rung 1's settled answer; otherwise the branch's own
/// pull requests are asked, and `resolve_landing` widens by round stem and head
/// commit. Only `Merged` becomes `Some`.
/// Test: `cleanup_removes_a_squash_merged_round_sibling`,
/// `cleanup_refuses_an_ahead_tree_with_no_merged_pr`.
///
/// [`LandingProbe`]: crate::session_manager::worktree_reclaim_pr_match::LandingProbe
pub