use serde_json::{Value, json};
use super::merge::merge_codex_hooks_with_profile;
use super::model::*;
use super::profiles::*;
pub fn render_source_assets(host: HostAdapter) -> Vec<RenderedAsset> {
render_source_assets_with_profile(host, &CCD_COMPAT_PROFILE)
}
pub fn render_source_assets_with_profile(
host: HostAdapter,
profile: &LifecycleProfile,
) -> Vec<RenderedAsset> {
if profile.validate().is_err() {
return Vec::new();
}
match host {
HostAdapter::Claude => vec![RenderedAsset {
relative_path: CLAUDE_SOURCE_SETTINGS,
contents: claude_settings_json_for(profile),
mode: None,
}],
HostAdapter::Codex => vec![
RenderedAsset {
relative_path: CODEX_SOURCE_README,
contents: codex_guidance_readme(),
mode: None,
},
RenderedAsset {
relative_path: CODEX_SOURCE_CONFIG,
contents: codex_config_toml(),
mode: None,
},
RenderedAsset {
relative_path: CODEX_SOURCE_HOOKS,
contents: codex_hooks_json_for(profile),
mode: None,
},
RenderedAsset {
relative_path: CODEX_SOURCE_LAUNCHER,
contents: codex_launcher_script(),
mode: Some(0o755),
},
],
HostAdapter::Hermes => vec![RenderedAsset {
relative_path: HERMES_SOURCE_ADAPTER,
contents: hermes_adapter_json(),
mode: None,
}],
HostAdapter::OpenClaw => vec![RenderedAsset {
relative_path: OPENCLAW_SOURCE_ADAPTER,
contents: openclaw_adapter_json(),
mode: None,
}],
}
}
pub fn render_required_source_assets(
host: HostAdapter,
mode: IntegrationMode,
) -> Vec<RenderedAsset> {
let assets = render_source_assets(host);
let required_paths: &[&str] = match (host, mode) {
(HostAdapter::Codex, IntegrationMode::ManualSkill) => &[CODEX_SOURCE_README],
(HostAdapter::Codex, IntegrationMode::LauncherWrapper) => {
&[CODEX_SOURCE_README, CODEX_SOURCE_LAUNCHER]
}
(HostAdapter::Codex, IntegrationMode::NativeHook) => {
&[CODEX_SOURCE_README, CODEX_SOURCE_CONFIG, CODEX_SOURCE_HOOKS]
}
_ => return assets,
};
assets
.into_iter()
.filter(|asset| required_paths.contains(&asset.relative_path))
.collect()
}
pub fn render_applied_assets(host: HostAdapter, mode: IntegrationMode) -> Vec<RenderedAsset> {
render_applied_assets_with_profile(host, mode, &CCD_COMPAT_PROFILE)
}
pub fn render_applied_assets_with_profile(
host: HostAdapter,
mode: IntegrationMode,
profile: &LifecycleProfile,
) -> Vec<RenderedAsset> {
if profile.validate().is_err() {
return Vec::new();
}
match (host, mode) {
(HostAdapter::Claude, IntegrationMode::NativeHook) => vec![RenderedAsset {
relative_path: CLAUDE_TARGET_SETTINGS,
contents: claude_settings_json_for(profile),
mode: None,
}],
(HostAdapter::Codex, IntegrationMode::LauncherWrapper) => vec![RenderedAsset {
relative_path: CODEX_TARGET_LAUNCHER,
contents: codex_launcher_script(),
mode: Some(0o755),
}],
(HostAdapter::Codex, IntegrationMode::NativeHook) => vec![
RenderedAsset {
relative_path: CODEX_TARGET_CONFIG,
contents: codex_config_toml(),
mode: None,
},
RenderedAsset {
relative_path: CODEX_TARGET_HOOKS,
contents: codex_hooks_json_for(profile),
mode: None,
},
],
(HostAdapter::Codex, IntegrationMode::ManualSkill) => Vec::new(),
(HostAdapter::Hermes, IntegrationMode::ReferenceAdapter) => vec![RenderedAsset {
relative_path: HERMES_TARGET_ADAPTER,
contents: hermes_adapter_json(),
mode: None,
}],
(HostAdapter::OpenClaw, IntegrationMode::ReferenceAdapter) => vec![RenderedAsset {
relative_path: OPENCLAW_TARGET_ADAPTER,
contents: openclaw_adapter_json(),
mode: None,
}],
_ => Vec::new(),
}
}
fn claude_settings_json_for(profile: &LifecycleProfile) -> String {
let mut hooks = serde_json::Map::new();
for (event, hook_arg, matcher) in profile.claude_managed_events {
hooks.insert(
(*event).to_string(),
json!([{
"matcher": matcher,
"hooks": [{
"type": "command",
"command": profile.claude_command(hook_arg),
}]
}]),
);
}
let value = json!({ "hooks": Value::Object(hooks) });
serde_json::to_string_pretty(&value).expect("claude settings json")
}
fn codex_guidance_readme() -> String {
format!(
r#"<!-- CCD-MANAGED -->
# Codex Host Guidance
Codex supports native repo-local hooks when `hooks` is enabled in
`.codex/config.toml` and `.codex/hooks.json` maps lifecycle events into CCD.
CCD installs a minimal native mapping:
- `SessionStart` -> `ccd host-hook --hook on-session-start`
- `UserPromptSubmit` -> `ccd host-hook --hook before-prompt-build`
- `PreCompact` -> `ccd host-hook --hook on-compaction-notice`
- `PostCompact` -> `ccd host-hook --hook on-compaction-notice`
- `Stop` -> `ccd host-hook --hook on-agent-end`
Human-driven Codex can still fall back to the manual CCD startup path:
- `/ccd-start`
- `ccd start --activate --path .`
That fallback is tracked as `manual_skill`, not as a product failure.
If you want the optional zero-ritual launcher/eval harness instead, run:
```bash
ccd host apply --host codex --with-launcher --path .
```
That applies the launcher wrapper at `./{CODEX_TARGET_LAUNCHER}`.
"#
)
}
fn codex_config_toml() -> String {
"[features]\nhooks = true\n".to_owned()
}
fn codex_hooks_json_for(profile: &LifecycleProfile) -> String {
let merged = merge_codex_hooks_with_profile(json!({}), profile)
.expect("empty object is a valid Codex hooks base for managed events");
serde_json::to_string_pretty(&merged).expect("codex hooks json")
}
fn codex_launcher_script() -> String {
r#"#!/bin/sh
# CCD-MANAGED
# Optional Codex launcher/eval harness.
set -e
if [ -n "$CCD_BIN" ] && [ -x "$CCD_BIN" ]; then
CCD="$CCD_BIN"
elif command -v ccd >/dev/null 2>&1; then
CCD=ccd
elif [ -x "$HOME/.ccd/bin/ccd" ]; then
CCD="$HOME/.ccd/bin/ccd"
elif [ -x "$HOME/.cargo/bin/ccd" ]; then
CCD="$HOME/.cargo/bin/ccd"
else
CCD=""
fi
if [ -n "$CCD" ]; then
"$CCD" host-hook --output json --path . --host codex --hook on-session-start >/dev/null 2>&1 || true
fi
exec codex "$@"
"#
.to_owned()
}
fn openclaw_adapter_json() -> String {
serde_json::to_string_pretty(&json!({
"host": "openclaw",
"integration_mode": "reference_adapter",
"commands": {
"session_start": "ccd --output json host-hook --path . --host openclaw --hook on-session-start --mode implement --lifecycle autonomous --owner-kind runtime-worker --actor-id runtime/openclaw-agent-1 --lease-seconds 900 --host-session-id acp-session-42 --host-run-id acp-run-42 --host-task-id req-openclaw-42",
"before_prompt_build": "ccd host-hook --path . --host openclaw --hook before-prompt-build",
"on_compaction_notice": "ccd host-hook --path . --host openclaw --hook on-compaction-notice",
"on_agent_end": "ccd host-hook --path . --host openclaw --hook on-agent-end",
"on_session_end": "ccd host-hook --path . --host openclaw --hook on-session-end"
},
"notes": [
"Inject only the top-level context payload into prompt-build.",
"Keep runtime transcript history outside CCD durable state.",
"Use separate worktrees for parallel writers."
]
}))
.expect("openclaw adapter json")
}
fn hermes_adapter_json() -> String {
serde_json::to_string_pretty(&json!({
"host": "hermes",
"integration_mode": "reference_adapter",
"commands": {
"session_start": "ccd host-hook --output json --path . --host hermes --hook on-session-start --mode implement --lifecycle autonomous --actor-id runtime/hermes-worker-1 --supervisor-id runtime/hermes-supervisor-1 --lease-seconds 900 --host-session-id hermes-channel-42 --host-run-id hermes-run-42 --host-task-id hermes-task-42",
"before_prompt_build": "ccd host-hook --path . --host hermes --hook before-prompt-build",
"on_compaction_notice": "ccd host-hook --path . --host hermes --hook on-compaction-notice",
"on_agent_end": "ccd host-hook --path . --host hermes --hook on-agent-end",
"on_session_end": "ccd host-hook --path . --host hermes --hook on-session-end",
"supervisor_tick": "ccd host-hook --path . --host hermes --hook supervisor-tick"
},
"notes": [
"Honor the top-level session_boundary before unattended continuation.",
"Use supervisor_tick when the runtime can refresh lease ownership.",
"Treat CCD outputs as control-plane truth rather than prompt folklore."
]
}))
.expect("hermes adapter json")
}