mod asset_notices;
mod entry;
pub use entry::{
prepare_isolated_session, prepare_session, prepare_session_for_managed,
prepare_session_for_repair, 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::{
build_system_prompt_for, build_system_prompt_for_with_roster,
build_system_prompt_for_with_style, build_system_prompt_for_with_style_and_native,
};
#[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;
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_prompt_feedback,
};
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,
}
#[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,
},
}
impl PrepError {
pub fn is_fatal(&self) -> bool {
matches!(self, Self::Instructions { .. })
}
}
#[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>,
}
impl<'a> HostInputs<'a> {
pub(super) fn with_home(home: Option<&'a Path>) -> Self {
Self {
home,
hook_exe: None,
memory_reachable: None,
}
}
}
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;
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 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 deploy = 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(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) {
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 effective_style: Option<String> = explicit_style
.map(str::to_owned)
.or_else(|| config.style.active.clone())
.or_else(|| plan.style.clone());
let resolved_prompt = build_system_prompt_for_with_style_and_native(
project_dir,
effective_style.as_deref(),
native_supported,
);
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()
),
}
let active_style_id = match crate::core::output_style::resolve_active_style(
&config,
effective_style.as_deref(),
) {
Ok(style) => style.id,
Err(err) => {
tracing::warn!(
%err,
"falling back to the default output style for settings.json"
);
crate::core::bundle::DEFAULT_OUTPUT_STYLE_ID
}
};
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) = 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}"),
}
if let Err(err) = ensure_status_line(project_dir) {
tracing::warn!("failed to write statusLine config: {err}");
}
let hooks_written = match write_project_hooks_with_prompt_feedback(
project_dir,
host.hook_exe,
config.hooks.prompt_context,
plan.divert_enabled,
crate::core::prompt_self_improvement::enabled_for(project_dir),
) {
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);
}
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(&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(project_dir) {
tracing::warn!("failed to deploy project-tier trusty-mpm output style: {err}");
}
if let Err(err) = crate::core::scaffold_gitignore::ensure_scaffold_gitignored(project_dir) {
tracing::warn!("failed to update .gitignore for harness scaffolding: {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 ctx = crate::core::catchup::run_catchup_blocking(opts, true);
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(
&fw.root,
&compiled,
&resolved_prompt,
)
.map_err(|source| PrepError::Instructions {
path: compiled.clone(),
source,
})?;
let asset_notices =
asset_notices::launch_asset_notices(fw, project_dir, quarantine_report.as_ref());
Ok(PrepReport {
deploy,
skill_deploy,
instructions,
stash,
compiled_prompt: compiled,
output_style,
hooks_written,
catchup_context,
roster_errors,
asset_notices,
memory_reachable,
})
}
pub fn build_system_prompt() -> Option<String> {
let composed = crate::core::instruction_pipeline::assemble_system_prompt();
let trimmed = composed.trim_end();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}