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
//! `tm doctor` full manifest-completeness check (issue #2158).
//!
//! Why: the pre-existing `agents`/`skills`/`output_style` probes each check
//! ONE facet in isolation (non-empty directory, resolvable style id) but
//! never diff the deployed payload against the canonical bundled roster as a
//! whole — a managed workspace could pass all three individually while still
//! missing several agents, carrying no ownership manifest, or missing the
//! `hooks` key, none of which the narrower probes catch. This check closes
//! that gap by delegating to [`crate::core::deploy_validate::validate_workspace`],
//! the same diff engine `tm validate` and the daemon's spawn/resume
//! auto-repair gate use, so all three surfaces report identical
//! completeness verdicts. Split into its own file (mirroring the existing
//! `doctor_output_style.rs` / `doctor_fs_checks.rs` split) to keep `doctor.rs`
//! under the 500-SLOC production cap.
//! What: [`check_deployment_completeness`] takes the already-resolved
//! [`FrameworkPaths`] `run_doctor` built for `project_dir` (home-relative when
//! no project is scoped) and folds every [`DeploymentGap`] into one
//! [`DoctorCheck`] — `Ok` when complete, `Fail` naming every gap otherwise.
//! Test: `deployment_check_ok_when_complete`, `deployment_check_fail_lists_gaps`.
use crate::core::deploy_validate::validate_workspace;
use crate::core::doctor::{CheckStatus, DoctorCheck};
use crate::core::paths::FrameworkPaths;
/// Probe deployment completeness by diffing against the canonical bundled roster.
///
/// Why: see the module doc — this is the single check that catches a
/// partially-provisioned `.claude/` payload as a whole, not just one facet
/// of it.
/// What: `Ok` when [`validate_workspace`] finds no gaps; `Fail` naming every
/// gap (semicolon-joined, capped implicitly by the small roster size so the
/// message stays readable) plus the actionable hint to run `tm validate
/// --repair` otherwise.
/// Test: `deployment_check_ok_when_complete`, `deployment_check_fail_lists_gaps`.
pub(super) fn check_deployment_completeness(paths: &FrameworkPaths) -> DoctorCheck {
let report = validate_workspace(paths);
if report.is_complete() {
return DoctorCheck::new(
"deployment",
CheckStatus::Ok,
"deployed .claude/{agents,skills} and settings.json match the canonical roster",
);
}
let detail: Vec<String> = report.gaps.iter().map(|g| g.describe()).collect();
DoctorCheck::new(
"deployment",
CheckStatus::Fail,
format!(
"{} deployment gap(s) found: {} — run `tm validate --repair` to fix",
detail.len(),
detail.join("; ")
),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deployment_check_ok_when_complete() {
// An entirely empty temp dir has no canonical source agents/skills to
// diff against (both `agent_source_dir()`/`skill_source_dir()` and the
// deploy targets are empty), so there is nothing to report missing —
// the settings.json probes are the only ones that can still fail, so
// seed a minimal complete settings.json plus manifests.
let tmp = tempfile::tempdir().unwrap();
let mut paths = FrameworkPaths::under(tmp.path());
paths.trusty_mpm_root = None;
let agents_dir = paths.agent_deploy_dir();
std::fs::create_dir_all(&agents_dir).unwrap();
crate::core::agent_manifest::AgentManifest::default()
.save(&agents_dir)
.unwrap();
// #6586: the skill ownership manifest the probe reads lives beside the
// bundled roster, in the managed config-dir tier — not the project's.
let skills_dir = paths.skill_deploy_dir();
std::fs::create_dir_all(&skills_dir).unwrap();
crate::core::skill_manifest::SkillManifest::default()
.save(&skills_dir)
.unwrap();
let claude_dir = paths.claude_home_dir().join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
let style_dir = claude_dir.join("output-styles");
std::fs::create_dir_all(&style_dir).unwrap();
let default_style = crate::core::bundle::OUTPUT_STYLES[0];
std::fs::write(
style_dir.join(default_style.file_name),
default_style.content(crate::core::content_source::test_support::rc()),
)
.unwrap();
std::fs::write(
claude_dir.join("settings.json"),
r#"{"outputStyle": "trusty-mpm", "hooks": {"SessionStart": []}}"#,
)
.unwrap();
let check = check_deployment_completeness(&paths);
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn deployment_check_fail_lists_gaps() {
// A totally unprovisioned workspace (no manifests, no settings.json)
// must Fail and name the gaps in the message.
let tmp = tempfile::tempdir().unwrap();
let mut paths = FrameworkPaths::under(tmp.path());
paths.trusty_mpm_root = None;
let check = check_deployment_completeness(&paths);
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("agent ownership manifest"));
assert!(check.message.contains("tm validate --repair"));
}
}