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
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
//! Git-native worktree enumeration and registry-root resolution (#4207 slice 1).
//!
//! Why: every worktree question this crate asks used to be answered by
//! GUESSING at the filesystem. `prune::find_orphaned_worktrees` walked five
//! hard-coded location shapes (`.worktrees/`, `.base/.worktrees/`,
//! `.claude/worktrees/`, `.base/.claude/worktrees/`, and
//! `.base/.worktrees/<id>/.claude/worktrees/`), and both
//! `worktree_safety::git_worktree_list_agrees` and
//! `decommission::remove_session_worktree` derived "which checkout owns this
//! worktree?" from the candidate's GRANDPARENT directory. Neither is a fact;
//! both are inferences that git can answer exactly, and both were wrong on
//! this machine on 2026-07-27: fourteen worktrees physically located inside
//! `<repo>/.base/.worktrees/` are registered to the PARENT repo `<repo>`, so
//! the grandparent rule asked `.base` about them, `.base` correctly said "not
//! mine", and the sweep skipped them conservatively — forever. They were not
//! merely un-reclaimed, they were STRUCTURALLY unreclaimable, and no amount of
//! adding a sixth location shape would have fixed it.
//!
//! Git already maintains the answer. `git worktree list --porcelain` is the
//! registry, and `git rev-parse --git-common-dir` names the checkout that owns
//! it. Deriving from those two commands makes location irrelevant: a worktree
//! is found because git registered it, not because it happened to be parked
//! under a directory name someone remembered to enumerate.
//!
//! What: [`RegisteredWorktree`] (one porcelain record),
//! [`parse_worktree_list`] (the pure parser), [`registry_root_for`] (replaces
//! grandparent-as-repo-root inference), [`list_registered_worktrees`] (what
//! git says about the repository owning a path),
//! [`scan_registered_worktrees`] (#4288 — EVERY registered worktree under the
//! repos root, each carrying the [`Admission`] verdict that says whether it is
//! a reclaim candidate and, when it is not, WHY), and
//! [`enumerate_registered_worktrees_in`] (the admitted subset — every registered
//! worktree living INSIDE a managed project under the repos root; see that
//! function's "positive assertion" section for the structural boundary that
//! bounds the sweep).
//!
//! # An unanswerable probe is never a verdict
//!
//! Every function here returns `Option`/`None` — never a `bool` — for the
//! "could not determine" case, so no caller can silently read a missing `git`
//! binary, a permissions error, or a stalled network mount as a fact about the
//! worktree. This is the same invariant PR #4202's round-1 fix broke by
//! collapsing every `canonicalize()` error kind into "destroyed"; on
//! 2026-07-21 a rule that loose would have told an operator to recreate ~70
//! fully intact worktrees. Callers decide what a `None` means in their own
//! direction of safety — [`enumerate_registered_worktrees_in`] treats it as "no
//! candidates from this anchor" (nothing is proposed for deletion), while
//! `git_worktree_list_agrees` keeps its long-standing best-effort `true`.
//!
//! Test: `worktree_registry_tests`.
use BTreeSet;
use ;
use bounded_git_output;
use git_command;
use WorktreeScope;
/// The bare-clone checkout trusty-mpm provisions alongside a managed project.
///
/// Why: a managed project has at most TWO git checkouts that can own a worktree
/// registry — the project checkout itself and the `.base` clone
/// `inproject::create_session_worktree` creates. Naming the segment
/// once keeps [`enumerate_registered_worktrees_in`] from repeating the literal.
/// This is NOT a worktree-location guess: it names a CHECKOUT to interrogate,
/// and git then reports where that checkout's worktrees actually live.
/// What: `".base"`.
/// Test: `enumerate_finds_worktree_registered_to_base_clone`.
pub const BASE_CLONE_DIRNAME: &str = ".base";
/// One worktree exactly as `git worktree list --porcelain` reports it (#4207).
///
/// Why: the porcelain format carries the four facts the reclaim path needs —
/// where the worktree is, whether it is the main checkout (never a reclaim
/// candidate), whether it is bare (no working tree to reclaim), and whether
/// git already considers it prunable (its directory is gone, so there is
/// nothing to delete). Modelling them as fields rather than re-grepping the
/// raw text at each call site keeps the parse in one place.
/// What: `path` is git's own absolute path for the worktree; `branch` is the
/// short branch name when the worktree is not detached; `is_main` marks the
/// FIRST record, which git always emits for the main worktree.
/// Test: `parse_worktree_list_reads_porcelain_records`.
pub
/// Does `reason` name the Claude Code harness's own agent-worktree lock (#6561)?
///
/// Why: this is the ONE fact that separates the harness's agent-lifetime lock
/// from an operator's standing veto, and it has to be read from the reason
/// string because git records nothing else about who locked a worktree. Measured
/// on this machine (git 2.54.0, Claude Code agent isolation): every live agent's
/// tree carries `claude agent agent-<id> (pid <n> start <time>)`, and every
/// finished agent's tree carries no lock at all — the harness releases it when
/// the agent ends.
/// What: a case-insensitive `claude agent` prefix test on the trimmed reason.
/// Deliberately a PREFIX and not a parse: nothing downstream reads the agent id
/// or the pid out of this string, so recognising the shape is the whole job, and
/// a reason that merely mentions "claude agent" further along is not matched.
/// Test: `harness_agent_lock_reason_matches_the_harness_shape`,
/// `harness_agent_lock_reason_rejects_an_operator_lock`.
pub
/// What git can say, right now, about whether the harness holds `path` (#6561).
///
/// Why: the delegation registry is a `DashMap` rebuilt empty at every daemon
/// boot, so after a restart it answers "nothing claims it" for an agent that is
/// still working — [`super::worktree_ownership::AgentDelegationState::Unknown`],
/// which ADR-0045 forbids reading as "free". Git's lock does not have that
/// defect: it is a file under `.git/worktrees/<id>/locked`, written by the
/// harness when it dispatches an agent into the tree and removed when that agent
/// ends, and it survives a daemon restart because no daemon writes it. So it is
/// the durable second source that can answer the question the registry cannot.
/// What: three answers, and the third is the point. [`Held`](Self::Held) — git
/// reports a harness agent lock. [`Released`](Self::Released) — git lists the
/// worktree and reports no lock on it, which is POSITIVE evidence the harness
/// let go. [`Undeterminable`](Self::Undeterminable) — git could not be asked, or
/// no longer lists the path; never read as released.
/// Test: `harness_lock_state_reports_a_harness_locked_tree_held`,
/// `harness_lock_state_reports_an_unlocked_tree_released`,
/// `harness_lock_state_of_a_non_worktree_is_undeterminable`.
pub
/// Ask git whether the harness still holds `path` (#6561).
///
/// Why: see [`HarnessLockState`]. Resolved from the candidate itself so the
/// answer comes from the repository that actually registered the worktree.
/// What: one `git -C <path> worktree list --porcelain`, matched on the
/// canonicalized path. An operator lock — a lock whose reason is not the
/// harness's — is reported [`Undeterminable`](HarnessLockState::Undeterminable)
/// rather than `Released`, because it is not evidence about the harness either
/// way and must never advance a destructive path.
/// Test: `harness_lock_state_reports_a_harness_locked_tree_held`,
/// `harness_lock_state_reports_an_unlocked_tree_released`,
/// `harness_lock_state_of_an_operator_locked_tree_is_undeterminable`,
/// `harness_lock_state_of_a_non_worktree_is_undeterminable`.
pub
/// The pid a harness agent lock reason names, if it names one (#7974).
///
/// Why: [`is_harness_agent_lock_reason`] deliberately recognises the SHAPE and
/// reads nothing out of it, because nothing needed the pid until adoption did.
/// Adoption needs it: after a daemon restart the delegation registry answers
/// `Unknown` for every agent (ADR-0045), and this lock — written by the harness,
/// never by a daemon — is the only durable record of WHICH process was
/// dispatched into the tree. A pid the kernel does not know is positive
/// evidence that process ended.
/// What: the digits after `(pid ` in a reason already confirmed to be a harness
/// agent lock. Measured shape (git 2.54.0, Claude Code agent isolation):
/// `claude agent agent-<id> (pid 17167 start Mon Sep 14 15:30:46 2026)`.
/// Returns `None` for a non-harness reason, a missing marker, or digits that do
/// not fit a `u32` — never a guess.
/// Test: `harness_lock_pid_reads_the_measured_reason_shape`,
/// `harness_lock_pid_rejects_an_operator_lock`,
/// `harness_lock_pid_is_none_when_the_reason_names_no_pid`.
pub
/// The pid of the harness agent lock git reports on `path`, if any (#7974).
///
/// Why: the one line that touches git, so the parse above stays pure.
/// What: resolves `path` in the registry that actually owns it and reads the
/// pid out of its lock reason. `None` when git could not be asked, when the
/// path is not a registered worktree, when it is unlocked or locked by an
/// operator, or when the reason carries no pid.
/// Test: `harness_lock_pid_of_a_non_worktree_is_none`.
pub
/// Is `pid` running, as far as this process can tell (#7974)?
///
/// Why a tri-state rather than [`crate::core::process::is_process_alive`]'s
/// bool: that wrapper folds every errno it did not expect into "dead", which is
/// the coercion ADR-0045 forbids on a probe feeding a mutation. Adoption takes
/// a tree away from whoever holds it, so an UNANSWERED probe must refuse, not
/// proceed.
/// What: `kill(pid, 0)` — a null signal that performs the existence and
/// permission check and delivers nothing. `Some(true)` when the process exists
/// (`EPERM` included: it exists, under another user); `Some(false)` only on
/// `ESRCH`; `None` for a pid outside the positive `pid_t` range (`0` and
/// negatives are process groups, never one process) and for any other errno.
/// Test: `pid_liveness_separates_running_gone_and_undeterminable` covers the
/// `Ok`, `ESRCH` and out-of-range arms against the live kernel;
/// `classify_kill_probe_*` cover every errno arm.
pub
/// What one `kill(pid, 0)` outcome says about the process (#7974).
///
/// Why separate from [`pid_liveness`]: the `EPERM` and unrecognised-errno arms
/// decide whether adoption refuses, and neither can be provoked hermetically
/// through the real syscall — `EPERM` needs a process owned by another uid that
/// is guaranteed to outlive the test, and no pid produces an arbitrary errno on
/// demand. Splitting the classification out makes both arms testable without a
/// trait seam or an injectable syscall.
/// What: `Some(true)` when the process exists — `EPERM` included, since the
/// kernel checked permission on a process it found; `Some(false)` only on
/// `ESRCH`; `None` for every other errno, which is the fail-closed direction
/// ADR-0045 requires of a probe feeding a mutation.
/// Test: `classify_kill_probe_reads_eperm_as_running`,
/// `classify_kill_probe_reads_an_unrecognised_errno_as_undeterminable`,
/// `classify_kill_probe_agrees_with_the_live_probe_on_ok_and_esrch`.
/// Parse `git worktree list --porcelain` output into records (#4207).
///
/// Why: the parse is the one piece of this module that can be tested without
/// spawning git, and the porcelain format has enough shape (blank-line-
/// separated stanzas, valueless `bare`/`detached`/`prunable` attribute lines)
/// that an ad-hoc `strip_prefix("worktree ")` over all lines — what
/// `git_worktree_list_agrees` did before this change — silently ignores every
/// other fact in the record.
/// What: splits on blank lines; each stanza starts with `worktree <path>` and
/// may carry `HEAD <sha>`, `branch <ref>`, `bare`, `detached`, `locked`, or
/// `prunable` (the last two optionally with a reason). The first stanza is
/// flagged [`RegisteredWorktree::is_main`]. Unknown attribute lines are
/// ignored so a future git version cannot break the parse.
/// Test: `parse_worktree_list_reads_porcelain_records`,
/// `parse_worktree_list_marks_only_the_first_record_main`,
/// `parse_worktree_list_reads_locked_with_and_without_reason`.
pub
/// The checkout that owns the worktree registry `anchor` belongs to (#4207).
///
/// Why: this REPLACES grandparent-as-repo-root inference
/// (`path.parent().and_then(|p| p.parent())`), which was correct only for the
/// two shapes it was written against and wrong for every other one —
/// `<repo>/.claude/worktrees/<name>` resolved to `<repo>/.claude`, which is
/// not a repository at all, and the fourteen `.base`-resident worktrees
/// registered to the parent repo resolved to `.base`, which disowns them.
/// `git rev-parse --git-common-dir` answers the same question as a fact: it
/// names the SHARED git directory every worktree of a repository points at,
/// which is exactly the registry `git worktree list` reads.
/// What: runs `git -C <anchor> rev-parse --path-format=absolute
/// --git-common-dir`. A normal checkout answers `<root>/.git`, so the root is
/// its parent; a bare clone answers with the bare directory itself, which IS
/// the root. Returns `None` when git cannot be run, exits non-zero (including
/// the ordinary "not a git repository" case), or prints nothing usable — never
/// a guessed path.
/// Test: `registry_root_for_linked_worktree_is_the_owning_checkout`,
/// `registry_root_for_non_repo_is_none`.
pub
/// Every worktree git registers for the repository that owns `anchor` (#4207).
///
/// Why: `git worktree list` is the registry. Asking it directly is what makes
/// worktree discovery independent of where a worktree is parked, and it is the
/// only source that can report a worktree registered to one checkout while
/// living inside another's directory tree — the exact state that made fourteen
/// worktrees on this machine unreclaimable.
/// What: runs `git -C <anchor> worktree list --porcelain` and parses the
/// result via [`parse_worktree_list`]. Git resolves the repository from
/// `anchor` itself, so `anchor` may be the checkout root, a linked worktree, or
/// any directory inside one. Returns `None` on a spawn failure or non-zero
/// exit — an unanswerable probe, never an empty list.
/// Test: `list_registered_worktrees_reports_a_real_worktree`,
/// `list_registered_worktrees_none_outside_a_repo`.
pub
/// [`list_registered_worktrees`], keeping WHY the probe failed (#8306).
///
/// Why: a caller that must fail closed on a wedged git has to tell a timeout
/// from "not a repository"; the `Option` form erases that.
/// What: `Err` with [`std::io::ErrorKind::TimedOut`] when git was killed at the
/// ceiling; any other `Err` for a spawn failure or a non-zero exit.
/// Test: `a_timed_out_worktree_list_disagrees`.
pub
/// Every git-registered worktree living under `repos_root` (#4207 slice 1).
///
/// Why: this is the derive-not-walk replacement for
/// `prune::find_orphaned_worktrees`' five hard-coded location shapes. Those
/// shapes could only ever find worktrees someone had already thought to
/// enumerate, and each new agent runtime added a shape (#3649, #3971, and the
/// #3971 follow-up were all "we missed a location" fixes). Asking git removes
/// the category: a worktree is discovered because it is REGISTERED, wherever
/// it sits.
/// What: for each `<repos_root>/<owner>/<repo>/`, interrogates the two
/// checkouts trusty-mpm actually provisions — `<repo>` and
/// `<repo>/.base` — but only after confirming (via [`registry_root_for`]) that
/// the anchor IS that repository's root. That confirmation is load-bearing:
/// without it, `git -C <non-repo>` walks UP the filesystem and would report an
/// unrelated enclosing repository's worktrees. Records that are bare, main,
/// already prunable, or git-`locked` are dropped, as is any worktree failing
/// the [`scan_from_anchor`] containment rule. Results are canonicalized (so
/// they compare cleanly against the canonicalized active-session set) and
/// returned sorted and de-duplicated, since both anchors may report the same
/// worktree.
///
/// # Containment is a POSITIVE assertion, not a denylist (#4224 review, HIGH)
///
/// Enumerating from git's registry rather than from five path shapes widens
/// what can be SEEN, and the first cut of this module let anything git
/// registered anywhere under `repos_root` through — leaving the #3649 ownership
/// sentinel as the single structural boundary in front of an unattended
/// `remove_dir_all`. Replayed against the dogfood machine that admitted seven
/// paths the old walk never produced, FIVE of them the operator's own
/// long-lived checkouts (`<owner>/ft-follow-links`,
/// `<owner>/trusty-tools-main-build`, `<owner>/trusty-tools-worktrees/wt-*`,
/// `<owner>/gb-connect-card`). None was deletable — each lacks a sentinel — but
/// "one gate happens to hold" is not a boundary, and this repository has
/// already lost a bare clone to an over-broad removal (2026-07-21).
///
/// So a candidate must additionally be a STRICT DESCENDANT of the managed
/// project directory whose registry named it. That is an assertion the path IS
/// inside a tree trusty-mpm provisions and manages, which is an invariant of
/// the WRITE side: `provisioner::workspace` only ever creates worktrees beneath
/// `<repos_root>/<owner>/<repo>/` (in the checkout, in its `.base` clone, or in
/// an agent runtime's directory inside either). It is deliberately NOT a list
/// of blessed path shapes — any depth and any directory name inside the project
/// qualifies, so the "we forgot a location" bug class this PR deletes (#3649,
/// #3971) stays deleted. And it is not a denylist of known operator paths: an
/// operator checkout is excluded by its POSITION relative to the repository
/// that owns it, so a newly created one is excluded on the day it is created,
/// under any name.
///
/// Both operands come from git — the path from `git worktree list --porcelain`,
/// the project root from `git rev-parse --git-common-dir` — so this adds no
/// state that could drift out of sync with git's own registry. That matters:
/// worktrees present in one registry and absent from another is a failure this
/// repository has already had twice.
///
/// ## Why provenance is not available, and branch role does not substitute
///
/// The discriminator one WANTS is "was this provisioned for a writing agent, on
/// a workstream branch?". Git cannot answer the first half at all: nothing in
/// the porcelain record, and nothing in the `worktrees/<id>/` admin directory,
/// records WHO ran `git worktree add`. The only provenance fact that exists is
/// trusty-mpm's own #3649 sentinel, which is why the sentinel gate stays where
/// it is — this boundary is layered IN FRONT of it, not a second copy of it.
///
/// Branch role does not substitute, measured rather than assumed. Across the
/// 201 candidates on this machine the branch namespaces are
/// `fix/` (88), `feat/` (44), `docs/` (12), detached (10), `session/` (9),
/// `chore/` (9) — and the operator's own checkouts, the Claude-native
/// `agent-*` worktrees, and the genuinely tm-owned ones are ALL drawn from
/// `fix/` and `feat/`. Gating reclamation on a `session|ws|workstream`
/// namespace would admit 9 of 201 candidates and ZERO of the 3 sentinel-bearing
/// ones — reclamation would silently drop to nothing while still looking
/// healthy. A branch namespace is also just a name pattern, and an operator may
/// check out any branch in their own checkout.
///
/// Containment is therefore the strongest git-observable boundary available,
/// and it is count- and name-independent: the 1.3.0 direction (one worktree per
/// writing agent, more of them, shorter-lived) makes it MORE useful, not less,
/// because every agent worktree is provisioned inside the project tree no
/// matter how many there are or what they are called.
///
/// Measured on the same machine, the rule drops all five operator checkouts,
/// retains all 54 `.base`-resident worktrees (the class that was structurally
/// unreclaimable before this PR), and leaves the count of sentinel-bearing —
/// i.e. actually reclaimable — candidates unchanged at 3. The narrowing costs
/// no reclaim capability; it only removes reach the sweep never needed.
/// Test: `enumerate_finds_worktree_at_an_unwalked_location`,
/// `enumerate_finds_worktree_registered_to_base_clone`,
/// `enumerate_ignores_plain_directory_that_is_not_a_worktree`,
/// `enumerate_excludes_the_main_checkout`,
/// `enumerate_excludes_worktrees_outside_the_repos_root`,
/// `enumerate_excludes_a_sibling_checkout_of_the_same_repo`,
/// `enumerate_excludes_a_worktree_parked_beside_the_project`,
/// `enumerate_excludes_a_locked_worktree`,
/// `enumerate_admits_a_worktree_under_an_adopted_checkout` (#7357).
///
/// `adopted` (#7357) names the checkouts of projects registered from somewhere
/// other than `<repos_root>/<owner>/<repo>`; see
/// [`scan_registered_worktrees`] for the containment rule they get. Pass `&[]`
/// for the walk alone.
///
/// # Scope and rows (#8782)
///
/// `scope` bounds the scan ([`WorktreeScope::all`] for every project). Each
/// admitted path comes back as its [`ScannedWorktree`] row, one row per path,
/// so a caller keeps the registry that named it without asking git again.
/// Test: `an_orphan_sweep_scoped_to_one_project_spares_another`.
pub
/// Order `paths` so a NESTED worktree always precedes the worktree that
/// contains it (#4288, in-scope item 4).
///
/// Why: the pre-#4288 order came straight out of a `BTreeSet<PathBuf>`, which
/// sorts lexicographically — so `…/parent` sorts BEFORE `…/parent/child`.
/// Nine registered worktrees on the dogfood machine are nested inside another
/// registered worktree, and removing a parent first takes its children's
/// directories with it: the child's own removal then finds nothing to remove
/// and git is left holding a dangling registry entry for a path that no longer
/// exists. Reversing the order makes each nested worktree removed by
/// `git worktree remove` (which de-registers it) before its parent is touched.
///
/// This changes ORDER only. It cannot admit a path the containment rules
/// already rejected, and it cannot cause a removal that would not otherwise
/// have happened — every element was already in the returned list.
/// What: sorts by path DEPTH descending (component count), breaking ties
/// lexicographically so the result stays deterministic. Depth-descending is
/// sufficient for the containment property: a descendant always has strictly
/// more components than its ancestor.
/// Test: `enumerate_orders_nested_worktree_before_its_parent`.
/// Why a registered worktree is, or is not, an orphan-reclaim CANDIDATE (#4288).
///
/// Why: `enumerate_registered_worktrees` returns only the admitted paths, so
/// everything it drops is invisible — and on the dogfood machine that is 33 of
/// 118 registered worktrees, 6 of which hold uncommitted work, including the
/// two landmines #4288 documents (a `workspace_path` that is an ordinary
/// subdirectory of a live checkout, and worktrees nested inside a live one).
/// Reconciliation cannot report what enumeration silently discards, so the
/// verdict is now a VALUE the scan carries rather than a `continue` statement.
/// What: [`Admitted`](Self::Admitted) is the reclaim-candidate set — identical
/// to what `enumerate_registered_worktrees` returned before #4288. Every other
/// variant names one exclusion, in the order the scan applies them.
/// Test: `scan_reports_the_excluded_set_with_reasons`.
pub
/// One registered worktree plus everything the scan learned about it (#4288).
///
/// Why: reconciliation needs the registry that named a worktree (that is half
/// the git-derived key), the project bounding it, and the admission verdict —
/// none of which survive `enumerate_registered_worktrees`' `Vec<PathBuf>`.
/// What: `path` is canonical when it resolved (raw otherwise, so an
/// [`Admission::Unresolvable`] entry can still be REPORTED); `project` is the
/// managed project directory; `registry_root` is the checkout whose
/// `git worktree list` named it; `branch` and `admission` complete the record.
/// Test: `scan_reports_the_excluded_set_with_reasons`.
pub
/// Every worktree BOTH managed anchors register under `repos_root`, admitted
/// or not (#4288).
///
/// Why: this is [`enumerate_registered_worktrees_in`] with the `continue`
/// statements turned into values, so reconciliation reports the excluded set
/// instead of inheriting enumeration's blind spot. Sharing one traversal is
/// the point — a second, parallel enumerator is exactly the drift that let the
/// pre-#4207 five-shape walk disagree with itself.
/// What: for each `<repos_root>/<owner>/<repo>/`, interrogates `<repo>` and
/// `<repo>/.base` (each only after [`registry_root_for`] confirms the anchor
/// IS that repository's root), and returns one [`ScannedWorktree`] per
/// porcelain record, deduplicated on (path, registry root) and sorted.
/// Test: `scan_reports_the_excluded_set_with_reasons`,
/// `scan_reports_both_registries_for_the_same_path_prefix`.
///
/// # `adopted` — the checkouts of projects registered elsewhere (#7357)
///
/// Why: the repos-root walk finds a project only at `<repos_root>/<owner>/<repo>`.
/// A project registered from a checkout anywhere else — the case #7357 reports —
/// is never interrogated at all, so reconcile reported zero rows for it, the
/// `--merged-prs` sweep reclaimed nothing under it, and `tm doctor` agreed
/// because all three read this one function. `adopted` is the set of checkouts
/// [`crate::project::worktree_adoption`] recorded when those projects were
/// registered.
/// What: the walk exactly as before, then one pass per adopted checkout. Two
/// rules keep the widening from re-attributing anything:
///
/// 1. An adopted checkout INSIDE `repos_root` is skipped — the walk already
/// owns it, and letting both produce a row would put one path under two
/// projects.
/// 2. An adopted checkout bounds its own candidates: containment is asserted
/// against the checkout itself (the same strict-descendant rule
/// [`enumerate_registered_worktrees_in`]' "positive assertion" section
/// describes), and a path some earlier anchor already produced is left with
/// the attribution it already has.
///
/// Test (#7357): `scan_admits_a_worktree_under_an_adopted_checkout`,
/// `scan_ignores_an_adopted_checkout_inside_the_repos_root`,
/// `scan_does_not_duplicate_a_repeated_adopted_checkout`. Pass `&[]` for the
/// walk alone.
pub
/// [`scan_registered_worktrees`], keeping only the rows `scope` admits (#8782).
///
/// Why: the one chokepoint both prune passes enumerate through, so a scoped
/// run cannot survey — or reclaim — another project's worktrees.
/// Test: `a_project_scope_admits_only_that_projects_worktrees`.
pub
/// Record every worktree registered at an ADOPTED project checkout (#7357).
///
/// Why: the repos-root walk cannot reach these checkouts, and the fix must not
/// widen anything else while reaching them. Both guards below exist so an
/// adopted anchor can only ADD rows for paths nothing else produced.
/// What: skips an anchor that lies inside `canonical_root` (the walk already
/// covers it) and any path already present in `found`; otherwise interrogates
/// the checkout and its `.base` clone exactly as the walk does, with the
/// checkout itself as BOTH the containment root and the bounding project.
/// Test: `scan_admits_a_worktree_under_an_adopted_checkout`,
/// `scan_ignores_an_adopted_checkout_inside_the_repos_root`,
/// `scan_does_not_duplicate_a_repeated_adopted_checkout`.
/// The non-key remainder of a [`ScannedWorktree`], carried through the dedup
/// `BTreeSet` (#4288).
///
/// Why: deduplication is on (path, registry root) — the identity of the entry —
/// but `BTreeSet` needs the WHOLE tuple to be `Ord`, and the payload has to
/// ride along. Keeping it in a named struct rather than a bare tuple keeps
/// `scan_registered_worktrees`' final `map` readable.
/// What: the fields of [`ScannedWorktree`] that are not part of its identity.
/// Test: covered by every `scan_*` test.
/// Record every worktree registered at `anchor`, with its admission verdict.
///
/// Why: extracted so [`scan_registered_worktrees`]' two anchors share one rule
/// rather than duplicating the root-confirmation, filtering, and containment
/// logic — the duplication that let the old walk's five shapes drift apart
/// from each other.
/// What: no-ops unless `anchor` is a directory that is ITSELF the root of the
/// repository owning its registry (see [`registry_root_for`]); then records
/// every worktree, marking [`Admission::Admitted`] only for the non-bare,
/// non-main, non-prunable, non-`locked` ones whose canonicalized path is a
/// STRICT DESCENDANT of `canonical_project` (the managed project directory —
/// see [`enumerate_registered_worktrees_in`]' "positive assertion" section) and
/// also lies under `canonical_root`.
///
/// Both containment checks are applied even though the first normally implies
/// the second: `canonical_project` is derived by canonicalizing a `read_dir`
/// entry, so a symlinked project directory could resolve OUTSIDE the managed
/// root, and the pair keeps the sweep bounded in that case too.
/// Test: covered by every `enumerate_*` and `scan_*` test.
/// The [`Admission`] verdict for one porcelain record (#4288).
///
/// Why: the exclusion rules were `continue` statements inside the scan loop, so
/// nothing could name WHICH rule fired. Splitting them out makes each verdict
/// reportable without changing any of them — the order and the conditions are
/// exactly those `collect_from_anchor` applied before #4288.
/// What: applies, in order, the git-reported vetoes, then canonicalization,
/// then the two containment checks.
/// Test: `scan_reports_the_excluded_set_with_reasons`,
/// `enumerate_excludes_a_locked_worktree`.