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
//! Every directory a bundled output style is deployed into (issue #7423).
//!
//! Why: output styles, like skills before #4605, have more than one
//! destination, and the probe and the repair each knew about only one of them.
//! `daemon::doctor_output_style::check_output_style_staleness` scanned
//! `<home>/.claude/output-styles/` and
//! `core::doctor_repair::repair_output_style` wrote `home.join(".claude")` —
//! but a tm-launched session reads `$CLAUDE_CONFIG_DIR` as its `user` tier
//! (`core::model_inject`'s `SETTING_SOURCES_FLAG_RELOCATED`), so the copy that
//! actually governs a managed session lives under the managed config dir.
//! Measured on 2026-09-11 after `tm doctor --fix --yes` on trusty-mpm 1.5.29:
//! `~/.claude/output-styles/trusty-mpm.md` was redeployed while
//! `$CLAUDE_CONFIG_DIR/output-styles/trusty-mpm.md` sat at the old 12,012
//! bytes, reported clean, and kept governing every managed session.
//!
//! What: [`output_style_tiers`] enumerates those directories, labelled and
//! deduplicated. Pure path arithmetic apart from one `is_dir` probe on the
//! managed tier — see the function doc for why that probe is there.
//!
//! Test: the `tests` module below, plus
//! `daemon::doctor_output_style`'s `staleness_warns_on_managed_config_drift`
//! and `core::doctor_repair`'s `output_style_repair_rewrites_the_managed_tier`.
use std::path::{Path, PathBuf};
/// One directory bundled output styles are deployed into.
///
/// Why: every finding has to name WHICH copy drifted — "trusty-mpm.md drifted"
/// is not actionable when two tiers hold that file and only one of them is what
/// a managed session reads. The label travels with the path for that reason.
/// What: a short stable label plus the `CLAUDE_CONFIG_DIR`-shaped directory
/// that `output-styles/` sits under — i.e. what
/// [`crate::core::output_style_deployer::deploy_output_styles`] takes.
/// Test: `tiers_cover_home_and_managed_config`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputStyleTier {
/// Short label for operator-facing output (`operator home`, `managed
/// config`) — the same vocabulary
/// [`crate::core::skill_deploy_tiers::SkillDeployTier`] uses.
pub label: &'static str,
/// The directory `output-styles/` lives under.
pub claude_dir: PathBuf,
}
impl OutputStyleTier {
/// The `output-styles/` directory itself.
///
/// Why: the drift scan takes the styles directory and the deploy takes its
/// parent; spelling the join once keeps the two from disagreeing.
/// What: `<claude_dir>/output-styles`.
/// Test: `tiers_cover_home_and_managed_config`.
pub fn styles_dir(&self) -> PathBuf {
self.claude_dir.join("output-styles")
}
}
/// Enumerate every output-style deploy target, highest-reach first.
///
/// Why: see the module doc — one enumeration is what keeps the doctor probe and
/// `tm doctor --fix` acting on the same set of files.
/// What: `<home>/.claude` always, then `managed_config` when it is supplied AND
/// already exists on disk. The existence probe is deliberate: a redeploy
/// materialising a managed `CLAUDE_CONFIG_DIR` that nothing has provisioned
/// would have `tm doctor --fix` create config state rather than repair it, and
/// an absent directory is `standalone::global_config`'s to create. A managed
/// path equal to `<home>/.claude` yields one tier, not two.
/// Test: `tiers_cover_home_and_managed_config`,
/// `an_absent_managed_dir_contributes_no_tier`,
/// `a_managed_dir_equal_to_home_is_not_listed_twice`.
pub fn output_style_tiers(home: &Path, managed_config: Option<&Path>) -> Vec<OutputStyleTier> {
let mut tiers = vec![OutputStyleTier {
label: "operator home",
claude_dir: home.join(".claude"),
}];
if let Some(managed) = managed_config.filter(|dir| dir.is_dir())
&& !tiers.iter().any(|t| t.claude_dir == managed)
{
tiers.push(OutputStyleTier {
label: "managed config",
claude_dir: managed.to_path_buf(),
});
}
tiers
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn tiers_cover_home_and_managed_config() {
let home = TempDir::new().unwrap();
let managed = TempDir::new().unwrap();
let tiers = output_style_tiers(home.path(), Some(managed.path()));
assert_eq!(tiers.len(), 2, "tiers: {tiers:?}");
assert_eq!(tiers[0].label, "operator home");
assert_eq!(
tiers[0].styles_dir(),
home.path().join(".claude").join("output-styles")
);
assert_eq!(tiers[1].label, "managed config");
assert_eq!(tiers[1].styles_dir(), managed.path().join("output-styles"));
}
#[test]
fn an_absent_managed_dir_contributes_no_tier() {
// `tm doctor --fix` repairs what exists; it does not provision a
// managed CLAUDE_CONFIG_DIR nothing has bootstrapped yet.
let home = TempDir::new().unwrap();
let tiers = output_style_tiers(home.path(), Some(&home.path().join("never-made")));
assert_eq!(tiers.len(), 1, "tiers: {tiers:?}");
assert_eq!(tiers[0].label, "operator home");
}
#[test]
fn a_managed_dir_equal_to_home_is_not_listed_twice() {
// Listing it twice would double every finding reported against it.
let home = TempDir::new().unwrap();
let claude = home.path().join(".claude");
std::fs::create_dir_all(&claude).unwrap();
let tiers = output_style_tiers(home.path(), Some(&claude));
assert_eq!(tiers.len(), 1, "tiers: {tiers:?}");
}
}