# ADR-0106: The lion machine-result contract, v1

- **Status**: Proposed
- **Kind**: Aspirational (records the target state; the MCP server implements part of it today and is brought into conformance by this ADR)
- **Area**: cli-surface
- **Date**: 2026-07-25
- **Relations**: extends ADR-0066

## Depth note

This ADR fixes a contract that two independent programs implement against, only one
of which lives in this repository. Everything normative is stated as a shape or an
enumerated case, because the other consumer cannot read our source to resolve an
ambiguity and will resolve it by guessing.

## Context

lionagi's orchestration surface is reachable today by exactly one route per caller:
run the `li` CLI, or speak to the MCP server in `lionagi/mcp/`, which spawns `li` and
reads back the state the CLI persists. A second consumer is now being written outside
this repository, in another language, to front the same orchestration through a
different agent-facing surface. It will reach lionagi the way the MCP server already
does — by executing the CLI as a subprocess — and it will hold no state of its own.

That second consumer is what makes this a contract rather than an implementation
detail. Until now, "what a submit returns" and "what a status read means" were
decided by reading `lionagi/mcp/jobs.py`. A consumer that cannot read that file, and
that must keep working across our releases, needs the answers written down and
versioned.

### The problems

**P1 — Two implementations of the same answers drift, and the drift is silent.**
Both consumers answer "is this run finished, and where is its output". Today each
would derive that from whatever fields it happens to find. A field one treats as
authoritative and the other treats as advisory produces two different answers to the
same question about the same run, and nothing fails: both return successfully.

**P2 — A consumer that owns a status vocabulary will eventually report a failure as a
success.** This is not hypothetical. `lionagi/mcp/jobs.py` previously matched the
CLI's terminal status against a local set and fell through to `"completed"` on a miss,
which silently converted every status that set did not list — `timed_out`,
`cancelled`, `aborted`, `completed_empty` — into a false success. The bug is
structural, not careless: a local vocabulary is a copy, copies go stale when the CLI
adds a status, and the failure mode of a stale copy is a wrong answer rather than an
error. Any consumer given the freedom to classify will re-acquire this bug.

**P2b — But "did it end" is not "did it succeed", and publishing only the first
recreates P2 one level up.** `completed_empty` is terminal and is not a success.
A consumer told to branch on a terminal flag, and forbidden from reading status, has
no way to distinguish them and will either invent the forbidden vocabulary or treat
every terminal run as done-and-fine. The producer therefore has to publish both facts,
not one.

**P3 — The reading consumer holds no state, so every question must be answerable from
lionagi's store alone.** The external consumer's own process may restart between a
submit and the status read that follows it. If any part of the answer lives only in
the consumer's memory, the answer changes across that restart, which makes a run's
recorded history depend on the health of a process that is not running it.

**P4 — A subprocess consumer has exactly two channels and they are not
interchangeable.** stdout carries the machine result; stderr carries diagnostics.
Today the CLI's human-facing surfaces mix progress, warnings and results across both,
because the reader was a person. A consumer that parses stderr, or that parses stdout
containing an unannounced progress line, breaks on any change to logging.

**P5 — A consumer built against one version of the contract will be run against
another, and the substitution can happen while it is alive.** lionagi releases
independently of its consumers. Worse, the consumer's own binary is replaced routinely
during operation, and an absolute path pins the path, not the build behind it. A
version established once at startup is a claim about a file that may since have been
replaced.

**P6 — Absence and failure look identical in a naive shape.** "No artifacts" and "the
artifacts directory could not be read" both compress to an empty list. "Not finished"
and "finished with no output" both compress to a null. A consumer cannot tell a fact
from a failure to establish one, and the safe reading is not knowable from the value.

**P7 — A broken lionagi installation is not a failed run, and the subprocess boundary
erases the difference.** A consumer sees a nonzero exit and no result. If the
environment cannot start `li` at all, attributing that to the submitted work is how a
four-hour outage gets recorded as a series of crashed agents.

**P8 — A push notification cannot be the only way a consumer learns a run ended.**
Delivery can fail, and the record of its failure lives in the state a consumer that is
not polling is by definition not reading. Nothing wakes it to learn that nothing will
wake it. A consumer restart across a successful delivery loses it the same way.

### Decisions

| Concern | Decision |
|---------|----------|
| What the contract covers and who owns it | D1: lionagi owns the contract; it is a CLI-level machine surface, not the MCP server's Python API |
| Version negotiation | D2: `contract_version` is an integer, checked on every envelope, not only at a handshake |
| The envelope every machine call returns | D3: one envelope shape — `ok`, `contract_version`, `data`, `error` |
| Run status | D4: `status` is opaque and verbatim; the producer also publishes `terminal` and `outcome`, on every status-bearing response |
| Submit | D5: submit returns a handle; the spawn phase is recorded rather than inferred from a missing pid |
| Reads | D6: one lifecycle authority for every path; liveness is advisory; a conclusively-gone `started` orphan is reaped terminal where the fenced reap publishes (ADR-0107); a `preparing` or inconclusive one, or a reap whose write was refused, stays non-terminal |
| Distinguishing absence from failure | D7: every read-derived field carries its own availability and reason |
| Process-level faults | D8: a valid envelope is authoritative; exit status is the transport-level answer, with a defined precedence |
| Terminal notification | D9: a notification is a prompt to read state, never proof and never the only path |
| Bounded observation | D10: `wait` is bounded, returns partial results, and shares ADR-0066 D6's wait contract — first stated here as extensions, since adopted into D6 by its 2026-07-27 and 2026-08-03 amendments; this ADR carries the fuller statement |

Out of scope:

- **The verb set khive exposes.** The pack curates which verbs it surfaces and under
  what names; that is its decision, not ours. This ADR fixes what a verb *returns*.
- **How the consumer reaches the binary** (sandboxing, per-call timeout budgets).
  Owned by the consumer; see D1 on why we nonetheless state a requirement about the
  binary path.
- **The MCP tool surface itself.** ADR-0066 decides that. This ADR constrains what
  those tools return, which is a strictly smaller question. D10 shares ADR-0066 D6's
  wait contract — the rules D10 first stated as additions were adopted into D6 by its
  2026-07-27 and 2026-08-03 amendments, so neither document re-decides the other; D10
  carries the fuller rationale.
- **Hosted / multi-tenant concerns.** No tenancy appears in this contract; a hosted
  join is built above it, not inside it.
- **Playbook and flow semantics.** What a flow *does* is unchanged here.

## Decision

### D1 — lionagi owns the contract, and it is a CLI-level surface

The contract is a property of the `li` executable, not of `lionagi.mcp`. Both the
in-repo MCP server and any external consumer obtain results the same way: by running a
CLI command that emits a machine result on stdout.

**The contract surface.** Machine mode is explicit, never inferred:

```text
li <command> ... --machine
```

Under `--machine`, a command writes exactly one JSON object to stdout and nothing else,
sends all human-facing output to stderr, and does not colour, paginate, or prompt.

**Why a CLI flag rather than the MCP server's Python API.** The external consumer is
not written in Python and cannot import our modules. Making the MCP server's functions
the contract would mean the external consumer's contract is a re-implementation of the
Python surface in another language, which is exactly the drift P1 describes. Making
the CLI the contract gives both consumers the same artifact, and it makes our own MCP
server a consumer of the contract rather than its definition — which is the only way
we find out when we break it.

**A requirement we place on consumers, despite D1's ownership split.** The consumer
must invoke the binary by absolute path and never through `PATH` resolution. This is
stated here rather than left to each consumer, because a consumer that resolves `li` on
`PATH` may execute a different installation from the one it validated against. Note
that this bounds *which path* is executed and not *which build is behind it*; D2 covers
the remainder.

### D2 — Version is an integer, and it is checked on every envelope

```text
li handshake --machine
```

```json
{
  "ok": true,
  "contract_version": 1,
  "data": {
    "contract_version": 1,
    "min_supported_version": 1,
    "implementation": "lionagi",
    "implementation_version": "0.30.2",
    "module": "/abs/path/to/lionagi/cli"
  },
  "error": null
}
```

**Exact semantics.**

- `contract_version` is a single integer, incremented only on a change that could break
  a conforming consumer.
- The handshake governs **registration**: a consumer performs it once at
  initialisation and refuses to register any verb if the version is outside the range
  it understands. It does not register and then fail per call, because a surface that
  appears and then errors is indistinguishable, to the agent using it, from a surface
  whose underlying work failed.
- **Every envelope repeats `contract_version`, and a consumer validates it before
  decoding `data`.** The handshake is not sufficient on its own: the binary at the
  pinned absolute path is replaced during normal operation, so a consumer that
  handshook v1 can have its next call answered by v2. On mismatch the consumer stops
  trusting the surface — it degrades or unregisters — rather than decoding v2 data
  under v1 assumptions.
- `min_supported_version` is the oldest contract this implementation still honours, so
  a dropped old contract is diagnosable rather than mysterious.
- `implementation_version` and `module` are diagnostic. They answer "which build is
  talking" for a human reading a report; a consumer that branches on
  `implementation_version` instead of `contract_version` has re-invented version
  sniffing and will break on a patch release.

**What "additive changes are free" does and does not mean.** A new optional field, a
new value in an open vocabulary, or a new command does not increment the version, and
a consumer must ignore fields it does not recognise. But *ignoring a field is only safe
when omitting it preserves every v1 meaning.* A field that qualifies an existing value
— an encoding marker beside `console.text`, a redaction flag, a partial-result marker
— changes what the existing field means to a reader that drops it, and a v1 consumer
would present encoded or partial content as the complete result. Such a field is a
breaking change and increments the version, however additive it looks. The test is not
"is the field new" but "does a v1 consumer that ignores it still hold only true
beliefs".

**Why an integer and not semver.** A consumer's only real decision is "can I speak this
or not". A version with three components invites partial-compatibility logic, which is
another copy of our rules living in code we do not control.

### D3 — One envelope, for every machine call that reaches the dispatcher

```json
{ "ok": true,  "contract_version": 1, "data": { }, "error": null }
```

```json
{
  "ok": false,
  "contract_version": 1,
  "data": null,
  "error": { "kind": "not_found", "message": "no job with id ...", "detail": null }
}
```

**Exact semantics.**

- `ok` is the only field a consumer inspects to decide request success. It is not
  derived from `error` being null, from `data` being non-empty, or from the exit status.
- Exactly one of `data` / `error` is non-null. Both null, or both set, is malformed and
  the consumer treats it as a contract violation rather than guessing.
- `error.kind` is drawn from a closed set: `not_found`, `invalid_input`, `conflict`,
  `unavailable`, `internal`. A consumer may branch on it; new kinds require a version
  increment, which is what makes branching safe. This is deliberately the opposite of
  D4's open vocabulary, because `error.kind` describes *our* refusal to answer, which
  we control, while run status describes the outcome of foreign work, which we do not.
- `error.message` is for a human and is never parsed. `error.detail` is an optional
  object whose contents are not contractual.
- The envelope is emitted even when the command fails, because a failure producing no
  JSON is indistinguishable from a crash.
- **The scope of "every call" is every request that reaches the machine dispatcher.** A
  failure before that point cannot emit an envelope, because the code that builds one has
  not loaded and the contract version it would carry is not knowable. Those cases are
  enumerated in D8 and are diagnosed by exit status alone; a consumer must not classify them
  as malformed output. Stating this here rather than only in D8 matters because a
  cross-language consumer implementing D3 literally would turn the intended environment
  diagnosis into a protocol violation, which is the loudest possible way to report the one
  fault that has nothing to do with the request.

### D4 — `status` is opaque; `terminal` and `outcome` are the producer's derivations

```json
{
  "run_id": "20260725T175010-97f334",
  "kind": "agent",
  "label": "example",
  "status": "completed_empty",
  "terminal": true,
  "outcome": "failed",
  "reason_code": "no_artifacts",
  "alive": false,
  "pid": 41234,
  "submitted_at": "2026-07-25T17:50:10.412331+00:00",
  "finished_at": "2026-07-25T17:51:28.636751+00:00"
}
```

**Exact semantics.**

- `status` is an **open vocabulary**. lionagi may add values in any release without
  incrementing `contract_version`. A consumer records and displays it verbatim and
  **must not** map it onto a local set. The values in use at v1 are `running`,
  `completed`, `completed_empty`, `failed`, `timed_out`, `aborted`, `cancelled`,
  `killed`, `exited`, `unknown` — informative, not exhaustive; a consumer treating this
  list as closed is non-conforming.
- `terminal` answers **"stop waiting"**. True when and only when the run reached an end
  state, derived by lionagi from a recorded end — `finished_at`, a producer-written spawn
  failure (D5) — never by matching `status` against a set, and never computed by a reader.
  In v1 those are the only two sources; the deferred reconciler in D6 would be a third.
  (2026-08-03: ADR-0107 has since implemented that reconciler — its orphan reaper is now
  the third source.)
- `outcome` answers **"did the work come out right"**. It is a **closed** vocabulary,
  `succeeded | failed | cancelled | indeterminate`, and it is **`null` whenever `terminal`
  is false**. (`cancelled` added by the 2026-08-03 erratum carried by ADR-0110 D6.
  Timing, recorded because D2 makes it matter: this text froze on 2026-07-25 with three
  values; the wire began emitting `cancelled` hours later that same day and has shipped
  it since. Envelope stamping itself began at 20:11 that day, so stamped v1 envelopes
  spoke three values for under three hours, four ever since. That unversioned expansion
  violated D2 when it happened; this correction documents the wire as it ships and moves
  no version, because the breaking event was the 2026-07-25 code change, not the text
  catching up to it. ADR-0107's Notes recorded the drift. Consumer notice and migration
  policy, normative: a consumer exhaustively matching three values has been exposed to an
  unlisted `cancelled` since 2026-07-25 22:54; add the `cancelled` branch, and until it is
  added treat an out-of-vocabulary `outcome` the way `indeterminate` is treated — result
  not establishable, never success and never failure.)
  Stated against `terminal` rather than against "the run is still going", because v1 has a
  state that is neither: an orphan has stopped and is still not terminal (D6), and a rule
  phrased around being in flight would leave that case undefined. Being closed, `outcome`
  may be branched on, and a new value costs a version increment. It is the producer's derivation, on the same principle as `terminal`:
  the party that owns the status vocabulary is the only party that can classify it without
  holding a copy.
- Both are needed because they are different questions and their answers diverge.
  `completed_empty` is `terminal: true, outcome: "failed"`. A consumer given only
  `terminal` must either invent the forbidden vocabulary or call every finished run a
  success, which is P2 recreated one level up (P2b).
- **`indeterminate` was reserved at freeze and is emitted today.** (2026-08-03: ADR-0107
  implemented the producer this paragraph deferred — its orphan reaper writes
  `indeterminate` with an attributable reason; the reservation strategy below worked as
  intended.) It is the value for a run that can be established to have ended but whose
  result cannot be established, and the component that produces it is the reconciler D6
  deferred and ADR-0107 then implemented. It was defined before any path emitted it, and
  deliberately: `outcome` is a closed vocabulary that consumers branch on, so
  adding a third value later would be a breaking change under D2 and would cost a contract
  version. Defining an unused value costs one sentence; introducing it in v2 costs every
  consumer an upgrade. A two-valued field would also force `failed` for the unknowable case,
  which asserts a fact from a failure to establish one — the defect D7's availability
  wrapper exists to prevent, smuggled back in through a field small enough to look like a
  boolean.
- `reason_code` is a short machine-readable qualifier for a terminal outcome, drawn
  from the same cross-kind lifecycle resolver ADR-0066 D6 uses. It is advisory: a
  consumer may surface it, and must not need it to decide `outcome`.
- `alive` reports process liveness at read time and is advisory; see D6.
- **Every status-bearing response carries all three of `status`, `terminal` and
  `outcome`** — status, wait entries, list rows, and the submit handle alike. A summary
  that carries `status` without the derivations hands the consumer the open vocabulary and
  nothing to classify it with, which is P2 again at whichever verb was left out. A response
  that genuinely cannot carry them must be documented as a handle that the consumer resolves
  through a status read before reporting any outcome.

**Why derivations rather than a closed status set.** This is P2 as a design rule. If
`terminal` and `outcome` did not exist, every consumer would need a set of terminal
statuses and a set of successful ones, both copies of ours, and a status added here
would silently become "not terminal" or "not a success" there. Publishing the
derivation instead of the inputs keeps the classification with the only party that
always has the current vocabulary.

### D5 — Submit returns a handle, and the spawn phase is recorded

```text
li agent --machine --detach [...]
li orchestrate flow --machine --detach [...]
li orchestrate fanout --machine --detach [...]
```

```json
{
  "ok": true,
  "contract_version": 1,
  "data": {
    "run_id": "20260725T175010-97f334",
    "pid": 41234,
    "status": "running",
    "terminal": false,
    "outcome": null,
    "log": "/abs/path/to/console.log"
  },
  "error": null
}
```

**Exact semantics.**

- Submit returns as soon as the child is spawned. It does not wait and reports nothing
  about the work itself.
- `run_id` is assigned before the child starts, so the id is known without polling.
- The child is fully detached: its own session and process group. This is required
  rather than incidental, because a consumer's daemon is expected to be replaced during
  a run's lifetime, and a job parented to it would die at every upgrade. Detachment
  happens in-process at spawn; there is no reliance on a `setsid` executable, which
  does not exist on every supported platform.
- **Input validation happens before anything is created on disk.** Argument-vector
  size, unreadable prompt files, and malformed parameters are rejected while the
  request still has no trace.
- **Spawn failure is a separate case and is not preventable.** The job record must be
  written before `Popen`, so the child's terminal hook always finds a record to mark;
  therefore a spawn that fails after that write leaves a record claiming `running` with
  no process that can ever terminalise it. The executable can be replaced with a
  non-executable file, the working directory can vanish, and descriptor limits can be
  exhausted, all after validation passed.
- **The spawn phase is recorded, never inferred.** The record carries `spawn_state`, and
  it rides writes that already have to happen, so no new failure mode is introduced:
  `"preparing"` in the pre-spawn write, `"started"` in the write that attaches the pid,
  `"failed"` when the producer catches the spawn error. On a spawn failure the producer
  also writes the terminal record — `terminal: true`, `outcome: "failed"`, a reason
  naming the spawn failure — and the envelope reports `ok: false`.
- **A reader must not infer spawn failure from a missing pid.** Between the pre-spawn
  write and the pid attachment, a perfectly healthy child is on disk with no pid, so a rule
  keyed on pid absence reports a run that is starting normally as terminally failed. That is
  worse than the ghost record it would be trying to remove, because a false terminal is one
  a consumer acts on. `spawn_state: "preparing"` is reported as exactly that: non-terminal,
  `outcome: null`, with no claim about the spawn's fate.
- **A `preparing` record that has gone stale is surfaced, not resolved.** Its age is
  reported, and it may be flagged as possibly orphaned, but staleness is advisory and never
  a terminal transition — a loaded machine and a dead spawn look identical from the record,
  and a bound chosen to tell them apart is a guess with a consumer belief riding on it.
  **In v1 it stays non-terminal, permanently**, and that is the contract rather than an
  omission from it; see D6.
- An earlier revision made both this and the orphan case in D6 **reader-derived**, on the
  reasoning that a fact a reader can compute should never depend on a write that can fail.
  That reasoning is right about *presentation* and wrong about *lifecycle*. A reader
  computing a lifecycle transition manufactures a durable fact out of a local observation,
  which is how one reader comes to disagree with another about the same unchanged record.
  The narrow form that survives: **a reader may derive a presentation field that is a
  deterministic function of durable authoritative facts; only the lifecycle authority may
  make a lifecycle transition.**
- Long instruction text is passed to the child in a file, written before the spawn, so
  editing it afterwards cannot change what an already-submitted run executes.

**Why submit carries no result.** Any result field on submit would be a second place
where run state lives, and it would be the one place a consumer could read without a
subsequent call — which is exactly the state P3 forbids it from holding.

### D6 — One lifecycle authority; liveness is advisory beside it

```text
li job status <run_id> --machine
li job output <run_id> --machine [--tail-chars N]
li job list --machine [--limit N] [--status S]
li job kill <run_id> --machine
```

**Exact semantics.**

- **Durable lifecycle state is authoritative.** Every terminal outcome comes from the
  persisted record, so a consumer restart between submit and read changes nothing.
  This is the operational form of P3 and is what lets the consumer be stateless.
- **Process liveness is a separate, advisory observation**, reported as `alive`. It is
  not part of the durable answer and must never override a recorded terminal outcome or
  be used to establish success. Liveness is derived by probing a pid, and a pid can be
  reused, denied, or already reaped, so two readers of the same record can legitimately
  observe different liveness. The earlier framing of reads as answerable from "the
  store alone" was an overclaim: the store alone settles the lifecycle, and liveness is
  extra information carried beside it with its own reliability.
- A run whose recorded pid is gone with no terminal recorded is an **orphan**. It is
  reported as `status: "exited"`, `terminal: false`, `outcome: null`, with `alive: false`
  and an advisory flag that it may be orphaned. As frozen, it never became terminal;
  since ADR-0107, a `started` orphan whose process is CONCLUSIVELY established gone —
  process-incarnation evidence under a fenced claim, never the advisory pid probe the
  next bullet warns about — is reaped to an attributable terminal
  (`outcome: indeterminate`) where the fenced write publishes; a refused write leaves
  it non-terminal until a later observation retries, and a `preparing` record or an
  inconclusive finding stays non-terminal exactly as before.
- **Liveness may not be the fact that establishes terminality**, and an earlier revision
  had it both ways: this decision declares the pid probe advisory because a pid can be
  reused or denied, and the next paragraph then derived a terminal outcome from that same
  probe. A bare `kill(pid, 0)` identifies neither the process incarnation nor the run that
  owns it, so a reused pid makes a dead run look alive, and the same unchanged record reads
  differently from two hosts. A lifecycle transition resting on that is a transition that
  depends on who asked.
- **In v1 as frozen, nothing terminalised an orphan.** (2026-08-03: ADR-0107 has since
  implemented the guarded reaper — a `started` run whose process is conclusively gone now
  receives an attributable terminal with `outcome: indeterminate` where the fenced reap
  publishes; a `preparing` record stays non-terminal exactly as described below.) The paragraph that follows records the
  decision as frozen. This is a
  decision, not a gap, and it is stated here so that a consumer can plan for it rather than
  discover it. A run whose process died without its terminal hook running, and a run whose
  producer died before it ever spawned, both remain `terminal: false` for as long as their
  records exist. As frozen, a consumer's bounded wait kept returning them as pending until
  its own window closed (today D10's amended contract returns the stopped cases in
  `stopped_without_end` and an aged `preparing` record in `unresolved_spawn`, at once
  rather than at window close); what the consumer does then is its own policy, and this
  contract does not pretend to make that decision for it.
- **Why not simply specify the reconciler here.** Two earlier revisions tried, in opposite
  directions, and both failed for the same underlying reason: terminalising a run you did
  not run requires evidence that a run *ended*, and neither a missing pid nor an
  unresponsive one is that evidence. Doing it properly needs a durable process-incarnation
  identity, a host boundary, and a fenced ownership protocol so two reconcilers cannot race.
  That is a distributed-systems protocol with its own failure modes, not a paragraph in a
  result contract. Naming an owner without specifying when its transition is *valid* moves
  the ambiguity from readers into an unspecified component and lets two conforming
  implementations disagree about whether the same run is over — which is the divergence P1
  describes, one level further in. The target shape is written out in the deferred section
  below.
- **The cost is real and is the right one to accept.** A visible stall is worse than
  nothing and better than every reader independently inventing a terminal state from an
  observation it cannot trust. A false pending wastes a consumer's time; a false terminal
  gets acted on.
- `kill` signals the run's whole process group. It reports what it did rather than
  raising, because "already exited" is an ordinary answer to a kill request. It reports
  **signal delivery**, not process death and not lifecycle terminality; a consumer that
  needs the run to be terminal observes it with D10 afterwards.
- `list` is ordered newest first, bounded by `limit`, and says so when truncated.

### D7 — Nothing read from disk is reported as a bare null or empty list

This is P6 made structural. Any field whose value is derived from a read that can fail
carries its own availability:

```json
{ "available": true,  "value": [],   "reason_code": null,        "detail": null }
{ "available": false, "value": null, "reason_code": "unreadable", "detail": "permission denied" }
```

**Exact semantics.**

- `available: true` with an empty value means, definitively, that there is none.
- `available: false` means lionagi could not establish the value; `reason_code` is a
  short machine-readable qualifier and `detail` is for a human.
- A consumer must not collapse the two, and specifically must not render
  `available: false` as "none" — absence of evidence presented as evidence of absence.
- **The wrapper applies to every read-derived field**, not only the obvious two. At v1
  that is: the artifacts listing, the console text, the run manifest, and the
  notification-delivery outcome. Each of these is currently collapsed to `None` or `[]`
  on a missing file, an unreadable file, and a malformed file alike, which makes a
  not-yet-created manifest and a corrupt one indistinguishable.
- It does **not** apply to fields that are intrinsically optional and whose absence is
  unambiguous, such as `label`. Wrapping those adds ceremony without removing an
  ambiguity.
- `console` additionally carries `truncated`, because a tail displayed as a whole log
  turns a partial record into an apparently complete one.

### D8 — A valid envelope is authoritative; the exit status answers at the transport level

The two signals answer different questions, and the precedence between them is stated
rather than left to the reader:

| Situation | Exit | Envelope | Consumer reads |
|-----------|------|----------|----------------|
| Request handled, succeeded or refused | 0 | present, `ok` true or false | the envelope |
| Environment cannot run the command | 78 | absent | environment fault; nothing executed |
| No envelope for any other reason | any | absent | transport fault |

**Exact semantics.**

- **A well-formed envelope whose `contract_version` the consumer accepts is
  authoritative, and the command exits 0 whenever one is emitted** — including for
  `ok: false`. A structured refusal is a successfully handled request; encoding the
  refusal a second time in the exit status creates two answers to one question, which
  is the ambiguity D3 exists to remove.
- **78 (`EX_CONFIG`)** is returned when a required module cannot be imported and no run
  has been allocated — that is, when nothing was executed. It is never returned once a
  run exists, because a run that started and then failed is a failed run, and reporting
  it as an environment fault sends the consumer away from durable state it should read.
  No envelope accompanies it, because the failure happens before the code that would
  emit one.
- **Any other exit without a parseable envelope is a transport fault**: the process
  could not be launched at all, it was killed by a signal, it timed out under the
  consumer's own bound, or stdout did not parse. These are distinct from both of the
  above and the consumer must not attribute them to the submitted work. In particular a
  launch failure cannot return 78, because no child existed to return anything.
- A consumer that sees a valid envelope ignores the exit status. A consumer that sees
  no valid envelope uses the exit status, and only then.

### D9 — A notification is a prompt to read state, never proof and never the only path

On reaching a terminal status, a run records that status and, when a delivery command
is configured, sends a terminal notice.

**Exact semantics.**

- **The notice carries no authority.** It says "go read the state", nothing more. A
  consumer that treats receipt as proof of a terminal outcome, or of a successful one,
  is non-conforming. This matches ADR-0066 D6, which already states that notify and
  bounded wait are complementary rather than competing.
- **A consumer must never rely on the notice as its only discovery path.** Delivery can
  fail, and the record of that failure lives in state a non-polling consumer is not
  reading, so nothing wakes it to learn that nothing will wake it (P8). A consumer
  restart across a successful delivery loses it identically. Reconciliation is D10's
  bounded wait, or a poll; the notice is an optimisation over that floor, not a
  replacement for it. That floor is bounded observation, and it is eventual resolution
  only for the case ADR-0107 closed: a conclusive `started` orphan whose fenced reap
  PUBLISHES durably is terminal, and the reap winner attempts the same configured
  delivery the dead child's hook would have sent. A reap that cannot publish — the lock
  or the write refused — leaves the record non-terminal and advisory-flagged, sends no
  notice, and is retried by the next observation. A `preparing` or inconclusive orphan
  sends no notice, because it
  never reaches a terminal status, and it does
  not resolve under polling either. A consumer that reads this decision as "the poll always
  gets there in the end" has the right fallback and the wrong stopping condition, which is
  what the consumer obligation on an unresolved bounded wait is for. D10 names such a run
  in the result rather than leaving it among the ids still pending, so the stopping
  condition is reachable without a timer; deciding what to do once it is reached is still
  the consumer's.
- The outcome of the delivery attempt is recorded and surfaced on `status` under D7's
  availability shape. `attempted: false` means no delivery was configured, which is a
  valid configuration and not a failure — and is a different fact from a delivery that
  was attempted and failed.
- Artifacts are written before the notice is sent, so a consumer woken by the notice
  finds the outputs already present.

### D10 — Bounded observation, shared with ADR-0066 D6, stated here in full

`wait` takes ids, a maximum wait, and a poll interval; both numbers are clamped to
documented bounds and the effective values are echoed back. `0` is a legal snapshot
request.

**Taken unchanged from ADR-0066 D6**, which decides them for the MCP surface:

- Every successful call returns one entry per requested id, in input order, carrying
  that id's kind, `status`, whether it is `terminal`, and its `reason_code`, plus
  `all_terminal`, `timed_out`, and the list of ids still pending. A bare boolean is
  rejected: mixed outcomes are the normal case, and collapsing them forces the
  follow-up poll the call existed to replace.
- **Expiry is not an error.** A timed-out wait means the observation window closed. It
  reports what was learned, so completed ids are not discarded and a retry is safe.
- Unknown or ambiguous ids are per-id errors inside the result and never prevent the
  other ids from being observed.
- Lifecycle state is the single authority for these answers, not the MCP job sidecar,
  because a universal verb cannot pick its source of truth based on who submitted the
  work; the same id would then answer differently depending on provenance.
- **That authority is the same one `status` reads**, and this is load-bearing rather than
  incidental. An earlier revision let `status` apply a reader rule that terminalised an
  orphan while `wait` resolved the same run through lifecycle state, which still said
  running. The consumer was told by one conforming call that the run had definitely ended
  and by another that it must keep waiting, for the same id, at the same moment. Two
  normative source-of-truth rules for one question is the defect; which of the two answers
  is nicer is not the point. Every status-bearing path in this contract resolves through one
  authority, and an orphan is terminal on all of them or on none.

**Three additions this ADR made when its text froze, which ADR-0066 then did not state.**
ADR-0066's amendments have since adopted all three (`outcome` on every entry and the
partition on 2026-07-27; the four-way partition, either-list floor, and fenced purity
exception on 2026-08-03), so they are shared rules now and this section carries the fuller
rationale. They were marked as
extensions rather than folded into the list above, because presenting a new decision as an
existing one tells every reader there is nothing left to reconcile, which is the most
effective way to prevent it being reconciled:

- **Observing does not touch the run — with one deliberate, fenced exception.** A wait
  that times out, is signalled, or whose caller disconnects leaves the durable run exactly
  as it was; cancelling an observation is not cancelling the work. The exception is the
  one ADR-0107 added: the first reader of a conclusively-gone `started` orphan reaps it
  where the fenced write publishes — an attributable terminal written under a fenced
  claim, before notification and wait aggregation — repairing a record no writer survived
  to finish, never mutating live work. A refused or unpublishable write leaves the run
  exactly as every other observation does; the retry belongs to the next reader. ADR-0066
  D6 as first written was silent on signal and disconnect, so an MCP
  implementer reading only that ADR could have let request cancellation propagate into the
  operation while an external consumer assumed it could not; its 2026-07-27 amendment
  states the purity rule and its 2026-08-03 amendment adds this same fenced exception.
- **Every entry carries `outcome`** as well as `terminal`, per D4. ADR-0066 D6's entry
  contract as first written listed kind, status, terminality and reason code, so a
  conforming implementation could have omitted the field this contract requires for
  reporting a result; the 2026-07-27 amendment added `outcome` and both documents now
  require it.
- **An id that waiting cannot resolve does not hold the window open, and the producer pays
  a floor for it.** A run whose process is gone with no end recorded has stopped, and both
  original writers of an end are past it. Where that finding is conclusive for a `started`
  run and the fenced reap publishes, the status read resolves it before aggregation
  (ADR-0107): it comes back terminal and never appears in this list. The list is keyed on
  what is known, not on a phase name: it holds every observed record that stopped, or
  cannot be shown to be progressing, and could not be resolved — a `started` case with an
  inconclusive finding, one whose conclusive transition could not be published (retried
  on the next observation), and a record written before the spawn phase existed that is
  not shown alive — phase absence alone is never stopped evidence, so a live pre-field
  record is classified running and stays in `pending`. The only records
  kept out are the ones the producer explicitly marked `preparing`: fresh, such a record
  stays in `pending`; aged past the producer's stated threshold, it is returned in its
  own fourth bucket, `unresolved_spawn`, with that threshold reported beside it (there is
  no process to prove absent, so it is a different fact than a stopped run). The stopped
  ids are returned in their own list, `stopped_without_end`, rather than in `pending`,
  and the call stops re-observing once every remaining id is either terminal or in one of
  those two lists. It is a
  separate list and not a per-id error, because observing them succeeded. For those entries
  nothing about the
  record changes: the entry stays non-terminal with a null outcome, and a run that does
  record an end afterwards is classified terminal by the next observation exactly as
  before. `all_terminal` stays false while any id is in the list, because a run that stopped
  without recording an end is not a completed one.

  **The window was doing two jobs, and the second one stays here.** Its duration bounded the
  observation and also rate-limited a caller that keeps re-asking about a run which never
  resolves. Dropping such ids from `pending` removes the second, and a consumer looping
  until `all_terminal` with no backoff of its own would turn a visible stall into a hot
  loop, which is worse than the stall it removes. So a call that would return without having
  waited at all, while at least one id is in `stopped_without_end` or `unresolved_spawn` —
  the floor keys on either list, since an aged `preparing` record resolves nothing by
  waiting either — first sleeps one poll
  interval — bounded by whatever is left of the window — and observes once more. The trigger
  is a property of the observation the call is about to return, not of any history, so it
  applies on a first observation as much as a later one.

  This is a floor on the call, not a charge added to it. A call that already waited on a
  running id has met it and pays nothing extra, and `max_wait=0` is untaxed by construction,
  having no window to spend, so it remains the documented snapshot. The cost no shape avoids
  is a caller joining an already-finished batch beside one stopped id: it pays at most one
  poll interval, once per call. That is accepted deliberately rather than engineered around.
  It is bounded and small, the hazard it replaces is unbounded spin at a shared boundary,
  and that asymmetry decides it without a measurement of how often either case occurs —
  which neither party to the decision had. The extra observation is not wasted either, since
  it is exactly the interval in which a slow end-writer finishes.

  **Why the producer and not the consumer.** Both were live options and the choice is not
  obvious. Pacing enforced here is enforced once for every client, including clients written
  before this rule and clients we do not control; a documented duty to back off is honoured
  only by the clients that read it. The decisive evidence is local: this ADR's own consumer
  obligation 8 was silently disarmed by a code change while its text sat unchanged. A
  boundary whose safety depends on N independent clients continuing to behave is not a
  boundary.

ADR-0066 D6 was amended on 2026-08-03 to state the same partition, the same either-list
floor, and the same fenced-reap exception, so the two documents agree; where the level of
detail differs, this ADR carries the fuller statement.

**The third was a definition of something ADR-0066 D6 as first written left open**, and it
is the clause whose history is worth keeping. D6 then stated the result as the entries plus
`all_terminal`, `timed_out`, and
the list of ids still pending. It named that key and nowhere said which ids qualify for it,
so it did not by itself decide where a run that stopped without an end belongs. A reading
was available on which it did: D6 names a per-id error channel for ids that could not be
observed, and naming one exclusion can be read as ruling out others. That reading is
recorded here because it was argued seriously, not because it was adopted. It was not
adopted — the error channel is for ids observation could not resolve, while a stopped id
was observed and classified, so naming that channel does not settle the pending rule by
exclusion. The honest conclusion was that D6 underdetermined this, and an underdetermined
clause is settled by amending it rather than by either document assuming its own reading.
ADR-0066's 2026-08-03 amendment therefore states the partition outright: `pending`,
`stopped_without_end`, `unresolved_spawn` and terminal are disjoint and exhaustive over
every observed id (2026-08-03: the fourth bucket — an aged `preparing` record — is
corrected into the partition here; the implementation and its tests already enforce the
four-way form). The amendment records a rule that is enforced rather than adding one
that is not.

**Why this is taken up rather than deferred.** An earlier draft deferred bounded wait on
the grounds that timeout, signal and disconnect had no v1 answer. That was wrong about
timeout, which ADR-0066 D6 decides outright, and deferring on that basis would have left two
accepted documents directing opposite implementations — the MCP surface exposing a bounded
wait while an external consumer polled — which is precisely the divergence P1 describes,
produced by our own paperwork rather than by anyone's mistake. It was also wrong in the
opposite direction once corrected: the first revision claimed ADR-0066 answered all three,
when it answers one. Both errors ran the same way, toward reading a document as settling
more than it says, in whichever direction removed the obstacle in front of the draft.

**DEFERRED: the orphan reconciler.** The component that would terminalise a run whose
process died without recording a terminal, or whose producer died before spawning. It is
deferred rather than dropped because v1's answer — those runs stay non-terminal forever —
is a real cost that should eventually be paid off. What it needs, so that whoever builds it
starts from the constraints rather than rediscovering them:

- A durable **process-incarnation identity** written at spawn, not a bare pid. A pid is
  reused, so "pid 41234 is gone" does not establish that *this run's* process is gone. Boot
  identity plus process start time is the usual portable-enough pair.
- A stated **host boundary**. The same record read from two machines must not yield two
  answers, so a reconciler has to know which runs it is entitled to judge.
- A **fenced ownership protocol** — a lease or compare-and-set — so two reconcilers cannot
  both decide, and a stalled one cannot wake up and overwrite a newer decision.
- An explicit **eligibility predicate per spawn phase**. `started` and `preparing` are
  different problems: a `preparing` record has no child at all, so there is no incarnation
  to prove absent, and it needs a durable producer-attempt identity to be resolvable in
  principle. A reconciler specified only for `started` cannot resolve `preparing`, and
  claiming otherwise was the defect that produced this deferral.
- A decision, when it is designed, about platforms where that identity cannot be
  obtained. The constraint to carry into it is that leaving the run pending beats
  improvising weaker evidence, since weaker evidence is what the first three attempts at
  this were made of.

Everything in this list is a constraint on a future design, not a requirement on a v1
implementation. Nothing here is normative: a v1 implementation has no reconciler, and the
list exists so that whoever builds one starts from the constraints rather than
rediscovering them. (2026-08-03: ADR-0107 built the `started`-phase reconciler within
exactly these constraints — incarnation identity, host boundary, fenced ownership,
per-phase eligibility; `preparing` remains unresolved, as this list predicted.) Adding it later is additive under D2 only because `outcome`'s
vocabulary and the terminal fields are defined now, which is why they are.

**DEFERRED: usage and cost accounting** — tokens, duration, and whatever else a metered
consumer needs to bill a run. Not v1, and recorded here at the external consumer's request
so that it is not designed ad hoc when it is needed. Its shape is already constrained: an
additive block under D7's availability semantics, so that a failure to read the meter is
reported as unavailable and never as a zero. A metering read that fails silently to zero is
the same defect as an unreadable artifacts directory reported as "no artifacts", with the
difference that this one shows up on an invoice.

**DEFERRED: a `--machine` stream mode emitting progress events as JSON lines.** Would
let a consumer show live progress rather than a spinner. Deferred because JSON Lines is
a different framing and lifecycle protocol, and it breaks D1's "exactly one JSON object
on stdout" invariant, which is the single property that makes the current shape
trivially parseable. It should not be weakened until a consumer genuinely needs it.

## Consumer obligations

A contract that only constrains the producer is half a contract. Everything below is
required of a conforming consumer, and each one exists because the corresponding
producer-side decision is defeated without it. They are collected here rather than left
scattered through the decisions, because a consumer author needs the list of what is
being asked of them in one place.

1. **Validate `contract_version` on every envelope before decoding `data`** (D2). A
   handshake governs registration only, and the binary at a pinned path is replaced
   during normal operation.
2. **Ignore unrecognised fields** (D2). Additive change is only free if the other side
   actually tolerates it.
3. **Never map `status` onto a local set** (D4). Record and display it verbatim; branch
   on `terminal` and `outcome`.
4. **Branch on `outcome` totally** (D4). All five cases — `null`, `succeeded`, `failed`,
   `cancelled`, `indeterminate` — need a defined behaviour (2026-08-03 erratum:
   `cancelled` joined the documented set, and ADR-0107's reaper now emits
   `indeterminate`, so neither formerly-theoretical branch is theoretical any longer).
   This is the obligation that made D4's reservation of `indeterminate` worth anything:
   the value was defined before any path emitted it, so its producer was added without a
   version increment — which is exactly what then happened. A branch written against the
   pre-reaper wire looks complete for as long as that wire is what it talks to. Test the
   branch against a hand-written envelope.
5. **Never render `available: false` as "none"** (D7). Absence of evidence is not
   evidence of absence, and the whole wrapper exists to stop those sharing an encoding.
6. **Check the exit status before parsing stdout, and do not parse it at all on 78**
   (D8). Attributing an environment fault to the submitted work is the misattribution
   this contract was written to remove.
7. **Never treat a terminal notice as proof, or as the only discovery path** (D9).
   Delivery can fail, and the record of that failure lives in state a non-polling
   consumer is not reading.
8. **Have a defined policy for a run a bounded wait does not resolve** (D10, D6). Three
   shapes reach you and all need the policy: an id still in `pending` when the window
   expires; an id in `stopped_without_end`, which comes back at once and comes back
   every time; and an id in `unresolved_spawn` — an aged `preparing` record with no
   process to prove absent, which likewise returns every time. This is the obligation created by v1's liveness choice, and it is the one
   most likely to be skipped, because most runs resolve and the case looks like an edge.
   It is not an edge: v1 as frozen terminalised no orphan, and although ADR-0107 now
   reaps the conclusive `started` cases whose fenced write publishes, a `preparing` or
   inconclusive orphan still never resolves on its own — so a consumer that runs long enough **will** meet a run that
   never resolves, and this contract does not supply the policy for it. Give up after N attempts, escalate to a human, mark it
   abandoned in your own store — any of those is conforming. Having no policy is not,
   because the failure mode is a consumer that waits forever on a run nobody will ever
   finish, or three integrators each inventing a different timeout behaviour, which is
   the divergence removed from the specification arriving back through the consumers.

   On pacing, this obligation describes rather than requires. A `stopped_without_end` or
   `unresolved_spawn` id
   does not consume the window, so the window is not backpressure for it — the producer
   spends a one-interval floor instead (D10), and the correctness of the boundary does not
   depend on you. A backoff of your own is still recommended, because a floor set by the
   poll interval is a floor and not a policy, and only you know how often re-asking about
   an unresolvable run is worth anything.

## Consequences

**Easier.** A second consumer can be written without reading lionagi's source, and
tested against a documented shape. Our own MCP server becomes a consumer of the same
contract, so a change that breaks external consumers breaks ours too, in our own test
suite, before release.

**Harder.** `--machine` is a second output mode for every command that has one, and a
stray `print` on stdout under it corrupts the result for every consumer while looking
fine to a human running the same command. This needs a per-command test asserting that
stdout parses as exactly one JSON object, not a convention.

**Harder, specifically.** D4's `outcome` and D7's wrapper mean the producer now owns
classifications it previously left to callers. That is the point — it is the only place
the current vocabulary reliably exists — but it means adding a run status is no longer
a local change: it must also be classified, on every status-bearing response.

**Given up.** The freedom to change what a machine call returns without thinking about
it. Additive change stays free under the narrowed rule in D2; anything else costs a
version increment and a coordinated update.

**Accepted in v1, and stated rather than discovered.** As frozen: a run whose process
died without recording a terminal, or whose producer died before spawning it, stayed
non-terminal for as long as its record existed. Since ADR-0107 the first case closes when
the evidence is conclusive and the fenced reap publishes — a `started` orphan is reaped
to an attributable terminal — while producer-death-before-spawn (`preparing`) and
inconclusive findings still stay non-terminal. A consumer's bounded wait reports the
inconclusive or unpublishable cases as stopped without an end every time, and
an aged `preparing` record in its own `unresolved_spawn` bucket, never as finished. Two earlier revisions tried to close this with a rule
every reader would apply, and both produced worse failures than the one they removed: a
healthy child reported as terminally failed, and two hosts disagreeing about one unchanged
record. Closing it properly needs a fenced reconciler with process-incarnation evidence,
which is a protocol with its own failure modes rather than a clause, so it is written out
in the deferred section instead of half-specified here — and was then built to exactly
that specification, for the `started` phase, by ADR-0107. Naming the run is not resolving it:
the caller learns at once that nobody will finish this run, and still has to decide what to
do about it, which is what consumer obligation 8 is for.

**New failure modes.** A consumer pinned to v1 against an implementation that dropped
v1 refuses to register rather than misbehaving, which operationally looks like a
surface disappearing and needs to be diagnosable — hence `min_supported_version`.
Per-envelope version checking (D2) means a mid-session upgrade degrades a live
consumer instead of corrupting its answers, which is louder and correct.

**What a contributor must now know.** That `lionagi/mcp/` is no longer where these
answers are decided. Adding a field to the MCP server's return value without adding it
here creates exactly the divergence this ADR exists to prevent, and the MCP server's
own tests will not catch it.

**Cost of reversal.** D3, D4 and D7 are cheap to extend and expensive to retract. D2 is
the escape hatch: a breaking change is expressible as a version increment rather than a
negotiation. D8 could be dropped without touching the envelope, at the cost of P7
returning. D10's rules are now stated in ADR-0066 D6 as well, so dropping them here would
not create disagreement; it would leave the verb surface carrying the rules with none of
the rationale, and reversing any of them is a coordinated amendment of both documents. The
partition clause had a window in which it was cheap to reverse — before ADR-0066's
2026-08-03 amendment an implementation could have put
stopped ids in `pending` and stayed conforming — and that window is closed: both documents
now state where every unresolved id belongs, and consumers may be written against it. The producer floor attached to it is cheaper to reverse than
to introduce, since removing a minimum call duration cannot break a caller that was
tolerating it.

## Alternatives considered

- **Make the MCP server's Python functions the contract.** It exists already and needs
  no new CLI surface. It lost because the external consumer is not written in Python:
  its contract would be a hand-written translation of our signatures, which is P1's
  drift with extra steps, and it makes our implementation its own specification, so
  there is no artifact to check either side against.

- **A long-lived lionagi daemon speaking a socket protocol.** Would avoid per-call
  process startup and allow push notification without a delivery command. It lost on
  P3: a daemon holding run state makes an answer depend on the daemon's uptime, and the
  consumer's whole value is being stateless. Subprocess-per-call also means a crashed
  call cannot corrupt another call's state. Retained as the obvious future direction if
  call latency ever dominates.

- **A closed status enumeration with a documented mapping.** Simplest for a consumer:
  branch directly on status. It lost to the recorded evidence in P2, where exactly this
  design silently converted timeouts into successes. The failure is not that someone
  wrote the mapping badly; it is that a copied vocabulary goes stale and its stale
  behaviour is a wrong answer rather than an error.

- **Publishing only `terminal`, leaving success to the consumer.** This was the
  previous draft's position, and it is wrong. It reads as the conservative choice —
  publish less, let the caller decide — but the caller cannot decide without the
  vocabulary we forbade it from holding, so in practice it either invents one or treats
  every terminal run as fine. `completed_empty` is the counter-example that settles it.

- **A boolean `succeeded` rather than the three-valued `outcome`.** Simpler, and it was the
  first revision's answer. It lost to the orphaned-run case in D6: a process that vanished
  with no terminal recorded definitely ended, and its result is unknowable. A boolean has
  only `false` for that, which asserts failure from an inability to observe. Notably the
  external consumer proposed the three-valued shape independently, from its own analytics
  requirements, while concluding it was not needed in v1 — two unrelated routes to the same
  field is a stronger argument than either one alone.

- **Letting readers derive the terminal state of a spawn failure and of an orphan.** This
  was the previous revision's answer, and it is the most instructive thing that has been
  rejected here. Its appeal was real: a corrective write can fail on exactly the disk that
  just refused the spawn, so a guarantee resting on that write is not a guarantee. But a
  reader computing a *lifecycle transition* invents a durable fact from a local observation,
  and both concrete rules proved it. Spawn-failure-from-missing-pid is racy against the
  required write ordering, so a healthy child is reported terminally failed in the window
  before its pid is attached. Orphan-from-pid-absence rests on a probe that cannot identify
  a process incarnation, so a reused pid hides a dead run and two hosts disagree about one
  unchanged record. What survives is the narrow form: readers derive presentation, the
  lifecycle authority derives lifecycle. The ghost record is answered by naming an owner for
  it, not by hoping every reader will independently reach the same conclusion.

- **Semantic versioning for the contract.** Would allow finer-grained compatibility
  statements. It lost because it invites partial-compatibility logic in code we do not
  control. One integer permits exactly one comparison and no interpretation.

- **A handshake-only version check.** Cheaper: one call at startup. It lost to P5 —
  the binary at the pinned path is replaced during operation, so a startup-only check
  is a claim about a file that no longer exists. Per-envelope checking costs an integer
  comparison against a field already present in every response.

- **Inferring machine mode from a non-TTY stdout.** Zero new flags, and it is what many
  tools do. It lost because it makes the output shape depend on how the process was
  invoked rather than on what the caller asked for: a consumer that works in production
  and differs under a test harness, a pipe, or a CI log is the class of bug hardest to
  reproduce. Machine mode is a request, so it is a flag.

## Notes

**Deliberately not decided here**, and left to the consumer or to a later revision, so
that a consumer author knows these are open rather than assuming a silent default:
per-verb timeout budgets and what happens to the child when a call times out; poll and
wait backoff, jitter and total deadline; record, log and artifact retention, and what a
run id means after collection; submit idempotency when a consumer crashes between spawn
and receiving the envelope; `run_id` validation and path-traversal protection; size and
truncation limits for lists, console text and artifacts; timestamp precision and
ordering guarantees.

The external consumer is expected to ship in-tree in its own repository first, because
that repository cannot load a component from outside its build today. That constraint
belongs to the consumer and does not affect anything here: the contract is the same
whether the consumer is linked in at build time or loaded later. Recorded because the
sequencing question was raised while deciding, and the answer — that it does not bear
on the contract — is worth having written down rather than re-derived.
