seam-probe · diff

git:20260724.8a27559 to git:20260725.6c1b23d

76 added, 438 removed. Audit A to A.

---
name: seam-probe
- description: >
- Use this skill to test, validate, fuzz, or debug a process boundary
- ("seam") that exposes itself as an FFI dynamic library or a Unix-domain
- socket — for example probing a Rust runtime hosted in a Swift or
- Electron app via its C ABI, or sending crafted length-prefixed frames
- to a daemon's UDS endpoint. The skill teaches how to drive the probe
- from the CLI, capture both the probe's NDJSON output AND the system-
- under-test's stderr/logs, and triage failures by correlating the two
- streams. Reach for it when the user asks to "probe", "fuzz",
- "exercise", "test the FFI surface", "test the socket protocol",
- "send raw frames", "discover what a dylib exports", "find a bug in
- the runtime", "reproduce a crash", or "see why the seam panics or
- hangs".
+ description: This skill should be used when testing or debugging an embedded-runtime boundary exposed through a dynamically loaded C-ABI library or Unix-domain socket, including requests to inspect exports, exercise FFI callbacks, send framed messages, reproduce seam crashes or hangs, fuzz a boundary, or correlate probe output with runtime logs. Not for ordinary APIs, HTTP/WebSocket services, static libraries, or generated bindings.
allowed-tools: Bash, Read, Grep, Write
---
# seam-probe
- `seam-probe` is a **generic** NDJSON probe for embedded-runtime seams. It
- ships **zero** app-specific knowledge. Everything app-specific is
- expressed in a small JSON **manifest** that the user (or you) writes and
- hands to the binary.
-
- There are two real seams in this skill's world:
-
- | Seam | What it is | Probe mode |
- | --------------- | ----------------------------------------------------- | --------------- |
- | FFI dylib | dlopen + extern "C" entry points + callback struct | `seam-probe ffi` |
- | UDS endpoint | Unix-domain socket carrying length-prefixed frames | `seam-probe socket` |
-
- Plus two utility modes that need no manifest:
-
- | Utility | What it does |
- | ---------------- | ----------------------------------------------------- |
- | `inspect` | Dump exported symbols of a dylib (Mach-O / ELF / PE) |
- | `vocab` | Print the stdin/stdout NDJSON contract verbatim |
-
- ### Scope: what counts as a "seam"
-
- The probe handles **dynamically-loaded native code** (FFI mode) and
- **Unix-domain sockets carrying byte frames** (socket mode). Concretely
- in scope:
-
- - Native `.dylib` (Mach-O) and `.so` (ELF) libraries loaded through
- `dlopen`. The library must export an `extern "C"` lifecycle function
- and accept a plain C-ABI callback struct.
- - `inspect` can also parse PE/COFF `.dll` files supplied on a POSIX host.
- - Unix-domain stream sockets with `be32` / `be64` framing or no framing.
- `varint` is a reserved option that reports the raw-mode LEB128 workaround.
-
- The packaged launcher and SessionStart hook require a POSIX host.
- Native Windows execution is not currently shipped.
-
- Out of scope today (would need probe extensions, not just a manifest):
-
- - **Statically-linked Rust crates** (`crate-type = ["staticlib"]`,
- produces `.a`/`.lib`). Static archives have no dynamic load
- surface — they're meant to be linked at build time. The probe
- cannot load them. **Tell the user to expose a cdylib alongside
- their static target for probing**; see "Preparing a Rust crate
- for probing" below. This is a build-config change only — no code
- changes, no impact on production distribution.
- - **UniFFI crates**. UniFFI produces a cdylib that *is* dlopen-able,
- but exports use UniFFI's own wire format: every call goes through
- `RustBuffer` (a `(ptr, len, capacity)` struct) with a
- `RustCallStatus` out-parameter, and arguments are serialised via
- UniFFI's binary format. The probe's three callback signature
- kinds (`json` / `json_with_sid` / `raw_with_seq`) don't model
- this. For UniFFI-exposed crates use **`uniffi-bindgen`** to
- generate a Swift / Kotlin / Python / Ruby binding and test
- through that. Probing UniFFI directly would require the probe to
- read the `*.uniffi.json` metadata and implement the FFI converter
- protocol — significant scope creep.
- - **TCP sockets** — same framing logic would apply, but the binary
- has no `--tcp host:port` mode yet. For loopback testing of a TCP
- service, fall back to `nc` / `socat` / `websocat`.
- - **Subprocess + stdio** — driving a CLI that reads NDJSON on stdin
- and emits responses on stdout. Common for "headless mode" Rust
- runtimes. For this, just use shell pipes + `tee`.
- - **Wasm modules** — `wasmtime` / `wasmer` calling exports with
- import shims. Different ABI from C; not implemented.
- - **gRPC, D-Bus, HTTP, WebSocket** — use `grpcurl`, `busctl`, `curl`,
- `websocat` respectively.
- - **Kernel modules, JNI, Python C API** — out of scope.
-
- If the user wants to probe one of the out-of-scope surfaces, say so
- and suggest the right tool rather than forcing a fit.
-
- ### Preparing a Rust crate for probing
-
- If the user's crate is `staticlib`-only (or `rlib`-only, or it's a
- binary crate with no library target), it's not loadable by the probe.
- Have them add a cdylib output to their `Cargo.toml`. This **does not**
- change what they ship — they can keep their existing target and add
- cdylib alongside:
-
- ```toml
- [lib]
- crate-type = ["cdylib", "staticlib"] # or just ["cdylib"] for probing only
- ```
-
- After `cargo build --release`, the cdylib lands at:
-
- | Platform | Path |
- | --- | --- |
- | macOS | `target/release/lib<crate_name>.dylib` |
- | Linux | `target/release/lib<crate_name>.so` |
- | Windows| `target/release/<crate_name>.dll` |
-
- The crate must also actually expose its surface as `extern "C"`:
-
- ```rust
- #[unsafe(no_mangle)]
- pub extern "C" fn foo_start(cb: *const FooCallbacks) -> i32 { /* … */ }
-
- #[unsafe(no_mangle)]
- pub extern "C" fn foo_stop() { /* … */ }
- ```
-
- If the crate uses `mangle` or `extern "Rust"`, it's not probe-able
- even as a cdylib — symbols won't be resolvable by name. Internal Rust
- APIs need to be wrapped with a thin C-ABI shim module. This is a
- small one-time cost; the user can gate it behind a feature flag
- (`#[cfg(feature = "probe")]`) so it doesn't pollute production.
-
- Generating a C header at build time with **`cbindgen`** is strongly
- recommended — the manifest's `callback_struct[]` field order has to
- match the C struct exactly, and the header is the source of truth.
-
- ## Preflight
-
- ```bash
- seam-probe vocab >/dev/null
- ```
-
- If this errors with `seam-probe is not built`, the SessionStart hook
- either hasn't finished yet (fresh install or plugin update — wait a
- moment and retry) or it failed. Tell the user to run
- `/seam-probe:seam-probe-setup` for verbose recovery output.
-
- If the command runs but a flag in this skill is missing or behaviour
- disagrees with the docs, stop and ask the user to **update** the
- plugin. Do not guess around missing features.
-
- ## When to reach for which mode
-
- 1. **You don't know what the dylib exports** → start with `seam-probe inspect`.
- 2. **You have an unfamiliar dylib but know its callback contract** → write
- a manifest, then `seam-probe ffi`.
- 3. **You have a UDS endpoint but don't know the framing** → try
- `--framing be32` first (most common); fall back to `--framing none`
- and inspect raw bytes if framing is wrong.
- 4. **You forget the NDJSON contract** → `seam-probe vocab`.
- 5. **You're hunting a bug, panic, or hang** → run the probe with stderr
- captured (FFI) or with the SUT log tailed (socket) and correlate the
- two streams. See "Read both streams" below.
-
- ## How to invoke the CLI
-
- The probe is one binary with four subcommands. **Always separate stdout
- (NDJSON) from stderr (probe diagnostics + anything the loaded library
- prints).** The two streams answer different questions and you usually
- need both.
-
- ### `inspect` — list a dylib's exports
-
- ```bash
- seam-probe inspect --lib /path/to/libfoo.dylib
- ```
-
- No manifest, no stdin. Emits one `symbol` line per export plus a
- `summary` line. Use it before writing a manifest, or to confirm a
- build actually exposes what you expect.
-
- ### `ffi` — drive a dylib via a manifest
-
- ```bash
- seam-probe ffi \
- --lib /path/to/libfoo.dylib \
- --manifest /path/to/foo.manifest.json \
- [--no-events] \
- [--shutdown-grace-ms 2000]
- ```
-
- | Flag | When to use it |
- | --- | --- |
- | `--lib` | Required. Shared library to dlopen. |
- | `--manifest` | Required. JSON describing the FFI surface (`references/manifest-schema.md`). |
- | `--no-events` | Mute callback `event`/`json_with_sid`/`raw_with_seq` lines. Use when you only care about send/call return codes. |
- | `--shutdown-grace-ms` | Total deadline for the stop call plus callback draining. Bump it (e.g. `5000`) if workers need longer. A hanging stop is cut off at this deadline. |
-
- Read input from stdin (one JSON op per line). Heredoc is the cleanest
- way to drive a deterministic sequence:
-
- ```bash
- seam-probe ffi --lib ./libfoo.dylib --manifest /tmp/foo.manifest.json \
- > /tmp/probe.ndjson 2> /tmp/sut.stderr <<'EOF'
- {"op":"call","name":"start_session","arg":"sess-1"}
- {"op":"send","lane":"events","payload":{"hello":"world"}}
- {"op":"sleep_ms","ms":500}
- {"op":"stop"}
- EOF
- echo "exit=$?"
- ```
-
- ### `socket` — drive a UDS endpoint
-
- ```bash
- seam-probe socket \
- --path /tmp/foo.sock \
- [--framing be32|be64|varint|none] \
- [--no-events]
- ```
-
- | Flag | When to use it |
- | --- | --- |
- | `--path` | Required. The Unix socket the SUT bound. |
- | `--framing` | Default `be32`. Switch when the SUT's read loop expects something else (`references/framing-modes.md`). |
- | `--no-events` | Suppress inbound `frame` events; emit only `rc`/`control`/`error`. |
-
- `varint` is reserved but not implemented directly; selecting it exits with the
- manual-prefix workaround. Use `none` and prepend the LEB128 length as described
- in `references/framing-modes.md`.
-
- Socket mode talks to a separate process — start it (or have the user
- start it) and tail its log file in parallel. See "Read both streams".
-
- ### `vocab` — print the I/O contract
-
- ```bash
- seam-probe vocab
- ```
-
- Run this when you forget input ops or output kinds. Cheaper than
- reading `references/ndjson-vocab.md`.
-
- ### Useful env vars (FFI mode only)
-
- The loaded library runs in the probe's process. Anything it prints to
- stderr lands on the probe's stderr. Two Rust-runtime knobs commonly
- matter:
-
- | Env var | Effect |
- | --- | --- |
- | `RUST_LOG=debug` (or `trace`) | Most Rust runtimes use `tracing` or `env_logger`; this dumps internal events to stderr. Invaluable for understanding why a callback never fired. |
- | `RUST_BACKTRACE=1` | Print a backtrace on panic. The whole probe process aborts on a Rust panic, but you still get the stack. |
-
- Apply at invocation:
-
- ```bash
- RUST_LOG=debug RUST_BACKTRACE=1 \
- seam-probe ffi --lib ./libfoo.dylib --manifest /tmp/foo.manifest.json \
- > /tmp/probe.ndjson 2> /tmp/sut.stderr < /tmp/input.ndjson
- ```
-
- For non-Rust runtimes use whatever logging-level/destination knob the
- runtime exposes.
-
- ## Read both streams (probe NDJSON + SUT logs)
-
- The probe tells you **what the seam returned**. The SUT's logs tell
- you **why**. Bugs and runtime errors are usually only visible by
- correlating the two — never reason from probe output alone unless the
- issue is a probe-side parse error or transport error.
-
- ### FFI: same process
-
- The loaded library shares the probe's address space, so:
-
- - The library's stderr → the probe's stderr.
- - A panic in the library aborts the probe.
- - A SIGSEGV in the library kills the probe.
-
- Capture and inspect:
-
- ```bash
- RUST_LOG=debug RUST_BACKTRACE=1 \
- seam-probe ffi --lib ./libfoo.dylib --manifest M.json \
- > /tmp/probe.ndjson 2> /tmp/sut.stderr < /tmp/input.ndjson
- echo "exit=$?"
-
- echo "--- probe NDJSON (head) ---"; head -50 /tmp/probe.ndjson
- echo "--- SUT stderr (head) ---"; head -50 /tmp/sut.stderr
- echo "--- probe errors only ---"; grep '"kind":"error"' /tmp/probe.ndjson
- echo "--- SUT panics/errors ---"; grep -iE 'panic|error|warn|fatal' /tmp/sut.stderr
- ```
-
- ### Socket: separate processes
-
- The SUT runs independently. Either the user already has it running
- (ask where logs go), or you start it yourself:
-
- ```bash
- SOCK=/tmp/foo.sock
- LOG=/tmp/sut.log
-
- rm -f $SOCK
- RUST_LOG=debug ./target/debug/sut > $LOG 2>&1 &
- SUT_PID=$!
-
- # Wait up to 2.5s for the socket to appear.
- for _ in $(seq 1 50); do [ -S $SOCK ] && break; sleep 0.05; done
- [ -S $SOCK ] || { echo "SUT failed to bind"; cat $LOG; kill $SUT_PID 2>/dev/null; exit 1; }
-
- seam-probe socket --path $SOCK --framing be32 \
- > /tmp/probe.ndjson < /tmp/input.ndjson
- PROBE_RC=$?
-
- kill -TERM $SUT_PID 2>/dev/null; wait $SUT_PID 2>/dev/null
-
- echo "probe=$PROBE_RC"
- echo "--- probe ---"; cat /tmp/probe.ndjson
- echo "--- sut ---"; cat $LOG
- ```
-
- If the user already has the SUT running, ask:
- 1. The socket path.
- 2. Where logs go (file? stderr to a pane? `journalctl -u …`? docker
- log stream?). Read from there.
-
- ### Triage table
-
- | Probe NDJSON shows | SUT log likely shows | Most likely cause |
- | --- | --- | --- |
- | `kind:"rc"` `rc!=0` after `call` | `error!()` line near that ts (or nothing) | Manifest-declared symbol returned non-zero. Check runtime's error semantics. |
- | no `event` lines despite `send` | `WARN dropped event` or silence | Lane not wired in runtime, or a callback field is missing/wrong in the manifest. |
- | probe process aborts mid-run | `thread '…' panicked at …` + backtrace | Bug in the runtime. **This is what the probe is for** — capture stdin as a repro. |
- | probe process aborts, no panic | `Segmentation fault (core dumped)` | ABI mismatch. Manifest's `callback_struct[]` is wrong (field order or signature kind). Re-run `inspect`, recheck the C header. |
- | `runtime stop exceeded shutdown grace` | `worker N still running` or nothing | Runtime stop is blocked. Increase `--shutdown-grace-ms` only for diagnosis; the runtime owns the shutdown bug. |
- | `kind:"error"` `msg:"unknown lane"` | nothing | Typo in `lane` op or in `manifest.lanes[].name`. |
- | `kind:"error"` `msg` starts with `connect failed:` (socket) | `bind: Address already in use` or nothing | SUT not listening, wrong path, or permission. `ls -l $SOCK`. |
- | probe hangs after first `send` (socket) | nothing | Wrong framing. Try `be64` or `none`; for varint, use raw mode with a manual LEB128 prefix. |
- | `kind:"error"` `msg` starts with `frame read error:` (socket) | `wrote oversized frame` | Framing mismatch or an oversized framed read. |
- | `kind:"error"` `msg` starts with `frame read error:` / `read error:` (socket) | panic / `signal: aborted` | SUT crashed or closed the connection. Capture as repro. |
-
- For the long-form playbook with recovery steps for each pattern, see
- `references/observability.md`.
-
- ### Filter NDJSON cheaply
+ Use the bundled executable as the interface:
```bash
- jq -c 'select(.kind=="error")' /tmp/probe.ndjson # probe errors
- jq -c 'select(.kind=="rc" and .rc!=0)' /tmp/probe.ndjson # failed ops
- jq -c 'select(.kind=="event")' /tmp/probe.ndjson # all events
- jq -c 'select(.kind=="event" and .callback=="on_x")' /tmp/probe.ndjson
+ SEAM_PROBE="${CLAUDE_PLUGIN_ROOT}/bin/seam-probe"
```
- If `jq` is missing, `grep '"kind":"error"'` covers most needs.
-
- ## Discovery workflow (do this for every new app)
-
- Apps differ. Don't assume any of the file shapes below. Always verify
- against the target app's source or generated header before writing a
- manifest.
-
- ### Discover an FFI surface
+ The short name `seam-probe` below means `"$SEAM_PROBE"`. A direct skill install
+ does not include the plugin-level launcher, crate, or hook; install the binary
+ independently on PATH or use the full plugin.
- 1. `seam-probe inspect --lib path/to/libfoo.dylib` → list exported symbols.
- 2. Read the app's generated C header (look for `*.h` produced by cbindgen
- or hand-maintained alongside the dylib). The header is the source of
- truth for callback struct field order and per-field signatures.
- 3. For each callback field, classify the signature into one of the
- probe's three kinds (see `references/manifest-schema.md`).
- 4. Write the manifest. Field order in `callback_struct[]` MUST match the
- C struct's declaration order — the manifest **is** the struct
- definition for the probe's purposes.
+ ## Pick the surface
- ### Discover a UDS protocol
+ - `seam-probe inspect` parses a library to discover exports without loading or
+ executing it.
+ - `seam-probe ffi` loads a C-ABI dylib and drives lifecycle, callbacks, and
+ input lanes described by a manifest.
+ - `seam-probe socket` sends NDJSON operations to a Unix-domain stream socket
+ using the selected framing.
+ - `seam-probe vocab` prints the current stdin/stdout contract.
- 1. Find where the server binds the socket (search for `bind`,
- `UnixListener`, `SOCK_STREAM`, framing constants).
- 2. Identify framing by reading the server's read loop. Look for
- `length_field_type::<u32>` (be32), explicit byte reads of 4 or 8
- bytes, or no length prefix at all.
- 3. Probe with `--framing be32` first. If the server hangs forever on the
- first byte, framing is wrong — try `be64`, or use `none` and
- reverse-engineer. For varint, prepend the LEB128 length manually.
+ Run `seam-probe vocab` as preflight. If the launcher reports that the binary is
+ not built, use `/seam-probe:seam-probe-setup`. If installed behavior disagrees
+ with this skill, inspect live help and request a plugin update rather than
+ inventing flags.
- See `references/discover-ffi-symbols.md` and
- `references/discover-socket-protocol.md` for concrete steps.
+ ## Investigate progressively
- ## Toy examples
+ Start with the least assumed knowledge:
- These are deliberately **synthetic** — no real app embedded in the skill.
- They exist to show the shape; real apps need real manifests.
+ 1. Inspect an unfamiliar library before writing symbol names into a manifest.
+ 2. Read headers, binding metadata, or the host's loading code to recover the
+ actual ABI and callback-field order.
+ 3. For sockets, inspect the server and client framing code before guessing.
+ Use a small probe only to distinguish remaining plausible framings.
+ 4. Create the smallest manifest that exposes the behavior under test.
+ 5. Reproduce one meaningful operation before expanding into a sequence or
+ fuzzing campaign.
- ### FFI: probe a dylib that exports `lib_start`/`lib_stop`/`lib_send_event`
+ Use `references/discover-ffi-symbols.md` and
+ `references/discover-socket-protocol.md` only when discovery is needed.
- ```bash
- cat > /tmp/foo.manifest.json <<'EOF'
- {
- "schema_version": 2,
- "lifecycle": {
- "start_symbol": "lib_start",
- "stop_symbol": "lib_stop"
- },
- "callback_struct": [
- { "name": "on_event", "kind": "json" }
- ],
- "lanes": [
- { "name": "events", "symbol": "lib_send_event" }
- ]
- }
- EOF
+ ## Read both sides of the seam
- # Drive the runtime
- {
- echo '{"op":"send","lane":"events","payload":{"hello":"world"}}'
- echo '{"op":"sleep_ms","ms":250}'
- echo '{"op":"stop"}'
- } | seam-probe ffi --lib ./libfoo.dylib --manifest /tmp/foo.manifest.json
- ```
+ The probe emits machine-readable NDJSON on stdout and diagnostics on stderr.
+ The system under test may emit its own stderr, structured logs, panic report,
+ or crash artifact. Capture these streams separately and correlate them by
+ operation, sequence, session, and time.
- ### UDS: send a JSON frame to a be32-framed Unix socket
+ A probe-side success means only that the command was accepted. Verify the
+ system-under-test postcondition and inspect its logs for delayed panics,
+ protocol errors, dropped callbacks, or incomplete shutdown. Use
+ `references/observability.md` for long-form correlation and hang triage.
- ```bash
- echo '{"op":"send","payload":{"ping":"pong"}}' | \
- seam-probe socket --path /tmp/foo.sock --framing be32
- ```
+ ## Do not force incompatible surfaces
- ### UDS: send raw hex bytes to a no-framing socket
+ The FFI mode expects a dynamically loadable library with an explicit C ABI and
+ one of the callback shapes supported by the manifest schema. Static archives,
+ Rust-only ABIs, UniFFI wire types, JNI, Python C extensions, Wasm imports,
+ gRPC, HTTP, and WebSocket are different interfaces. Use their generated
+ bindings or native protocol tools unless the probe is intentionally extended.
- ```bash
- echo '{"op":"raw","hex":"deadbeef"}' | \
- seam-probe socket --path /tmp/foo.sock --framing none
- ```
+ Socket mode supports the framing implemented by the installed binary. Check
+ `seam-probe vocab`, live help, and `references/framing-modes.md`; do not infer
+ framing from payload appearance.
- ## Output: NDJSON, one event per line
+ ## Safety invariants
- Every line of stdout is a single JSON object terminated by `\n`. Every
- line of stdin must be a single JSON object terminated by `\n`. The full
- vocabulary is in `references/ndjson-vocab.md`. When in doubt run
- `seam-probe vocab`.
+ Loading arbitrary native code can crash or corrupt the probe process:
- ## Safety: things you must internalise
+ - Do not unload a library while its threads or callbacks may still execute.
+ - Match callback layout and signatures exactly; an ABI mismatch is undefined
+ behavior, not a recoverable validation error.
+ - Treat a sentinel abort as evidence that the manifest omitted or misdeclared a
+ callback field, but only after verifying the callback struct has at most 64
+ pointer fields.
+ - Run destructive fuzzing in an isolated process with bounded input, time, and
+ artifact storage.
+ - Only lifecycle `stop` is grace-bounded. Start, lane, and op calls execute
+ synchronously and can block indefinitely; supervise hang-prone probes with an
+ external process timeout.
- The probe loads arbitrary native code into its own address space. Three
- rules guard against UB:
+ The supported manifest grammar and callback forms live in
+ `references/manifest-schema.md`; rationale and shutdown details live in
+ `references/safety-notes.md`.
- 1. **The probe never calls `dlclose`.** Loaded libraries leak
- intentionally — runtime-spawned worker threads may still be in
- flight when stdin closes, and unloading their `.text` segment would
- segfault. See `references/safety-notes.md`.
- 2. **Callback fields not declared in the manifest are bound to a
- sentinel that aborts.** If the runtime's actual callback struct has
- more fields than the manifest declares and the runtime calls into
- that slot, the probe aborts loudly rather than invoking
- uninitialised memory. The fix: add the missing field to the
- manifest.
- 3. **Signatures are restricted to three shapes** (see
- `references/manifest-schema.md`). Anything else (variadic, struct-
- by-value, stdcall, fastcall) requires a code change to the probe.
+ ## Contract references
- ## Reference docs
+ - `seam-probe vocab` prints the authoritative input, output, lifecycle, and
+ exit-code contract.
+ - `references/framing-modes.md` documents socket framing and its limitations.
+ - `references/manifest-schema.md` documents the FFI manifest.
+ - `references/observability.md` documents two-stream diagnosis.
+ - `references/discover-ffi-symbols.md` and
+ `references/discover-socket-protocol.md` cover discovery.
+ - `references/safety-notes.md` covers ABI and process-lifetime hazards.
- - `references/manifest-schema.md` — full manifest grammar and the three
- callback signature kinds.
- - `references/observability.md` — long-form triage and log-correlation
- playbook for finding bugs in the SUT.
- - `references/discover-ffi-symbols.md` — how to learn an unfamiliar
- dylib's surface.
- - `references/discover-socket-protocol.md` — how to learn an unfamiliar
- UDS endpoint's protocol.
- - `references/framing-modes.md` — be32 / be64 / none, plus the varint workaround.
- - `references/ndjson-vocab.md` — every input op and every output kind.
- - `references/safety-notes.md` — never-unload, ABI over-allocation,
- shutdown grace, sentinel slots.
+ Prefer the executable's `vocab`, `inspect`, and `--help` interfaces over copied
+ examples. Report the exact command, manifest, input sequence, output events,
+ system logs, and resulting postcondition so the failure can be reproduced.