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 r#"<!-- CCD-MANAGED -->
187# Codex Host Guidance
188
189Codex supports native repo-local hooks when `hooks` is enabled in
190`.codex/config.toml` and `.codex/hooks.json` maps lifecycle events into
191the continuity client. Those files are generated alongside this README.
192
193Human-driven Codex drives CCD continuity through the operator loop:
194
195- `/ccd-start`
196- `ccd attach` (one-time, per project)
197- `ccd start` (open or roll forward a spin)
198- `ccd checkpoint` (record summary/handover)
199
200That manual path is tracked as `manual_skill`, not as a product failure.
201"#
202 .to_owned()
203}
204
205fn codex_config_toml() -> String {
206 "[features]\nhooks = true\n".to_owned()
207}
208
209fn codex_hooks_json_for(profile: &LifecycleProfile) -> String {
210 let merged = merge_codex_hooks_with_profile(json!({}), profile)
211 .expect("empty object is a valid Codex hooks base for managed events");
212 serde_json::to_string_pretty(&merged).expect("codex hooks json")
213}
214
215fn codex_launcher_script() -> String {
216 r#"#!/bin/sh
217# CCD-MANAGED
218# Optional Codex launcher/eval harness.
219
220set -e
221
222if [ -n "$CCD_BIN" ] && [ -x "$CCD_BIN" ]; then
223 CCD="$CCD_BIN"
224elif command -v ccd >/dev/null 2>&1; then
225 CCD=ccd
226elif [ -x "$HOME/.ccd/bin/ccd" ]; then
227 CCD="$HOME/.ccd/bin/ccd"
228elif [ -x "$HOME/.cargo/bin/ccd" ]; then
229 CCD="$HOME/.cargo/bin/ccd"
230else
231 CCD=""
232fi
233
234if [ -n "$CCD" ]; then
235 "$CCD" host-hook --output json --path . --host codex --hook on-session-start >/dev/null 2>&1 || true
236fi
237
238exec codex "$@"
239"#
240 .to_owned()
241}
242
243fn openclaw_adapter_json() -> String {
244 serde_json::to_string_pretty(&json!({
245 "host": "openclaw",
246 "integration_mode": "reference_adapter",
247 "commands": {
248 "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",
249 "before_prompt_build": "ccd host-hook --path . --host openclaw --hook before-prompt-build",
250 "on_compaction_notice": "ccd host-hook --path . --host openclaw --hook on-compaction-notice",
251 "on_agent_end": "ccd host-hook --path . --host openclaw --hook on-agent-end",
252 "on_session_end": "ccd host-hook --path . --host openclaw --hook on-session-end"
253 },
254 "notes": [
255 "Inject only the top-level context payload into prompt-build.",
256 "Keep runtime transcript history outside CCD durable state.",
257 "Use separate worktrees for parallel writers."
258 ]
259 }))
260 .expect("openclaw adapter json")
261}
262
263fn hermes_adapter_json() -> String {
264 serde_json::to_string_pretty(&json!({
265 "host": "hermes",
266 "integration_mode": "reference_adapter",
267 "commands": {
268 "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",
269 "before_prompt_build": "ccd host-hook --path . --host hermes --hook before-prompt-build",
270 "on_compaction_notice": "ccd host-hook --path . --host hermes --hook on-compaction-notice",
271 "on_agent_end": "ccd host-hook --path . --host hermes --hook on-agent-end",
272 "on_session_end": "ccd host-hook --path . --host hermes --hook on-session-end",
273 "supervisor_tick": "ccd host-hook --path . --host hermes --hook supervisor-tick"
274 },
275 "notes": [
276 "Honor the top-level session_boundary before unattended continuation.",
277 "Use supervisor_tick when the runtime can refresh lease ownership.",
278 "Treat CCD outputs as control-plane truth rather than prompt folklore."
279 ]
280 }))
281 .expect("hermes adapter json")
282}