git:20261003.dc0d872 to git:20261028.e62486e

216 added, 173 removed. Audit A to A.

---
name: pi-web-ui-internal-api-orchestration
- description: "Drive real agent work through Pi Web UI's local Internal API from any directory or harness: run a task on several models and compare, spin up child or sub-agent sessions, split work across runtimes, score or classify batch items with one model, hand a job to another model, start something and check back later, get woken when a child finishes or a condition fires, answer a child agent's question, monitor or clean up running sessions, transfer context between sessions, adopt an existing session as a child, or automate Pi Web UI without the browser. Use it as a parent orchestrator or multi-agent orchestrator managing children, or when the user mentions the Internal API, Unix-socket orchestration, session fan-out, session adoption, runtime comparison, watch-wake, or dispatching by voice. Every child you dispatch must be told to follow the `orchestrated-child-worker` skill, and the waiting discipline is in `long-horizon-waiting-strategies`: use all three together. To prove a change to Pi Web UI itself works, use the Pi Web UI repository's live-validation docs instead."
+ description: "Drive real agent work through Pi Web UI's local Internal API from any directory, harness or voice assistant on the same host. Use to run a task on several models and compare, spin up child or sub-agent sessions, split work across runtimes, batch-score items, be woken when a child finishes, answer a child's question, or monitor and clean up sessions. Tell every child to follow `orchestrated-child-worker`; waiting: `long-horizon-waiting-strategies`. To prove a change to Pi Web UI itself works, use the repository's live-validation documentation."
---
# Pi Web UI Internal API Orchestration
Pi Web UI is a browser interface and backend sitting in front of several agent
runtimes. The part that matters here is that it exposes a **local-only Internal
API over a Unix socket**, which turns it into an orchestration control plane you
can drive from anywhere on the machine — a different repo, a terminal, a script,
a voice assistant — without being inside the web UI at all. Sessions you create
land in the same registry the browser uses, so the operator can open them later.
- ## Which job do you want?
+ ## When to use
- The two Pi Web UI jobs split on one question: **are you *using* Pi Web UI, or
+ Also: score or classify batch items on a model, hand a job to another model, start something and check back later, transfer context between sessions, adopt a session as a child, automate Pi Web UI without the browser, and manage children as a parent orchestrator (Unix-socket orchestration, session fan-out, runtime comparison, watch-wake).
+
+ ## Which skill do you want?
+
+ The two Pi Web UI skills split on one question: **are you *using* Pi Web UI, or
*testing* it?**
- | You are… | Approach | Default target |
+ | You are… | Skill | Default target |
|---|---|---|
| Dispatching real agent work — fan-out, comparison, a long task to check back on | **this skill** | the **production** socket |
- | Proving a change to Pi Web UI or a runtime actually works | the repository's live-validation docs | a **disposable** server, always |
+ | Proving a change to Pi Web UI or a runtime actually works | the repository's live-validation documentation | a **disposable** server, always |
That distinction decides the safety posture, so get it right before your first
call. Using production here is correct and expected — these sessions are *meant*
to be visible in the operator's Web UI. What is never acceptable is validating a
code change against production, or stopping, restarting, redeploying, or
- reconfiguring the Pi Web UI service on your own initiative — the one exception is a
- programme whose owner authorised a restart, run by the parent under the protocol in
- `references/orchestrator-governance.md`. If the task is "does my change work?",
- stop and read the live-validation docs.
+ reconfiguring the service that hosts your orchestration on your own initiative —
+ the one exception is a programme whose operator authorised a restart, run by the
+ parent under the protocol in `references/orchestrator-governance.md`. If the task
+ is "does my change work?", stop and read the live-validation docs.
## How to read this skill
This is a reference, not a tutorial — read only what the current job needs,
not everything. The usual path: **Connect → Golden rule → Choosing the route**
once, then straight to the numbered step you're on.
- This file holds the workflow and the rules that keep you out of trouble. The
- reference files hold the field-level detail, organised in three reading tiers:
- must-read on every run, must-read when the situation matches, and optional.
- **Tier 1 is not advisory** — skipping it before dispatching is the most common
- way an orchestration run goes wrong (stale model lists, wrong routes,
- exhausted provider quota). Tier 2 rows are mandatory *within* their stated
- situation and skippable outside it. Tier 3 is genuinely optional.
-
- **Tier 1 — Must read, every orchestration run (before you dispatch anything):**
-
- | Must-read | Why |
- |---|---|
- | `long-horizon-waiting-strategies` skill (separate skill) | roughly all Internal API orchestration is long-horizon: the pre-idle checklist, backstop selection, supervision and liveness rules. Read it before your first idle window |
- | `orchestrated-child-worker` skill (separate skill) — **you do not follow it, you mandate it** | every child prompt must carry the child directive in §3, for implementers, researchers and reviewers alike. Nothing injects it automatically |
- | *Golden rule* section (above), including the **provider quota check** | live capability/capacity/model discovery, plus provider headroom — `/capacity` and `/sessions/usage` are **not** provider quota, and subscription peak windows change the right route |
- | `references/routing.md` | **model routing principles — mandatory**: choosing an exact live selector, checking provider quota, reviewer families, and the rules that no longer hold |
-
- **Tier 2 — Must read when the situation matches:**
-
- | Reference | Mandatory when… | What it holds |
- |---|---|---|
- | `references/pi-orch.md` | **before your first `pi-orch` call in a run**, and before dispatching a programme through it | verbs and flags, goal budget, owner-id convention, `status --owner`, idle vs in-turn waiting, route caps, exit codes |
- | `references/adoption.md` | **before adopting any session as a child** — registered or native CLI; includes the operator's CLI-exit confirmation gate | discovery, payloads, responses, errors, post-adoption supervision wiring, worked example |
- | `references/claude-code-orchestrator.md` | **MANDATORY when orchestrating from Claude Code** (interactive `claude` CLI, `claude -p`, or a managed Claude session) | Internal API side for Claude parents: which-Claude wake route, dispatch-and-idle with a watch-wake mod, fan-in, `onFire` for managed Claude, Claude-parent routing notes. Wake mechanics, backstops and zero-token discipline: `long-horizon-waiting-strategies/references/claude-code-watch-wake-and-background-watcher.md` |
- | `references/antigravity-orchestrator.md` | **MANDATORY when orchestrating from Antigravity CLI / IDE** | Zero-token reactive waiting protocol, persistent wait-watch helper, no-in-turn-polling invariant, milestone notification standards, post-compaction checkpoint anchor |
- | `references/orchestrator-governance.md` | **when running several children** — competing priorities, production restarts, or defect handling | autonomous boundaries, defect design & dispatch, single-writer repository staging, restart protocol & recovery, check-in discipline, notifications |
- | `references/multi-phase.md` | **before the first child of a multi-wave programme** — dependent phases, shared repositories, concurrent writers, or another agent lineage owning overlapping scope | programme artefacts, wave derivation and dependency kinds, interim waves, design gates, the reviewer child, ownership contracts, correction cycles |
- | `references/programme-kit.md` | **with `multi-phase.md`, when you write the programme's files** | copyable templates (checkpoint, common and lane briefs, correction brief), parent verification checklist, worktree and shared-dependency rules, merge order, deploy and restart runbook, close-out |
- | `references/walk-away.md` | **before your first detached dispatch** | retention, the watch-wake payload, fan-in under parallelism, every condition type, notification edges, bare-CLI wake paths |
- | `references/goals.md` | **before arming a goal** on a child, or dispatching while your own session runs under a goal; also high-value for complex or longer child tasks | goal API, per-runtime honesty matrix, read-back discipline, parent-goal parking |
- | `references/evidence.md` | **before declaring a child complete** | the evidence ladder, receipt dispositions, transcript scope traps, the Claude `/events` caveat |
- | `references/sessions.md` | binding a model to a session, answering a child's question or permission request, or finding/identifying sessions | control payloads, model-binding verification, request/response shapes, identifier forms |
-
- **Tier 3 — Optional reads:**
+ This file holds the workflow and the rules that keep you out of trouble.
+ **Tier 1 — the only must-read: `references/parent-card.md`** (one page: the rules that
+ bit, copy-paste sequences per harness, on-wake triage, routing). Read it before you
+ dispatch anything and again after every compaction. Also read `references/routing.md`
+ before choosing a model, and mandate the `orchestrated-child-worker` skill in every
+ child prompt (§3; you do not follow it, you mandate it). Everything else is reference:
+ open a file only when the card or a numbered step points there.
| Reference | Open it when |
|---|---|
- | `references/patterns.md` | designing a fan-out or batch job, or reporting a run to the operator — background reference, not required |
+ | `references/pi-orch.md` | before your first `pi-orch` call (verbs, flags, exit codes) |
+ | `references/walk-away.md` | before your first detached dispatch (conditions, fan-in) |
+ | `references/claude-code-orchestrator.md` / `antigravity-orchestrator.md` | you orchestrate from that harness |
+ | `references/goals.md` | arming a goal, or dispatching under your own goal |
+ | `references/evidence.md` | before declaring a child complete |
+ | `references/sessions.md` | binding a model, answering a child's question, identifying sessions |
+ | `references/adoption.md` | adopting a session as a child |
+ | `references/orchestrator-governance.md` | several children, restarts, defects |
+ | `references/multi-phase.md`, `programme-kit.md` | a multi-wave programme (templates: `assets/programme-kit/`) |
+ | `references/patterns.md` | designing a fan-out or reporting to the operator |
Where things are in this file:
| If you are… | Read |
|---|---|
| Making your first calls | *Connect from anywhere*, *Golden rule: discover first* |
| Picking runtimes/models/roles for children | *Choosing the route* → `references/routing.md` |
| Planning a programme (dependent lanes, shared repos, other lineages) | *Planning a multi-phase programme* → `references/multi-phase.md` |
| Creating, preparing, dispatching; writing the child brief | §1–§3 (child directive and brief: §3) |
| Dispatching an independent reviewer child | §3c → `references/multi-phase.md` (*The reviewer child*) |
| Dispatching long work and walking away; fan-in of several children | §4 → `references/walk-away.md` |
| Arming goals, or pausing your own goal to supervise | §4b–§4c → `references/goals.md` |
| A child question or approval; finding a session by any id | §5, §7 → `references/sessions.md` |
| Proving a child actually finished | §6 → `references/evidence.md` |
| Adopting an existing session as a child | §8 → `references/adoption.md` (operator CLI-exit gate first) |
| Transfers, usage totals, cleanup | §9 |
- | Several children, restarts, defects, notifications | §10 → `references/orchestrator-governance.md` |
+ | Several children, production restarts, defects, notifications | §10 → `references/orchestrator-governance.md` |
- **Contract versions.** Feature notes below name the Internal API contract version
- that introduced them (for example 1.49.0 for the heap and lag refusals). The
- Pi Web UI repository's `docs/INTERNAL-API.md` and
- `docs/INTERNAL-API-CONTRACT.md` are the authoritative full reference — open them
- when a shape below doesn't answer your question. A deployment can declare a
- stability window in which the contract takes bug fixes only; `/capabilities`
- reports the served version, and a client such as `pi-orch` warns when its bundled
- snapshot is stale.
+ **Contract version.** Every feature note below names the Internal API contract version
+ that introduced it. An additive minor release keeps existing shapes; if `/capabilities`
+ reports a different minor than your client's snapshot expects, read the contract's
+ exception table first — `pi-orch` also warns `SNAPSHOT_STALE`.
+ Fields and endpoints here are the working subset; the pi-web-ui repo's
+ `docs/INTERNAL-API.md` and `docs/INTERNAL-API-CONTRACT.md` are the authoritative
+ full reference — open them when a shape below doesn't answer your question.
+
## Connect from anywhere
- **Preferred: the `pi-orch` client** (https://github.com/valtterimelkko/pi-orch —
- a thin parent client with no runtime dependencies beyond Node). It builds every
- request against a drift-guarded contract snapshot, sends `X-Parent-Session` for you
- when it knows your session id, and uses documented exit codes. Its `wait` blocks in
- the server's long poll and never polls. Programmes dispatch through it, not through
- hand-written curl scripts. **Read `references/pi-orch.md` before your first call**:
- verbs, goal budget, owner ids, waiting patterns, route caps, exit codes.
+ **Preferred: the `pi-orch` client** (public: https://github.com/valtterimelkko/pi-orch; `pi-orch` on
+ PATH is the bin of that repo). It builds every request against a drift-guarded contract
+ snapshot, sends `X-Parent-Session` for you when it knows your session id, and uses documented
+ exit codes. Its `wait` blocks in the server's long poll and never polls. Programmes dispatch
+ through it, not through hand-written curl scripts. **Read `references/pi-orch.md` before your
+ first call**: verbs, goal budget, retention ids, waiting patterns, route caps, exit codes.
```bash
SID=$(pi-orch spawn --runtime pi --cwd /work/dir --model-selector <live-selector> \
--thinking low --owner <you> --ttl 3600 --id-only) # + --goal-objective "…" --goal-budget-tokens N for a goal child
RID=$(pi-orch prompt "$SID" --message "…" --id-only) # detached, idempotency key built in
- pi-orch wait "$SID" --run-id "$RID" --deadline 1800 --json # in-turn wait only; an idle parent registers a watch and ends its turn (§4)
- pi-orch result "$RID" --json # final text + parsed completion block
+ pi-orch wait "$SID" --run-id "$RID" --deadline 1800 --json # in-turn wait only; an idle parent registers a watch and ends the turn (§4)
+ pi-orch result "$RID" --json # final text + parsed completion block
pi-orch verify "$SID" --run-id "$RID" --since <base> [--rerun "npm test"] --json # check the block's claims
pi-orch status --parent <you> --json; pi-orch cleanup "$SID" --lease <id> --owner <you>
```
- Rules worth carrying: a goal child that may run long needs `--goal-budget-tokens` (the
- 5,000,000 default pauses it mid-run) and a single-line objective; a Claude Code parent
- names its children `orch-<programme>-<parentShort8>-<lane>` because it has no lineage;
- exit `3` is a deadline (re-wait, never re-dispatch), `18` means reconcile with `status`
- before any re-spawn, `25` means the route cap refused the spawn (nothing was created);
- the completion block is a claim, so run `verify` before accepting it. Spread a large
- wave across routes rather than queueing it on one.
+ Rules worth carrying: a goal child that may run long needs `--goal-budget-tokens` (the 5,000,000
+ default pauses it mid-run) and a single-line objective; a Claude Code parent names its children
+ `orch-<programme>-<parentShort8>-<lane>` because it has no managed parent identity; exit `3` is a
+ deadline (re-wait, never re-dispatch), `18` means reconcile with `status` before any re-spawn,
+ `25` means the route cap refused the spawn (nothing was created); the completion block is a
+ claim, so run `verify` before accepting it. Per-route caps: spread a large wave across routes
+ rather than queueing it on one (one provider route stops completing above roughly five to eight
+ concurrent children).
**Fallback: raw requests.** Use them for anything the client does not cover
- (adoption, transfers, diagnostics, goal pause and resume), or when `pi-orch` is
+ (adoption, transfers, diagnostics), or when `pi-orch` is
unavailable. Nothing here assumes a working directory. The socket and token are
absolute paths, overridable by environment variable so the same snippet works in any
harness:
```bash
SOCKET="${PI_WEB_UI_SOCKET:-$HOME/.pi-web-ui/internal-api.sock}"
TOKEN_PATH="${PI_WEB_UI_TOKEN_PATH:-$HOME/.pi-web-ui/internal-api-token}"
TOKEN="$(cat "$TOKEN_PATH")"
API_BASE="${PI_WEB_UI_API_BASE:-http://localhost/api/v1}"
SELF_ID="${PI_WEB_UI_SESSION_ID:-${PI_SESSION_ID:-}}" # your own internal id, when you are a managed session
api() {
curl --silent --show-error --unix-socket "$SOCKET" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
${SELF_ID:+-H "X-Parent-Session: $SELF_ID"} \
"$@"
}
```
When you run inside a Pi Web UI-managed session, `PI_WEB_UI_SESSION_ID` (contract
1.47.0; Pi also keeps `PI_SESSION_ID`) is set automatically, and the runtime also
exposes `PI_WEB_UI_SESSION_ORIGIN` and `PI_WEB_UI_PARENT_SESSION_ID` (OpenCode and the
Claude channel backend get none). From a bare CLI there is no internal id: leave
`SELF_ID` empty and omit the header rather than inventing one. It is optional
identification, not auth.
**Parent linkage (contract 1.34.0):** the `X-Parent-Session` header links the
calls you make to your session, so linkage events (`child_dispatched`,
`child_turn_ended`, `watch_registered`, `watch_fired`) are published to your
session's event stream and are watchable like any other event. Linked
children persist `parentSessionId`; `GET /sessions/:id` gains an additive
- `children` list. Unresolvable values are ignored. A session that already
+ `children` list. Unresolvable values are ignored — and a native Claude Code
+ parent is not a Pi Web UI session, so its `X-Parent-Session` does not link.
+ From a native Claude parent, put `$CLAUDE_CODE_SESSION_ID` in the child
+ directive as the parent session ID (§3), and record the family in whatever
+ coordination surface you use, so peers can see which dispatch a child belongs
+ to. A session that already
exists — created earlier without linkage, or a native CLI session on disk —
can be linked under you after the fact instead of re-created; see §8 and
`references/adoption.md`.
Confirm Pi Web UI is actually present before planning anything:
```bash
ls -l "$SOCKET" "$TOKEN_PATH"
```
Auth is `Authorization: Bearer <token>` over the socket. This is a **trusted
same-host surface, not an RBAC boundary**: any token holder can inspect and
control every session. So be explicit with the operator about target, runtime,
model, thinking level, session count, retention, and likely side effects before
acting, and never delete, abort, transfer, or repurpose sessions your
orchestration does not own.
The repository also ships an experimental seven-tool stdio MCP adapter, but it is
**retained and disabled**. Do not try to route through it; use the socket.
## Golden rule: discover first, every time
Ask the server what exists right now. Machines differ, backends change, and
model lists are live.
```text
GET $API_BASE/capabilities
GET $API_BASE/capacity
GET $API_BASE/models
```
- **`/capabilities`** — verify `contract.name` is `pi-web-ui-internal-api` and
check `contract.majorVersion`. Capability-gate optional features rather than
assuming them. **The runtime feature flags are per-runtime, nested under
`runtimes.<runtime>`** (not top-level): read `supportsFollowUp`,
`followUpSemantics`, `supportsSteer`, `supportsSteerWhileBusy`,
`supportsThinkingLevel`, `supportsApprovals`,
`supportsInteractiveQuestions`, `supportsReplayHistory` from the entry of the
specific runtime you plan to use.
- **`/capacity`** — a preflight, not a reservation. Prompt admission is
rechecked at dispatch and can still return `429 ADMISSION_CAPACITY_EXHAUSTED`
for slot/runtime limits, or `503` for resource pressure, with `Retry-After`.
Resource refusals include `heap_pressure` and `event_loop_lag` (1.49.0; `/capacity`
shows `heap` and `eventLoopLag`), and `503 SERVER_DRAINING` while a deploy drains
(1.51.0). Treat any `429`/`503` with `Retry-After` as "wait and retry" (a drain ends
in a restart: re-check `/capabilities` afterwards), and never read the wire
contract's code list as closed.
An `event_loop_lag` refusal usually means the **host** is overloaded (another agent's
build or benchmark), not that your request is wrong: `pi-orch` waits out `Retry-After`
- for you; raw callers must too. Heavy work of your own belongs under `nice` and a capped
- scope, so it cannot starve the server's single event loop: on systemd,
- `systemd-run --scope -p CPUQuota=… -p MemoryMax=… -- nice -n 10 <cmd>` (put `nice`
- inside the scope; `-p Nice=10` is rejected by `systemd-run`).
+ for you; raw callers must too. Heavy work of your own belongs under `nice` and capped
+ CPU and memory (`systemd-run --scope -p CPUQuota=… -p MemoryMax=… -- nice -n 10 <cmd>`;
+ some `systemd-run` versions reject `-p Nice=10`, so put `nice` inside the scope), so it
+ cannot starve the server's single event loop.
Read actual tasks, reservations and the limiting reason—not just the nominal
turn ceiling. Admission permits are not a complete count of browser/native
work or autonomous goal continuations; separate request receipts can also
refer to one executing turn.
- **`/models`** — choose exactly one live-advertised entry. Read its `id`,
`provider`, `thinkingLevels`, and for Claude profiles `backend` and
`claudeModel`. A missing field is *unknown*, not permission to fill it in from
memory. Profile ids are opaque strings; since contract 1.25.0 both
`/models` profile entries and `/capabilities.claudeProfiles` carry the same
underlying alias in `claudeModel` / `model`. After creating a session with an
explicit model, trust only the create response's `resolvedModel` /
`modelBinding` fields (contract ≥ 1.25.0) or a follow-up `/info` read-back —
never assume the echo means the binding happened.
- **Watch mutation capability** — `features.watchGenerationPreconditions`, when
present with the documented field names, enables opaque-generation conditional
register/delete. Use observed tokens and exact ownership; absence means legacy
non-CAS semantics, not permission to claim atomic deletion. See `walk-away.md`.
- **Provider quota** — `/capacity` is Pi Web UI's local admission capacity and
`/sessions/usage` is our own recorded consumption; neither is the provider's
- remaining subscription quota. Check that before choosing a route: read the
+ remaining subscription quota. Check that before choosing a route: read each
provider's remaining quota in its own dashboard or CLI (a zero-model-token,
- read-only check), including per-provider windows, resets, shared pools, and any
- peak-window flag. This is the authoritative, zero-token pre-dispatch check.
+ read-only check), including per-provider windows, resets, shared pools and any
+ peak-window flag. That check is your authoritative pre-dispatch evidence.
**Read `enabled` as well as `available`.** Since contract 1.15.0 they mean
different things: `enabled: false` is an operator-disabled runtime, which is not
the same as uninstalled or unhealthy. Treat disabled as unavailable and never
silently substitute another runtime.
**There are five runtime families, not four**: `pi`, `claude`, `opencode`,
`antigravity`, and the feature-gated `commandcode`. Don't hard-code the old
four-runtime assumption.
- **Provider policy** — on contract 1.16.0+,
+ **Pi provider policy** — on contract 1.16.0+,
`capabilities.features.piProviderPolicy.blockedProviders` lists Pi providers
- barred from automation to prevent accidental metered spend. Operators can block
- exact provider ids via `INTERNAL_API_BLOCKED_PI_PROVIDERS`, and a non-empty
- deployment rejects blocked providers with `403 PROVIDER_NOT_ALLOWED`. Don't guess
- around an omitted model on deployments that do omit them. Being served is **not**
- being authorised: pay-as-you-go providers (metered API keys, gateway catalogues)
- need the operator's direct authorisation before any use — see
- `references/routing.md`. Subscription-backed providers are distinct from metered
- direct providers and are never covered by this policy.
+ barred from automation to prevent accidental metered spend. The shipped default is
+ **empty**: the direct `openai` provider and the full OpenRouter gateway catalogue are
+ both served through `/models` and selectable like any other provider (check
+ `blockedProviders` for the effective per-deployment list — operators can re-block
+ exact provider ids via `INTERNAL_API_BLOCKED_PI_PROVIDERS`, and a non-empty deployment
+ rejects blocked providers with `403 PROVIDER_NOT_ALLOWED`). Don't guess around an
+ omitted model on deployments that do omit them. Being served is **not** being
+ authorised: pay-as-you-go providers (direct Google API, OpenRouter, a direct
+ `anthropic` API key, a native DeepSeek API, plain `openai`) need the operator's
+ direct authorisation before any use — see `references/routing.md`. A
+ subscription-backed provider is distinct from a metered direct provider and is
+ never covered by this policy.
**If a model you expect is missing**, refresh the catalogue rather than
improvising: `POST $API_BASE/models/refresh` with `{"runtime":"pi"}` (refreshes
the OpenRouter catalogue and returns an ids diff) or the default/`"opencode"`
mode (warms the models.dev cache and recycles the idle backend). It returns
public ids and metadata only, never credentials.
Never use a remembered model list, a family-name match, a coarse `reasoning`
flag, a stale contract version, or a guessed fallback. If the exact combination
you need is absent, stop and report why.
---
+ # Choosing the child path
+
+ **Internal API first** for every child size: it keeps routing and quota control. Use the harness's own subagents (Agent tool) or a Claude Code cloud child **only when the Internal API is unavailable or the operator asks**.
+
+ | Path | Who pays | Wake | Coordination record |
+ |---|---|---|---|
+ | Internal API | subscription pools; metered only with operator leave | server watch | visible in Pi Web UI |
+ | Agent tool subagent | the harness's own plan | task result in your turn | none |
+ | Claude Code cloud child | separate cloud credits | none on a clean finish (see below) | none |
+
+ - Agent-tool `isolation:"worktree"` can lose its context (every Bash refused): create the worktree yourself and tell the child to `cd` into it, using absolute paths.
+ - Cloud children bill credits your provider dashboards do not show. A clean finish emits nothing watchable, so give each lane a branch, end every child turn with a pushed commit whose subject starts `QUESTION|BLOCKED|HANDBACK`, run a capped `git ls-remote` watcher as a background task in the dispatch turn (Bash timeout 7,200,000 ms; the 30 min default kills it) and arm a ≤ 60 min `wake_deadline` that checks `get_session`.
+ - If the Internal API persistently refuses admission (e.g. `event_loop_lag`), retry briefly, then use an Agent-tool child on a capable model as the fallback for the same bounded task — provided the operator has allowed that path.
+
+ ---
+
# Choosing the route
Treat a dispatch route as the complete tuple:
```text
runtime + selector/id + provider + profile/backend/Claude-model (when used)
+ advertised thinking or discovered native effort + invocation role
```
- Names are **not** selectors. Immediately before real dispatch, get one fresh
+ Names are **not selectors**. Immediately before real dispatch, get one fresh
`/models` response, filter the named runtime list, and select exactly one entry
satisfying every predicate; use its returned `selector` (since contract 1.26.0
every entry carries one — the exact string `POST /sessions` accepts, so copy it
rather than constructing `provider/id` yourself). Zero matches, multiple
matches, a missing required field, or an absent required level is a **refusal** —
never a reason to take the first similar entry or another backend. Keep the
matched metadata with the dispatch record.
- **→ `references/routing.md`** carries the general principles: choosing from live
- metadata, checking provider headroom before dispatch, using a different model
- family for reviewers, asserting the served model, and the hard exclusions
- (metered providers without authorisation, disabled or suspended backends). Read
- it before choosing a model.
+ **→ `references/routing-rationale.md`** explains *why* a route table is shaped as it is (why a list price is the wrong yardstick, what to check before changing a row); read it before changing any route.
+ **→ `references/routing.md`** carries the method: choosing from live metadata, checking
+ provider headroom, the three **work classes** (Class 1 routine children on cheap,
+ high-quota models; Class 2 more demanding work; Class 3 the most demanding), the
+ PAYG rule (metered providers only with the operator's direct authorisation), the
+ reviewer-from-a-different-family rule, per-route concurrency caps, hard exclusions
+ and route-discipline rules. **Read it before choosing a model** — the exclusions and
+ quota lanes are the kind of thing that looks harmless to guess at and is not.
+
---
# Planning a multi-phase programme
Most runs are one wave — dispatch, watch, fan in, done — and §1–§9 below cover
that. When the work is a **programme** (a sequence of dependent improvements
over multiple waves, often in shared repos alongside other active agents),
plan the organisation *before* the first dispatch. Parents fail at the
joins between waves, not inside them. Three rules carry the shape:
- **Write the programme into three durable artefacts** that survive parent
compaction and restarts: a stable **strategy document** (scope, waves,
- ownership, gates; owner-gated status header `PLANNING ONLY` → `READY` →
+ ownership, gates; operator-gated status header `PLANNING ONLY` → `READY` →
`EXECUTING`; amendments recorded in place; records scope, not completion),
a mutable **live checkpoint** re-read before acting (per-child status,
receipts, wake-delivery ids, next sequence, honest counters), and a
**per-child brief + evidence directory** (bounded outcome, owned paths,
handback format with a FROZEN marker).
- **Brief every child with a bounded, self-contained packet:** frozen success criteria,
bounded files, exact test commands and guardrails, rather than dumping repo context.
A child that already holds its context should not re-query or loop.
- **Give every concurrent child an isolated git worktree** with disjoint owned paths;
- the child announces its owned paths (and what it leaves to others) in its
- coordination directory (§3b).
+ the child states those paths (and what it leaves to others) in its hand-back, or in
+ whatever coordination surface your setup provides (§3b).
- **Derive waves from the sequence, not from available children**: a
parent-owned bootstrap wave first when the supervision machinery itself is
suspect; parallel children only on disjoint owned paths; later phases gated
on earlier interface stability or another lineage's work landing (postpone
the seam, don't block); a fresh read-only **reviewer child after each lane** (§3c),
- not only at the end, and a named **reviewer live re-run lane** before the wave closes;
- code/test/deployment reported separately. Declare
+ not only at the end, and a named **reviewer live re-run lane** before the wave closes; code/test/deployment reported separately. Declare
dependencies by what a step needs (interface, data, deploy order), and put a
**design gate** in the brief of any behaviour-critical lane.
- **Acceptance is a verdict, not a receipt**: the parent independently re-runs
verifiers and writes its own probes; every defect returns with RED evidence
as one bounded correction brief; accepted phases freeze.
**→ `references/multi-phase.md`** has the full pattern: the programme trigger
table, the three artefacts in detail, wave derivation, ownership contracts for
concurrent writers, correction cycles, and cross-wave supervision;
**`references/programme-kit.md`** has the templates and runbooks to copy. **Read it
before dispatching the first child of anything longer than one wave.**
---
# The orchestration loop
## 1. Create sessions
`POST /sessions` for one child, `POST /sessions/batch` for several. For anything
long-lived, ask for retention **atomically at creation** so a failure can't leave
an orphan:
```json
{
"runtime": "claude",
"model": "profile:<discovery-resolved-id>", // SDK profiles only, see below
"thinkingLevel": "max",
"cwd": "/approved/working/directory",
- "retention": { "mode": "durable", "ttlSeconds": 3600, "ownerId": "<unique-owner>" }
+ "retention": { "mode": "durable", "ttlSeconds": 3600, "ownerId": "<retention-id>" }
}
```
**Claude on the Internal API is SDK-only** (contract 1.46.0): `/models?runtime=claude`
lists only SDK profiles, and a create or prompt through any other Claude backend fails
`403 CLAUDE_BACKEND_NOT_ALLOWED` (`features.claudeBackendPolicy` in `/capabilities`; it
also covers watch wake, goal dispatch and batch). Take the exact `profile:<id>` from the
live `/models` entry and do not construct one.
Prefer a single create when atomic retention matters — batch creation does not
guarantee leases. Record at minimum the canonical `sessionId`, the exact route
tuple, the invocation role, your `ownerId`, the returned `retention.leaseId`, and
your own orchestration id. Since contract 1.34.0 the create response echoes
`parentSessionId` when you identified yourself via `X-Parent-Session`, and
`GET /sessions/:id` lists linked `children`. Since 1.54.0 every create also records
`parentSource` (`header` | `body` | `bash` | `peer`): without the header the server
resolves the calling session from socket peer credentials, fails closed (no link)
on any ambiguity, and costs a slower create, so still send `X-Parent-Session`
whenever you have an id. `GET /sessions?parent=<id>` lists a parent's children
(404 for an unknown parent). There is still no `orchestrationId`, so keep your own
tree record too.
**Create into a directory that exists** (1.53.0): every create checks the effective
`cwd` (exists, is a directory, writable) and refuses `400 PREFLIGHT_FAILED` with a
`failures[]` list before any model token is spent, so make the child's worktree
first. Add `"preflight": {"paths": [...], "tools": [...]}` (absolute paths, bare tool
names; also accepted on `POST /sessions/:id/prompt`) for the files and tools the
brief depends on: a bad workspace then fails in one cheap round trip instead of
43% of children discovering it mid-task. Batch create answers `200` with
per-entry `PREFLIGHT_FAILED`. Treat it as fix-then-resend, never blind retry.
**The create response's model echo is not proof of binding.** Verify the
effective, provider-qualified model via `/info` (and for Pi, the last
`model_change` entry on disk) before spending a long run — a silent rebind to a
3× costlier model has happened with a correct-looking echo.
**→ `references/sessions.md`** (Model binding) has the verification reads, the
Claude-profile rules, and the observed failure shape.
**Bindings are durable (contract 1.33.0) — the run receipt is authoritative.** After
dispatch read `servedModel` and `modelRebound` on the receipt; a binding that cannot be
applied fails loudly (`MODEL_NOT_APPLIED` / `PROVIDER_NOT_ALLOWED`). Never re-verify by
hand-scraping child session files during a run (`references/sessions.md`).
## 2. Prepare, if needed
Model, thinking-level and effort switches, and retention leases, go through
`POST /sessions/:id/control` (`set_model`, `set_thinking_level`, `set_effort`,
`acquire/renew/release_retention`). Thinking levels are capability-gated; for
Pi the response `level` is the *effective* read-back after SDK clamping, not an
echo. `set_effort` is the **Command Code** axis — idle-only, next-turn, never
changes `thinkingLevel` (models with no adjustable effort take no effort
request). For Claude, pick the backend/provider profile **at session creation**
rather than switching later; backend comparisons are cleaner as one session per
profile.
**→ `references/sessions.md`** (Control actions) has the exact action payloads.
## 3. Dispatch
`POST /sessions/:id/prompt`, `POST /sessions/batch/prompt`.
| Field | Notes |
|---|---|
| `message` | required |
| `verbosity` | `answers` (default, non-streaming, final text only), `tasks` (SSE status headlines), `full` (SSE everything) |
| `mode` | `prompt`, `follow_up`, or `steer` — see below |
| `detach` | fire-and-forget; **only valid with `verbosity=answers`**, otherwise `400` |
| `idempotencyKey` | session-scoped, 1–128 chars. A matching retry reuses the run; a different payload on a live key returns `IDEMPOTENCY_KEY_CONFLICT` |
| `requireActiveTurn` | for `follow_up`, forbid idle promotion |
**Modes** — this is how you redirect a child that's already working, and it's
easy to miss:
| Mode | Meaning | Pi | Claude | OpenCode / Antigravity |
|---|---|---|---|---|
- | `prompt` | start a new turn | `409 SESSION_BUSY` if busy. From 1.45.0: a swallowed prompt (extension input-hook swallow / fence) fails fast as `PROMPT_NOT_EXECUTED` after a 2 s grace; a session whose lease a **live foreign runtime** holds refuses up front as `409 SESSION_OWNED_BY_OTHER_RUNTIME` (`ownerPid`/`ownerMode`, no run created); a dead owner is auto-recovered (dispose→rehydrate, pins kept) before dispatch | `409 SESSION_BUSY` if running | `409 SESSION_BUSY` if running |
+ | `prompt` | start a new turn | `409 SESSION_BUSY` if busy. From 1.45.0: a swallowed prompt (extension input-hook swallow / fence) fails fast as `PROMPT_NOT_EXECUTED` after a 2 s grace; a session whose lease a **live foreign runtime** holds refuses up front as `409 SESSION_OWNED_BY_OTHER_RUNTIME` (`ownerPid`/`ownerMode`, no run created); a dead lease holder is auto-recovered (dispose→rehydrate, pins kept) before dispatch | `409 SESSION_BUSY` if running | `409 SESSION_BUSY` if running |
| `follow_up` | deliver after the current turn | busy with a live turn → queued (receipt `queued`); idle → **promoted** to a new turn with `dispatchMode:"prompt"`; busy with **no** live turn (auto-compaction) → `409 SESSION_BUSY` + `Retry-After` (1.57.0). Pi "busy" (1.57.0) = manager busy/streaming, or `sdkStreaming`, or compacting: the same predicate drives `prompt` refusals, `busy` on `GET /sessions/:id`, and watch settlement | no queue: running → `409 SESSION_BUSY` + `Retry-After`; idle → new turn | OpenCode: same as Claude. Antigravity (contract 1.37.0): busy → **queued** — the write goes into the live agy stdin stream and runs as the next turn |
| `steer` | join the active turn | requires an active turn; idle → `409 SESSION_NOT_STREAMING` | SDK backend only (`backendMode:"sdk"`, contract 1.29.0): delivered at the next tool boundary without interrupting; the receipt completes when the joined turn ends and the response carries the continuation text. Idle → `409 SESSION_NOT_STREAMING`; non-SDK backends → `UNSUPPORTED_OPERATION` | OpenCode → `UNSUPPORTED_OPERATION`; Antigravity → `409 SESSION_BUSY` (no mid-run join in the agy stdin protocol — use `follow_up`, which queues natively) |
Every successful response and receipt carries `dispatchMode`, the operation
*actually* performed, alongside the requested `mode`. `requireActiveTurn: true`
turns idle promotion into `409 SESSION_NOT_STREAMING` everywhere. Capability-gate
the differences with `followUpSemantics` and `supportsSteerWhileBusy` — steering
is a Pi capability and, since contract 1.29.0, a Claude SDK-backend capability
(`runtimes.claude.supportsSteer`).
A busy `steer` is not admission-checked (it joins an existing turn), so steering
a busy parent works even when every slot is full; new turns can still get `429`/`503`
with `Retry-After` — detail in `references/sessions.md` (*Dispatch modes and admission*).
Persist the returned `runId` before doing more work.
**For anything that may outlive your connection, use detached dispatch.** An
- attached owner `tasks`/`full` stream disconnect *cancels* its run; use
+ attached `tasks`/`full` stream disconnect *cancels* its run; use
`verbosity=answers` with `detach:true` for disconnect-safe execution.
- A separate `/events` observer is not the execution owner. Repaired SSE helpers
+ A separate `/events` observer does not own the execution. Repaired SSE helpers
close slow/oversized responses at their buffer limit, so streaming is not a
substitute for durable receipts:
```text
POST /sessions/:id/prompt {"message":"…","verbosity":"answers","detach":true} → 202 + runId
GET /runs/:runId
```
Large or slow fan-outs should prefer detached individual prompts, because
`batch/prompt` holds answer bodies until its response completes.
- **Mandatory child skill requirement.** The `orchestrated-child-worker` skill is
- **not** injected into API-created sessions by any harness; only your prompt delivers
- it. Every child prompt, for implementers, researchers and reviewers alike, must carry
- this directive. Without it a worker drifts into narrative, omits command exit
- statuses, ignores exclusions and self-signs-off:
+ **Mandatory child skill requirement.** The `orchestrated-child-worker` skill is **not injected
+ into API-created sessions by any harness; only your prompt delivers it. Every child prompt,
+ for implementers, researchers and reviewers alike, must carry this directive. Without it a
+ worker drifts into narrative, omits command exit statuses, ignores exclusions and
+ self-signs-off:
- > "You are dispatched as an orchestrated child worker. You MUST load and strictly follow the `orchestrated-child-worker` skill. Your session ID is `<sessionId>`. Work strictly inside execution worktree `<cwd>`. Owned paths: `<paths>`. Negative exclusions: `<paths>`. Hand-back directory: `<dir>`. Success criteria are frozen. Announce your task (a one-line note in the hand-back directory) before editing files, and leave a note when you finish. <Push / do not push.> Hand back by ending your turn only when your own commands have finished; never wait or poll."
+ > "You are dispatched as an orchestrated child worker. You MUST load and strictly follow the `orchestrated-child-worker` skill. Your session ID is `<sessionId>`. Your parent session ID is `<parentSessionId>`. Work strictly inside execution worktree `<cwd>`. Owned paths: `<paths>`. Negative exclusions: `<paths>`. Hand-back directory: `<dir>`. Success criteria are frozen. Announce your task once before editing files (a one-line note in the hand-back directory), and leave a closing note when you finish. <Push / do not push.> Hand back by ending your turn only when your own commands have finished; never wait or poll."
Put the directive **in the prompt itself**, not only in a brief file it points to, and restate
it inside a goal objective ("follow `orchestrated-child-worker`; mark the goal achieved and end") —
- goal-engine dispatches that dropped it produced most of the unmandated children in an
- audit of 241 child sessions. The rest of a good brief (each item traces to a
+ goal-engine dispatches that dropped it produced most of the unmandated children in a
+ September 2026 audit of 241 child sessions. The rest of a good brief (each item traces to a
recurring failure in that audit):
- **Its own worktree** for every concurrent child — never two children in one main checkout.
- **Push stance.** Say whether the child may push. Lane branches are often never pushed
(children commit only; the parent merges) — state the rule anyway.
- **The long commands and how to run them** (foreground with a timeout), and that a child
must not end its turn while its own job runs — or must start the message `NOT FINISHED:`.
- **The RED method**: test first; no `git stash` (its stack is shared across worktrees).
- **Restarts stay with you**: never delegate a restart of the service that hosts the child.
- - **Read-only roles**: say "no edits, no stash, no capture"; add "no lookups" only for a genuine fresh-reader or blind test.
+ - **Read-only roles**: say "no edits, no stash, no extra turns" (lookups are fine); add "no lookups" only for a genuine fresh-reader or blind test.
- **A budget for research children** (time or tool calls), a checkpoint cadence and a
hand-back milestone — unbounded research children ran for thousands of tool calls.
- **An end-of-task report block** (contract 1.58.0), so the server captures the child's
claims (§6). `pi-orch` appends it for you (for a goal child also the `Status:` marker and
field-shape instructions); paste it by hand only for raw dispatch, verbatim inside this
- four-backtick fence:
+ four-backtick fence (a cheap fast model followed it 16 times out of 16 in live trials):
````text
END-OF-TASK REPORT (required): the LAST thing in your final answer must be exactly this kind of fenced block (info string `completion`, JSON body):
```completion
{"schema":"pi-completion/v1","status":"done","summary":"<one line>","commands":[{"command":"<a command you ran>","exitCode":0}],"filesChanged":["<path>"]}
```
Fill in the real values; add "tests", "commits", "openIssues" or "blockedReason" fields only if they apply. Do not end your turn with only a tool call: after your final tool call, always write a short final answer that ends with the report block. Nothing after the closing fence.
````
-
- **Completion-block shape** (learned from a live proof where two blocks were rejected or mis-verified): `commands` entries are `{command, exitCode}` objects; `commits` entries are `{sha, repo, subject?}` (sha is 7–64 hex chars; `repo` is an absolute path); `filesChanged` entries are paths **relative to a claimed repo** — coordination or hand-back files outside the work tree are not part of the claim and should be omitted. A malformed block is still parsed to a typed `completionError` (`fieldPath` names the first bad field), so a rejected block costs one reporting follow-up, not a re-run.
- **Live proofs in the child's own disposable server** copy only the credential of
the approved route into the isolated agent directory, count `models.json` entries
- that carry `apiKey` as credential copies and delete every copy afterwards, name the
- child route explicitly in any inner brief, and assert the served model by script. A
- parent with a full credential copy can silently pick a different model for its children.
- - **Host rules the brief must carry** (binding wording in `programme-kit.md`, common
- brief): disposable servers started without the inherited production placement or
- tool-root variables; heavy commands in a `systemd-run --scope` with a hard
- `MemoryMax`; changed-file lint against the lane base; never touch the main checkout.
- The child-side form is `orchestrated-child-worker` §3 and §7.
+ that carry `apiKey` as credential copies and delete every copy afterwards, name the child
+ route explicitly in any inner brief, and assert the served model by script. A parent
+ holding a full credential copy can silently pick a different model for its children.
+ - **Host rules the brief must carry** (binding wording in `programme-kit.md`, common brief):
+ disposable servers started with the inherited production placement or tool-root
+ variables removed (`env -u <VAR> …`); heavy commands in a `systemd-run --scope` with a
+ hard `MemoryMax`; changed-file lint against the lane base; never touch the main
+ checkout. The child-side form is `orchestrated-child-worker` §3 and §7.
- Templates for all of this: `references/programme-kit.md`.
- For a read-only reviewer, replace the owned-paths, exclusions and push sentences with "You are a read-only reviewer: do not edit, commit, stash or push, and leave the tree untouched; numbered answer and correction files amend the brief", and add the verdict format (§3c), to be returned in the review file and the final message. The harness already delivers the identity packet and scope warnings to the child, so the brief need not restate them.
+ For a read-only reviewer, replace the owned-paths, exclusions and push sentences with "You are a read-only reviewer: do not edit, commit, stash or push, and leave the tree untouched; numbered answer and correction files amend the brief", and add the verdict format (§3c), to be returned in the review file and the final message. Your own prompt already carries the identity and scope context, so the brief need not restate it.
- ### 3b. Task announcement ownership: parent vs child
+ ### 3b. Task announcement: parent vs child
- **The child owns runtime presence; the parent defines boundaries and lineage.**
+ **If you run a coordination layer** (a shared task board, a sibling-agent registry, a
+ worktree claim file), the child announces what it touches and the parent defines the
+ boundaries and lineage:
- - **The child announces**, from inside its worktree, with owned paths and, optionally, what it leaves to others:
- a one-line note in the coordination directory (for example `00-declared.md`).
- Where the parent's environment provides a shared task surface, use it as well.
- On finishing or blocking the child adds a closing note before handing back.
- - **The parent:**
- 1. Defines the worktree, owned paths, exclusions and frozen criteria in the brief, mandating the child skill.
- 2. Passes the child its `sessionId` (from `POST /sessions`).
- 3. Records its own dispatch lineage for the child where its environment supports it
- (the `X-Parent-Session` header / `parentSessionId` is the API-level link).
- 4. Closes its record of the child if it crashed, timed out or aborted without a closing note.
+ - **The child announces** what it owns, once, before editing, and says what it leaves to
+ others; on finishing or blocking it leaves a closing note before handing back. If the
+ layer has a command for this, pass the child its `sessionId` from `POST /sessions` so the
+ announcement merges onto the existing linkage. Where the layer distinguishes an
+ "exclude" list, note that it is information for peers, not a keep-out zone.
+ - **The parent** defines the worktree, owned paths, exclusions and frozen criteria in the
+ brief; records the dispatch lineage itself; and closes the record of a child that
+ crashed, timed out or aborted without a closing note.
+ - **No coordination layer?** Then this section is one line of the brief: "say what you own
+ before you start, and tell me when you stop." Nothing else changes.
### 3c. Reviewer child: a fresh, independent read-only session
After an implementer hands back, and before you accept or merge, dispatch a **fresh**
session to review the change. Fresh context is the point: the implementer's history, and
yours, are polluted by the path taken, while a new session sees only the diff, the brief
and the evidence. Run one per lane and again after each correction; it is not only a last wave.
- - **Route (a role, not a model):** one of the recommended worker models, on a **different model family from the implementer**, at the highest thinking level it advertises, never a metered provider without direct authorisation, respecting disabled/suspended backends — rules in `references/routing.md` *Reviewer route*. Check provider quota first and confirm `fallbackApplied:false` on create.
- - **Create** with `cwd` = the implementer's worktree and durable retention (`ttlSeconds` sized for the wait, `ownerId:"<prog>-<lane>-reviewer"`; a Claude Code parent uses the `orch-<prog>-<parentShort8>-<lane>-reviewer` convention, `references/pi-orch.md`), dispatch detached, watch `agent_end` with `max_wakes` headroom (each auto-compaction also fires an `agent_end`).
+ - **Route (a role, not a model):** one of your recommended worker models, on a **different model family from the implementer**, at the highest thinking level its live route advertises, never a metered provider without direct authorisation, respecting disabled or suspended backends — rules in `references/routing.md` (*Use an independent reviewer*). Check provider headroom first and confirm `fallbackApplied:false` on create.
+ - **Create** with `cwd` = the implementer's worktree and durable retention (`ttlSeconds` sized for the wait, `ownerId:"orch-<prog>-<parentShort8>-<lane>-reviewer"`), dispatch detached, watch `agent_end` with `max_wakes` headroom (each auto-compaction also fires an `agent_end`).
- **Read-only is enforced by the brief only.** Check `git status` is clean afterwards.
- **The verdict is evidence, not sign-off.** You still re-run gates and adjudicate; a defect goes back to the same implementer as a bounded correction, and the same reviewer closes the previous findings.
+ **Proportionality.** A finding must be able to change the decision; if it cannot, verify, record the limitation and accept. Use a capability-strong reviewer for safety-critical code (cheap reviewers have missed a HIGH confirm-gate gap that a stronger reviewer caught). A quick "ACCEPT, no findings" from a cheap reviewer is a weak signal, not sign-off. Time-box reviews. Never shortcut production safety checks.
+
Cap: one full review, one closure round on the same reviewer, then a final parent-verified correction (no round 3). Brief contents, the verdict format, the closure loop and the compaction gotchas are in `references/multi-phase.md` (Review wave). For several parallel critiques of one draft, use `references/patterns.md` (Reviewer fan-out) instead.
## 4. Set a long task and walk away
Four pieces combine into "dispatch and check back later": **retention** keeps the
session alive, **detached dispatch** starts the turn and returns, **watch-wake**
prompts *you* when the child finishes, and **notifications** ping the human.
**Who are you? Decide this BEFORE any detached dispatch.** Only a *managed* session
(one Pi Web UI created) can be an `onFire` target. A **bare CLI process cannot be
prompted by the server**: a server-side `onFire` armed for it — by you, a script, curl
- or a repo dispatcher helper — sits undelivered while you idle. A server-side watch
- existing is not a wake being deliverable. Before ending your turn, name the exact tool
- call that delivers each wake into this session.
+ or a repo dispatcher helper — sits undelivered while you idle. Observed: a parent audited
+ its script-armed watch as active, asserted it would auto-resume, and slept until a human
+ noticed. **A server-side watch existing is not a wake being deliverable.** Before ending
+ your turn, name the exact tool call that delivers each wake into this session.
| You are | Wake delivery (plus one model-free backstop) | Playbook |
|---|---|---|
| Managed session, any runtime | server `onFire` on the child targeting your `$PI_WEB_UI_SESSION_ID`, plus a `deadline` condition | `references/walk-away.md` |
| Bare **Pi** CLI | `watch_wake_register` from *this* session + `wake_deadline`; if the tool is unavailable/broken, an owned pure-observer ledger plus a bounded in-harness relay whose **process exit** wakes you (record its actual delivery id; a raw server watch never counts as local delivery) | `long-horizon-waiting-strategies` |
| Bare **Claude Code** CLI | the `watch-wake` mod: `mcp__watch-wake__watch_wake_register` + `wake_deadline` (wakes a fully idle CLI); an exiting background watcher only as fallback | `references/claude-code-orchestrator.md` |
| **Antigravity** `agy` / IDE | `scripts/wait-watch.sh <watchId>` via `run_command` + native `schedule` backstop | `references/antigravity-orchestrator.md` |
| **Command Code** | its watch-wake mod (parks in `sleep`; cannot start an idle run) | `references/walk-away.md` (Bare-CLI parents) |
| `claude -p` / one-shot | cannot wait: hand back instead | — |
The rules worth carrying in your head:
- **Watch the child, wake the parent.** Register the watch on the child with an
`onFire` action targeting your own session, then return your turn. Never watch
yourself — an idle session emits no events.
- **Command-shaped work is not child work.** Long shell jobs (test suites,
- builds, watchers, validation scripts) run natively in a harness that provides
- background commands (a background-shell extension or the harness's
- `run_in_background` mechanism): launch, end your turn, and the completion
- wake carries the exit code and output tail — no child session, no watch, no
- tokens while waiting. That is **for you, the parent**: a *child* that ends its turn
- on its own background job wakes you with a false hand-back, which is why children run
- long commands in the foreground (`orchestrated-child-worker` §2). Reserve child dispatch and watch-wake for work that
+ builds, watchers, validation scripts) run natively where the harness provides a
+ background command (a background-shell extension, or the harness's own
+ `run_in_background` mechanism): launch, end your turn, and the completion wake
+ carries the exit code and output tail — no child session, no watch, no tokens while
+ waiting. That is **for you, the parent**: a *child* that ends its turn on its own
+ background job wakes you with a false hand-back, which is why children run long
+ commands in the foreground (`orchestrated-child-worker` §2). Reserve child dispatch and watch-wake for work that
needs another model context; cross-runtime waiting rules are unchanged.
- **Bare-CLI parent ⇒ local delivery is mandatory** (table above), then one
appropriate backstop per `long-horizon-waiting-strategies` **before** idling:
the loaded `wake_deadline` on a healthy host, an independent fallback when the
host/tool itself is suspect. Repo scripts that arm their own server-side
watches serve audit/durability — they do not count toward your wake.
- **`event_type: agent_end` is the default condition.** One wake per child turn,
and it also fires when the child stops early to ask, which is exactly when you
should look.
- **Parallel children are supported.** Wake budgets are per watch record, so N
children give N independent budgets with no cross-child suppression.
- **Two fan-in rules (contract 1.32.0+)**: one watch per session
- (re-registering explicitly returns `replaced:true` and starts a new ledger), and
- only one steer may be pending per target. Use `mode:'steer'` for a busy
+ (re-registering explicitly returns `replaced:true` and starts a new ledger),
+ and only one steer may be pending per target. Use `mode:'steer'` for a busy
Pi/Claude SDK parent and reconcile every child on any wake; additional
concurrent steers are recorded as `steer_pending` suppression. Transient
delivery failures do not consume budget and get one bounded retry.
- **Never poll in-turn.** Holding your turn open to watch a child is the failure
- this whole section exists to remove. That includes a foreground `pi-orch wait`: it is
- for scripts and in-turn waits; an idle parent registers a watch and ends its turn
+ this whole section exists to remove. That includes a foreground `pi-orch wait`: it is for
+ scripts and in-turn waits; an idle parent registers a watch and ends its turn
(`references/pi-orch.md`, *Waiting*).
- **`202` is queue acceptance, not delivery.** Never report "notified" from it.
**→ `references/walk-away.md`** has the full detail: retention modes and lease
ownership, the watch-wake payload and its cross-runtime matrix, the fan-in
best-practice list, bare-CLI wake paths, every condition type with what it
actually promises, and the notification edges. **Read it before your first
detached dispatch.**
## 4b. Goal-driven children (contract 1.27.0)
A **goal** is a durable objective that survives compaction on every supported runtime —
the strongest primitive for long-horizon children, and protection against ambient traffic
that *sounds* like a session ending (a plain-prompt probe child once stopped mid-task to
- obey an automated capture request). Arm one for work that must outlive a context window or
+ obey an automated end-of-session summary request). Arm one for work that must outlive a context window or
must not be derailed; use a plain prompt for bounded single-turn tasks. **Write the aim,
not the method**: what must be true, the evidence that settles it, where the record lives,
the invariants. Arm via `POST|GET /api/v1/sessions/:id/goal` or atomically at create; watch
terminal `goal_end` (filter a reused session's old-goal clear by the exact new objective),
not per-run `agent_end`. A `start` receipt is **not** completion. The objective must be a
**single line** (the API rejects newlines), and a goal that may run long needs an explicit
- token budget (`budgetTokens`; `pi-orch --goal-budget-tokens`): the 5,000,000 default pauses
- it mid-run.
+ token budget (`budgetTokens`; `pi-orch --goal-budget-tokens`): the
+ 5,000,000 default pauses it mid-run — measured programme lanes needed tens of millions.
**→ `references/goals.md`** — the API, per-runtime honesty matrix, read-back discipline.
**Read it before arming a goal.**
## 4c. Pausing YOUR OWN goal to supervise a child (Pi coding agent only)
Keep the parent goal running while independent work remains. Before idling: arm the watch
and backstop, `goal` **pause** (reason `"supervising <child>"`), end the turn. **Never** end
- with a needs-user-input marker for supervision (the wake would auto-resume the goal), and
+ with `Status: NEEDS_USER_INPUT` for supervision (the wake would auto-resume the goal), and
**resume only when fully settled** (no re-dispatch, questions or child watches outstanding).
Verify the goal engine version you rely on is actually loaded.
**→ `references/goals.md`** (Part 2) — full lifecycle and trigger table. **Read it before
dispatching a child under an active goal.**
## 5. When a child asks a question
A child that stops to ask is otherwise a dead end. Read pending requests with
`GET /sessions/:id/approvals/pending` and answer via
`POST /sessions/:id/approvals/:requestId/respond` — structured answers keyed by
the **exact question text**, `cancelled: true` to dismiss. Never fabricate an
answer on the operator's behalf, and never treat an accepted HTTP response as
resolved work.
**→ `references/sessions.md`** has the exact request/response shapes, error
codes, and the permission-request form. **Open it before answering a child.**
## 6. Monitor, and know what counts as evidence
| Need | Endpoint |
|---|---|
| Live progress | `GET /sessions/:id/events` (SSE) |
| Completion signal | `GET /sessions/:id/wait?status=idle&timeout=…` |
| Durable dispatch state | `GET /runs/:runId` |
| One bounded troubleshooting read | `GET /sessions/:id/evidence` |
| Scoped logs, no shell needed | `GET /sessions/:id/diagnostics`, `GET /diagnostics` |
`/events` is **supervision, not durable completion evidence**. For detached or
disconnect-prone work, the receipt is the truth.
- **A `/wait` timeout means "still running", not "failed".** Re-wait; never
restart, re-dispatch or alert on one.
- **Before reporting a blocker, rule out your own harness** — over-strict route
assertions, parsers that reject valid JSON wrapped in prose, schemas stricter
than the one you asked for. A false blocker costs the operator more than a slow
run.
- **A blank transcript projection is not proof there was no answer.** Compare the
full projection, receipt evidence, history and diagnostics first.
- **Terminal codes that are not child failures to retry as-is.**
- `NEVER_STARTED` (1.57.0): an accepted, dispatched run showed no runtime activity
inside the start window (default 120 s). The watch fires with `runNeverStarted: true`.
The dispatch never ran: re-dispatch once, then look at the session. Turns started
by an extension or the browser (goal continuations, watch-wake deadlines) have no
receipt, so this does not cover them; keep `goal_end` watches plus a backstop.
- `RUN_TRANSPORT_LOST` (1.57.0): a synchronous dispatch's receipt went terminal but
its response chain never returned. The outcome is in the receipt; read it and do
not re-dispatch blindly.
- `RUN_BUDGET_EXCEEDED`: the run hit a per-run cap. The caps are streamed tool
arguments (64 KiB per call, 256 KiB per run), 1,000,000 output tokens, and 4 MiB of
streamed output. The `run_budget_exceeded` / `tool_args_budget_exceeded` event
names which cap. Split the task, or ask for files instead of inline output.
- `interrupted` with `SERVER_RESTART`: a deploy cut the run off. The watch fires with
`interruptedByRestart: true`, and receipt-less goal or extension turns carry
`runId: "busy-<sessionId>"` (1.52.0). On contract ≥ 1.60.0 a **Pi goal child** cut off
by a RESTART interruption does not need you: the server continues it once at boot — a
`goal_state` with `status: "running"` + top-level `autoContinued: true`, progress,
never a `goal_end` — and only a stop it could NOT continue surfaces as `goal_state`
`paused` + `pausedReason: "interrupted"` with the cause — exactly one of:
`restart_interruption`/`rehydrate_pause` that could not be continued,
`second_transient`, `continue_failed`, `unsupported_runtime` (non-Pi goals, boot sweep
only). A provider-aborted goal is NOT continued: it ends exactly as before wave K (a
failed `goal_end`) and needs you as it always did. Resume a visible stop (`POST /goal
{"action":"resume"}`) or re-dispatch; every non-Pi goal still needs the old
resume-or-re-dispatch move. On older servers, resume or re-dispatch after the
restart, as before. Watch forms: `dataMatch {"autoContinued": true}` sees continues;
`dataMatch {"status":"paused","pausedReason":"interrupted"}` sees stops (both plus
your objective filter); `pi-orch wait` already registers the continue condition,
counts continues into `autoContinues` and settles a visible stop as exit 5 with the
cause and a note.
**Completion blocks (1.58.0).** A child that ends its final answer with a
`pi-completion/v1` block (see §3's brief list) gets it parsed server-side. The receipt
carries `completion` (or `completionError` with a typed code and field path) and
`completionDelimiter`. `GET /sessions/:id` carries `latestCompletion`, which also
covers goal children whose final turns have no receipt. The block is the child's
*claim*, not proof: run `pi-orch verify` (or check commits and files yourself) before
accepting it. A `contradicted` verdict is a correction item, not a retry.
- `verify --since <ref>` requires each claimed commit to be an **ancestor of `<ref>`**:
- pass the branch head you inspected (the ref that must contain the work), never the
- pre-work baseline — the check is inverted from the natural reading of "since".
**→ `references/evidence.md`** has the five-step evidence ladder, the
`receipt.outputEvidence` dispositions, transcript scope semantics (including the
`scope=screen` trap), what receipt terminality does and does not prove, and the
Claude channel `/events` caveat. **Read it before declaring anything complete.**
## 7. Find and identify sessions
```text
GET $API_BASE/sessions # list: sessionId, runtime, model, status, cwd, messageCount, lastActivity
GET $API_BASE/sessions/:id # detail
GET $API_BASE/sessions/:id/info # enriched runtime/session metadata
```
Listing is the natural first move when you arrive cold and need to know what is
already running. **The list returns the full persistent registry — including
long-dead sessions** — ordered newest-first by `lastActivity`; liveness comes
from `busy`/`lastActivity`, never from mere presence.
**→ `references/sessions.md`** has server-side filters (contract 1.30.0), the
native-CLI session scan, and the identifier-forms warning (internal id vs
`sessionPath` vs runtime-native id — resolve first, never guess).
## 8. Adopt existing sessions as children (contract 1.40.0)
Adoption links an already-existing session under you **after the fact**, with the same
linkage and fan-out as create-time children. A registered session: `POST /sessions/:id/adopt`
(or `control` action `adopt`). A native CLI session never registered: discover with
`GET /sessions/native`, then `POST /sessions/adopt-native` and use the returned internal
`sessionId`. Adoption is display-only metadata — it never moves transcripts or changes
ownership — and adopted children are full supervision targets.
> **Operator gate — never skip.** If the session was initiated or used in a CLI tool
> (`claude`, `cmdc`, `agy`, `opencode`, `pi`), **ask the operator and wait for explicit
> confirmation that they have exited that CLI** before adopting or dispatching. Your
> dispatches resume the same native artefact in the server's subprocess; a still-open CLI
- > is a second live owner (Claude refuses `--resume` with `Session ID already in use`, and
+ > is a second live writer (Claude refuses `--resume` with `Session ID already in use`, and
> concurrent writers diverge the transcript).
**→ `references/adoption.md`** — discovery recipes, payloads, responses, errors,
ownership/fencing, post-adoption wiring, worked example. **Read it before your first adoption.**
## 9. Transfer, aggregate, clean up
- `POST /sessions/:id/transfer` moves visible context into a target session you
own — cleaner than inventing your own handoff framing. It is a handoff, not
shared memory; track the target id and send a follow-up instruction after.
- `POST /sessions/usage` aggregates token/cost across the batch. Keep the
session/run mapping so cost stays attributable.
- After positive quiescence, `release_retention` **only** the exact lease you
own (payload key `retentionLeaseId`). Then `POST /sessions/:id/abort` if work
must stop, and `DELETE /sessions/:id` for disposable sessions you created.
- `DELETE` of a Pi session runs its extensions' `session_shutdown` (reason `quit`,
- bounded 5 s; contract 1.5x): the child's background processes (dev servers,
- watchers) and goal/watch timers end with it, and the call can take up to 5 s
- longer. Collect anything a child's background task produced before deleting it.
+ bounded 5 s; B5, 2026-09-29): the child's `bg_run` background processes (dev
+ servers, watchers) and goal/watch timers end with it, and the call can take up
+ to 5 s longer. Collect anything a child's background task produced before
+ deleting it.
- If release or deletion is unconfirmed, preserve that uncertainty in the
handoff. Never blanket-unpin, broad-kill processes, or delete an
operator-owned session.
## 10. Orchestrator governance
For the parent only (children follow `orchestrated-child-worker`). The full playbook, with the
decision boundaries, restart protocol and notification standards, is
**`references/orchestrator-governance.md`**; the rules to carry:
- **Decide and fix autonomously within scope.** Diagnose defects, design fixes, write
briefs and dispatch small-to-medium corrections without asking; keep the operator
informed at milestones. Escalate only what the governance file lists (destructive or
irreversible steps, external submissions, production restarts and deploys unless the
programme's authority already covers them, locked-decision pivots, quota exhaustion,
unadjudicable ambiguity).
- **One writer per working tree.** Concurrent children in one repository each get an
isolated git worktree with disjoint owned paths; where a worktree is impractical, queue
the second child until the first has committed. Cross-repository parallelism is safe.
- - **Sequence gating is yours, not the owner's.** When an earlier lane lands and the tree
+ - **Sequence gating is yours, not the operator's.** When an earlier lane lands and the tree
is clear, un-gate the queued lane at once; never ask "may I start phase X?".
- **Production restarts** of the service that hosts your orchestration: only with the
- owner's authority for the programme, only by the parent (never from inside a child it
+ operator's authority for the programme, only by the parent (never from inside a child it
hosts), and through the deployment's drain-then-restart procedure. Pi Web UI ships
`production:drain-restart` under a production lock; adapt it to your deployment.
- On contract ≥ 1.52.0 the drain waits for every busy session, goal continuations and
browser turns included.
- On contract ≥ 1.60.0 a Pi goal child the restart still cuts off continues once on its
own at boot; you resume only the stops it reports as `paused`/`interrupted`.
- On an older server, pause goal-armed children first: its drain cannot see their turns.
- Afterwards verify `/capabilities` and reconcile every child and watch
(`references/orchestrator-governance.md` §3).
- **On every wake, check every child**, not only the one named in the wake.
- - **Flatten child chains**: re-parent finished sub-workers under your own record.
- - **Mandate the child skill** in every dispatch (§3) and require child-side
+ - **Flatten child chains**: adopt or re-parent finished sub-workers under your own record.
+ - **Mandate `orchestrated-child-worker`** in every dispatch (§3) and require child-side
task announcement with owned paths.
- - **Notify at milestones** through your own notification path — a short milestone /
- question / blocked / done message, never a running commentary.
+ - **Verify every fix a child claims against the diff**, not its report. Children have
+ claimed fixes they never made and root causes that were wrong; a correction brief should
+ demand a file:line list per claimed fix and a test on the real code path, and the
+ parent checks both (details in the governance file §8).
+ - **Contain your own proofs.** Any memory or isolation experiment runs under its own hard
+ memory cap, inside a subtree you created and verified, with self-capped allocations: an
+ escaped proof once grew to tens of gigabytes and the host OOM killer took the service down.
+ - **Run the CI's gate list, not a subset**, before merging or deploying — including
+ changed-source ratchets against an explicit base (`npm run lint:ratchet -- --base <lane base or previous master>` in a
+ repo that has one; the default base sees nothing on a merge commit). Local `lint` passing while CI's ratchet fails is a real gap.
+ - **Notify at milestones** through your own notification path — a short milestone, question,
+ blocked or done message, never as a running commentary — and check that it was delivered.
# Current limitations
- Parent linkage and child surfacing exist from contract 1.34.0 (automatic
lineage and `?parent=` since 1.54.0), but there is no general orchestration job
registry. Keep the programme checkpoint and exact
session/run/watch/lease mapping; linkage is not execution or completion proof.
- Run receipts are dispatch-scoped; there is no general job queue or scheduler.
- `/approvals/pending` is backend-specific; runtimes without a synchronous
pending surface return an empty list.
- Command Code exposes only its attested shadow profile through the Internal
API. Browser-contained Command Code sessions are deliberately invisible to
Internal API session, diagnostic, notification, receipt, and transfer routes.
# Related
+ - **Pi Web UI live-validation documentation** — proving a Pi Web UI or runtime change
+ works, short-horizon or long-horizon, always on a disposable server. Use it instead of
+ this skill whenever the question is "does my change work?".
- **`long-horizon-waiting-strategies`** — going idle safely during long waits:
the pre-idle checklist, harness-specific wake and backstop mechanics (Pi, Claude
Code, Antigravity, Command Code), and the supervision and liveness rules.
- **`orchestrated-child-worker`** — the skill every child must follow; you mandate it (§3).
- **`secret-scanning`** — the credential-scanning companion for work that produces
artefacts, evidence bundles or repositories.
- - **pi-orch** — the thin parent client: https://github.com/valtterimelkko/pi-orch.
- - **Pi Web UI** — https://github.com/valtterimelkko/pi-web-ui. When the repo is
- checked out, the canonical docs are `docs/INTERNAL-API.md`,
- `docs/INTERNAL-API-ORCHESTRATION.md`, `docs/INTERNAL-API-CONTRACT.md`,
- `docs/LIVE-VALIDATION.md`, `docs/TROUBLESHOOTING.md`;
+ - In the Pi Web UI repo when present: `docs/INTERNAL-API-QUICKSTART.md`,
+ `docs/INTERNAL-API.md`, `docs/INTERNAL-API-ORCHESTRATION.md`,
+ `docs/INTERNAL-API-RECIPES.md`, `docs/INTERNAL-API-CONTRACT.md`,
+ `docs/NOTIFICATIONS.md`, `docs/SHARP-EDGES.md`, `docs/DURABILITY-MATRIX.md`,
+ `docs/TROUBLESHOOTING.md`, `docs/CLAUDE-BACKENDS.md`.
If the repo is not checked out, this skill plus live `/capabilities` is enough.
+
+ ## Part of the Pi Web UI orchestration set
+
+ - **Pi Web UI** — https://github.com/valtterimelkko/pi-web-ui (docs:
+ `docs/ORCHESTRATION-OVERVIEW.md`, `docs/INTERNAL-API-ORCHESTRATION.md`).
+ - **pi-orch** — https://github.com/valtterimelkko/pi-orch, the parent client whose bin is
+ `pi-orch` on `${PATH}`.
+ - **The pack** —
+ https://github.com/valtterimelkko/agent-workflow-skills/blob/main/packs/pi-web-ui-orchestration-pack/README.md.
+ - **The watch-wake companion** — `companions/watch-wake/` in this repository: the Pi
+ extension and the Claude Code mod that wake an idle parent, plus the model-free
+ wake deadlines.
+ - Sibling skills in this repository: [`../orchestrated-child-worker/`](../orchestrated-child-worker/),
+ [`../long-horizon-waiting-strategies/`](../long-horizon-waiting-strategies/),
+ [`../secret-scanning/`](../secret-scanning/).