omx(team): auto-checkpoint worker-4 [unknown]

This commit is contained in:
bellman
2026-05-14 17:46:07 +09:00
parent 9ec4d8398e
commit 79d3b809f9

View File

@@ -1972,8 +1972,16 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
let (project_root, git_branch) =
parse_git_status_metadata(project_context.git_status.as_deref());
let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref());
let branch_freshness = BranchFreshness::from_git_status(project_context.git_status.as_deref());
let empty_config = runtime::RuntimeConfig::empty();
let sandbox_config = config.as_ref().ok().unwrap_or(&empty_config);
let boot_preflight = build_boot_preflight_snapshot(
&cwd,
project_root.as_deref(),
project_context.git_status.as_deref(),
config.as_ref().ok(),
config.as_ref().err().map(ToString::to_string).as_deref(),
);
let context = StatusContext {
cwd: cwd.clone(),
session_path: None,
@@ -1986,7 +1994,9 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
project_root,
git_branch,
git_summary,
branch_freshness,
session_lifecycle: classify_session_lifecycle_for(&cwd),
boot_preflight,
sandbox_status: resolve_sandbox_status(sandbox_config.sandbox(), &cwd),
// Doctor path has its own config check; StatusContext here is only
// fed into health renderers that don't read config_load_error.
@@ -1998,6 +2008,7 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
check_config_health(&config_loader, config.as_ref()),
check_install_source_health(),
check_workspace_health(&context),
check_boot_preflight_health(&context),
check_sandbox_health(&context.sandbox_status),
check_system_health(&cwd, config.as_ref().ok()),
],
@@ -2393,6 +2404,73 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
]))
}
fn check_boot_preflight_health(context: &StatusContext) -> DiagnosticCheck {
let preflight = &context.boot_preflight;
let missing_binaries = preflight
.required_binaries
.iter()
.filter(|binary| !binary.available)
.map(|binary| binary.name)
.collect::<Vec<_>>();
let socket_details = preflight
.control_sockets
.iter()
.map(|socket| {
format!(
"Control socket {} configured={} exists={} path={}",
socket.name,
socket.configured,
socket.exists,
socket.path.as_deref().unwrap_or("<none>")
)
})
.collect::<Vec<_>>();
let mut details = vec![
format!("Repo exists {}", preflight.repo_exists),
format!("Worktree exists {}", preflight.worktree_exists),
format!("Git dir exists {}", preflight.git_dir_exists),
format!("Branch behind {}", preflight.branch_freshness.behind),
format!("Trust allowlist {:?}", preflight.trust_gate_allowed),
format!("Trusted roots {}", preflight.trusted_roots_count),
format!(
"MCP eligible {} · servers {}",
preflight.mcp_startup_eligible, preflight.mcp_servers_configured
),
format!(
"Plugin eligible {} · configured {}",
preflight.plugin_startup_eligible, preflight.plugins_configured
),
format!(
"Last failed boot {}",
preflight
.last_failed_boot_reason
.as_deref()
.unwrap_or("<none>")
),
];
details.extend(preflight.required_binaries.iter().map(|binary| {
format!(
"Required binary {} available={}",
binary.name, binary.available
)
}));
details.extend(socket_details);
DiagnosticCheck::new(
"Boot preflight",
if preflight.repo_exists && preflight.worktree_exists && missing_binaries.is_empty() {
DiagnosticLevel::Ok
} else {
DiagnosticLevel::Warn
},
preflight.summary(),
)
.with_details(details)
.with_data(Map::from_iter([(
"boot_preflight".to_string(),
preflight.json_value(),
)]))
}
fn check_sandbox_health(status: &runtime::SandboxStatus) -> DiagnosticCheck {
let degraded = status.enabled && !status.active;
let mut details = vec![
@@ -2841,7 +2919,9 @@ struct StatusContext {
project_root: Option<PathBuf>,
git_branch: Option<String>,
git_summary: GitWorkspaceSummary,
branch_freshness: BranchFreshness,
session_lifecycle: SessionLifecycleSummary,
boot_preflight: BootPreflightSnapshot,
sandbox_status: runtime::SandboxStatus,
/// #143: when `.claw.json` (or another loaded config file) fails to parse,
/// we capture the parse error here and still populate every field that
@@ -2852,6 +2932,162 @@ struct StatusContext {
config_load_error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct BranchFreshness {
upstream: Option<String>,
ahead: u32,
behind: u32,
fresh: Option<bool>,
}
impl BranchFreshness {
fn from_git_status(status: Option<&str>) -> Self {
let first_line = status
.and_then(|status| status.lines().next())
.unwrap_or_default();
let upstream = first_line
.split_once("...")
.and_then(|(_, rest)| rest.split([' ', '[']).next())
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let mut ahead = 0;
let mut behind = 0;
if let Some((_, bracketed)) = first_line.split_once('[') {
let bracketed = bracketed.trim_end_matches(']');
for part in bracketed.split(',').map(str::trim) {
if let Some(value) = part.strip_prefix("ahead ") {
ahead = value.parse().unwrap_or(0);
} else if let Some(value) = part.strip_prefix("behind ") {
behind = value.parse().unwrap_or(0);
}
}
}
let fresh = upstream.as_ref().map(|_| behind == 0);
Self {
upstream,
ahead,
behind,
fresh,
}
}
fn json_value(&self) -> serde_json::Value {
json!({
"upstream": self.upstream,
"ahead": self.ahead,
"behind": self.behind,
"fresh": self.fresh,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct BinaryPreflight {
name: &'static str,
available: bool,
}
impl BinaryPreflight {
fn json_value(&self) -> serde_json::Value {
json!({
"name": self.name,
"available": self.available,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ControlSocketPreflight {
name: &'static str,
configured: bool,
exists: bool,
path: Option<String>,
}
impl ControlSocketPreflight {
fn json_value(&self) -> serde_json::Value {
json!({
"name": self.name,
"configured": self.configured,
"exists": self.exists,
"path": self.path,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct BootPreflightSnapshot {
repo_exists: bool,
worktree_exists: bool,
git_dir_exists: bool,
branch_freshness: BranchFreshness,
trust_gate_allowed: Option<bool>,
trusted_roots_count: usize,
required_binaries: Vec<BinaryPreflight>,
control_sockets: Vec<ControlSocketPreflight>,
mcp_startup_eligible: bool,
mcp_servers_configured: usize,
plugin_startup_eligible: bool,
plugins_configured: usize,
last_failed_boot_reason: Option<String>,
}
impl BootPreflightSnapshot {
fn json_value(&self) -> serde_json::Value {
json!({
"repo": {
"exists": self.repo_exists,
"worktree_exists": self.worktree_exists,
"git_dir_exists": self.git_dir_exists,
},
"branch_freshness": self.branch_freshness.json_value(),
"trust_gate": {
"allowlisted": self.trust_gate_allowed,
"trusted_roots_count": self.trusted_roots_count,
},
"required_binaries": self.required_binaries.iter().map(BinaryPreflight::json_value).collect::<Vec<_>>(),
"control_sockets": self.control_sockets.iter().map(ControlSocketPreflight::json_value).collect::<Vec<_>>(),
"mcp_startup": {
"eligible": self.mcp_startup_eligible,
"servers_configured": self.mcp_servers_configured,
},
"plugin_startup": {
"eligible": self.plugin_startup_eligible,
"plugins_configured": self.plugins_configured,
},
"last_failed_boot_reason": self.last_failed_boot_reason,
})
}
fn summary(&self) -> String {
let trust = self
.trust_gate_allowed
.map(|value| {
if value {
"allowlisted"
} else {
"not allowlisted"
}
})
.unwrap_or("unknown");
let freshness = self
.branch_freshness
.fresh
.map(|fresh| if fresh { "fresh" } else { "behind" })
.unwrap_or("no upstream");
format!(
"repo={} worktree={} branch={} trust={} mcp={} plugins={} last_failed={}",
self.repo_exists,
self.worktree_exists,
freshness,
trust,
self.mcp_startup_eligible,
self.plugin_startup_eligible,
self.last_failed_boot_reason.as_deref().unwrap_or("none")
)
}
}
#[derive(Debug, Clone, Copy)]
struct StatusUsage {
message_count: usize,
@@ -3294,6 +3530,118 @@ fn parse_git_workspace_summary(status: Option<&str>) -> GitWorkspaceSummary {
summary
}
fn build_boot_preflight_snapshot(
cwd: &Path,
project_root: Option<&Path>,
git_status: Option<&str>,
runtime_config: Option<&runtime::RuntimeConfig>,
config_load_error: Option<&str>,
) -> BootPreflightSnapshot {
let branch_freshness = BranchFreshness::from_git_status(git_status);
let worktree_exists = run_git_bool(cwd, &["rev-parse", "--is-inside-work-tree"]);
let git_dir_exists = run_git_capture_in(cwd, &["rev-parse", "--git-dir"])
.map(|path| {
let path = PathBuf::from(path.trim());
if path.is_absolute() {
path
} else {
cwd.join(path)
}
})
.is_some_and(|path| path.exists());
let trusted_roots = runtime_config
.map(runtime::RuntimeConfig::trusted_roots)
.unwrap_or(&[]);
let trust_gate_allowed = runtime_config.map(|_| {
trusted_roots
.iter()
.any(|root| path_matches_trusted_root_local(cwd, Path::new(root)))
});
let plugin_configured = runtime_config
.map(|config| config.plugins().enabled_plugins().len())
.unwrap_or_default();
let mcp_configured = runtime_config
.map(|config| config.mcp().servers().len())
.unwrap_or_default();
let config_ok = config_load_error.is_none();
BootPreflightSnapshot {
repo_exists: project_root.is_some_and(Path::exists),
worktree_exists,
git_dir_exists,
branch_freshness,
trust_gate_allowed,
trusted_roots_count: trusted_roots.len(),
required_binaries: vec![
BinaryPreflight {
name: "claw",
available: env::current_exe().is_ok_and(|path| path.exists()),
},
BinaryPreflight {
name: "git",
available: command_available("git"),
},
BinaryPreflight {
name: "tmux",
available: command_available("tmux"),
},
],
control_sockets: vec![tmux_control_socket_preflight()],
mcp_startup_eligible: config_ok,
mcp_servers_configured: mcp_configured,
plugin_startup_eligible: config_ok,
plugins_configured: plugin_configured,
last_failed_boot_reason: last_failed_boot_reason(cwd),
}
}
fn run_git_bool(cwd: &Path, args: &[&str]) -> bool {
Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.is_ok_and(|output| output.status.success())
}
fn command_available(command: &str) -> bool {
Command::new(command)
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
}
fn tmux_control_socket_preflight() -> ControlSocketPreflight {
let path = env::var("TMUX")
.ok()
.and_then(|value| value.split(',').next().map(str::to_string))
.filter(|value| !value.is_empty());
let exists = path.as_ref().is_some_and(|path| Path::new(path).exists());
ControlSocketPreflight {
name: "tmux",
configured: path.is_some(),
exists,
path,
}
}
fn last_failed_boot_reason(cwd: &Path) -> Option<String> {
env::var("CLAW_LAST_FAILED_BOOT_REASON")
.ok()
.filter(|value| !value.trim().is_empty())
.or_else(|| {
fs::read_to_string(cwd.join(".claw").join("last-failed-boot.txt"))
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
})
}
fn path_matches_trusted_root_local(cwd: &Path, trusted_root: &Path) -> bool {
let cwd = fs::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf());
let trusted_root =
fs::canonicalize(trusted_root).unwrap_or_else(|_| trusted_root.to_path_buf());
cwd == trusted_root || cwd.starts_with(trusted_root)
}
fn resolve_git_branch_for(cwd: &Path) -> Option<String> {
let branch = run_git_capture_in(cwd, &["branch", "--show-current"])?;
let branch = branch.trim();
@@ -5810,6 +6158,8 @@ fn status_json_value(
path.file_stem().map(|n| n.to_string_lossy().into_owned())
}),
"session_lifecycle": context.session_lifecycle.json_value(),
"branch_freshness": context.branch_freshness.json_value(),
"boot_preflight": context.boot_preflight.json_value(),
"loaded_config_files": context.loaded_config_files,
"discovered_config_files": context.discovered_config_files,
"memory_file_count": context.memory_file_count,
@@ -5843,7 +6193,8 @@ fn status_context(
// so that one malformed `mcpServers.*` entry doesn't take down the whole
// health surface (workspace, git, model, permission, sandbox can still be
// reported independently).
let (loaded_config_files, sandbox_status, config_load_error) = match loader.load() {
let runtime_config = loader.load();
let (loaded_config_files, sandbox_status, config_load_error) = match runtime_config.as_ref() {
Ok(runtime_config) => (
runtime_config.loaded_entries().len(),
resolve_sandbox_status(runtime_config.sandbox(), &cwd),
@@ -5864,6 +6215,14 @@ fn status_context(
let (project_root, git_branch) =
parse_git_status_metadata(project_context.git_status.as_deref());
let git_summary = parse_git_workspace_summary(project_context.git_status.as_deref());
let branch_freshness = BranchFreshness::from_git_status(project_context.git_status.as_deref());
let boot_preflight = build_boot_preflight_snapshot(
&cwd,
project_root.as_deref(),
project_context.git_status.as_deref(),
runtime_config.as_ref().ok(),
config_load_error.as_deref(),
);
Ok(StatusContext {
cwd: cwd.clone(),
session_path: session_path.map(Path::to_path_buf),
@@ -5873,7 +6232,9 @@ fn status_context(
project_root,
git_branch,
git_summary,
branch_freshness,
session_lifecycle: classify_session_lifecycle_for(&cwd),
boot_preflight,
sandbox_status,
config_load_error,
})
@@ -5948,6 +6309,8 @@ fn format_status_report(
Untracked {}
Session {}
Lifecycle {}
Branch fresh {}
Boot preflight {}
Config files loaded {}/{}
Memory files {}
Suggested flow /status → /diff → /commit",
@@ -5967,6 +6330,12 @@ fn format_status_report(
|path| path.display().to_string()
),
context.session_lifecycle.signal(),
context
.branch_freshness
.fresh
.map(|fresh| if fresh { "yes" } else { "behind" })
.unwrap_or("no upstream"),
context.boot_preflight.summary(),
context.loaded_config_files,
context.discovered_config_files,
context.memory_file_count,
@@ -12119,6 +12488,33 @@ mod tests {
assert!(report.contains("Switch models with /model <name>"));
}
fn test_branch_freshness() -> super::BranchFreshness {
super::BranchFreshness {
upstream: Some("origin/main".to_string()),
ahead: 0,
behind: 0,
fresh: Some(true),
}
}
fn test_boot_preflight() -> super::BootPreflightSnapshot {
super::BootPreflightSnapshot {
repo_exists: true,
worktree_exists: true,
git_dir_exists: true,
branch_freshness: test_branch_freshness(),
trust_gate_allowed: Some(false),
trusted_roots_count: 0,
required_binaries: Vec::new(),
control_sockets: Vec::new(),
mcp_startup_eligible: true,
mcp_servers_configured: 0,
plugin_startup_eligible: true,
plugins_configured: 0,
last_failed_boot_reason: None,
}
}
#[test]
fn model_switch_report_preserves_context_summary() {
let report = format_model_switch_report("claude-sonnet", "claude-opus", 9);
@@ -12165,6 +12561,7 @@ mod tests {
untracked_files: 1,
conflicted_files: 0,
},
branch_freshness: test_branch_freshness(),
session_lifecycle: SessionLifecycleSummary {
kind: SessionLifecycleKind::IdleShell,
pane_id: Some("%7".to_string()),
@@ -12173,6 +12570,7 @@ mod tests {
workspace_dirty: true,
abandoned: true,
},
boot_preflight: test_boot_preflight(),
sandbox_status: runtime::SandboxStatus::default(),
config_load_error: None,
},
@@ -12300,6 +12698,7 @@ mod tests {
project_root: Some(PathBuf::from("/tmp/project")),
git_branch: Some("feature/session-lifecycle".to_string()),
git_summary: GitWorkspaceSummary::default(),
branch_freshness: test_branch_freshness(),
session_lifecycle: SessionLifecycleSummary {
kind: SessionLifecycleKind::RunningProcess,
pane_id: Some("%9".to_string()),
@@ -12308,6 +12707,7 @@ mod tests {
workspace_dirty: false,
abandoned: false,
},
boot_preflight: test_boot_preflight(),
sandbox_status: runtime::SandboxStatus::default(),
config_load_error: None,
};