mod custom_mcp;
mod native_mcp;
mod palace_alias;
mod project_hooks;
mod search_index;
mod settings;
mod sync_assets;
#[cfg(test)]
mod tests;
mod workstream_label;
#[cfg(test)]
#[path = "native_mcp_tests.rs"]
mod native_mcp_tests;
#[cfg(test)]
#[path = "custom_mcp_tests.rs"]
mod custom_mcp_tests;
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;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::core::agent_deployer::{DeployResult, deploy_agents_filtered};
use crate::core::agent_skill_codeploy::{co_deploy_skill_set, log_declared_skills};
use crate::core::instruction_pipeline::{PipelineInput, PipelineOutput, build_instructions};
use crate::core::paths::FrameworkPaths;
use crate::core::skill_deployer::DeployStats;
use crate::core::skill_tiers::{
deploy_all_skill_tiers, list_project_custom_stems, list_source_stems,
};
use custom_mcp::inject_custom_trusty_mcps;
use native_mcp::inject_native_trusty_mcps;
use search_index::{inject_trusty_search_mcp, register_project_index};
use settings::{
deploy_output_style, inject_trusty_memory_mcp, preseed_workspace_trust_home,
remove_global_trusty_memory_hooks, write_output_style, write_project_hooks, write_status_line,
};
pub(crate) use settings::{OUTPUT_STYLE, is_stale_statusline_command, resolve_statusline_command};
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 output_style: Option<PathBuf>,
pub hooks_written: bool,
pub catchup_context: Option<String>,
pub roster_errors: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum PrepError {
#[error("agent deploy failed: {0}")]
Deploy(String),
#[error("skill deploy failed: {0}")]
SkillDeploy(String),
#[error("instruction pipeline failed: {0}")]
Instructions(#[from] crate::core::instruction_pipeline::PipelineError),
#[error("io error for {path}: {source}")]
Io {
path: PathBuf,
source: std::io::Error,
},
}
pub fn prepare_session(fw: &FrameworkPaths, project_dir: &Path) -> Result<PrepReport, PrepError> {
prepare_session_with_style(fw, project_dir, None)
}
pub fn prepare_session_with_repo_url(
fw: &FrameworkPaths,
project_dir: &Path,
repo_url: Option<&str>,
) -> Result<PrepReport, PrepError> {
let native = crate::core::output_style::claude_supports_native_output_style();
prepare_session_inner(fw, project_dir, None, native, repo_url)
}
pub fn ensure_status_line(project_dir: &Path) -> Result<(), PrepError> {
settings::write_status_line(project_dir)
}
pub fn prepare_session_with_style(
fw: &FrameworkPaths,
project_dir: &Path,
explicit_style: Option<&str>,
) -> Result<PrepReport, PrepError> {
let native = crate::core::output_style::claude_supports_native_output_style();
prepare_session_with_style_and_native(fw, project_dir, explicit_style, native)
}
pub fn prepare_session_with_style_and_native(
fw: &FrameworkPaths,
project_dir: &Path,
explicit_style: Option<&str>,
native_supported: bool,
) -> Result<PrepReport, PrepError> {
prepare_session_inner(fw, project_dir, explicit_style, native_supported, None)
}
fn prepare_session_inner(
fw: &FrameworkPaths,
project_dir: &Path,
explicit_style: Option<&str>,
native_supported: bool,
repo_url: Option<&str>,
) -> Result<PrepReport, PrepError> {
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, &fw.root, &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.claude_agents_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()
}
};
if let Err(err) = crate::core::skill_source::ensure_skill_source_fresh(fw) {
tracing::warn!("failed to refresh skill source directory: {err}");
}
let stale =
crate::core::skill_staleness::stale_skills(&plan.skill_source, &fw.claude_skills_dir());
if !stale.is_empty() {
tracing::warn!(
project_dir = %project_dir.display(),
stale_skills = %stale.join(", "),
"deployed skills were stale relative to bundled assets — refreshing now \
(run `tm doctor` to audit; long-lived worktrees drift until re-provisioned)"
);
}
let bundled_stems = list_source_stems(&plan.skill_source).unwrap_or_default();
let user_stems = list_source_stems(&fw.user_skill_source_dir()).unwrap_or_default();
let project_stems = list_project_custom_stems(&fw.claude_skills_dir()).unwrap_or_default();
let co_deploy_skills = co_deploy_skill_set(&deploy.declared_skills);
log_declared_skills(
&deploy.declared_skills,
&project_stems,
&user_stems,
&bundled_stems,
);
crate::core::provisioning_stage::emit(
crate::core::provisioning_stage::ProvisioningStage::DeployingSkills,
);
let skill_deploy = match deploy_all_skill_tiers(
&plan.skill_source,
&fw.user_skill_source_dir(),
&fw.claude_skills_dir(),
|name| plan.skill_selected(name) || co_deploy_skills.contains(name),
) {
Ok(result) => result.stats,
Err(err) => {
tracing::error!(
project_dir = %project_dir.display(),
"skill deploy FAILED — session will launch WITHOUT the tm/mpm skill \
set: {err}. Identity/output-style provisioning continues regardless."
);
roster_errors.push(format!("skill deploy failed: {err}"));
DeployStats::default()
}
};
crate::core::provisioning_stage::emit(
crate::core::provisioning_stage::ProvisioningStage::BuildingInstructions,
);
let input = PipelineInput {
framework_instructions_path: fw.framework_instructions_path(),
agents_dir: fw.claude_agents_dir(),
claude_md_path: project_dir.join("CLAUDE.md"),
};
let instructions = build_instructions(&input)?;
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 = project_dir.join(".trusty-mpm");
std::fs::create_dir_all(&stash_dir).map_err(|source| PrepError::Io {
path: stash_dir.clone(),
source,
})?;
let stash = stash_dir.join("last-instructions.md");
std::fs::write(&stash, &resolved_prompt).map_err(|source| PrepError::Io {
path: stash.clone(),
source,
})?;
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_status_line(project_dir) {
tracing::warn!("failed to write statusLine config: {err}");
}
let hooks_written = match write_project_hooks(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_memory {
if let Err(err) = inject_trusty_memory_mcp(project_dir, repo_url) {
tracing::warn!("failed to inject trusty-memory MCP server: {err}");
}
} else {
tracing::debug!("manifest disables trusty-memory MCP injection");
}
if plan.inject_trusty_search {
let pinned_index = register_project_index(project_dir);
if let Err(err) = inject_trusty_search_mcp(project_dir, pinned_index.as_deref()) {
tracing::warn!("failed to inject trusty-search MCP server: {err}");
}
} else {
tracing::debug!("manifest disables trusty-search MCP injection");
}
if let Err(err) = inject_native_trusty_mcps(project_dir) {
tracing::warn!("failed to inject native trusty MCP servers: {err}");
}
let project_scope_mcp_names =
match inject_custom_trusty_mcps(project_dir, &plan.custom_mcp_servers) {
Ok(names) => names,
Err(err) => {
tracing::warn!("failed to bridge custom MCP servers: {err}");
BTreeSet::new()
}
};
if let Err(err) = preseed_workspace_trust_home(project_dir, &project_scope_mcp_names) {
tracing::warn!("failed to pre-seed workspace trust: {err}");
}
if let Err(err) = remove_global_trusty_memory_hooks() {
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_url = trusty_common::mcp::memory_rpc::resolve_memory_base_url_or_unreachable();
let opts = crate::core::catchup::CatchupOptions {
project_dir: project_dir.to_path_buf(),
memory_url,
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
};
Ok(PrepReport {
deploy,
skill_deploy,
instructions,
stash,
output_style,
hooks_written,
catchup_context,
roster_errors,
})
}
pub fn build_system_prompt() -> Option<String> {
let home = dirs::home_dir()?;
let path = home
.join(".trusty-mpm")
.join("framework")
.join("instructions")
.join("INSTRUCTIONS.md");
if let Ok(contents) = std::fs::read_to_string(&path) {
let trimmed = contents.trim_end();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
let generated = crate::core::instruction_pipeline::install_system_prompt().ok()?;
let contents = std::fs::read_to_string(&generated).ok()?;
let trimmed = contents.trim_end();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub fn build_system_prompt_for(project_dir: &Path) -> String {
build_system_prompt_for_with_style(project_dir, None)
}
pub fn build_system_prompt_for_with_style(
project_dir: &Path,
explicit_style: Option<&str>,
) -> String {
let native = crate::core::output_style::claude_supports_native_output_style();
build_system_prompt_for_with_style_and_native(project_dir, explicit_style, native)
}
pub fn build_system_prompt_for_with_style_and_native(
project_dir: &Path,
explicit_style: Option<&str>,
native_supported: bool,
) -> String {
let prompt = crate::core::instruction_overrides::resolve_pm_prompt(project_dir);
crate::core::output_style::apply_output_style_to_prompt_with_native(
project_dir,
explicit_style,
prompt,
native_supported,
)
}