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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! Advisory locking + checkout detection for `RealGitBackend::ensure_base_checkout`.
//!
//! Why: split out of `workspace.rs` (which grew past the 500-SLOC production
//! cap once trusty-review's PR #1936 / #1935 findings were fixed) to keep
//! both files under the mechanical `scripts/check_line_cap.sh` gate. Grouping
//! these items together here — rather than trimming their Why/What/Test
//! documentation — keeps the fix's rationale fully documented in place.
//! What: [`is_established_checkout`] replaces the fragile `HEAD.is_file()`
//! idempotency probe (review finding #2) with a real `git rev-parse` check;
//! [`acquire_base_checkout_lock`] (backed by [`BaseCheckoutLock`], a
//! dependency-free marker-file mutex) plus [`lock_is_stale`] serialize the
//! check-and-clone window across concurrent callers to fix the TOCTOU
//! provisioning race (review finding #1). [`stale_base_dir_error`] turns the
//! "the base path is occupied by a broken directory" state into an actionable
//! operator error (issue #1937 item 1), and is shared by BOTH backends;
//! [`fake_is_established_checkout`] / [`write_fake_checkout`] let
//! `FakeGitBackend` mirror the real backend's `rev-parse`-based validity
//! semantics rather than a superficial file-exists probe (issue #1937 item 3).
//! [`stale_base_quarantine_path`] backs that error's non-destructive recovery
//! hint (issue #3605: the hint used to be a literal `rm -rf`).
//!
//! #4270 moved the base checkout from `<project_dir>/.base` (bare) to
//! `<project_dir>` itself (non-bare), converging on the shape the in-project
//! spawn path already produces. The detection predicate moved with it, and
//! [`protected_dir_error`] guards the cases that move introduces: a project
//! directory already holding git or trusty-mpm state, whose worktrees may be
//! live.
//! Test: `ensure_base_checkout_recovers_from_concurrent_race`,
//! `ensure_base_checkout_rejects_stale_directory`,
//! `base_checkout_lock_recovers_stale_lock_marker`,
//! `fake_ensure_base_checkout_rejects_stale_directory`,
//! `stale_base_dir_error_suggests_quarantine_not_deletion`,
//! `provision_in_leaves_an_existing_dot_base_store_untouched`, all in
//! `provisioner/workspace/tests.rs`.
use ;
use ProvisionError;
/// Determine whether `dir` is an already-established git checkout ROOT.
///
/// Why: the previous idempotency guard for [`super::RealGitBackend`]'s
/// `ensure_base_checkout` (`base_dir.join("HEAD").is_file()`) only checks for
/// a file literally named `HEAD` at the root of `base_dir` — it never
/// confirms that directory is actually a valid, complete git repository. A
/// directory that merely contains a stray `HEAD` file (e.g. left behind by a
/// clone that crashed mid-flight — git writes `HEAD` early, before the rest
/// of the object database/refs — or any other stale/corrupt artifact) would
/// pass the old check and be silently mistaken for an established shared base
/// (trusty-review finding #2 on PR #1936 / #1935). Asking git itself cannot be
/// fooled by directory layout alone. This helper is also reused to resolve the
/// TOCTOU provisioning race (finding #1): after acquiring
/// [`acquire_base_checkout_lock`], re-running this check tells the caller
/// whether a concurrent caller already finished establishing a valid base
/// while this one was waiting.
///
/// #4270: the base checkout is now the non-bare clone at `<project_dir>`, the
/// same one `daemon::managed_routes::inproject::ensure_base_clone` establishes,
/// so the predicate asks for a WORKING TREE whose root is `dir` itself. The
/// toplevel comparison is load-bearing twice over: a `dir` nested inside some
/// unrelated enclosing repository would otherwise report `true` without holding
/// a clone of its own, and a bare repository (the legacy `.base` shape) reports
/// `false` because it has no working tree at all.
/// What: shells out to `git -C dir rev-parse --show-toplevel` and returns
/// `true` only when the command exits successfully AND the printed toplevel
/// canonicalizes to the same path as `dir`. Returns `false` (never panics) if
/// the subprocess fails to spawn, exits non-zero (not a git working tree),
/// or names a different root.
/// Test: `ensure_base_checkout_recovers_from_concurrent_race`,
/// `ensure_base_checkout_rejects_stale_directory`.
pub
/// Directory name of the LEGACY per-project bare base store retired by #4270.
///
/// Why: one literal, shared by the two guards that must recognise a legacy
/// store and refuse to touch it. Re-exported from [`crate::core::harness_root`]
/// rather than respelled, so the reader that maps a `.base` common-dir back to
/// its project and the writer that refuses to clobber it can never disagree.
/// What: `.base`.
/// Test: `provision_in_leaves_an_existing_dot_base_store_untouched`.
pub use crateBASE_CLONE_DIRNAME as LEGACY_BASE_DIRNAME;
/// Build the refusal for a project directory holding git or trusty-mpm state —
/// or `None` when it holds none.
///
/// Why (#4270): provisioning now clones the base into `<project_dir>` itself,
/// and `git clone` refuses a non-empty destination. Before #4270 `base_dir` was
/// `<project_dir>/.base`, which for an in-project-shaped project does not
/// exist, so [`stale_base_dir_error`] returned `None` and its `mv` hint was
/// unreachable there. Pointing `base_dir` at the project directory made that
/// hint reachable — aimed at the directory holding `.worktrees/<id>` for every
/// live session, and at `.base` with every worktree beneath it. That is the
/// #3605 string with a bigger target, so the whole class is diverted here to a
/// refusal that suggests nothing which moves or deletes anything.
///
/// Reaching this on a HEALTHY repository is the case that makes it matter:
/// [`is_established_checkout`] reports `false` for a transient git spawn
/// failure, a missing `git` binary, and a dubious-ownership rejection alike, so
/// a momentary hiccup lands here with `.git` present. Naming that state and
/// stopping is right in every one of those; suggesting a rename is not.
///
/// KNOWN REGRESSION, accepted. A crashed `git clone` leaves a partial `.git`,
/// so the half-clone #1937 item 1 was written for now lands here and gets NO
/// quarantine command — on the pre-#4270 code it reached the `mv` hint, because
/// a partial BARE clone writes no `.git`. Recovery for that case is genuinely
/// worse. It is accepted because a half-clone and a healthy checkout whose git
/// probe just failed are indistinguishable from here, and one of them holds
/// live worktrees: guessing wrong in the recoverable direction destroys work,
/// guessing wrong in this direction costs the operator one manual decision.
/// What: returns `Some(ProvisionError::Git(..))` naming `project_dir` and the
/// protected entry [`crate::core::harness_root::protected_state_in`] found;
/// `None` otherwise, leaving the caller on its normal path.
/// Test: `provision_in_leaves_an_existing_dot_base_store_untouched`,
/// `stale_base_dir_error_never_offers_to_move_a_dir_holding_live_worktrees`.
/// Build the timestamped sibling path a stale base directory should be
/// QUARANTINED to (moved aside), rather than deleted.
///
/// Why: the recovery hint in [`stale_base_dir_error`] is read and executed
/// verbatim by autonomous agents, so it must name a concrete destination —
/// an agent handed `mv <path> <somewhere-you-choose>` will improvise, and an
/// agent handed a shell-substitution (`$(date …)`) may mangle it. Emitting a
/// fully-resolved path keeps the suggested command copy-pasteable while
/// keeping it non-destructive. The timestamp suffix means repeated recoveries
/// never collide, so an earlier quarantine is never clobbered by a later one.
/// What: `<base_dir>.stale-<UTC YYYYmmdd-HHMMSS>`, a sibling of `base_dir`
/// (same parent, so the `mv` is a cheap same-filesystem rename).
/// Test: `stale_base_dir_error_suggests_quarantine_not_deletion`.
pub
/// Build an actionable error for a base-checkout path occupied by a broken
/// directory — or return `None` when the path is safe to clone into.
///
/// Why: `ensure_base_checkout` correctly refuses to reuse a base path that is
/// not a valid checkout (a leftover from a crashed mid-clone, or any other
/// stale directory), but the resulting failure used to be an opaque
/// `git clone` "destination path already exists" message that gives the
/// operator no recovery path (issue #1937 item 1). This error is, however,
/// consumed almost exclusively by an autonomous agent, which executes a
/// suggested command verbatim — so the message's own recovery hint is a
/// privileged instruction channel, not advice. The original hint was a literal
/// `rm -rf <path>` and is the most plausible trigger of the 2026-07-21 incident
/// (issue #3605) in which `<project>/.base` was destroyed and ~70 worktrees were
/// orphaned machine-wide, each then emitting phantom git-discovery test
/// failures. `.base` is SHARED by every worktree of the project, so deleting it
/// is never a local, recoverable act. The hint is therefore QUARANTINE
/// (`mv` aside to [`stale_base_quarantine_path`]) — matching this crate's
/// existing corrupt-state convention (`core::mcp_config`,
/// `core::standalone::trust_seed` both rename rather than delete) — and the
/// message states the shared-ownership blast radius explicitly, which is the
/// context whose absence made the destructive hint look safe to follow.
/// What: returns `None` when `base_dir` is absent or empty (either is safe —
/// `git clone` accepts a missing or empty target); otherwise returns
/// `Some(ProvisionError::Git(..))` whose message names the exact path, warns
/// that the path is shared across sessions, and gives a single non-destructive
/// `mv <path> <path>.stale-<timestamp>` command. It never emits a recursive
/// delete. A whole class is answered ahead of that, via [`protected_dir_error`]:
/// a directory holding git or trusty-mpm state gets a refusal that suggests
/// moving nothing (#4270), because the `mv` hint aimed at a project directory
/// would rename live worktrees out from under their sessions. Only foreign
/// debris — no `.git`, no `.base`, no `.worktrees` — still gets the quarantine
/// hint. Callers MUST invoke this only AFTER confirming the path is not already
/// a valid checkout (via [`is_established_checkout`]) so a healthy base is
/// never flagged.
/// Test: `ensure_base_checkout_rejects_stale_directory`,
/// `fake_ensure_base_checkout_rejects_stale_directory` (both assert
/// the message names the path and carries the quarantine hint), and
/// `stale_base_dir_error_suggests_quarantine_not_deletion` (asserts the message
/// contains NO destructive command — the #3605 regression guard).
pub
/// Determine whether `dir` is an already-established (fake) checkout,
/// mirroring [`is_established_checkout`]'s intent for `FakeGitBackend`.
///
/// Why: `FakeGitBackend::ensure_base_checkout` used to treat a lone
/// `dir.join("HEAD").is_file()` as proof of an established base — the exact
/// superficial file-existence probe the real backend abandoned in favour of
/// asking git (issue #1937 item 3). A future test author using the fake to
/// simulate a stale base (a stray `HEAD` file with no repository structure)
/// would have gotten a false-positive "already established" pass, hiding the
/// very stale-directory bug a real-backend test would catch. This check
/// restores that fidelity in the filesystem-light fake.
/// What: returns `true` only when the `.git/config` file
/// [`write_fake_checkout`] writes exists — the same marker
/// `FakeGitBackend::is_git_repo` and `clone_repo` already use, and the
/// filesystem-light stand-in for the real backend's working-tree probe. A
/// directory containing only a stray `HEAD` (the stale-mid-clone shape) reads
/// as NOT established, matching the real backend.
/// Test: `fake_ensure_base_checkout_rejects_stale_directory`,
/// `provision_reuses_base_checkout_across_sessions`.
pub
/// Write the minimal structural markers that make a directory read as an
/// established (fake) checkout.
///
/// Why: `FakeGitBackend::ensure_base_checkout` must leave behind enough state
/// that [`fake_is_established_checkout`] recognises it on the next call
/// (idempotent reuse across sessions) while still being distinguishable from a
/// stale directory that merely holds a stray `HEAD` file (issue #1937 item 3).
/// What: creates `dir/.git` (and parents) and writes a `config` naming
/// `repo_url` as `origin`, matching what `FakeGitBackend::clone_repo` produces
/// so `is_git_repo`/`remote_url` answer consistently for a faked base clone.
/// Returns any I/O error as [`ProvisionError::Io`].
/// Test: `provision_reuses_base_checkout_across_sessions` (second provision is a
/// no-op reuse), `fake_ensure_base_checkout_rejects_stale_directory`.
pub
/// Maximum time a caller will wait to acquire the base-checkout lock before
/// giving up.
///
/// Why: a real clone of a large repository can legitimately take a while;
/// this must be generous enough not to spuriously fail a normal (non-racing)
/// first provision on a slow network, while still bounding how long a
/// session-spawn request can block.
/// What: 60 seconds.
/// Test: `ensure_base_checkout_recovers_from_concurrent_race` completes well
/// within this window against a local, near-instant clone.
pub const LOCK_ACQUIRE_TIMEOUT: Duration = from_secs;
/// Interval between retries while polling for the base-checkout lock.
///
/// Why: short enough that a waiting caller notices the lock's release
/// quickly, long enough not to busy-spin the CPU.
/// What: 50 milliseconds.
/// Test: covered indirectly by every test that exercises the lock.
const LOCK_POLL_INTERVAL: Duration = from_millis;
/// Age after which an existing lock marker file is treated as abandoned.
///
/// Why: the lock marker is a plain file, not a kernel-tracked `flock` — if
/// the process holding it crashes (killed, panicked, power loss) mid-clone,
/// the marker file is never cleaned up by a `Drop` impl that never ran. A
/// purely time-bound marker would otherwise deadlock every future
/// provisioning attempt for that project forever.
/// What: 5 minutes — comfortably longer than [`LOCK_ACQUIRE_TIMEOUT`] so a
/// live, slow-but-progressing clone is never mistaken for an abandoned one.
/// Test: `base_checkout_lock_recovers_stale_lock_marker`.
pub const LOCK_STALE_AFTER: Duration = from_secs;
/// Compute the sibling marker-file path used to serialize concurrent
/// `ensure_base_checkout` calls for the same `base_dir`.
///
/// Why: the lock must live NEXT TO `base_dir` (not inside it), since
/// `base_dir` itself may not exist yet when the lock is first acquired.
/// What: `<base_dir's parent>/<base_dir's file name>.lock`, e.g.
/// `<repos_root>/<owner>/<repo>.lock` for a `<owner>/<repo>` project dir.
/// Test: exercised transitively by every `ensure_base_checkout` test.
pub
/// RAII guard holding the base-checkout provisioning lock.
///
/// Why: guarantees the marker file is removed when `ensure_base_checkout`
/// returns via ANY path (success, error, or early return) — a manual
/// remove-on-every-branch approach is error-prone and easy to miss on a new
/// return path added later.
/// What: wraps the locked path; `Drop` best-effort removes the marker file
/// (a failure to remove is not actionable here and must not panic in a
/// destructor).
/// Test: `ensure_base_checkout_recovers_from_concurrent_race` (the lock is
/// released promptly enough for every racing thread to eventually proceed).
pub
/// Acquire the advisory base-checkout provisioning lock at `lock_path`.
///
/// Why: resolves the TOCTOU race (#1935 review finding #1) at its root by
/// serializing the whole check-and-clone window, rather than trying to
/// detect and recover from corruption after the fact — a real `git clone`
/// is not safe against a concurrent writer into the same target
/// directory (empirically observed failure mode without this lock: two
/// interleaved clones can corrupt each other's partial checkout, e.g.
/// colliding on copying `hooks/commit-msg.sample`, rather than cleanly
/// failing with "already exists").
/// What: retries `OpenOptions::new().write(true).create_new(true)` — atomic
/// at the filesystem level, failing with `AlreadyExists` iff another caller
/// currently holds the lock — for up to [`LOCK_ACQUIRE_TIMEOUT`], sleeping
/// [`LOCK_POLL_INTERVAL`] between attempts. A lock file older than
/// [`LOCK_STALE_AFTER`] is treated as abandoned (left behind by a crashed
/// holder) and force-removed before retrying, so a crash mid-clone can never
/// permanently deadlock future provisioning attempts for the same project.
/// Returns a [`BaseCheckoutLock`] guard that releases the lock on drop.
/// Test: `ensure_base_checkout_recovers_from_concurrent_race`,
/// `base_checkout_lock_recovers_stale_lock_marker`.
pub
/// Determine whether the lock marker at `lock_path` is old enough to be
/// treated as abandoned by a crashed holder.
///
/// Why: split out of [`acquire_base_checkout_lock`] so the staleness policy
/// is independently testable and readable in isolation.
/// What: returns `true` iff the file's modified time is more than
/// [`LOCK_STALE_AFTER`] in the past. Returns `false` (never panics) if the
/// file vanished, its metadata can't be read, or the clock query fails —
/// treating an unreadable lock as NOT stale is the conservative, safe default
/// (it just means this caller waits and retries rather than force-clearing
/// state it can't reason about).
/// Test: `base_checkout_lock_recovers_stale_lock_marker`.
pub