mod asset_notices;
mod entry;
pub use entry::{
prepare_isolated_session, prepare_isolated_session_under, prepare_session,
prepare_session_for_managed, prepare_session_for_repair, prepare_session_for_repair_under,
prepare_session_with_home, prepare_session_with_memory_reachable,
prepare_session_with_repo_url, prepare_session_with_repo_url_and_exe,
prepare_session_with_style, prepare_session_with_style_and_native,
};
mod divert_hooks;
mod hook_group_diff;
pub use asset_notices::{PrepScope, log_prep_findings};
pub(crate) use hook_group_diff::{ProjectHookGroupGaps, project_hook_group_gaps};
mod palace_alias;
pub(crate) mod malformed_backup;
mod project_hooks;
mod prompt_feedback_hooks;
#[cfg(test)]
#[path = "prompt_feedback_hooks_tests.rs"]
mod prompt_feedback_hooks_tests;
mod quarantine_shadows;
mod resume_hooks;
pub use resume_hooks::{ensure_project_hooks, missing_lifecycle_hook_events};
pub(crate) use resume_hooks::ensure_project_hooks_with;
mod search_index;
mod settings;
mod skills;
mod statusline;
pub use statusline::{ensure_status_line, ensure_status_line_in};
mod sync_assets;
mod system_prompt;
pub use system_prompt::{
CliLaunch, build_system_prompt, build_system_prompt_for, build_system_prompt_for_profile,
build_system_prompt_for_with_roster, build_system_prompt_for_with_style,
build_system_prompt_for_with_style_and_native, cli_launch,
};
mod supervisor_model;
#[cfg(test)]
mod tests;
mod workstream_label;
mod worktree_sync;
#[cfg(test)]
#[path = "tests_roster.rs"]
mod tests_roster;
#[cfg(test)]
#[path = "tests_scaffold_gitignore.rs"]
mod tests_scaffold_gitignore;
#[cfg(test)]
#[path = "tests_search_index.rs"]
mod tests_search_index;
#[cfg(test)]
#[path = "tests_launch_trust_3926.rs"]
mod tests_launch_trust_3926;
#[cfg(test)]
#[path = "tests_claude_json_concurrency_4072.rs"]
mod tests_claude_json_concurrency_4072;
#[cfg(test)]
#[path = "tests_quarantine_4448.rs"]
mod tests_quarantine_4448;
#[cfg(test)]
#[path = "tests_skill_overrides_7751.rs"]
mod tests_skill_overrides_7751;
#[cfg(test)]
#[path = "tests_malformed_settings_7780.rs"]
mod tests_malformed_settings_7780;
#[cfg(test)]
#[path = "tests_settings_lock_7762.rs"]
mod tests_settings_lock_7762;
#[cfg(test)]
#[path = "tests_style_selection_8533.rs"]
mod tests_style_selection_8533;
#[cfg(test)]
#[path = "tests_supervisor_profile_8453.rs"]
mod tests_supervisor_profile_8453;
#[cfg(test)]
#[path = "tests_pm_guard_9018.rs"]
mod tests_pm_guard_9018;
#[cfg(test)]
#[path = "tests_catchup_root_8311.rs"]
mod tests_catchup_root_8311;
use std::path::{Path, PathBuf};
use crate::core::agent_deployer::{DeployResult, deploy_agents_filtered, retract_framework_agents};
use crate::core::instruction_pipeline::{PipelineInput, PipelineOutput, build_instructions};
use crate::core::paths::FrameworkPaths;
use crate::core::skill_deployer::DeployStats;
use settings::{
deploy_output_style,
preseed_workspace_trust_home,
remove_global_trusty_memory_hooks,
write_auto_memory_enabled,
write_output_style,
write_project_hooks_with,
};
pub(crate) use settings::{write_enabled_plugins, write_enabled_plugins_with_trust};
pub(crate) use settings::{OUTPUT_STYLE, is_stale_statusline_command, resolve_statusline_command};
pub(crate) use settings::{AUTO_MEMORY_KEY, merge_settings_key};
pub(crate) use search_index::register_project_index;
pub(crate) use settings::resolve_palace_slug;
pub(crate) use palace_alias::maybe_register_palace_alias;
pub use worktree_sync::resume_self_heal;
pub use sync_assets::{SyncAssetsError, SyncAssetsReport, sync_session_assets};
pub use workstream_label::{LabelOutcome, ensure_workstream_label, spawn_workstream_label_ensure};
#[derive(Debug)]
pub struct PrepReport {
pub deploy: DeployResult,
pub skill_deploy: DeployStats,
pub instructions: PipelineOutput,
pub stash: PathBuf,
pub compiled_prompt: PathBuf,
pub output_style: Option<PathBuf>,
pub hooks_written: bool,
pub catchup_context: Option<String>,
pub roster_errors: Vec<String>,
pub asset_notices: Vec<String>,
pub memory_reachable: bool,
pub profile: crate::core::session_profile::SessionProfile,
}
#[derive(Debug, thiserror::Error)]
pub enum PrepError {
#[error("agent deploy failed: {0}")]
Deploy(String),
#[error("skill deploy failed: {0}")]
SkillDeploy(String),
#[error("io error for {path}: {source}")]
Io {
path: PathBuf,
source: std::io::Error,
},
#[error("could not resolve a stable hook binary: {source}")]
HookExe {
#[source]
source: crate::core::standalone::hooks::StableHookExeError,
},
#[error("could not snapshot {path} before rewriting it: {source}")]
HookSnapshot {
path: PathBuf,
source: std::io::Error,
},
#[error("refused to rewrite {path}: its current contents could not be preserved: {source}")]
SettingsBackup {
path: PathBuf,
source: std::io::Error,
},
#[error("could not acquire the settings lock for {path}: {source}")]
SettingsLock {
path: PathBuf,
source: std::io::Error,
},
#[error(
"{}",
crate::core::instruction_pipeline::instructions_failure_message(path, source)
)]
Instructions {
path: PathBuf,
source: std::io::Error,
},
#[error("cannot compose the PM instructions: {source}")]
Content {
source: crate::core::content_source::AgentContentError,
},
}
impl PrepError {
pub fn is_fatal(&self) -> bool {
matches!(self, Self::Instructions { .. } | Self::Content { .. })
}
}
#[derive(Clone, Copy, Default)]
pub(super) struct HostInputs<'a> {
pub(super) home: Option<&'a Path>,
pub(super) hook_exe: Option<&'a Path>,
pub(super) memory_reachable: Option<bool>,
pub(super) palace_registry: Option<&'a Path>,
}
impl<'a> HostInputs<'a> {
pub(super) fn with_home(home: Option<&'a Path>) -> Self {
Self {
home,
..Self::default()
}
}
}
pub(super) fn prepare_session_inner(
fw: &FrameworkPaths,
project_dir: &Path,
explicit_style: Option<&str>,
native_supported: bool,
repo_url: Option<&str>,
session_id: Option<&str>,
host: HostInputs<'_>,
) -> Result<PrepReport, PrepError> {
let home = host.home;
let ledger_before = crate::session_manager::provisioning_ledger::snapshot(project_dir);
crate::core::manifest::invalidate_stack_detection(project_dir);
let memory_reachable =
crate::core::memory_reachable::resolve_memory_reachable(host.memory_reachable);
let config = crate::core::config::MpmConfig::load(&fw.root);
let profile = crate::core::session_profile::resolve(project_dir, &config);
let crate::core::content_source::LaunchContent {
framework: content,
roster,
} = crate::core::content_source::launch_content_for(project_dir)
.map_err(|source| PrepError::Content { source })?;
let catalog_root = crate::content::catalog_root_for(&fw.root);
let manifest_sources =
crate::core::manifest::ManifestSources::resolve(project_dir, &catalog_root);
let manifest = crate::core::manifest::resolve_manifest(&manifest_sources);
let plan = crate::core::manifest::HarnessPlan::from_manifest(&manifest, fw, &catalog_root);
let mut roster_errors: Vec<String> = Vec::new();
crate::core::provisioning_stage::emit(
crate::core::provisioning_stage::ProvisioningStage::DeployingAgents,
);
let stale_source = plan.agent_source == fw.agents && roster.is_err();
let deploy = if stale_source {
DeployResult::default()
} else {
match deploy_agents_filtered(
&plan.agent_source,
&fw.agent_deploy_dir(),
&fw.skill_deploy_dir(),
|name| plan.agent_selected(name),
) {
Ok(result) => result,
Err(err) => {
tracing::error!(
project_dir = %project_dir.display(),
"agent deploy FAILED — session will launch WITHOUT the tm/mpm agent \
roster: {err}. Identity/output-style provisioning continues regardless."
);
roster_errors.push(format!("agent deploy failed: {err}"));
DeployResult::default()
}
}
};
match retract_framework_agents(&project_dir.join(".claude").join("agents")) {
Ok(retracted) if !retracted.removed.is_empty() => {
tracing::info!(
project_dir = %project_dir.display(),
count = retracted.removed.len(),
"retracted per-workspace bundled agents; the roster now lives in the \
tm-managed config dir (issue #4409)"
);
}
Ok(_) => {}
Err(err) => {
tracing::warn!(
project_dir = %project_dir.display(),
"workspace bundled-agent retraction failed: {err}. The stale project-tier \
copies may shadow the canonical roster until this is resolved."
);
roster_errors.push(format!("agent retraction failed: {err}"));
}
}
let mut quarantine_report = None;
match quarantine_shadows::quarantine_workspace_shadows(fw, project_dir) {
Ok(report) => {
if let Some(summary) = quarantine_shadows::summarize(&report) {
tracing::warn!(project_dir = %project_dir.display(), "{summary}");
}
if !report.failed.is_empty() {
roster_errors.push(format!(
"agent quarantine could not move {} shadowing file(s)",
report.failed.len()
));
}
quarantine_report = Some(report);
}
Err(quarantine_shadows::ShadowQuarantineError::Roster(err)) if err.is_not_installed() => {
trusty_agents_common::agent_content::mark_not_installed_reported();
roster_errors.push(err.to_string());
}
Err(err) => {
tracing::warn!(
project_dir = %project_dir.display(),
"shadowing-agent quarantine refused to run: {err}. Untracked project-tier \
copies may still shadow the canonical roster."
);
roster_errors.push(format!("agent quarantine refused: {err}"));
}
}
if let Err(err) = crate::core::skill_source::ensure_skill_source_fresh(fw, &content) {
tracing::warn!("failed to refresh skill source directory: {err}");
}
let skill_deploy = skills::deploy_session_skills(
fw,
&plan,
project_dir,
&deploy.declared_skills,
&mut roster_errors,
);
crate::core::provisioning_stage::emit(
crate::core::provisioning_stage::ProvisioningStage::BuildingInstructions,
);
let input = PipelineInput {
project_dir: project_dir.to_path_buf(),
claude_md_path: project_dir.join("CLAUDE.md"),
home: home.map(Path::to_path_buf),
};
let instructions = build_instructions(&input).map_err(|e| match e {
crate::core::instruction_pipeline::PipelineError::Io { path, source } => {
PrepError::Instructions { path, source }
}
})?;
let managed_config = crate::core::trusty_tools_config::managed_claude_config_dir();
crate::core::ancestor_claude_md::warn_ancestors(project_dir, home, managed_config.as_deref());
let selected_style = crate::core::output_style::select_style(
project_dir,
explicit_style,
&config,
|| plan.style.clone(),
profile,
);
let effective_style: Option<String> = selected_style.id.clone();
let mut style_notice = selected_style.warning;
let active_style_id = match crate::core::output_style::native_style_id(
&content,
project_dir,
&selected_style.style,
) {
Ok(id) => id,
Err(err) => {
let warning = format!(
"output style '{}': cannot write its composite with the trusty-mpm floor \
({err}); using `{OUTPUT_STYLE}` for native launches",
selected_style.style.id()
);
tracing::warn!("{warning}");
style_notice.get_or_insert(warning);
OUTPUT_STYLE.to_string()
}
};
if let Err(err) = write_output_style(project_dir, Some(&active_style_id)) {
tracing::warn!("failed to set trusty-mpm output style: {err}");
}
if let Err(err) = supervisor_model::write_supervisor_model(project_dir, profile) {
tracing::warn!("failed to update the supervisor model: {err}");
}
let resolved_prompt = build_system_prompt_for_profile(
&content,
project_dir,
effective_style.as_deref(),
native_supported,
profile,
);
let stash_dir = crate::core::harness_root::harness_dir(project_dir);
let stash = stash_dir.join("last-instructions.md");
match std::fs::create_dir_all(&stash_dir)
.and_then(|()| std::fs::write(&stash, &resolved_prompt))
{
Ok(()) => {}
Err(e) => tracing::warn!(
"could not refresh the instruction stash at {} (non-fatal): {e}",
stash.display()
),
}
if let Err(err) = write_auto_memory_enabled(project_dir, !memory_reachable) {
tracing::warn!("failed to write Claude Code `{AUTO_MEMORY_KEY}`: {err}");
} else if !memory_reachable {
tracing::info!(
"trusty-memory is not reachable — wrote `{AUTO_MEMORY_KEY}: true` to restore \
Claude Code auto-memory as this project's fallback (#7685)"
);
}
if let Err(err) = write_enabled_plugins(
project_dir,
crate::core::trusty_tools_config::managed_claude_config_dir().as_deref(),
) {
tracing::warn!("failed to write the project plugin allowlist: {err}");
}
match crate::core::skill_overrides::write_skill_overrides(project_dir) {
Ok(crate::core::skill_overrides::SkillOverridesOutcome::Written(added)) => {
tracing::info!(
"turned off {} skill(s) this stack cannot use: {added:?}",
added.len()
);
}
Ok(_) => {}
Err(err) => tracing::warn!("failed to write stack-profile skillOverrides: {err}"),
}
let user_settings = home.map(|h| h.join(".claude").join("settings.json"));
if let Err(err) = ensure_status_line_in(project_dir, user_settings.as_deref()) {
tracing::warn!("failed to write statusLine config: {err}");
}
let additions = project_hooks::project_managed_hook_additions_with_pm_guard(
host.hook_exe,
config.hooks.prompt_context,
plan.divert_enabled,
crate::core::prompt_self_improvement::enabled_for(project_dir),
config.pm_guard.enabled,
);
let hooks_written = match write_project_hooks_with(project_dir, additions) {
Ok(()) => true,
Err(err) => {
tracing::warn!("failed to write trusty-mpm project hooks: {err}");
false
}
};
crate::core::provisioning_stage::emit(
crate::core::provisioning_stage::ProvisioningStage::ConfiguringMcp,
);
if plan.inject_trusty_search {
let _ = register_project_index(project_dir);
} else {
tracing::debug!("manifest disables the trusty-search project index");
}
if plan.inject_trusty_memory {
maybe_register_palace_alias(project_dir, repo_url, host.palace_registry);
}
if let Err(err) = preseed_workspace_trust_home(home, project_dir) {
tracing::warn!("failed to pre-seed workspace trust: {err}");
}
if let Err(err) = remove_global_trusty_memory_hooks(home) {
tracing::warn!("failed to remove global trusty-memory hooks: {err}");
}
let output_style = match deploy_output_style(&content, &fw.claude_home_dir()) {
Ok(path) => Some(path),
Err(err) => {
tracing::warn!("failed to deploy trusty-mpm output style file: {err}");
None
}
};
if let Err(err) = deploy_output_style(&content, project_dir) {
tracing::warn!("failed to deploy project-tier trusty-mpm output style: {err}");
}
match crate::core::harness_exclude::ensure_scaffold_excluded(project_dir) {
Ok(added) if !added.is_empty() => tracing::info!(
"added {} to the shared info/exclude (#8758)",
added.join(", ")
),
Ok(_) => {}
Err(err) => tracing::warn!("harness scaffolding NOT excluded (non-fatal, #8758): {err}"),
}
if let Err(err) =
crate::session_manager::provisioning_ledger::record(project_dir, &ledger_before)
{
tracing::warn!("failed to record the provisioning ledger (#8663): {err}");
}
let catchup_context = if config.catchup.auto {
let memory_socket = trusty_common::memory_rpc::resolve_memory_socket_or_unreachable();
let opts = crate::core::catchup::CatchupOptions {
project_dir: project_dir.to_path_buf(),
memory_socket,
include_git: config.catchup.include_git,
include_palace: config.catchup.include_palace,
git_limit: config.catchup.git_limit,
drawer_limit: config.catchup.drawer_limit,
full: false,
};
let state_root = Some(fw.root.clone());
let ctx = crate::core::catchup::run_catchup_blocking_in(opts, true, state_root);
if ctx.is_empty() { None } else { Some(ctx) }
} else {
None
};
for legacy_base in [
project_dir,
&crate::core::harness_root::harness_root(project_dir),
] {
match crate::core::instruction_pipeline::remove_legacy_compiled_prompt(legacy_base) {
Ok(true) => tracing::info!(
base = %legacy_base.display(),
"removed the pre-#4832 per-project compiled prompt"
),
Ok(false) => {}
Err(e) => tracing::warn!(
base = %legacy_base.display(),
"could not remove the pre-#4832 compiled prompt (non-fatal): {e}"
),
}
}
let scope = crate::core::harness_root::session_scope(session_id);
let compiled = crate::core::instruction_pipeline::compiled_prompt_path(project_dir, &scope);
crate::core::instruction_pipeline::write_compiled_prompt_recording_in(
&content,
&fw.root,
&compiled,
&resolved_prompt,
)
.map_err(|source| PrepError::Instructions {
path: compiled.clone(),
source,
})?;
let mut asset_notices =
asset_notices::launch_asset_notices(fw, project_dir, quarantine_report.as_ref());
asset_notices.extend(style_notice);
Ok(PrepReport {
deploy,
skill_deploy,
instructions,
stash,
compiled_prompt: compiled,
output_style,
hooks_written,
catchup_context,
roster_errors,
asset_notices,
memory_reachable,
profile,
})
}