# Profile-qualified string resolution — the hook has never read an overlay

> JIT plan (AGENTS.md §Plans). Gate for every task: `sh ci/run-gates.sh` PASS **and**
> `traceable-reqs check` exit 0. Mirror-excluded by the root JIT-plan predicate on its name alone.
>
> Opened 2026-09-09 from an operator field report, ahead of `BUSY-RACE-PLAN.md`'s remaining steps
> (which are waiting on wall-clock accumulation anyway).

## The defect

perri, doyle and flynn all recharged; all three set the session colour to **purple** — flynn on
`:ccs`, doyle and perri on `:alt`. Purple is the **base** value, and every profile overrides it, so
none of the three should have been purple.

`spt adapter get-string` takes `<adapter>[:profile]` and resolves the overlay itself. Measured:

| option | value |
|---|---|
| `claude-spt` | purple |
| `claude-spt:alt` | green |
| `claude-spt:ccs` | orange |

`hook.rs:37` hardcodes `const ADAPTER: &str = "claude-spt"` and passes that bare name at **all 13**
`get_string` call sites; `plugin/sptc/hooks/dispatch.sh:44` hardcodes it too. **The hook has never
once resolved a profile overlay.**

So the bug is not the colour — the colour is the one overlay visible enough to notice. Every
`[profiles.*.strings]` key is affected: today `session_color` and `adapter_label`, on both profiles.

**Why it shipped looking right.** The manifest comment beside `session_color` says it is "Read
hook-side (where the profile overlay is visible)" — the intent was correct and the resolution
silently dropped it. `REQ-SESSION-COLOR`'s unit test asserts the *payload shape*
(`rename_payload(name, "purple")` in, JSON out) and never the *resolution*, so it stayed green
through a defect that made the feature inert for every profiled endpoint. **A test that supplies the
value it is checking cannot see a resolution defect** — the same family as
`REQ-HAZARD-DIGEST-ROOT-BLIND`.

## The fix

`spt api endpoint-info` is a documented, read-only public call and already answers with the bound
option (`"adapter": "claude-spt:alt"`). No on-disk internals, no spt-core source.

1. Resolve the option from `endpoint-info` once, at SessionStart, and cache it the way
   `SPTC_HOOK_BIN` is already cached — an extra `spt` spawn on **every** hook is latency on exactly
   the path `#32` is measuring, where busy already runs to p90 1083 ms.
2. **Degrade to the bare name, never to nothing.** A qualified option that does not resolve makes
   every `get_string` return empty — no skills, no briefs, no hints. That failure is far worse than
   the defect being fixed, so the resolved option is *probed* against a key known to exist before it
   is adopted, and anything unexpected falls back to base.
3. `SPT_ADAPTER` (exported at `hook.rs:2439` from the same bare const) should carry the qualified
   option too — it is the value an agent reads to learn what it is running on, and today it lies by
   omission.

## Settled while building (both questions below, answered)

- **A hook that fires before the endpoint is bound is fine.** `endpoint-info` simply fails, and the
  resolution degrades to the bare name — which *is* the pre-fix behaviour, so the worst case for an
  unbound hook is exactly what shipped in 0.39.0. That is why the fallback direction is fixed: base,
  never empty.
- **`adapter_label` is not read by the hook at all.** Enumerating the call sites, the hook only reads
  `session_color`, `briefs.*` and `skills.*`; the label is core-side (registration/observability).
  So the only overlay this fix makes live *today* is the colour, and no label surface changes
  underneath anyone. The seam is fixed for whatever gets overlaid next.
- **The dispatcher keeps the bare name deliberately.** `dispatch.sh` reads `hook_cmd`, a base key no
  profile overlays, and touching the plugin would force a skeleton republish (the #35 trap) for no
  behaviour change. Left alone on purpose.
- **The resolved option is proven, not trusted.** An unknown profile fails LOUDLY but on **stderr**
  (`ADAPTER_STRING_FAIL:…`), with stdout EMPTY — and `get_string` reads stdout and ignores the exit
  code, so a bad option would silently empty every lookup rather than erroring. Hence the probe.
  Measured, not assumed: `spt adapter get-string claude-spt:nosuchprofile session_color` → exit 1,
  stdout `''`, token on stderr.

## Original open questions (kept for the record)

- Does any hook fire **before** the endpoint is bound (pre-`/sptc:setup`, or SessionStart's own seed)?
  If so the resolution must be lazy-and-cached rather than SessionStart-only, and the fallback path
  is the normal path rather than the error path.
- Is `adapter_label` consumed anywhere that would visibly change for `:ccs` / `:alt` users? The fix
  makes two overlays live at once; the colour is wanted, and the label change should be checked
  rather than discovered.

  **ANSWERED 2026-09-09, post-v0.39.1 — no visible change, nothing to discover.** A repo-wide scan
  (every file, not a `src/`-only grep) puts `adapter_label` in exactly three places: the manifest
  that declares it, the plans that discuss it, and three CI ints carrying four assertions between
  them (`ci/manifest/registration-int.sh` asserts both base and `:ccs`;
  `ci/publish/release-acquire-int.sh` and `ci/setup/activate-int.sh` assert base) — each of which
  passes its option **explicitly** and asserts a literal. **No hook call site, no
  runtime path, no rendered surface reads it.** It is what the manifest comment beside it already
  called it: an observability leaf, a resolve-proof key for the registration int. So the second
  overlay going live alongside `session_color` changed nothing a user sees, and none of those int
  assertions move (they name their own option, so the base ones still read base).

  Field values now resolving: base `Claude Code (spt)` · `:alt` `Claude Code (spt, alt account)` ·
  `:ccs` `Claude Code (spt, ccs)`.

## Requirement

Minted **with** the fix, not before, and it names what was measured: the hook resolves adapter
strings through the bound profile option. `REQ-SESSION-COLOR` stays as it is — its subject is the
rename payload — and gains the resolution test it never had.
