# Decision: Lifecycle integration profile abstraction
**Status:** Accepted on 2026-05-09, scope `lifeloop.v0.2`.
**Issue:** [#26 — Generalize lifecycle integration asset profiles beyond CCD compatibility][issue-26].
**Release gate:** [v1 freeze gate 4 — "Asset application and profile story"][gate-4]
in `docs/release-gates.md`.
## Decision
Lifeloop renders Claude/Codex host integration hook assets through a
**`LifecycleProfile`** struct, not through hardcoded CCD-flavored
constants. The first profile is `CCD_COMPAT_PROFILE`, identical in
output to the pre-#26 renderer; the second is
`LIFELOOP_DIRECT_PROFILE`, the post-slimdown shape in which the
harness invokes `${LIFELOOP_BIN:-lifeloop} host-hook ...` directly
without CCD acting as broker.
Concretely, in `src/host_assets.rs`:
- `LifecycleProfile { id, claude_command_prefix, claude_legacy_substrings,
claude_managed_events, codex_command_prefix, codex_managed_events }`
is the new public type. All fields are `&'static`; the struct is
`Copy`. Methods (`claude_command`, `codex_command`,
`claude_entry_is_managed_or_legacy`, `codex_entry_is_managed`) are
pure functions of those fields.
- `CCD_COMPAT_PROFILE` and `LIFELOOP_DIRECT_PROFILE` are the two
built-in profiles. A consumer-defined profile is a single
`LifecycleProfile` literal — no trait impl, no registry.
- `render_applied_assets`, `render_source_assets`,
`merge_claude_settings`, `merge_codex_hooks`,
`merge_claude_settings_text`, `merge_codex_hooks_text`,
`codex_hooks_contain_managed_lifecycle`, `claude_settings_status`,
and `codex_hooks_status` keep their pre-#26 signatures and continue
to render through `CCD_COMPAT_PROFILE` (back-compat parity is pinned
by tests in `tests/host_assets_profiles.rs`).
- Each of the back-compat fns has a paired `*_with_profile` form that
takes a `&LifecycleProfile` argument and is the natural entry point
for non-CCD callers and slimdown work.
## Why a struct, not a trait
Lifecycle integration profiles are **data**, not behavior:
- The renderer pipeline is fixed (the lifecycle event vocabulary and
the merge algorithm are harness-defined facts, not client-defined).
- What varies between profiles is the command-prefix string, the
legacy-substring scrub list, and (optionally) the managed event
table — all `&'static` data.
- A `Copy` struct of static slices keeps the kernel-purity boundary
intact (no new traits leaking into call sites, no dyn dispatch
through the renderer), and parallels the existing const-table style
the rest of `host_assets.rs` uses.
A trait would add ceremony without expressive power: any non-trivial
behavior a profile would want belongs in the renderer or the merge
logic, not on the profile itself. If a future profile ever needs
profile-specific behavior, that is the moment to revisit; until then,
the data shape suffices.
## How this supports the CCD slimdown (dusk-network/ccd#723)
The slimdown lands by **switching the active install profile from
`CCD_COMPAT_PROFILE` to a non-CCD profile**, not by rewriting the
renderer. Concretely:
1. Today, every host-asset install uses `CCD_COMPAT_PROFILE`. The
harness invokes `${CCD_BIN:-ccd} host-hook ...` and CCD calls back
into Lifeloop over the JSON stdio callback path
(`SubprocessCallbackInvoker`, landed in #29).
2. After the slimdown, the harness invokes `${LIFELOOP_BIN:-lifeloop}
host-hook ...` directly, with no CCD process in the loop.
`LIFELOOP_DIRECT_PROFILE` is the rendering surface for that shape.
3. Migration is per-installation: a `lifeloop asset preview --profile
lifeloop-direct` (a CLI knob added in a follow-up MR) returns the
rendered assets for the new profile. The operator (or CCD's own
slimdown migration) writes them. Existing CCD installs keep
working because the back-compat API is unchanged.
4. **In-place migration is non-destructive but deterministic.**
`LIFELOOP_DIRECT_PROFILE.claude_legacy_substrings` includes
`CCD_COMPAT_PROFILE.claude_command_prefix`, so a lifeloop-direct
merge over an existing CCD-compat settings.json scrubs the old
CCD-managed entries during the merge and emits a single set of
managed hooks in the new shape. User-owned (non-managed) entries
are preserved verbatim per the merge invariants.
5. The reverse direction is intentionally additive: a CCD-compat
merge over a lifeloop-direct settings.json preserves the
lifeloop-direct entries (CCD has no claim to a successor
profile's shape). This asymmetry is pinned by tests in
`tests/host_assets_profiles.rs` —
`lifeloop_direct_merge_scrubs_ccd_compat_entries_for_clean_migration`
covers the migration direction;
`ccd_compat_merge_does_not_recognize_lifeloop_direct_managed_entries`
covers the reverse asymmetry; and
`lifeloop_direct_merge_preserves_user_owned_entries_during_migration`
pins the user-data-safety property.
`docs/release-gates.md` calls out this profile abstraction as the
prerequisite that lets the slimdown ship as an installation-time
toggle rather than a renderer rewrite.
## Scope notes
- **Hermes / OpenClaw reference adapters** continue to render with
CCD-flavored adapter JSON regardless of profile. Those files are
per-host illustrative documentation, not active command surfaces;
graduating them to per-profile rendering is a follow-up if and when
a non-CCD client adopts them.
- **`codex_launcher_script` and `codex_guidance_readme`** still
reference `ccd` directly. They are CCD-specific tooling (the
optional zero-ritual launcher and its README), not the active hook
surface. A non-CCD client that wants a launcher writes its own.
- The non-CCD pilot work (#28) is the natural first consumer of
`LIFELOOP_DIRECT_PROFILE` — the thread-sync publisher already
exists as a callback client; #28 graduates it to a product pilot
that consumes lifeloop's payload delivery and receipt surfaces.
[issue-26]: https://code.nanto.org/nanto/lifeloop/-/work_items/26
[gate-4]: ../release-gates.md