lifeloop-cli 0.3.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# Lifeloop Lifecycle Contract

Lifeloop is the provider-neutral lifecycle normalization substrate. It turns
harness-specific hooks, launchers, reference adapters, and telemetry into one
small lifecycle surface that clients can consume without inheriting each
other's product semantics.

CCD is the first client. Recursive Language Models (RLM) are the known
second-client design pressure. This contract therefore defines only lifecycle
normalization: event vocabulary, adapter manifests, capability negotiation,
opaque payload delivery, receipts, degradation events, failure classes, and
retry classes.

## Goals

- Normalize lifecycle timing and delivery across harnesses.
- Make adapter capability gaps explicit and machine-readable.
- Carry opaque client payloads without interpreting their meaning.
- Emit bounded receipts that clients can project into their own state.
- Keep lifecycle failure and retry posture consistent across adapters.
- Preserve room for non-CCD clients such as RLM without importing their
  semantics.

## Non-Goals

- No CCD continuity product semantics inside Lifeloop.
- No recursive inference semantics inside Lifeloop.
- No tool, skill, model, provider, prompt-cache, approval, or memory
  abstraction unless it is directly required to classify a lifecycle event or
  deliver a lifecycle payload.
- No public crate, standalone repository, native Lifeloop CLI, or command
  rename in this contract.

## Ownership Boundary

Lifeloop owns lifecycle normalization. Clients own the meaning of actions they
take after receiving normalized lifecycle facts.

Lifeloop-owned concepts:

- adapter identity and aliases
- integration mode metadata
- lifecycle event names
- lifecycle capability manifests
- payload placement negotiation
- delivery facts and receipt shape
- context-pressure and activity observations when they affect lifecycle timing
- lifecycle failure classes and retry classes
- optional receipt-ledger, payload-store, and negotiation-cache capabilities
  when an adapter or Lifeloop deployment explicitly provides them

Client-owned concepts:

- any continuity, recovery, task, trajectory, memory, approval, policy, or
  recursive-inference model
- payload generation, priority, semantic interpretation, and persistence
- state stores that consume receipts
- product-specific escalation or fallback behavior

Lifeloop may transport a client payload with client-specific format labels,
but those strings are opaque routing facts. Lifeloop must not parse payload
formats or infer client policy from them.

## Boundary With Existing Specs

- `machine-runtime-api` remains the control-plane operation contract. Lifeloop
  narrows lifecycle normalization below it and does not replace runtime
  commands.
- `runtime-state-contract` remains the CCD-local state ownership contract.
  Lifeloop receipts are evidence and correlation data, not mutable CCD state.
- `mid-session-context-refresh` remains the CCD policy for when to refresh,
  checkpoint, or stop. Lifeloop only reports lifecycle pressure evidence.
- `lifeloop-ccd-client` is downstream of this contract. It maps CCD ceremonies
  to the neutral lifecycle vocabulary and owns CCD-specific reactions.

Call direction for the first CCD extraction is:

1. A harness transport invokes Lifeloop or a compatibility command backed by
   Lifeloop.
2. Lifeloop normalizes the lifecycle fact, evaluates adapter capabilities, and
   returns receipts.
3. The client reacts to those receipts and lifecycle facts through its own
   state and policy APIs.

Lifeloop must not call CCD continuity state modules directly. CCD may call
Lifeloop from inside a synchronous compatibility command and then produce the
same compatibility response the harness already expects.

## Lifecycle Event Vocabulary

Event names are stable lowercase dot-separated strings.

| Event | Meaning |
|---|---|
| `session.starting` | A harness session or top-level lifecycle is about to start, attach, or resume. |
| `session.started` | The harness session is live enough for a client to bind correlation state. |
| `frame.opening` | A prompt, turn, recursive frame, or equivalent execution frame is about to receive client payloads. |
| `frame.opened` | The frame accepted delivery or explicitly skipped delivery. |
| `context.pressure_observed` | Lifecycle-relevant budget, compaction, idle-reset, or context-pressure evidence was observed. |
| `context.compacted` | A harness completed a context compaction or equivalent context-rewrite lifecycle moment. |
| `frame.ending` | A frame is about to close, finish, fail, or be interrupted. |
| `frame.ended` | A frame has closed, failed, or been abandoned. |
| `session.ending` | The top-level harness session is about to close or needs close-out coordination. |
| `session.ended` | The top-level harness session has closed or been abandoned. |
| `supervisor.tick` | A supervisor, scheduler, watchdog, or bridge is polling an active lifecycle. |
| `capability.degraded` | A previously negotiated lifecycle capability changed support state mid-session. |
| `receipt.emitted` | A lifecycle receipt was emitted and is available for client projection. |
| `receipt.gap_detected` | Lifeloop observed a receipt sequence gap it cannot reconstruct in a harness sequence or declared receipt ledger. |

Rules:

- `session.*` events describe a top-level harness lifecycle.
- `frame.*` events describe one execution or inference frame inside a session.
- A simple chat or coding harness may have one frame per prompt turn.
- A recursive client may create nested frames under the same session.
- `context.pressure_observed` is not a checkpoint, compaction, or policy
  action. It is the neutral observation that a client may react to.
- `context.compacted` reports that the harness context was rewritten or
  compacted. It is still only a lifecycle fact; clients decide what that means.
- Lifeloop events do not grant write authority or policy approval.
- Lifeloop dispatch is not inherently asynchronous. A compatibility transport
  may observe an event, run the client reaction, and return a synchronous
  response in one invocation.

## Adapter Manifest

Every adapter exposes a manifest before client negotiation. The manifest is an
honest report of lifecycle behavior, not a lowest-common-denominator API.

Illustrative shape:

```json
{
  "contract_version": "lifeloop.v0.2",
  "adapter_id": "codex",
  "adapter_version": "0.1.0",
  "display_name": "Codex",
  "role": "primary_worker",
  "integration_modes": ["native_hook", "manual_skill"],
  "lifecycle_events": {
    "session.starting": { "support": "native", "modes": ["native_hook"] },
    "frame.opening":    { "support": "native", "modes": ["native_hook"] },
    "context.pressure_observed": { "support": "synthesized", "modes": ["native_hook"] },
    "context.compacted": { "support": "unavailable", "modes": [] },
    "supervisor.tick":  { "support": "unavailable", "modes": [] }
  },
  "placement": {
    "pre_session":         { "support": "native",      "max_bytes": 8192 },
    "pre_frame_leading":   { "support": "native",      "max_bytes": 8192 },
    "pre_frame_trailing":  { "support": "unavailable" },
    "tool_result":         { "support": "unavailable" },
    "manual_operator":     { "support": "manual" }
  },
  "context_pressure": {
    "support": "synthesized",
    "evidence": "managed Codex lifecycle hooks observe context.pressure_observed when telemetry signals are available"
  },
  "receipts": {
    "native": false,
    "lifeloop_synthesized": true,
    "receipt_ledger": "unavailable"
  },
  "session_identity": {
    "harness_session_id": "native",
    "harness_run_id":     "synthesized",
    "harness_task_id":    "unavailable"
  },
  "renewal": {
    "reset": {
      "native": "unavailable",
      "wrapper_mediated": "synthesized",
      "manual": "manual"
    },
    "continuation": {
      "observation": "native",
      "payload_delivery": "synthesized"
    },
    "profiles": ["ccd-renewal"],
    "evidence": "Codex Stop block-as-continuation evidence plus the opt-in ccd-renewal host-hook profile proves wrapper-mediated reset prepare and out-of-band continuation-token delivery"
  },
  "failure_modes": ["transport_error", "payload_too_large"],
  "known_degradations": []
}
```

### Required manifest fields

| Field | Meaning |
|---|---|
| `contract_version` | The Lifeloop contract version this manifest targets, e.g. `lifeloop.v0.2`. |
| `adapter_id` | Stable wire identifier (e.g. `codex`, `claude`). Must round-trip through any `adapter_id` field elsewhere on the wire. |
| `adapter_version` | The adapter's own version string. Independent of the contract version so adapters can iterate without bumping the contract. |
| `display_name` | Human-facing label. |
| `role` | Single [adapter role]#adapter-roles the manifest claims. Adapters that fill more than one role expose more than one manifest. |
| `integration_modes` | Non-empty list of integration modes the adapter supports. |
| `lifecycle_events` | Map of [lifecycle event names]#lifecycle-event-vocabulary to per-event support claims. |
| `placement` | Map of [manifest placement classes]#manifest-placement-classes to per-class support claims. |
| `context_pressure` | Single capability claim describing how the adapter surfaces `context.pressure_observed` evidence. |
| `receipts` | Capability claim describing receipt emission and ledger support. |

### Optional manifest fields

| Field | Meaning |
|---|---|
| `session_identity` | Per-id support claims for `harness_session_id`, `harness_run_id`, `harness_task_id`. |
| `session_rename` | Capability claim for the adapter's session-rename surface. Omitted when the adapter has no rename concept. |
| `renewal` | Capability claim for reset/continuation renewal. Omitted when the adapter has not evaluated renewal mechanics. |
| `approval_surface` | Capability claim for operator approval/intervention surfaces. |
| `failure_modes` | List of [failure classes]#failure-classes the adapter is known to surface. Diagnostic; absence does not mean other failure classes cannot occur. |
| `telemetry_sources` | List of telemetry source descriptors the adapter exposes for lifecycle evidence. |
| `known_degradations` | Pre-declared capability degradations the adapter ships with (e.g. a previously native capability now unavailable in a specific build). |

### Renewal/reset capability

The optional `renewal` manifest field describes whether an adapter can prove a
safe reset/continuation lifecycle path. It is intentionally not a CCD renewal
lease, continuation token, thread binding, or fail-closed policy surface.
Clients decide whether a renewal is allowed; Lifeloop only reports adapter
capability and emits delivery receipts for lifecycle facts.

Shape:

```json
{
  "reset": {
    "native": "unavailable",
    "wrapper_mediated": "partial",
    "manual": "manual"
  },
  "continuation": {
    "observation": "partial",
    "payload_delivery": "unavailable"
  },
  "profiles": [],
  "evidence": "operator wrapper can observe continuation, but cannot inject payloads"
}
```

Fields:

| Field | Meaning |
|---|---|
| `reset.native` | The harness exposes a direct reset or renewal boundary. |
| `reset.wrapper_mediated` | A launcher, wrapper, reference adapter, or extension can mediate reset. |
| `reset.manual` | Reset depends on an operator/manual surface. |
| `continuation.observation` | The adapter can prove that the post-reset continuation boundary happened. |
| `continuation.payload_delivery` | The adapter can carry client-provided continuation facts across the boundary. |
| `profiles` | Optional non-empty host integration profile ids required for this claim. Absent or empty means the claim is profile-independent. |
| `evidence` | Optional human-readable evidence. It must not contain client-owned continuation tokens or policy state. |

All reset fields set to `unavailable` means the manifest declares no safe reset
path. `continuation.observation` and `continuation.payload_delivery` are
separate so observation-only integrations cannot be mistaken for integrations
that can deliver opaque client continuation payloads.

The first shipped positive claim is Codex and is scoped to the opt-in
`ccd-renewal` profile. Lifeloop's host-hook broker observes CCD's `session_boundary.action =
"renew"`, invokes `ccd session renew prepare --adapter codex --reset-path
wrapper`, stores the opaque continuation token outside hook stdout, and consumes
that token on the next Codex `SessionStart` by invoking `ccd start --refresh
--continuation <token>`. The token remains client-owned; Lifeloop records only
delivery evidence and thread-binding checks.

### Support states

Support values describe how strongly the adapter satisfies a capability claim.

| Value | Meaning |
|---|---|
| `native` | The harness exposes the behavior directly. |
| `synthesized` | Lifeloop can synthesize the behavior from another stable signal (telemetry, log scraping, derived hooks). |
| `manual` | The behavior depends on an operator or manual wrapper. |
| `partial` | The adapter exposes an incomplete or lossy form of the behavior. Clients must explicitly accept partial support before Lifeloop treats it as satisfied; otherwise it degrades. |
| `unavailable` | The adapter cannot provide the behavior. |

Note: pre-issue-#6 drafts of this section listed `simulated` and `inferred` as
separate values. `synthesized` replaces `simulated` (clearer about the
direction of derivation); `inferred` is folded into `partial` (telemetry-derived
behavior is partial behavior).

### Manifest placement classes

The manifest declares placement support using a trust-neutral, lifecycle-timing
vocabulary. These classes describe *where in the lifecycle* an adapter accepts
payload placement, independent of whether that placement is comparable to a
"developer" or "system" frame in any specific harness's UI.

| Class | Meaning |
|---|---|
| `pre_session` | Before any frame opens; e.g. session-init context. |
| `pre_frame_leading` | At the leading edge of a frame, before any user prompt or task input arrives. |
| `pre_frame_trailing` | At the trailing edge of a frame, after the prompt/input but before the model executes. |
| `tool_result` | Inside a tool-result envelope returned to the model. |
| `manual_operator` | Through an operator or manual surface (skill, command, wrapper). |

These classes are distinct from the [payload placement classes](#opaque-payload-envelope)
the runtime uses for routing concrete payloads on `acceptable_placements`. The
manifest placement vocabulary describes capability claims; the payload placement
vocabulary describes routing requests. A future revision may unify them; the
current contract keeps them separate.

### Integration modes

- `manual_skill`
- `launcher_wrapper`
- `native_hook`
- `reference_adapter`
- `telemetry_only`

`telemetry_only` is for integrations that can observe lifecycle evidence from
logs, activity files, or other telemetry, but cannot inject payloads or control
the harness lifecycle directly.

### Adapter roles

- `primary_worker`
- `worker`
- `supervisor`
- `observer`

### Manifest registry

Lifeloop ships a built-in manifest registry that lists adapters with shipped
support. The registry distinguishes:

- **v1 conformance adapters** — Codex and Claude have manifests whose claims
  must be backed by extracted code paths (asset rendering, telemetry,
  placement support). Registry tests verify each capability claim against the
  underlying implementation.
- **pre-conformance adapters** — Hermes, OpenClaw, Gemini, OpenCode may
  ship initial manifests whose claims are not yet test-verified end-to-end.
  These are useful for client negotiation against partially-supported adapters
  but should not be assumed to be exhaustive.

Registry lookup is by `adapter_id`; adapters do not import client modules.

## Capability Negotiation

A client request declares each lifecycle capability with a requirement level.

Requirement levels:

| Level | Meaning |
|---|---|
| `required` | Lifeloop must refuse before dispatch when the adapter cannot satisfy the requested support. |
| `preferred` | Lifeloop may continue with a degraded receipt and warning. |
| `optional` | Lifeloop may omit the capability without warning unless the adapter previously promised it and then degraded. |

Negotiation outcomes:

| Outcome | Meaning |
|---|---|
| `satisfied` | The adapter meets the requested capability and support level. |
| `degraded` | Lifeloop can proceed, but support is weaker than requested. |
| `unsupported` | The adapter cannot provide the capability. |
| `requires_operator` | Manual setup or approval is required before dispatch. |

Clients negotiate lifecycle capabilities, payload placements, identity
correlation, context-pressure observations, and receipt behavior before using
an adapter. Lifeloop owns the negotiation result. Clients own how strict they
are about the result.

## Negotiation Timing And Transport

Negotiation is a Lifeloop operation, but it does not require a new public CLI
command in the first extraction.

Rules:

- Every adapter must expose a manifest before an operation is dispatched.
- Lifeloop evaluates client requirements against the manifest before every
  lifecycle operation that can mutate client-visible state or deliver payloads.
- A synchronous compatibility invocation may cache a negotiation result only
  inside that invocation.
- Cross-invocation negotiation caching requires an explicit manifest capability
  and a Lifeloop-owned or adapter-owned cache boundary.
- A session implementation that advertises cross-invocation caching must
  recheck cached capabilities when telemetry or adapter state can degrade
  mid-session.
- For `host-hook` compatibility, negotiation happens inside the existing
  synchronous command invocation. Unsupported required capabilities fail before
  client-owned state mutation.
- For `host apply` compatibility, install/apply remains a CLI compatibility
  command, but the installed assets are lifecycle integration assets whose
  semantic target is Lifeloop normalization.
- A later native Lifeloop transport may expose explicit preflight negotiation,
  but it must preserve these same requirement levels and outcomes.

## Mid-Session Degradation

If a capability changes after negotiation, Lifeloop emits
`capability.degraded`.

Required degradation fields:

```json
{
  "event": "capability.degraded",
  "capability": "context_pressure",
  "previous_support": "native",
  "current_support": "unavailable",
  "observed_at_epoch_s": 1778100000,
  "evidence": "telemetry file missing for two consecutive polls",
  "retry_class": "retry_after_reconfigure"
}
```

Rules:

- Degradation is lifecycle evidence, not a client decision.
- Lifeloop must name the degraded capability and support transition.
- The same degradation may make one client stop and another continue.
- Lifeloop must not silently downgrade a `required` capability after dispatch;
  it must emit a receipt and a degradation event.

## Opaque Payload Envelope

Payloads are client-owned data delivered at lifecycle moments. Lifeloop handles
transport and placement only.

```json
{
  "schema_version": 1,
  "payload_id": "pay_01K...",
  "client_id": "example-client",
  "payload_kind": "instruction_frame",
  "format": "client-defined",
  "content_encoding": "utf8",
  "body": "opaque client payload",
  "body_ref": null,
  "byte_size": 1200,
  "content_digest": "sha256:...",
  "acceptable_placements": [
    {
      "placement": "developer_equivalent_frame",
      "requirement": "preferred"
    },
    {
      "placement": "pre_prompt_frame",
      "requirement": "required"
    }
  ],
  "idempotency_key": "idem_01K...",
  "expires_at_epoch_s": null,
  "redaction": "none",
  "metadata": {}
}
```

Rules:

- `body` and `body_ref` are mutually exclusive.
- Lifeloop validates size, digest, encoding, expiration, and placement support.
- Lifeloop does not parse `body` beyond transport-safe encoding checks.
- `metadata` is for client correlation only. Lifeloop may echo it but must not
  assign semantics to unknown keys.
- Payload priority is not a Lifeloop concept. Clients express placement needs
  through acceptable placements and requirement levels.

For harnesses whose hook protocol surfaces a single `additionalContext` slot
per event (e.g. Claude Code, Codex), Lifeloop renders a transport envelope of
the form `{"payloads": [...]}` carrying one object per eligible payload, in
input order, each containing `payload_id`, `payload_kind`, and either `body`
(as a verbatim JSON string) or `body_ref`. `body` is never parsed: a body
whose contents happen to be a JSON object literal is carried as a string, so
overlapping JSON keys across payloads remain distinguishable. `body_ref` is a
reference and is never dereferenced by the renderer. `payloads` is the only
Lifeloop-reserved key in the envelope.

Placement classes:

| Placement | Meaning |
|---|---|
| `developer_equivalent_frame` | Strong instruction/context layer comparable to developer or system-adjacent harness context. |
| `pre_prompt_frame` | Context prepended before the next user or task prompt. |
| `side_channel_context` | Out-of-band context channel exposed by the harness or reference adapter. |
| `receipt_only` | No prompt injection; payload metadata is recorded or correlated only. |

Placement resolution:

- `acceptable_placements` is an ordered list of alternative placements, not a
  request to deliver the same payload everywhere.
- Lifeloop selects the first satisfiable placement in client order.
- If no placement is satisfiable and at least one acceptable placement is
  `required`, the operation fails with `placement_unavailable`.
- If only `preferred` or `optional` placements are unavailable, Lifeloop may
  continue with a degraded or skipped payload receipt according to the client
  request.
- Delivering to multiple placements requires a future explicit multi-delivery
  option; it is not implied by listing multiple acceptable placements.
- `receipt_only` stores `payload_id`, digest, placement decision, and metadata
  needed for correlation. It must not persist the opaque payload body unless an
  explicit `payload_store` capability is negotiated.

### Dispatch envelope (transport boundary)

The CLI and the subprocess invoker carry payload envelopes alongside the
callback request through a single transport-boundary shape, the dispatch
envelope:

```json
{
  "schema_version": "lifeloop.v0.2",
  "request": { "...CallbackRequest...": "..." },
  "payloads": [ { "...PayloadEnvelope...": "..." } ]
}
```

Rules:

- `request` is the validated `CallbackRequest`. `payloads` is the (possibly
  empty) list of envelopes the dispatch is delivering with; on the wire the
  key is omitted when the list is empty.
- The dispatch envelope is the contract between Lifeloop and any subprocess
  callback client: clients deserialize this single document from stdin and
  return a `CallbackResponse` on stdout. The same shape is what `lifeloop
  event invoke` reads on stdin.
- Lifeloop never parses `payloads[].body` — bodies are transported verbatim
  per the opacity rule above. Cross-correlation between
  `request.payload_refs` and `payloads[]` is intentionally not enforced at
  the transport boundary; that responsibility belongs to negotiation and
  receipt synthesis.

## Receipt Schema

Every lifecycle operation returns or emits a bounded receipt.

Required receipt fields:

```json
{
  "schema_version": 1,
  "receipt_id": "lfr_01K...",
  "idempotency_key": "idem_01K...",
  "client_id": "example-client",
  "adapter_id": "example-harness",
  "invocation_id": "inv_01K...",
  "event": "frame.opening",
  "event_id": "evt_01K...",
  "sequence": 42,
  "parent_receipt_id": "lfr_01K_parent",
  "integration_mode": "native_hook",
  "status": "delivered",
  "at_epoch_s": 1778100000,
  "harness_session_id": "session-123",
  "harness_run_id": "run-456",
  "harness_task_id": null,
  "payload_receipts": [
    {
      "payload_id": "pay_01K...",
      "payload_kind": "instruction_frame",
      "placement": "pre_prompt_frame",
      "status": "delivered",
      "byte_size": 1200,
      "content_digest": "sha256:..."
    }
  ],
  "telemetry_summary": {
    "context_pressure": "moderate"
  },
  "capability_degradations": [],
  "failure_class": null,
  "retry_class": "safe_retry",
  "warnings": []
}
```

Receipt statuses:

- `observed`
- `delivered`
- `skipped`
- `degraded`
- `failed`

Identifier rules:

- `client_id` is a client-declared stable label. CCD uses `ccd`; an RLM client
  uses its own non-CCD label such as `rlm`.
- Idempotency keys are scoped by `(client_id, adapter_id, idempotency_key)`.
- `invocation_id` identifies one transport invocation. A single synchronous
  command may emit multiple event receipts with the same `invocation_id`.
- `receipt_id` is a new opaque identifier for this emitted receipt.
- `event_id` identifies the observed lifecycle invocation. If a harness
  supplies a stable hook/run identifier, Lifeloop uses it; otherwise Lifeloop
  synthesizes one for that process invocation and records the weaker ordering
  in the receipt.
- `sequence` is required and nullable. It is monotonic within the strongest
  available durable session scope. When harness sequencing or a receipt ledger
  is unavailable, Lifeloop may set `sequence` to `null` rather than inventing a
  misleading cross-invocation order.
- `parent_receipt_id` is required and nullable. It is `null` for root receipts
  and set for nested or causally linked lifecycle receipts.
- `idempotency_key` is the client-supplied replay boundary when present.
  Lifeloop must not infer durable idempotency from timestamp-derived event IDs.
- `payload_receipts[]` entries identify the payload artifact Lifeloop placed.
  `payload_kind` is required and is scoped by the receipt's `client_id`.
  `content_digest` is optional and is omitted when the negotiated payload did
  not carry a digest. Lifeloop echoes the negotiated payload's digest; it does
  not invent or normalize one during receipt synthesis.

Ordering rules:

- Ordering is per harness session when stable harness sequencing exists.
- Otherwise ordering is per declared receipt ledger when that capability
  exists.
- Otherwise receipts are only diagnostic artifacts ordered by
  `(at_epoch_s, receipt_id)`.
- Clients may sort by `(harness_session_id, sequence)` when both are present.
- Clients may sort diagnostics by `(at_epoch_s, receipt_id)`.
- Clients must not assume a total order across adapters or harness sessions.

Idempotency rules:

- If the caller supplies `idempotency_key`, repeated delivery with identical
  receipt content is an idempotent replay when a receipt ledger is available.
- Reusing an `idempotency_key` with different content is a
  `duplicate_id_conflict` when a receipt ledger is available.
- Without an idempotency key, `receipt_id` is the replay boundary.
- Without a receipt ledger, Lifeloop validates and echoes idempotency keys but
  does not claim durable duplicate detection across synchronous process
  invocations. Durable client-side mutation safety remains client-owned.

Loss semantics:

- Lifeloop receipts are evidence, not the client's source of truth.
- Lifeloop does not silently reconstruct lost receipts.
- Lifeloop emits `receipt.gap_detected` only when stable harness sequencing or
  a receipt ledger makes a gap observable.
- Clients recover from their own state stores.

## Failure Classes

Failure classes are lifecycle classifications owned by Lifeloop.

| Failure class | Meaning |
|---|---|
| `adapter_unavailable` | The selected adapter cannot be reached or loaded. |
| `capability_unsupported` | A required capability is unavailable before dispatch. |
| `capability_degraded` | A previously available capability is now weaker or absent. |
| `placement_unavailable` | No acceptable payload placement can be satisfied. |
| `payload_too_large` | The payload exceeds the negotiated placement limit. |
| `payload_rejected` | The harness rejected a payload for lifecycle/transport reasons. |
| `identity_unavailable` | Required harness identity fields cannot be observed. |
| `transport_error` | The harness transport failed. |
| `timeout` | The lifecycle operation timed out. |
| `operator_required` | Manual setup, approval, or intervention is required. |
| `state_conflict` | Lifecycle correlation or idempotency state conflicted. |
| `invalid_request` | The client request failed schema or precondition validation. |
| `internal_error` | Lifeloop failed unexpectedly. |

Adapters provide raw evidence. Lifeloop maps that evidence into these classes.
Clients must not text-parse adapter errors for retry posture.

Renewal/reset flows use the existing failure classes:

| Renewal condition | Failure class | Meaning |
|---|---|---|
| Reset capability is unavailable before dispatch. | `capability_unsupported` | No safe adapter-owned reset path exists. |
| Continuation payload delivery was lost or rejected. | `payload_rejected` | The lifecycle fact was understood, but delivery failed. |
| A previously negotiated renewal capability is stale or weaker. | `capability_degraded` | The client should reread capability state before deciding. |
| The only reset path is operator/manual. | `operator_required` | A human or manual wrapper must act before retry. |

## Retry Classes

| Retry class | Meaning |
|---|---|
| `safe_retry` | The same request may be retried with the same idempotency key. |
| `retry_after_reread` | The client should reread lifecycle or client state before retrying. |
| `retry_after_reconfigure` | Adapter setup or capability configuration must change first. |
| `retry_after_operator` | Operator action is required before retry. |
| `do_not_retry` | Blind retry would repeat an unsafe or invalid operation. |

Default mapping:

| Failure class | Default retry class |
|---|---|
| `adapter_unavailable` | `retry_after_reconfigure` |
| `capability_unsupported` | `do_not_retry` |
| `capability_degraded` | `retry_after_reread` |
| `placement_unavailable` | `retry_after_reconfigure` |
| `payload_too_large` | `do_not_retry` |
| `payload_rejected` | `retry_after_reconfigure` |
| `identity_unavailable` | `retry_after_reconfigure` |
| `transport_error` | `safe_retry` |
| `timeout` | `safe_retry` |
| `operator_required` | `retry_after_operator` |
| `state_conflict` | `retry_after_reread` |
| `invalid_request` | `do_not_retry` |
| `internal_error` | `retry_after_reread` |

Adapters may tighten retry posture for known unsafe operations, but they must
not loosen it without a spec update or dedicated conformance proof.

## Conformance Expectations

An implementation of this contract should include:

- manifest schema validation for every shipped adapter
- registry-backed capability-claim verification for v1 conformance adapters:
  every claim in a Codex or Claude manifest that depends on extracted code
  (asset rendering, telemetry parsing, placement support) is paired with a
  test that runs the corresponding code path and asserts the manifest claim
  is true
- negotiation tests for `required`, `preferred`, and `optional`
- negotiation tests proving `partial` support degrades unless a client
  explicitly marks it acceptable
- payload placement tests for success, degradation, and refusal
- receipt ordering and idempotency tests
- receipt schema tests proving required nullable fields such as
  `sequence` and `parent_receipt_id` are present even when null
- identifier tests proving `receipt_id`, `event_id`, `sequence`, and
  `idempotency_key` semantics do not collapse into one field
- invocation tests proving multi-event synchronous invocations share an
  `invocation_id` and use `parent_receipt_id` for causal chaining
- synchronous dispatch tests proving compatibility transports can preserve
  existing command-response behavior while using Lifeloop internally
- stateless first-slice tests proving no cross-invocation sequence,
  gap-detection, negotiation-cache, or idempotency-store behavior is claimed
  unless the manifest advertises the needed stateful capability
- manifest tests covering `telemetry_only` adapters separately from native or
  reference adapters
- degradation tests proving mid-session capability loss is surfaced
- failure-class mapping tests for every adapter
- static ownership checks proving Lifeloop core does not import client-owned
  continuity or recursive-inference modules. In this repo, the current gate is
  `tests/kernel_purity.rs::lifeloop_static_boundary_proof_keeps_client_vocabulary_out`.

## Implementation Status

The `lifeloop.v0.2` slice of this contract is implemented in the
`lifeloop` crate. The repo carries the lifecycle event vocabulary,
adapter manifest registry (with v1 conformance manifests for Codex and
Claude plus pre-conformance manifests for Hermes, OpenClaw, Gemini, and
OpenCode), capability/placement negotiation, opaque payload envelopes,
the dispatch envelope transport-boundary shape, lifecycle receipts with
per-payload receipt provenance, and the 13-class `FailureClass` /
5-class `RetryClass` enums with the spec's failure-to-retry default
mapping.

The lifecycle router (`src/router/`) wires those pieces together as:
pre-dispatch validation + adapter resolution → capability/placement
negotiation → callback invocation (in-process or subprocess over JSON
stdio via the dispatch envelope) → receipt synthesis with idempotency.
The native Lifeloop transport surface is the `lifeloop` CLI plus the
documented subprocess callback contract. The synchronous `host-hook`
broker is one client of this contract: it exists to connect installed
host assets to lifecycle normalization and to mediate opt-in CCD renewal
without making CCD renewal leases or continuation-token policy part of
Lifeloop's contract.

A non-CCD client class is shipping in this repo as the thread-sync publisher
(`crates/thread-sync-publisher/`); it consumes the same callback contract and
is the first proof that lifecycle reach is reusable beyond CCD. The #28
product pilot is the Fixity client (`crates/fixity-pilot/`), which consumes
`DispatchEnvelope` payload bodies or refs through the real subprocess dispatch
path while keeping repeated-signal policy outside Lifeloop core. A durable
receipt ledger and additional non-CCD client classes (an RLM prototype and
other lifecycle-only clients) remain follow-on work governed by the v1 freeze
gates in `docs/release-gates.md`.