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
//! Startup hygiene for managed base clones (#1709).
//!
//! Why: the protected base clone used by the in-project spawn path (#1706) must
//! always be on the default branch and up to date with the remote when the
//! daemon starts. Without hygiene, a stale or diverged base clone silently
//! yields sessions on an outdated branch, confusing agents. Running a short
//! fetch + fast-forward sequence at daemon startup keeps every base clone
//! current. Dead worktrees from previous sessions are also pruned to prevent
//! the base clone's worktree list from growing indefinitely.
//!
//! CRITICAL SAFETY INVARIANT (#2177, #4961): the hygiene sweep must NEVER
//! discard local work of any kind. The update step is a NON-DESTRUCTIVE
//! fast-forward (`git merge --ff-only`), never `git reset --hard`, and it is
//! preceded by three gates: the checked-out branch must be the default branch
//! (so the safety checks validate the very ref being moved), it must have zero
//! unpushed commits and a clean working tree, and no path the update would
//! write may already exist untracked on disk.
//!
//! That last gate is what #4961 added, and it is not redundant with the dirty
//! check. `git status --porcelain` does not report gitignored paths at all, so
//! a gitignored file holding real user content reads as "clean" — and both
//! `git reset --hard` AND `git merge --ff-only` silently overwrite it when the
//! target commit tracks the same path. A user pointing Obsidian, Cursor, or
//! any editor at a managed checkout creates exactly that shape. Adding
//! `--ignored` to the dirty check is NOT the fix: `target/` is gitignored and
//! present in any built checkout, so the gate would always see dirt and
//! hygiene would become a permanent no-op.
//!
//! What: [`get_default_branch`] reads the origin/HEAD symref;
//! [`run_hygiene_for_base`] runs fetch, a gated fast-forward, and worktree
//! prune for one base clone directory; [`run_hygiene_for_all_bases`] walks
//! `<repos_root>/<owner>/<repo>/` and calls the per-base function for each;
//! wired into daemon startup via
//! [`super::inproject_hygiene_sweep::spawn_if_enabled`]. A per-repo opt-out
//! marker file ([`HYGIENE_OPT_OUT_MARKER`]) disables the sweep for a single
//! checkout.
//!
//! # Bounded, throttled and skippable (#7965)
//!
//! Every git spawn goes through [`git_bounded`], which is the ONLY place this
//! module builds a `Command`: it carries `GIT_TERMINAL_PROMPT=0` and a
//! [`GIT_TIMEOUT`] ceiling, because a `git fetch` waiting on a credential prompt
//! nobody can answer is what `sample` caught pinning a blocking-pool thread. On
//! top of that, [`fetch_is_due`] skips the expensive step for a base fetched
//! within [`FETCH_MIN_INTERVAL`], [`PASS_BUDGET`] bounds the whole walk, and a
//! short pause between bases keeps the sweep from occupying every core
//! continuously. The daemon's request path and this sweep share one resource —
//! this process's CPU and its share of the host's IO — and on a host with 72 base
//! clones an unbounded pass held it for over nine minutes.
//! Test: unit coverage in `inproject_hygiene_tests.rs`; `hygiene_*` integration
//! tests in `crates/trusty-mpm/tests/inproject_hygiene_test.rs` exercise real
//! temp git repos for the ahead/dirty/clean/gitignored/off-default/opt-out
//! cases.
use HashSet;
use Path;
use ;
use ;
use crate;
/// Wall-clock ceiling for any single `git` invocation in this sweep (#7965).
///
/// Why 60 seconds: every step but the fetch is a local git read that answers in
/// milliseconds, and the fetch is the only one that legitimately touches the
/// network. 60 s is generous for a cold fetch on a slow link and FINITE for a
/// wedged one — which is the case #7965 caught, a blocking-pool thread parked in
/// a child's stdout read with no bound at all. A base that hits the ceiling is
/// abandoned and logged; the sweep moves on.
/// Test: `a_wedged_hygiene_fetch_neither_hangs_the_sweep_nor_delays_health`.
pub const GIT_TIMEOUT: Duration = from_secs;
/// Wall-clock ceiling for one whole sweep over every base clone (#7965).
///
/// Why: the per-command ceiling bounds one child, not the pass. The reporting
/// host has 72 base clones; at ~10 commands each, a pathological pass could still
/// occupy the maintenance lane for over an hour. Hygiene is a freshness chore —
/// skipping the tail of a pass leaves some base clones one boot staler and
/// discards nothing — so bounding the pass is free, and it is what keeps the
/// sweep from becoming a permanent tax on the request path.
/// Test: `hygiene_pass_budget_stops_the_sweep`.
pub const PASS_BUDGET: Duration = from_secs;
// The pointer above and the one on `run_hygiene_for_all_bases_within` name the
// same test deliberately: the constant is only meaningful through the walk that
// spends it.
/// Skip a base clone's fetch when it was already fetched this recently (#7965).
///
/// Why: the fetch is the expensive step, and the sweep runs once per daemon
/// boot. On a host that restarts the daemon a few times an hour, 72 network
/// fetches per restart is the bulk of the load this issue is about, and almost
/// all of it re-fetches what the previous pass already has.
/// What: read from `.git/FETCH_HEAD`'s mtime — git writes it on every fetch, so
/// no new state is persisted for this. An unreadable mtime FETCHES, which is the
/// pre-#7965 behaviour.
/// Test: `hygiene_skips_a_recently_fetched_base`, `fetch_is_due_without_a_fetch_head`.
pub const FETCH_MIN_INTERVAL: Duration = from_secs;
/// Pause between base clones so the sweep leaves the machine some air (#7965).
///
/// Why: the sweep is a serial loop, but `git fetch` spawns its own multi-threaded
/// `index-pack`, so back-to-back bases keep every core busy continuously. A short
/// gap per base costs ~14 s over 72 bases and gives the request path a scheduling
/// window it did not have.
const BASE_PAUSE: Duration = from_millis;
/// Run one `git` command against a base clone, bounded and non-interactive.
///
/// Why: the single place this sweep spawns git, so the #7965 bound and the
/// credential-prompt refusal cannot be forgotten by a call site. `git fetch` with
/// no terminal-prompt guard blocks forever on a repository whose credentials have
/// expired — git waits on a prompt nobody can answer, which is exactly the
/// unbounded stdout read `sample` caught.
/// What: `trusty_common::git::command()` (so #7171's maintenance/gc suppression
/// still applies) plus `-C <base_path>`, `GIT_TERMINAL_PROMPT=0` and an ssh
/// transport in batch mode, run through [`run_bounded`] with [`GIT_TIMEOUT`].
/// `Err` carries a one-line reason for a spawn failure, a timeout, or a wait
/// failure; a non-zero EXIT is `Ok` and left for the caller to judge.
/// Test: `a_wedged_hygiene_fetch_neither_hangs_the_sweep_nor_delays_health`.
/// Marker file that opts a single base clone out of the startup hygiene sweep.
///
/// Why: before #4961 the only control was the process-wide
/// `TRUSTY_MPM_INPROJECT_HYGIENE` env var — all-or-nothing across every managed
/// checkout. A user who actively edits one checkout in an external editor had
/// no way to exempt just that one without disabling hygiene everywhere.
/// What: an empty file named `.trusty-mpm-no-hygiene` in the base clone root.
/// Its mere presence skips every step of the sweep for that checkout.
/// Test: `hygiene_opt_out_marker_skips_update` (integration),
/// `hygiene_opt_out_marker_detected` (unit).
pub const HYGIENE_OPT_OUT_MARKER: &str = ".trusty-mpm-no-hygiene";
/// Decision on whether the fast-forward update step may proceed.
///
/// Why: the update decision has several independent inputs (checked-out branch
/// identity, ahead-count, working-tree cleanliness, detached/unknown states)
/// that are easiest to reason about — and unit-test — as a small pure function
/// separated from the git-shelling plumbing that gathers those inputs.
/// What: two variants — `Update` (safe to fast-forward the base clone to
/// origin) and `Skip(reason)` (refuse, carrying a human-readable reason for
/// the warning log).
/// Test: `decide_update_*` unit tests in `inproject_hygiene_tests.rs`.
/// Decide whether the fast-forward may proceed, given branch, ahead and dirty state.
///
/// Why: centralizes the data-loss-prevention rule in one pure, testable place
/// rather than scattering conditionals through the git-shelling code. Any
/// input that could not be determined (detached HEAD, no upstream, a failed
/// `git` invocation) is treated conservatively as "do not update".
///
/// The branch gate closes #4961's second finding: the ahead-count is measured
/// as `origin/<checked-out>..<checked-out>`, but the update targets
/// `origin/<default>`. Off the default branch those are different refs, so the
/// safety check could pass against one ref while the operation moved the local
/// branch to another. Requiring `checked_out == default_branch` makes the
/// check validate exactly the ref being moved.
/// What: `checked_out: None` (detached HEAD) yields `Skip`. A `checked_out`
/// that differs from `default_branch` yields `Skip`. `ahead_count: None`
/// (unknown — no upstream, or a `rev-list` failure) yields `Skip`. `dirty:
/// None` (a `status` failure) yields `Skip`. `ahead_count: Some(n) if n > 0`
/// yields `Skip` (unpushed commits). `dirty: Some(true)` yields `Skip`. Only a
/// checked-out default branch with `Some(0)` ahead and `Some(false)` dirty
/// yields `Update`.
/// Test: `decide_update_ahead_skips`, `decide_update_dirty_skips`,
/// `decide_update_unknown_ahead_skips`, `decide_update_unknown_dirty_skips`,
/// `decide_update_detached_head_skips`, `decide_update_non_default_branch_skips`,
/// `decide_update_clean_and_even_updates`.
/// Read the short name of the currently checked-out branch, if any.
///
/// Why: the ahead-count and branch-identity checks need to know which branch is
/// actually checked out in the base clone (not merely the repo's configured
/// default branch) — updating a detached HEAD or misidentifying the branch
/// could silently move the wrong ref.
/// What: runs `git -C <base_path> symbolic-ref --short HEAD`; returns `None`
/// on any failure, including detached HEAD (where `symbolic-ref` exits
/// non-zero because `HEAD` does not point at a branch).
/// Test: `hygiene_non_default_branch_is_not_updated` (integration) drives a
/// real non-default checkout through this function; the `None` case is modelled
/// by `decide_update_detached_head_skips`.
/// Count commits on `branch` that are not yet on `origin/<branch>`.
///
/// Why: this is the direct measure of "would an update to origin discard
/// committed work" — the data-loss bug #2177 fixed.
/// What: runs `git -C <base_path> rev-list --count origin/<branch>..<branch>`
/// and parses the count. Returns `None` if the command fails (e.g. no
/// `origin/<branch>` upstream exists) or the output does not parse as a
/// number — both are treated as "unknown" by [`decide_update`], which refuses
/// to proceed on unknown input.
/// Test: `hygiene_ahead_branch_is_not_reset` integration test (via
/// [`run_hygiene_for_base`]).
/// Determine whether the working tree has uncommitted changes to TRACKED files.
///
/// Why: an update must not clobber uncommitted modifications; this is one half
/// of the data-loss guard. It is deliberately NOT the whole guard — see
/// [`colliding_untracked_paths`] for the gitignored-content half (#4961).
/// What: runs `git -C <base_path> status --porcelain`; `Some(true)` if any
/// output is produced (dirty), `Some(false)` if output is empty (clean),
/// `None` if the command itself fails. Note that `--porcelain` without
/// `--ignored` reports nothing for gitignored paths, which is why it cannot be
/// the only guard.
/// Test: `hygiene_dirty_tree_is_not_reset` integration test (via
/// [`run_hygiene_for_base`]).
/// Read `git status --porcelain` for a base clone as a list of entries.
///
/// Why: two callers need the same answer at different resolutions. [`is_dirty`]
/// wants the verdict; the cold-start reuse gate
/// ([`super::inproject_cold_start`]) wants the entries themselves, because its
/// error message names what is dirty. Sharing one reader keeps them from
/// drifting into two different definitions of "dirty" for the same directory.
/// What: runs `git -C <base_path> status --porcelain` and returns the non-empty
/// stdout lines verbatim. `Err` carries the spawn or non-zero-exit failure —
/// callers decide whether that is fail-safe (`is_dirty` → `None` → refuse the
/// update) or fatal (cold start → stop). Note that `--porcelain` without
/// `--ignored` reports nothing for gitignored paths, which is why this cannot
/// be the only data-loss guard; see [`colliding_untracked_paths`].
/// Test: `hygiene_dirty_tree_is_not_reset` (via [`run_hygiene_for_base`]);
/// `dirty_existing_checkout_warns_and_proceeds` (via the cold-start gate).
pub
/// [`porcelain_status`] with the per-command ceiling as a parameter (#7965).
///
/// Why: see [`run_hygiene_for_base_within`] — a test proving the bound fires
/// must not wait out [`GIT_TIMEOUT`].
/// Test: as [`porcelain_status`].
pub
/// Run `git -C <base_path> <args>` and return NUL-separated stdout entries.
///
/// Why: both halves of the collision check parse `-z` git output the same way;
/// a shared helper keeps the parsing (and the "any failure is fatal to the
/// check" contract) in one place.
/// What: spawns git, returns `Err` with a description on spawn failure or a
/// non-zero exit, otherwise splits stdout on NUL and drops empty entries.
/// Test: exercised through `colliding_untracked_paths` by
/// `hygiene_gitignored_file_is_not_clobbered`.
/// Paths the pending update would write that already exist untracked on disk.
///
/// Why: this is the #4961 fix. Git itself will NOT protect these files. A
/// gitignored path is invisible to `git status --porcelain`, and both `git
/// reset --hard` and `git merge --ff-only` silently overwrite an ignored file
/// when the target commit tracks the same path (`--overwrite-ignore` is git's
/// default and `merge` offers no way to turn it off). Reproduced with a
/// gitignored `notes.md` holding real content: destroyed, no warning, no
/// recovery path. Detecting the collision ourselves and refusing is the only
/// way to make the update genuinely non-destructive.
/// What: takes the paths the update would touch (`git diff --name-only HEAD
/// <target>`), subtracts everything currently tracked (`git ls-files`), and
/// returns those of the remainder that exist on disk. A path deleted by the
/// target is tracked at HEAD, so it is never flagged. `symlink_metadata` is
/// used so a broken symlink still counts as present. Any git failure returns
/// `Err`, which the caller treats as "refuse".
/// Test: `hygiene_gitignored_file_is_not_clobbered` (integration).
/// Best-effort write of a recovery ref pointing at the pre-update HEAD.
///
/// Why: defense-in-depth — even when every gate correctly clears an update to
/// proceed, leaving a cheap breadcrumb to the prior HEAD costs nothing and
/// gives a manual recovery path (`git reset --hard
/// refs/trusty-mpm/pre-hygiene/<branch>`) if some unanticipated case still
/// loses committed work. It is structurally incapable of restoring
/// never-committed working-tree content, which is why the gates — not this ref
/// — are the actual guarantee. This must never abort the sweep: any failure
/// here is logged and swallowed, matching the file's existing "every step
/// logged, no step is fatal" pattern.
/// What: resolves the current HEAD sha via `git rev-parse HEAD`, then runs
/// `git update-ref refs/trusty-mpm/pre-hygiene/<branch> <sha>`. Both steps are
/// best-effort; failures are logged via `warn!` and otherwise ignored.
/// Test: `hygiene_recovery_ref_written_before_update` integration test (via
/// [`run_hygiene_for_base`]).
/// Read the default branch for a base clone by inspecting `origin/HEAD`.
///
/// Why: the update step needs the default branch name; reading the symref
/// is more reliable than hardcoding `main` across diverse repositories.
/// What: runs `git -C <base_path> symbolic-ref --short refs/remotes/origin/HEAD`
/// and returns the short branch name (e.g. `main`) on success, or `None` if git
/// fails or there is no `origin/HEAD` symref (the caller falls back to `main`).
/// Test: `get_default_branch_returns_none_for_non_git` (unit).
/// [`get_default_branch`] with the per-command ceiling as a parameter (#7965).
///
/// Test: as [`get_default_branch`].
/// Fetch, then fast-forward the base clone to origin if every gate clears.
///
/// Why: split out of [`run_hygiene_for_base`] so the update step's four gates
/// read as one sequence rather than being buried between the fetch and prune
/// steps. Returns nothing: like every other step in the sweep, a refusal or a
/// git failure is logged and never propagated.
/// What: resolves the default branch, the checked-out branch, its ahead-count
/// and dirty state, and feeds them to [`decide_update`]. On `Update` it then
/// runs the [`colliding_untracked_paths`] check and refuses if any path the
/// update would write already exists untracked on disk (#4961). Only when that
/// too is clear does it write a recovery ref and run `git merge --ff-only
/// origin/<default>` — which is itself non-destructive and fails cleanly
/// rather than overwriting when it cannot fast-forward.
/// Test: `hygiene_gitignored_file_is_not_clobbered`,
/// `hygiene_non_default_branch_is_not_updated`, `hygiene_ahead_branch_is_not_reset`,
/// `hygiene_dirty_tree_is_not_reset`, `hygiene_clean_branch_is_fast_forwarded`.
/// Whether this base clone is due a fetch, given how long ago the last one was.
///
/// Why: see [`FETCH_MIN_INTERVAL`] — the fetch is the expensive step and the
/// sweep runs once per boot, so on a host that restarts the daemon often the same
/// 72 network fetches repeat for nothing.
/// What: `.git/FETCH_HEAD`'s mtime, which git rewrites on every fetch. Due when
/// that file is missing, its mtime is unreadable, or it is older than
/// `min_interval`. Every failure path is DUE — the pre-#7965 behaviour — so an
/// unreadable clock can only cost a redundant fetch, never a stale base clone.
/// Test: `fetch_is_due_without_a_fetch_head`,
/// `fetch_is_due_on_both_sides_of_the_interval`,
/// `fetch_is_due_when_the_mtime_is_in_the_future`,
/// `hygiene_skips_a_recently_fetched_base`.
/// Run hygiene for a single base clone: fetch, gated fast-forward, prune worktrees.
///
/// Why: each step targets a distinct failure mode — fetch syncs the remote object
/// store; the fast-forward (when safe) brings the checkout up to the remote
/// default branch; worktree prune cleans up working-tree entries for worktrees
/// whose paths no longer exist (left behind by decommissioned sessions). All
/// steps are non-fatal: a failure is logged as a warning and the next step
/// still runs, so a transient git error does not prevent the other steps from
/// running. Critically (#2177, #4961), the update step can never discard local
/// work — see [`update_to_origin`].
/// What: delegates to [`run_hygiene_for_base_within`] with the production
/// [`GIT_TIMEOUT`]; it is the only caller that passes that constant.
/// Test: `run_hygiene_skips_missing_dir` (unit — directory absent → early
/// return); `hygiene_opt_out_marker_skips_update`,
/// `hygiene_ahead_branch_is_not_reset`, `hygiene_dirty_tree_is_not_reset`,
/// `hygiene_clean_branch_is_fast_forwarded`, `hygiene_gitignored_file_is_not_clobbered`,
/// `hygiene_non_default_branch_is_not_updated`,
/// `hygiene_recovery_ref_written_before_update` (integration, real temp git repos).
/// [`run_hygiene_for_base`] with the per-command ceiling as a parameter (#7965).
///
/// Why a parameter: the regression test for the bound drives a `git` that never
/// answers, and at [`GIT_TIMEOUT`] one base would cost minutes of test time.
/// What: returns early when `.git` is absent, or when the
/// [`HYGIENE_OPT_OUT_MARKER`] file is present (a per-repo opt-out; every step is
/// skipped so an opted-out checkout is left entirely alone). Otherwise: (1) `git
/// fetch origin`, unless [`fetch_is_due`] says a recent enough one already
/// happened; (2) [`update_to_origin`]; (3) `git worktree prune`. Every git spawn
/// goes through [`git_bounded`], so a child that never answers is killed at
/// `budget` rather than waited on.
/// Test: `a_wedged_hygiene_fetch_neither_hangs_the_sweep_nor_delays_health`
/// (the bound), `hygiene_skips_a_recently_fetched_base` (the fetch gate).
pub
/// Run hygiene for every base clone under `repos_root`.
///
/// Why: at daemon startup all managed base clones should be freshened in one
/// pass so sessions spawned shortly after startup always see a current default
/// branch. Walking the two-level `<owner>/<repo>` layout avoids hardcoding
/// specific project paths.
/// What: enumerates `<repos_root>/<owner>/` directories, then for each
/// `<owner>/<repo>` subdirectory calls [`run_hygiene_for_base`]. Non-git
/// directories are silently skipped. All errors are logged as warnings; no
/// single repo failure prevents the rest from being processed.
/// Test: `run_hygiene_for_all_bases_skips_missing_root` (unit).
/// [`run_hygiene_for_all_bases`] with the pass budget as a parameter (#7965).
///
/// Why a parameter: a test must be able to prove the budget STOPS the walk
/// without waiting out [`PASS_BUDGET`], and resolving it internally would make
/// that test three minutes long.
/// What: identical to the public entry point except the deadline. Bases reached
/// after it are skipped; nothing is removed or modified for them, so an abandoned
/// tail is only staleness. Returns how many were skipped — the same number the
/// `warn!` line carries, so a test can assert the abandonment itself rather than
/// infer it from wall-clock time.
/// Test: `hygiene_pass_budget_stops_the_sweep`.
pub