Integration guide for adapters
This is the walkthrough for a consumer of processkit-cli — an orchestrator or
adapter (in particular the processkit-py CLI) that launches runs through this
binary and reads its results back — rather than for a contributor to this
repository (see docs/architecture.md for that audience). It
ties together, in the order an adapter actually exercises them, the five
normative documents that each cover one part of the compatibility surface on
their own: docs/schema.md, docs/exit-codes.md,
docs/control-plane.md, and
docs/registry.md. This document does not restate their
normative text — every concrete claim below is a pointer to, and a minimal
worked example of, the contract those documents define; on any disagreement,
the linked document is the source of truth.
1. Fail-closed preflight: probe
Before launching anything through a candidate processkit-cli binary, verify
it is compatible. probe is side-effect-free — it spawns no child and touches
no registry or container — and prints one JSON report line to stdout:
processkit-cli probe --json \
--require-schema-version 1 \
--require-exit-code-band 100-119 \
--require-surface run:--jsonl \
--require-surface run:--capture-dir \
--require-surface inspect:--json \
--require-surface cancel:--run-id \
--require-surface kill:--run-id
The report (one line, shown reformatted here):
{
"probe_version": 1,
"binary": "processkit-cli",
"version": "0.2.2",
"schema_version": 1,
"exit_code_band": { "start": 100, "end": 119 },
"surface": ["cancel", "cancel:--run-id", "inspect", "..."],
"compatible": true,
"mismatches": []
}
- Pin
schema_version(--require-schema-version <N>) and the reserved exit-code band (--require-exit-code-band <start>-<end>) so a future breaking change is caught here, before a run, rather than by a JSONL parser or an exit-code table drifting silently out of sync. - Pin the exact CLI flags the adapter is about to use with one
--require-surface <token>per token (a bare subcommand name, or<subcommand>:--<long-flag>) — this is how an adapter confirms a flag it depends on (for examplerun:--capture-dir) actually exists on this build before passing it. - An unmet expectation makes
probeexitPROBE_INCOMPATIBLE(110) withcompatible: falseand the concretemismatches; a malformed--require-*argument (not an incompatibility, a bad flag) is the ordinaryUSAGE(100). A satisfied — or unrequested — surface exits0.
This is a fail-closed contract: an adapter that skips the preflight (or
silently proceeds after a PROBE_INCOMPATIBLE) re-introduces exactly the
uncontained-launch hazard this project exists to prevent. See
src/probe.rs and the normative exit-code table in
docs/exit-codes.md.
The report's shape is published as a JSON Schema with a golden fixture —
fixtures/schema/cli/probe.schema.json and probe.jsonl — so an adapter can
validate what it parsed instead of re-deriving the shape by hand. Every
machine-readable output in this guide has such a pair; see
fixtures/schema/cli/README.md
for the full table, and docs/compatibility.md, "Machine-output schemas", for
why these outputs carry no version field of their own (the probe report's
probe_version and the inspect snapshot's snapshot_version are the two that
do).
probe --print-schema is a separate, simpler mode on the same subcommand: it
prints this binary's embedded JSONL event-schema document instead of the
report above and exits 0, so an adapter that only needs the schema for its
own version — no clone, no tag to match — can fetch it offline without a
compatibility check. It cannot be combined with any --require-* flag:
that combination is rejected as an ordinary USAGE (100) parse error, never a
silent skip of the requested checks, so it can never produce a false "ok" on
an invocation that also asked probe to verify expectations. See
docs/schema.md, "Getting the schema without a git checkout".
2. Launching a run
The recommended invocation for an adapter:
processkit-cli run \
--run-id build-42 \
--jsonl .processkit/build-42.jsonl \
--capture-dir .processkit/build-42/capture \
--env-clear \
--env PATH="$PATH" \
--env-remove CI_SECRET_TOKEN \
--timeout 10m \
--grace 5s \
-- dotnet build
--jsonl <file>is the only place lifecycle events are written — never stdout, so the child's own stdout/stderr stay pristine. Give every run a distinct path; the file is created or truncated at the start of the run.--run-id <id>is the identifierinspect/cancel/killlater match on — supply one you control (rather than the generated default) so the supervision step (§4) has a stable handle. Two live runs sharing one--run-idis legal but makes every supervision command against it fail closed as ambiguous (§4, §6) — keep run ids unique across an adapter's own concurrently-live runs.--capture-dir <dir>additionally tees stdout/stderr to<dir>/stdout.log/<dir>/stderr.logwith a byte count, a SHA-256, and explicit truncation/write-error flags per stream (theoutput_capturedevent, §3) — use this when the adapter needs the transcript as a file rather than (or in addition to) the live echo.--no-echosuppresses the runner's own live retransmission of the child's stdout/stderr — the exact "pure noise" an adapter reading results from--jsonl/--capture-diralone does not want interleaved with its own output. The pipe,--capture-dir, and the JSONL stream are all unaffected; it conflicts with--inherit-stdio, which runs no pump to suppress in the first place.--detachreturns as soon as the run has provably started instead of blocking for its whole duration — the "launch and let go" shape, for an adapter that supervises out of band (§4) rather than by staying the runner's parent. It re-spawns the CLI detached (a new session on Unix, aDETACHED_PROCESSon Windows) and waits only until that copy has registered the run and writtenrun_startedto--jsonl, so on return the run is already visible tolist/inspect/wait. An adapter that captures the launch command's output (subprocess.run(..., capture_output=True)) gets end-of-file when the call returns, not when the run ends: the detached runner keeps none of the caller's pipes open. The exit code changes meaning under this flag and only under it: it reports the start —0once the run started, or the same reserved code the failure would have produced in the foreground (a missing program is stillSPAWN101) — never the child's own code, which stays in the terminalrunner_exitevent (§3). Adapters that need the child's result must read it there, or viawaitplus the event stream. It conflicts with--inherit-stdio/--inherit-stdin(nothing interactive survives detaching) and implies--no-echo's discarding sinks, while--jsonl,--capture-dir,--idle-timeout, and--snapshot-intervalbehave exactly as they do in the foreground. The detached runner's own stderr isnull, so--jsonlis the only channel that reports anything — including a failed member read, which is why that failure is a flagged event rather than a warning (§3). On Windows, pair it with--create-no-windowfor a console child: the detached runner has no console to lend it, so the OS gives the child one of its own. Seedocs/exit-codes.md, "Detached runs".--env-clear/--env-remove <KEY>/--env <KEY=VALUE>give the adapter control over the child's environment, applied in that fixed order — clear, then remove, then set — regardless of flag order on the command line, so an explicit--envalways wins on a duplicated key. SeeREADME.md, "Environment", for the full precedence rule.--max-memory <size>/--max-processes <n>/--cpu-quota <cores>cap the run's whole process tree. Enforcement needs a real container (Windows Job Object or Linux cgroup v2 at the real hierarchy root); where the platform or environment can't apply a cap the run fails fast with alimit_hitevent (§3) andBACKEND(102) rather than running silently unbounded — so an adapter that depends on a cap must treat alimit_hitas a hard failure, not a warning. SeeREADME.md, "Resource limits", for the platform matrix (macOS/BSD and the Linux process-group fallback are unsupported; cgroup v2 is often unenforceable under systemd/containers/typical CI) and the Linux--max-processescaveat.- Command-line redaction.
run_started'scommandfield is redacted by default: the raw argv is not recorded, only a one-way SHA-256 fingerprint (argv_sha256) and a classified worker-shapehint(both derived from argv but unable to reveal it) — filled on every run whether or not--argv-rawis given. Pass--argv-rawonly when the adapter's own storage for the resulting JSONL is at least as trusted as the command line itself; do not default to it. Seedocs/schema.md("Command redaction") for the exact fingerprint encoding, which an adapter reproducing the digest independently must match byte for byte.
run is not shell-free by accident — everything after -- is the literal
<program> <args...>, with no shell to expand or reinterpret it; an adapter
that needs shell features passes the shell as the program explicitly.
3. Reading the JSONL stream
--jsonl accumulates one JSON object per line as the run proceeds; parse it as
newline-delimited JSON, dispatching on each object's event field. A minimal
reader:
import json
with open(jsonl_path, encoding="utf-8") as f:
for line in f:
evt = json.loads(line)
if evt["schema_version"] != 1:
raise IncompatibleSchema(evt["schema_version"])
handle(evt["event"], evt)
Pin schema_version here too (or rely on the probe preflight in §1 to have
already ruled out a mismatch) — never assume a fixed shape without checking it.
Or let the binary read it back for you. events is the first-party reader of
the same stream, so an adapter that only needs to show a run's story — or to
check one — does not have to write the loop above at all:
processkit-cli events --run-id build-42 # rendered for a human
processkit-cli events --run-id build-42 --follow # ... as it happens
processkit-cli events --file "$jsonl" --json # the runner's own bytes
processkit-cli events --file "$jsonl" --validate # conformance check
It resolves the stream through the registry (--run-id, the same jsonl locator
list --json publishes in §5) or reads a path directly (--file, for a stream
whose registry record is already gone — a clean exit deletes its own record — or
one this registry never knew about). Exactly one of the two is required and they
are mutually exclusive; there is no precedence rule, so passing both is a USAGE
(100) error. Like list/wait it is read-only: registry opened read-only, no
control-plane round trip, nothing mutated.
Three properties matter for an adapter:
--jsonis a pass-through, not a re-serialization. Each line is emitted byte for byte as the runner wrote it, so a field a newer runner added survives the trip — the pipelineprocesskit-cli events --file … --json | your-parseris exactly as lossless as reading the file yourself. A line that is not JSON is reported on stderr instead of emitted, so stdout stays parseable JSONL.--followis bounded by the run, never by an invented deadline. It returns at the terminalrunner_exit, or once the registry reports the run over and the stream has stopped growing — the abrupt-death case, explained on stderr rather than passed off as a complete stream. It hands out only complete lines, so a half-written event is never parsed as an event.--validateis a conformance gate. It checks every line against the schema document this binary embeds — the same oneprobe --print-schemaprints in §1 — reports each violation by line number and by what it violated, and exitsEVENTS_INVALID(114) if any line fails,0if none does. An unreadable stream is stillSETUP(111) and a--run-idnaming no single stream is stillCONTROL(103), so a fixture-checking CI job can tell "invalid" from "could not be checked". This is the recommended way for an adapter to keep its own recorded fixtures honest against the runner version it targets.
Ordering (normative: docs/schema.md). A normal run
emits, in order:
run_started— the child was spawned; carriesrun_id,root_pid, containmentmechanism, theabrupt_cleanuptri-state, and the redactedcommand.members_snapshot(reason: "spawn") — the container's members at that point. Exactly one by default; a run started with--snapshot-interval <duration>emits additionalmembers_snapshotevents (reason: "interval") on that cadence, all of them after this one and all of them before step 3 — never inside the teardown pair. Route by event type and treat the count as open-ended: within a schema version an adapter must not assume an event type it knows occurs only once (the full list of what a reader must tolerate within a version is indocs/compatibility.md). Every one of these events carriesread_error; when it istruethe read failed andmembersis an empty fallback, not a confirmed-empty tree — check the flag before drawing a conclusion about the tree from an empty array.- Either the natural-exit path (
root_exited,cleanup_started,cleanup_finished) or a runner-imposed ending's reason event (timeout,cancelled, orkilled) followed by the samecleanup_started/cleanup_finishedpair. output_captured, only when--capture-dirwas set.runner_exit— always the last line, the terminal event of every run, including a runner failure before the child ever started (in which casespawn_failedorcontainer_failedprecedes it instead, with norun_started).
Telling outcomes apart. Two signals distinguish how a run ended, and an
adapter should use both together: the process's own exit code (fastest to
check, no parsing needed) and the terminal runner_exit event's source and
code fields (authoritative — see docs/exit-codes.md,
"Why a band is not enough on its own"):
runner_exit.source | Exit code | Meaning |
|---|---|---|
child_exit | the child's own code (child_code, echoed in code too) | The child ran to completion on its own. |
timeout | 106 | A runner deadline elapsed and the runner tore the tree down — the whole-run --timeout or the --idle-timeout (child silent past the idle window). The preceding timeout event's reason (overall / idle) says which; both reuse this one source and code. |
cancelled | 107 | A local stop signal cancelled the run: a Ctrl-C, on Unix a SIGTERM/SIGHUP (an external kill/systemctl stop/cancelled CI job, or a hung-up terminal), or on Windows a Ctrl-Break/console close/logoff/system shutdown. The preceding cancelled event's source (ctrl_c / sigterm / sighup / ctrl_break / ctrl_close / ctrl_logoff / ctrl_shutdown) says which; all reuse this one source and code. |
control_cancel | 108 | A control-plane cancel (§4) cancelled the run. |
control_kill | 109 | A control-plane kill (§4) force-killed the run. |
spawn_error | 101 | The child never started (spawn_failed precedes it). |
container_error | 102 | The container could not be created or joined (container_failed precedes it) — including a requested resource limit (--max-memory/--max-processes/--cpu-quota) the platform could not apply, in which case a limit_hit naming the limit precedes the container_failed (see docs/schema.md). |
internal | 104 | A genuine runner bug — the runner's own logic hit a state it rules out. |
setup | 111 | An ordinary fail-closed setup failure (an unwritable --jsonl/--capture-dir, an unreadable --stdin-file) — distinct from internal, and the caller can usually act on it (bad path, permissions, resources). |
Only source: "child_exit" carries a non-null child_code; every other
source means the child's own exit code was never produced or is not what
code reports, and child_code is null. See the full field reference in
docs/schema.md and the exit-code contract in
docs/exit-codes.md.
4. Supervising a live run: inspect / cancel / kill / wait
Once a run has started (its run_id is known — supplied at launch, per §2),
an adapter can query, steer, and wait for it while it is still live. Every
command resolves the target purely by run_id through the per-user registry —
never by PID. This is also the whole supervision story for a run launched with
--detach (§2): a detached run is an ordinary run in the registry, and these
four commands are how an adapter that is no longer its parent steers it —
alongside events (§3), which reads that run's stream back without contacting it
at all:
processkit-cli inspect --run-id build-42 --json
processkit-cli cancel --run-id build-42
processkit-cli kill --run-id build-42
processkit-cli wait --run-id build-42 --timeout 10m
The first three reach the live runner over the local control plane described
normatively in docs/control-plane.md; wait does not
contact the runner at all and is described in docs/registry.md,
"Waiting — wait".
inspectis read-only: it prints a snapshot (mechanism,root_pid,started_at, the currentmembers) to stdout and changes nothing — as JSON with--json(shown above), or a human-readable rendering by default.cancelends the run through the same soft-stop → grace → hard-kill teardown a--timeoutor a localCtrl-Cdrives, exiting the run withCONTROL_CANCELLED(108).killhard-kills the whole tree immediately — no soft stop, no grace — exiting the run withCONTROL_KILLED(109).waitblocks until the run is no longer live and exits0. It is the answer for an adapter that is not the runner's parent — one that restarted, or that supervises runs another process launched — and so has no child process to wait on. It prints nothing (the exit code is the answer), never touches the run, and needs no control endpoint, so it also works for a run whose transport never came up. Adding--report-outcome(single-run only) makes it print one JSON object naming how the run ended —statusreportedwith the terminal event'scode/source/child_code, orstatusunknownwith all threenullwhen the outcome could not be established — without changing any of the exit codes below. Seedocs/registry.md, "Waiting —wait".
Each of these outputs has a published JSON Schema and golden fixture under
fixtures/schema/cli/: inspect.schema.json (the single snapshot and the
--all array), control-ack.schema.json (the cancel/kill ack and the
--all report array), and wait.schema.json (--report-outcome).
Both mutating verbs' outcomes are also written to the target run's own
--jsonl stream (a cancelled/killed event with source
control_cancel/control_kill, and the matching terminal runner_exit), so
an adapter watching that stream sees the command take effect even without
reading the cancel/kill client's own ack.
Tearing down everything at once: --all (T-217). cancel --all / kill --all (mutually exclusive with --run-id, one of the two required) are the
aggregate counterpart to the by-run_id form above: instead of one named run
they act on every run confirmed live in a snapshot taken when the invocation
starts, applying the identical per-run mutation to each, and print a single
JSON array on stdout — one {"run_id":...,"accepted":...} entry per snapshot
target — instead of one ack. An adapter driving a full environment teardown
(e.g. before shutting down its own process) typically issues cancel --all
in place of a loop over individually-known run_ids, then wait --all /
list --json / prune to confirm the fleet is actually gone. See
docs/control-plane.md, "cancel --all / kill --all",
for the exit-code and report contract — it differs from the by-run_id form's
103 in the note right below.
Waiting for a run an adapter did not launch. The typical shape — cancel a run, then confirm it is really gone before releasing the resources it held:
processkit-cli cancel --run-id build-42 # 0: the runner acked
processkit-cli wait --run-id build-42 --timeout 30s
case $? in
0) ;; # the run is over; its own exit/JSONL say how it ended
112) ;; # still live at the deadline — the run was NOT touched
103) ;; # ambiguous run id: more than one live run uses it (§6)
esac
0means "not running". It is also what an unknownrun_idreturns, on purpose: a clean exit deletes its own registry entry, so "never registered" and "already finished and cleaned up" are the same observation, and failing on the second would turn the ordinary "it finished while I was starting up" race into an error. The flip side an adapter must respect: a typo'drun_idalso returns0, so never readwait's0as proof the run existed — establish that from the launch itself or fromlist(§5).WAIT_TIMEOUT(112) is the waiter's deadline, not the run's: the run was left running and untouched, and is still going. Do not confuse it with the run's ownTIMEOUT(106) in §3's table, which means the runner tore the tree down. Retrying the samewaitis a reasonable response to a112.CONTROL(103) here means only one thing — an ambiguousrun_id(§6);waithas no runner to fail to reach.- Without
--timeout,waitblocks indefinitely. Prefer an explicit deadline in an adapter, so a supervisor never inherits an unbounded wait.
CONTROL (103) is the one exit code all four of these clients' by-run_id
form can return, and for all four the usual reason is the same: the command could
not be resolved to the single target run. inspect has one further reason of its
own, where the target was resolved and reached and did answer — its reply declared
a control-plane snapshot version this client does not read, so the answer was
refused rather than rendered (see docs/control-plane.md, "Snapshot version: a
newer runner's reply is refused, an older one is read"). See §6 for the concrete
situations that produce a 103, that one included. cancel --all / kill --all
reuse the same code for a different reason — one or more snapshot targets failed,
not "no single target run" — see the --all paragraph above and
docs/control-plane.md.
5. Housekeeping: list / prune
list and prune scan the registry directly rather than reaching a specific
live run, and are the tools for an adapter that manages many runs or wants to
clean up after abrupt failures — see the normative "Discovery" and "Reaping"
sections of docs/registry.md.
processkit-cli list --json # every registered run, whatever its health
processkit-cli prune --json # reap only the confirmed-stale entries
list --jsonprints one JSON object per registry entry (run_id, health,started_at,hint,argv_sha256,endpoint), sorted deterministically.argv_sha256andhintare the same redaction-safe command identification therun_startedevent carries (§3) — the full 64-character digest here, so an adapter can join a registry entry to the events of the run that wrote it, or group several live entries by "same command" without ever handling a command line. Both arenullon a record written before those fields existed, andhintisnullfor the common case of a command matching no known worker shape. Health islive,stale(confirmed dead — no live holder found), orunprobed(the liveness lock could not even be opened, e.g. permission denied — a distinct, additive value: liveness is unknown, never printed as the confirmed-deadstale). All three are listed, never hidden — a stale entry (a leftover from a runner that died abruptly) is exactly what an operator or adapter wants visible here, and an unprobed one is exactly the case where guessing would mislead.prune --jsondeletes only entries it can confirm are stale, printing a tally:{"pruned":N,"live":N,"unprobed":N,"orphaned_locks":N}. A live run is never touched, and an entry whose liveness could not even be probed is left in place rather than guessed at — see "The reaping safety invariant" indocs/registry.md. On unix each reaped entry also takes with it the private control-socket directory that record published, so an abruptly-killed run leaves nopkc-…litter in the temp directory either; the tally fields are unchanged (that socket is counted by its own entry'spruned). Worth scheduling if your adapter starts many runs — see "Reaping the control socket" indocs/registry.md.
Both are read-only with respect to any live run's control transport; neither carries the "could not reach the target run" failure modes of §4.
Their machine-readable shapes are published too: fixtures/schema/cli/list.schema.json
(one entry object per line) and fixtures/schema/cli/prune.schema.json (the plain
tally and, as #/$defs/dryRunReport, the --dry-run form with its candidates
list), each with a golden *.jsonl fixture beside it.
6. Typical errors
- Stale registry entry. The runner behind a
run_iddied abruptly (crash,SIGKILL, a parent's Job Object terminate); its record is left behind but its liveness lock is released.inspect/cancel/killdetect this before connecting and report it as aCONTROL(103) failure with an explanatory message on stderr — never a hang, and never silently treated as live.liststill shows the entry (markedstale);pruneis what removes it. An ordinary UnixSIGTERM/SIGHUP, or a WindowsCtrl-Break/console close/logoff/system shutdown, is not in this class: the runner catches those signals/events and runs the full cancel teardown (acancelledevent, the cleanup pair,runner_exitcancelled/107, and removal of the registry entry), so stopping a run withkill <pid>(Unix) or a closed console (Windows) leaves neither a stale entry nor a surviving descendant. - Unprobeable registry entry. The entry's liveness lock could not be probed
at all (permission denied, a rejected symlink/reparse point, a non-regular
file in its place), so nothing about the run is confirmed either way. This is
the same
CONTROL(103) refusal —inspect/cancel/killact only on a confirmed-live entry — but it is reported honestly asunprobed, not as a gone runner;listshows the same entry asunprobedandpruneleaves it in place. Investigate the registry directory rather than deleting the record by hand (seedocs/troubleshooting.md). - Died mid-conversation. The registry entry read as live, but the runner
exited between the liveness check and the reply reaching the client — the
connect fails, or the connection closes before a complete response. Also a
bounded
CONTROL(103) failure, never a wedge: every wait in the control plane (connecting, and the request/response exchange) is deadline-bounded. - Ambiguous
run_id. The registry does not enforcerun_iduniqueness; if more than one live entry matches, every by-run-idcommand — the read-onlyinspectandwaitincluded — fails closed withCONTROL(103) rather than guessing which entry the scan happened to return first. Keeprun_ids unique among an adapter's own concurrently-live runs (§2) to avoid this entirely. - An unreadable snapshot version (
inspectonly). The runner was reached and answered, but its reply declared a control-planesnapshot_versionoutside the range this client reads — newer than the version it implements, or older than the version it still decodes — soinspectrefuses the answer instead of rendering it under semantics its sender never promised. Also aCONTROL(103), with a message naming the version that arrived and the range this build reads. Unlike the four above it says nothing about the run's liveness: the target is registered, live, reachable, and healthy, andcancel/kill/wait/listagainst it are unaffected (an ack carries no version). Do not treat it as a lost runner or retry it; inspect that run with a build that speaks its version — for a newer runner, one at least as new as the binary that started the run. Seedocs/control-plane.md, "Snapshot version: a newer runner's reply is refused, an older one is read", anddocs/compatibility.md, "Machine-output schemas". CONTROL-class exit codes are not run outcomes. A103from the by-run_idform ofinspect/cancel/kill/waitdescribes a failure on the client's side of the exchange — it could not resolve or reach a single target, or (theinspect-only case above) could not read the answer it got — and says nothing about how the target run itself ended (or is still running). Do not conflate it with the run-outcome codes in §3's table (106–109, or the child's own code); those come only from the run's own process exit and itsrunner_exitevent. The same separation applies toWAIT_TIMEOUT(112): it is the waiting client giving up, never the run being stopped (§4).cancel --all/kill --all's own103is the one exception where the code can coincide with some targets having genuinely been acted on — see the--allparagraph in §4.- A
--detachexit code is not a run outcome either.run --detach's0means "the run started", not "the child succeeded", and its non-zero codes mean "the run never started" — carrying the same reserved code the failure would have produced in the foreground. An adapter that branches on a detached launch's exit code as if it were the child's result will read every long-running failure as a success; the child's outcome is in the terminalrunner_exitevent (§3), reached afterwait(§4). Seedocs/exit-codes.md, "Detached runs". SETUP(111) vs.INTERNAL(104). Arunthat could not write its--jsonl/--capture-dir, or open a--stdin-file, fails closed withSETUP(111) — an ordinary, usually-actionable environment problem (bad path, permissions), not a runner bug.INTERNAL(104) is reserved for a genuine invariant violation in the runner's own logic. See "Setup failures vs internal faults" indocs/exit-codes.md.
See also
docs/agent-workflows.md— a policy and execution strategy for automation agents that launch external tools through the runner.docs/schema.md— the normative JSONL event schema (every field, every event, versioning rules).fixtures/schema/cli/README.md— the JSON Schema documents and golden fixtures for every machine-readable output in this guide (probe,list,inspect, thecancel/killacks,prune,wait --report-outcome), and the versioning decision behind them (probeandinspectcarry their own version field; the other four deliberately carry none).docs/compatibility.md— the compatibility surfaces, the pinning procedure, and the upgrade/downgrade checklists.docs/exit-codes.md— the normative reserved exit-code band and the child-fidelity rule.docs/control-plane.md— the normative local transport, wire protocol, andinspect/cancel/killbehavior.docs/registry.md— the normative registry location, record format, and staleness/reaping rules.docs/architecture.md— the map of this repository's own modules, for a contributor rather than a consumer.docs/troubleshooting.md— symptom-to-cause diagnosis for an operator, organized by what you observe rather than by call sequence.