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
//! Is a live process standing in this directory? (#4311, DOC-66 §1.3)
//!
//! Why: every gate guarding a worktree removal today asks a REGISTRY — the
//! session store's `workspace_path`s, and the delegation tracker's registered
//! trees (`agent_worktree_reap::paths_in_use`). A process nothing registered is
//! invisible to all of them. On 2026-08-15 a `trusty-memory serve --foreground`
//! started by hand inside an agent worktree ran for a day; session_manager and
//! delegation tracking both had no record of it, so `git worktree remove
//! --force` would have deleted the directory out from under a running process
//! holding open descriptors in it. That gap was dormant while the reaper reaped
//! nothing; making agent worktrees attributable is what wakes it up, so the
//! check lands in the same change.
//!
//! What: [`process_holding`] asks the OS, not a registry — one `lsof` call
//! listing every process's current working directory, prefix-matched against
//! the candidate.
//!
//! # Fail direction
//!
//! Toward IN USE. `lsof` missing, unspawnable, or returning output this cannot
//! parse all resolve to "something may be in there", never to "free" — the
//! [ADR-0045](../../../../docs/adr/0045-distinguish-absent-from-undeterminable-on-destructive-paths.md)
//! rule that an empty observation on a destructive path is UNDETERMINABLE and
//! not ABSENT. On a machine with no `lsof` this refuses every reap and says so,
//! which is the correct trade for a `git worktree remove --force`.
//!
//! # Stated gap: this misses the agent process itself
//!
//! State the limit at its real width, not its flattering one. This catches a
//! process that `cd`-ed into the tree. It does NOT reliably catch the agent the
//! worktree was granted to: measured on this machine, every running `claude`
//! process has its cwd at a project root and none inside `.claude/worktrees/`.
//! Coverage of a live agent is therefore incidental — it exists only where that
//! agent happened to `cd`. What this gate reliably catches is the class of
//! long-running command started BY an agent from inside its tree, which is the
//! 2026-08-15 `trusty-memory serve --foreground` shape.
//!
//! The agent process itself is covered by the registry instead:
//! `agent_worktree_reap::paths_in_use` reads every non-terminal delegation in
//! every session. The two are complementary, and neither alone is sufficient.
//!
//! `lsof +D <path>` would additionally catch a process holding a descriptor
//! inside the tree with its cwd elsewhere, at a cost that scales with the tree —
//! an agent worktree here carries a 1.5 GiB `target/`. Not paid for on every
//! reap, and not the fix for the gap above.
//! Test: the `#[cfg(test)]` suite in `worktree_liveness_tests.rs`.
use Path;
/// Name a live process whose working directory is inside `path`, or explain why
/// the question could not be answered.
///
/// Why: the one gate in the removal chain that consults the operating system
/// rather than a record trusty-mpm wrote itself, so a process nobody registered
/// still protects its directory.
/// What: `Some(reason)` means DO NOT REMOVE — either a process was found (the
/// reason names its pid, command and cwd) or the probe could not complete (the
/// reason says which step failed). `None` means the probe ran and found
/// nothing, which is the only result that permits removal.
///
/// `path` is canonicalized first so a symlinked candidate still matches the
/// resolved cwd `lsof` reports; a canonicalize failure is itself an
/// undeterminable answer, not a pass.
/// Test: `liveness_reports_a_process_standing_in_the_directory`,
/// `liveness_ignores_a_sibling_directory`,
/// `liveness_treats_a_missing_lsof_as_in_use`,
/// `liveness_treats_an_unparsable_probe_as_in_use`.
pub
/// The probe binary. One literal so the doc, the error text and the call agree.
const LSOF: &str = "lsof";
/// Ask `bin` for every process's current working directory.
///
/// Why: `-d cwd` restricts the descriptor set to the one entry per process this
/// check cares about, which is what keeps a system-wide listing to roughly a
/// thousand lines and about a second. `-w` suppresses the warnings `lsof` emits
/// for processes it may not examine — those are expected for another user's
/// processes and are not a probe failure.
///
/// `bin` is a parameter rather than a constant so the fail-toward-in-use arms
/// can be exercised against an absent binary. It is NOT configuration: the one
/// production call site passes [`LSOF`], and an environment override would be
/// process-global state this crate does not use.
/// What: `Ok(stdout)` for a zero exit; `Err(reason)` for a spawn failure or a
/// non-zero exit, each carrying the text the caller reports as its refusal.
/// Test: `liveness_treats_a_missing_lsof_as_in_use`.
/// Find the first process in an `lsof -F pcn` listing whose cwd is under `root`.
///
/// Why: split from the spawn so the parser is testable against fixed text,
/// including the shapes that must NOT be read as "free".
/// What: `lsof`'s field output repeats `p<pid>`, `c<command>`, then `n<path>`;
/// the `p`/`c` values carry forward until the next process set. Returns
/// `Some(reason)` for the first match, and for any listing that fails the
/// SELF-VISIBILITY proof below.
///
/// # Why "the listing was non-empty" is not proof it saw anything
///
/// Without root, `lsof` reports only the caller's own processes and exits 0
/// regardless — measured on this machine, `lsof -w -d cwd -F pcn` returned 429
/// of 667 processes and ZERO of the 229 owned by other users. A flag that only
/// asks "did ANY `n` line appear" is satisfied by that partial listing, so
/// blindness to the very process that matters reads as a clean negative, which
/// is a PERMIT on a `git worktree remove --force`. That is #4470's
/// empty-`lsof`-is-not-`Free` defect in a second subsystem.
///
/// So the proof is positive and specific: the listing must contain THIS
/// process's own pid with a working directory. If the probe cannot see the
/// process that ran it, its silence about every other process carries no
/// information at all.
/// Test: `liveness_reports_a_process_standing_in_the_directory`,
/// `liveness_ignores_a_sibling_directory`,
/// `liveness_treats_an_unparsable_probe_as_in_use`,
/// `liveness_treats_an_empty_probe_as_in_use`,
/// `liveness_treats_a_listing_that_cannot_see_this_process_as_in_use`.