lifeloop-cli 0.1.1

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
# 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