use serde::Deserialize;
use crate::cli::SessionAction;
use crate::commands::project::resolve_dir;
use crate::formatters::session::{delegation_roster_line, deploy_summary_line};
pub(crate) async fn start_session(
daemon: &trusty_mpm::client::DaemonClient,
dir: Option<String>,
) -> anyhow::Result<()> {
let path = resolve_dir(dir)?;
let path = path.canonicalize().unwrap_or(path);
if let Some((_source_id, _workspace, git_root)) = crate::commands::guided::derive_project(&path)
{
let new_action = SessionAction::New {
repo: git_root.to_string_lossy().to_string(),
git_ref: "HEAD".to_string(),
task: String::new(),
name_hint: None,
runtime: trusty_mpm::runtime::RuntimeKind::default(),
no_inject: false,
deliverable: None,
};
crate::commands::managed_route::run(daemon, &new_action).await?;
return Ok(());
}
refuse_outside_a_git_project(&path)?;
let fw = trusty_mpm::core::paths::FrameworkPaths::default();
start_session_in_place(daemon, &path, &fw, dirs::home_dir().as_deref()).await
}
pub(crate) fn inplace_session_spec(
cwd: &std::path::Path,
config_root: &std::path::Path,
prompt: &InplacePrompt,
) -> trusty_mpm::runtime::launch_spec::LaunchSpec {
let mut spec = trusty_mpm::runtime::cli_launch::inplace_spec(
cwd,
&prompt.file,
trusty_mpm::core::alt_screen::configured_alternate_screen_at(config_root),
);
spec.env_set.push(prompt.stamp.clone());
spec
}
#[derive(Debug, Clone)]
pub(crate) struct InplacePrompt {
pub(crate) file: std::path::PathBuf,
pub(crate) stamp: (String, String),
}
pub(crate) fn refuse_outside_a_git_project(path: &std::path::Path) -> anyhow::Result<()> {
if trusty_mpm::core::harness_root::harness_root_for(path).is_some() {
return Ok(());
}
anyhow::bail!(
"{} is not inside a git repository, so there is no project to hold this \
session's state.\n\
`tm` writes a session's agents, instructions and compiled prompt under the \
project's `.trusty-mpm/`; starting here would scatter them into this \
directory instead. Run `git init` here, or `cd` into the repository you \
meant to work in, and retry.",
path.display()
)
}
async fn start_session_in_place(
daemon: &trusty_mpm::client::DaemonClient,
path: &std::path::Path,
fw: &trusty_mpm::core::paths::FrameworkPaths,
home: Option<&std::path::Path>,
) -> anyhow::Result<()> {
let native = trusty_mpm::core::output_style::claude_supports_native_output_style();
match trusty_mpm::core::session_launch::prepare_session_with_home(fw, path, None, native, home)
{
Ok(report) => {
println!(
"{}",
deploy_summary_line(
"Agents",
report.deploy.deployed.len(),
report.deploy.skipped.len(),
report.deploy.unchanged.len(),
)
);
println!(
"{}",
deploy_summary_line(
"Skills",
report.skill_deploy.deployed.len(),
report.skill_deploy.skipped.len(),
report.skill_deploy.unchanged.len(),
)
);
if report.instructions.claude_md_created {
println!(" Created CLAUDE.md stub in {}", path.display());
}
println!(
"{}",
delegation_roster_line(
report.instructions.agent_count,
&report.instructions.unreadable_agent_paths,
)
);
for path in &report.instructions.unreadable_agent_paths {
eprintln!(
"error: agent roster incomplete: {} could not be read; \
any agent it holds is undelegatable this session",
path.display()
);
}
println!(
" Merged instructions written to {}",
report.stash.display()
);
if let Some(ctx) = report.catchup_context {
println!("\n---\n\n## Recent Activity (catch-up)\n\n{ctx}");
}
for err in &report.roster_errors {
eprintln!("error: roster provisioning gap: {err}");
}
for notice in &report.asset_notices {
println!(" assets: {notice}");
}
}
Err(err) if err.is_fatal() => anyhow::bail!("{err}"),
Err(err) => eprintln!("warning: session preparation failed: {err}"),
}
let prompt = inplace_prompt_file(path, &std::env::temp_dir())?;
#[derive(Deserialize)]
struct Body {
#[serde(default)]
name: String,
}
let body: Body = daemon
.post("/sessions")
.json(&serde_json::json!({
"project": path,
"project_path": path,
}))
.send()
.await?
.error_for_status()?
.json()
.await?;
let workdir = path.to_string_lossy().to_string();
let new_session =
trusty_mpm::core::tmux::create_managed_session(None, &body.name, Some(&workdir));
match new_session {
Ok(outcome) if outcome.output.status.success() => {
if !outcome.options_verified {
eprintln!(
"warning: tmux scrollback limit could not be verified for session {} — the \
pane may be capped at tmux's factory 2000-line history-limit instead of \
the configured value; see issue #3386",
body.name
);
}
let root = fw.crate_config_root();
let spec = inplace_session_spec(path, &root, &prompt);
let spec_dir = trusty_mpm::runtime::launch_spec::LaunchSpec::root_at(&root);
match trusty_mpm::runtime::cli_launch::send_spec_launch(&body.name, &spec, &spec_dir) {
Ok(()) => {
println!("started session {} (tmux + claude)", body.name);
}
Err(e) => {
eprintln!(
"warning: tmux session {} created but failed to start claude: {e}",
body.name
);
println!("started session {}", body.name);
}
}
}
Ok(_) | Err(_) => {
eprintln!("{}", manual_launch_hint(&body.name, &workdir, &prompt.file));
println!("started session {}", body.name);
}
}
Ok(())
}
pub(crate) fn manual_launch_hint(
session: &str,
workdir: &str,
prompt_file: &std::path::Path,
) -> String {
format!(
"warning: failed to create tmux session {session}; run \
`claude --append-system-prompt-file '{}'` manually in {workdir}",
prompt_file.display()
)
}
fn inplace_prompt_file(
path: &std::path::Path,
dir: &std::path::Path,
) -> anyhow::Result<InplacePrompt> {
let content = trusty_mpm::core::content_source::framework_content_for(path)
.map_err(|err| anyhow::anyhow!("cannot compose the PM instructions: {err}"))?;
let cli = trusty_mpm::core::session_launch::cli_launch(&content, path, None);
let file =
trusty_mpm::core::model_inject::write_pm_prompt_file_in(dir, &cli.prompt, path, "launch")?;
Ok(InplacePrompt {
file,
stamp: trusty_mpm::core::session_profile::launch_env(cli.profile),
})
}
#[cfg(test)]
#[path = "start_tests.rs"]
mod tests;