supercode-harness 0.4.19

The optional native Supercode agent and tool harness
Documentation
//! UNI-18: the Hermes write path, through Hermes's own door.
//!
//! Hermes 0.21.0 ships `hermes sessions import --from {claude,codex} <file>`:
//! a per-session import that reads a foreign transcript and writes it into
//! the Hermes session store with Hermes's OWN writer — its schema, its
//! migrations, its WAL single-writer lock. supercode never opens a live
//! Hermes `state.db` for writing: it renders the session as a Codex rollout
//! (the format Hermes imports most faithfully) and hands the file to that
//! door. The new session is Hermes's (a fresh Hermes id; `origin_json`
//! records the rollout path and the foreign id), and `hermes --resume <id>`
//! continues it.

use std::path::{Path, PathBuf};
use std::process::Command;

use serde::Serialize;
use supercode_interchange::{Session, SessionFormat};

use crate::Result;

/// What the door wrote.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HermesImported {
    /// The Hermes session id the import minted; `hermes --resume <id>`.
    pub session_id: String,
    /// The Hermes home whose store now holds it.
    pub home: PathBuf,
    /// The Codex rollout Hermes read (kept: Hermes records it as provenance).
    pub rollout: PathBuf,
    /// The door.
    pub via: &'static str,
}

/// `HERMES_HOME`, else `~/.hermes` — the same resolution Hermes itself uses.
pub fn hermes_home() -> PathBuf {
    std::env::var_os("HERMES_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| {
            let home = std::env::var_os("HOME")
                .map(PathBuf::from)
                .unwrap_or_default();
            home.join(".hermes")
        })
}

/// Write `session` into the Hermes store at `home` (default: [`hermes_home`])
/// through `hermes sessions import`.
pub fn import_into_hermes(session: &Session, home: Option<&Path>) -> Result<HermesImported> {
    let home = home.map(Path::to_path_buf).unwrap_or_else(hermes_home);
    let rollout_dir = home.join("imports").join("supercode");
    std::fs::create_dir_all(&rollout_dir)?;
    let stamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0);
    let stem = session
        .meta
        .session_id
        .clone()
        .unwrap_or_else(|| "session".into());
    let rollout = rollout_dir.join(format!("rollout-{stamp}-{stem}.jsonl"));
    std::fs::write(&rollout, session.to_jsonl(SessionFormat::Codex)?)?;
    let output = Command::new("hermes")
        .args(["sessions", "import", "--from", "codex"])
        .arg(&rollout)
        .env("HERMES_HOME", &home)
        .env("HERMES_NO_ONBOARDING", "1")
        .env("HERMES_NONINTERACTIVE", "1")
        .output()
        .map_err(|e| {
            crate::Error::Other(format!("`hermes sessions import` could not start: {e}"))
        })?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    let session_id = stdout
        .lines()
        .find_map(|line| line.split(" session as ").nth(1))
        .map(|rest| rest.trim().to_string())
        .filter(|s| !s.is_empty());
    match (output.status.success(), session_id) {
        (true, Some(session_id)) => Ok(HermesImported {
            session_id,
            home,
            rollout,
            via: "hermes sessions import --from codex",
        }),
        _ => Err(crate::Error::Other(format!(
            "`hermes sessions import --from codex {}` did not report a session id (exit {}): {}{}",
            rollout.display(),
            output.status,
            stdout.trim(),
            String::from_utf8_lossy(&output.stderr).trim()
        ))),
    }
}