use std::path::PathBuf;
use async_trait::async_trait;
use relayburn_sdk::{Enrichment, IngestReport, WatchController};
pub mod claude;
pub mod codex;
pub mod opencode;
pub mod pending_stamp;
pub mod registry;
#[cfg(test)]
pub(crate) mod test_env;
pub use registry::{list_harness_names, lookup};
#[derive(Debug, Clone)]
pub struct PlanCtx {
pub cwd: PathBuf,
pub passthrough: Vec<String>,
pub tags: Enrichment,
pub ledger_home: Option<PathBuf>,
pub spawn_start_ts: std::time::SystemTime,
}
#[derive(Debug, Clone, Default)]
pub struct SpawnPlan {
pub binary: String,
pub args: Vec<String>,
pub env_overrides: Vec<(String, String)>,
pub session_id: Option<String>,
}
impl SpawnPlan {
pub fn new(binary: impl Into<String>, args: Vec<String>) -> Self {
Self {
binary: binary.into(),
args,
env_overrides: Vec::new(),
session_id: None,
}
}
}
#[async_trait]
pub trait HarnessAdapter: Send + Sync {
fn name(&self) -> &'static str;
fn session_root(&self) -> PathBuf;
async fn plan(&self, ctx: &PlanCtx) -> anyhow::Result<SpawnPlan>;
async fn before_spawn(&self, _ctx: &PlanCtx, _plan: &SpawnPlan) -> anyhow::Result<()> {
Ok(())
}
fn start_watcher(
&self,
_ctx: &PlanCtx,
_on_report: relayburn_sdk::ReportSink,
) -> Option<WatchController> {
None
}
async fn after_exit(&self, ctx: &PlanCtx, plan: &SpawnPlan) -> anyhow::Result<IngestReport>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_plan_new_minimal_shape() {
let plan = SpawnPlan::new("claude", vec!["--help".into()]);
assert_eq!(plan.binary, "claude");
assert_eq!(plan.args, vec!["--help".to_string()]);
assert!(plan.env_overrides.is_empty());
assert!(plan.session_id.is_none());
}
struct FakeAdapter;
#[async_trait]
impl HarnessAdapter for FakeAdapter {
fn name(&self) -> &'static str {
"fake"
}
fn session_root(&self) -> PathBuf {
PathBuf::from("/tmp/fake")
}
async fn plan(&self, _ctx: &PlanCtx) -> anyhow::Result<SpawnPlan> {
Ok(SpawnPlan::new("fake", vec![]))
}
async fn after_exit(
&self,
_ctx: &PlanCtx,
_plan: &SpawnPlan,
) -> anyhow::Result<IngestReport> {
Ok(IngestReport::default())
}
}
#[tokio::test]
async fn fake_adapter_round_trip() {
let adapter: &dyn HarnessAdapter = &FakeAdapter;
assert_eq!(adapter.name(), "fake");
assert_eq!(adapter.session_root(), PathBuf::from("/tmp/fake"));
let ctx = PlanCtx {
cwd: PathBuf::from("/tmp"),
passthrough: vec![],
tags: Enrichment::new(),
ledger_home: None,
spawn_start_ts: std::time::SystemTime::now(),
};
let plan = adapter.plan(&ctx).await.unwrap();
assert_eq!(plan.binary, "fake");
let report = adapter.after_exit(&ctx, &plan).await.unwrap();
assert_eq!(report.scanned_sessions, 0);
assert_eq!(report.ingested_sessions, 0);
}
}