# Harness-hosted bringup: an adapter-agnostic seed + bind-time adapter/profile resolution

## Status

accepted (2026-06-16)

## Context

A harness-hosted SPT session (the harness binary — e.g. `claude` — is
user-launched and is spt's parent; today's Monitor model) brings a live (or
ready) agent up through the seed→listen handshake (CONTEXT §Startup flows,
REQ-START-2): the SessionStart hook calls `spt api seed --pid <parent_pid>`, the
daemon holds an in-memory seed keyed by that pid, and the agent's
`$LIVE start <id>` alias = `$SPT listen <id>` self-discovers its `parent_pid`,
matches the seed, binds the perch, and relays.

Two problems surfaced when **perri** drove `/sptc:live` on `spt-claude-code`
v0.2.0 (2026-06-16):

1. **`--adapter` is required on every `api` call** (`api/mod.rs`:
   `ApiArgs.adapter: String`, non-optional). So both `seed` and `listen` are
   forced to pass `--adapter claude-spt:live`. But on `listen` the flag is
   **dead**: `bind_from_seed` already resolves the adapter *from the seed* by
   `parent_pid` (`startup.rs`: "the seed carries the creating adapter — the
   adapter-at-creation rule"), ignoring the flag. The mandatory flag breaks the
   legacy-parity target — legacy `$LIVE start wall-a` → `$SPT listen wall-a`
   carries no adapter — and pushes adapter-side complexity (the seed must be
   passed an adapter; seed+listen must be chained in one background process so
   they share a pid; etc.).
2. **The seed snapshots a single adapter** at seed-time. A machine that hosts
   more than one harness adapter for the same binary, or that `adapter add`s one
   *after* SessionStart, cannot be served by a single generic SessionStart hook.

The desired end state: a SessionStart hook that is **identical for every adapter
profile** of a harness binary — `spt api seed --pid <pid> --session-id <sid>`,
nothing adapter-specific — with the daemon resolving *which* adapter/profile owns
the session at the moment it binds.

## Decision

**1. The seed is adapter-agnostic.** `api seed` carries only `parent_pid` +
`session_id` (+ optional `cwd`). No `adapter_name`. The seed is a pure *"a
harness session exists at this pid"* record. (Removes the single-adapter snapshot
and the `--adapter` requirement on `seed`.)

**2. `--adapter` becomes an optional override** across the `api` group (an
explicit `name[:profile]` for adapter dev/iteration), never required. Omitted,
the adapter/profile is **resolved at bind** as a pure read against the live
registry — never a seed-time value that can drift. Applies to both LiveAgent
(`listen`) and ReadyAgent (`poll`) bringup.

**3. The match-key is a new `[adapter] host_binaries` manifest field** — the
executable basenames a `kind="harness"` adapter hosts agents inside (e.g.
`host_binaries = ["claude"]`). Bind-time resolution:
- the seed's `parent_pid` → that process's executable **basename**
  (case-insensitive, `.exe`-stripped);
- **candidate adapters** = registered harness adapters whose `host_binaries`
  contains that basename;
- zero candidates → a friendly error naming the binary and the `--adapter`
  escape hatch.

**4. Profile selection: an explicit pointer, with a `registered_at_ms`
fallback.**
- A durable **active-profile pointer** — `$SPT_HOME/adapters/active-profiles.toml`,
  a flat `host_binary → "adapter[:profile]"` map — is the primary. Read at bind.
- **Unset** → fall back to the candidate adapter with the greatest
  `registered_at_ms` (the recorded registration timestamp on `record.toml`,
  sturdier than fs-mtime), **base profile** (a specific *profile* is only ever
  chosen by the pointer); name-ascending on ties. So a fresh single-adapter
  install "just works" with no pointer.
- The pointer is written **only** by `spt adapter use <adapter>[:profile]`
  (`--clear` to drop). **Never auto-written** by install / update / `adapter add`
  — that is precisely what would let an update silently flip the active profile.
  A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored,
  fall back, warn once. Pruned on `adapter remove`.
- Stored at the registry root, **sibling to the per-adapter `<name>/` dirs**, so
  `adapter add/update/remove` (which only rewrite a `<name>/` subdir) can never
  clobber it.

**Storage asymmetry:** the **seed** is ephemeral (in-memory, daemon-held,
per-session); the **pointer** is durable (on-disk, a standing user preference).

## Consequences

- Legacy parity restored: `$LIVE start <id>` → `$SPT listen <id>` with no
  mandatory `--adapter`. A generic SessionStart hook —
  `spt api seed --pid <pid> --session-id <sid>` — works for every profile of a
  harness binary.
- Adapter authors declare `host_binaries` and call `spt adapter use
  <adapter>[:profile]` once per unique host binary they support (mdBook guides
  this). Without a pointer, the freshest-registered matching adapter's base
  profile is the default.
- New surfaces are additive + N-1-safe: `host_binaries` is an optional manifest
  field (omitted-serialized, like `shortcut_basename`); `active-profiles.toml` is
  a new optional file; `--adapter` relaxes from required to optional (no break for
  callers that still pass it).
- An explicit profile choice survives adapter updates (the pointer is immune to
  `registered_at_ms`/mtime churn) — the footgun pure-mtime selection would have
  introduced is closed.

## Alternatives considered

- **Seed carries the matched-adapter *set*** (resolve at seed-time): rejected —
  a seed written before an `adapter add` misses the new adapter; resolution as a
  bind-time read is drift-free and strictly simpler.
- **Derive the match-key from `[session.self]`** instead of a new field:
  rejected — `[session.self]` is the *spt-launches-harness* command (spt-hosted
  direction); harness-hosted is the inverse, so no existing token reliably names
  the parent binary, and an explicit field gives a precise "no adapter for this
  binary" error and multi-alias support.
- **Pure fs-mtime selection + `touch` to change** (the first proposal):
  rejected as the *primary* — `adapter update`/`add` rewrites manifests and bumps
  mtime, silently flipping the active profile. Kept only as the spirit of the
  unset-fallback, now hardened to `registered_at_ms` + an explicit pointer.
- **Auto-write the pointer on install/update**: rejected — re-introduces the
  update-churn footgun; absence-means-heuristic is the correct default.
