use std::path::{Path, PathBuf};
use serde::Serialize;
use trusty_mpm::core::architect_session::check_session_binding;
use trusty_mpm::core::config::MpmConfig;
use trusty_mpm::core::project_config::PROJECT_CONFIG_FILE;
use trusty_mpm::core::session_profile::{self, SUPERVISOR_PROFILE_ID};
use trusty_mpm::core::twin_arming;
use super::launch::PaneState;
use super::session_name::{self, SessionNames};
use super::{Probe, user_config_path};
use crate::commands::pm_guard_architect_reason::session_binding;
use crate::commands::pm_guard_trust_anchor::{ClaudeLookup, HookEnv};
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Check {
pub(crate) name: &'static str,
pub(crate) ok: bool,
pub(crate) detail: String,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct StatusReport {
pub(crate) dir: PathBuf,
pub(crate) session: String,
pub(crate) checks: Vec<Check>,
pub(crate) complete: bool,
pub(crate) binding: Check,
pub(crate) this_session: Option<Check>,
}
impl StatusReport {
pub(crate) fn render(&self) -> String {
let mut out = format!("Architect: {}\n", self.dir.display());
for c in &self.checks {
let mark = if c.ok { "ok " } else { "MISSING" };
out.push_str(&format!(" {mark} {:<12} {}\n", c.name, c.detail));
}
for c in std::iter::once(&self.binding).chain(&self.this_session) {
let mark = if c.ok { "ok " } else { "UNBOUND" };
out.push_str(&format!(" {mark} {:<12} {}\n", c.name, c.detail));
}
if self.complete {
out.push_str("complete\n");
} else {
out.push_str(&format!(
"incomplete: run `tm fleet init --dir {}` to finish the setup\n",
self.dir.display()
));
}
out
}
}
pub(crate) fn status(
dir: &Path,
home: &Path,
probe: Probe,
session: Option<&SessionNames>,
) -> StatusReport {
let dir = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
let names = session.map_or_else(|| session_name::recorded_names(home), |n| Ok(n.clone()));
let checks = vec![
allowlist_check(&dir, home),
profile_check(&dir),
session_check(&dir, probe, &names),
stamp_check(probe, &names),
];
let complete = checks.iter().all(|c| c.ok);
StatusReport {
binding: binding_check(&dir, home, probe, &names),
session: names
.as_ref()
.map(|n| n.architect().to_owned())
.unwrap_or_default(),
dir,
checks,
complete,
this_session: None,
}
}
pub(crate) fn binding_check(
dir: &Path,
home: &Path,
probe: Probe,
names: &Result<SessionNames, String>,
) -> Check {
let result = match names {
Err(err) => Err(err.clone()),
Ok(names) => bound(dir, home, probe, names.architect()),
};
let (ok, detail) = match result {
Ok(detail) => (true, detail),
Err(why) => (false, format!("not bound: {why}")),
};
Check {
name: "binding",
ok,
detail,
}
}
pub(super) fn bound(
dir: &Path,
home: &Path,
probe: Probe,
session: &str,
) -> Result<String, String> {
match (probe.pane)(session) {
PaneState::Live(_) => {}
PaneState::Dead(_) => return Err(format!("the pane of tmux session {session} is dead")),
PaneState::Absent => return Err(format!("tmux session {session} is not running")),
PaneState::Unknown(err) => {
return Err(format!("cannot read tmux session {session}: {err}"));
}
}
let pid = (probe.claude)(session).ok_or_else(|| {
format!("no `claude` process was found in tmux session {session}, or tmux could not say")
})?;
check_session_binding(&home.join(".trusty-mpm"), dir, session, pid)
.map_err(|why| why.to_string())?;
Ok(format!(
"the `claude` in tmux session {session} is the Architect `tm fleet init` launched"
))
}
pub(crate) fn this_session_check(
dir: &Path,
mut env: HookEnv,
config: impl FnOnce() -> MpmConfig,
) -> Check {
if session_profile::hook_project_dir(env.project_dir.clone()).is_none() {
env.project_dir = Some(dir.as_os_str().to_owned());
}
let (ok, detail) = match session_binding(&env, config) {
Ok(()) => (true, "this session is the Architect".to_owned()),
Err(why) => (false, format!("this session is not the Architect: {why}")),
};
Check {
name: "this_session",
ok,
detail,
}
}
pub(crate) fn this_session_env() -> HookEnv {
HookEnv {
claude: ClaudeLookup::new(|| twin_arming::nearest_claude_for_status(std::process::id())),
..HookEnv::ambient()
}
}
fn allowlist_check(dir: &Path, home: &Path) -> Check {
let path = user_config_path(home);
let (ok, detail) = match std::fs::read_to_string(&path) {
Err(_) => (false, format!("{} is absent or unreadable", path.display())),
Ok(raw) => match super::config::parse_user_config(&raw, &path) {
Err(err) => (false, format!("{err:#}")),
Ok((_, cfg)) if session_profile::is_allow_listed(dir, &cfg.supervisor) => (
true,
format!("listed in `[supervisor] projects` of {}", path.display()),
),
Ok(_) => (
false,
format!("not in `[supervisor] projects` of {}", path.display()),
),
},
};
Check {
name: "allowlist",
ok,
detail,
}
}
fn profile_check(dir: &Path) -> Check {
let ok = session_profile::requested(dir).is_supervisor();
let file = dir.join(PROJECT_CONFIG_FILE);
let detail = if ok {
format!(
"`profile = \"{SUPERVISOR_PROFILE_ID}\"` in {}",
file.display()
)
} else {
format!(
"{} does not request `profile = \"{SUPERVISOR_PROFILE_ID}\"`",
file.display()
)
};
Check {
name: "profile",
ok,
detail,
}
}
fn session_check(dir: &Path, probe: Probe, names: &Result<SessionNames, String>) -> Check {
let session = match names {
Ok(names) => names.architect(),
Err(err) => {
return Check {
name: "session",
ok: false,
detail: err.clone(),
};
}
};
let (ok, detail) = match (probe.pane)(session).dir() {
None => (false, format!("tmux session {session} is not running")),
Some(running) if running == dir => (true, format!("tmux session {session} is running")),
Some(running) => (
false,
format!(
"tmux session {session} runs in {}, not here",
running.display()
),
),
};
Check {
name: "session",
ok,
detail,
}
}
fn stamp_check(probe: Probe, names: &Result<SessionNames, String>) -> Check {
let stamp = match names {
Ok(names) => (probe.stamp)(names.architect()),
Err(err) => {
return Check {
name: "launch_stamp",
ok: false,
detail: err.clone(),
};
}
};
let (ok, detail) = match stamp {
Some(v) if v == SUPERVISOR_PROFILE_ID => (true, format!("launched as `{v}`")),
Some(v) => (
false,
format!("launched as `{v}`, not `{SUPERVISOR_PROFILE_ID}`"),
),
None => (
false,
"no launch stamp: the session was not started by `tm fleet init`".to_owned(),
),
};
Check {
name: "launch_stamp",
ok,
detail,
}
}