lifeloop-cli 0.1.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! Per-adapter source-file template bodies.
//!
//! Each adapter declares which source-file path it owns and the
//! lifecycle integration metadata its managed section advertises.
//! Templates are pure functions of (adapter, integration_mode); they
//! never embed client semantics. Client-supplied content reaches the
//! managed section only through the opaque slot rendered by
//! [`super::render_for`].

use crate::IntegrationMode;
use crate::host_assets::HostAdapter;

/// Adapters Lifeloop ships source-file rendering for. Mirrors
/// [`HostAdapter`] plus Gemini, which has a source file but no host
/// integration assets in `host_assets.rs` today.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum SourceFileAdapter {
    Claude,
    Codex,
    Gemini,
    Hermes,
    OpenClaw,
}

impl SourceFileAdapter {
    pub const ALL: &'static [Self] = &[
        Self::Claude,
        Self::Codex,
        Self::Gemini,
        Self::Hermes,
        Self::OpenClaw,
    ];

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Claude => "claude",
            Self::Codex => "codex",
            Self::Gemini => "gemini",
            Self::Hermes => "hermes",
            Self::OpenClaw => "openclaw",
        }
    }

    pub fn from_id(name: &str) -> Option<Self> {
        match name {
            "claude" | "claude-code" => Some(Self::Claude),
            "codex" => Some(Self::Codex),
            "gemini" => Some(Self::Gemini),
            "hermes" => Some(Self::Hermes),
            "openclaw" => Some(Self::OpenClaw),
            _ => None,
        }
    }

    /// The path Lifeloop renders the managed section into, relative to
    /// the repository root the caller writes against. Stable identifier
    /// only; the caller resolves filesystem location.
    pub fn relative_path(self) -> &'static str {
        match self {
            Self::Claude => "CLAUDE.md",
            Self::Codex => "AGENTS.md",
            Self::Gemini => "GEMINI.md",
            Self::Hermes => "HERMES.md",
            Self::OpenClaw => "OPENCLAW.md",
        }
    }

    /// Convert from the host-asset adapter enum. `None` for adapters
    /// that have a source file but no host integration asset surface
    /// (currently: Gemini).
    pub fn from_host_adapter(host: HostAdapter) -> Self {
        match host {
            HostAdapter::Claude => Self::Claude,
            HostAdapter::Codex => Self::Codex,
            HostAdapter::Hermes => Self::Hermes,
            HostAdapter::OpenClaw => Self::OpenClaw,
        }
    }
}

/// Current template version. Bumped when the body shape of the
/// managed section changes in a way that requires stale-section
/// replacement on existing repos. Independent of `lifeloop.v0.x`
/// schema versions.
pub const TEMPLATE_VERSION: u32 = 1;

/// Describe an integration mode in human-readable form for the
/// managed-section body. Stays factual: which mode, what timing
/// posture. No client semantics.
pub(super) fn describe_integration_mode(mode: IntegrationMode) -> &'static str {
    match mode {
        IntegrationMode::ManualSkill => {
            "manual_skill — operator-typed entry point; no native hook fires"
        }
        IntegrationMode::LauncherWrapper => {
            "launcher_wrapper — wrapper script invokes lifecycle hooks before exec"
        }
        IntegrationMode::NativeHook => {
            "native_hook — harness-native hooks fire lifecycle events directly"
        }
        IntegrationMode::ReferenceAdapter => {
            "reference_adapter — adapter implements lifecycle calls explicitly"
        }
        IntegrationMode::TelemetryOnly => {
            "telemetry_only — no installable assets; lifecycle is read from telemetry"
        }
    }
}

/// Lifecycle event timing summary lines for an adapter, in
/// harness-native vocabulary mapped onto Lifeloop event names. Returned
/// as bullet-ready strings (no leading dash). Stays factual; no client
/// language.
pub(super) fn lifecycle_timing_summary(
    adapter: SourceFileAdapter,
    mode: IntegrationMode,
) -> Vec<&'static str> {
    if matches!(mode, IntegrationMode::TelemetryOnly) {
        return vec!["lifecycle events derived from telemetry; no hook timing applies"];
    }
    match adapter {
        SourceFileAdapter::Claude => vec![
            "SessionStart -> session.starting",
            "UserPromptSubmit -> frame.opening",
            "PreCompact -> context.pressure",
            "Stop -> frame.ended",
            "SessionEnd -> session.ended",
        ],
        SourceFileAdapter::Codex => vec![
            "SessionStart -> session.starting",
            "UserPromptSubmit -> frame.opening",
            "PreCompact -> context.pressure",
            "PostCompact -> context.compacted",
            "Stop -> frame.ended",
        ],
        SourceFileAdapter::Gemini => vec![
            "session boot -> session.starting (telemetry-derived in Synthesized mode)",
            "prompt submit -> frame.opening",
            "session end -> session.ended",
        ],
        SourceFileAdapter::Hermes => vec![
            "on-session-start -> session.starting",
            "before-prompt-build -> frame.opening",
            "on-compaction-notice -> context.pressure",
            "on-agent-end -> frame.ended",
            "on-session-end -> session.ended",
            "supervisor-tick -> lease.refreshed",
        ],
        SourceFileAdapter::OpenClaw => vec![
            "on-session-start -> session.starting",
            "before-prompt-build -> frame.opening",
            "on-compaction-notice -> context.pressure",
            "on-agent-end -> frame.ended",
            "on-session-end -> session.ended",
        ],
    }
}

/// Host integration asset paths the adapter's managed section
/// references. Empty when the integration mode does not install host
/// assets (e.g. `manual_skill` for adapters without scaffold files).
pub(super) fn host_asset_paths(
    adapter: SourceFileAdapter,
    mode: IntegrationMode,
) -> Vec<&'static str> {
    use crate::host_assets as ha;
    match (adapter, mode) {
        (SourceFileAdapter::Claude, IntegrationMode::NativeHook) => {
            vec![ha::CLAUDE_TARGET_SETTINGS]
        }
        (SourceFileAdapter::Codex, IntegrationMode::NativeHook) => {
            vec![ha::CODEX_TARGET_CONFIG, ha::CODEX_TARGET_HOOKS]
        }
        (SourceFileAdapter::Codex, IntegrationMode::LauncherWrapper) => {
            vec![ha::CODEX_TARGET_LAUNCHER]
        }
        (SourceFileAdapter::Hermes, IntegrationMode::ReferenceAdapter) => {
            vec![ha::HERMES_TARGET_ADAPTER]
        }
        (SourceFileAdapter::OpenClaw, IntegrationMode::ReferenceAdapter) => {
            vec![ha::OPENCLAW_TARGET_ADAPTER]
        }
        _ => Vec::new(),
    }
}