# cc-launcher bind fix — spt-hosted endpoint-run self-bind — JIT plan

> **Trigger:** doyle finding (2026-06-17). Operator ran `spt endpoint run <claude-spt> wall-b`
> (spt-hosted) → CC PTY came up but `spt send wall-b` → `NO_PERCH: wall-b is not listening`, and
> **zero perch on disk** under owlery/ for wall-b. doyle localized to the adapter side + confirmed the
> meta-hole: this path is the **M12 cc-launcher**, impl+UNIT only, **never int-proven E2E**.

## ROOT-CAUSE LANDED (2026-06-17, perri — F-013)

Hard repro via throwaway probe-adapter (public-surface only). **`spt endpoint run` (0.9.1) threads the
endpoint `{id}` to the `[session.self]` spawn ONLY via `{id}` substitution in the command ARGV;
`[env.<VAR>].value = "{id}"` is NOT substituted (injects EMPTY)** — despite the schema documenting
`value` as "with substitution". Bare `claude` is flagless (no argv slot for the id), so the adapter
routed it via `[env.SPT_ENDPOINT_ID]` — a no-op → SessionStart sees empty `$SPT_ENDPOINT_ID` →
`sptc_register_verb` returns **`seed` not `bind`** → seeds-by-PPID instead of binding → **zero perch →
wall-b NO_PERCH**. `bind` itself is fine (direct `api bind` builds a fully reachable perch; needs no
broker-parentage/token). Tasks 1 (root-cause) DONE → F-013 in `docs/SPT-CORE-FINDINGS.md`.

**RULED (doyle 2026-06-17): fork (a) — spt-core BUG.** The schema already promises `value` "with
substitution", so spt-core not applying it is a silent correctness bug. **Adapter manifest is CORRECT
as-is — NO wrapper.** Dispatched `REQ-HAZARD-ENV-SUBST` → todlando (v0.11.0-findings). **Task 3 (int)
HELD** until that fix gates+releases — doyle pings, then the int asserts `endpoint run <id>` →
`SPT_ENDPOINT_ID` populated → SessionStart **binds** → bound perch on disk + reachable via next poll.
Secondary ghost-roster = separate daemon-side finding doyle is minting (leave it, harmless).

> **No adapter work outstanding here** — both remaining halves are spt-core (REQ-HAZARD-ENV-SUBST +
> REQ-SEND-SPT-HOSTED). Re-enter only when doyle pings that env-subst released, to write the int.

## Two-layer resolution (doyle-agreed)
- **NOW (this plan, adapter):** make `spt endpoint run` yield a BOUND perch, reachable-next-turn via
  the per-prompt `UserPromptSubmit` `api poll` hook (ready-style — the manifest-configured delivery
  method, CONTEXT.md:188). Fix the bind + add the missing int.
- **DESIGN end-state (doyle, spt-core, separate):** the daemon routes inbound `spt send` into the
  held PTY for spt-hosted endpoints (today inbound delivery is the api-listen TCP-port model → an
  spt-hosted endpoint has no port → no inbound route; daemon delivers brain EVENTS to the PTY but not
  inbound messages). doyle verifies + may mint a spt-core REQ. NOT our work.

## What [session.self] does today (confirmed)
- `[session.self] command = "claude"` (bare interactive PTY), `keys = ["id"]`.
- `[env.SPT_ENDPOINT_ID] inject value = "{id}"` — the only thing threaded to the spawn.
- Binding is HOOK-driven: `plugin/sptc/hooks/session-start.sh` `bind)` branch (fires when
  `sptc_register_verb` sees `SPT_ENDPOINT_ID` set):
  `spt api --adapter claude-spt bind "$SPT_ENDPOINT_ID" --set-session-id "$sid" >/dev/null 2>&1 || true`
- It's a BIND (establish perch + associate session), NOT a listen. Delivery = per-prompt `api poll`
  hook (ready-style). The zero-perch means this bind didn't fire/succeed on the wall-b spawn.

## Tasks
1. **ROOT-CAUSE the zero-perch (before fixing).** Reproduce `spt endpoint run claude-spt <disp-id>`
   and trace, in order:
   - Does the broker-spawned `claude` LOAD the sptc plugin at all? (If the operator's spawn used a
     claude config without sptc active, no hook fires — that's the bug, and the fix is ensuring the
     launcher's claude loads the plugin, e.g. via config/env, OR documenting the requirement.)
   - Is `SPT_ENDPOINT_ID` actually present in the spawned env? (Confirm `[env]` inject reaches the
     PTY process + the hook's `$SPT_ENDPOINT_ID`.)
   - Does `sptc_register_verb` return `bind`? (source value on an endpoint-run SessionStart — is it
     `startup`/other, not `clear`/`compact`? confirm the branch is reached.)
   - Does `spt api bind --adapter claude-spt <id> --set-session-id <sid>` SUCCEED in the broker-spawn
     context? The `bind` auth is "broker parentage is the credential" — verify it isn't failing auth
     (no broker parent detected) and getting `|| true`-swallowed. Temporarily drop the `2>&1 || true`
     to surface the real error during the trace.
   - HARD PART: endpoint-run spawns an interactive PTY — need a way to drive/inspect it headlessly
     (a throwaway disposable id, capture the spawned session's hook stderr, inspect owlery/ for the
     perch). Mind REQ-HAZARD-PERCH-COLLISION (disposable id only, never a live agent's).
2. **FIX** per the root-cause. Likely candidates: (a) the bind branch needs the real error surfaced +
   a retry / correct flags; (b) the launcher's claude must be guaranteed to load the sptc plugin;
   (c) the bind may need to ALSO arm next-turn poll delivery (confirm the poll hook drains for a
   bound-but-never-/sptc:ready perch). Match surrounding hook style; keep `bind`/`boundary` explicit
   `--adapter` (per the 0.9.0 PREP-4 scope).
3. **ADD THE INT (the real gap).** `ci/...` new int: `spt endpoint run claude-spt <disp-id>` (or the
   closest headless-drivable equivalent) → assert (a) a bound perch exists on disk under owlery/, (b)
   it's reachable — a queued `spt send <disp-id>` drains via the next `api poll`. Gated
   `SPTC_ACCEPTANCE=1`, disposable id, full teardown. This int would have caught the zero-perch.
   Tag `[int->REQ-DIST-SHORTCUT-BASENAME]` (or mint a dedicated REQ for cc-launcher-reachability if
   the surface warrants — check traceable-reqs; REQ-DIST-SHORTCUT-BASENAME is currently doc/impl/unit,
   so activating its `int` stage or minting REQ-CC-LAUNCHER-BIND is the traceable move).
4. **Gate:** `sh ci/run-gates.sh` + `traceable-reqs check` green + the new int passes on live spt.
5. **Ship** as a patch (likely v0.3.1) if it changes session-start.sh (cplugs skeleton structural →
   plugin.json bump + republish) — confirm release shape with doyle. If it's manifest/strings only,
   adapter-release only.

## Open design question for doyle (if root-cause = "bind works but not enough")
If the bind fires fine and the gap is purely "reachable-next-turn isn't immediate," confirm with
doyle that ready-style (next-turn poll) is the accepted NOW behavior (it is, per his sequencing) and
the immediate-live case is his spt-core end-state — so we do NOT try to force an active relay into the
interactive PTY.

## Context
v0.3.0 SHIPPED + parity arc CLOSED (PREP4-PLAN.md, memory prep4-v030-parity-closed). This is the
wall-b follow-up — a pre-existing untested path (M12 cc-launcher), NOT a v0.3.0 regression.
