mod claude_code;
pub mod launch_report;
pub mod launch_spec;
mod managed_launch;
mod pane_handshake;
mod prompt_file;
mod tcode;
#[cfg(test)]
pub(crate) mod test_helpers;
pub(crate) use claude_code::session_id_exists;
pub(crate) use claude_code::encode_project_dir;
pub use claude_code::{ClaudeCodeAdapter, InPlaceResumeCommand, build_inplace_resume_command};
pub(crate) use managed_launch::managed_env_unset;
pub use tcode::TcodeAdapter;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::session_manager::ManagedTmuxDriver;
#[derive(Debug, Error)]
pub enum RuntimeError {
#[error("spawn failed: {0}")]
Spawn(String),
#[error("tmux unavailable: {0}")]
TmuxUnavailable(String),
#[error("binary not found: {0}")]
BinaryNotFound(String),
}
pub trait RuntimeAdapter: Send + Sync {
fn spawn(
&self,
tmux_name: &str,
cwd: &Path,
task: &str,
session_id: &str,
gh_env: &[(String, String)],
) -> Result<(), RuntimeError>;
#[allow(clippy::too_many_arguments)]
fn spawn_resume(
&self,
tmux_name: &str,
pane_id: Option<&str>,
cwd: &Path,
task: &str,
claude_session_id: Option<&str>,
session_id: &str,
gh_env: &[(String, String)],
) -> Result<(), RuntimeError> {
let _ = (pane_id, claude_session_id);
self.spawn(tmux_name, cwd, task, session_id, gh_env)
}
fn identify(&self) -> &str;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum RuntimeKind {
#[value(name = "claude-code")]
ClaudeCode,
#[value(name = "tcode")]
Tcode,
}
impl Default for RuntimeKind {
fn default() -> Self {
RuntimeKind::ClaudeCode
}
}
impl RuntimeKind {
pub fn as_str(&self) -> &'static str {
match self {
RuntimeKind::ClaudeCode => "claude-code",
RuntimeKind::Tcode => "tcode",
}
}
}
impl FromStr for RuntimeKind {
type Err = RuntimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"claude-code" | "claude_code" | "claude" => Ok(RuntimeKind::ClaudeCode),
"tcode" | "trusty-code" => Ok(RuntimeKind::Tcode),
other => Err(RuntimeError::Spawn(format!(
"unknown runtime '{other}'; supported values: claude-code, tcode"
))),
}
}
}
pub fn build_adapter(
kind: RuntimeKind,
tmux: Arc<dyn ManagedTmuxDriver + Send + Sync>,
memory_reachable: Option<bool>,
framework_root: &std::path::Path,
) -> Box<dyn RuntimeAdapter> {
match kind {
RuntimeKind::ClaudeCode => Box::new(ClaudeCodeAdapter::new(
tmux,
memory_reachable,
framework_root,
)),
RuntimeKind::Tcode => Box::new(TcodeAdapter::new(tmux)),
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::FakeTmux;
use super::*;
#[test]
fn runtime_kind_default_is_claude_code() {
assert_eq!(RuntimeKind::default(), RuntimeKind::ClaudeCode);
}
#[test]
fn runtime_kind_as_str_matches_identify() {
assert_eq!(RuntimeKind::ClaudeCode.as_str(), "claude-code");
assert_eq!(RuntimeKind::Tcode.as_str(), "tcode");
}
#[test]
fn runtime_kind_from_str_accepts_known() {
assert_eq!(
"claude-code".parse::<RuntimeKind>().unwrap(),
RuntimeKind::ClaudeCode
);
assert_eq!(
"CLAUDE".parse::<RuntimeKind>().unwrap(),
RuntimeKind::ClaudeCode
);
assert_eq!("tcode".parse::<RuntimeKind>().unwrap(), RuntimeKind::Tcode);
assert_eq!(
"trusty-code".parse::<RuntimeKind>().unwrap(),
RuntimeKind::Tcode
);
}
#[test]
fn runtime_kind_from_str_rejects_unknown() {
let err = "gpt".parse::<RuntimeKind>().unwrap_err();
assert!(err.to_string().contains("unknown runtime"));
}
#[test]
fn runtime_kind_serde_round_trip() {
for kind in [RuntimeKind::ClaudeCode, RuntimeKind::Tcode] {
let json = serde_json::to_string(&kind).unwrap();
let back: RuntimeKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
}
assert_eq!(
serde_json::to_string(&RuntimeKind::ClaudeCode).unwrap(),
"\"claude-code\""
);
assert_eq!(
serde_json::to_string(&RuntimeKind::Tcode).unwrap(),
"\"tcode\""
);
}
#[test]
fn runtime_kind_value_enum_matches_wire() {
for kind in [RuntimeKind::ClaudeCode, RuntimeKind::Tcode] {
let value = kind.to_possible_value().expect("kind has a CLI value");
assert_eq!(value.get_name(), kind.as_str());
let parsed = kind.as_str().parse::<RuntimeKind>().expect("parses");
assert_eq!(parsed, kind);
}
let names: Vec<String> = RuntimeKind::value_variants()
.iter()
.map(|k| k.to_possible_value().unwrap().get_name().to_owned())
.collect();
assert_eq!(names, vec!["claude-code".to_owned(), "tcode".to_owned()]);
}
#[test]
fn build_adapter_returns_matching_identify() {
let tmux = FakeTmux::new();
let root = std::env::temp_dir()
.join("tm-inert-root")
.join(".trusty-mpm");
let claude = build_adapter(RuntimeKind::ClaudeCode, tmux.clone(), None, &root);
assert_eq!(claude.identify(), "claude-code");
let tcode = build_adapter(RuntimeKind::Tcode, tmux, None, &root);
assert_eq!(tcode.identify(), "tcode");
}
}