use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{Context as _, Result};
use tracing::{info, warn};
use trusty_mpm::core::session_launch::prepare_isolated_session;
use trusty_mpm::runtime::{RuntimeKind, build_adapter};
use trusty_mpm::session_manager::real_tmux::RealTmuxDriver;
use trusty_mpm::session_manager::{ManagedSessionId, ManagedTmuxDriver, SessionManager};
pub(crate) const POLL_INTERVAL: Duration = Duration::from_secs(2);
pub(crate) const TRANSCRIPT_LINES: usize = 400;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PollDecision {
Continue,
TimedOut,
}
pub(crate) fn poll_decision(elapsed: Duration, timeout: Duration) -> PollDecision {
if elapsed >= timeout {
PollDecision::TimedOut
} else {
PollDecision::Continue
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LaunchOutcome {
Exited,
TimedOut,
}
impl LaunchOutcome {
pub(crate) fn status(&self) -> &'static str {
match self {
LaunchOutcome::Exited => "exited",
LaunchOutcome::TimedOut => "timed-out",
}
}
}
#[derive(Debug)]
pub(crate) struct LaunchReport {
pub(crate) outcome: LaunchOutcome,
pub(crate) tmux_name: String,
pub(crate) transcript: String,
}
pub(crate) async fn new_session_manager(state_dir: &Path) -> Result<SessionManager> {
let tmux = RealTmuxDriver::discover()
.context("tmux not found — the metaharness POC requires tmux to host the claude session")?;
let tmux: Arc<dyn ManagedTmuxDriver> = Arc::new(tmux);
SessionManager::new(state_dir, tmux)
.await
.context("failed to initialise the standalone session manager")
}
pub(crate) async fn launch_and_wait(
mgr: &SessionManager,
project_dir: &Path,
task: Option<&str>,
timeout: Duration,
) -> Result<LaunchReport> {
match prepare_isolated_session(project_dir, None) {
Ok(report) => info!(
agents = report.deploy.deployed.len(),
project = %project_dir.display(),
"meta run: prepared session (agents/skills/CLAUDE.md/MCP deployed)"
),
Err(e) if e.is_fatal() => anyhow::bail!("{e}"),
Err(e) => warn!(
project = %project_dir.display(),
"meta run: session preparation failed (continuing): {e}"
),
}
let id = ManagedSessionId::new();
let task_label = task.unwrap_or("metaharness session").to_string();
let record = mgr
.create_with_id(
id,
task_label,
Some(project_dir.to_path_buf()),
None,
None,
None,
None,
RuntimeKind::ClaudeCode,
false,
false, )
.await
.context("failed to create the tmux session for the metaharness")?;
info!(
id = %record.id,
tmux = %record.tmux_name,
"meta run: tmux session created (no provision — local dir used in place)"
);
let adapter = build_adapter(RuntimeKind::ClaudeCode, mgr.tmux_driver());
adapter
.spawn(
&record.tmux_name,
project_dir,
&record.task,
&record.id.to_string(),
&[],
)
.context("failed to spawn the claude runtime in the tmux pane")?;
info!(tmux = %record.tmux_name, "meta run: claude runtime spawned");
if let Some(task) = task {
match mgr.inject_task_when_ready(&record.id, task).await {
Ok(true) => info!(tmux = %record.tmux_name, "meta run: demo task delivered to session"),
Ok(false) => {
warn!(tmux = %record.tmux_name, "meta run: runtime never became ready; demo task not delivered")
}
Err(e) => warn!(tmux = %record.tmux_name, "meta run: failed to inject demo task: {e}"),
}
}
let start = Instant::now();
let outcome = loop {
let alive = mgr.observe(&record.id, 1).await.map(|o| o.runtime_active);
match alive {
Ok(false) => break LaunchOutcome::Exited,
Ok(true) => {}
Err(e) => warn!(tmux = %record.tmux_name, "meta run: liveness probe failed: {e}"),
}
if poll_decision(start.elapsed(), timeout) == PollDecision::TimedOut {
break LaunchOutcome::TimedOut;
}
tokio::time::sleep(POLL_INTERVAL).await;
};
info!(
tmux = %record.tmux_name,
outcome = outcome.status(),
elapsed_secs = start.elapsed().as_secs(),
"meta run: poll loop finished"
);
let transcript = mgr
.capture_pane(&record.id, TRANSCRIPT_LINES)
.await
.unwrap_or_default();
if let Err(e) = mgr.decommission(&record.id, None).await {
warn!(tmux = %record.tmux_name, "meta run: decommission failed (best-effort): {e}");
}
Ok(LaunchReport {
outcome,
tmux_name: record.tmux_name,
transcript,
})
}
pub(crate) fn state_dir(project_dir: &Path) -> PathBuf {
project_dir.join(".trusty-mpm").join("meta-sessions")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn poll_decision_continues_before_deadline() {
assert_eq!(
poll_decision(Duration::from_secs(5), Duration::from_secs(10)),
PollDecision::Continue
);
}
#[test]
fn poll_decision_times_out_at_deadline() {
assert_eq!(
poll_decision(Duration::from_secs(10), Duration::from_secs(10)),
PollDecision::TimedOut
);
}
#[test]
fn poll_decision_times_out_after_deadline() {
assert_eq!(
poll_decision(Duration::from_secs(11), Duration::from_secs(10)),
PollDecision::TimedOut
);
}
#[test]
fn poll_decision_zero_timeout_times_out_immediately() {
assert_eq!(
poll_decision(Duration::from_secs(0), Duration::from_secs(0)),
PollDecision::TimedOut
);
}
#[test]
fn launch_outcome_status_tokens() {
assert_eq!(LaunchOutcome::Exited.status(), "exited");
assert_eq!(LaunchOutcome::TimedOut.status(), "timed-out");
}
#[test]
fn state_dir_is_project_scoped() {
let dir = state_dir(Path::new("/work/proj"));
assert_eq!(
dir,
Path::new("/work/proj/.trusty-mpm/meta-sessions"),
"state dir must be project-scoped"
);
}
}