Skip to main content

lifeloop/source_files/
adapters.rs

1//! Per-adapter source-file template bodies.
2//!
3//! Each adapter declares which source-file path it owns and the
4//! lifecycle integration metadata its managed section advertises.
5//! Templates are pure functions of (adapter, integration_mode); they
6//! never embed client semantics. Client-supplied content reaches the
7//! managed section only through the opaque slot rendered by
8//! [`super::render_for`].
9
10use crate::IntegrationMode;
11use crate::host_assets::HostAdapter;
12
13/// Adapters Lifeloop ships source-file rendering for. Mirrors
14/// [`HostAdapter`] plus Gemini, which has a source file but no host
15/// integration assets in `host_assets.rs` today.
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
17pub enum SourceFileAdapter {
18    Claude,
19    Codex,
20    Gemini,
21    Hermes,
22    OpenClaw,
23}
24
25impl SourceFileAdapter {
26    pub const ALL: &'static [Self] = &[
27        Self::Claude,
28        Self::Codex,
29        Self::Gemini,
30        Self::Hermes,
31        Self::OpenClaw,
32    ];
33
34    pub fn as_str(self) -> &'static str {
35        match self {
36            Self::Claude => "claude",
37            Self::Codex => "codex",
38            Self::Gemini => "gemini",
39            Self::Hermes => "hermes",
40            Self::OpenClaw => "openclaw",
41        }
42    }
43
44    pub fn from_id(name: &str) -> Option<Self> {
45        match name {
46            "claude" | "claude-code" => Some(Self::Claude),
47            "codex" => Some(Self::Codex),
48            "gemini" => Some(Self::Gemini),
49            "hermes" => Some(Self::Hermes),
50            "openclaw" => Some(Self::OpenClaw),
51            _ => None,
52        }
53    }
54
55    /// The path Lifeloop renders the managed section into, relative to
56    /// the repository root the caller writes against. Stable identifier
57    /// only; the caller resolves filesystem location.
58    pub fn relative_path(self) -> &'static str {
59        match self {
60            Self::Claude => "CLAUDE.md",
61            Self::Codex => "AGENTS.md",
62            Self::Gemini => "GEMINI.md",
63            Self::Hermes => "HERMES.md",
64            Self::OpenClaw => "OPENCLAW.md",
65        }
66    }
67
68    /// Convert from the host-asset adapter enum. `None` for adapters
69    /// that have a source file but no host integration asset surface
70    /// (currently: Gemini).
71    pub fn from_host_adapter(host: HostAdapter) -> Self {
72        match host {
73            HostAdapter::Claude => Self::Claude,
74            HostAdapter::Codex => Self::Codex,
75            HostAdapter::Hermes => Self::Hermes,
76            HostAdapter::OpenClaw => Self::OpenClaw,
77        }
78    }
79}
80
81/// Current template version. Bumped when the body shape of the
82/// managed section changes in a way that requires stale-section
83/// replacement on existing repos. Independent of `lifeloop.v0.x`
84/// schema versions.
85pub const TEMPLATE_VERSION: u32 = 1;
86
87/// Describe an integration mode in human-readable form for the
88/// managed-section body. Stays factual: which mode, what timing
89/// posture. No client semantics.
90pub(super) fn describe_integration_mode(mode: IntegrationMode) -> &'static str {
91    match mode {
92        IntegrationMode::ManualSkill => {
93            "manual_skill — operator-typed entry point; no native hook fires"
94        }
95        IntegrationMode::LauncherWrapper => {
96            "launcher_wrapper — wrapper script invokes lifecycle hooks before exec"
97        }
98        IntegrationMode::NativeHook => {
99            "native_hook — harness-native hooks fire lifecycle events directly"
100        }
101        IntegrationMode::ReferenceAdapter => {
102            "reference_adapter — adapter implements lifecycle calls explicitly"
103        }
104        IntegrationMode::TelemetryOnly => {
105            "telemetry_only — no installable assets; lifecycle is read from telemetry"
106        }
107    }
108}
109
110/// Lifecycle event timing summary lines for an adapter, in
111/// harness-native vocabulary mapped onto Lifeloop event names. Returned
112/// as bullet-ready strings (no leading dash). Stays factual; no client
113/// language.
114pub(super) fn lifecycle_timing_summary(
115    adapter: SourceFileAdapter,
116    mode: IntegrationMode,
117) -> Vec<&'static str> {
118    if matches!(mode, IntegrationMode::TelemetryOnly) {
119        return vec!["lifecycle events derived from telemetry; no hook timing applies"];
120    }
121    match adapter {
122        SourceFileAdapter::Claude => vec![
123            "SessionStart -> session.starting",
124            "UserPromptSubmit -> frame.opening",
125            "PreCompact -> context.pressure",
126            "Stop -> frame.ended",
127            "SessionEnd -> session.ended",
128        ],
129        SourceFileAdapter::Codex => vec![
130            "SessionStart -> session.starting",
131            "UserPromptSubmit -> frame.opening",
132            "PreCompact -> context.pressure",
133            "PostCompact -> context.compacted",
134            "Stop -> frame.ended",
135        ],
136        SourceFileAdapter::Gemini => vec![
137            "session boot -> session.starting (telemetry-derived in Synthesized mode)",
138            "prompt submit -> frame.opening",
139            "session end -> session.ended",
140        ],
141        SourceFileAdapter::Hermes => vec![
142            "on-session-start -> session.starting",
143            "before-prompt-build -> frame.opening",
144            "on-compaction-notice -> context.pressure",
145            "on-agent-end -> frame.ended",
146            "on-session-end -> session.ended",
147            "supervisor-tick -> lease.refreshed",
148        ],
149        SourceFileAdapter::OpenClaw => vec![
150            "on-session-start -> session.starting",
151            "before-prompt-build -> frame.opening",
152            "on-compaction-notice -> context.pressure",
153            "on-agent-end -> frame.ended",
154            "on-session-end -> session.ended",
155        ],
156    }
157}
158
159/// Host integration asset paths the adapter's managed section
160/// references. Empty when the integration mode does not install host
161/// assets (e.g. `manual_skill` for adapters without scaffold files).
162pub(super) fn host_asset_paths(
163    adapter: SourceFileAdapter,
164    mode: IntegrationMode,
165) -> Vec<&'static str> {
166    use crate::host_assets as ha;
167    match (adapter, mode) {
168        (SourceFileAdapter::Claude, IntegrationMode::NativeHook) => {
169            vec![ha::CLAUDE_TARGET_SETTINGS]
170        }
171        (SourceFileAdapter::Codex, IntegrationMode::NativeHook) => {
172            vec![ha::CODEX_TARGET_CONFIG, ha::CODEX_TARGET_HOOKS]
173        }
174        (SourceFileAdapter::Codex, IntegrationMode::LauncherWrapper) => {
175            vec![ha::CODEX_TARGET_LAUNCHER]
176        }
177        (SourceFileAdapter::Hermes, IntegrationMode::ReferenceAdapter) => {
178            vec![ha::HERMES_TARGET_ADAPTER]
179        }
180        (SourceFileAdapter::OpenClaw, IntegrationMode::ReferenceAdapter) => {
181            vec![ha::OPENCLAW_TARGET_ADAPTER]
182        }
183        _ => Vec::new(),
184    }
185}