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
//! Wire the #4448 quarantine into a workspace, once (issue #4448).
//!
//! Why: two live paths need this sweep — `prepare_session_inner` on every
//! session launch and `sync_session_assets` on every `tm sessions sync-assets`.
//! Both must aim it at the SAME directory with the SAME roster and the SAME
//! backup root. Two hand-rolled call sites would drift, and the drift is
//! asymmetric harm: one path repairs a workspace the other re-breaks, or one
//! aims somewhere the other never would.
//!
//! What: [`quarantine_workspace_shadows`] resolves the roster, the workspace
//! tier, and the backup root, then delegates to
//! [`trusty_agents_common::agents::quarantine::quarantine_shadowing_agents`].
//!
//! 🔴 THE TARGET IS ALWAYS `project_dir`'s OWN `.claude/agents`, spelled out
//! rather than taken from `fw.claude_agents_dir()`. Those are the same directory
//! for a managed `fw`, but `fw` is a home-tier `FrameworkPaths::default()` on
//! the non-git `tm session start` and TUI `/connect` paths — where
//! `fw.claude_agents_dir()` is the operator's `~/.claude/agents`. Sweeping THAT
//! would move the roster out of a Claude Code install with nothing to do with
//! trusty-mpm, and it is strictly more dangerous there than retraction is:
//! retraction can only delete ledger-tracked files (of which the operator's home
//! tier has none), while this sweep moves UNTRACKED ones, which is exactly what
//! that directory is full of. Binding the target to the workspace path here,
//! once, makes that structural.
//! Test: `prepare_session_never_quarantines_the_operator_home_agents_tier`.
//!
//! Test: `crates/trusty-mpm/src/core/session_launch/tests.rs`.
use ;
use ;
use QuarantineReport;
use cratebundled_roster;
use crateFrameworkPaths;
/// Where a workspace's quarantined agents are preserved.
///
/// Deliberately OUTSIDE `.claude/agents/` — a backup nested inside the tier it
/// was taken from is one recursive loader away from being an agent again.
/// The workspace agent tier this sweep may touch — never `fw.claude_agents_dir()`.
/// Quarantine every shadowing leftover in `project_dir`'s own agent tier.
///
/// Why: `retract_framework_agents` (#4409) removes only what its ownership
/// ledger names. A copy written before that ledger existed is invisible to it,
/// outranks the canonical user tier, and is never refreshed again — #4408 made
/// permanent. This is the half that reaches those.
/// What: resolves the shared [`bundled_roster`] (the SAME one `tm doctor`'s
/// `asset_tier` probe reports from), stamps a UTC run id so a second sweep
/// never overwrites the first run's backups, and delegates. Returns whatever the
/// sweep reports, including a partially failed run — the caller logs it.
/// Callers must run this AFTER retraction: the two target the same directory,
/// and retraction is the cheaper, ledger-proven half.
/// Test: `prepare_session_quarantines_a_shadowing_workspace_agent`,
/// `prepare_session_never_quarantines_the_operator_home_agents_tier`,
/// `sync_assets_quarantines_a_shadowing_workspace_agent`.
/// Why [`quarantine_workspace_shadows`] moved nothing.
/// Render one sweep's outcome as the line an operator needs, or `None` when
/// nothing happened.
///
/// Why: a sweep that moved a file changed the working tree under an operator
/// who did not ask for it; that must be visible, with the receipt path, not
/// buried at debug level. A clean sweep must say nothing at all — this runs on
/// every launch.
/// What: `None` when nothing moved and nothing failed. Otherwise a summary
/// naming the counts and the receipt (or why it could not be written).
/// Test: `sync_assets_quarantines_a_shadowing_workspace_agent`,
/// `sync_assets_is_silent_when_nothing_shadows`,
/// `prepare_session_leaves_a_clean_project_clean`.