diff --git a/cli/src/services/config/mod.rs b/cli/src/services/config/mod.rs index ae02d2c9..53587a4f 100644 --- a/cli/src/services/config/mod.rs +++ b/cli/src/services/config/mod.rs @@ -14,8 +14,8 @@ use render::{format_show_output, format_validate_output}; use resolver::resolve_runtime_config; pub(crate) use resolver::{ - resolve_agent_trace_storage_runtime_config, resolve_auth_runtime_config, - resolve_bash_policy_runtime_config, resolve_hook_runtime_config, + resolve_agent_trace_auto_sync_runtime_config, resolve_agent_trace_storage_runtime_config, + resolve_auth_runtime_config, resolve_bash_policy_runtime_config, resolve_hook_runtime_config, resolve_observability_runtime_config, }; pub(crate) use schema::validate_config_file; diff --git a/cli/src/services/config/resolver.rs b/cli/src/services/config/resolver.rs index 5702c259..79dbba7a 100644 --- a/cli/src/services/config/resolver.rs +++ b/cli/src/services/config/resolver.rs @@ -126,6 +126,29 @@ pub(crate) fn resolve_hook_runtime_config(cwd: &Path) -> Result Result> { + let runtime = resolve_runtime_config_with( + &ConfigRequest { + report_format: ReportFormat::Text, + config_path: None, + log_level: None, + timeout_ms: None, + }, + cwd, + |key| std::env::var(key).ok(), + |path| { + std::fs::read_to_string(path) + .with_context(|| format!("Failed to read config file '{}'.", path.display())) + }, + Path::exists, + resolve_default_global_config_path, + )?; + + Ok(runtime.agent_trace_auto_sync) +} + pub(crate) fn resolve_agent_trace_storage_runtime_config( cwd: &Path, ) -> Result { diff --git a/cli/src/services/default_paths.rs b/cli/src/services/default_paths.rs index e03cb544..0cf47082 100644 --- a/cli/src/services/default_paths.rs +++ b/cli/src/services/default_paths.rs @@ -383,7 +383,6 @@ pub(crate) mod claude_asset { pub const SETTINGS_FILE: &str = "settings.json"; pub const HOOKS_DIR: &str = "hooks"; pub const SKILLS_DIR: &str = "skills"; - pub const AGENTS_DIR: &str = "agents"; pub const COMMANDS_DIR: &str = "commands"; } diff --git a/cli/src/services/doctor/inspect.rs b/cli/src/services/doctor/inspect.rs index 38b8212c..2c5c98c7 100644 --- a/cli/src/services/doctor/inspect.rs +++ b/cli/src/services/doctor/inspect.rs @@ -23,11 +23,10 @@ use crate::services::setup::{ use super::types::{ AgentTraceDbHealth, CheckoutIdentityHealth, DoctorFixResultRecord, DoctorProblem, FileLocationHealth, FixResult, GlobalStateHealth, HookContentState, HookDoctorReport, - HookFileHealth, HookPathSource, IntegrationChildHealth, IntegrationContentState, - IntegrationGroupHealth, ProblemCategory, ProblemFixability, ProblemKind, ProblemSeverity, - Readiness, CLAUDE_AGENTS_LABEL, CLAUDE_COMMANDS_LABEL, CLAUDE_PLUGINS_LABEL, - CLAUDE_SKILLS_LABEL, OPENCODE_AGENTS_LABEL, OPENCODE_COMMANDS_LABEL, OPENCODE_PLUGINS_LABEL, - OPENCODE_SKILLS_LABEL, PI_EXTENSIONS_LABEL, PI_PROMPTS_LABEL, PI_SKILLS_LABEL, + HookFileHealth, HookPathSource, IntegrationArea, IntegrationChildHealth, + IntegrationContentState, IntegrationGroupHealth, IntegrationGroupKey, IntegrationTarget, + PostCommitAutoSyncHealth, PostCommitAutoSyncState, ProblemCategory, ProblemFixability, + ProblemKind, ProblemSeverity, Readiness, }; use super::{is_executable, DoctorDependencies, DoctorMode, REQUIRED_HOOKS}; @@ -129,6 +128,12 @@ fn build_report_without_service_owned_problem_checks( Vec::new() }; + let post_commit_auto_sync = collect_post_commit_auto_sync_health( + repository_root, + detected_repository_root.is_some(), + &hooks, + ); + let integration_targets_absent = should_show_no_integrations_message( git_available, bare_repository, @@ -150,6 +155,7 @@ fn build_report_without_service_owned_problem_checks( repository_root: detected_repository_root, hook_path_source, hooks_directory, + post_commit_auto_sync, config_locations: global_state.config_locations, hooks, integration_groups, @@ -158,6 +164,55 @@ fn build_report_without_service_owned_problem_checks( } } +fn collect_post_commit_auto_sync_health( + repository_root: &Path, + repository_is_available: bool, + hooks: &[HookFileHealth], +) -> PostCommitAutoSyncHealth { + let resolved = config::resolve_agent_trace_auto_sync_runtime_config(repository_root).ok(); + let (enabled, source, config_source) = + resolved.map_or((true, "unresolved", None), |resolved| { + ( + resolved.value, + resolved.source.as_str(), + resolved + .source + .config_source() + .map(ConfigPathSource::as_str), + ) + }); + + let state = post_commit_auto_sync_state(enabled, repository_is_available, hooks); + + PostCommitAutoSyncHealth { + state, + enabled, + source, + config_source, + } +} + +fn post_commit_auto_sync_state( + enabled: bool, + repository_is_available: bool, + hooks: &[HookFileHealth], +) -> PostCommitAutoSyncState { + if !repository_is_available { + PostCommitAutoSyncState::NotApplicable + } else if !enabled { + PostCommitAutoSyncState::Disabled + } else if hooks.iter().any(|hook| { + hook.name == "post-commit" + && hook.exists + && hook.executable + && hook.content_state == HookContentState::Current + }) { + PostCommitAutoSyncState::Ready + } else { + PostCommitAutoSyncState::NotReady + } +} + fn collect_global_state_locations( repository_root: &Path, dependencies: &DoctorDependencies<'_>, @@ -232,6 +287,7 @@ fn collect_agent_trace_db_health( summary: problem.summary.clone(), remediation: problem.remediation.clone(), next_action: problem.next_action, + scope: None, }); continue; } @@ -359,6 +415,7 @@ fn inspect_repository_hooks( summary: String::from("Git is not available on this machine."), remediation: String::from("Install an accessible 'git' binary and ensure it is on PATH before rerunning 'sce doctor'."), next_action: "manual_steps", + scope: None, }); return Vec::new(); } @@ -374,6 +431,7 @@ fn inspect_repository_hooks( ), remediation: String::from("Run 'sce doctor' from a non-bare working tree clone to inspect repo-scoped SCE hook health."), next_action: "manual_steps", + scope: None, }); return Vec::new(); } @@ -387,6 +445,7 @@ fn inspect_repository_hooks( summary: String::from("The current directory is not inside a git repository."), remediation: String::from("Run 'sce doctor' from inside the target repository working tree to inspect repo-scoped SCE hook health."), next_action: "manual_steps", + scope: None, }); return Vec::new(); } @@ -404,6 +463,7 @@ fn inspect_repository_hooks( summary: String::from("Unable to resolve git hooks directory."), remediation: String::from("Verify that git repository inspection succeeds and rerun 'sce doctor' inside a non-bare git repository."), next_action: "manual_steps", + scope: None, }); Vec::new() } @@ -490,6 +550,7 @@ fn inspect_repository_integrations( "Run 'sce setup --opencode', 'sce setup --claude', 'sce setup --pi', or 'sce setup --all' to install integration assets.", ), next_action: "manual_steps", + scope: None, }); return Vec::new(); } @@ -627,6 +688,7 @@ fn collect_global_state_health( summary: format!("Unable to resolve expected state root: {error}"), remediation: String::from("Verify that the current platform exposes a writable SCE state directory before rerunning 'sce doctor'."), next_action: "manual_steps", + scope: None, }), } @@ -648,6 +710,7 @@ fn collect_global_state_health( global_path.display() ), next_action: "manual_steps", + scope: None, }); } } @@ -665,6 +728,7 @@ fn collect_global_state_health( summary: format!("Unable to resolve expected global config path: {error}"), remediation: String::from("Verify that the current platform exposes a writable SCE config directory before rerunning 'sce doctor'."), next_action: "manual_steps", + scope: None, }), } @@ -676,7 +740,7 @@ fn collect_global_state_health( category: ProblemCategory::GlobalState, severity: ProblemSeverity::Error, fixability: ProblemFixability::ManualOnly, - summary: format!( + summary: format!( "Local config file '{}' failed validation: {error}", local_path.display() ), @@ -685,6 +749,7 @@ fn collect_global_state_health( local_path.display() ), next_action: "manual_steps", + scope: None, }); } } @@ -718,6 +783,7 @@ fn collect_hook_health(directory: &Path, problems: &mut Vec) -> V directory.display() ), next_action: "doctor_fix", + scope: None, }); } else if !directory.is_dir() { problems.push(DoctorProblem { @@ -731,6 +797,7 @@ fn collect_hook_health(directory: &Path, problems: &mut Vec) -> V directory.display() ), next_action: "manual_steps", + scope: None, }); } @@ -760,6 +827,7 @@ fn collect_hook_health(directory: &Path, problems: &mut Vec) -> V "Run 'sce doctor --fix' to install the canonical '{hook_name}' hook, or run 'sce setup --hooks' directly." ), next_action: "doctor_fix", + scope: None, }); } else if !executable { problems.push(DoctorProblem { @@ -773,6 +841,7 @@ fn collect_hook_health(directory: &Path, problems: &mut Vec) -> V hook_path.display() ), next_action: "doctor_fix", + scope: None, }); } @@ -791,6 +860,7 @@ fn collect_hook_health(directory: &Path, problems: &mut Vec) -> V "Run 'sce doctor --fix' to reinstall the canonical '{hook_name}' hook content, or run 'sce setup --hooks' directly." ), next_action: "doctor_fix", + scope: None, }); } @@ -863,13 +933,14 @@ fn push_opencode_integration_missing_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} required file(s) are missing: {}.", - group.label, missing_paths + group.display_label(), missing_paths ), remediation: format!( "Reinstall repo-root OpenCode assets to restore the missing {} file(s), then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -900,13 +971,14 @@ fn push_opencode_integration_mismatch_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} file(s) differ from the canonical embedded content: {}.", - group.label, mismatched_paths + group.display_label(), mismatched_paths ), remediation: format!( "Reinstall repo-root OpenCode assets to restore the canonical {} content, then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -935,6 +1007,7 @@ fn push_opencode_integration_read_fail_problems( child.path.display() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -966,13 +1039,14 @@ fn push_claude_integration_missing_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} required file(s) are missing: {}.", - group.label, missing_paths + group.display_label(), missing_paths ), remediation: format!( "Reinstall repo-root Claude assets to restore the missing {} file(s), then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1003,13 +1077,14 @@ fn push_claude_integration_mismatch_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} file(s) differ from the canonical embedded content: {}.", - group.label, mismatched_paths + group.display_label(), mismatched_paths ), remediation: format!( "Reinstall repo-root Claude assets to restore the canonical {} content, then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1038,6 +1113,7 @@ fn push_claude_integration_read_fail_problems( child.path.display() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1069,13 +1145,14 @@ fn push_pi_integration_missing_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} required file(s) are missing: {}.", - group.label, missing_paths + group.display_label(), missing_paths ), remediation: format!( "Reinstall repo-root Pi assets to restore the missing {} file(s), then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1106,13 +1183,14 @@ fn push_pi_integration_mismatch_problems( fixability: ProblemFixability::ManualOnly, summary: format!( "{} file(s) differ from the canonical embedded content: {}.", - group.label, mismatched_paths + group.display_label(), mismatched_paths ), remediation: format!( "Reinstall repo-root Pi assets to restore the canonical {} content, then rerun 'sce doctor'.", - group.label.to_ascii_lowercase() + group.display_label().to_ascii_lowercase() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1141,6 +1219,7 @@ fn push_pi_integration_read_fail_problems( child.path.display() ), next_action: "manual_steps", + scope: Some(group.key), }); } } @@ -1182,6 +1261,10 @@ fn inspect_opencode_plugin_registry_health( manifest_path.display() ), next_action: "manual_steps", + scope: Some(IntegrationGroupKey::new( + IntegrationTarget::OpenCode, + IntegrationArea::Plugins, + )), }); } @@ -1232,6 +1315,10 @@ fn inspect_opencode_asset_presence( asset_path.display() ), next_action: "manual_steps", + scope: Some(IntegrationGroupKey::new( + IntegrationTarget::OpenCode, + IntegrationArea::Plugins, + )), }); } @@ -1305,22 +1392,22 @@ fn collect_opencode_integration_groups( sort_integration_children(&mut skill_children); vec![ - IntegrationGroupHealth { - label: OPENCODE_PLUGINS_LABEL, - children: plugin_children, - }, - IntegrationGroupHealth { - label: OPENCODE_AGENTS_LABEL, - children: agent_children, - }, - IntegrationGroupHealth { - label: OPENCODE_COMMANDS_LABEL, - children: command_children, - }, - IntegrationGroupHealth { - label: OPENCODE_SKILLS_LABEL, - children: skill_children, - }, + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::OpenCode, IntegrationArea::Plugins), + plugin_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::OpenCode, IntegrationArea::Agents), + agent_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::OpenCode, IntegrationArea::Commands), + command_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::OpenCode, IntegrationArea::Skills), + skill_children, + ), ] } @@ -1336,7 +1423,6 @@ fn collect_claude_integration_groups( ) .collect::>(); let mut plugin_children = Vec::new(); - let mut agent_children = Vec::new(); let mut command_children = Vec::new(); let mut skill_children = Vec::new(); @@ -1354,11 +1440,6 @@ fn collect_claude_integration_groups( .starts_with(&format!("{}/", claude_asset::HOOKS_DIR)) { plugin_children.push(child); - } else if child - .relative_path - .starts_with(&format!("{}/", claude_asset::AGENTS_DIR)) - { - agent_children.push(child); } else if child .relative_path .starts_with(&format!("{}/", claude_asset::COMMANDS_DIR)) @@ -1373,27 +1454,22 @@ fn collect_claude_integration_groups( } sort_integration_children(&mut plugin_children); - sort_integration_children(&mut agent_children); sort_integration_children(&mut command_children); sort_integration_children(&mut skill_children); vec![ - IntegrationGroupHealth { - label: CLAUDE_PLUGINS_LABEL, - children: plugin_children, - }, - IntegrationGroupHealth { - label: CLAUDE_AGENTS_LABEL, - children: agent_children, - }, - IntegrationGroupHealth { - label: CLAUDE_COMMANDS_LABEL, - children: command_children, - }, - IntegrationGroupHealth { - label: CLAUDE_SKILLS_LABEL, - children: skill_children, - }, + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::ClaudeCode, IntegrationArea::Plugins), + plugin_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::ClaudeCode, IntegrationArea::Commands), + command_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::ClaudeCode, IntegrationArea::Skills), + skill_children, + ), ] } @@ -1438,18 +1514,18 @@ fn collect_pi_integration_groups( sort_integration_children(&mut extension_children); vec![ - IntegrationGroupHealth { - label: PI_PROMPTS_LABEL, - children: prompt_children, - }, - IntegrationGroupHealth { - label: PI_SKILLS_LABEL, - children: skill_children, - }, - IntegrationGroupHealth { - label: PI_EXTENSIONS_LABEL, - children: extension_children, - }, + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::Pi, IntegrationArea::Prompts), + prompt_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::Pi, IntegrationArea::Skills), + skill_children, + ), + IntegrationGroupHealth::new( + IntegrationGroupKey::new(IntegrationTarget::Pi, IntegrationArea::Extensions), + extension_children, + ), ] } @@ -1597,6 +1673,7 @@ fn inspect_hook_content_state( hook_path.display() ), next_action: "manual_steps", + scope: None, }); HookContentState::Unknown } diff --git a/cli/src/services/doctor/mod.rs b/cli/src/services/doctor/mod.rs index 30a2ca85..fd3ed083 100644 --- a/cli/src/services/doctor/mod.rs +++ b/cli/src/services/doctor/mod.rs @@ -196,6 +196,7 @@ fn doctor_problem_from_health(problem: HealthProblem) -> DoctorProblem { summary: problem.summary, remediation: problem.remediation, next_action: problem.next_action, + scope: None, } } diff --git a/cli/src/services/doctor/render.rs b/cli/src/services/doctor/render.rs index ff7b4867..2cb5d7c9 100644 --- a/cli/src/services/doctor/render.rs +++ b/cli/src/services/doctor/render.rs @@ -4,14 +4,13 @@ use serde_json::json; use crate::services::style::{heading, label, supports_color, value, OwoColorize}; use super::types::{ - fix_result_outcome, problem_category, problem_fixability, problem_severity, FileLocationHealth, - HookContentState, HookDoctorReport, HookFileHealth, HookPathSource, HumanTextStatus, - IntegrationChildHealth, IntegrationContentState, IntegrationGroupHealth, ProblemKind, - ProblemSeverity, Readiness, CLAUDE_AGENTS_LABEL, CLAUDE_COMMANDS_LABEL, CLAUDE_PLUGINS_LABEL, - CLAUDE_SKILLS_LABEL, OPENCODE_AGENTS_LABEL, OPENCODE_COMMANDS_LABEL, OPENCODE_PLUGINS_LABEL, - OPENCODE_SKILLS_LABEL, + fix_result_outcome, problem_category, problem_fixability, problem_severity, + DoctorDisplayDetail, DoctorDisplayNode, DoctorDisplayNodeKind, DoctorDisplayStatus, + HookContentState, HookDoctorReport, HookFileHealth, HookPathSource, IntegrationArea, + IntegrationChildHealth, IntegrationContentState, IntegrationGroupHealth, IntegrationGroupKey, + IntegrationTarget, PostCommitAutoSyncState, ProblemKind, ProblemSeverity, Readiness, }; -use super::{DoctorExecution, DoctorFormat, DoctorMode, DoctorRequest, NAME, REQUIRED_HOOKS}; +use super::{DoctorExecution, DoctorFormat, DoctorMode, DoctorRequest, NAME}; /// Guidance message rendered in the Integrations section when no integration /// targets are configured, detected, or both. @@ -66,74 +65,79 @@ fn format_report_with_color_policy(report: &HookDoctorReport, color_enabled: boo .filter(|problem| problem.severity == ProblemSeverity::Warning) .count(); let mut lines = Vec::new(); - lines.push(format!( - "{} {}", - label("SCE doctor"), - value(match report.mode { - DoctorMode::Diagnose => "diagnose", - DoctorMode::Fix => "fix", - }) - )); - - lines.push(format!("\n{}:", heading("Environment"))); - lines.push(format_human_text_row( - color_enabled, - state_root_status(report), - "State root", - report.state_root.as_ref().map_or_else( - || String::from("not detected"), - |location| location.path.display().to_string(), - ), - )); + lines.push(match report.mode { + DoctorMode::Diagnose => heading("SCE doctor"), + DoctorMode::Fix => heading("SCE doctor fix"), + }); - lines.push(format!("\n{}:", heading("Configuration"))); - push_configuration_section_rows(report, color_enabled, &mut lines); + lines.push(format!("\n{}", heading("Environment"))); + for node in environment_nodes(report) { + render_display_node(&mut lines, &node, color_enabled, 2, true); + } - lines.push(format!("\n{}:", heading("Repository"))); - lines.push(format_human_text_row( + lines.push(format!("\n{}", heading("Repository"))); + render_display_node( + &mut lines, + &top_level_node( + "Git repository", + repository_root_status(report), + problem_details(report, |kind| { + matches!( + kind, + ProblemKind::GitUnavailable + | ProblemKind::BareRepository + | ProblemKind::NotInsideGitRepository + ) + }), + ), color_enabled, - repository_root_status(report), - "Repository", - report.repository_root.as_ref().map_or_else( - || String::from("not detected"), - |path| path.display().to_string(), + 2, + true, + ); + render_display_node( + &mut lines, + &top_level_node( + post_commit_auto_sync_label(report.post_commit_auto_sync.state), + post_commit_auto_sync_status(report.post_commit_auto_sync.state), + Vec::new(), ), - )); - lines.push(format_human_text_row( color_enabled, - hooks_directory_status(report), - "Hooks", - report.hooks_directory.as_ref().map_or_else( - || String::from("not detected"), - |path| path.display().to_string(), + 2, + true, + ); + render_display_node( + &mut lines, + &top_level_node( + "Git hooks", + git_hooks_status(report), + problem_details(report, is_git_hooks_problem), ), - )); - - push_git_hooks_section(report, color_enabled, &mut lines); + color_enabled, + 2, + true, + ); - lines.push(format!("\n{}:", heading("Integrations"))); + lines.push(format!("\n{}", heading("Integrations"))); if report.integration_targets_absent { - lines.push(format_human_text_row( + render_display_node( + &mut lines, + &top_level_node( + NO_INTEGRATIONS_MESSAGE, + DoctorDisplayStatus::Fail, + problem_details(report, |kind| { + matches!(kind, ProblemKind::NoIntegrationsInstalled) + }), + ), color_enabled, - HumanTextStatus::Fail, - NO_INTEGRATIONS_MESSAGE, - "", - )); + 2, + true, + ); } else { - for group in integration_groups_for_text(report) { - lines.push(format_human_text_row( - color_enabled, - integration_group_status(&group, report.repository_root.is_some()), - group.label, - "", - )); - for child in &group.children { - lines.push(format_human_text_child_row( - color_enabled, - integration_child_status(child, report.repository_root.is_some()), - &child.relative_path, - integration_child_detail(child), - )); + for target in integration_targets_for_text(report) { + lines.push(format!(" {}", integration_target_label(target))); + for group in groups_for_target(report, target) { + let node = integration_group_node(&group, report); + render_display_node(&mut lines, &node, color_enabled, 4, true); } } } @@ -148,134 +152,149 @@ fn format_report_with_color_policy(report: &HookDoctorReport, color_enabled: boo lines.join("\n") } -fn push_configuration_section_rows( - report: &HookDoctorReport, - color_enabled: bool, - lines: &mut Vec, -) { - for location in &report.config_locations { - lines.push(format_human_text_row( - color_enabled, - config_location_status(report, location), - location.label, - location.path.display().to_string(), - )); - } - - if let Some(identity) = &report.checkout_identity { - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Pass, - "Checkout identity", - identity.checkout_id.clone(), - )); - } - - if let Some(agent_trace_db) = &report.agent_trace_db { - lines.push(format_human_text_row( - color_enabled, - agent_trace_db_status(report), - agent_trace_db.label, - agent_trace_db.path.display().to_string(), - )); - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Pass, - "Agent Trace repository ID", - agent_trace_db.repository_id.clone(), - )); - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Pass, - "Agent Trace identity source", - agent_trace_db.identity_source.clone(), - )); - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Pass, - "Agent Trace canonical identity", - agent_trace_db.canonical_identity.clone(), - )); - if let Some(remote) = &agent_trace_db.configured_remote { - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Pass, - "Agent Trace configured remote", - remote.clone(), - )); +fn post_commit_auto_sync_label(state: PostCommitAutoSyncState) -> &'static str { + match state { + PostCommitAutoSyncState::Disabled => { + "Post-commit Agent Trace auto-sync (disabled by config)" + } + PostCommitAutoSyncState::NotApplicable => { + "Post-commit Agent Trace auto-sync (not applicable)" + } + PostCommitAutoSyncState::Ready | PostCommitAutoSyncState::NotReady => { + "Post-commit Agent Trace auto-sync" } } } -fn push_git_hooks_section(report: &HookDoctorReport, color_enabled: bool, lines: &mut Vec) { - lines.push(format!("\n{}:", heading("Git Hooks"))); - if report.hooks.is_empty() { - for hook_name in REQUIRED_HOOKS { - lines.push(format_human_text_row( - color_enabled, - HumanTextStatus::Fail, - hook_name, - "not inspected", - )); +fn post_commit_auto_sync_status(state: PostCommitAutoSyncState) -> DoctorDisplayStatus { + match state { + PostCommitAutoSyncState::Ready | PostCommitAutoSyncState::Disabled => { + DoctorDisplayStatus::Pass } - } - for hook in &report.hooks { - lines.push(format_human_text_row( - color_enabled, - hook_human_text_status(hook), - hook.name, - hook.path.display().to_string(), - )); + PostCommitAutoSyncState::NotReady => DoctorDisplayStatus::Fail, + PostCommitAutoSyncState::NotApplicable => DoctorDisplayStatus::Miss, } } fn format_human_text_row( color_enabled: bool, - status: HumanTextStatus, + indent: usize, + status: DoctorDisplayStatus, name: &str, - detail: impl AsRef, -) -> String { - let detail = detail.as_ref(); - - if detail.is_empty() { - format!( - " {} {}", - value(&human_text_status_token(status, color_enabled)), - value(name), - ) - } else { - format!( - " {} {} ({})", - value(&human_text_status_token(status, color_enabled)), - value(name), - value(detail) - ) - } -} - -fn format_human_text_child_row( - color_enabled: bool, - status: HumanTextStatus, - name: &str, - detail: impl AsRef, ) -> String { format!( - " {} {} ({})", + "{}{} {}", + " ".repeat(indent), value(&human_text_status_token(status, color_enabled)), value(name), - value(detail.as_ref()) ) } -fn human_text_status_label(status: HumanTextStatus) -> &'static str { +fn environment_nodes(report: &HookDoctorReport) -> Vec { + vec![ + top_level_node( + "State", + state_root_status(report), + problem_details(report, |kind| { + matches!(kind, ProblemKind::UnableToResolveStateRoot) + }), + ), + top_level_node( + "Configuration", + configuration_status(report), + problem_details(report, |kind| { + matches!( + kind, + ProblemKind::GlobalConfigValidationFailed + | ProblemKind::UnableToResolveGlobalConfigPath + | ProblemKind::LocalConfigValidationFailed + | ProblemKind::AgentTraceDbConnectionFailed + | ProblemKind::AgentTraceDbSchemaNotReady + ) + }), + ), + top_level_node( + "Repository identity", + repository_identity_status(report), + problem_details(report, |kind| { + matches!( + kind, + ProblemKind::UnableToResolveStateRoot + | ProblemKind::AgentTraceDbConnectionFailed + | ProblemKind::AgentTraceDbSchemaNotReady + ) + }), + ), + ] +} + +fn top_level_node( + label: &str, + status: DoctorDisplayStatus, + details: Vec, +) -> DoctorDisplayNode { + DoctorDisplayNode::branch_with_status( + DoctorDisplayNodeKind::Domain, + label, + status, + details, + Vec::new(), + ) +} + +fn problem_details(report: &HookDoctorReport, matches: F) -> Vec +where + F: Fn(ProblemKind) -> bool, +{ + report + .problems + .iter() + .filter(|problem| problem.scope.is_none() && matches(problem.kind)) + .map(|problem| DoctorDisplayDetail::Problem { + summary: problem.summary.clone(), + remediation: problem.remediation.clone(), + }) + .collect() +} + +fn scoped_problem_details( + report: &HookDoctorReport, + scope: IntegrationGroupKey, +) -> Vec { + report + .problems + .iter() + .filter(|problem| problem.scope == Some(scope)) + .map(|problem| DoctorDisplayDetail::Problem { + summary: problem.summary.clone(), + remediation: problem.remediation.clone(), + }) + .collect() +} + +fn is_git_hooks_problem(kind: ProblemKind) -> bool { + matches!( + kind, + ProblemKind::HooksDirectoryMissing + | ProblemKind::HooksPathNotDirectory + | ProblemKind::UnableToResolveGitHooksDirectory + | ProblemKind::RequiredHookMissing + | ProblemKind::HookNotExecutable + | ProblemKind::HookContentStale + | ProblemKind::HookReadFailed + ) +} + +fn human_text_status_label(status: DoctorDisplayStatus) -> &'static str { match status { - HumanTextStatus::Pass => "PASS", - HumanTextStatus::Fail => "FAIL", - HumanTextStatus::Miss => "MISS", + DoctorDisplayStatus::Pass => "PASS", + DoctorDisplayStatus::Warn => "WARN", + DoctorDisplayStatus::Fail => "FAIL", + DoctorDisplayStatus::Miss => "MISS", } } -fn human_text_status_token(status: HumanTextStatus, color_enabled: bool) -> String { +fn human_text_status_token(status: DoctorDisplayStatus, color_enabled: bool) -> String { let token = format!("[{}]", human_text_status_label(status)); if !color_enabled { @@ -283,190 +302,370 @@ fn human_text_status_token(status: HumanTextStatus, color_enabled: bool) -> Stri } match status { - HumanTextStatus::Pass => token.green().bold().to_string(), - HumanTextStatus::Fail | HumanTextStatus::Miss => token.red().bold().to_string(), + DoctorDisplayStatus::Pass => token.green().bold().to_string(), + DoctorDisplayStatus::Warn => token.yellow().bold().to_string(), + DoctorDisplayStatus::Fail | DoctorDisplayStatus::Miss => token.red().bold().to_string(), } } -fn state_root_status(report: &HookDoctorReport) -> HumanTextStatus { - if report +fn status_for_problems(report: &HookDoctorReport, matches: F) -> DoctorDisplayStatus +where + F: Fn(ProblemKind) -> bool, +{ + report .problems .iter() - .any(|problem| problem.kind == ProblemKind::UnableToResolveStateRoot) - { - HumanTextStatus::Fail - } else { - HumanTextStatus::Pass - } + .filter(|problem| matches(problem.kind)) + .fold(DoctorDisplayStatus::Pass, |status, problem| { + status.worst(match problem.severity { + ProblemSeverity::Error => DoctorDisplayStatus::Fail, + ProblemSeverity::Warning => DoctorDisplayStatus::Warn, + }) + }) } -fn config_location_status( - report: &HookDoctorReport, - location: &FileLocationHealth, -) -> HumanTextStatus { - if report.problems.iter().any(|problem| { - problem.summary.starts_with(location.label) && problem.summary.contains("failed validation") - }) { - HumanTextStatus::Fail +fn state_root_status(report: &HookDoctorReport) -> DoctorDisplayStatus { + let status = status_for_problems(report, |kind| { + matches!(kind, ProblemKind::UnableToResolveStateRoot) + }); + if report.state_root.is_none() { + status.worst(DoctorDisplayStatus::Miss) } else { - HumanTextStatus::Pass + status } } -fn agent_trace_db_status(report: &HookDoctorReport) -> HumanTextStatus { - if let Some(agent_trace_db) = &report.agent_trace_db { - if !agent_trace_db.path.exists() { - return HumanTextStatus::Miss; - } - if report.problems.iter().any(|p| { - p.kind == ProblemKind::UnableToResolveStateRoot && p.summary.contains("agent trace") - || p.kind == ProblemKind::AgentTraceDbConnectionFailed - || p.kind == ProblemKind::AgentTraceDbSchemaNotReady - }) { - return HumanTextStatus::Fail; - } - HumanTextStatus::Pass +fn configuration_status(report: &HookDoctorReport) -> DoctorDisplayStatus { + status_for_problems(report, |kind| { + matches!( + kind, + ProblemKind::GlobalConfigValidationFailed + | ProblemKind::UnableToResolveGlobalConfigPath + | ProblemKind::LocalConfigValidationFailed + | ProblemKind::UnableToResolveStateRoot + | ProblemKind::AgentTraceDbConnectionFailed + | ProblemKind::AgentTraceDbSchemaNotReady + ) + }) +} + +fn repository_identity_status(report: &HookDoctorReport) -> DoctorDisplayStatus { + let status = status_for_problems(report, |kind| { + matches!( + kind, + ProblemKind::UnableToResolveStateRoot + | ProblemKind::AgentTraceDbConnectionFailed + | ProblemKind::AgentTraceDbSchemaNotReady + ) + }); + if report.repository_root.is_none() { + status.worst(DoctorDisplayStatus::Miss) } else { - HumanTextStatus::Fail + status } } -fn repository_root_status(report: &HookDoctorReport) -> HumanTextStatus { - let has_blocking_problem = report.problems.iter().any(|p| { +fn repository_root_status(report: &HookDoctorReport) -> DoctorDisplayStatus { + if report.problems.iter().any(|problem| { matches!( - p.kind, + problem.kind, ProblemKind::BareRepository | ProblemKind::NotInsideGitRepository ) - }); - if has_blocking_problem { - HumanTextStatus::Fail + }) { + DoctorDisplayStatus::Fail } else if report.repository_root.is_some() { - HumanTextStatus::Pass + DoctorDisplayStatus::Pass } else { - HumanTextStatus::Miss + DoctorDisplayStatus::Miss } } -fn hooks_directory_status(report: &HookDoctorReport) -> HumanTextStatus { - let has_blocking_problem = report.problems.iter().any(|p| { +fn git_hooks_status(report: &HookDoctorReport) -> DoctorDisplayStatus { + if report.problems.iter().any(|problem| { matches!( - p.kind, + problem.kind, ProblemKind::HooksDirectoryMissing | ProblemKind::HooksPathNotDirectory | ProblemKind::UnableToResolveGitHooksDirectory + | ProblemKind::RequiredHookMissing + | ProblemKind::HookNotExecutable + | ProblemKind::HookContentStale + | ProblemKind::HookReadFailed ) - }); - if has_blocking_problem { - HumanTextStatus::Fail + }) { + return DoctorDisplayStatus::Fail; + } + if report + .hooks + .iter() + .any(|hook| !matches!(hook_human_text_status(hook), DoctorDisplayStatus::Pass)) + { + DoctorDisplayStatus::Fail } else if report.hooks_directory.is_some() { - HumanTextStatus::Pass + DoctorDisplayStatus::Pass } else { - HumanTextStatus::Miss + DoctorDisplayStatus::Miss } } -fn hook_human_text_status(hook: &HookFileHealth) -> HumanTextStatus { +fn hook_human_text_status(hook: &HookFileHealth) -> DoctorDisplayStatus { if !hook.exists { - HumanTextStatus::Miss + DoctorDisplayStatus::Miss } else if matches!( hook.content_state, HookContentState::Stale | HookContentState::Unknown ) || !hook.executable { - HumanTextStatus::Fail + DoctorDisplayStatus::Fail } else { - HumanTextStatus::Pass + DoctorDisplayStatus::Pass } } -fn integration_groups_for_text(report: &HookDoctorReport) -> Vec { - if report.repository_root.is_none() { - return vec![ - IntegrationGroupHealth { - label: OPENCODE_PLUGINS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: OPENCODE_AGENTS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: OPENCODE_COMMANDS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: OPENCODE_SKILLS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: CLAUDE_PLUGINS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: CLAUDE_AGENTS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: CLAUDE_COMMANDS_LABEL, - children: Vec::new(), - }, - IntegrationGroupHealth { - label: CLAUDE_SKILLS_LABEL, - children: Vec::new(), - }, - ]; - } - - report.integration_groups.clone() +fn integration_group_status( + group: &IntegrationGroupHealth, + report: &HookDoctorReport, +) -> DoctorDisplayStatus { + let child_status = group + .children + .iter() + .fold(DoctorDisplayStatus::Pass, |status, child| { + status.worst(match child.content_state { + IntegrationContentState::Match => DoctorDisplayStatus::Pass, + IntegrationContentState::Missing + | IntegrationContentState::Mismatch + | IntegrationContentState::ReadFailed(_) => DoctorDisplayStatus::Fail, + }) + }); + let problem_status = report + .problems + .iter() + .filter(|problem| problem.scope == Some(group.key)) + .fold(DoctorDisplayStatus::Pass, |status, problem| { + status.worst(match problem.severity { + ProblemSeverity::Error => DoctorDisplayStatus::Fail, + ProblemSeverity::Warning => DoctorDisplayStatus::Warn, + }) + }); + child_status.worst(problem_status) } -fn integration_group_status( +fn integration_group_node( group: &IntegrationGroupHealth, - repository_available: bool, -) -> HumanTextStatus { - if !repository_available - || group - .children - .iter() - .any(|child| !matches!(&child.content_state, IntegrationContentState::Match)) + report: &HookDoctorReport, +) -> DoctorDisplayNode { + let children = integration_asset_nodes(group); + let status = integration_group_status(group, report); + DoctorDisplayNode::branch_with_status( + DoctorDisplayNodeKind::Group, + integration_area_label(group.key.area), + status, + scoped_problem_details(report, group.key), + children, + ) +} + +fn integration_asset_nodes(group: &IntegrationGroupHealth) -> Vec { + let mut nodes = Vec::new(); + for child in &group.children { + let components = asset_path_components(group.key.area, &child.relative_path); + let components = if components.is_empty() { + vec![child.relative_path.clone()] + } else { + components + }; + insert_asset_node(&mut nodes, &components, child); + } + nodes +} + +fn asset_path_components(area: IntegrationArea, relative_path: &str) -> Vec { + let mut components = std::path::Path::new(relative_path) + .components() + .filter_map(|component| match component { + std::path::Component::Normal(value) => Some(value.to_string_lossy().into_owned()), + _ => None, + }) + .collect::>(); + let expected_prefix = match area { + IntegrationArea::Plugins => Some("plugins"), + IntegrationArea::Agents => Some("agents"), + IntegrationArea::Commands => Some("commands"), + IntegrationArea::Skills => Some("skills"), + IntegrationArea::Prompts => Some("prompts"), + IntegrationArea::Extensions => Some("extensions"), + }; + if expected_prefix.is_some_and(|prefix| components.first().is_some_and(|first| first == prefix)) { - HumanTextStatus::Fail - } else { - HumanTextStatus::Pass + components.remove(0); } + components } -fn integration_child_status( +fn insert_asset_node( + nodes: &mut Vec, + components: &[String], child: &IntegrationChildHealth, - repository_available: bool, -) -> HumanTextStatus { - if repository_available { - match &child.content_state { - IntegrationContentState::Match => HumanTextStatus::Pass, - IntegrationContentState::Missing => HumanTextStatus::Miss, - IntegrationContentState::Mismatch | IntegrationContentState::ReadFailed(_) => { - HumanTextStatus::Fail - } - } - } else { - HumanTextStatus::Fail +) { + let label = components[0].clone(); + if components.len() == 1 { + let mut node = child.display_node(); + node.label = label; + nodes.push(node); + return; } + + let index = nodes + .iter() + .position(|node| node.label == label) + .unwrap_or_else(|| { + nodes.push(DoctorDisplayNode::branch_with_status( + DoctorDisplayNodeKind::Asset, + label, + DoctorDisplayStatus::Pass, + Vec::new(), + Vec::new(), + )); + nodes.len() - 1 + }); + insert_asset_node(&mut nodes[index].children, &components[1..], child); + nodes[index].status = nodes[index] + .children + .iter() + .fold(DoctorDisplayStatus::Pass, |status, child| { + status.worst(child.status) + }); } -fn integration_child_detail(child: &IntegrationChildHealth) -> String { - match &child.content_state { - IntegrationContentState::Mismatch => { - format!("{} - content mismatch", child.path.display()) +fn render_display_node( + lines: &mut Vec, + node: &DoctorDisplayNode, + color_enabled: bool, + indent: usize, + expand_unhealthy: bool, +) { + lines.push(format_human_text_row( + color_enabled, + indent, + node.status, + &node.label, + )); + if !expand_unhealthy || node.status == DoctorDisplayStatus::Pass { + return; + } + + for detail in &node.details { + render_display_detail(lines, detail, indent + 2); + } + for child in &node.children { + render_display_node(lines, child, color_enabled, indent + 2, true); + } +} + +fn render_display_detail(lines: &mut Vec, detail: &DoctorDisplayDetail, indent: usize) { + let prefix = " ".repeat(indent); + match detail { + DoctorDisplayDetail::MissingPath(path) => { + lines.push(format!("{prefix}Missing: {}", path.display())); + } + DoctorDisplayDetail::ContentMismatch { path } => { + lines.push(format!("{prefix}Path: {}", path.display())); + lines.push(format!( + "{prefix}Content mismatch: canonical content differs." + )); } - IntegrationContentState::ReadFailed(_) => { - format!("{} - read failed", child.path.display()) + DoctorDisplayDetail::ReadFailed { path, error } => { + lines.push(format!("{prefix}Path: {}", path.display())); + lines.push(format!("{prefix}Read error: {error}")); } - IntegrationContentState::Match | IntegrationContentState::Missing => { - child.path.display().to_string() + DoctorDisplayDetail::Problem { + summary, + remediation, + } => { + lines.push(format!("{prefix}Problem: {summary}")); + lines.push(format!("{prefix}Remediation: {remediation}")); } } } +fn integration_targets_for_text(report: &HookDoctorReport) -> Vec { + [ + IntegrationTarget::ClaudeCode, + IntegrationTarget::OpenCode, + IntegrationTarget::Pi, + ] + .into_iter() + .filter(|target| { + report + .integration_groups + .iter() + .any(|group| group.key.target == *target) + }) + .collect() +} + +fn groups_for_target( + report: &HookDoctorReport, + target: IntegrationTarget, +) -> Vec { + let mut groups = report + .integration_groups + .iter() + .filter(|group| group.key.target == target) + .cloned() + .collect::>(); + groups.sort_by_key(|group| integration_area_order(target, group.key.area)); + groups +} + +fn integration_target_label(target: IntegrationTarget) -> &'static str { + match target { + IntegrationTarget::ClaudeCode => "Claude Code", + IntegrationTarget::OpenCode => "OpenCode", + IntegrationTarget::Pi => "Pi", + } +} + +fn integration_area_label(area: IntegrationArea) -> &'static str { + match area { + IntegrationArea::Plugins => "Plugins", + IntegrationArea::Agents => "Agents", + IntegrationArea::Commands => "Commands", + IntegrationArea::Skills => "Skills", + IntegrationArea::Prompts => "Prompts", + IntegrationArea::Extensions => "Extensions", + } +} + +fn integration_area_order(target: IntegrationTarget, area: IntegrationArea) -> usize { + match target { + IntegrationTarget::OpenCode => match area { + IntegrationArea::Plugins => 0, + IntegrationArea::Agents => 1, + IntegrationArea::Commands => 2, + IntegrationArea::Skills => 3, + IntegrationArea::Prompts => 4, + IntegrationArea::Extensions => 5, + }, + IntegrationTarget::ClaudeCode => match area { + IntegrationArea::Plugins => 0, + IntegrationArea::Commands => 1, + IntegrationArea::Skills => 2, + IntegrationArea::Agents => 3, + IntegrationArea::Prompts => 4, + IntegrationArea::Extensions => 5, + }, + IntegrationTarget::Pi => match area { + IntegrationArea::Extensions => 0, + IntegrationArea::Prompts => 1, + IntegrationArea::Skills => 2, + IntegrationArea::Plugins => 3, + IntegrationArea::Agents => 4, + IntegrationArea::Commands => 5, + }, + } +} + fn render_report_json(execution: &DoctorExecution) -> Result { let report = &execution.report; let hooks = report @@ -538,6 +737,12 @@ fn render_report_json(execution: &DoctorExecution) -> Result { .hooks_directory .as_ref() .map(|path| path.display().to_string()), + "post_commit_auto_sync": { + "state": post_commit_auto_sync_state(report.post_commit_auto_sync.state), + "enabled": report.post_commit_auto_sync.enabled, + "source": report.post_commit_auto_sync.source, + "config_source": report.post_commit_auto_sync.config_source, + }, "config_paths": config_paths, "hooks": hooks, "problems": report.problems.iter().map(|problem| json!({ @@ -566,6 +771,15 @@ fn render_report_json(execution: &DoctorExecution) -> Result { serde_json::to_string_pretty(&payload).context("failed to serialize doctor report to JSON") } +fn post_commit_auto_sync_state(state: PostCommitAutoSyncState) -> &'static str { + match state { + PostCommitAutoSyncState::Ready => "ready", + PostCommitAutoSyncState::Disabled => "disabled", + PostCommitAutoSyncState::NotReady => "not_ready", + PostCommitAutoSyncState::NotApplicable => "not_applicable", + } +} + fn hook_state(hook: &HookFileHealth) -> &'static str { if !hook.exists { "missing" diff --git a/cli/src/services/doctor/types.rs b/cli/src/services/doctor/types.rs index 3d3f84dd..c17ae4c4 100644 --- a/cli/src/services/doctor/types.rs +++ b/cli/src/services/doctor/types.rs @@ -1,17 +1,5 @@ use std::path::PathBuf; -pub(super) const OPENCODE_PLUGINS_LABEL: &str = "OpenCode plugins"; -pub(super) const OPENCODE_AGENTS_LABEL: &str = "OpenCode agents"; -pub(super) const OPENCODE_COMMANDS_LABEL: &str = "OpenCode commands"; -pub(super) const OPENCODE_SKILLS_LABEL: &str = "OpenCode skills"; -pub(super) const CLAUDE_PLUGINS_LABEL: &str = "ClaudeCode plugins"; -pub(super) const CLAUDE_AGENTS_LABEL: &str = "ClaudeCode agents"; -pub(super) const CLAUDE_COMMANDS_LABEL: &str = "ClaudeCode commands"; -pub(super) const CLAUDE_SKILLS_LABEL: &str = "ClaudeCode skills"; -pub(super) const PI_PROMPTS_LABEL: &str = "Pi prompts"; -pub(super) const PI_SKILLS_LABEL: &str = "Pi skills"; -pub(super) const PI_EXTENSIONS_LABEL: &str = "Pi extensions"; - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum Readiness { Ready, @@ -65,6 +53,7 @@ pub(super) struct HookDoctorReport { pub(super) repository_root: Option, pub(super) hook_path_source: HookPathSource, pub(super) hooks_directory: Option, + pub(super) post_commit_auto_sync: PostCommitAutoSyncHealth, pub(super) config_locations: Vec, pub(super) hooks: Vec, pub(super) integration_groups: Vec, @@ -72,6 +61,22 @@ pub(super) struct HookDoctorReport { pub(super) problems: Vec, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct PostCommitAutoSyncHealth { + pub(super) state: PostCommitAutoSyncState, + pub(super) enabled: bool, + pub(super) source: &'static str, + pub(super) config_source: Option<&'static str>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum PostCommitAutoSyncState { + Ready, + Disabled, + NotReady, + NotApplicable, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct CheckoutIdentityHealth { pub(super) checkout_id: String, @@ -88,12 +93,101 @@ pub(super) struct AgentTraceDbHealth { pub(super) configured_remote: Option, } +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) enum IntegrationTarget { + OpenCode, + ClaudeCode, + Pi, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) enum IntegrationArea { + Plugins, + Agents, + Commands, + Skills, + Prompts, + Extensions, +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(super) struct IntegrationGroupKey { + pub(super) target: IntegrationTarget, + pub(super) area: IntegrationArea, +} + +impl IntegrationGroupKey { + pub(super) const fn new(target: IntegrationTarget, area: IntegrationArea) -> Self { + Self { target, area } + } + + pub(super) const fn display_label(self) -> &'static str { + match (self.target, self.area) { + (IntegrationTarget::OpenCode, IntegrationArea::Plugins) => "OpenCode plugins", + (IntegrationTarget::OpenCode, IntegrationArea::Agents) => "OpenCode agents", + (IntegrationTarget::OpenCode, IntegrationArea::Commands) => "OpenCode commands", + (IntegrationTarget::OpenCode, IntegrationArea::Skills) => "OpenCode skills", + (IntegrationTarget::ClaudeCode, IntegrationArea::Plugins) => "ClaudeCode plugins", + (IntegrationTarget::ClaudeCode, IntegrationArea::Commands) => "ClaudeCode commands", + (IntegrationTarget::ClaudeCode, IntegrationArea::Skills) => "ClaudeCode skills", + (IntegrationTarget::Pi, IntegrationArea::Prompts) => "Pi prompts", + (IntegrationTarget::Pi, IntegrationArea::Skills) => "Pi skills", + (IntegrationTarget::Pi, IntegrationArea::Extensions) => "Pi extensions", + // These combinations are not produced by inspection, but retaining + // deterministic labels keeps the key total for future targets/areas. + (IntegrationTarget::Pi, IntegrationArea::Plugins) => "Pi plugins", + (IntegrationTarget::Pi, IntegrationArea::Agents) => "Pi agents", + (IntegrationTarget::Pi, IntegrationArea::Commands) => "Pi commands", + (IntegrationTarget::ClaudeCode, IntegrationArea::Prompts) => "ClaudeCode prompts", + (IntegrationTarget::ClaudeCode, IntegrationArea::Extensions) => "ClaudeCode extensions", + (IntegrationTarget::ClaudeCode, IntegrationArea::Agents) => "Unsupported Claude area", + (IntegrationTarget::OpenCode, IntegrationArea::Prompts) => "OpenCode prompts", + (IntegrationTarget::OpenCode, IntegrationArea::Extensions) => "OpenCode extensions", + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct IntegrationGroupHealth { - pub(super) label: &'static str, + pub(super) key: IntegrationGroupKey, pub(super) children: Vec, } +impl IntegrationGroupHealth { + pub(super) fn new(key: IntegrationGroupKey, children: Vec) -> Self { + Self { key, children } + } + + pub(super) const fn display_label(&self) -> &'static str { + self.key.display_label() + } + + #[allow(dead_code)] + pub(super) fn display_node(&self) -> DoctorDisplayNode { + let children = self + .children + .iter() + .map(IntegrationChildHealth::display_node) + .collect::>(); + let status = children + .iter() + .fold(DoctorDisplayStatus::Pass, |status, child| { + status.worst(if child.status == DoctorDisplayStatus::Miss { + DoctorDisplayStatus::Fail + } else { + child.status + }) + }); + DoctorDisplayNode::branch_with_status( + DoctorDisplayNodeKind::Group, + self.display_label(), + status, + Vec::new(), + children, + ) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub(super) struct IntegrationChildHealth { pub(super) relative_path: String, @@ -109,6 +203,150 @@ pub(super) enum IntegrationContentState { ReadFailed(String), } +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[allow(dead_code)] +pub(super) enum DoctorDisplayStatus { + Pass, + Warn, + Miss, + Fail, +} + +impl DoctorDisplayStatus { + #[allow(dead_code)] + pub(super) const fn worst(self, other: Self) -> Self { + match (self, other) { + (Self::Fail, _) | (_, Self::Fail) => Self::Fail, + (Self::Miss, _) | (_, Self::Miss) => Self::Miss, + (Self::Warn, _) | (_, Self::Warn) => Self::Warn, + (Self::Pass, Self::Pass) => Self::Pass, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(dead_code)] +pub(super) enum DoctorDisplayDetail { + MissingPath(PathBuf), + ContentMismatch { + path: PathBuf, + }, + ReadFailed { + path: PathBuf, + error: String, + }, + Problem { + summary: String, + remediation: String, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(dead_code)] +pub(super) enum DoctorDisplayNodeKind { + Domain, + Group, + Asset, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(dead_code)] +pub(super) struct DoctorDisplayNode { + pub(super) kind: DoctorDisplayNodeKind, + pub(super) label: String, + pub(super) status: DoctorDisplayStatus, + pub(super) details: Vec, + pub(super) children: Vec, +} + +impl DoctorDisplayNode { + #[allow(dead_code)] + pub(super) fn domain(label: impl Into, children: Vec) -> Self { + Self::branch(DoctorDisplayNodeKind::Domain, label, children) + } + + #[allow(dead_code)] + pub(super) fn group(label: impl Into, children: Vec) -> Self { + Self::branch(DoctorDisplayNodeKind::Group, label, children) + } + + #[allow(dead_code)] + fn asset( + label: impl Into, + status: DoctorDisplayStatus, + detail: Option, + ) -> Self { + Self { + kind: DoctorDisplayNodeKind::Asset, + label: label.into(), + status, + details: detail.into_iter().collect(), + children: Vec::new(), + } + } + + #[allow(dead_code)] + fn branch(kind: DoctorDisplayNodeKind, label: impl Into, children: Vec) -> Self { + Self::branch_with_status(kind, label, DoctorDisplayStatus::Pass, Vec::new(), children) + } + + pub(super) fn branch_with_status( + kind: DoctorDisplayNodeKind, + label: impl Into, + status: DoctorDisplayStatus, + details: Vec, + children: Vec, + ) -> Self { + let status = children.iter().fold(status, |status, child| { + status.worst( + if matches!( + kind, + DoctorDisplayNodeKind::Domain | DoctorDisplayNodeKind::Group + ) && child.status == DoctorDisplayStatus::Miss + { + DoctorDisplayStatus::Fail + } else { + child.status + }, + ) + }); + Self { + kind, + label: label.into(), + status, + details, + children, + } + } +} + +impl IntegrationChildHealth { + #[allow(dead_code)] + pub(super) fn display_node(&self) -> DoctorDisplayNode { + let (status, detail) = match &self.content_state { + IntegrationContentState::Match => (DoctorDisplayStatus::Pass, None), + IntegrationContentState::Missing => ( + DoctorDisplayStatus::Miss, + Some(DoctorDisplayDetail::MissingPath(self.path.clone())), + ), + IntegrationContentState::Mismatch => ( + DoctorDisplayStatus::Fail, + Some(DoctorDisplayDetail::ContentMismatch { + path: self.path.clone(), + }), + ), + IntegrationContentState::ReadFailed(error) => ( + DoctorDisplayStatus::Fail, + Some(DoctorDisplayDetail::ReadFailed { + path: self.path.clone(), + error: error.clone(), + }), + ), + }; + DoctorDisplayNode::asset(self.relative_path.clone(), status, detail) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum ProblemCategory { GlobalState, @@ -179,6 +417,7 @@ pub(crate) struct DoctorProblem { pub(crate) summary: String, pub(crate) remediation: String, pub(crate) next_action: &'static str, + pub(super) scope: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -188,13 +427,6 @@ pub(crate) struct DoctorFixResultRecord { pub(crate) detail: String, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum HumanTextStatus { - Pass, - Fail, - Miss, -} - pub(super) fn problem_category(category: ProblemCategory) -> &'static str { match category { ProblemCategory::GlobalState => "global_state", diff --git a/context/architecture.md b/context/architecture.md index 3c644ddb..d9487596 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -126,13 +126,13 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/setup/mod.rs` defines the setup command contract (`SetupMode`, `SetupTarget`, `SetupRequest`, CLI flag parser/validator), an `inquire`-backed interactive target prompter (`InquireSetupTargetPrompter`), setup dispatch outcomes (proceed/cancelled), additive durable-context bootstrap (`bootstrap_context_baseline` for standalone `--bootstrap-context` and every normal successful setup path), and compile-time embedded asset access (`EmbeddedAsset`, target-scoped iterators, required-hook asset iterators/lookups). It also owns the install-time optional-workflow seam: the `OptionalWorkflow` type plus the build-generated `OPTIONAL_WORKFLOWS` catalog, a per-target `WorkflowAssetLayout` built from the existing `default_paths` command/skill directory constants (`command`/`commands`/`prompts` plus `skills`), and `iter_embedded_assets_for_setup_target_with_selection(target, selection)`, which yields every embedded asset except the `{command_dir}/{command_slug}.md` file and `{skills_dir}/{skill_slug}/` subtree of each optional workflow the selection omits. Membership is derived from the catalog's slugs rather than an enumerated file list, so a new optional workflow needs no Rust change. This filtered iterator is the only way embedded assets are enumerated; setup installs through it and doctor inspects through it, so there is no unfiltered enumeration path that could reintroduce an unselected workflow. The non-interactive selection flows through the repeatable `--workflow ` flag into `SetupRequest.optional_workflows: Option>` (`None` meaning the flag was absent); `validate_optional_workflow_slugs` checks each slug against `OPTIONAL_WORKFLOWS` during request resolution, before any write, and `run_setup_for_mode` resolves `None` to the persisted `integrations.optional_workflows` (exported as `persisted_optional_workflows`) before installing through the filtered iterator and persisting the resolved selection. The interactive selection flows through the same seam: `SetupTargetPrompter` carries `prompt_target` plus `prompt_optional_workflows(defaults)` (returning `None` for a cancelled prompt), `SetupDispatch::Proceed { mode, optional_workflows }` carries a prompted selection alongside the resolved mode, and `resolve_setup_dispatch(mode, prompter, defaults)` runs the workflow prompt only after an interactive target prompt, mapping either cancellation to `SetupDispatch::Cancelled`. The prompt module builds its `inquire::MultiSelect` from `optional_workflow_prompt_inputs(catalog, defaults)`, which returns `None` for an empty catalog (skipping the prompt) and otherwise catalog-ordered rows plus the indices to pre-check, ignoring ids absent from the catalog. `setup/command.rs` therefore resolves the repository root before dispatch, so a non-git directory fails before any prompt. For repository builds, `cli/build.rs` validates the `SCE_CLI_GENERATED_INPUT_DIR` payload and canonical-input inventories, copies the payload into Cargo `OUT_DIR/pkl-generated`, stages `cli/assets/hooks/**` under `OUT_DIR/static`, requires the staged `config/optional-workflows.json`, and generates both the setup manifest and the optional-workflow catalog (`optional_workflows.rs`, rejecting a manifest whose `schemaVersion` is not 1 or whose entries lack a non-empty `id`/`title`/`description`/`commandSlug`/`skillSlug`) in `OUT_DIR`; focused internal seams separate install-flow from prompt-flow logic; `cli/src/services/setup/command.rs` owns the `SetupCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Its install engine/orchestrator (`install_assets_for_concrete_target_with_rename`) installs each embedded asset individually: it stages the asset's content next to its final destination and swaps it into place by renaming the staging file directly over the destination — never unlinking the destination first, since `fs::rename` already replaces an existing file atomically — with deterministic recovery guidance naming the failing asset's path on swap failure (the pre-existing destination content, if any, is untouched) and no backup artifact creation; it never removes an integration target directory as a whole, so files a repository owns inside `.opencode`/`.claude`/`.pi` — including nested inside an SCE-owned subdirectory such as `skills/` or `commands/` — survive a setup run. For the two assets that are merge targets — the Claude target's `settings.json`, detected by `is_claude_settings_merge_target`, and the OpenCode target's `opencode.json`, detected by `is_opencode_config_merge_target` — the content staged is not the embedded asset's bytes but the result of `cli/src/services/setup/config_merge.rs::merge_or_create_claude_settings`/`merge_or_create_opencode_config(existing_bytes, generated_bytes, source_path)`: each returns the generated document verbatim when no file exists yet, otherwise parses the existing file as JSON (a parse failure is a hard error naming `source_path`, and nothing is written) and merges it with a pure per-shape function that copies `$schema` from the generated document and preserves every other key from the existing file untouched. `merge_claude_settings` replaces, per hook event key the generated document declares, only the entries whose command contains the ownership marker `run-sce-or-show-install-guidance.sh`, preserving every event key and non-SCE hook entry from the existing file. `merge_opencode_config` merges the `plugin` array as a set: existing entries whose path starts with the ownership marker `./plugins/sce-` are dropped structurally — so a stale plugin path an older or renamed catalog once installed is still recognized and pruned even after the current generated document stops declaring it — and the generated document's `plugin` entries are appended after the surviving entries. After that per-asset install loop, `prune_stale_assets_for_concrete_target` deletes every path the full embedded catalog for the concrete target claims but the resolved selection did not install (a deselected optional workflow, or an asset a newer catalog renamed or dropped), and `remove_empty_ancestor_directories` removes any parent directory left empty by that deletion, stopping at the target root or at a directory that still holds something such as a user file. It formats deterministic completion messaging; required-hook install orchestration (`install_required_git_hooks`, backed by the rename-injectable `install_required_git_hooks_with_rename`) is a third content-computation seam alongside the two JSON merge targets: `install_single_required_hook_with_rename` computes the bytes to stage with `cli/src/services/setup/hook_merge.rs::merge_or_create_hook(existing_bytes, canonical_bytes, hook_name)` rather than writing `hook_asset.bytes` verbatim — a foreign hook (no SCE managed block, no legacy guidance-URL marker) is kept as an exact byte prefix with the canonical block appended after it, an SCE-owned hook has only its block spliced in place or left unchanged, and a legacy pre-marker hook is replaced wholesale — then follows the same per-file stage/atomic-swap choreography as config-asset install: the staging file is renamed directly over the existing hook without unlinking it first, so a rename failure leaves the prior hook's bytes and executable bit intact, with deterministic recovery guidance on swap failure. `Installed`/`Updated`/`Skipped` are decided against the merged bytes plus the executable bit rather than the canonical asset's raw bytes, so an already-current foreign-plus-block hook reports `Skipped`; `RequiredHookInstallResult.unreachable_block_advisory` is set, and rendered as a named advisory line in setup's hook output, when an appended block follows a foreign hook's zero-indent `exec`/`exit` and so would never run. After the Git gate, setup always ensures the context baseline; context-only requests return there, while normal modes derive a repo-root-scoped context before aggregating static lifecycle provider `setup` dispatch across providers (config → local_db → auth_db → agent_trace_db → hooks when requested), so setup providers consume only repo-root access from the scoped context. - `cli/src/services/setup/mod.rs` keeps those responsibilities inside one file for now, but the current ownership split is explicit: the inline `install` module owns repository-path normalization, staging/swap install behavior, required-hook installation, and filesystem safety guards, while the inline `prompt` module owns interactive target selection and prompt styling. - `cli/src/services/security.rs` provides shared security utilities for deterministic secret redaction (`redact_sensitive_text`) and directory write-permission probes (`ensure_directory_is_writable`) used by app/setup/observability surfaces. -- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Agent Trace database inspection is no longer a doctor-adjacent command surface; doctor owns repository-scoped DB health and checkout identity facts, while `sce sync` owns control-plane synchronization. Report fact collection preserves environment/repository/hook/integration display data, while service-owned lifecycle providers own config validation, local DB and repository-scoped Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. Integration inspection in `doctor/inspect.rs` is scoped twice over: `resolve_doctor_integration_targets` picks which targets to inspect, and `persisted_optional_workflows` (reused from setup) resolves which optional workflows the repository selected, which the OpenCode/Claude/Pi child collectors apply through `iter_embedded_assets_for_setup_target_with_selection`. An unselected optional workflow therefore contributes no expected children at all, so no row and no missing/mismatch problem can be produced for it, while a selected one keeps the unchanged presence and content-hash checks. +- `cli/src/services/doctor/mod.rs` owns the current doctor request/report surface while focused submodules (`doctor/inspect.rs`, `doctor/render.rs`, `doctor/fixes.rs`, `doctor/types.rs`) split report fact collection, rendering, manual fix reporting, and doctor-owned domain types into smaller seams; `cli/src/services/doctor/command.rs` owns the `DoctorCommand` payload used by the static `RuntimeCommand` enum and executes against any context implementing repo-root scoping. Runtime doctor execution resolves a repository root, derives a scoped context, requests the shared static lifecycle provider catalog with hooks included for service-owned `diagnose` and `fix` behavior, adapts lifecycle-owned health/fix records into doctor-owned problem/fix records, and then renders stable text/JSON problem records with category/severity/fixability/remediation fields plus deterministic fix-result reporting in fix mode. Agent Trace database inspection is no longer a doctor-adjacent command surface; doctor owns repository-scoped DB health and checkout identity facts, while `sce sync` owns control-plane synchronization. Report fact collection preserves environment/repository/hook/integration display data and adds a non-launching `post_commit_auto_sync` fact based on canonical post-commit managed-block currency plus resolved `agent_trace.auto_sync` source/value; this fact does not launch `sce sync`, and existing hook problem/remediation/readiness semantics remain authoritative. Service-owned lifecycle providers own config validation, local DB and repository-scoped Agent Trace DB readiness/bootstrap, and hook rollout diagnosis/repair. Integration inspection in `doctor/inspect.rs` is scoped twice over: `resolve_doctor_integration_targets` picks which targets to inspect, and `persisted_optional_workflows` (reused from setup) resolves which optional workflows the repository selected, which the OpenCode/Claude/Pi child collectors apply through `iter_embedded_assets_for_setup_target_with_selection`. An unselected optional workflow therefore contributes no expected children at all, so no row and no missing/mismatch problem can be produced for it, while a selected one keeps the unchanged presence and content-hash checks. - `cli/src/services/version/mod.rs` defines the version command parser/rendering contract (`parse_version_request`, `render_version`) with deterministic text output and stable JSON runtime-identification fields; `cli/src/services/version/command.rs` owns the `VersionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/completion/mod.rs` defines completion parser/rendering contract (`parse_completion_request`, `render_completion`) with deterministic Bash/Zsh/Fish script output aligned to current parser-valid command/flag surfaces; `cli/src/services/completion/command.rs` owns the `CompletionCommand` payload used by the static `RuntimeCommand` enum. - `cli/src/services/hooks/mod.rs` defines the current local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) plus a commit-msg co-author policy seam (`apply_commit_msg_coauthor_policy`) that injects one canonical SCE trailer only when the enabled-by-default attribution-hooks config/env control is not opted out, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); the preflight is wired into `run_commit_msg_subcommand_in_repo` and logs `sce.hooks.commit_msg.ai_overlap_error` on error paths; `cli/src/services/hooks/command.rs` owns the `HooksCommand` payload used by the static `RuntimeCommand` enum. In the current attribution-only baseline, `pre-commit` and `post-rewrite` are deterministic no-op surfaces; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace persistence entrypoint (captures current commit patch, queries recent repository-level `diff_traces` from the bounded past-7-days window, combines valid patches via `patch::combine_patches`, intersects with post-commit patch via `patch::intersect_patches`, persists result to `post_commit_patch_intersections`, then persists built Agent Trace payloads with range-level `content_hash` values to `agent_traces` in the repository-scoped Agent Trace DB without post-commit file artifacts); after successful validation and persistence, the default-enabled config-file-only `agent_trace.auto_sync` gate launches one detached sync-owned `sync --format json` child unless explicitly disabled in config, with launcher failures ignored and no high-frequency hook trigger; `diff-trace` performs STDIN JSON intake, validates required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent/`null` → `None`), required nullable/non-empty `tool_version` plus required `u64` `time` (Unix epoch milliseconds), rejects values that cannot fit signed `time_ms` storage, prefixes the stored `diff_traces.session_id` before insert construction (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi, same-tool idempotent), and inserts the parsed payload fields into `RepositoryAgentTraceDb` without creating a parsed-payload `context/tmp` artifact; Claude structured `PostToolUse` diff-trace intake resolves model attribution event-locally: direct top-level or nested metadata wins, otherwise the event's `transcript_path` is scanned for the assistant envelope whose `tool_use.id` matches `tool_use_id`; either source is normalized once with the `claude/` prefix and lookup failures remain nullable. `session-model` is no longer a supported hook route. - Generated Claude settings no longer register `SessionStart` for Agent Trace model attribution, and `sce hooks session-model` is no longer a supported hook command. The `session_models` table/API and session-level fallback lookup were removed in T02 of the `remove-session-models-direct-claude-model-id` plan; `diff-trace` now uses direct-first/event-transcript-second Claude `model_id` resolution and direct `tool_version` values, without restoring session-level state. - `cli/src/services/resilience.rs` defines bounded retry/timeout/backoff execution policy (`RetryPolicy`, `run_with_retry`) for transient operation hardening with deterministic failure messaging and retry observability. -- `context/cli/agent-trace-auto-sync.md` documents the sync-owned, one-shot post-commit launcher boundary: it reuses `sce sync`, has no daemon or local retry machinery, and fails open when child startup cannot be completed. +- `context/cli/agent-trace-auto-sync.md` documents the sync-owned, one-shot post-commit launcher boundary: it reuses `sce sync`, has no daemon or local retry machinery, and fails open when child startup cannot be completed; doctor reports this capability without invoking the launcher. - `cli/src/services/sync/progress.rs` owns the sync-local, consumer-typed progress seam: generic `ProgressReporter` supports event delivery plus explicit successful finalization, closure-based collectors, and a no-op implementation alongside the fixed `indicatif` stderr presentation adapter. `cli/src/services/sync/sync.rs` owns `SyncProgressEvent` and its four-stream payload semantics, while `sync/command.rs` selects the terminal adapter for text and the no-op reporter for JSON. There is no top-level `cli/src/services/progress/` module; sync orchestration depends only on its sync-owned contract, so terminal-library details stay at the sync presentation boundary. - `sce sync [--format text|json]` is implemented: `cli/src/services/sync/sync.rs` resolves repository-scoped Agent Trace storage, authenticates against the control plane with stored WorkOS credentials, uses the config-resolved `control_plane_base_url` with baked default `https://sce.crocoderlab.dev`, calls the ingestion `/state` endpoint once, then starts the `messages`/`parts`/`diff_traces`/`agent_traces` capture-stream state machines concurrently via `AgentTraceExportReader` and a shared per-stream reconciliation engine. Batches and cursor refreshes remain sequential within each stream, while fixed stream order is retained for final and stream-completion reporting; `cli/src/services/sync/render_sync.rs` renders the converged `AgentTraceSyncReport` as concise per-stream text or `camelCase` JSON without a nested subcommand field (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization otherwise still flow through lifecycle providers aggregated by setup, while repository-scoped DB health/repair flows through the doctor surface. The former trace database inspection and nested sync surfaces are unavailable. - `cli/src/services/patch.rs` defines the standalone patch domain model (`ParsedPatch`, `PatchFileChange`, `FileChangeKind`, `PatchHunk`, `TouchedLine`, `TouchedLineKind`) for in-memory parsed unified-diff representation, capturing only touched lines (added/removed) plus minimal per-file/per-hunk metadata while excluding non-hunk headers and unchanged context lines. All types are `serde`-serializable/deserializable with `snake_case` JSON field naming. The module also provides `parse_patch`, a public parser function that converts raw unified-diff text (both `Index:` SVN-style and `diff --git` git-style formats) into `ParsedPatch` structs, with `ParseError` for actionable malformed-input diagnostics. Storage-agnostic JSON load helpers (`load_patch_from_json` for string input, `load_patch_from_json_bytes` for byte input) reconstruct `ParsedPatch` from serialized JSON content with `PatchLoadError` for actionable deserialization diagnostics. Its patch-set operations now include deterministic ordered combination plus target-shaped intersection that prefers exact touched-line matches and falls back to historical `kind`+`content` matching when incremental diffs and canonical post-commit diffs have drifted line numbers; `parse_patch`, `combine_patches`, and `intersect_patches` are consumed by the active post-commit hook runtime. @@ -196,6 +196,7 @@ Shared Context Plan and Shared Context Code remain separate architectural roles. - `/change-to-plan` and `/next-task` remain separate command entrypoints aligned to those roles. - Reuse is handled through shared canonical guidance blocks and skill-owned phase contracts, not by collapsing both roles into one agent. - OpenCode agents are thin routing surfaces rather than behavior owners: Plan routes to `/change-to-plan`; Code routes to `/next-task`, `/validate`, `/commit`, `/handover`, and `/brownfield`. Claude and Pi have no generated agents. Workflow commands and self-contained skill packages are the sole behavior owners. +- Doctor follows that target capability boundary in its installed-asset inventory: Claude exposes only `Plugins`, `Commands`, and `Skills`, while OpenCode retains `Plugins`, `Agents`, `Commands`, and `Skills`; the shared `IntegrationArea::Agents` model remains for OpenCode. - The canonical `/change-to-plan` workflow sequences `sce-context-load` and `sce-plan-authoring`; `/next-task` sequences `sce-plan-review`, `sce-task-execution`, and `sce-task-context-sync`; `/validate` runs `sce-validation` only and reports its Validation Report; `/commit` sequences around `sce-atomic-commit`; `/handover` and `/brownfield` have no sibling phases at all — their single `sce-handover` and `sce-brownfield` skills own their whole routing directly. Those phase modules are canonical authoring source; no target generates them as packages. - Every target embeds those same phase boundaries inside `sce-change-to-plan`, `sce-next-task`, `sce-validate`, `sce-commit`, `sce-handover`, and `sce-brownfield`, so no generated command or prompt invokes a phase or sibling SCE package. Workflow skills may use relevant non-SCE helpers inside the active step, but the helper returns control to that step; the only SCE sibling invocation remains the successful task-synchronization decision gate's bounded `sce-decision` call. - OpenCode, Claude, and Pi all generate `/handover` routed to exactly `sce-handover` (see [Handover workflow](sce/handover-workflow.md)) and `/brownfield` routed to exactly `sce-brownfield` (see [Brownfield workflow](sce/brownfield-workflow.md)); the automated OpenCode profile is removed. diff --git a/context/cli/agent-trace-auto-sync.md b/context/cli/agent-trace-auto-sync.md index a6c6a268..be66f21b 100644 --- a/context/cli/agent-trace-auto-sync.md +++ b/context/cli/agent-trace-auto-sync.md @@ -35,6 +35,21 @@ Automatic synchronization is not invoked by `pre-commit`, `diff-trace`, or watcher, polling loop, scheduler, daemon, retry queue, persistent service, or second synchronization database. +## Doctor readiness + +`sce doctor` reports the capability without invoking it. The post-commit hook's +canonical managed block is the hook-side readiness proof, using the same +managed-block currency semantics as setup. The report exposes +`post_commit_auto_sync` in JSON with `state`, `enabled`, `source`, and +`config_source` fields. Its states are `ready` for enabled/current, +`disabled` for an explicit false opt-out, `not_ready` for enabled but missing, +stale, unreadable, or otherwise non-current hook content, and `not_applicable` +outside repository scope. The disabled state is healthy and does not alter the +existing problem/remediation or overall readiness rules for unrelated hook +issues. Text uses `[PASS] Post-commit Agent Trace auto-sync`, the explicit +disabled label, or `[FAIL] Post-commit Agent Trace auto-sync` accordingly. +Doctor never launches `sce sync` or a background process. + ## Manual synchronization and retryability The explicit operator flow remains: diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index 9443425d..671dbd0a 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -64,7 +64,7 @@ An interactive `setup` run instead resolves the selection through an `inquire` m `setup` now also exposes compile-time embedded config assets for OpenCode/Claude/Pi targets, sourced from the generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees via `cli/build.rs` with normalized forward-slash relative paths and target-scoped iteration APIs; the embedded asset set includes the OpenCode bash-policy plugin wrapper plus Claude settings `PreToolUse` Bash policy hook, both delegating to the Rust `sce policy bash` path. `setup` additionally includes a repository-root install engine (`install_embedded_setup_assets`) that installs each embedded asset individually into `.opencode/`/`.claude/`/`.pi/` — stage next to the final destination, remove only that destination file if present, swap into place, with deterministic recovery guidance naming the failing asset's path on swap failure — never removing an integration target directory as a whole, while treating bash-policy enforcement files as first-class SCE-managed assets. See [setup non-destructive per-asset install policy](../sce/setup-no-backup-policy-seam.md) for the full contract, including the pending pruning gap for deselected/stale assets. `setup` now executes end-to-end and prints deterministic completion details including selected target(s) and per-target install count. -`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only and `sce doctor --fix` selects repair-intent mode. The former Agent Trace database inspection routes are unavailable; doctor owns repository-scoped Agent Trace DB health and checkout-identity diagnostics. The current `doctor` runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and repository-scoped Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and repo-root installed OpenCode, Claude, and Pi integration presence/content health for their embedded setup assets. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. +`doctor` now executes end-to-end with explicit diagnosis and repair-intent surfaces: `sce doctor` stays read-only and `sce doctor --fix` selects repair-intent mode. The former Agent Trace database inspection routes are unavailable; doctor owns repository-scoped Agent Trace DB health and checkout-identity diagnostics. The current `doctor` runtime aggregates `ServiceLifecycle::diagnose` and `ServiceLifecycle::fix` calls across all registered service providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) plus integration checks, covering state-root resolution, global and repo-local `sce/config.json` readability/schema validation, local DB and repository-scoped Agent Trace DB path/health, DB-parent readiness barriers, the repo hook rollout slice when a repository target is detected, and post-commit Agent Trace auto-sync readiness derived from canonical managed-block currency plus resolved configuration. The readiness fact reports enabled/current, explicit disabled, not-ready, and not-applicable states in text and JSON without launching synchronization; Claude's inventory is only `Plugins`, `Commands`, and `Skills`, while OpenCode retains `Agents`. Fix mode delegates to each provider's `fix` implementation, which reuses the canonical setup hook install flow to repair missing/stale/non-executable required hooks and missing hooks directories, and it can bootstrap missing canonical database parent directories when the resolved paths match canonical owned locations. `sce sync [--format text|json]` is the implemented user-invocable synchronization command: it synchronizes the current repository's Agent Trace DB with the control-plane ingestion API; local DB and Agent Trace DB bootstrap continue to happen through `setup`, and DB health/repair continues to happen through `doctor`. See [agent-trace-sync-command.md](agent-trace-sync-command.md) and [sync-command.md](sync-command.md). ## Command loop and error model @@ -81,7 +81,7 @@ An interactive `setup` run instead resolves the selection through an `inquire` m - Interactive `sce setup` prompt cancellation/interrupt exits cleanly with: `Setup cancelled. No files were changed.` - Command handlers return deterministic status messaging: - `setup`: `Context baseline ensured.` on every successful path; context-only `--bootstrap-context` stops there. Normal modes continue with `Setup completed successfully.` plus selected targets and per-target install destinations/counts. -- `doctor`: current runtime emits `SCE doctor diagnose` / `SCE doctor fix` human text headers plus ordered `Environment`, `Configuration` (including checkout identity plus repository-scoped Agent Trace DB rows with repository ID, identity source, safe canonical identity, configured remote, and path when available), `Repository`, `Git Hooks`, and `Integrations` sections with bracketed `[PASS]`/`[FAIL]`/`[MISS]` row tokens, shared-style green pass plus red fail/miss colorization when enabled, simplified `label (path)` rows, top-level-only hook rows, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode and reports `checkout_identity` plus the resolved repository-scoped Agent Trace DB record with credential-safe metadata. +- `doctor`: current runtime emits compact `SCE doctor` / `SCE doctor fix` human text headers plus ordered `Environment`, `Repository`, and `Integrations` domains with typed target-scoped integration areas, bracketed `[PASS]`/`[WARN]`/`[FAIL]`/`[MISS]` status tokens, shared-style colorization, healthy-row metadata suppression, a post-commit Agent Trace auto-sync readiness row, and a deterministic summary footer; JSON output carries stable problem/fixability records plus deterministic fix-result records in fix mode, reports `checkout_identity` plus the resolved repository-scoped Agent Trace DB record with credential-safe metadata, and includes the stable `post_commit_auto_sync` `state`/`enabled`/`source`/`config_source` object. Existing hook problems continue to own remediation and overall readiness, and launcher failures remain fail-open in the hook runtime. Use `--format json` for complete path and identity detail; there is no verbose text flag. - `hooks`: deterministic hook subcommand status messaging for runtime entrypoint invocation and argument/STDIN contract validation. ## Service contracts @@ -89,7 +89,7 @@ An interactive `setup` run instead resolves the selection through an `inquire` m - `cli/src/services/setup/mod.rs` defines setup parsing/selection contracts, additive `bootstrap_context_baseline`, and runtime install orchestration (`run_setup_for_mode`) over the embedded asset install engine; `cli/src/services/setup/command.rs` owns the setup runtime command handler. After the Git gate, setup always ensures the context baseline; context-only requests return there, while normal modes aggregate `ServiceLifecycle::setup` calls across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`) in order, using a `ContextWithRepoRoot`-scoped context with resolved repository root. - `cli/src/services/setup/mod.rs` now keeps its larger internal responsibilities behind focused inline support modules: `install` owns repository canonicalization, staging/swap install flows, required-hook installation, and repo/writeability guards, while `prompt` owns interactive target selection and styled prompt labels. - `cli/src/services/config/mod.rs` defines config parser/runtime contracts (`show`, `validate`, `--help`), strict config-file key/type validation, deterministic text/JSON rendering, repo-configured bash-policy preset/custom validation and reporting under `policies.bash`, and shared auth-key metadata that declares env key, config-file key, and optional baked-default eligibility for supported auth runtime values starting with `workos_client_id` (`WORKOS_CLIENT_ID` vs `workos_client_id`); auth-key provenance/preference metadata stays on `show`, while `validate` stays trimmed to validation status plus issues/warnings. `cli/src/services/config/lifecycle.rs` implements `ServiceLifecycle` for config health checks and setup (global/local config validation and repo-local config bootstrap). -- `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorAction`, `DoctorMode`, `run_doctor`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, repository-scoped Agent Trace DB health, stable text/JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, repo-root installed OpenCode, Claude, and Pi integration inventory derived from embedded setup asset catalogs, shared-style bracketed human status token rendering (`[PASS]`, `[FAIL]`, `[MISS]`) with simplified `label (path)` text rows, and repair-mode delegation to service-owned fix implementations. Claude grouping is path-based: `settings.json`/`hooks/**` as `ClaudeCode plugins` (including `.claude/hooks/run-sce-or-show-install-guidance.sh`), plus `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`; Pi grouping is path-based: `prompts/**` as `Pi prompts` and `skills/**` as `Pi skills`. + - `cli/src/services/doctor/mod.rs` defines the implemented doctor request/report contract (`DoctorRequest`, `DoctorMode`, `run_doctor_with_context`) while focused submodules under `cli/src/services/doctor/` handle runtime command dispatch (`command.rs`), diagnosis (`inspect.rs`), rendering (`render.rs`), fix execution (`fixes.rs`), and doctor-owned domain types (`types.rs`). Together they preserve explicit fix-mode parsing, checkout identity diagnostics, repository-scoped Agent Trace DB health, stable JSON problem and database-record rendering, deterministic fix-result reporting, and aggregation of `ServiceLifecycle::diagnose`/`ServiceLifecycle::fix` across registered providers (`config`, `local_db`, `auth_db`, `agent_trace_db`, `hooks`). The doctor module coordinates state-root/config/database reporting and validation, path-source detection plus required-hook presence/executable/content checks when a repository target is detected, canonical non-launching post-commit auto-sync readiness reporting from managed-block currency plus resolved config, repo-root installed OpenCode, Claude, and Pi integration inventory derived from embedded setup asset catalogs, typed target/area grouping with compact human status tokens (`[PASS]`, `[WARN]`, `[FAIL]`, `[MISS]`), and repair-mode delegation to service-owned fix implementations. The human renderer emits the compact `SCE doctor` / `SCE doctor fix` hierarchy, suppresses healthy paths and identity metadata, and expands only unhealthy branches; JSON retains the complete path, identity, problem, fix-result, and `post_commit_auto_sync` detail. Claude grouping is typed and path-based: `settings.json`/`hooks/**` as the `Plugins` area (including `.claude/hooks/run-sce-or-show-install-guidance.sh`), plus `Commands` and `Skills`; OpenCode retains `Plugins`, `Agents`, `Commands`, and `Skills`; Pi grouping includes `prompts/**`, `skills/**`, and `extensions/**` areas. - `cli/src/services/version/mod.rs` defines the version parser/output contract (`parse_version_request`, `render_version`) with deterministic text/JSON output modes; `cli/src/services/version/command.rs` owns the version runtime command handler. - `cli/src/services/completion/mod.rs` defines the completion output contract (`render_completion`) using clap_complete to generate deterministic shell scripts for Bash, Zsh, and Fish; `cli/src/services/completion/command.rs` owns the completion runtime command handler. - `cli/src/services/hooks/mod.rs` defines production local hook runtime parsing/dispatch (`HookSubcommand`, `run_hooks_subcommand`) for `pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`; `cli/src/services/hooks/command.rs` owns the hook runtime command handler. Current runtime behavior is commit-msg-only attribution behind the enabled-by-default attribution gate with explicit opt-out controls; `pre-commit` and `post-rewrite` are deterministic no-ops; `post-commit` requires validated `--remote-url`, threads that value through Agent Trace flow, prints it to stderr, and remains an active intersection + Agent Trace DB persistence path; `diff-trace` performs STDIN JSON intake, required-field validation, and best-effort AgentTraceDb insertion with tool-prefixed stored `session_id` values plus direct nullable `model_id` / `tool_version` attribution. Claude structured `PostToolUse` payloads may derive `model_id` from top-level or nested `model` metadata with `claude/` prefix normalization. `session-model` is no longer a supported hooks route. `cli/src/services/hooks/lifecycle.rs` implements `ServiceLifecycle` for hook health checks, fix, and setup (hook rollout integrity and required-hook installation). diff --git a/context/cli/config-precedence-contract.md b/context/cli/config-precedence-contract.md index 44b4080a..91e92a12 100644 --- a/context/cli/config-precedence-contract.md +++ b/context/cli/config-precedence-contract.md @@ -4,7 +4,7 @@ This contract documents the implemented `sce config` command behavior, runtime resolver, renderer, and canonical Pkl-authored `sce/config.json` schema. The schema is emitted to payload-relative `config/schema/sce-config.schema.json` under Cargo `OUT_DIR` or packaging fallbacks and embedded by `cli/src/services/config/schema.rs` as `SCE_CONFIG_SCHEMA_JSON`; no generated schema is committed. -The current implementation resolves flat logging keys and Agent Trace runtime keys with deterministic precedence and source metadata, exposes resolved-value inspection through `sce config show`, and keeps `sce config validate` focused on validation status plus errors/warnings. Threshold, format, directory, and `log_file_retention_limit` values are consumed by runtime logging; the concrete logger uses the retention value for primary and v2 creation-triggered cleanup. The default-enabled `agent_trace.auto_sync` value is consumed by the post-commit trigger boundary and can be disabled explicitly. +The current implementation resolves flat logging keys and Agent Trace runtime keys with deterministic precedence and source metadata, exposes resolved-value inspection through `sce config show`, and keeps `sce config validate` focused on validation status plus errors/warnings. Threshold, format, directory, and `log_file_retention_limit` values are consumed by runtime logging; the concrete logger uses the retention value for primary and v2 creation-triggered cleanup. The default-enabled `agent_trace.auto_sync` value is consumed by the post-commit trigger boundary and by doctor readiness reporting, and can be disabled explicitly. ## Command surface @@ -125,6 +125,7 @@ When a default-discovered global or repo-local config file exists but fails JSON - `validate` text output is limited to `SCE config validation`, `Validation issues`, and `Validation warnings` lines. - `validate` JSON output is limited to `result.command`, `result.valid`, `result.issues`, and `result.warnings`. - `show` includes resolved Agent Trace configuration under `result.resolved.agent_trace` (JSON: `repository_id` optional-value shape, `repository_remote` and `auto_sync` resolved-value shapes) and as per-key text lines, reporting `(unset)` for a missing `repository_id`, `source: default` for the `origin` remote fallback, and `source: default` for omitted `auto_sync`. +- Doctor consumes the same resolved `agent_trace.auto_sync` value and source metadata; its separate `post_commit_auto_sync` report fact documents hook readiness without launching synchronization. - `show` includes resolved bash-tool policies under `result.resolved.policies.bash`. - Bash-policy output includes resolved preset IDs, expanded custom entries (`id`, `match.argv_prefix`, `message`), and config-file source metadata when present. - `show` text output renders `policies.bash` as a single deterministic line and reports `(unset)` when no policy config resolves. diff --git a/context/context-map.md b/context/context-map.md index 0aaedc35..6c9f9e7c 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -17,7 +17,7 @@ Feature/domain context: - `context/cli/patch-service.md` (standalone patch domain model, parser, JSON load helpers, and set operations in `cli/src/services/patch.rs` for in-memory parsed unified-diff representation, capturing only touched lines plus minimal per-file/per-hunk metadata, supporting both `Index:` SVN-style and `diff --git` git-style formats, with `ParseError` for actionable malformed-input diagnostics, `PatchLoadError`/`load_patch_from_json`/`load_patch_from_json_bytes` for storage-agnostic JSON reconstruction, `intersect_patches` for target-shaped overlap with exact-match-first and historical `kind`+`content` fallback semantics plus matched-constructed-line `session_id` and matched-constructed-hunk `model_id` provenance inheritance, and `combine_patches` for ordered patch combination with later-wins conflict resolution plus winning-hunk `model_id` provenance inheritance; repository structured-row reconstruction supplies persisted hunk-model and canonical touched-line-session provenance before these operations; `parse_patch`, `intersect_patches`, and `combine_patches` are consumed by the active post-commit hook runtime) - `context/cli/structured-patch-service.md` (Claude structured editor-hook derivation in `cli/src/services/structured_patch.rs`, including `Write` structured-update hunks, `Write` `tool_input.content` create fallback, `Edit` structured patches, deterministic skip reasons, `ParsedPatch` output semantics, Rust golden fixture coverage, and repository read-time enrichment that assigns persisted row `model_id` to each hunk and canonical row `session_id` to each touched line) - `context/cli/styling-service.md` (CLI text-mode output styling with `owo-colors`, TTY/`NO_COLOR` policy, shared helper API for human-facing surfaces including sync completion markers, and per-column right-to-left RGB gradient banner rendering) -- `context/cli/agent-trace-auto-sync.md` (default-enabled post-commit Agent Trace synchronization with explicit-false opt-out: the existing `sce sync` command launched once through the current executable after local persistence, detached null-standard-stream behavior, fail-open startup, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) +- `context/cli/agent-trace-auto-sync.md` (default-enabled post-commit Agent Trace synchronization with explicit-false opt-out plus doctor readiness reporting: the existing `sce sync` command launched once through the current executable after local persistence, detached null-standard-stream behavior, fail-open startup, canonical managed-block proof of hook readiness, stable text/JSON enabled/disabled/not-ready/not-applicable states, no daemon/queue/high-frequency trigger, and retryability through manual sync and control-plane cursor authority) - `context/cli/sync-command.md` (the top-level `sce sync` command: repository-scoped Agent Trace storage resolution, WorkOS-authenticated four-stream control-plane synchronization through the sync-owned consumer-typed `services::sync::progress` reporter contract with sync-owned events, its generic/no-op contract and `indicatif` presentation adapter for aligned stderr progress with independent stream completion, explicit successful finalization, JSON stdout silence, and rejection of the removed `sce trace` command group) - `context/cli/agent-trace-sync-command.md` (composed local-to-control-plane `sce sync` architecture: the `hooks/plugins → repository Agent Trace DB → AgentTraceExportReader → sce sync → HTTPS + WorkOS Bearer → control plane` data flow, the `sce auth login` / `cd ` / `sce sync` user flow, the no-local-cursor/no-`agent-trace-sync.db`/no-Turso-Sync/no-`BridgeLock`/no-local-DWH invariants, and `401`/`409`/ambiguous-batch-failure recovery semantics) - `context/cli/config-precedence-contract.md` (implemented `sce config` show/validate command contract, deterministic `flags > env > config file > defaults` resolution order, focused `config/resolver.rs` ownership for config discovery/merge/runtime precedence plus default-discovered invalid-file degradation, focused `config/render.rs` ownership for `show`/`validate` text+JSON output construction, canonical `$schema` acceptance for startup-loaded `sce/config.json` files, shared auth-key env/config/optional baked-default support starting with `workos_client_id`, shared runtime resolution for flat logging observability keys including `log_dir` / `SCE_LOG_DIR` with `/sce/logs` defaulting plus config-file/default-only positive `log_file_retention_limit`, config-file-only `agent_trace.repository_id`/`agent_trace.repository_remote` repository-identity keys with default remote `origin`, default-enabled `agent_trace.auto_sync` boolean resolution with explicit-false opt-out for the post-commit trigger boundary, the catalog-derived `integrations.optional_workflows` optional-workflow selection key, JSON-pointer-prefixed schema-validation errors, canonical Pkl-generated `sce/config.json` schema ownership plus CLI embedding/reuse contract including `policies.attribution_hooks.enabled` default-true/explicit-false opt-out metadata, config-file selection order, `show` provenance output, and trimmed `validate` output contract) @@ -49,8 +49,8 @@ Feature/domain context: - `context/sce/agent-trace-pre-commit-staged-checkpoint.md` (historical pre-commit staged-checkpoint contract; current runtime baseline has replaced this path with a deterministic no-op) - `context/sce/agent-trace-commit-msg-coauthor-policy.md` (current commit-msg canonical co-author trailer policy with enabled-by-default attribution hooks, explicit opt-out controls, `SCE_DISABLED` kill switch, caller-provided `ai_contribution_present` transformer seam wired from staged-diff AI-overlap preflight, idempotent dedupe, the `agent_trace::patches_have_overlap` pure overlap seam, the `StagedDiffAiOverlapResult` three-valued evidence gate, and `sce.hooks.commit_msg.ai_overlap_error` error logging) - `context/sce/agent-trace-post-commit-dual-write.md` (historical post-commit no-op/dual-write reference; current post-commit behavior is documented in `agent-trace-hooks-command-routing.md`) -- `context/sce/agent-trace-hook-doctor.md` (approved operator-environment contract for broadening `sce doctor` into the canonical health-and-repair entrypoint, including stable problem taxonomy, `--fix` semantics, checkout-aware Agent Trace DB reporting, setup-to-doctor alignment rules, canonical Git-hook payload restoration, and the approved downstream human text-mode layout/status/integration contract) -- `context/sce/doctor-human-text-contract.md` (implemented `sce doctor` human text layout contract: section order, `[PASS]`/`[FAIL]`/`[MISS]` status vocabulary, simplified hook rows, target-scoped integration checks with configured/detected/empty target resolution, selection-scoped optional-workflow inventory read from `integrations.optional_workflows`, no-installed-integrations guidance, and OpenCode, Claude, plus Pi integration group rendering rules including the `Pi extensions` group) +- `context/sce/agent-trace-hook-doctor.md` (approved operator-environment contract for broadening `sce doctor` into the canonical health-and-repair entrypoint, including stable problem taxonomy, `--fix` semantics, checkout-aware Agent Trace DB reporting, post-commit Agent Trace auto-sync readiness proof and opt-out behavior, setup-to-doctor alignment rules, canonical Git-hook payload restoration, and the approved downstream human text-mode layout/status/integration contract) +- `context/sce/doctor-human-text-contract.md` (implemented compact `sce doctor` human text contract: Environment/Repository/Integrations hierarchy, post-commit Agent Trace auto-sync readiness labels, `[PASS]`/`[WARN]`/`[FAIL]`/`[MISS]` status vocabulary, healthy-row metadata suppression, typed Claude Code/OpenCode/Pi target and area ordering, configured/detected/empty target resolution, selection-scoped optional-workflow inventory, no-installed-integrations guidance, and JSON as the full-detail route) - `context/sce/setup-githooks-install-contract.md` (canonical `sce setup --hooks` install contract for target-path resolution, all-hook non-blocking missing-CLI bootstrap behavior, foreign-hook preservation and managed-block merge/idempotent outcomes, atomic-swap replacement behavior, and doctor-readiness alignment) - `context/sce/setup-no-backup-policy-seam.md` (non-destructive per-asset install policy: config install writes/swaps each embedded asset individually by atomic rename over the destination, without ever unlinking it first, and never removes an integration target directory as a whole, then prunes catalog-derived stale/deselected asset paths and any parent directory left empty by that pruning; required-hook install uses the same per-file stage/atomic-swap choreography and, like the two JSON merge targets, computes its staged content ahead of the swap — a foreign hook's bytes are kept as an exact prefix with the SCE managed block appended; `.claude/settings.json` and `.opencode/opencode.json` are merge targets whose staged content is computed by JSON-merging the generated document into the user's existing one before the shared stage/swap step; no backup creation; a swap failure leaves prior destination content untouched, with deterministic recovery guidance naming the failing asset) - `context/sce/setup-githooks-hook-asset-packaging.md` (compile-time `sce setup --hooks` required-hook template packaging contract, including all-hook non-blocking missing-`sce` install guidance, available-CLI argument forwarding, post-commit-only origin remote lookup plus remote-URL forwarding/fallback behavior, setup-service accessor surface, and current validation posture) diff --git a/context/glossary.md b/context/glossary.md index b10372f5..cb719241 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -78,6 +78,7 @@ - `auth DB adapter`: Module in `cli/src/services/auth_db/mod.rs` that defines `AuthDbSpec` and exposes `AuthDb` as an `EncryptedTursoDb` alias. It resolves the canonical `/sce/auth.db` path with `auth_db_path()`, keeps encryption mandatory with `SCE_AUTH_DB_ENCRYPTION_KEY` env-secret precedence before OS keyring fallback and no plaintext mode, and embeds ordered auth migrations where baseline SQL creates `auth_credentials` without `user_id`, with `updated_at`, and a trigger that auto-refreshes `updated_at` on row updates. Auth runtime token-storage is now wired through `cli/src/services/token_storage.rs`, which persists tokens via the `auth_credentials` table in the encrypted auth DB instead of a JSON file. - `AuthDbLifecycle`: Lifecycle provider in `cli/src/services/auth_db/lifecycle.rs` that implements `ServiceLifecycle` for encrypted auth DB setup/doctor integration. `diagnose` collects auth DB path health problems, `fix` bootstraps missing auth DB parent directory, and `setup` calls `AuthDb::new()`. Registered as `LifecycleProviderId::AuthDb` in the shared lifecycle catalog. - `agent trace DB adapter`: Modules under `cli/src/services/agent_trace_db/` that define the sole repository-scoped `RepositoryAgentTraceDb = TursoDb` adapter (the checkout-scoped `AgentTraceDb`/`AgentTraceDbSpec` adapter and its 15-file migration chain were removed by the `retire-legacy-agent-trace-db` plan). The repository adapter uses the `agent-trace-repository` migration set (fresh baseline schema plus the additive `source_instance_id` migration) with `repository_metadata`, repository-level `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, and `parts` tables, no row-level `checkout_id`, typed parameterized insert helpers, and chronological recent `diff_traces` query/parse support. `AgentTraceDbLifecycle` initializes/checks repository-scoped storage through `agent_trace_storage`. +- `post-commit Agent Trace auto-sync readiness`: The doctor report fact that explains whether the enabled post-commit trigger is ready without invoking it. Doctor compares the installed `post-commit` hook's SCE managed block using the same currency semantics as setup and resolves config-file-only `agent_trace.auto_sync` with source metadata. JSON states are `ready`, `disabled`, `not_ready`, and `not_applicable`; explicit disable is healthy, while existing hook problems continue to own overall readiness and remediation. See [automatic Agent Trace synchronization](cli/agent-trace-auto-sync.md) and [doctor human text](sce/doctor-human-text-contract.md). - `structured patch service`: Pure synchronous Rust service in `cli/src/services/structured_patch.rs` that derives supported structured editor hook payloads into canonical `ParsedPatch` values. The current implemented source is Claude `PostToolUse` payloads for `Write` creates and `Edit` structured patches; wired into `sce hooks diff-trace` for Claude payload classification at intake and into `RepositoryAgentTraceDb::recent_diff_trace_patches` for post-commit structured payload parsing, where persisted row `model_id` is assigned to every hunk and persisted canonical row `session_id` to every touched line before downstream reconstruction. - `Agent Trace SCE metadata`: Implementation-owned top-level metadata emitted by `build_agent_trace(...)` as `metadata.sce.version`; the value is sourced from the compiled `sce` CLI package version via `env!("CARGO_PKG_VERSION")`, is schema-validated with the rest of the payload, and is persisted in AgentTraceDb `agent_traces.trace_json` without changing the top-level Agent Trace payload/schema `version`. - `Agent Trace range content_hash`: Per-range `content_hash` emitted by `build_agent_trace(...)` inside every `ranges[]` entry as `murmur3:`, computed from the touched-line kind/content of the `post_commit_patch` or embedded-patch hunk used to emit that range while excluding positions, paths, metadata, and database IDs. @@ -91,10 +92,8 @@ - `no-migration DB open path`: `TursoDb::open_without_migrations()` / `TursoDb::open_without_migrations_at(path)` plus Agent Trace adapter-specific no-migration seams; opens/connects a local Turso database with parent-directory creation and configured connection-open retry but does not create `__sce_migrations` or run embedded schema migrations. Active Agent Trace hook callers first try the repository-scoped no-migration path and then fall back to migration-running initialization when readiness or repository metadata validation fails. - `TursoDb migration readiness check`: Public methods on `TursoDb` in `cli/src/services/db/mod.rs` for non-mutating schema-readiness verification: `migration_metadata_problems(&self) -> Result>` queries `__sce_migrations` metadata and compares applied IDs against `M::migrations()`, returning problems (missing table, incomplete migrations, unexpected migrations) or an empty list when ready; `ensure_schema_ready(&self, setup_guidance: &str) -> Result<()>` calls `migration_metadata_problems()` and bails with a formatted error including `M::db_name()` and the caller-provided guidance string when problems are found. `RepositoryAgentTraceDb::ensure_schema_ready_for_hooks()` delegates to `TursoDb::ensure_schema_ready()` with the Agent Trace–specific `AGENT_TRACE_SCHEMA_SETUP_GUIDANCE` constant. - `database_retry config namespace`: Nested config namespace under `policies.database_retry` in `sce/config.json`, authored in `config/pkl/base/sce-config-schema.pkl` and parsed/resolved in `cli/src/services/config/mod.rs`. Supports per-database overrides (`local_db`, `agent_trace_db`, `auth_db`) each with optional `connection_open` and `query` objects containing `max_attempts`, `timeout_ms`, `initial_backoff_ms`, `max_backoff_ms`. Validated against JSON Schema at config load and surfaced in `sce config show`/`validate`. Wired into DB adapter constructors and operation methods via config-aware retry resolution with fallback to hardcoded defaults. -- `DatabaseRetryConfig`: Rust type in `cli/src/services/config/mod.rs` holding parsed and validated per-database retry policy overrides (`local_db`/`agent_trace_db`/`auth_db`, each `Option`) from the `policies.database_retry` config namespace. Initialized at app startup via `DATABASE_RETRY_CONFIG` `OnceLock` and consumed by config-aware retry resolution in DB adapters. -- `PerDbRetryConfig`: Rust type in `cli/src/services/config/mod.rs` holding optional `connection_open` and `query` retry policies (`Option`) for one database in the `database_retry` config namespace. -- `DB connection-open retry policy`: Retry policy used by `TursoDb::new()` and `EncryptedTursoDb::new()` for local Turso open/connect, resolved at app startup from `policies.database_retry..connection_open` via the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`3` attempts, `1s` elapsed-attempt timeout, `25ms` initial backoff, `200ms` max backoff) through `run_with_retry_sync`; embedded migrations are not covered by this policy. -- `DB query retry policy`: Retry policy used by `TursoDb::execute()`, `TursoDb::query()`, `TursoDb::query_map()`, `EncryptedTursoDb::execute()`, `EncryptedTursoDb::query()`, and `EncryptedTursoDb::query_map()` for local Turso operation retry, resolved from `policies.database_retry..query` via the `DATABASE_RETRY_CONFIG` `OnceLock` with fallback to hardcoded defaults (`5` attempts, `200ms` elapsed-attempt timeout, `25ms` initial backoff, `100ms` max backoff; default worst-case failure budget `<= 2_000ms`) through `run_with_retry_sync`. `query_map()` retries the initial query and row-fetch loop, then runs caller row mapping outside retry. +- `DatabaseRetryConfig` / `PerDbRetryConfig`: Rust types in `cli/src/services/config/mod.rs` holding parsed, validated per-database retry policy overrides (`local_db`/`agent_trace_db`/`auth_db`, each `Option`) and one database's optional `connection_open`/`query` policies from the `policies.database_retry` config namespace. Initialized at app startup via `DATABASE_RETRY_CONFIG` `OnceLock` and consumed by config-aware retry resolution in DB adapters. +- `DB connection-open/query retry policies`: Connection-open retry applies to `TursoDb::new()` and `EncryptedTursoDb::new()` through `policies.database_retry..connection_open`, with fallback to `3` attempts, `1s` timeout, and `25..200ms` backoff; query retry applies to `execute()`/`query()`/`query_map()` through `policies.database_retry..query`, with fallback to `5` attempts, `200ms` timeout, and `25..100ms` backoff (worst-case `<= 2_000ms`). Both resolve through `DATABASE_RETRY_CONFIG` and `run_with_retry_sync`; embedded migrations stay outside connection-open retry and `query_map()` keeps caller row mapping outside retry. - `__sce_migrations`: Per-database migration metadata table created by the shared `TursoConnectionCore` migration path behind public adapter `run_migrations()` methods; records applied migration IDs after successful execution so later setup/lifecycle initialization applies only migrations not yet recorded, while existing metadata-less DBs are brought forward by re-applying the current idempotent migration set and recording each ID. - `CLI generated migration manifest`: Build-time Rust source at `OUT_DIR/generated_migrations.rs` written by `cli/build.rs` from immediate `cli/migrations//*.sql` directories after staging SQL under `OUT_DIR/static/migrations`; constants are named from the database directory (for example `AGENT_TRACE_REPOSITORY_MIGRATIONS`, `AUTH_MIGRATIONS`), sorted by the numeric filename prefix before `_`, and embed staged SQL via `include_str!`. - `sync command deferral` (historical): Former plan/state note that a user-invocable sync command was deferred to `0.4.0`; superseded first by nested `sce trace sync` and now by top-level `sce sync` (see `context/cli/sync-command.md`). Local DB bootstrap and setup-time repository-scoped Agent Trace DB initialization still flow through lifecycle providers aggregated by the setup command, hook runtime still keeps a lazy repository Agent Trace DB fallback for repositories where setup has not run or schema metadata is incomplete, and DB health/repair still flows through the doctor surface. @@ -187,6 +186,7 @@ - `local DB migration contract`: `cli/src/services/local_db/mod.rs` delegates migration execution to `TursoDb` through the `DbSpec::migrations()` contract. The current `LocalDbSpec` migration list is empty, so `LocalDb::new()` opens/creates the canonical local DB without creating local tables. - `hook no-op baseline`: Current `cli/src/services/hooks/mod.rs` runtime posture where `pre-commit` and `post-rewrite` return deterministic no-op status text, `commit-msg` is a gated mutating path, `post-commit` persists intersections and built Agent Trace payloads without post-commit file artifacts, `diff-trace` validates STDIN payloads, resolves Claude `model_id` event-locally with direct metadata before fail-open transcript lookup, persists direct `tool_version`, applies stored `session_id` prefixes (`oc_`/`cc_`/`pi_`), and inserts DB-only AgentTraceDb rows, and `conversation-trace` is the active message/part intake path. `session-model` is no longer a supported hook route. - `sce doctor` operator-health contract: `cli/src/services/doctor/mod.rs` is the stable doctor entrypoint, with focused `doctor/{inspect,render,fixes,types}.rs` submodules implementing the current approved operator-health surface in `context/sce/agent-trace-hook-doctor.md`: `sce doctor --fix` selects repair intent, Agent Trace DB discovery is repository-scoped only (the checkout-scoped `sce trace --legacy` surface was removed by the `retire-legacy-agent-trace-db` plan), and output exposes deterministic doctor mode, readiness, stable problem taxonomy/fixability fields, checkout/database records, and fix-result records. The runtime validates state-root resolution, global and repo-local `sce/config.json` readability/schema health, local DB and repository-scoped Agent Trace DB path/health, DB-parent readiness barriers, git availability, non-repo vs bare-repo targeting failures, effective hook-path source resolution, required hook presence/executable/content drift against canonical embedded hook assets, and repo-root installed OpenCode, Claude, plus Pi integration content health. Human text mode uses the approved sectioned layout (`Environment`, `Configuration` with checkout identity plus repository-scoped Agent Trace DB rows when available, `Repository`, `Git Hooks`, `Integrations`), `SCE doctor diagnose` / `SCE doctor fix` headers, bracketed `[PASS]`/`[FAIL]`/`[MISS]` status tokens with shared-style green/red colorization when enabled, simplified `label (path)` row formatting, top-level-only hook rows, and integration parent/child rows where missing files surface as `[MISS]`, mismatches/read failures as `[FAIL]`, and affected parent groups as `[FAIL]`. Agent Trace DB rows include repository ID, identity source, safe canonical identity, configured remote name, and never raw remote URLs. Current integration groups include `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills`, `ClaudeCode plugins`, `ClaudeCode commands`, `ClaudeCode skills`, `Pi prompts`, and `Pi skills`; Claude `settings.json` plus `hooks/**` belong to `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, while Pi `prompts/**` and `skills/**` map to the Pi groups. Fix mode reuses canonical setup hook installation for missing/stale/non-executable required hooks and missing hooks directories and can bootstrap canonical missing SCE-owned DB parent directories. +- `compact doctor text report`: Current human-readable `sce doctor` rendering collapses healthy state, repository, and typed integration-area checks into the `Environment`/`Repository`/`Integrations` hierarchy, suppresses healthy paths and identity metadata, and uses `--format json` as the full-detail route. The canonical current contract is [doctor human text](sce/doctor-human-text-contract.md). - `cli warnings-denied lint policy`: `cli/Cargo.toml` sets `warnings = "deny"`, so plain `cargo clippy --manifest-path cli/Cargo.toml` already fails on warnings without needing an extra `-- -D warnings` tail. - `agent trace local DB schema migration contract`: Retired `apply_core_schema_migrations` behavior removed from the current runtime during `agent-trace-removal-and-hook-noop-reset` T01; the local DB baseline is now file open/create only. - `agent trace removed local-hook paths`: Current-state shorthand for the removed local-hook runtime behaviors that are no longer active: staged-checkpoint persistence, post-commit dual-write, post-rewrite remap ingestion, rewrite trace transformation, and retry replay. diff --git a/context/overview.md b/context/overview.md index 45aff438..452f2cd1 100644 --- a/context/overview.md +++ b/context/overview.md @@ -12,65 +12,59 @@ The generated `/next-task` workflow persists task-level context-synchronization - **Stderr diagnostics:** stable `SCE-ERR-{PARSE,VALIDATION,RUNTIME,DEPENDENCY}` codes with class-default `Try:` remediation (see `context/sce/cli-error-code-taxonomy.md`). - **Stdout/stderr:** command payloads on stdout only; redacted diagnostics and text-mode `sce sync` progress on stderr, while JSON sync remains silent (see `context/sce/cli-stdout-stderr-contract.md`). - **Observability:** config-resolved logging to stderr, optional dated/session-partitioned `log_dir` / `SCE_LOG_DIR` files with retention (see `context/sce/cli-observability-contract.md`). -- **Config precedence:** `flags > env > config file > defaults` (see `context/cli/config-precedence-contract.md`); the config-file-only `agent_trace.auto_sync` setting defaults to `true` and is resolved with source metadata for the post-commit trigger boundary. Its asynchronous post-commit behavior is documented in `context/cli/agent-trace-auto-sync.md`. +- **Config precedence:** `flags > env > config file > defaults` (see `context/cli/config-precedence-contract.md`); the config-file-only `agent_trace.auto_sync` setting defaults to `true` and is resolved with source metadata for the post-commit trigger and doctor readiness boundaries. Its asynchronous post-commit behavior and doctor capability reporting are documented in `context/cli/agent-trace-auto-sync.md`. - **Attribution hooks:** enabled by default, gated by staged-diff AI-overlap preflight; `SCE_ATTRIBUTION_HOOKS_DISABLED` opt-out (see `context/sce/agent-trace-commit-msg-coauthor-policy.md`). - **Install channels:** repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`); Homebrew deferred (see `context/sce/cli-first-install-channels-contract.md`). The CLI crate currently depends on `anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, and `uuid`, with target-specific keyring backend dependencies for Linux/FreeBSD, macOS, and Windows. No CLI dev-dependencies are currently declared. Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with authenticated whoami reading the Control Plane `GET /me` profile and rendering flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, and the top-level `sync` command. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. +The current doctor presentation contract supersedes the earlier output-shape scaffolding wording above: human text uses the compact Environment/Repository/Integrations hierarchy with healthy rows collapsed and unhealthy branches expanded, while JSON retains complete path, identity, problem, and fix-result detail. See `context/sce/doctor-human-text-contract.md`. The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: `2` parse failures, `3` invocation validation failures, `4` runtime failures, and `5` dependency startup failures. The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing. The command boundary's former flat, string-only `ClassifiedError` has been replaced by typed `CliError` in `cli/src/services/error.rs`: `CliError::User` carries a closed `UserError` catalog (currently only `NotAuthenticated`) for expected, deliberately-explained failures rendered as a friendly sentence with no `Try:` suffix, while `CliError::Internal` carries a live `anyhow::Error` source rendered as the real error chain with class-default remediation; `app_support` is the sole owner of turning either into the final styled stderr diagnostic, and `sce sync` is the first command to classify a failure (authentication) into `CliError::User`. See `context/sce/cli-error-code-taxonomy.md` for the full contract. The app runtime now also includes a structured observability baseline in `cli/src/services/observability.rs`: deterministic env-controlled log threshold/format (`SCE_LOG_LEVEL` defaults to `error`; `SCE_LOG_FORMAT` defaults to `text`), default-backed log-directory routing (`SCE_LOG_DIR` / config-file `log_dir` / `/sce/logs`) with per-operation machine-local dated file selection, optional session filename partitioning, and creation-triggered retention of direct regular `*.log` files to 10 entries, stable lifecycle event IDs, stderr primary emission so stdout command payloads remain pipe-safe, and `observability::traits` boundaries for logger and telemetry behavior. The app command dispatcher now enforces a centralized stdout/stderr stream contract in `cli/src/app.rs`: command success payloads are emitted on stdout only, while redacted user-facing diagnostics and text-mode sync progress are emitted on stderr; JSON sync remains silent. `cli/src/app.rs` also now runs through explicit startup phases — dependency check, observability config resolution, runtime initialization, command parse/execute, and output rendering — with the app runtime carrying logger/telemetry plus static command-catalog state across those phases while preserving the existing exit-code and degraded-startup contracts. Within that lifecycle, `parse_command_phase` delegates clap-to-runtime conversion to `cli/src/services/parse/command_runtime.rs`, which returns a static `RuntimeCommand` enum, `services::app_support::execute_command_phase` logs around enum-owned `execute_with_stderr(...)` dispatch, and generic `RunOutcome` rendering logs classified errors through the logger trait boundary without coupling render support to the production logger type. Command payload structs for `help`, `version`, `completion`, `auth`, `config`, `setup`, `doctor`, `hooks`, and `sync` live in service-owned `command.rs` files; `cli/src/services/command_registry.rs` owns the deterministic static command-name catalog and enum variants instead of boxed command trait objects. -The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local_db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for repository-scoped Agent Trace DB setup and repository DB path health/parent readiness from resolved repository identity, returning an actionable "requires a Git repository" diagnostic outside repository context (no global/checkout fallback path). Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. +The CLI now also enforces a shared output-format parser contract in `cli/src/services/output_format.rs`, with canonical `--format ` parsing and command-specific actionable invalid-value guidance reused by `config` and `version` services. A compile-safe service lifecycle seam also exists in `cli/src/services/lifecycle.rs`: `ServiceLifecycle` exposes default no-op `diagnose`, `fix`, and `setup` methods against the narrow `HasRepoRoot` accessor, uses lifecycle-owned health/fix/setup result types, and owns the shared static `LifecycleProvider` enum catalog/factory with deterministic config → local*db → auth_db → agent_trace_db → hooks ordering and no boxed provider aggregation. Hooks has a `services/hooks/lifecycle.rs` provider for hook rollout diagnosis/fix/setup, config has a `services/config/lifecycle.rs` provider for global/repo-local config validation plus repo-local config bootstrap, local_db has a `services/local_db/lifecycle.rs` provider for canonical local DB path health, parent-directory readiness/bootstrap, and `LocalDb::new()` setup, auth_db has a `services/auth_db/lifecycle.rs` provider for canonical auth DB path health, parent-directory readiness/bootstrap, and `AuthDb::new()` setup, and agent_trace_db has a `services/agent_trace_db/lifecycle.rs` provider for repository-scoped Agent Trace DB setup and repository DB path health/parent readiness from resolved repository identity, returning an actionable "requires a Git repository" diagnostic outside repository context (no global/checkout fallback path). Doctor runtime aggregates the full shared provider catalog for `diagnose` and `fix` and adapts lifecycle records into doctor-owned output records; setup command aggregates the shared provider catalog for `setup` with hooks included only when requested and adapts lifecycle setup outcomes before rendering setup-owned messages. Agent Trace lifecycle setup now resolves repository storage, creates/reuses checkout identity for diagnostics, and initializes `/sce/repos//agent-trace.db` via `RepositoryAgentTraceDb`; hook runtime lazy initialization uses the same repository storage resolver when setup has not prepared the DB or schema metadata is incomplete. The CLI now also includes a shared text styling service in `cli/src/services/style.rs` that provides deterministic color enablement via `owo-colors`, automatic TTY detection, and `NO_COLOR` compliance for human-facing text output; stdout help/text surfaces, stderr diagnostics, and interactive prompt-adjacent text now reuse that shared styling policy while JSON, completion, and other non-interactive/machine-readable flows remain unstyled. The service exports color-detection, conditional styling, help/diagnostic/label/prompt styling, and `banner_with_gradient()` helpers for use across command surfaces while preserving pipe-safe output for non-interactive environments. The `setup` command includes an `inquire`-backed target-selection flow: default interactive selection for OpenCode/Claude/Pi/All with required-hook installation in the same run, explicit non-interactive target flags (`--opencode`, `--claude`, `--pi`, `--all`), standalone `--bootstrap-context` for additive durable-context baseline creation without integration installs, deterministic mutually-exclusive validation, and non-destructive cancellation exits; the former `--both` flag was removed in favor of `--all` (opencode+claude+pi). Every normal successful setup path also ensures the same context baseline after the Git gate. Workflows the catalog marks optional are installed only when a repository opts in: the repeatable `sce setup --workflow ` flag names the selection for a run, an omitted flag reuses the selection persisted in `integrations.optional_workflows`, and the resolved selection filters the installed target assets and is written back to repo-local config. Interactive runs ask for the selection instead: a multi-select prompt follows target selection with every row unchecked on a first run and pre-checked from the persisted selection afterwards, cancelling it exits non-destructively like the target prompt, and the prompt is skipped when the catalog marks no workflow optional. `brownfield` is currently the only optional workflow, so a default run installs the five core workflows and no brownfield assets. `sce doctor` scopes its integration checks to that same recorded selection, so it never reports an unselected optional workflow's files as missing. For repository generation consumers, `config/pkl/generator-inputs.txt` declares the canonical Pkl/plugin input set and `scripts/produce-cli-generated-input.sh` owns its discovery, two-pass `config/pkl/generate.pkl` evaluation, determinism comparison, payload/input inventories, in-flight input-mutation rejection, atomic handoff publication, and staging cleanup. `scripts/run-cli-cargo.sh` creates a fresh temporary destination, delegates generation to that producer, invokes the requested Cargo workflow with `SCE_CLI_GENERATED_INPUT_DIR`, and removes the handoff after Cargo success, failure, or handled signals. `config/pkl/check-generated.sh` delegates the same production mechanics while retaining contract and path assertions. `scripts/prepare-cli-generated-assets.sh` moves the producer-validated Pkl payload and checksums into the unchanged package fallback, adds hooks, migrations, and the Agent Trace schema, and appends only those static checksums to the combined inventory. The root flake's pre-Cargo `cliGeneratedInput` derivation invokes the same producer from a declarative source containing the producer plus its declared inputs. `cli/build.rs` rejects missing, incomplete, modified, or stale repository handoffs, copies the validated payload into Cargo `OUT_DIR/pkl-generated`, stages static inputs under `OUT_DIR/static`, and writes setup-asset, optional-workflow-catalog, and migration Rust manifests into `OUT_DIR`; it never invokes Pkl. Published crates carry the ignored packaging-only fallback, and unpacked downstream builds validate and copy it into their own `OUT_DIR` without requiring Pkl or parent repository paths. -The setup service also provides repository-root install orchestration: it resolves the repository root, ensures the additive durable-context baseline, then for normal modes derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup installs config assets (`.opencode`/`.claude`/`.pi`) per file: each embedded asset is staged and swapped into its own destination path, creating parent directories as needed, without removing or recreating the target directory as a whole, so files a repository owns inside an SCE-managed target directory survive a setup run untouched. Two assets are merge targets rather than verbatim writes: Claude's `.claude/settings.json` and OpenCode's `.opencode/opencode.json`. For each, setup JSON-merges the generated document into the user's existing file rather than overwriting it, and fails deterministically without writing if the existing file is not valid JSON; a missing file is still created from the generated document verbatim. Claude's merge replaces only SCE-owned hook entries (identified by a command containing `run-sce-or-show-install-guidance.sh`) and the `$schema` key while preserving every other key and hook entry untouched. OpenCode's merge replaces the `$schema` key and merges the `plugin` array as a set: any entry shaped like an SCE plugin path (`./plugins/sce-*`) is dropped, structurally, so a path an older or renamed catalog once installed is still recognized and pruned, and the generated document's canonical plugin entries are appended after the surviving user entries. Required-hook install uses the same per-file stage/atomic-swap choreography as config-asset install — the staging file is renamed directly over an existing hook without unlinking it first, so a rename failure leaves the prior hook untouched. Both flows return deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. After installing, config install prunes stale SCE-owned assets: it deletes every path the full embedded catalog for the target claims but the current selection did not install (a deselected optional workflow, or an asset a newer catalog renamed or dropped), then removes any parent directory left empty by that deletion, leaving a directory intact if a user file still lives inside it. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs` now separates install-flow logic from interactive prompt logic through focused support seams. -The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction, `sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. -The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_dir`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config_root}/sce/config.json` then `.sce/config.json` with local override, where `config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_dir=/sce/logs`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, custom-policy `satisfied_by` wrapper exemption (a policy does not fire when the matched command was unwrapped from a declared wrapper such as `nix shell nixpkgs#ripgrep`), and a canonical Pkl-authored `sce/config.json` JSON Schema generated beneath Cargo `OUT_DIR` and embedded by `cli/src/services/config/mod.rs` for both `sce config validate` and doctor-time config checks. Runtime startup config loading keeps parity with that schema by accepting its `$schema` declaration in repo-local and global config files, so startup commands such as `sce version` no longer fail before dispatch on that field; the canonical declaration is `"https://sce.crocoder.dev/config.json"`; this schema URL is separate from the `https://sce.crocoderlab.dev` baked default used by `sce sync` for control-plane ingestion. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for `log_dir`; positive-integer `log_file_retention_limit` uses config-file/default precedence, defaults to `10`, and controls creation-triggered cleanup for primary and v2 log files; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/base/bash-policy-presets.pkl` and `context/sce/bash-tool-policy-enforcement-contract.md`. +The setup service also provides repository-root install orchestration: it resolves the repository root, ensures the additive durable-context baseline, then for normal modes derives a repo-root-scoped `AppContext` from the runtime command context, aggregates `ServiceLifecycle::setup` calls across lifecycle providers (config → local_db → auth_db → agent_trace_db → hooks when requested), handles interactive or flag-based target selection for config asset installation, and reports deterministic completion details (selected target(s) and installed file counts). Setup installs config assets (`.opencode`/`.claude`/`.pi`) per file: each embedded asset is staged and swapped into its own destination path, creating parent directories as needed, without removing or recreating the target directory as a whole, so files a repository owns inside an SCE-managed target directory survive a setup run untouched. Two assets are merge targets rather than verbatim writes: Claude's `.claude/settings.json` and OpenCode's `.opencode/opencode.json`. For each, setup JSON-merges the generated document into the user's existing file rather than overwriting it, and fails deterministically without writing if the existing file is not valid JSON; a missing file is still created from the generated document verbatim. Claude's merge replaces only SCE-owned hook entries (identified by a command containing `run-sce-or-show-install-guidance.sh`) and the `$schema` key while preserving every other key and hook entry untouched. OpenCode's merge replaces the `$schema` key and merges the `plugin` array as a set: any entry shaped like an SCE plugin path (`./plugins/sce-*`) is dropped, structurally, so a path an older or renamed catalog once installed is still recognized and pruned, and the generated document's canonical plugin entries are appended after the surviving user entries. Required-hook install uses the same per-file stage/atomic-swap choreography as config-asset install — the staging file is renamed directly over an existing hook without unlinking it first, so a rename failure leaves the prior hook untouched. Both flows return deterministic recovery guidance (recover from version control) on swap failure, without creating backup artifacts. After installing, config install prunes stale SCE-owned assets: it deletes every path the full embedded catalog for the target claims but the current selection did not install (a deselected optional workflow, or an asset a newer catalog renamed or dropped), then removes any parent directory left empty by that deletion, leaving a directory intact if a user file still lives inside it. The setup command gates all modes on an existing git repository before any writes. Internally, `cli/src/services/setup/mod.rs`now separates install-flow logic from interactive prompt logic through focused support seams. +The CLI now also applies baseline security hardening for reliability-driven automation: diagnostics/logging paths use deterministic secret redaction,`sce setup --hooks --repo ` canonicalizes and validates repository paths before execution, and setup write flows run explicit directory write-permission probes before staging/swap operations. +The config service now provides deterministic runtime config resolution with explicit precedence (`flags > env > config file > defaults`), strict config-file validation (`$schema`, `log_level`, `log_format`, `log_dir`, `timeout_ms`, `workos_client_id`, and nested `policies.bash`, `policies.attribution_hooks.enabled`, plus `policies.database_retry` with per-DB `connection_open`/`query` retry policy specs), deterministic default discovery/merge of global+local config files (`${config*root}/sce/config.json`then`.sce/config.json`with local override, where`config_root` comes from the shared default-path seam with XDG/`dirs::config_dir()` config-root resolution), defaults for the resolved observability value set (`log_level=error`, `log_format=text`, `log_dir=/sce/logs`), shared auth-key resolution with optional baked defaults starting at `workos_client_id`, first-class bash-policy preset/custom parsing with deterministic conflict and duplicate-prefix validation, custom-policy `satisfied_by`wrapper exemption (a policy does not fire when the matched command was unwrapped from a declared wrapper such as`nix shell nixpkgs#ripgrep`), and a canonical Pkl-authored `sce/config.json`JSON Schema generated beneath Cargo`OUT_DIR`and embedded by`cli/src/services/config/mod.rs`for both`sce config validate`and doctor-time config checks. Runtime startup config loading keeps parity with that schema by accepting its`$schema`declaration in repo-local and global config files, so startup commands such as`sce version`no longer fail before dispatch on that field; the canonical declaration is`"https://sce.crocoder.dev/config.json"`; this schema URL is separate from the `https://sce.crocoderlab.dev` baked default used by `sce sync` for control-plane ingestion. App-runtime observability now consumes flat logging keys through the shared resolver, so env values still override config-file values while config files provide deterministic fallback for `log_dir`; positive-integer `log_file_retention_limit` uses config-file/default precedence, defaults to `10`, and controls creation-triggered cleanup for primary and v2 log files; `sce config show` reports resolved observability/auth/policy values with provenance, while `sce config validate` is now a trimmed validation surface that reports only pass/fail plus validation errors or warnings in text and JSON modes. The canonical preset catalog and matching contract live in `config/pkl/base/bash-policy-presets.pkl` and `context/sce/bash-tool-policy-enforcement-contract.md`. Invalid default-discovered config files now also degrade gracefully at startup: `sce` keeps running with degraded observability defaults, logs `sce.config.invalid_config` warnings, and reserves hard failures for explicit `--config` / `SCE_CONFIG_FILE` targets or other truly invalid runtime observability inputs. -`cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*` and `pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show` / `sce config validate` text+JSON output construction to `cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config` unchanged. The CLI now has a generic borrowed `AppContext` dependency view in `cli/src/app.rs`; `AppRuntime` owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional `repo_root: Option`. `AppContext::with_repo_root(...)` / `ContextWithRepoRoot` derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in `cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps` wrap filesystem operations and `GitOps`/`ProcessGitOps` wrap git process execution plus repository-root/hooks-directory resolution. -The shared default path service in `cli/src/services/default_paths.rs` is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal `roots` seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. -The Rust CLI also centralizes SCE-owned web URI construction in `cli/src/services/agent_trace.rs`, with `SCE_WEB_BASE_URL` as the single Rust owner for `https://sce.crocoder.dev` and helpers consumed by Agent Trace conversation URLs, Agent Trace persisted trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. The config resolver separately owns `control_plane_base_url` and its `https://sce.crocoderlab.dev` baked sync default; the two URL owners must not be conflated. +`cli/src/services/config/mod.rs` is now a module facade that declares focused config submodules (`types`, `schema`, `policy`, `resolver`, private `render`, `command`, and `lifecycle`), re-exporting `pub use types::*`and`pub(crate) use schema::validate_config_file`. Shared config primitive ownership is delegated to `cli/src/services/config/types.rs`; schema loading and file parsing to `cli/src/services/config/schema.rs`; bash-policy semantic validation and policy-specific formatting to `cli/src/services/config/policy.rs`; runtime discovery/precedence to `cli/src/services/config/resolver.rs`; and `sce config show`/`sce config validate`text+JSON output construction to`cli/src/services/config/render.rs`. Downstream modules continue importing through `services::config`unchanged. The CLI now has a generic borrowed`AppContext`dependency view in`cli/src/app.rs`; `AppRuntime`owns concrete production logger/telemetry/fs/git dependencies, and command execution receives context views that borrow those dependencies plus an optional`repo_root: Option`. `AppContext::with_repo_root(...)`/`ContextWithRepoRoot`derives repo-root-scoped views while preserving the borrowed runtime dependencies, and command execution is generic over associated-type narrow accessor traits where practical. The broad capability seam lives in`cli/src/services/capabilities.rs`, where `FsOps`/`StdFsOps`wrap filesystem operations and`GitOps`/`ProcessGitOps`wrap git process execution plus repository-root/hooks-directory resolution. +The shared default path service in`cli/src/services/default_paths.rs`is now the canonical owner for production CLI path definitions. It resolves per-user config/state/cache roots through a dedicated internal`roots`seam, exposes the current persisted-artifact inventory (global config and auth tokens), and also defines named DB paths (auth DB, local DB, Agent Trace DB) plus the repo-relative, install, hook, and context-path accessors consumed across current CLI production code. Non-test production modules should consume this shared catalog instead of hardcoding owned path literals. No default cache-backed persisted artifact currently exists, so cache-root resolution remains available without speculative cache-path features and no legacy default-path fallback is supported. +The Rust CLI also centralizes SCE-owned web URI construction in`cli/src/services/agent_trace.rs`, with `SCE_WEB_BASE_URL`as the single Rust owner for`https://sce.crocoder.dev` and helpers consumed by Agent Trace conversation URLs, Agent Trace persisted trace URLs, Agent Trace session URLs, and setup-created repo-local config schema URLs. The config resolver separately owns `control_plane_base_url` and its `https://sce.crocoderlab.dev` baked sync default; the two URL owners must not be conflated. The current user-facing synchronization entrypoint is `sce sync`; references to the former nested spelling in historical records do not describe an available command. Sync owns the complete progress boundary in `cli/src/services/sync/progress.rs`: the consumer-typed `ProgressReporter` contract, no-op reporter, focused contract tests, and fixed `indicatif` terminal adapter. `SyncProgressEvent` remains owned by `cli/src/services/sync/sync.rs`; `sync/command.rs` selects the adapter or no-op implementation by output format, there is no top-level `cli/src/services/progress/` module, and JSON callers use the sync-owned no-op reporter. The same config resolver now also owns the attribution-hooks gate used by local hook runtime: opt-out env `SCE_ATTRIBUTION_HOOKS_DISABLED` overrides `policies.attribution_hooks.enabled` with inverted semantics, and the gate defaults to enabled unless explicitly disabled. The config service split now includes `cli/src/services/config/resolver.rs` as the focused owner for config-file discovery, file-layer merging, env/flag/default precedence, auth-key resolution, observability resolution, attribution-hooks resolution, and default-discovered invalid-file degradation; `cli/src/services/config/mod.rs` remains the facade/rendering orchestration surface while preserving existing `services::config` imports. -Generated config now includes repo-local OpenCode plugin assets: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves Claude `model_id` event-locally with direct metadata first and matching `transcript_path`/`tool_use_id` JSONL fallback while keeping `tool_version` direct (with no `session_models` runtime), and persists tool-prefixed `session_id` values (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), `model_id`, `tool_name`, and nullable `tool_version` into `diff_traces` through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust `sce policy bash` command: the generated OpenCode plugin at `config/.opencode/plugins/sce-bash-policy.ts` is a thin wrapper that calls `sce policy bash --input normalized --output json` via `spawnSync` and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former `bash-policy/runtime.ts` TypeScript runtime has been removed. Preset... -Claude bash-policy enforcement is also generated through `.claude/settings.json` as a `PreToolUse` `Bash` command hook running `sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call` handler blocks denied bash commands via `sce policy bash` and fails open when the policy check cannot run (see `context/sce/pi-extension-runtime.md`). -Local database bootstrap is now owned by `LocalDbLifecycle::setup` and `AgentTraceDbLifecycle::setup` aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID for diagnostics and initializes the repository-scoped `/sce/repos//agent-trace.db` with the repository schema; hook runtime lazily creates or upgrades that repository DB when setup has not run or schema metadata is incomplete. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path. `sce sync` is fully implemented: it resolves repository-scoped storage, authenticates against the control plane with stored WorkOS credentials, fetches authoritative cursors once, synchronizes the four Agent Trace capture streams concurrently while preserving sequential batches within each stream, and renders the documented concise text/JSON output (see `context/cli/sync-command.md`). The former `sce trace` command group and its database inspection surfaces are unavailable. -The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, and builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated `SCE_CLI_GENERATED_INPUT_DIR` store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting. `cli-tests`, `cli-clippy`, and `cli-fmt` remain Crane-backed check derivations. -The root flake splits native and release outputs: `packages.sce` and `packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce` / `.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` target the native binary, and `nix build .#sce-release` / `nix run .#sce-release -- ...` (plus `nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks` is the explicit long-running validation tier: `nix build .#ci-checks` builds the `.#sce-release` package and, on Linux, audits the real release binary for forbidden `/nix/store/` references, so the expensive work stays out of `nix flake check` (which never builds `.#sce-release`). -Git-commit embedding is **release-only**: `SCE_GIT_COMMIT` is injected via a `releaseCommitArgs` fragment applied only to the release derivations (`scePackageMusl` on Linux, `sceReleasePackageNative` on Darwin), not to `commonCargoArgs`. So native `.#sce`/`.#default` and every `nix flake check` derivation (`cli-tests`, `cli-clippy`, `cli-fmt`) build without the commit in their inputs and stay cache-reusable across commits (native `sce version` reports `unknown`), while `.#sce-release` still reports the real commit via `sce version`. `cli/build.rs` `emit_git_commit` emits `SCE_GIT_COMMIT` only when the env var is explicitly set — no `git rev-parse` fallback and no `.git/HEAD`/`.git/packed-refs` rerun watches. On Darwin the release now uses a distinct native-toolchain derivation (native toolchain + commit), so it diverges from `.#sce` to carry the commit while native stays commit-independent. -The default development shell is slimmed for fast iteration: `devShells.default` no longer includes `scePackage` or `tursoPackage`, so `nix develop` compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain and JS/pkl tooling for `cargo`/`biome`/`pkl` work. Turso stays available as `packages..turso` and through a new opt-in `devShells..database` shell (default tools + `tursoPackage`), entered via `nix develop .#database`. Both shells share `defaultDevShellPackages`/`defaultDevShellHook` `let` bindings so they cannot drift. -The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked` and local checkout installation through `./scripts/run-cli-cargo.sh install --path cli --locked`. Direct `cargo install --git` is unsupported because it cannot run the repository pre-Cargo producer. The published crate installs the `sce` binary. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. -The repository-root flake is the single Nix entrypoint for repo tooling and CLI packaging/checks, so root-level `nix flake check` evaluates the Crane-backed CLI checks (`cli-tests`, `cli-clippy`, `cli-fmt`), the ephemeral `pkl-generated` inventory check, Linux-only Flatpak checks, `workflow-actionlint`, and the split npm/config-lib JavaScript checks without nested-flake indirection. Repository Cargo builds copy a validated pre-Cargo generated payload into Cargo `OUT_DIR`; crates.io packaging prepares a self-contained Pkl-free fallback in a temporary clean workspace, and Flatpak helpers prepare the same payload beside generated manifests before the sandboxed source build. No general-purpose `cli/assets/generated/` mirror or committed generated target tree remains. -Config-lib JS flake checks execute from `config/lib/`, but the copied Nix check source is repo-shaped when tests require shared repo fixtures; the current Claude agent-trace golden tests are fully Rust-owned in `cli/src/services/structured_patch/fixtures` (Claude TypeScript plugin test removed in T07). -Local developer Nix tuning guidance now lives in `AGENTS.md`, including optional user-level `~/.config/nix/nix.conf` recommendations for `max-jobs` and `cores` plus an explicit system-level-only note for `auto-optimise-store`. -The Pkl authoring layer owns generated OpenCode plugin registration for SCE-managed plugins: `config/pkl/base/opencode.pkl` defines the canonical plugin entries, `config/pkl/renderers/common.pkl` re-exports the shared plugin list for renderer use, and generated `config/.opencode/opencode.json` registers `./plugins/sce-bash-policy.ts` and `./plugins/sce-agent-trace.ts` through OpenCode's `plugin` field. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated `.claude/settings.json` as a `PreToolUse` `Bash` command hook routed through `.claude/hooks/run-sce-or-show-install-guidance.sh` before running `sce policy bash`. -The current CLI install/distribution contract for `sce` includes repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`) as supported channels, while `Homebrew` remains deferred from the current implementation stage. Nix-managed build/release entrypoints are the source of truth for existing binary rollout surfaces, npm consumes Nix-produced release artifacts, and repo-root `.version` is the canonical checked-in release version source that release packaging and downstream Cargo/npm publication must match. Flatpak is the approved source-built exception to binary artifact reuse: its package builds the Rust CLI from source inside Flatpak, uses a Flathub-style release-source manifest plus a Nix-generated local checkout override, and receives an ephemeral checksummed package fallback prepared by the Nix-side helper because Pkl is unavailable in the Flatpak build sandbox. Runtime Git access still uses a `/app/bin/git` wrapper delegating to `flatpak-spawn --host git` with the required `org.freedesktop.Flatpak` permission. The active Flatpak release contract approves GitHub Release source-manifest assets (manifest tarball, checksum, and JSON metadata) and source-built `.flatpak` bundle assets (`sce-v-x86_64.flatpak` / `sce-v-aarch64.flatpak` plus `.sha256` / `.json`), with `.github/workflows/release-sce.yml` building/uploading those assets alongside CLI/npm assets, while still excluding automatic Flathub submission, prebuilt (non-source-built) Flatpak binaries/bundles, OSTree repositories, and release-version bumping. The shared release artifact foundation is now implemented through root-flake apps `release-artifacts` and `release-manifest`, which emit canonical `sce-v-.tar.gz` archives, SHA-256 checksum files, merged manifest outputs, and a detached `sce-v-release-manifest.json.sig` produced from a non-repo private signing key; the npm distribution surface is now implemented as a checked-in `npm/` launcher package plus root-flake `release-npm-package`, which packs `sce-v-npm.tgz`, refuses mismatched checked-in package metadata, and installs the native CLI by downloading the release manifest plus detached signature, verifying the manifest with the bundled npm public key, and only then checksum-verifying the matching GitHub release archive at npm `postinstall` time. GitHub Releases remain the canonical publication surface for binary release artifacts and approved Flatpak source-manifest package assets, while crates.io and npm registry publication are separate non-bumping publish stages under the approved release topology. GitHub CLI release automation now lives in dedicated `release-sce*.yml` workflows split by Linux, Linux ARM, and macOS ARM, and `.github/workflows/release-sce.yml` now orchestrates those three reusable platform lanes before assembling the signed release manifest, npm tarball, and GitHub release payload. The orchestrator tags/releases the checked-in `.version` directly and rejects version mismatches instead of generating a new semver during workflow execution; `.github/workflows/publish-crates.yml` and `.github/workflows/publish-npm.yml` own registry publication after release assets exist. -The Linux root flake now also exposes `nix run .#release-flatpak-package -- --version --out-dir `, delegating to `packaging/flatpak/sce-flatpak.sh release-package` to emit deterministic Flatpak source-manifest tarball/checksum/JSON release assets from checked-in packaging source while running the Nix-built version-parity validator script across `.version`, `cli/Cargo.toml`, `npm/package.json`, and AppStream release metadata; `.github/workflows/release-sce.yml` runs that app into `dist/flatpak` and uploads `*.tar.gz`, `*.sha256`, and `*.json` Flatpak assets to the GitHub Release. Linux root flake also exposes `nix run .#release-flatpak-bundle -- --version --arch --out-dir `, delegating to `sce-flatpak.sh release-bundle` to build a source-built `.flatpak` bundle from the checkout using imperative `flatpak-builder` + `flatpak build-bundle` (network + bubblewrap, kept out of pure Nix), emitting per-architecture `.flatpak`/`.sha256`/`.json` files; `.github/workflows/release-sce-linux.yml` and `.github/workflows/release-sce-linux-arm.yml` build and upload x86_64/aarch64 bundles respectively, assembled by `.github/workflows/release-sce.yml`. -The checked-in Flatpak packaging surface lives under `packaging/flatpak/` with Nix-owned generation: `dev.crocoder.sce.yml` is rendered from a Nix expression (`nix/flatpak/manifest.nix`) via the standard nixpkgs YAML formatter (`pkgs.formats.yaml.generate`) and regenerated by `nix run .#regenerate-flatpak-manifest`; `cargo-sources.json` is generated from `cli/Cargo.lock` by a Nix derivation wrapping `flatpak-builder-tools`/`flatpak-cargo-generator.py` and regenerated by `nix run .#regenerate-cargo-sources`; both are guarded by `flatpak-manifest-parity` and `cargo-sources-parity` flake checks. Static manifest validation is Bash-owned (`nix/flatpak/static-validate.sh`), and release-version parity validation is Bash-owned (`nix/flatpak/version-parity.sh`). AppStream metadata and the host-git wrapper source remain checked in, and `sce-flatpak.sh` is a thin imperative orchestrator (no manifest text rewriting, no embedded Python) around `flatpak-builder` and `flatpak build-bundle`, consumed by the reduced flake app surface and by Flatpak source-manifest release packaging. +Generated config now includes repo-local OpenCode plugin assets: `sce-bash-policy.ts` plus `sce-agent-trace.ts` are emitted under `config/.opencode/plugins/`; the OpenCode agent-trace plugin extracts `{ sessionID, diff, time, model_id }` from user `message.updated` events with diffs, tracks per-session OpenCode client version from `session.created`/`session.updated`, and sends payloads to `sce hooks diff-trace` with `tool_name="opencode"` plus optional `tool_version`. Claude generated config now routes supported `PostToolUse Write|Edit|MultiEdit|NotebookEdit` events directly to `sce hooks diff-trace`; it no longer registers a `SessionStart` hook or calls `sce hooks session-model`. Rust handles extraction, validation, and persistence without a TypeScript intermediary; the former `config/.claude/plugins/sce-agent-trace.ts` Bun runtime was removed in T07 of the `claude-rust-diff-trace` plan. The Rust hook validates required fields, resolves Claude `model_id` event-locally with direct metadata first and matching `transcript_path`/`tool_use_id` JSONL fallback while keeping `tool_version` direct (with no `session_models` runtime), and persists tool-prefixed `session_id` values (`oc*`for OpenCode,`cc*`for Claude,`pi*`for Pi),`model_id`, `tool_name`, and nullable `tool_version`into`diff_traces`through AgentTraceDb. Bash-policy now delegates OpenCode enforcement to the Rust`sce policy bash`command: the generated OpenCode plugin at`config/.opencode/plugins/sce-bash-policy.ts`is a thin wrapper that calls`sce policy bash --input normalized --output json`via`spawnSync`and throws on deny decisions; it no longer contains independent TypeScript policy logic. The former`bash-policy/runtime.ts`TypeScript runtime has been removed. Preset... +Claude bash-policy enforcement is also generated through`.claude/settings.json`as a`PreToolUse` `Bash`command hook running`sce policy bash`, so Claude and OpenCode both delegate to the Rust policy evaluator without a Claude TypeScript runtime. Pi bash-policy enforcement is delegated the same way through a project-local Pi extension (`config/lib/pi-plugin/sce-pi-extension.ts`, emitted to `config/.pi/extensions/sce/index.ts`) whose `tool_call`handler blocks denied bash commands via`sce policy bash`and fails open when the policy check cannot run (see`context/sce/pi-extension-runtime.md`). +Local database bootstrap is now owned by `LocalDbLifecycle::setup`and`AgentTraceDbLifecycle::setup`aggregated by the setup command. Agent Trace setup creates/reuses the current checkout ID for diagnostics and initializes the repository-scoped`/sce/repos//agent-trace.db`with the repository schema; hook runtime lazily creates or upgrades that repository DB when setup has not run or schema metadata is incomplete. Doctor validates the repository-scoped DB path/health and can bootstrap missing parent directories; outside a Git repository it reports an actionable "requires a Git repository" diagnostic instead of probing a sentinel path.`sce sync`is fully implemented: it resolves repository-scoped storage, authenticates against the control plane with stored WorkOS credentials, fetches authoritative cursors once, synchronizes the four Agent Trace capture streams concurrently while preserving sequential batches within each stream, and renders the documented concise text/JSON output (see`context/cli/sync-command.md`). The former `sce trace` command group and its database inspection surfaces are unavailable. +The repository-root flake (`flake.nix`) applies a Rust overlay-backed stable toolchain pinned to `1.95.0`(with`rustfmt`and`clippy`), reads package/check version from the repo-root `.version`file, and builds`packages.sce`through a Crane`buildDepsOnly`+`buildPackage`pipeline. One deterministic pre-Cargo Nix derivation invokes the shared generated-input producer and supplies its validated`SCE_CLI_GENERATED_INPUT_DIR`store path to native, release, test, and Clippy Cargo derivations. Pkl is absent from those Cargo environments; dependency-only and format derivations do not receive the handoff, so canonical generation changes invalidate compiling outputs without invalidating dependency artifacts or formatting.`cli-tests`, `cli-clippy`, and `cli-fmt`remain Crane-backed check derivations. +The root flake splits native and release outputs:`packages.sce`and`packages.default` build the **native** development binary (`scePackage`), while `packages.sce-release` builds the release binary (`sceReleasePackage`: static musl on Linux, native on Darwin). So `nix build .#sce`/`.#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering`target the native binary, and`nix build .#sce-release`/`nix run .#sce-release -- ...`(plus`nix run .#release-artifacts`, which builds `.#sce-release`) target the release binary. On Linux the native and release outputs are distinct store paths, and the release output passes the native portability audit. `packages..ci-checks`is the explicit long-running validation tier:`nix build .#ci-checks`builds the`.#sce-release`package and, on Linux, audits the real release binary for forbidden`/nix/store/`references, so the expensive work stays out of`nix flake check`(which never builds`.#sce-release`). +Git-commit embedding is **release-only**: `SCE_GIT_COMMIT`is injected via a`releaseCommitArgs` fragment applied only to the release derivations (`scePackageMusl`on Linux,`sceReleasePackageNative`on Darwin), not to`commonCargoArgs`. So native `.#sce`/`.#default`and every`nix flake check` derivation (`cli-tests`, `cli-clippy`, `cli-fmt`) build without the commit in their inputs and stay cache-reusable across commits (native `sce version`reports`unknown`), while `.#sce-release`still reports the real commit via`sce version`. `cli/build.rs` `emit_git_commit`emits`SCE_GIT_COMMIT`only when the env var is explicitly set — no`git rev-parse`fallback and no`.git/HEAD`/`.git/packed-refs`rerun watches. On Darwin the release now uses a distinct native-toolchain derivation (native toolchain + commit), so it diverges from`.#sce`to carry the commit while native stays commit-independent. +The default development shell is slimmed for fast iteration:`devShells.default`no longer includes`scePackage`or`tursoPackage`, so `nix develop`compiles neither the CLI package nor the Turso CLI — it provides only the Rust toolchain and JS/pkl tooling for`cargo`/`biome`/`pkl`work. Turso stays available as`packages..turso`and through a new opt-in`devShells..database`shell (default tools +`tursoPackage`), entered via `nix develop .#database`. Both shells share `defaultDevShellPackages`/`defaultDevShellHook` `let`bindings so they cannot drift. +The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in`cli/README.md`; supported Cargo install paths are `cargo install shared-context-engineering --locked`and local checkout installation through`./scripts/run-cli-cargo.sh install --path cli --locked`. Direct `cargo install --git`is unsupported because it cannot run the repository pre-Cargo producer. The published crate installs the`sce`binary. The crate also keeps`cargo clippy --manifest-path cli/Cargo.toml`warnings-denied through`cli/Cargo.toml`lint configuration, so an extra`-- -D warnings`flag is redundant. +The repository-root flake is the single Nix entrypoint for repo tooling and CLI packaging/checks, so root-level`nix flake check` evaluates the Crane-backed CLI checks (`cli-tests`, `cli-clippy`, `cli-fmt`), the ephemeral `pkl-generated`inventory check, Linux-only Flatpak checks,`workflow-actionlint`, and the split npm/config-lib JavaScript checks without nested-flake indirection. Repository Cargo builds copy a validated pre-Cargo generated payload into Cargo `OUT_DIR`; crates.io packaging prepares a self-contained Pkl-free fallback in a temporary clean workspace, and Flatpak helpers prepare the same payload beside generated manifests before the sandboxed source build. No general-purpose `cli/assets/generated/`mirror or committed generated target tree remains. +Config-lib JS flake checks execute from`config/lib/`, but the copied Nix check source is repo-shaped when tests require shared repo fixtures; the current Claude agent-trace golden tests are fully Rust-owned in `cli/src/services/structured_patch/fixtures`(Claude TypeScript plugin test removed in T07). +Local developer Nix tuning guidance now lives in`AGENTS.md`, including optional user-level `~/.config/nix/nix.conf`recommendations for`max-jobs`and`cores`plus an explicit system-level-only note for`auto-optimise-store`. +The Pkl authoring layer owns generated OpenCode plugin registration for SCE-managed plugins: `config/pkl/base/opencode.pkl`defines the canonical plugin entries,`config/pkl/renderers/common.pkl`re-exports the shared plugin list for renderer use, and generated`config/.opencode/opencode.json`registers`./plugins/sce-bash-policy.ts`and`./plugins/sce-agent-trace.ts`through OpenCode's`plugin`field. Claude does not use an OpenCode-style plugin manifest; Claude bash-policy enforcement is registered through generated`.claude/settings.json`as a`PreToolUse` `Bash`command hook routed through`.claude/hooks/run-sce-or-show-install-guidance.sh`before running`sce policy bash`. +The current CLI install/distribution contract for `sce` includes repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`) as supported channels, while `Homebrew`remains deferred from the current implementation stage. Nix-managed build/release entrypoints are the source of truth for existing binary rollout surfaces, npm consumes Nix-produced release artifacts, and repo-root`.version`is the canonical checked-in release version source that release packaging and downstream Cargo/npm publication must match. Flatpak is the approved source-built exception to binary artifact reuse: its package builds the Rust CLI from source inside Flatpak, uses a Flathub-style release-source manifest plus a Nix-generated local checkout override, and receives an ephemeral checksummed package fallback prepared by the Nix-side helper because Pkl is unavailable in the Flatpak build sandbox. Runtime Git access still uses a`/app/bin/git`wrapper delegating to`flatpak-spawn --host git`with the required`org.freedesktop.Flatpak`permission. The active Flatpak release contract approves GitHub Release source-manifest assets (manifest tarball, checksum, and JSON metadata) and source-built`.flatpak` bundle assets (`sce-v-x86_64.flatpak`/`sce-v-aarch64.flatpak`plus`.sha256`/`.json`), with `.github/workflows/release-sce.yml`building/uploading those assets alongside CLI/npm assets, while still excluding automatic Flathub submission, prebuilt (non-source-built) Flatpak binaries/bundles, OSTree repositories, and release-version bumping. The shared release artifact foundation is now implemented through root-flake apps`release-artifacts`and`release-manifest`, which emit canonical `sce-v-.tar.gz`archives, SHA-256 checksum files, merged manifest outputs, and a detached`sce-v-release-manifest.json.sig`produced from a non-repo private signing key; the npm distribution surface is now implemented as a checked-in`npm/`launcher package plus root-flake`release-npm-package`, which packs `sce-v-npm.tgz`, refuses mismatched checked-in package metadata, and installs the native CLI by downloading the release manifest plus detached signature, verifying the manifest with the bundled npm public key, and only then checksum-verifying the matching GitHub release archive at npm `postinstall`time. GitHub Releases remain the canonical publication surface for binary release artifacts and approved Flatpak source-manifest package assets, while crates.io and npm registry publication are separate non-bumping publish stages under the approved release topology. GitHub CLI release automation now lives in dedicated`release-sce*.yml`workflows split by Linux, Linux ARM, and macOS ARM, and`.github/workflows/release-sce.yml`now orchestrates those three reusable platform lanes before assembling the signed release manifest, npm tarball, and GitHub release payload. The orchestrator tags/releases the checked-in`.version`directly and rejects version mismatches instead of generating a new semver during workflow execution;`.github/workflows/publish-crates.yml`and`.github/workflows/publish-npm.yml`own registry publication after release assets exist. +The Linux root flake now also exposes`nix run .#release-flatpak-package -- --version --out-dir `, delegating to `packaging/flatpak/sce-flatpak.sh release-package`to emit deterministic Flatpak source-manifest tarball/checksum/JSON release assets from checked-in packaging source while running the Nix-built version-parity validator script across`.version`, `cli/Cargo.toml`, `npm/package.json`, and AppStream release metadata; `.github/workflows/release-sce.yml`runs that app into`dist/flatpak`and uploads`*.tar.gz`, `*.sha256`, and `\_.json`Flatpak assets to the GitHub Release. Linux root flake also exposes`nix run .#release-flatpak-bundle -- --version --arch --out-dir `, delegating to `sce-flatpak.sh release-bundle`to build a source-built`.flatpak`bundle from the checkout using imperative`flatpak-builder`+`flatpak build-bundle`(network + bubblewrap, kept out of pure Nix), emitting per-architecture`.flatpak`/`.sha256`/`.json`files;`.github/workflows/release-sce-linux.yml`and`.github/workflows/release-sce-linux-arm.yml`build and upload x86_64/aarch64 bundles respectively, assembled by`.github/workflows/release-sce.yml`. +The checked-in Flatpak packaging surface lives under `packaging/flatpak/`with Nix-owned generation:`dev.crocoder.sce.yml` is rendered from a Nix expression (`nix/flatpak/manifest.nix`) via the standard nixpkgs YAML formatter (`pkgs.formats.yaml.generate`) and regenerated by `nix run .#regenerate-flatpak-manifest`; `cargo-sources.json`is generated from`cli/Cargo.lock`by a Nix derivation wrapping`flatpak-builder-tools`/`flatpak-cargo-generator.py`and regenerated by`nix run .#regenerate-cargo-sources`; both are guarded by `flatpak-manifest-parity`and`cargo-sources-parity` flake checks. Static manifest validation is Bash-owned (`nix/flatpak/static-validate.sh`), and release-version parity validation is Bash-owned (`nix/flatpak/version-parity.sh`). AppStream metadata and the host-git wrapper source remain checked in, and `sce-flatpak.sh`is a thin imperative orchestrator (no manifest text rewriting, no embedded Python) around`flatpak-builder`and`flatpak build-bundle`, consumed by the reduced flake app surface and by Flatpak source-manifest release packaging. The current supported automated release target matrix is `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, and `aarch64-apple-darwin`; npm launcher platform support remains a separate current-state surface documented in the npm distribution contract and launcher code. + - Native release binary portability auditing is exposed as `nix run .#native-portability-audit -- --binary [--platform auto|linux|macos]` plus the `native-portability-audit` flake check; it reports forbidden `/nix/store/` runtime references found by Linux ELF/string inspection or macOS `otool -L` install-name inspection. `release-artifacts` runs that audit against the staged `bin/sce` before tarball creation and, on macOS, rewrites Nix-store `libiconv.*.dylib` install names to `/usr/lib/...` with ad-hoc re-signing before the audit. The three native reusable release workflows also extract the generated archive, smoke-run `bin/sce version --format json`, and rerun the native portability audit before uploading native artifacts. -The downstream publish-stage implementation is now complete for both registries: `.github/workflows/publish-crates.yml` publishes the checked-in crate version after `.version`/tag/Cargo parity checks, and `.github/workflows/publish-npm.yml` publishes the checked-in npm package after `.version`/tag/npm parity checks plus verification of the canonical `sce-v-npm.tgz` GitHub release asset. -The repository root now also owns the canonical Biome contract for the current JavaScript tooling slice: `biome.json` scopes formatting/linting to `npm/` and the shared `config/lib/` plugin package root while excluding package-local `node_modules/`, and the root Nix dev shell provides the `biome` binary so contributors do not need a host-installed formatter/linter for those areas. -Flatpak validation/build orchestration is reduced to a minimal app surface: Linux flake apps expose the umbrella `sce-flatpak` (`nix run .#sce-flatpak -- ` for `validate`, `prepare-local-manifest`, etc.) plus `release-flatpak-package`, `release-flatpak-bundle`, and the `regenerate-flatpak-manifest` / `regenerate-cargo-sources` helpers; the previously separate `flatpak-validate`, `flatpak-local-manifest`, and `flatpak-build` wrapper apps are removed. Default `nix flake check` keeps the lightweight Nix-built static/AppStream validator plus the parity checks (`flatpak-manifest-parity`, `cargo-sources-parity`) and does not run a network-heavy Flatpak build. The former standalone install-channel integration runner and `install-channel-integration-tests` flake app are not active current-state surfaces. -Shared Context Plan and Shared Context Code remain separate OpenCode routing roles: the generated Plan agent routes only to `/change-to-plan`, while the generated Code agent routes to `/next-task`, `/validate`, `/commit`, `/handover`, and `/brownfield`. Workflow behavior lives in the six workflow entrypoints and their six skill packages rather than in agent bodies. `config/pkl/base/workflow-catalog.pkl` assigns each workflow to its role, and OpenCode command routing plus each agent's ordered `skill:` permissions derive from those records: ordinary non-SCE skills are allowed by the wildcard, arbitrary `sce-*` skills are denied, and only the role's owned workflows are allowed after that deny — `sce-change-to-plan` for Plan; `sce-next-task`, `sce-validate`, `sce-commit`, `sce-handover`, and `sce-brownfield` for Code. The Code agent additionally allows `sce-decision` for task synchronization. -The canonical workflow definitions remain phase-decomposed as authoring source: `/change-to-plan` sequences `sce-context-load` then `sce-plan-authoring`; `/next-task` sequences `sce-plan-review`, `sce-task-execution`, and `sce-task-context-sync`; `/validate` runs `sce-validation` only and reports its Validation Report; `/commit` delegates staged-diff analysis and message generation to `sce-atomic-commit`; `/handover` has no phases, since writer and loader mode has no SCE sibling handoff or wait mid-run; `/brownfield` likewise has none, since its single skill owns investigation, the blocking clarification gate, writing, and reporting itself. Relevant non-SCE skills may help inside an active workflow step, but they return control to that step without changing its canonical invariants. No target generates those phase modules as packages. All three consume them as inputs to the shared `workflow-composite.pkl` renderer, which composes each workflow into one skill package. Every workflow supplies typed package/composite render values for frontmatter, bodies, semantic references, phases, persisted-document formats where applicable, and output references; the composite renderer performs no prose-wide internalization or frontmatter stripping. -Every target preserves the same gates and lifecycle semantics through six renderer-composed workflow packages: `sce-change-to-plan`, `sce-next-task`, `sce-validate`, `sce-commit`, `sce-handover`, and `sce-brownfield`. Each thin command or Pi prompt invokes exactly one corresponding skill, and OpenCode command frontmatter names that single skill as both `entry-skill` and the whole `skills` chain. Each phase-based package keeps control flow, internal status branching, waits, and same-session resume in `SKILL.md`, while package-local Markdown references own phase instructions and persisted-document formats; `references/output.md` remains the sole definition of human-visible gates and terminal Markdown. Phase-free `/handover` retains `SKILL.md`, `references/handover-template.md`, and `references/output.md`, while `/brownfield` retains `SKILL.md` plus `references/output.md`. No target emits phase-skill packages or inter-skill machine contracts; phase statuses stay internal to one skill invocation. -Context sync uses an important-change gate: cross-cutting/policy/architecture/terminology changes require root shared-file edits, while localized tasks run verify-only root checks without default churn. -OpenCode and Claude no longer generate legacy bootstrap or context-sync skills; `/commit` and `/handover` are generated only as catalog-registered composite workflow packages. OpenCode retains only thin routing agents, while Claude emits no agents. The superseded grouped Markdown catalog and automated OpenCode profile have been removed from Pkl ownership and generated outputs. -The prior no-git-wrapper Agent Trace design artifacts under `context/sce/agent-trace-*.md` are retained only as historical reference; the current CLI runtime no longer wires the removed Agent Trace schema adaptation, payload building, retry replay, or rewrite handling paths into local hook execution. -The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time, then assigning each structured hunk the persisted row model and every structured touched line the persisted canonical `cc_...` row session), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. -The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime now matches the implemented T06 slice for `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, and bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path. -The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local diff-trace attribution without a `session_models` API/table dependency. -The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, remains the active bounded recent-diff-trace intersection path, and after successful Agent Trace persistence optionally launches the detached sync-owned `sync --format json` child when config-file-only `agent_trace.auto_sync` is true; `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. -The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. -The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. -The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. + The downstream publish-stage implementation is now complete for both registries: `.github/workflows/publish-crates.yml` publishes the checked-in crate version after `.version`/tag/Cargo parity checks, and `.github/workflows/publish-npm.yml` publishes the checked-in npm package after `.version`/tag/npm parity checks plus verification of the canonical `sce-v-npm.tgz` GitHub release asset. + The hooks service now uses a minimal attribution-only runtime: `commit-msg` is the only hook that mutates behavior, conditionally injecting exactly one canonical SCE trailer when the attribution-hooks gate is enabled, `SCE_DISABLED` is false, and the staged-diff AI-overlap preflight confirms AI/editor evidence (`StagedDiffAiOverlapResult::Overlap`); when the preflight returns `NoOverlap` or `Error` (including DB open failure, schema not ready, query error, staged diff read failure, or zero overlap), the trailer is not appended and errors are logged via `sce.hooks.commit_msg.ai_overlap_error`; `pre-commit` and `post-rewrite` remain deterministic no-op entrypoints; `post-commit` requires validated `--remote-url`, threads that URL through the Agent Trace flow, prints it to stderr, captures current commit patch, queries recent `diff_traces` from past 7 days (dispatching `patch` rows through existing unified-diff parsing and `structured` rows through `structured_patch::derive_claude_structured_patch` at read time, then assigning each structured hunk the persisted row model and every structured touched line the persisted canonical `cc_...` row session), combines/intersects patches, persists intersection metadata to `post_commit_patch_intersections`, and persists the schema-validated built Agent Trace payload, including optional top-level `tool` metadata from recent diff-trace rows, top-level `metadata.sce.version` from the compiled `sce` CLI package version, and range-level `content_hash` values, to AgentTraceDb `agent_traces` (DB-only, no post-commit Agent Trace file artifact); `diff-trace` currently validates/persists required non-empty `sessionID`/`diff`/`tool_name`, optional `model_id` (absent or `null` → `None`, present+non-empty → `Some`, present+empty → error), required nullable/non-empty `tool_version`, plus required `u64` millisecond `time`, uses direct-first/event-transcript-second Claude `model_id` resolution plus direct `tool_version` without any `session_models` runtime, and continues with `None` when event-local lookup cannot resolve attribution, with same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. Claude structured `PostToolUse` diff-trace intake prefers direct top-level or nested model metadata, then matches the event's `tool_use_id` in its JSONL `transcript_path`, normalizes either source once with the `claude/` prefix, and fails open to `None`. + The CLI now also includes an approved operator-environment doctor contract documented in `context/sce/agent-trace-hook-doctor.md`; the runtime includes `sce doctor --fix` parsing/help, stable problem/fix-result reporting, canonical hook-repair reuse, bounded doctor-owned local-DB directory bootstrap for the missing SCE-owned DB parent path, and target-scoped integration inventory: Claude reports only `Plugins`, `Commands`, and `Skills`, while OpenCode reports `Plugins`, `Agents`, `Commands`, and `Skills`. Its non-launching post-commit Agent Trace auto-sync fact reports enabled/current, explicit disabled, not-ready, and not-applicable states using canonical managed-block currency and resolved configuration; existing hook remediation and readiness semantics remain unchanged. + The local DB service now provides `LocalDb` as a thin `TursoDb` alias in `cli/src/services/local_db/mod.rs`; `LocalDbSpec` resolves the canonical local DB path from the shared default-path catalog and currently declares zero migrations. Shared Turso infrastructure lives in `cli/src/services/db/mod.rs`, where `DbSpec` and generic `TursoDb` support local or remote sync-mode opens, parent-directory creation, connection setup, synchronous query helpers, embedded migration execution, and shared DB lifecycle helpers. Auth DB persistence uses encrypted `AuthDb = EncryptedTursoDb` and token storage persists credentials through the `auth_credentials` table. Agent Trace persistence uses the sole `RepositoryAgentTraceDb = TursoDb` adapter at `/sce/repos//agent-trace.db`, with a one-file repository schema for `repository_metadata`, `diff_traces`, `post_commit_patch_intersections`, `agent_traces`, `messages`, `parts`, indexes/triggers, and no `checkout_id` columns on trace rows. The checkout-scoped `AgentTraceDb = TursoDb` adapter, its `agent_trace_db_path()`/`agent_trace_db_path_for_checkout()` helpers, and the 15-file `cli/migrations/agent-trace/` chain were removed by the `retire-legacy-agent-trace-db` plan; active hook runtime writes nullable event-local diff-trace attribution without a `session_models` API/table dependency. + The hooks command surface now also supports concrete runtime subcommand routing (`pre-commit`, `commit-msg`, `post-commit`, `post-rewrite`, `diff-trace`, and `conversation-trace`) with deterministic argument/STDIN validation; `session-model` is no longer supported. Current runtime behavior keeps commit-msg attribution enabled by default unless explicitly opted out: the attribution gate enables canonical trailer insertion in `commit-msg` only when the staged-diff AI-overlap preflight confirms AI/editor evidence (no trailer is appended when the preflight finds no overlap or encounters any error); `pre-commit`/`post-rewrite` remain deterministic no-ops, `post-commit` requires validated `--remote-url`, threads that URL into the Agent Trace flow, prints it to stderr, remains the active bounded recent-diff-trace intersection path, and after successful Agent Trace persistence optionally launches the detached sync-owned `sync --format json` child when config-file-only `agent_trace.auto_sync` is true; `diff-trace` is the active intake path for parsed STDIN `{ sessionID, diff, time, model_id?, tool_name, tool_version }` payload persistence with optional `model_id`, required non-empty `tool_name`, required nullable/non-empty `tool_version`, direct-first/event-transcript-second Claude `model_id` plus direct `tool_version` values (no session-model fallback or cache), required `u64` millisecond `time`, same-tool-idempotent stored `session_id` prefixing (`oc_` for OpenCode, `cc_` for Claude, `pi_` for Pi), non-lossy AgentTraceDb `time_ms` conversion, and no parsed-payload artifact fallback. This behavior is documented in `context/sce/agent-trace-hooks-command-routing.md`. The removed `sce hooks claude-capture` raw capture route is documented in `context/sce/claude-raw-hook-capture.md` as a removed feature. + The setup service now also exposes deterministic required-hook embedded asset accessors (`iter_required_hook_assets`, `get_required_hook_asset`) backed by canonical templates in `cli/assets/hooks/` for `pre-commit`, `commit-msg`, and `post-commit`; this behavior is documented in `context/sce/setup-githooks-hook-asset-packaging.md`. + The setup service now also includes required-hook install orchestration (`install_required_git_hooks`) that resolves repository root and effective hooks path from git truth, computes the bytes to stage by merging the canonical hook template with any existing hook (preserving a foreign hook's content as an exact prefix with the SCE managed block appended, or bringing an SCE-owned block current in place) rather than writing canonical bytes verbatim, enforces deterministic per-hook outcomes (`Installed`/`Updated`/`Skipped`) against that merged content, surfaces a deterministic advisory when an appended block would be unreachable, and uses a unified atomic-swap policy that renames staged content directly over existing hooks without unlinking them first, with deterministic recovery guidance on swap failures; this behavior is documented in `context/sce/setup-githooks-install-flow.md`. + The setup command parser/dispatch now also supports composable setup+hooks runs (`sce setup --opencode|--claude|--pi|--all --hooks`) plus hooks-only mode (`sce setup --hooks` with optional `--repo `), enforces deterministic compatibility validation (`--repo` requires `--hooks`; target flags remain mutually exclusive), and emits deterministic setup/hook outcome messaging (`installed`/`updated`/`skipped`); this behavior is documented in `context/sce/setup-githooks-cli-ux.md`. ## Repository model diff --git a/context/plans/compact-doctor-output.md b/context/plans/compact-doctor-output.md new file mode 100644 index 00000000..681a62ef --- /dev/null +++ b/context/plans/compact-doctor-output.md @@ -0,0 +1,539 @@ +# Plan: compact-doctor-output + +## Change summary + +Improve the default human-readable `sce doctor` report so it is a compact, +domain-oriented health summary instead of an inventory of every successful file +check. The change is a presentation redesign over the existing doctor report: +diagnosis remains complete and read-only, while healthy paths, IDs, hashes, and +individual integration files are suppressed in text mode. Failed or warning +nodes retain the relevant path, state, diagnostic, and remediation details +already produced by the doctor checks. + +The existing `--format json` report and `sce doctor --fix` repair behavior remain +available. This plan does not add a new CLI flag: there is no existing doctor +verbose/debug mode, and JSON already provides the complete machine-readable +detail needed for troubleshooting and automation. A future verbose text mode +can be added independently if operators demonstrate a need for expanded +successful checks. + +## Current architecture + +- `cli/src/cli_schema.rs` defines `doctor { --fix, --format text|json }`; no + `--verbose` or debug output mode exists. Clap conversion is in + `cli/src/services/parse/command_runtime.rs`, and + `cli/src/services/doctor/command.rs` is the thin runtime adapter. +- `cli/src/services/doctor/mod.rs` resolves the repository root, creates a + repo-scoped context, invokes the shared `lifecycle_providers(true)` catalog, + adapts lifecycle results into doctor-owned problems, builds a report, and + delegates text/JSON rendering. +- `cli/src/services/lifecycle.rs` owns provider-neutral `HealthProblem` data: + kind, category, severity, fixability, summary, remediation, and next action. + Config, local DB, auth DB, Agent Trace DB, and Git-hook providers produce + these results. Their checks must not become terminal-rendering logic. +- `cli/src/services/doctor/types.rs` owns `HookDoctorReport`, location and + identity facts, hook health, flat `IntegrationGroupHealth` records, flat + integration child records, and doctor problem/result enums. +- `cli/src/services/doctor/inspect.rs` gathers the report facts and performs + integration inventory checks. Integration groups are currently emitted as + flat labels such as `ClaudeCode skills`, with one child for every embedded + installed asset. Existing optional-workflow filtering and content/merge + validation are the source of truth and must remain unchanged. +- `cli/src/services/doctor/render.rs` currently renders the fixed sections + `Environment`, `Configuration`, `Repository`, `Git Hooks`, and + `Integrations`; every successful path-backed row, identity value, and + integration child path is printed. It also renders existing problem counts, + fix results, and the complete JSON payload. +- Paths and metadata originate in the report facts collected by `inspect.rs` + (`state_root`, config locations, repository/hooks paths, checkout identity, + Agent Trace DB identity/path, and integration child paths), while failure + paths and messages also originate in provider `HealthProblem.summary` and + `remediation` strings. Integration child state supplies missing, mismatch, + and read-error classification. + +## Proposed output model + +### Domain hierarchy + +Use a render-only diagnostic tree with this text-mode order: + +```text +SCE doctor + +Environment + [STATUS] State + [STATUS] Configuration + [STATUS] Repository identity + +Repository + [STATUS] Git repository + [STATUS] Git hooks + +Integrations + Claude Code + [STATUS] Plugins + [STATUS] Agents + [STATUS] Commands + [STATUS] Skills + + OpenCode + [STATUS] Plugins + [STATUS] Agents + [STATUS] Commands + [STATUS] Skills + + Pi + [STATUS] Extensions + [STATUS] Prompts + [STATUS] Skills +``` + +Only configured/detected integration targets are rendered, preserving the +existing target-resolution behavior. The implementation should use typed target +and area metadata for integration groups rather than recovering hierarchy by +parsing labels such as `ClaudeCode skills`. Existing terminology is normalized +for display (`Claude Code`, `Plugins`, `Extensions`) without changing the +underlying setup asset paths or target IDs. + +`State` summarizes state-root and local/auth database readiness. `Configuration` +summarizes global and local config validation. `Repository identity` summarizes +checkout identity and repository-scoped Agent Trace identity/database health. +`Git hooks` summarizes the effective hooks directory plus the required hook +rollout. This removes the current standalone `Git Hooks` section while keeping +the same checks and failure facts. + +### Status rules + +- `[PASS]` means the node and all descendants are healthy. +- `[WARN]` means the node has a non-blocking warning and no blocking failure. + This makes existing warning severity visible without pretending it is a + successful check. +- `[FAIL]` means a blocking error or failed validation exists below the node. +- `[MISS]` remains the leaf status for a required file/check that is absent; + its parent is `[FAIL]` because the missing asset blocks readiness. +- Parent status is the worst descendant status in the order `FAIL`, `MISS`, + `WARN`, `PASS`. Readiness and exit-code classification continue to use the + existing problem severity/readiness model rather than the renderer's status + token. +- Color behavior remains the shared style policy: pass is green, warning is + yellow if a warning style exists or otherwise unstyled, and fail/miss are + red; non-TTY and `NO_COLOR` output contains deterministic plain tokens. + +### Collapse and expansion rules + +- A healthy top-level node renders one concise row and no details. Do not show + absolute paths, UUIDs, repository IDs, repository state-directory hashes, + canonical identities, configured remote names, or implementation metadata on + successful text rows. +- A healthy integration area renders one row (`[PASS] Skills`) and never lists + its installed files. +- A warning or failure expands only the affected branch. The affected group + row is followed by the child asset/check rows needed to locate the problem; + healthy sibling groups remain collapsed. +- Integration children are projected from the existing flat child facts into a + generic relative-path tree. For `skills`, the first workflow directory is a + child node and its files are nested beneath it, so one missing `SKILL.md` can + render as `Skills -> sce-commit -> SKILL.md`. Other asset groups use the + meaningful relative asset path without assuming a fixed number of files. +- A failing child shows the existing state-specific context: `Missing: ` + for absent files, `Path: ` plus content-mismatch information for stale + files, and the stored read error for unreadable files. The affected group also + renders the matching existing problem summary and remediation, deduplicated + by the typed problem/child association rather than by substring matching. +- A failing top-level node renders the relevant existing problem summary and + remediation, including absolute paths and expected/actual or invalid-value + information where the diagnostic already supplies it. No filesystem or Git + check is added to the renderer. +- Fix-mode text keeps the same report tree and appends the existing `Fix + results` section. Fix details remain detailed because they describe actions + taken, not healthy state. + +### Information hidden versus retained + +On success, hide all path and implementation metadata listed above, including +individual integration asset paths and checkout/Agent Trace IDs. On warning or +failure, retain the checked path, missing/stale/read state, provider summary, +remediation, and any diagnostic value already present in the report. JSON keeps +its current complete path/identity/problem fields, so this text compaction does +not remove machine-readable troubleshooting data. + +## Implementation approach + +### Data model and ownership + +Keep diagnosis and lifecycle providers presentation-neutral. The collapse +algorithm belongs in the doctor text presentation layer because it changes only +what is shown, not what is checked, what is considered ready, or how repairs are +selected. Do not move filesystem checks, content hashing, optional-workflow +selection, or severity calculation into `render.rs`. + +Extend the doctor-owned report model only to preserve typed relationships needed +by the renderer: + +- Add typed integration target/area metadata to `IntegrationGroupHealth` (or an + equivalent doctor-owned group key) and derive display labels from it. Keep + `IntegrationChildHealth` as the source of relative path and content state. +- Add a render-only node/status/detail representation in `doctor/types.rs` or a + focused private section of `doctor/render.rs`. It should accept a completed + `HookDoctorReport` and never access the filesystem. +- Reuse `ProblemKind`, `ProblemSeverity`, `DoctorProblem.summary`, and + `DoctorProblem.remediation` for top-level and group failure details. If a + typed association is needed to avoid fragile summary matching, add a small + doctor-owned problem scope/key during report construction; do not parse + human-readable summaries to determine status. +- Do not change lifecycle `HealthProblem` semantics unless implementation + proves an existing failure detail cannot be associated with a node. If a + detail extension is unavoidable, make it structured and provider-neutral, + copy it through the existing doctor/lifecycle adapters, and add it + additively to JSON only with an explicit compatibility review. + +### Renderer changes + +Refactor `doctor/render.rs` so text and JSON are separate contracts: + +- Build the new text tree from the existing report facts/problems, compute + worst-descendant status, render concise healthy rows, and recursively render + only unhealthy branches. +- Render the default diagnose header as `SCE doctor`; retain an explicit, + deterministic fix-mode header that identifies repair mode without restoring + the old `diagnose` inventory wording. +- Keep `render_report_json` field names and values unchanged unless a narrowly + justified additive field is required by the existing contract. In particular, + do not apply text redaction rules to JSON. +- Keep shared TTY/`NO_COLOR` styling and stdout payload ownership unchanged. + +`doctor/inspect.rs` should only change to provide typed group keys or structured +associations required by the renderer. Existing inventory checks, optional +workflow filtering, content-state classification, and problem generation must +remain the same. `doctor/mod.rs`, the provider modules, the parser, and exit +code handling should not change unless the typed adapter change requires it. + +### Architectural decisions and trade-offs + +1. **Presentation collapse, not diagnostic collapse.** Keeping every leaf fact + in the report preserves correctness and JSON/debuggability, while a + render-only tree gives the default text UX the desired healthy-collapse / + failure-expansion behavior. Aggregating in providers would risk hiding facts + from JSON, fix mode, or future renderers. +2. **Typed group keys instead of label parsing.** This adds a small semantic + result-model seam, but avoids coupling hierarchy to display spelling and + supports future target/asset types without duplicating checks. +3. **No `--verbose` in this change.** No such mechanism exists today. Adding it + would create a second text contract and a new compatibility surface; JSON is + already the stable full-detail route. Revisit only if compact text cannot + serve operators who need successful-file inventories. +4. **Warnings become visible.** Existing warning problems such as optional + OpenCode asset health should not silently appear as `[PASS]`. `[MISS]` is + retained for absent required leaves, while blocking parent nodes remain + `[FAIL]`. + +## Backward compatibility + +- Preserve process exit-code semantics: successful report generation remains + exit code `0` even when the report says `not_ready`, and parse/validation/ + runtime/dependency failures retain the existing class mapping (`2/3/4/5`). +- Preserve `sce doctor --fix` behavior, provider ordering, repair ownership, + idempotence, and fix-result outcomes. +- Preserve `--format json` field names, values, problem records, path/identity + detail, and machine-readable readiness. Text layout is intentionally a + human-facing contract change; scripts should use JSON rather than parse the + compact text hierarchy. +- Preserve non-TTY and CI behavior: no ANSI sequences when output is not a TTY + or `NO_COLOR` is set, deterministic ordering of domains/groups/children, and + no extra stdout/stderr streams. +- Update existing exact text-contract documentation/tests because the current + approved section order and `SCE doctor diagnose` header will change. No parser + or help compatibility change is needed because no new option is introduced. +- There is no existing verbose/debug mechanism to preserve. The current fully + expanded success inventory remains available through the complete JSON report, + not through a new text flag. + +## Testing strategy + +Add pure renderer/view-model tests with synthetic `HookDoctorReport` fixtures, +plus focused integration-state tests where existing inspection helpers are the +best source of truth. Assert exact plain-text output with color disabled and +assert that JSON remains unchanged for representative fields. + +Required cases: + +- **Everything passes:** only the compact Environment, Repository, and selected + target/group rows appear; no successful path, UUID, repository ID, canonical + identity, or integration file path appears. +- **One top-level check fails:** the parent becomes `[FAIL]`, its failure + summary/remediation and checked path remain visible, and unrelated healthy + domains stay collapsed. +- **One deeply nested integration file fails:** the target and area expand to + the affected asset/workflow and file; the missing/mismatch/read-error path + and existing diagnostic are visible while healthy siblings remain concise. +- **Several failures in one domain:** one domain status is emitted with all + affected child branches, deterministic ordering, and no duplicated summary + lines. +- **Failures across multiple domains:** each affected domain expands + independently; no failure detail is lost or attached to the wrong target. +- **Unusual paths/spaces:** paths containing spaces, parentheses, quotes, and + non-ASCII characters remain intact as path values and do not break hierarchy + construction or detail rendering. Use `PathBuf`/structured fields rather than + splitting rendered strings. +- **Warnings:** a non-blocking warning renders `[WARN]`, while a missing + required child renders `[MISS]` and its parent renders `[FAIL]`. +- **Fix mode:** the compact report is followed by the existing fix-result + vocabulary/details; no repair is triggered by rendering. +- **JSON/CI:** JSON remains parseable and retains existing path/identity/problem + detail; plain text from a non-TTY contains no ANSI sequences and keeps stable + status tokens. +- **No verbose mode:** parser/help tests continue to reject no newly implied + option, and `--format json` is documented/tested as the full-detail route. + +## Example outputs + +### Completely healthy installation + +```text +SCE doctor + +Environment + [PASS] State + [PASS] Configuration + [PASS] Repository identity + +Repository + [PASS] Git repository + [PASS] Git hooks + +Integrations + Claude Code + [PASS] Plugins + [PASS] Agents + [PASS] Commands + [PASS] Skills + + OpenCode + [PASS] Plugins + [PASS] Agents + [PASS] Commands + [PASS] Skills + + Pi + [PASS] Extensions + [PASS] Prompts + [PASS] Skills + +Summary: 0 blocking problem(s), 0 warning(s) +``` + +### Nested integration failure + +```text +SCE doctor + +Environment + [PASS] State + [PASS] Configuration + [PASS] Repository identity + +Repository + [PASS] Git repository + [PASS] Git hooks + +Integrations + Claude Code + [PASS] Plugins + [PASS] Agents + [PASS] Commands + [FAIL] Skills + [PASS] sce-change-to-plan + [MISS] sce-commit + Missing: /home/user/project/.claude/skills/sce-commit/SKILL.md + Problem: ClaudeCode skills required file(s) are missing. + Remediation: Reinstall repo-root Claude assets, then rerun 'sce doctor'. + [PASS] sce-handover + + OpenCode + [PASS] Plugins + [PASS] Agents + [PASS] Commands + [PASS] Skills + + Pi + [PASS] Extensions + [PASS] Prompts + [PASS] Skills + +Summary: 1 blocking problem(s), 0 warning(s) +``` + +The concrete problem line must be rendered from the existing structured +`DoctorProblem` summary/remediation and child state; the example is not a new +hardcoded diagnostic. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [ ] AC1: Default text output uses the Environment/Repository/Integrations hierarchy and groups integration areas beneath typed Claude Code, OpenCode, and Pi target nodes without listing every healthy file. + - Validate: exact plain-text renderer tests for an all-pass report and selected-target ordering. +- [ ] AC2: Healthy rows hide absolute paths, IDs, hashes, canonical identities, remote names, and individual integration paths, while all current checks still execute and readiness is unchanged. + - Validate: all-pass report assertions plus existing inspection/lifecycle tests and JSON field assertions. +- [ ] AC3: Blocking failures and non-blocking warnings retain actionable details, including paths, missing/stale/read state, existing summaries, remediations, and nested integration context; healthy siblings remain collapsed. + - Validate: renderer tests covering top-level, nested, same-domain, cross-domain, warning, and unusual-path fixtures. +- [ ] AC4: Parent/domain statuses are deterministic summaries of descendants, using `[PASS]`, `[WARN]`, `[FAIL]`, and `[MISS]` according to the documented severity rules. + - Validate: status aggregation unit tests for every status combination and exact output assertions. +- [ ] AC5: `--format json`, fix behavior, stream routing, non-TTY styling, and existing exit-code semantics remain compatible. + - Validate: JSON regression tests, fix-mode rendering tests, parser/app contract tests, and `NO_COLOR`/non-TTY renderer tests. +- [ ] AC6: The updated doctor text contract and CLI documentation describe the new hierarchy, detail policy, JSON full-detail route, and lack of a verbose flag. + - Validate: inspection of the updated durable context files against the renderer and `nix run .#pkl-check-generated`/`nix flake check` where applicable. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of which +criterion they map to. + +- `nix run .#pkl-check-generated` +- `nix flake check` + +### Context sync + +- `context/sce/doctor-human-text-contract.md` — replace the old flat text layout, + status/header rules, and integration row contract with the compact hierarchy + and failure-expansion contract. +- `context/sce/agent-trace-hook-doctor.md` — update the approved operator-health + text-mode and output-shape description while preserving readiness, repair, and + JSON contracts. +- `context/cli/cli-command-surface.md` — update the current doctor output + description and the statement that text rows expose path details. +- `context/overview.md` and `context/architecture.md` — update only if their + current doctor text claims are no longer accurate after implementation. + +## Constraints and non-goals + +- **In scope:** Rust doctor report/view-model and text renderer changes, + typed integration grouping metadata, doctor-focused tests, and the durable + doctor/CLI text-contract updates listed under Context sync. +- **Out of scope:** changing diagnostic checks, health taxonomy, optional + workflow selection, setup/install behavior, repair logic, JSON field semantics, + exit codes, top-level CLI parsing, or integration asset contents. +- **Constraints:** use existing lifecycle/report facts; keep renderer pure and + filesystem-free; preserve deterministic ordering; use the shared style/TTY + policy; keep stdout payload ownership in the app layer; run repository checks + through Nix. +- **Non-goal:** introduce a generic health-dashboard framework or a new + `--verbose`/debug text mode. The render-only tree is specific to doctor text + output and must not become a second diagnostic engine. + +## Assumptions + +- The user accepts an intentional human-text contract change from + `SCE doctor diagnose` plus separate `Git Hooks` to the compact `SCE doctor` + hierarchy shown here; JSON and process semantics remain the compatibility + boundary. +- `[WARN]` is acceptable for existing non-blocking warning problems, while + `[MISS]` remains useful for required missing leaves. This follows the request + to define PASS/WARN/FAIL behavior and the existing warning severity model. +- Existing summaries/remediations contain sufficient failure detail for the + first implementation; a structured detail field is added only if typed + association cannot be achieved without parsing strings. + +## Task stack + +- [x] T01: `Add typed doctor display grouping and status projection seams` (status:done) + - Task ID: T01 + - Scope: In — extend `cli/src/services/doctor/types.rs` and the integration-group construction in `doctor/inspect.rs` with typed target/area keys and render-only node/status/detail helpers; preserve all existing health facts and checks. Out — changing rendered output, lifecycle provider behavior, JSON serialization, or CLI options. + - Dependencies: none + - Done when: the completed report can be projected into a deterministic domain/group/asset tree without parsing display labels or consulting the filesystem, and existing inspection tests still describe the same expected assets/states. + - Verify: targeted doctor Rust tests covering typed group keys, optional-workflow filtering, and deterministic child ordering. + - Completed: 2026-08-17 + - Files changed: `cli/src/services/doctor/types.rs`, `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/render.rs` + - Result: Added typed integration target/area keys with derived display labels, filesystem-free display node/status/detail projection helpers, and coverage for typed grouping, optional-workflow filtering, and deterministic child ordering without changing rendered output or health checks. + - Done checks: Report projection is deterministic and filesystem-free (done); existing inspection assets/states remain covered (done). + - Context impact: local — doctor report/view-model and inspection seams changed; durable doctor text-contract context remains unchanged until the rendering tasks. + - Context synchronization: synced + +- [x] T02: `Render compact healthy doctor domains and integration groups` (status:done) + - Task ID: T02 + - Scope: In — refactor text rendering in `cli/src/services/doctor/render.rs` to emit the new header, Environment/Repository/Integrations hierarchy, concise pass rows, target-scoped group rows, and deterministic status/color handling; add all-pass, target-selection, non-TTY, and no-integration fixtures. Out — failure-detail expansion, diagnostic check changes, JSON shape changes, and fix execution. + - Dependencies: T01 + - Done when: a healthy report contains only the compact rows from the approved output model, selected targets remain the only rendered integration targets, and no successful path/ID/file inventory leaks into text mode. + - Verify: exact plain-text renderer tests with color disabled and non-TTY/`NO_COLOR` policy assertions. + - Completed: 2026-08-17 + - Files changed: `cli/src/services/doctor/render.rs`, `cli/src/services/doctor/types.rs` + - Result: Refactored human-readable doctor rendering to the compact SCE doctor, Environment, Repository, and target-scoped Integrations hierarchy; healthy rows suppress paths and identity metadata, target ordering is deterministic, warnings have color-aware status tokens, JSON remains unchanged, and no-integration guidance is retained. + - Done checks: Healthy reports render only compact approved rows (done); selected targets are the only integration targets rendered (done); successful paths, IDs, and file inventory are hidden from text output (done). + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::doctor` passed (12 tests, including exact compact renderer, selected-target ordering, no-integration, non-ANSI, optional-workflow, and inspection coverage); `nix develop -c sh -c 'cd cli && cargo fmt'` passed. + - Context impact: local — doctor text presentation and its render status seam changed; durable doctor text-contract context remains pending until T04. + - Context synchronization: synced + +- [x] T03: `Expand warnings and failures with nested diagnostic details` (status:done) + - Task ID: T03 + - Scope: In — implement recursive unhealthy-branch expansion in `doctor/render.rs`, associate existing `DoctorProblem` details with top-level/group nodes, render nested integration asset/workflow failures, add `[WARN]`/`[FAIL]`/`[MISS]` aggregation, preserve fix-result detail, and add all required failure/path test cases. Make only the minimal `doctor/inspect.rs` or doctor-owned model adjustment needed for typed associations. Out — new checks, provider logic, repair behavior, verbose mode, and JSON redesign. + - Dependencies: T02 + - Done when: every failed/warned node exposes enough existing context to troubleshoot immediately, healthy siblings stay collapsed, multiple failures across one or more domains render deterministically, and paths with spaces/unusual characters remain intact. + - Verify: exact renderer tests for top-level failure, deeply nested integration failure, multiple same-domain failures, cross-domain failures, warnings, missing/mismatch/read errors, unusual paths, and fix mode. + - Completed: 2026-08-17 + - Files changed: `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/mod.rs`, `cli/src/services/doctor/render.rs`, `cli/src/services/doctor/types.rs` + - Result: Added typed problem-to-integration-group associations and a pure recursive text projection that expands only unhealthy domains, groups, and asset paths; preserved JSON and fix-result rendering while exposing warning, missing, mismatch, and read-error diagnostics. + - Done checks: Failed and warned nodes retain actionable summaries, remediations, and path/read-state details (done); healthy siblings remain collapsed (done); multi-domain output and unusual paths are deterministic and intact (done). + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::doctor` passed (14 tests, including top-level, nested, warning, multiple-domain, mismatch, read-error, unusual-path, fix-mode, optional-workflow, and inspection coverage); `nix develop -c sh -c 'cd cli && cargo fmt'` passed; `git diff --check` passed. + - Context impact: local — doctor problem associations, render-only diagnostic projection, recursive failure rendering, and doctor renderer tests changed; durable doctor text-contract context remains pending until T04. + - Context synchronization: synced + +- [x] T04: `Lock compatibility and update the doctor text contract` (status:done) + - Task ID: T04 + - Scope: In — add JSON regression assertions and parser/app compatibility coverage as needed, verify unchanged exit/stream/fix semantics, and update `context/sce/doctor-human-text-contract.md`, `context/sce/agent-trace-hook-doctor.md`, and `context/cli/cli-command-surface.md` to match the implemented output. Update root context claims only when they are stale. Out — a final validation-only task, new CLI flags, unrelated documentation, and application behavior outside doctor presentation. + - Dependencies: T03 + - Done when: the new text contract is documented once, JSON and command compatibility are covered, and the implementation leaves a complete actionable plan for `/validate` without a trailing cleanup task. + - Verify: focused doctor/app tests plus documentation-to-code inspection; repository-wide checks are listed under Full validation for `/validate`. + - Completed: 2026-08-17 + - Files changed: `cli/src/services/doctor/render.rs`, `cli/src/services/parse/command_runtime.rs`, `context/sce/doctor-human-text-contract.md`, `context/sce/agent-trace-hook-doctor.md`, `context/cli/cli-command-surface.md`, `context/overview.md` + - Result: Locked representative JSON path, identity, problem, and fix-result fields against text redaction; added parser coverage for read-only text mode and fix-mode JSON requests; and updated the canonical doctor, operator, CLI, and overview context to describe the compact hierarchy and JSON full-detail boundary. + - Done checks: The compact text contract is documented once with the JSON compatibility boundary (done); JSON and command compatibility are covered (done); the plan now records a complete actionable `/validate` handoff without a cleanup task (done). + - Verify: `nix develop -c sh -c './scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::doctor && ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::parse::command_runtime'` passed (16 tests); `nix develop -c sh -c 'cd cli && cargo fmt'` passed; `git diff --check` passed; documentation-to-code inspection confirmed the required compact hierarchy, status tokens, healthy-row suppression, JSON detail boundary, and absence of a verbose flag. + - Context impact: cross-cutting — doctor text/JSON compatibility and CLI command-surface documentation changed; affected durable doctor, operator-health, CLI, and root overview context because the human output contract and machine-readable compatibility boundary are now finalized. + - Context synchronization: synced + +## Open questions + +None. The request specifies the required UX outcome and explicitly permits a +proposed hierarchy. The main trade-offs (render-only aggregation, typed group +keys, warning token, and no verbose flag) are resolved above without changing +diagnostic correctness or machine-readable compatibility. + +## Validation Report + +**Status:** failed +**Date:** 2026-08-17 + +### Commands run + +- `nix run .#pkl-check-generated` -> failed (removed generated output still exists at `cli/assets/generated`; exit not captured on the first run) +- `nix run .#pkl-check-generated` -> exit 1 (removed generated output still exists at `cli/assets/generated`) +- `nix flake check` -> not completed (tool wrapper interrupted the first run after its timeout) +- `nix flake check` -> exit 0 (all 7 flake checks passed) +- `nix develop -c sh -c './scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::doctor && ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml services::parse::command_runtime'` -> exit 0 (doctor: 10 tests passed; parser/runtime: 6 tests passed) + +### Success-criteria verification + +- [ ] AC1: Default text output uses the Environment/Repository/Integrations hierarchy and typed target grouping without listing healthy files -> the required exact all-pass and selected-target renderer tests are not present; the focused run only covered existing inspection tests and one JSON regression test. +- [ ] AC2: Healthy rows hide sensitive/implementation metadata while checks and readiness remain unchanged -> JSON and inspection checks passed, but the required all-pass text redaction assertions are not present. +- [ ] AC3: Failures and warnings retain actionable nested details while healthy siblings stay collapsed -> the required top-level, nested, multi-domain, warning, and unusual-path renderer fixtures are not present. +- [ ] AC4: Parent/domain statuses aggregate deterministically using the documented status tokens -> the required status-combination unit tests and exact output assertions are not present. +- [ ] AC5: JSON, fix behavior, streams, non-TTY styling, and exit-code semantics remain compatible -> JSON/parser checks passed, but the required fix-mode and NO_COLOR/non-TTY renderer coverage is not present. +- [ ] AC6: Durable context documents the updated contract and repository checks pass -> documentation inspection matched the compact hierarchy and JSON boundary, but `nix run .#pkl-check-generated` failed because `cli/assets/generated` remains. + +### Failed checks and follow-ups + +- `nix run .#pkl-check-generated`: exit 1 because removed generated output remains at `cli/assets/generated`; required: remove the leftover generated artifact outside validation and rerun final validation. +- AC1–AC5 renderer coverage: the plan-authored validation checks require exact compact-output, failure-expansion, status-aggregation, fix-mode, and non-TTY tests that are not present in the current doctor test sources; required: add or restore the authorized focused tests in a normal implementation session, then rerun validation. +- AC6 repository generation check: blocked by the leftover generated artifact described above; required: rerun after the artifact is cleared. + +### Residual risks + +- The compact renderer behavior is not covered by the complete acceptance-test matrix specified by the plan. + +### Retry + +After repairs, rerun: + +`/validate context/plans/compact-doctor-output.md` diff --git a/context/plans/doctor-claude-agents-auto-sync.md b/context/plans/doctor-claude-agents-auto-sync.md new file mode 100644 index 00000000..e6d522da --- /dev/null +++ b/context/plans/doctor-claude-agents-auto-sync.md @@ -0,0 +1,135 @@ +# Plan: doctor-claude-agents-auto-sync + +## Change summary + +Align `sce doctor` with the generated target inventory by removing the stale Claude `Agents` inspection and output while retaining OpenCode agent inspection. Add explicit doctor coverage for the post-commit automatic Agent Trace sync capability: inspect the installed canonical `post-commit` SCE managed block using the same merge/current semantics as setup, resolve the effective `agent_trace.auto_sync` setting, and report whether automatic sync is enabled, intentionally disabled, or not ready. + +This is worth building because the current Claude row reports an asset that setup never generates, while the new post-commit trigger can silently stop being available through hook drift or configuration state even though the existing generic hook checks do not explain that capability. A smaller alternative would only remove the Claude row and rely on the existing hook-content check; that fixes the false Claude failure but does not make the automatic-sync readiness and explicit opt-out observable, so it does not satisfy the operator-health gap. + +## Acceptance criteria + +How this plan is proven complete. Each criterion is observable and names the +check that proves it. `/validate` runs these checks; no task in the stack +performs final validation. + +- [x] AC1: Claude doctor inspection and both doctor renderers expose only the generated Claude areas (`Plugins`, `Commands`, and `Skills`), while OpenCode continues to expose its `Agents` area and existing target-scoped ordering remains deterministic. + - Validate: focused doctor tests assert no Claude `Agents` group/children or rendered `Claude Code` agent label and assert the OpenCode `Agents` group remains present; inspect text and JSON fixtures for the same target inventory. +- [x] AC2: In a repository with a current installed canonical `post-commit` managed block and resolved `agent_trace.auto_sync: true` (including the omitted default), doctor reports automatic sync as enabled and ready in text and `--format json`; with explicit `false`, it reports a healthy intentional disabled opt-out without marking overall readiness not ready. + - Validate: focused doctor/config tests assert the text status/label and stable JSON auto-sync state, enabled value, and resolved source for default, configured true, and configured false cases. +- [x] AC3: Doctor reports automatic sync as not ready when the effective `post-commit` managed block is missing, stale, unreadable, or otherwise not current, while preserving the existing hook problem/remediation and readiness behavior; doctor never launches `sce sync` or any background process. + - Validate: filesystem-backed hook/doctor tests cover current, drifted, missing, and unreadable post-commit states and assert no launcher invocation; existing hook lifecycle tests continue to pass. +- [x] AC4: The post-commit runtime still forwards `origin` metadata, launches the existing detached `sync --format json` only after successful Agent Trace persistence when enabled, and remains fail-open for launcher failures; no canonical hook asset, launcher semantics, setup flow, or high-frequency trigger changes. + - Validate: focused hook tests and inspection of `cli/assets/hooks/post-commit`, `hooks/mod.rs`, and `sync/auto_sync.rs` confirm the existing ordering, argument forwarding, and fail-open behavior are unchanged. +- [x] AC5: Durable context accurately describes the Claude target inventory and the doctor automatic-sync readiness, text, JSON, default, and opt-out contracts without claiming that Claude generates agents. + - Validate: manual review of the context files listed under `Context sync` against the implemented report fields and focused tests. + +### Full validation + +Repository-wide checks `/validate` runs after the last task, regardless of +which criterion they map to. + +- `nix flake check` + +### Context sync + +- `context/sce/agent-trace-hook-doctor.md` +- `context/cli/cli-command-surface.md` +- `context/architecture.md` +- `context/overview.md` +- `context/sce/doctor-human-text-contract.md` when the new text row/status shape is implemented + +## Task context synchronization lifecycle + +Persist this field in every plan; this is durable plan state, not chat state: + +- **Task context synchronization:** every task carries `pending | synced | blocked`. A completed task must be `synced` before another task can start or the plan can finish. +- For `blocked`, record **Blocker**, **Required action**, and **Retry condition** beside the status. Never infer `synced` from conversation history; write every lifecycle transition to the plan file. + +## Constraints and non-goals + +- **In scope:** Claude-specific doctor integration grouping and ordering/output claims; OpenCode agent preservation; doctor report types, inspection, rendering, JSON fields, and focused tests; hook lifecycle reuse of canonical managed-block/current semantics; resolved `agent_trace.auto_sync` readiness reporting; the durable context files listed under Context sync. +- **Out of scope:** changing `cli/assets/hooks/{pre-commit,commit-msg,post-commit}`; changing setup installation or merge behavior; changing `cli/src/services/hooks/mod.rs` post-commit execution; changing `cli/src/services/sync/auto_sync.rs`; launching sync from doctor; adding a daemon, watcher, scheduler, retry queue, or new synchronization engine; removing the shared `IntegrationArea::Agents` model required by OpenCode. +- **Constraints:** use the existing canonical hook asset and `hook_merge`/managed-block currency semantics rather than a second parser or shell execution; resolve `agent_trace.auto_sync` through the existing config resolver with default `true` and global-then-local precedence; preserve existing problem categories, remediation, exit/readiness semantics, text status vocabulary, JSON compatibility, and setup/doctor ownership boundaries; use repository wrappers and Nix-managed validation commands. +- **Non-goal:** making an explicit `agent_trace.auto_sync: false` opt-out fail doctor readiness or making doctor prove that a detached sync child completed. + +## Assumptions + +- This is a new plan: the existing automatic-sync and doctor plans are completed historical plans with different scopes, not revision targets. +- Automatic-sync readiness is a doctor report fact, not a new independent failure class: existing hook/config problems continue to determine overall readiness, while the new fact explains the capability state without duplicating remediation records. +- A current canonical `post-commit` managed block is the readiness proof for the hook side; doctor does not execute the hook, invoke the launcher, or inspect child-process/network outcomes. +- Text reports a ready enabled state as `[PASS] Post-commit Agent Trace auto-sync`, a deliberate opt-out as `[PASS] Post-commit Agent Trace auto-sync (disabled by config)`, and a non-ready repository hook state as `[FAIL] Post-commit Agent Trace auto-sync`; JSON carries a stable `post_commit_auto_sync` object with `state`, `enabled`, and resolved configuration source fields, plus the existing problem records. +- The shared `IntegrationArea::Agents` enum and generic labels remain because OpenCode still owns generated agents; only Claude-specific production, rendering-order, and documentation claims are removed. + +## Task stack + +- [x] T01: `Remove Claude Agents from doctor integration inspection` (status:done) + - Task ID: T01 + - Scope: In — Claude integration asset classification, group construction, target-specific area ordering/labels, and focused doctor inventory tests; preserve Claude plugins/commands/skills and the complete OpenCode plugins/agents/commands/skills inventory. Out — shared OpenCode agent types, setup assets, generated config, hook behavior, and auto-sync reporting. + - Dependencies: none + - Done when: Claude doctor reports no `Agents` group or agent asset expectation in text or JSON, OpenCode still reports its `Agents` group, deterministic target ordering remains valid, and focused doctor tests cover the regression. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml doctor::` with assertions for Claude group inventory and OpenCode agent preservation; inspect `doctor/inspect.rs`, `doctor/render.rs`, and `doctor/types.rs` for no Claude-specific agent production path. + - Context synchronization: synced + - Completed: 2026-08-20 + - Files changed: `cli/src/services/default_paths.rs`, `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/render.rs`, `cli/src/services/doctor/types.rs`, `context/plans/doctor-claude-agents-auto-sync.md` + - Result: Removed Claude agent asset classification and group construction while preserving OpenCode agents, Claude plugins/commands/skills, target-specific ordering, and focused regression coverage. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml doctor::` — pass (10 tests); inspection confirmed the Claude collector has no agent production path and OpenCode retains its Agents group. + - Context impact: interface — Claude doctor inventory and rendered target-area ordering changed; the listed doctor, CLI surface, architecture, overview, and human-text contract context files require synchronization before the next task. + +- [x] T02: `Report post-commit automatic-sync readiness in doctor` (status:done) + - Task ID: T02 + - Scope: In — `HooksLifecycle`/doctor inspection seam using canonical embedded `post-commit` managed-block currency, resolved `agent_trace.auto_sync` state, doctor report types, human text row, JSON object, and filesystem/config/format regression tests. Out — canonical hook files, setup installation, the post-commit runtime trigger, sync launcher implementation, new daemon/retry behavior, and unrelated hook problem taxonomy changes. + - Dependencies: T01 + - Done when: doctor deterministically reports enabled/default, explicit disabled, not-ready, and not-applicable states; current hook plus resolved configuration produces ready output; hook drift/missing/read failures preserve existing problem/remediation and readiness behavior; doctor performs no sync launch; text and JSON output are stable and tested. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml doctor::`; `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::`; inspect `cli/src/services/config/resolver.rs`, `cli/src/services/hooks/lifecycle.rs`, `cli/src/services/hooks/mod.rs`, and `cli/src/services/sync/auto_sync.rs` to confirm the existing default/opt-out and fail-open runtime contracts remain unchanged. + - Context synchronization: synced + - Completed: 2026-08-20 + - Files changed: `cli/src/services/config/mod.rs`, `cli/src/services/config/resolver.rs`, `cli/src/services/doctor/inspect.rs`, `cli/src/services/doctor/render.rs`, `cli/src/services/doctor/types.rs` + - Result: Added deterministic post-commit Agent Trace auto-sync readiness facts using canonical managed-block currency and resolved configuration, with enabled/disabled/not-ready/not-applicable text and JSON reporting while preserving existing hook problems, readiness, and runtime launcher behavior. + - Verify: `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml doctor::` — pass (15 tests); `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::` — pass (25 tests); inspected `cli/src/services/config/resolver.rs`, `cli/src/services/hooks/lifecycle.rs`, `cli/src/services/hooks/mod.rs`, and `cli/src/services/sync/auto_sync.rs` — existing default/opt-out, managed-block, forwarding, ordering, detached-launch, and fail-open contracts remain unchanged. + - Context impact: interface — doctor report types, human text, JSON fields, and configuration-resolution consumption changed; `context/sce/agent-trace-hook-doctor.md`, `context/cli/cli-command-surface.md`, `context/architecture.md`, `context/overview.md`, and `context/sce/doctor-human-text-contract.md` require synchronization before the next task. + +- [x] T03: `Synchronize doctor and automatic-sync context contracts` (status:done) + - Task ID: T03 + - Scope: In — update the durable doctor operator contract, CLI command surface, architecture/overview summaries, and human text contract as required by the implemented state/field names and Claude inventory. Out — application code, tests, generated outputs, historical plan files, and any change to the runtime behavior. + - Dependencies: T02 + - Done when: the named context files no longer claim Claude has generated agents and document the doctor auto-sync readiness proof, default-enabled setting, explicit opt-out, text/JSON observables, preserved existing hook remediation, and no-launch/fail-open boundaries. + - Verify: manual code-to-context review against T01/T02 output and the focused doctor/hook test results; confirm no context file outside the listed sync set was changed. + - Context synchronization: synced + - Completed: 2026-08-20 + - Files changed: `context/sce/agent-trace-hook-doctor.md`, `context/cli/cli-command-surface.md`, `context/architecture.md`, `context/overview.md`, `context/sce/doctor-human-text-contract.md` + - Result: Synchronized the five durable doctor context contracts with the implemented Claude inventory and post-commit auto-sync readiness states, fields, configuration sources, remediation boundaries, and fail-open/no-launch behavior. + - Verify: manual code-to-context review against T01/T02 implementation and recorded focused doctor/hook results — pass; `git diff --check` — pass; changed paths are limited to the five listed sync files before the required plan-state write. + - Context impact: documentation — current doctor inventory, text/JSON output, configuration-resolution, and hook-runtime boundaries are now synchronized; no application behavior changed. + +## Open questions + +None. The supplied brief and repository conventions determine the readiness proof, opt-out semantics, output shape, ownership boundary, and non-goals; the remaining choices are recorded assumptions. + +## Validation Report + +**Status:** validated +**Date:** 2026-08-20 + +### Commands run + +- `nix flake check` -> exit 0 (flake evaluation passed; the command reported no checks executed for the current system) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml doctor::` -> exit 0 (15 focused doctor tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml hooks::` -> exit 0 (25 focused hook tests passed) +- `nix develop -c ./scripts/run-cli-cargo.sh test --manifest-path cli/Cargo.toml config::` -> exit 0 (26 focused config tests passed) +- `git status --short && git diff --check` -> exit 0 (only intended plan/application/context paths are modified; no whitespace errors) + +### Success-criteria verification + +- [x] AC1: Claude doctor inspection and both doctor renderers expose only the generated Claude areas (`Plugins`, `Commands`, and `Skills`), while OpenCode continues to expose its `Agents` area and existing target-scoped ordering remains deterministic. -> 15 focused doctor tests passed, including exact Claude/OpenCode inventory, Claude agent exclusion, OpenCode agent preservation, and deterministic text ordering; source and renderer tests confirm the text/JSON inventory contract. +- [x] AC2: In a repository with a current installed canonical `post-commit` managed block and resolved `agent_trace.auto_sync: true` (including the omitted default), doctor reports automatic sync as enabled and ready in text and `--format json`; with explicit `false`, it reports a healthy intentional disabled opt-out without marking overall readiness not ready. -> Doctor tests passed for default-enabled, configured true, and explicit disabled text/JSON states; 26 config tests passed for default, configured true, local precedence, and explicit false resolution. +- [x] AC3: Doctor reports automatic sync as not ready when the effective `post-commit` managed block is missing, stale, unreadable, or otherwise not current, while preserving the existing hook problem/remediation and readiness behavior; doctor never launches `sce sync` or any background process. -> Doctor tests passed for current, missing, stale, and unreadable states plus hook diagnostics; focused hook tests passed and source inspection confirmed doctor has no sync-launch path. +- [x] AC4: The post-commit runtime still forwards `origin` metadata, launches the existing detached `sync --format json` only after successful Agent Trace persistence when enabled, and remains fail-open for launcher failures; no canonical hook asset, launcher semantics, setup flow, or high-frequency trigger changes. -> 25 focused hook tests passed; inspection of `cli/assets/hooks/post-commit`, `cli/src/services/hooks/mod.rs`, and `cli/src/services/sync/auto_sync.rs` confirmed origin forwarding, persistence-before-launch ordering, detached JSON sync, and fail-open behavior. +- [x] AC5: Durable context accurately describes the Claude target inventory and the doctor automatic-sync readiness, text, JSON, default, and opt-out contracts without claiming that Claude generates agents. -> Manual review of the five listed context files against the implemented report fields and focused tests confirmed the Claude inventory, readiness states, source fields, opt-out, no-launch, and fail-open contracts; the worktree contains no untracked artifacts and `git diff --check` passed. + +### Failed checks and follow-ups + +- None. + +### Residual risks + +- None identified. diff --git a/context/sce/agent-trace-hook-doctor.md b/context/sce/agent-trace-hook-doctor.md index c690a542..0ca47704 100644 --- a/context/sce/agent-trace-hook-doctor.md +++ b/context/sce/agent-trace-hook-doctor.md @@ -24,32 +24,33 @@ The runtime in `cli/src/services/doctor/mod.rs` exposes the approved doctor comm - explicit mode selection through `sce doctor` (`diagnose`) and `sce doctor --fix` (`fix`) - command/help wiring for `--fix` plus stable text/JSON mode reporting -- human text rendering with `SCE doctor diagnose` / `SCE doctor fix` header + ordered `Environment`, `Configuration`, `Repository`, `Git Hooks`, and `Integrations` sections -- exact human text status vocabulary `[PASS]`, `[FAIL]`, and `[MISS]` +- compact human text rendering with `SCE doctor` / `SCE doctor fix` header + ordered `Environment`, `Repository`, and `Integrations` domains; Environment contains State, Configuration, and Repository identity, while Repository contains Git repository, post-commit Agent Trace auto-sync readiness, and Git hooks +- human text status vocabulary `[PASS]`, `[WARN]`, `[FAIL]`, and `[MISS]`, with healthy rows collapsed to status plus display label - text summary footer with blocking-problem and warning counts - local DB reporting in default doctor output - stable problem records with category, severity, fixability, and remediation metadata - deterministic fix-result records in fix mode with `fixed`, `skipped`, `manual`, and `failed` outcomes -- simplified `label (path)` human rows for healthy path-backed state/config/repository/hook entries, without redundant `present` / `expected` prose +- healthy human rows suppress path, identity, and implementation metadata; JSON retains the complete path/identity detail - default global/local config-file location reporting, plus validation of existing global and repo-local `sce/config.json` readability and schema compliance (delegated to `ConfigLifecycle::diagnose`) - startup config resolution no longer blocks doctor on invalid default-discovered config files; doctor reaches its own config-validation path, reports those files as problems, and keeps invalid-config remediation manual-only - local DB location reporting, DB parent-directory readiness checks, and existing-DB health validation (delegated to `LocalDbLifecycle::diagnose`) -- local DB reporting plus checkout-aware Agent Trace DB reporting in default doctor output +- local DB reporting plus checkout-aware Agent Trace DB diagnostics; healthy identity and path metadata stay in JSON while human text summarizes them under `Environment` → `Repository identity` - explicit git-unavailable, outside-repo, and bare-repo repository-targeting failures - effective hook-path source (`default`, local `core.hooksPath`, global `core.hooksPath`) - repository root and hooks directory resolution when a repository target is detected -- top-level-only human text hook rows for `pre-commit`, `commit-msg`, and `post-commit`, with nested `content` / `executable` detail removed from text mode +- Git hook health is summarized beneath the Repository domain; individual healthy hook paths are not rendered in compact text - required hook presence and executable permissions for `pre-commit`, `commit-msg`, and `post-commit` when repo-scoped checks apply (delegated to `HooksLifecycle::diagnose`) -- byte-for-byte stale-content detection for required hook payloads against canonical embedded SCE-managed hook assets (delegated to `HooksLifecycle::diagnose`) +- post-commit automatic-sync readiness from the installed canonical managed block and resolved `agent_trace.auto_sync` setting; enabled/current reports ready, explicit `false` reports a healthy disabled opt-out, and enabled-but-missing, stale, unreadable, or non-executable post-commit state reports not ready without launching sync +- managed-block currency checks for required hook payloads against canonical embedded SCE hook assets (delegated to `HooksLifecycle::diagnose` and reused by doctor inspection); `post_commit_auto_sync` is an explanatory capability fact rather than a new problem category, with JSON `state`, `enabled`, `source`, and `config_source` fields, while existing hook problem records, remediation, and overall readiness remain authoritative; doctor never launches `sce sync` or another background process, and runtime launcher failures remain fail-open to a successful post-commit operation - integration target resolution that reads `integrations.target` from repo-local `.sce/config.json` when present, or falls back to detecting repo-root `.opencode/`, `.claude/`, and `.pi/` directories when config has no `integrations` or `integrations.target`; only the resolved targets are inspected -- repo-root installed OpenCode integration inventory for `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, and `OpenCode skills`, Claude integration inventory for `ClaudeCode plugins`, `ClaudeCode agents`, `ClaudeCode commands`, and `ClaudeCode skills`, plus Pi integration inventory for `Pi prompts` and `Pi skills`, scoped to the resolved targets -- integration child-row reporting validates installed files against embedded SHA-256 content; missing files render as `[MISS]`, content mismatches render as `[FAIL]`, and any affected parent group renders as `[FAIL]` -- OpenCode plugin inventory includes the installed manifest file plus plugin/preset artifacts as required presence-only files; Claude groups are derived from embedded `.claude` assets (`settings.json` and `hooks/**` under `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, then `agents/**`, `commands/**`, and `skills/**`); Pi groups are derived from embedded `.pi` assets (`prompts/**` under `Pi prompts`, `skills/**` under `Pi skills`); generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees are not inspected by doctor +- repo-root installed OpenCode integration inventory for typed `Plugins`, `Agents`, `Commands`, and `Skills` areas, Claude inventory for generated `Plugins`, `Commands`, and `Skills` areas with no `Agents` expectation, plus Pi inventory for `Extensions`, `Prompts`, and `Skills`, all scoped to the resolved targets +- integration groups are rendered beneath typed, target-scoped `Claude Code`, `OpenCode`, and `Pi` nodes in deterministic target-specific area order; healthy groups render one concise status row without listing installed files +- OpenCode plugin inventory includes the installed manifest file plus plugin/preset artifacts as required presence-only files; Claude groups are derived from embedded `.claude` assets (`settings.json` and `hooks/**` under `ClaudeCode plugins`, including `.claude/hooks/run-sce-or-show-install-guidance.sh`, then `commands/**` and `skills/**`); Pi groups are derived from embedded `.pi` assets (`prompts/**` under `Pi prompts`, `skills/**` under `Pi skills`); generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees are not inspected by doctor - repair-mode delegation to `ServiceLifecycle::fix` implementations: `HooksLifecycle::fix` reuses `install_required_git_hooks` for missing hooks directories plus missing, stale, or non-executable required hooks, so repair restores the canonical all-hook non-blocking missing-`sce` guidance, available-CLI argument/failure propagation, and post-commit-only remote forwarding contract; `LocalDbLifecycle::fix`, `AuthDbLifecycle::fix`, and `AgentTraceDbLifecycle::fix` handle bootstrap of missing canonical SCE-owned DB parent directories ## Approved human text-mode contract -The implemented human-facing `sce doctor` text contract is split into `context/sce/doctor-human-text-contract.md`. +The implemented human-facing `sce doctor` text contract is split into `context/sce/doctor-human-text-contract.md`, which is the single source of truth for section order, healthy-row suppression, status aggregation, and unhealthy-branch expansion. ## Command surface contract @@ -128,7 +129,7 @@ The broadened contract for `sce doctor` must cover the following problem invento - local DB and repository-scoped Agent Trace DB parent directories are missing or not writable - local DB and repository-scoped Agent Trace DB bootstrap or health is broken - Agent Trace DB file exists but cannot be opened (connection failure) or has incomplete schema (missing/unapplied migrations) — reported as `AgentTraceDbConnectionFailed` / `AgentTraceDbSchemaNotReady` with manual-only remediation directing to `sce setup` -- Agent Trace checkout ID plus repository-scoped DB path/health are reported in `Configuration` section output when available; repository DB rows include repository ID, identity source, safe canonical identity, configured remote name, and never raw remote URLs +- Agent Trace checkout ID plus repository-scoped DB path/health remain in the complete report and JSON; human text summarizes them under `Environment` → `Repository identity` without exposing healthy identity metadata. Repository DB records include repository ID, identity source, safe canonical identity, configured remote name, and never raw remote URLs ### Repository targeting and git readiness diff --git a/context/sce/doctor-human-text-contract.md b/context/sce/doctor-human-text-contract.md index ccaaf001..b2737ba8 100644 --- a/context/sce/doctor-human-text-contract.md +++ b/context/sce/doctor-human-text-contract.md @@ -1,108 +1,111 @@ # SCE doctor human text contract -Plan `doctor-human-text-integration-audit` task `T01` locks the approved human-facing `sce doctor` text contract for downstream implementation tasks. -This contract is implemented by the current runtime and remains normative for future changes. +The default human-readable `sce doctor` report is a compact, domain-oriented +health summary. Diagnosis remains complete and read-only; JSON remains the +full-detail machine-readable route. ## Text-mode section order -Human text output for `sce doctor` must render these sections in this exact order: +Human text output renders these sections in this exact order: 1. `Environment` -2. `Configuration` (includes Agent Trace DB health row) -3. `Repository` -4. `Git Hooks` -5. `Integrations` - -## Human text status vocabulary - -Human text rows must use exactly this status vocabulary: - -- `[PASS]`: healthy -- `[FAIL]`: SCE will not work unless fixed -- `[MISS]`: required file is missing - -No alternate human text status labels are allowed for this layout. - -When shared CLI color output is enabled, `[PASS]` renders green and `[FAIL]` / `[MISS]` render red. -When color is disabled, human text still renders the exact bracketed tokens without ANSI sequences. - -## Header and row formatting - -Diagnose mode renders the header `SCE doctor diagnose`. -Fix mode renders the header `SCE doctor fix`. - -Human text rows with path detail use the simplified `label (path)` form. -Healthy human rows do not append redundant prose such as `present`, `expected`, or `all required files present`. - -Repository rows use the labels `Repository` and `Hooks` in text mode. - -## Git Hooks text simplification - -Human text output for `Git Hooks` is simplified to top-level required-hook presence rows only. -Nested human text rows for hook `content` or `executable` detail are not part of the approved layout. -This simplification is text-mode only and does not change JSON output requirements. - -## Integrations text contract - -Integration checks are target-scoped. The doctor resolves which integration targets to inspect using the following priority: - -1. **Configured targets**: If `.sce/config.json` has `integrations.target` with a non-empty array, only the listed targets (`opencode`, `claude`, `pi`) are inspected. -2. **Empty target array**: If `integrations.target` exists but is an empty array `[]`, the user has not recorded any integration targets. The doctor returns no targets and renders a guidance message instead of group rows. -3. **Directory detection fallback**: When config has no `integrations` property or `integrations.target` property is absent, the doctor falls back to detecting installed repo-root directories — `.opencode/` is detected as OpenCode, `.claude/` is detected as Claude, and `.pi/` is detected as Pi. -4. **No targets**: When directory detection identifies no installed directories either, the `Integrations` section renders `[FAIL] No integrations installed; run 'sce setup'` and a blocking `NoIntegrationsInstalled` problem is recorded, so the Summary counts it as a blocking problem. - -Human text output renders group rows only for the resolved targets: - -- `OpenCode plugins` -- `OpenCode agents` -- `OpenCode commands` -- `OpenCode skills` -- `ClaudeCode plugins` -- `ClaudeCode agents` -- `ClaudeCode commands` -- `ClaudeCode skills` -- `Pi prompts` -- `Pi skills` -- `Pi extensions` - -Within a resolved target, the required inventory is additionally scoped to the repository's optional-workflow selection. The doctor reads `integrations.optional_workflows` from `.sce/config.json`; an absent, unreadable, or key-less file means nothing is selected. There is no directory-detection fallback for optional workflows. An unselected optional workflow's command file and skill subtree are not part of the required inventory, so no child row and no missing-file problem is produced for them. A selected optional workflow's assets are required inventory like any core workflow's, keeping `[MISS]` and content-mismatch `[FAIL]` detection unchanged. Files belonging to a previously selected but now unselected optional workflow are not reported as stray; the doctor simply stops expecting them. See [setup local bootstrap](setup-repo-local-config-bootstrap.md). - -Integration checks for this contract inspect installed repo-root artifacts only. -They validate file presence and content against embedded OpenCode, Claude, and Pi setup assets: byte-exact `sha256` for every asset except the two JSON configs `sce setup` installs by merge (`.claude/settings.json`, `.opencode/opencode.json`), which instead validate that the file's SCE-owned fragment matches the embedded catalog — a file that also carries extra user keys, permissions, or plugins still renders `[PASS]` as long as that fragment is current (see [non-destructive setup install merge seam](setup-no-backup-policy-seam.md)). -Generated `config/.opencode/**`, `config/.claude/**`, and `config/.pi/**` trees are out of scope for doctor integration checks in this change stream. - -Required git hooks (`Git Hooks` section) are a third merge-target family with the same fragment-currency rule: a hook is `[PASS]` when merging the canonical template into its on-disk bytes is a no-op, whether or not foreign content (a hand-written hook, husky, lefthook) sits around the SCE managed block — not byte-exact equality against the canonical hook (see [git hooks install contract](setup-githooks-install-contract.md)). - -Claude installed assets are grouped by repo-root `.claude/` relative path: - -- `settings.json` and `hooks/**` -> `ClaudeCode plugins` (including `hooks/run-sce-or-show-install-guidance.sh`) -- `agents/**` -> `ClaudeCode agents` -- `commands/**` -> `ClaudeCode commands` -- `skills/**` -> `ClaudeCode skills` - -Pi installed assets are grouped by repo-root `.pi/` relative path: - -- `prompts/**` -> `Pi prompts` -- `skills/**` -> `Pi skills` -- `extensions/**` -> `Pi extensions` - -For each resolved target, the grouped installed repo-root asset trees are required inventory. -If any required file in an integration group is missing or mismatched: - -- missing child rows render `[MISS]` -- mismatched child rows render `[FAIL]` and include a content-mismatch detail -- the parent integration group renders `[FAIL]` - -An integration group renders `[PASS]` only when every required installed file in that group is present. - -Healthy integration parent rows render the group name only. -Integration child rows render as `[STATUS] relative/path (absolute/path)` in text mode. - -## Non-goals for this contract slice - -- no JSON output shape or semantic changes -- no Claude plugin registry or preset-catalog checks - -These non-goals scoped the original text-contract slice only. A later plan (`non-destructive-setup-install` task `T05`) added `sce doctor --fix` behavior for the two merge-target JSON configs: when their SCE-owned fragment is missing or stale, `--fix` reinstalls just that one asset through the same per-asset merge-install path `sce setup` uses, leaving every other asset and every user key untouched. The status vocabulary and section order above are unchanged by that addition. - -See also: [doctor operator contract](agent-trace-hook-doctor.md), [CLI command surface](../cli/cli-command-surface.md). +2. `Repository` +3. `Integrations` + +The `Environment` domain contains `State`, `Configuration`, and `Repository +identity`. The `Repository` domain contains `Git repository`, `Post-commit Agent +Trace auto-sync`, and `Git hooks`. + +## Header and status vocabulary + +- Diagnose mode renders `SCE doctor`. +- Fix mode renders `SCE doctor fix` and appends the existing `Fix results` + section. +- `[PASS]` means the node is healthy. +- `[WARN]` means a non-blocking warning exists below the node. +- `[FAIL]` means a blocking failure exists below the node. +- `[MISS]` is reserved for a missing required leaf; parent aggregation may + promote that condition to `[FAIL]`. + +When shared CLI color output is enabled, pass is green, warning is yellow, and +fail/miss are red. Non-TTY and `NO_COLOR` output contains the exact bracketed +tokens without ANSI sequences. + +Healthy rows contain only their status and display label. They do not expose +absolute paths, UUIDs, repository IDs, hashes, canonical identities, configured +remote names, or individual integration asset paths. + +This redaction is presentation-only: diagnosis still performs the complete +read-only inspection, and `--format json` remains the full-detail route for +paths, identities, problem records, and fix results. + +The post-commit Agent Trace auto-sync row uses these stable labels and states: + +- `[PASS] Post-commit Agent Trace auto-sync` means the canonical post-commit + managed block is current and the resolved setting is enabled. +- `[PASS] Post-commit Agent Trace auto-sync (disabled by config)` means the + explicit `agent_trace.auto_sync: false` opt-out is active; it does not make + overall doctor readiness fail. +- `[FAIL] Post-commit Agent Trace auto-sync` means the enabled capability is not + ready because the post-commit managed block is missing, stale, unreadable, or + otherwise not current. +- Outside an applicable repository scope, the row is `[MISS] ... (not + applicable)` and does not launch or inspect a synchronization process. + +JSON exposes the same fact as `post_commit_auto_sync` with stable `state`, +`enabled`, `source`, and `config_source` fields. Existing hook problem records, +remediation, and overall readiness semantics remain the source of blocking +diagnostics. + +The resolved `enabled` value defaults to `true` when `agent_trace.auto_sync` is +omitted and is `false` only for the explicit config opt-out. `source` reports +`default` or `config_file` for resolved values, or `unresolved` when config +resolution fails; `config_source` identifies the discovered global or local +config layer when applicable and is otherwise `null`. Doctor only reports +this fact: it never launches `sce sync` or a background process. The post-commit +runtime still launches one detached `sync --format json` child only after +successful Agent Trace persistence when enabled, and launcher failures remain +fail-open. + +## Integration hierarchy + +Integration checks remain target-scoped. The doctor resolves targets using this +priority: + +1. A non-empty `.sce/config.json` `integrations.target` array selects only the + listed targets (`opencode`, `claude`, `pi`). +2. An explicitly empty target array selects no targets and renders the no-target + guidance row. +3. Without a configured target property, repo-root `.opencode/`, `.claude/`, + and `.pi/` directories are detected. + +Only resolved targets render. Display labels are normalized as `Claude Code`, +`OpenCode`, and `Pi`; typed target/area keys, not display-label parsing, own the +hierarchy. Areas render in deterministic order: + +- Claude Code: `Plugins`, `Commands`, `Skills` +- OpenCode: `Plugins`, `Agents`, `Commands`, `Skills` +- Pi: `Extensions`, `Prompts`, `Skills` + +Healthy areas render one concise `[PASS]` row and never list installed files. +The report and JSON payload still retain the complete inspected asset facts for +diagnostics and later unhealthy-branch rendering. + +When no integration target is resolved, the `Integrations` section renders: + +`[FAIL] No integrations installed; run 'sce setup'` + +The existing optional-workflow selection remains the source of truth for which +assets inspection expects. An unselected optional workflow produces no required +child fact or missing-file problem. + +## Compatibility boundary + +The compact text layout is intentionally a human-facing contract change. JSON +field names, identity/path/problem detail, readiness classification, exit-code +semantics, stream ownership, diagnosis read-only behavior, and fix behavior +remain unchanged. Scripts should use `--format json` rather than parse compact +text. + +See also [doctor operator contract](agent-trace-hook-doctor.md) and [CLI command +surface](../cli/cli-command-surface.md).