trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
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
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
//! Where a worktree's ownership marker lives, and moving it there (#8511).
//!
//! Why: the marker used to sit in the working tree as `.trusty-mpm-worktree`.
//! In a project that does not ignore it, git counts it as untracked, so the
//! tree reads dirty: `git worktree remove` without `--force` refuses, Claude
//! Code's own cleanup refuses, and `git add -A` can commit it (#8368). Claude
//! Code keeps its own marker in git metadata, outside the tree; this module
//! puts ours there too.
//! What: the marker now lives at `git rev-parse --git-path trusty-mpm-worktree`
//! for the tree — `<common-dir>/worktrees/<name>/trusty-mpm-worktree` for a
//! linked worktree, `.git/trusty-mpm-worktree` for a main checkout. It is
//! resolved from the tree's own `.git` entry ([`admin_location`]), never
//! guessed. A directory with no `.git` entry is not a git working tree, so no
//! git clean check can count a file in it; there the in-tree path is the only
//! location. A `.git` entry that exists but does not resolve (a dangling
//! `gitdir:` after the main checkout moved, an unreadable pointer) is NOT that
//! case: the marker may sit in the unreachable admin dir, so ownership is
//! undetermined — the strict reader answers unreadable and retention protects.
//!
//! Readers never write. A read answers the admin marker when present, else the
//! legacy one. Only [`migrate_legacy_sentinel`] (the startup pass, registration
//! and `tm doctor --fix --yes`) and [`write_sentinel_bytes`] change the disk.
//! The migration's decision table, which [`preview_migration`] answers without
//! writing:
//!
//! | on disk | migration does |
//! |---|---|
//! | admin only | nothing |
//! | legacy only | copy to a private temp file, verify, link it in as the admin marker, then remove legacy |
//! | both, same bytes | remove the leftover legacy copy |
//! | both, different bytes | log the conflict, keep both (the admin marker wins on read) |
//! | either unreadable, or `.git` unresolvable | report a failure, keep both |
//! | legacy tracked by git | nothing — never removed (#8368) |
//! | no `.git` entry | nothing |
//!
//! Concurrency: readers, the startup pass, the doctor apply and agent claims
//! can all run at once. Two rules keep ownership from being lost:
//! - every writer creates the admin marker BEFORE it removes the legacy one,
//!   and every reader reads the legacy marker BEFORE the admin one, so no
//!   interleaving answers "no marker" for a tree that always had one;
//! - the migration never overwrites or deletes an admin marker: it links its
//!   verified temp file in with no-clobber semantics, so a concurrent writer's
//!   marker always survives.
//!
//! Test: `worktree_ownership_location_tests`.

use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use super::decommission::WORKTREE_SENTINEL_FILE;
use super::worktree_ownership::{SentinelOwner, WorktreeSentinel, owner_from_payload};

/// The marker's file name inside the git admin directory (#8511).
///
/// Why: `git rev-parse --git-path trusty-mpm-worktree` is the documented
/// location; spelling the name once keeps the writer and every reader on it.
pub(crate) const ADMIN_SENTINEL_NAME: &str = "trusty-mpm-worktree";

/// Where a tree's admin-dir marker lives, as its `.git` entry answers (#8511).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AdminLocation {
    /// No `.git` entry: not a git working tree, so the in-tree path is the only
    /// marker location.
    NotGit,
    /// The marker path inside the resolved git admin dir.
    At(PathBuf),
    /// A `.git` entry exists but does not resolve — dangling, unreadable or
    /// malformed. A marker may sit in the unreachable admin dir, so ownership
    /// is undetermined; the string says why.
    Unresolvable(String),
}

/// Resolve the admin-dir marker location for the tree rooted at `worktree`
/// (#8511).
///
/// Why: the path must match what `git rev-parse --git-path` prints, and must be
/// cheap — readers call this once per candidate in every sweep, so a `git`
/// subprocess per call is not affordable. "Not a git tree" and "a git tree we
/// cannot resolve" must stay apart: after a migration the second may still
/// hold the only marker (round 2 finding 1).
/// What: `<worktree>/.git` absent (not even a dangling symlink) is
/// [`AdminLocation::NotGit`]. A directory is the git dir itself; a file is a
/// linked worktree's `gitdir: <path>` pointer, resolved against `worktree`
/// when relative, and accepted only when it names an existing directory.
/// Anything else is [`AdminLocation::Unresolvable`].
/// Test: `admin_path_matches_git_rev_parse_git_path`,
/// `a_migrated_tree_with_an_unresolvable_git_entry_stays_protected`.
pub(crate) fn admin_location(worktree: &Path) -> AdminLocation {
    use AdminLocation::{At, NotGit, Unresolvable};
    let dot_git = worktree.join(".git");
    match std::fs::symlink_metadata(&dot_git) {
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return NotGit,
        Err(e) => return Unresolvable(format!("cannot stat {}: {e}", dot_git.display())),
        Ok(_) => {}
    }
    let meta = match std::fs::metadata(&dot_git) {
        Ok(meta) => meta,
        Err(e) => return Unresolvable(format!("cannot follow {}: {e}", dot_git.display())),
    };
    if meta.is_dir() {
        return At(dot_git.join(ADMIN_SENTINEL_NAME));
    }
    if !meta.is_file() {
        return Unresolvable(format!("{} is not a file or directory", dot_git.display()));
    }
    let text = match std::fs::read_to_string(&dot_git) {
        Ok(text) => text,
        Err(e) => return Unresolvable(format!("cannot read {}: {e}", dot_git.display())),
    };
    let raw = text
        .lines()
        .find_map(|l| l.strip_prefix("gitdir:"))
        .map(str::trim)
        .unwrap_or_default();
    if raw.is_empty() {
        return Unresolvable(format!("{} names no gitdir", dot_git.display()));
    }
    let git_dir = if Path::new(raw).is_absolute() {
        PathBuf::from(raw)
    } else {
        worktree.join(raw)
    };
    if git_dir.is_dir() {
        At(git_dir.join(ADMIN_SENTINEL_NAME))
    } else {
        Unresolvable(format!("gitdir {} does not resolve", git_dir.display()))
    }
}

/// The admin-dir marker path, when [`admin_location`] resolves one.
/// Test: `admin_path_matches_git_rev_parse_git_path`.
pub(crate) fn admin_sentinel_path(worktree: &Path) -> Option<PathBuf> {
    match admin_location(worktree) {
        AdminLocation::At(path) => Some(path),
        AdminLocation::NotGit | AdminLocation::Unresolvable(_) => None,
    }
}

/// The legacy in-tree marker path, `<worktree>/.trusty-mpm-worktree`.
pub(crate) fn legacy_sentinel_path(worktree: &Path) -> PathBuf {
    worktree.join(WORKTREE_SENTINEL_FILE)
}

/// Does `worktree` carry an ownership marker in either location (#8511)?
///
/// Why: the removal gates and the workspace scanner ask "was this tree
/// provisioned by trusty-mpm?" by the marker's existence; a marker moved to
/// the admin dir must still answer yes.
/// What: legacy file exists, or the admin-dir file exists — legacy first, the
/// module doc's read order. An unresolvable `.git` answers `false`: every
/// caller reads presence as a PERMISSION to remove or as discovery, so "no" is
/// the safe answer there; protection reads [`admin_location`] instead.
/// Test: `a_tree_marked_only_in_the_admin_dir_is_still_marked`.
pub(crate) fn sentinel_present(worktree: &Path) -> bool {
    legacy_sentinel_path(worktree).exists()
        || admin_sentinel_path(worktree).is_some_and(|p| p.exists())
}

/// What one migration of a tree's legacy marker did (#8511).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MarkerMigration {
    /// No legacy in-tree marker: nothing to move.
    NoLegacy,
    /// Not a git working tree, so the in-tree location is the only one.
    NoAdminDir,
    /// The legacy marker was copied, verified, and removed.
    Migrated,
    /// Both existed with identical bytes; the leftover legacy copy was removed.
    DuplicateRemoved,
    /// Both existed with different bytes, or a writer created a different
    /// admin marker mid-copy. The admin marker wins; both are kept.
    Conflict,
    /// The legacy marker is tracked by git (#8368), so it is left in place.
    Tracked,
    /// A step failed, or a marker could not be read. The legacy marker is
    /// kept, so ownership is not lost.
    Failed(String),
}

/// Is `worktree`'s legacy marker tracked by git (#8368)?
///
/// Why: removing a committed marker would dirty the tree. Only asked when a
/// legacy marker exists, so the subprocess stays off the common path.
/// What: `git ls-files -- .trusty-mpm-worktree` is non-empty. A git failure
/// answers `true` — keeping a file is the safe direction.
/// Test: `a_committed_legacy_marker_is_never_removed`.
pub(crate) fn legacy_is_tracked(worktree: &Path) -> bool {
    let mut ls_files = trusty_common::git::command_in(worktree);
    ls_files.args(["ls-files", "--", WORKTREE_SENTINEL_FILE]);
    // #8306: bounded; a timeout answers `true` like every other failure.
    match super::git_ceiling::bounded_git_output(ls_files) {
        Ok(out) if out.status.success() => !out.stdout.is_empty(),
        _ => true,
    }
}

/// A marker that exists but could not be read, or does not parse (#8511).
///
/// Why: a reclaim gate must keep a tree whose marker is present but
/// unreadable, yet may permit one with no marker; the tolerant reader answers
/// both as owner-unknown, so the strict reader names the difference.
/// Test: `strict_read_separates_missing_from_unreadable_and_corrupt`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OwnerReadError {
    /// The marker exists at `path` but reading it failed — or the tree's
    /// `.git` entry does not resolve, so the admin-dir marker cannot be
    /// looked for; `path` is then the `.git` entry.
    Unreadable { path: PathBuf, reason: String },
    /// The marker at `path` was read but is not a valid ownership payload.
    Corrupt { path: PathBuf, reason: String },
}

/// Read `path`: `Ok(None)` when absent, `Err` for any other failure.
fn read_present(path: &Path) -> Result<Option<Vec<u8>>, OwnerReadError> {
    match std::fs::read(path) {
        Ok(bytes) => Ok(Some(bytes)),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(e) => Err(OwnerReadError::Unreadable {
            path: path.to_path_buf(),
            reason: e.to_string(),
        }),
    }
}

/// Where the winning marker was found and its bytes, `Ok(None)` for no marker.
type Found = Result<Option<(PathBuf, Vec<u8>)>, OwnerReadError>;

/// The marker's location and bytes, or `Ok(None)` when neither location holds
/// one (#8511). Reads only; never writes.
/// Test: `the_admin_marker_wins_when_both_exist`,
/// `a_migration_between_the_two_reads_still_finds_the_marker`.
pub(crate) fn read_sentinel_bytes_strict(worktree: &Path) -> Found {
    read_both_with(worktree, &|| {})
}

/// The one read path, with a hook between its two reads so a test can
/// interleave a migration or a write there.
///
/// Legacy is read FIRST (see the module doc): a writer removes legacy only
/// after the admin marker exists, so a legacy miss here means the admin read
/// that follows sees the marker.
fn read_both_with(worktree: &Path, between: &dyn Fn()) -> Found {
    let legacy = legacy_sentinel_path(worktree);
    let legacy_read = read_present(&legacy);
    let admin = match admin_location(worktree) {
        AdminLocation::NotGit => return legacy_read.map(|b| b.map(|b| (legacy, b))),
        AdminLocation::At(admin) => admin,
        // Round 2 finding 1: the admin marker, which wins, cannot be looked for.
        // Round 3 finding (#8511): this runs on every read of a dangling
        // `gitdir:` — the tolerant `read_sentinel_owner` path and every
        // candidate in the ~60s orphan sweep — so it stays at `debug!`, not
        // `warn!`. `migrate_with` still logs a per-tree `warn!` once per pass
        // (see its `MarkerMigration::Failed` arm); the returned
        // `Err(Unreadable{reason})` already carries the reason for any caller
        // that wants to report it once.
        AdminLocation::Unresolvable(reason) => {
            tracing::debug!(
                worktree = %worktree.display(),
                "ownership marker: the git admin dir cannot be resolved ({reason}); owner unknown (#8511)"
            );
            return Err(OwnerReadError::Unreadable {
                path: worktree.join(".git"),
                reason,
            });
        }
    };
    between();
    match read_present(&admin) {
        Ok(Some(bytes)) => Ok(Some((admin, bytes))),
        Ok(None) => legacy_read.map(|b| b.map(|b| (legacy, b))),
        Err(e) => {
            tracing::warn!(
                worktree = %worktree.display(),
                "ownership marker: the admin-dir marker cannot be read ({e:?}); owner unknown (#8511)"
            );
            Err(e)
        }
    }
}

/// Read and STRICTLY parse `worktree`'s ownership marker (#8511).
///
/// Why: see [`OwnerReadError`]. [`super::worktree_ownership::read_sentinel_owner`]
/// stays tolerant for every existing caller.
/// What: [`read_sentinel_bytes_strict`], then a parse; never writes.
/// `Ok(None)` — no marker in either location. `Ok(Some(owner))` — a marker is
/// present and parses; an empty (pre-#3649) marker or one naming no owner is
/// `Ok(Some(SentinelOwner::Unknown))`, which a caller must treat as PRESENT.
/// `Err(Unreadable)` — the winning location exists but cannot be read.
/// `Err(Corrupt)` — it was read but is not a valid payload.
/// Test: `strict_read_separates_missing_from_unreadable_and_corrupt`.
pub(crate) fn read_sentinel_owner_strict(
    worktree: &Path,
) -> Result<Option<SentinelOwner>, OwnerReadError> {
    let Some((path, bytes)) = read_sentinel_bytes_strict(worktree)? else {
        return Ok(None);
    };
    if bytes.is_empty() {
        return Ok(Some(SentinelOwner::Unknown));
    }
    serde_json::from_slice::<WorktreeSentinel>(&bytes)
        .map(|payload| Some(owner_from_payload(payload)))
        .map_err(|e| OwnerReadError::Corrupt {
            path,
            reason: e.to_string(),
        })
}

/// Writes `bytes` to a NEW file at `path` — the production migration copy step.
fn write_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
    let mut file = std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)?;
    file.write_all(bytes)?;
    file.sync_all()
}

/// Move `worktree`'s legacy marker to the admin dir, if it has one (#8511).
///
/// Why: the one-shot fleet migration, registration and the doctor repair are
/// the only paths allowed to move a marker; readers never do.
/// What: the module doc's decision table. Never overwrites or deletes an admin
/// marker.
/// Test: `a_legacy_marker_is_read_then_migrated`,
/// `a_failed_migration_keeps_the_legacy_marker`.
pub(crate) fn migrate_legacy_sentinel(worktree: &Path) -> MarkerMigration {
    migrate_with(worktree, &write_new)
}

/// [`migrate_legacy_sentinel`] with an injectable copy step, so a test can make
/// the copy lie or race a writer.
/// Test: `a_copy_that_does_not_verify_keeps_the_legacy_marker`,
/// `a_concurrent_writers_marker_survives_the_migration`,
/// `identical_bytes_from_a_concurrent_migration_settle_cleanly`,
/// `a_failed_duplicate_removal_is_logged_for_the_tree`.
fn migrate_with(
    worktree: &Path,
    copy: &dyn Fn(&Path, &[u8]) -> std::io::Result<()>,
) -> MarkerMigration {
    let outcome = match decide(worktree) {
        Decision::Settled(outcome) => outcome,
        Decision::RemoveLegacy(legacy) => remove_duplicate(&legacy),
        Decision::Copy {
            legacy,
            admin,
            bytes,
        } => migrate_bytes(&legacy, &admin, &bytes, copy),
    };
    // Round 2 finding 5: one warning per tree, whichever step produced it.
    match &outcome {
        MarkerMigration::Failed(reason) => tracing::warn!(
            worktree = %worktree.display(),
            "ownership marker: legacy marker kept in the tree — {reason} (#8511)"
        ),
        MarkerMigration::Conflict => tracing::warn!(
            worktree = %worktree.display(),
            "ownership marker: the admin-dir and in-tree markers differ; the admin-dir \
             marker wins and both are kept (#8511)"
        ),
        _ => {}
    }
    outcome
}

/// What [`migrate_legacy_sentinel`] would do to `worktree` now, decided by
/// reads alone (#8511).
///
/// Why: the doctor's dry run must report what apply will do (round 2 finding
/// 3); planning a move that apply then refuses never converges.
/// What: the outcome [`migrate_legacy_sentinel`] returns when nothing changes
/// on disk in between. Never writes, never logs.
/// Test: `the_doctor_dry_run_reports_what_apply_will_do`.
pub(crate) fn preview_migration(worktree: &Path) -> MarkerMigration {
    match decide(worktree) {
        Decision::Settled(outcome) => outcome,
        Decision::RemoveLegacy(_) => MarkerMigration::DuplicateRemoved,
        Decision::Copy { .. } => MarkerMigration::Migrated,
    }
}

/// The migration's decision for one tree, taken from reads only.
enum Decision {
    /// Nothing to change on disk; this is the outcome.
    Settled(MarkerMigration),
    /// Both markers hold the same bytes: remove the leftover legacy copy.
    RemoveLegacy(PathBuf),
    /// Only the legacy marker exists: copy its bytes into the admin dir.
    Copy {
        legacy: PathBuf,
        admin: PathBuf,
        bytes: Vec<u8>,
    },
}

/// The module doc's decision table, shared by the migration and its preview.
fn decide(worktree: &Path) -> Decision {
    use MarkerMigration::{Conflict, Failed, NoAdminDir, NoLegacy, Tracked};
    let admin = match admin_location(worktree) {
        AdminLocation::NotGit => return Decision::Settled(NoAdminDir),
        AdminLocation::At(admin) => Ok(admin),
        AdminLocation::Unresolvable(reason) => Err(reason),
    };
    let legacy = legacy_sentinel_path(worktree);
    let bytes = match read_present(&legacy) {
        Ok(Some(b)) => b,
        Ok(None) => return Decision::Settled(NoLegacy),
        // An unreadable legacy marker is a failure the operator must see.
        Err(e) => {
            return Decision::Settled(Failed(format!("the in-tree marker is unreadable: {e:?}")));
        }
    };
    let admin = match admin {
        Ok(admin) => admin,
        Err(reason) => {
            return Decision::Settled(Failed(format!(
                "the git admin dir cannot be resolved: {reason}"
            )));
        }
    };
    if legacy_is_tracked(worktree) {
        return Decision::Settled(Tracked);
    }
    match read_present(&admin) {
        Ok(Some(admin_bytes)) if admin_bytes == bytes => Decision::RemoveLegacy(legacy),
        Ok(Some(_)) => Decision::Settled(Conflict),
        Ok(None) => Decision::Copy {
            legacy,
            admin,
            bytes,
        },
        Err(e) => Decision::Settled(Failed(format!("the admin-dir marker is unreadable: {e:?}"))),
    }
}

/// Both locations hold the same bytes: remove the leftover legacy copy.
fn remove_duplicate(legacy: &Path) -> MarkerMigration {
    match std::fs::remove_file(legacy) {
        Ok(()) => MarkerMigration::DuplicateRemoved,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => MarkerMigration::DuplicateRemoved,
        Err(e) => MarkerMigration::Failed(format!("could not remove the duplicate: {e}")),
    }
}

/// A fresh temp-file path beside `admin`, unique within and across processes.
fn temp_beside(admin: &Path) -> PathBuf {
    static SEQ: AtomicU64 = AtomicU64::new(0);
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |d| d.as_nanos());
    let name = format!(
        "{ADMIN_SENTINEL_NAME}.migrate-{}-{nanos}-{}",
        std::process::id(),
        SEQ.fetch_add(1, Ordering::Relaxed)
    );
    admin.with_file_name(name)
}

/// Copy to a private temp file, verify it byte for byte, link it in as the
/// admin marker, then remove the legacy marker (#8511).
///
/// The verification runs on a file only this call knows, and the link fails
/// rather than replace an existing admin marker — so this never overwrites or
/// deletes a concurrent writer's marker. Any failure removes only the temp
/// file and keeps the legacy marker.
fn migrate_bytes(
    legacy: &Path,
    admin: &Path,
    bytes: &[u8],
    copy: &dyn Fn(&Path, &[u8]) -> std::io::Result<()>,
) -> MarkerMigration {
    let temp = temp_beside(admin);
    let staged = copy(&temp, bytes)
        .map_err(|e| format!("copy to {} failed: {e}", temp.display()))
        .and_then(|()| match std::fs::read(&temp) {
            Ok(copied) if copied == bytes => Ok(()),
            Ok(_) => Err("the copy did not verify byte for byte".to_string()),
            Err(e) => Err(format!("the copy could not be read back: {e}")),
        });
    let linked = staged.and_then(|()| match std::fs::hard_link(&temp, admin) {
        Ok(()) => Ok(true),
        // A writer created the admin marker first; it is authoritative.
        Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(false),
        Err(e) => Err(format!("could not link {}: {e}", admin.display())),
    });
    if let Err(e) = std::fs::remove_file(&temp)
        && e.kind() != std::io::ErrorKind::NotFound
    {
        tracing::warn!(temp = %temp.display(), "ownership marker: temp copy not removed ({e}) (#8511)");
    }
    match linked {
        Err(reason) => MarkerMigration::Failed(reason),
        // Round 2 finding 2: a concurrent migration that linked the same bytes
        // settles the tree; only different bytes are a conflict.
        Ok(false) => match read_present(admin) {
            Ok(Some(admin_bytes)) if admin_bytes == bytes => remove_duplicate(legacy),
            Ok(Some(_)) => MarkerMigration::Conflict,
            Ok(None) => MarkerMigration::Failed(format!(
                "{} refused the link, then vanished",
                admin.display()
            )),
            Err(e) => MarkerMigration::Failed(format!("the admin-dir marker is unreadable: {e:?}")),
        },
        Ok(true) => match std::fs::remove_file(legacy) {
            Ok(()) => MarkerMigration::Migrated,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => MarkerMigration::Migrated,
            // Both copies now match; the next migration removes the duplicate.
            Err(e) => {
                MarkerMigration::Failed(format!("verified copy made, legacy not removed: {e}"))
            }
        },
    }
}

/// Write `bytes` as `worktree`'s ownership marker (#8511).
///
/// Why: every marker writer — the agent claim, session provisioning, adoption —
/// goes through here, so a new marker never lands in the working tree.
/// What: writes the admin-dir marker when the tree has one, THEN removes any
/// legacy in-tree marker unless git tracks it (the new write supersedes it; a
/// failed removal is logged, and the admin marker still wins on read). The
/// order is the module doc's writer rule. With no `.git` entry it writes the
/// in-tree path; an unresolvable `.git` is an error, since a marker written
/// in the tree there could not win over one in the unreachable admin dir.
/// Write errors propagate.
/// Test: `the_agent_marker_is_written_to_the_admin_dir`.
pub(crate) fn write_sentinel_bytes(worktree: &Path, bytes: &[u8]) -> std::io::Result<()> {
    let legacy = legacy_sentinel_path(worktree);
    let admin = match admin_location(worktree) {
        AdminLocation::NotGit => return std::fs::write(legacy, bytes),
        AdminLocation::At(admin) => admin,
        AdminLocation::Unresolvable(reason) => {
            return Err(std::io::Error::other(format!(
                "ownership marker not written: the git admin dir cannot be resolved ({reason})"
            )));
        }
    };
    std::fs::write(&admin, bytes)?;
    if !legacy.exists() || legacy_is_tracked(worktree) {
        return Ok(());
    }
    match std::fs::remove_file(&legacy) {
        Ok(()) => {}
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
        Err(e) => tracing::warn!(
            worktree = %worktree.display(),
            "ownership marker: written to the admin dir, but the old in-tree marker could \
             not be removed ({e}) (#8511)"
        ),
    }
    Ok(())
}

#[cfg(test)]
#[path = "worktree_ownership_location_tests.rs"]
mod worktree_ownership_location_tests;