# Decision: Lifecycle asset and source-file apply boundary

**Status:** Accepted on 2026-05-08, scope `lifeloop.v0.2`.

**Issue:** [#23 — Resolve lifecycle asset and source-file apply boundary][issue-23].
**Release gate:** [v1 freeze gate 4 — "Asset application and profile story"][gate-4]
in `docs/release-gates.md`.
**Source-file update:** the source-file portion of this decision was
superseded on 2026-05-16 by
[`source-files-are-user-owned.md`](source-files-are-user-owned.md).

## Decision

**Lifeloop does not own filesystem writes.** Callers (CCD, RLM, future
non-CCD profile clients) consume Lifeloop's rendered host assets as
in-memory data and perform their own filesystem IO. The earlier
managed-source-file renderer is no longer in scope; see
[`source-files-are-user-owned.md`](source-files-are-user-owned.md).

Concretely, in the CLI:

- `lifeloop asset apply` is removed. It was a side-effect-free alias of
  `asset preview` whose name implied filesystem mutation that the
  command never performed.
- `lifeloop asset preview` remains the only subcommand under `asset`. It
  prints the rendered host integration assets as a JSON array of
  `{ relative_path, mode, contents }` rows. Callers write the rows where
  they want them.
- The former `src/source_files::apply` managed-section renderer is removed
  by the follow-up source-file ownership decision. A pure replacement-body
  renderer still points callers at user-owned instruction files, so the
  stricter rule is read/suggest-only for those files.

The brief's recommendation, which this decision adopts, was Option B
("callers own writes; rename or tombstone the CLI apply surface").

## Why

Two arguments converged on Option B.

1. **Lifeloop's stated boundary is lifecycle reach normalization, not
   filesystem ownership.** [`AGENTS.md`](../../AGENTS.md): *"Lifeloop
   normalizes lifecycle reach. It does not normalize meaning."* The
   product thesis frames Lifeloop as a transport library; safe-write
   policy (root selection, atomic rename, no-clobber, drift detection)
   is filesystem-shaped policy that every caller has slightly different
   needs for. Owning it in Lifeloop pulls the library off-thesis.
2. **No client today asks for it.** The CCD slimdown (dusk-network/ccd#723)
   is a callback consumer; it does not require Lifeloop to write its
   config files. The thread-sync publisher (#11) is subprocess-only with
   no on-disk footprint. The non-CCD profile client expected by #26
   reads renderer output as data; it is not blocked by lack of file IO
   in Lifeloop. Building safe-write infrastructure for zero current
   consumers is the wrong shape of investment for a P1 boundary
   decision.

The release-gates v1 freeze (gate 4) phrases the requirement as
"Lifeloop has a clear, documented boundary on whether it owns filesystem
writes". Clarity is the goal; this document plus the CLI rename
satisfies that.

## Implications

### `src/host_assets.rs`

Unchanged. The module already operates on `serde_json::Value`, strings,
and byte slices, with an explicit "callers handle reads, writes, mode
bits, and atomic replace" boundary in its module docs. That boundary is
now the documented decision rather than an interim posture.

### `src/source_files/`

Superseded by
[`source-files-are-user-owned.md`](source-files-are-user-owned.md). The
module was removed because pure rendering of replacement bodies for
instruction files still violates the user-owned source-file boundary.

### `src/cli/asset.rs`

`asset apply` is removed from the dispatch. Calling it now returns a
usage error pointing at `asset preview`. Help text, the `match` on
subcommand strings, and the integration tests are updated in the same
change.

### #26 profiles

#26 ships a profile abstraction that is also a pure renderer: it
returns rendered host asset content, not file IO. This decision aligns
#26 with #23 — both are "Lifeloop renders host assets, clients install".
A profile rendered for a non-CCD harness produces host-asset strings, not
files. Instruction source files are governed by the stricter read/suggest
boundary in the source-file ownership decision.

### CCD slimdown (dusk-network/ccd#723)

CCD continues to perform its own host-asset installation. The extraction it
depends on is the rendering surface in `host_assets` plus the callback
driver (#22), not file-write helpers. CCD-side source-file generation is
out of scope under the follow-up source-file ownership decision.

## How to revisit

Reopen this decision and consider Option A (Lifeloop owns safe
filesystem apply) when **at least one** of the following holds:

- Two or more non-CCD clients ask for filesystem-apply primitives in
  their integration tickets, with a stated reason that pure rendering
  forces them to duplicate identical write code.
- The CCD slimdown stalls because clients refuse to install assets when
  Lifeloop only renders them, and the path forward is "Lifeloop applies
  the render".
- A second-client-class profile (per #26) ships and its operator UX
  shows that callers consistently re-implement the same root-selection
  and atomic-rename logic with subtle drift between implementations.

If any of those land, an Option A follow-up would scope tightly: a
`--root` flag on a new `asset install` subcommand, dry-run, atomic
write-then-rename, no-clobber, fail-closed managed-file provenance checks,
and structured action summaries (`created` / `updated` / `no_op` /
`drift`) — kept entirely behind the new subcommand and not leaking
filesystem helpers into the renderer modules.

[issue-23]: https://code.nanto.org/nanto/lifeloop/-/work_items/23
[gate-4]: ../release-gates.md
