diff --git a/docs/src/design/orchestrator/orchestrator-overview.md b/docs/src/design/orchestrator/orchestrator-overview.md
index 54f14166f..a005e2b05 100644
--- a/docs/src/design/orchestrator/orchestrator-overview.md
+++ b/docs/src/design/orchestrator/orchestrator-overview.md
@@ -1,4 +1,4 @@
-# Orchestrator State Machine
+# Orchestrator
The orchestrator is the eRoT's boot-sequence controller. It walks the platform
trust chain — verifying each component's firmware and releasing it from reset in
@@ -28,6 +28,10 @@ avoid the two drifting apart.
- [**Platform Architecture**](./orchestrator-platform.md): The platform half around
the core — surrounding services, capability contracts, the board device table,
and the fail-safe rules at the responsibility boundary.
+- [**Runtime**](./orchestrator-runtime.md): How the runtime loop
+ gathers hardware interrupts, IPC channel messages, and watchdog deadlines into
+ the core's event stream, and carries the resulting effects and decisions back
+ out.
## Design Principles
@@ -48,6 +52,22 @@ hiding them as implicit state changes. See the
The platform supplies the trust chain (component ids, kinds, and required/optional
policy) and the recovery-retry cap at startup.
+## Board composition
+
+*Board composition* (or *system composition*) is the per-target choice — described
+declaratively in a [`system.json5`](../../architecture.md) file, assembled by
+Pigweed at build time — of how the platform's functions are split across
+processes and which resources each process owns: hardware register blocks and the
+kernel [interrupt objects and IPC channels](../pw-kernel-ipc.md) built on top of
+them. It is separate from the
+board-supplied *policy* above: policy is *what* to verify (the trust chain);
+composition is *how* the surrounding services are wired. The orchestrator core
+and its platform-agnostic crates are the same across every composition — a driver
+may own a GPIO bank and forward boot-progress over a channel in one image, while
+the orchestrator holds the pins directly in another. That choice changes which
+inbound sources the [runtime](./orchestrator-runtime.md) sees and who owns each
+device, but never the core's states or the loop that serves them.
+
## Relationship to CSA Architecture
The state machine is a direct implementation of the boot sequence described in
diff --git a/docs/src/design/orchestrator/orchestrator-runtime.md b/docs/src/design/orchestrator/orchestrator-runtime.md
new file mode 100644
index 000000000..fbff52c24
--- /dev/null
+++ b/docs/src/design/orchestrator/orchestrator-runtime.md
@@ -0,0 +1,111 @@
+# Runtime
+
+The **runtime** is the orchestrator's event loop: the single task that sits
+between two collaborators — the pure [state machine](./orchestrator-machine.md)
+(the *core*) that decides, and the `PlatformDriver` that executes — turning
+outside happenings (interrupts, timeouts, IPC messages) into the `Event`s the
+core consumes, and handing the `Effect`s it emits to the driver. It holds no
+policy of its own — every decision stays in the core; the runtime only moves
+information across the process boundary.
+
+It gathers three possible inbound sources — **hardware interrupts**
+(boot-progress lines the orchestrator owns directly), **IPC channels**, and
+**watchdog deadlines** (timeouts) — into the one event stream the core
+supervises, and carries the core's decision back out. Which sources are present
+is a [board-composition](./orchestrator-overview.md#board-composition) choice:
+boot-progress is a hardware interrupt only when the orchestrator owns the pins;
+otherwise a monitor forwards it as an IPC message.
+
+The unifying idea is the kernel **wait group**: interrupts and IPC are not
+separate mechanisms but interchangeable *members* of one group, and a watchdog
+deadline is that same wait's *timeout* — so all three collapse into the return
+of one `object_wait(handle, signal_mask, deadline)`. A *signal* here is just a
+named bit on a waitable object, not a source in its own right: a latched IRQ bit
+on an interrupt object, or `READABLE` / `USER` on a channel (a service raises
+`Signals::USER` on a client channel to notify without a reply). The loop is
+written once against "a member that signaled, or the deadline that lapsed,"
+never against a specific source; what differs between sources is only the
+*decoder* that turns each into an `Event`.
+
+Two neighboring pages carry the supporting detail:
+[Platform Architecture](./orchestrator-platform.md) names the services and the
+responsibility boundary, and [pw_kernel IPC](../pw-kernel-ipc.md) gives the
+concrete channel syscalls. The worked, compiling reference is the QEMU
+integration test at `target/ast10x0/tests/orchestrator/runtime/main.rs`.
+
+## The single wait point
+
+The runtime is a single-threaded loop parked in one place: a kernel
+`object_wait` over a **wait group**. Every inbound source is registered once as a
+*member* of that group (`wait_group_add`), and the wait returns whichever member
+signaled. Each member resolves to at most one `Event`:
+
+- **Boot-progress signals** — a component reaching a checkpoint raises a
+ boot-progress signal. Depending on the board's composition it arrives as an
+ interrupt object the orchestrator holds directly or as a message a monitor
+ forwards over a channel; either way the loop maps it to
+ [`Event::ComponentReady`] (an `Active` component's iRoT-verified readiness)
+ or [`Event::Booted`] (a `Passive` component's liveness).
+- **Watchdog deadlines** — the timer is not a separate task. `BootWatchdogs`
+ (`services/orchestrator/server`) folds all armed boot windows and the commit
+ window into a *single deadline* passed straight to `object_wait`. When the
+ wait returns `DeadlineExceeded`, `poll_expired()` yields the mapped
+ [`Event::Timeout`] / [`Event::CommitTimeout`].
+- **IPC channel messages** — an update agent, management path, peer service, or
+ a boot-progress-forwarding monitor holds a channel *initiator*; the runtime
+ holds the *handler*. A readable channel is another object the loop waits on;
+ its message decodes to an `Event` — an [`Event::UpdateRequest`] to answer, or
+ a forwarded [`Event::Booted`] / [`Event::ComponentReady`] notification.
+
+The three share one `object_wait`, so a slow image hash on one path cannot
+delay a boot window on another — this is the "never block" rule of the
+[Platform Architecture](./orchestrator-platform.md#responsibility-scope) made
+concrete: the loop only ever blocks at the wait, and only until the *nearest*
+of any signal, any channel, or the nearest deadline.
+
+**Members are uniform; only the decode differs.** A wait-group member is an
+object watched for a signal bit — a latched IRQ bit on an *interrupt object*, or
+`READABLE` / `USER` on a *channel* — and the loop treats them identically: wait,
+see which member signaled, run that member's decode, dispatch the resulting
+`Event`. Nothing above the decode step knows which kind a member is. That
+uniformity pushes two questions *below* the runtime layer, where they belong:
+
+- **Who owns the underlying hardware** — does the orchestrator own the GPIO bank
+ and hold the boot-progress interrupt object itself, or does a monitor/GPIO
+ server own the pins and forward boot-progress over a channel? — is a
+ [board-composition](./orchestrator-overview.md#board-composition) choice made
+ in `system.json5`. Either shape is just one member of the group; the loop is
+ byte-for-byte the same.
+- **What a member means** is the per-member decode.
+
+```mermaid
+flowchart LR
+ subgraph SRC["Inbound sources (SIG/CH are wait-group members; TMR is the wait's timeout; boot-progress rides CH if a monitor owns the pins)"]
+ SIG["Boot-progress signal
(directly-owned interrupt object, latched)"]
+ TMR["Watchdog deadline
(BootWatchdogs → one Instant)"]
+ CH["IPC channel
(handler endpoint, READABLE)"]
+ end
+
+ WAIT["object_wait(signals, deadline)
the single park point"]
+
+ subgraph MAP["Inbound adapters (source → Event)"]
+ SMAP["signal → ComponentReady / Booted"]
+ TMAP["poll_expired → Timeout / CommitTimeout"]
+ CMAP["channel_read → decode → UpdateRequest / ..."]
+ end
+
+ CORE["Orchestrator::dispatch
(pure reducer)"]
+
+ OUT["Effects → PlatformDriver
+ response → channel_respond"]
+
+ SIG --> WAIT --> SMAP --> CORE
+ TMR --> WAIT --> TMAP --> CORE
+ CH --> WAIT --> CMAP --> CORE
+ CORE --> OUT
+```
+
+[`Event::ComponentReady`]: ./orchestrator-machine.md
+[`Event::Booted`]: ./orchestrator-machine.md
+[`Event::Timeout`]: ./orchestrator-machine.md
+[`Event::CommitTimeout`]: ./orchestrator-machine.md
+[`Event::UpdateRequest`]: ./orchestrator-machine.md