1use serde_json::{Value, json};
4
5use super::merge::merge_codex_hooks_with_profile;
6use super::model::*;
7use super::profiles::*;
8
9pub fn render_source_assets(host: HostAdapter) -> Vec<RenderedAsset> {
17 render_source_assets_with_profile(host, &CCD_COMPAT_PROFILE)
18}
19
20pub fn render_source_assets_with_profile(
25 host: HostAdapter,
26 profile: &LifecycleProfile,
27) -> Vec<RenderedAsset> {
28 if profile.validate().is_err() {
29 return Vec::new();
30 }
31 match host {
32 HostAdapter::Claude => vec![RenderedAsset {
33 relative_path: CLAUDE_SOURCE_SETTINGS,
34 contents: claude_settings_json_for(profile),
35 mode: None,
36 }],
37 HostAdapter::Codex => vec![
38 RenderedAsset {
39 relative_path: CODEX_SOURCE_README,
40 contents: codex_guidance_readme(),
41 mode: None,
42 },
43 RenderedAsset {
44 relative_path: CODEX_SOURCE_CONFIG,
45 contents: codex_config_toml(),
46 mode: None,
47 },
48 RenderedAsset {
49 relative_path: CODEX_SOURCE_HOOKS,
50 contents: codex_hooks_json_for(profile),
51 mode: None,
52 },
53 RenderedAsset {
54 relative_path: CODEX_SOURCE_LAUNCHER,
55 contents: codex_launcher_script(),
56 mode: Some(0o755),
57 },
58 ],
59 HostAdapter::Hermes => vec![RenderedAsset {
60 relative_path: HERMES_SOURCE_ADAPTER,
61 contents: hermes_adapter_json(),
62 mode: None,
63 }],
64 HostAdapter::OpenClaw => vec![RenderedAsset {
65 relative_path: OPENCLAW_SOURCE_ADAPTER,
66 contents: openclaw_adapter_json(),
67 mode: None,
68 }],
69 }
70}
71
72pub fn render_required_source_assets(
77 host: HostAdapter,
78 mode: IntegrationMode,
79) -> Vec<RenderedAsset> {
80 let assets = render_source_assets(host);
81 let required_paths: &[&str] = match (host, mode) {
82 (HostAdapter::Codex, IntegrationMode::ManualSkill) => &[CODEX_SOURCE_README],
83 (HostAdapter::Codex, IntegrationMode::LauncherWrapper) => {
84 &[CODEX_SOURCE_README, CODEX_SOURCE_LAUNCHER]
85 }
86 (HostAdapter::Codex, IntegrationMode::NativeHook) => {
87 &[CODEX_SOURCE_README, CODEX_SOURCE_CONFIG, CODEX_SOURCE_HOOKS]
88 }
89 _ => return assets,
90 };
91 assets
92 .into_iter()
93 .filter(|asset| required_paths.contains(&asset.relative_path))
94 .collect()
95}
96
97pub fn render_applied_assets(host: HostAdapter, mode: IntegrationMode) -> Vec<RenderedAsset> {
106 render_applied_assets_with_profile(host, mode, &CCD_COMPAT_PROFILE)
107}
108
109pub fn render_applied_assets_with_profile(
118 host: HostAdapter,
119 mode: IntegrationMode,
120 profile: &LifecycleProfile,
121) -> Vec<RenderedAsset> {
122 if profile.validate().is_err() {
123 return Vec::new();
124 }
125 match (host, mode) {
126 (HostAdapter::Claude, IntegrationMode::NativeHook) => vec![RenderedAsset {
127 relative_path: CLAUDE_TARGET_SETTINGS,
128 contents: claude_settings_json_for(profile),
129 mode: None,
130 }],
131 (HostAdapter::Codex, IntegrationMode::LauncherWrapper) => vec![RenderedAsset {
132 relative_path: CODEX_TARGET_LAUNCHER,
133 contents: codex_launcher_script(),
134 mode: Some(0o755),
135 }],
136 (HostAdapter::Codex, IntegrationMode::NativeHook) => vec![
137 RenderedAsset {
138 relative_path: CODEX_TARGET_CONFIG,
139 contents: codex_config_toml(),
140 mode: None,
141 },
142 RenderedAsset {
143 relative_path: CODEX_TARGET_HOOKS,
144 contents: codex_hooks_json_for(profile),
145 mode: None,
146 },
147 ],
148 (HostAdapter::Codex, IntegrationMode::ManualSkill) => Vec::new(),
149 (HostAdapter::Hermes, IntegrationMode::ReferenceAdapter) => vec![RenderedAsset {
150 relative_path: HERMES_TARGET_ADAPTER,
151 contents: hermes_adapter_json(),
152 mode: None,
153 }],
154 (HostAdapter::OpenClaw, IntegrationMode::ReferenceAdapter) => vec![RenderedAsset {
155 relative_path: OPENCLAW_TARGET_ADAPTER,
156 contents: openclaw_adapter_json(),
157 mode: None,
158 }],
159 _ => Vec::new(),
160 }
161}
162
163fn claude_settings_json_for(profile: &LifecycleProfile) -> String {
168 let mut hooks = serde_json::Map::new();
169 for (event, hook_arg, matcher) in profile.claude_managed_events {
170 hooks.insert(
171 (*event).to_string(),
172 json!([{
173 "matcher": matcher,
174 "hooks": [{
175 "type": "command",
176 "command": profile.claude_command(hook_arg),
177 }]
178 }]),
179 );
180 }
181 let value = json!({ "hooks": Value::Object(hooks) });
182 serde_json::to_string_pretty(&value).expect("claude settings json")
183}
184
185fn codex_guidance_readme() -> String {
186 format!(
187 r#"<!-- CCD-MANAGED -->
188# Codex Host Guidance
189
190Codex supports native repo-local hooks when `hooks` is enabled in
191`.codex/config.toml` and `.codex/hooks.json` maps lifecycle events into CCD.
192
193CCD installs a minimal native mapping:
194
195- `SessionStart` -> `ccd host-hook --hook on-session-start`
196- `UserPromptSubmit` -> `ccd host-hook --hook before-prompt-build`
197- `PreCompact` -> `ccd host-hook --hook on-compaction-notice`
198- `PostCompact` -> `ccd host-hook --hook on-compaction-notice`
199- `Stop` -> `ccd host-hook --hook on-agent-end`
200
201Human-driven Codex can still fall back to the manual CCD startup path:
202
203- `/ccd-start`
204- `ccd start --activate --path .`
205
206That fallback is tracked as `manual_skill`, not as a product failure.
207
208If you want the optional zero-ritual launcher/eval harness instead, run:
209
210```bash
211ccd host apply --host codex --with-launcher --path .
212```
213
214That applies the launcher wrapper at `./{CODEX_TARGET_LAUNCHER}`.
215"#
216 )
217}
218
219fn codex_config_toml() -> String {
220 "[features]\nhooks = true\n".to_owned()
221}
222
223fn codex_hooks_json_for(profile: &LifecycleProfile) -> String {
224 let merged = merge_codex_hooks_with_profile(json!({}), profile)
225 .expect("empty object is a valid Codex hooks base for managed events");
226 serde_json::to_string_pretty(&merged).expect("codex hooks json")
227}
228
229fn codex_launcher_script() -> String {
230 r#"#!/bin/sh
231# CCD-MANAGED
232# Optional Codex launcher/eval harness.
233
234set -e
235
236if [ -n "$CCD_BIN" ] && [ -x "$CCD_BIN" ]; then
237 CCD="$CCD_BIN"
238elif command -v ccd >/dev/null 2>&1; then
239 CCD=ccd
240elif [ -x "$HOME/.ccd/bin/ccd" ]; then
241 CCD="$HOME/.ccd/bin/ccd"
242elif [ -x "$HOME/.cargo/bin/ccd" ]; then
243 CCD="$HOME/.cargo/bin/ccd"
244else
245 CCD=""
246fi
247
248if [ -n "$CCD" ]; then
249 "$CCD" host-hook --output json --path . --host codex --hook on-session-start >/dev/null 2>&1 || true
250fi
251
252exec codex "$@"
253"#
254 .to_owned()
255}
256
257fn openclaw_adapter_json() -> String {
258 serde_json::to_string_pretty(&json!({
259 "host": "openclaw",
260 "integration_mode": "reference_adapter",
261 "commands": {
262 "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",
263 "before_prompt_build": "ccd host-hook --path . --host openclaw --hook before-prompt-build",
264 "on_compaction_notice": "ccd host-hook --path . --host openclaw --hook on-compaction-notice",
265 "on_agent_end": "ccd host-hook --path . --host openclaw --hook on-agent-end",
266 "on_session_end": "ccd host-hook --path . --host openclaw --hook on-session-end"
267 },
268 "notes": [
269 "Inject only the top-level context payload into prompt-build.",
270 "Keep runtime transcript history outside CCD durable state.",
271 "Use separate worktrees for parallel writers."
272 ]
273 }))
274 .expect("openclaw adapter json")
275}
276
277fn hermes_adapter_json() -> String {
278 serde_json::to_string_pretty(&json!({
279 "host": "hermes",
280 "integration_mode": "reference_adapter",
281 "commands": {
282 "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",
283 "before_prompt_build": "ccd host-hook --path . --host hermes --hook before-prompt-build",
284 "on_compaction_notice": "ccd host-hook --path . --host hermes --hook on-compaction-notice",
285 "on_agent_end": "ccd host-hook --path . --host hermes --hook on-agent-end",
286 "on_session_end": "ccd host-hook --path . --host hermes --hook on-session-end",
287 "supervisor_tick": "ccd host-hook --path . --host hermes --hook supervisor-tick"
288 },
289 "notes": [
290 "Honor the top-level session_boundary before unattended continuation.",
291 "Use supervisor_tick when the runtime can refresh lease ownership.",
292 "Treat CCD outputs as control-plane truth rather than prompt folklore."
293 ]
294 }))
295 .expect("hermes adapter json")
296}