---
name: v0391-profile-string-resolution
description: "v0.39.1 SHIPPED + FIELD-VERIFIED 2026-09-09 (#36): hook passed the BARE adapter name to get-string, so every [profiles.*.strings] overlay was inert. ⭐ Replay the io-funnel to observe what a BOUNDARY emitted — the hook trace never carries SessionStart output."
metadata: 
  node_type: memory
  type: project
  originSessionId: f3635e58-bddb-43d0-8b7a-586a2afd835b
  modified: 2026-09-09T04:22:38.322Z
---

**v0.39.1 shipped and is FIELD-VERIFIED end-to-end** (commit `35d1d74`, tag `v0.39.1`, private
release + public mirror; verification recorded in `42e1f5d`). `#36` is `state: done`. NO skeleton
step — nothing under `plugin/` changed.

## The defect

Operator field report: perri, doyle and flynn all recharged and ALL THREE came back PURPLE — even
though flynn is `:ccs` while doyle and perri are `:alt`. Purple is BASE.

Root cause was MINE, not core's. `spt adapter get-string` takes `<adapter>[:profile]` and applies the
overlay itself; `hook.rs` passed the BARE name at all 13 call sites, so every `[profiles.*.strings]`
key had been inert on the hook path since the seam shipped. Core was resolving correctly the whole
time — it was never given a profile-qualified option.

Fix: resolve the option from `spt api endpoint-info` (documented, read-only — no core source, no
on-disk internals), PROVE it with a probe before adopting, memoise per process, cache to
`$CLAUDE_ENV_FILE` like `$SPTC_HOOK_BIN`, and export the qualified option on `$SPT_ADAPTER` too.
`REQ-PROFILE-STRING-RESOLUTION`. Design questions settled in `PROFILE-STRINGS-PLAN.md`.

⭐ **The fallback DIRECTION is load-bearing.** A bad option fails on STDERR with STDOUT EMPTY, and
`get_string` reads stdout and ignores the exit code — so a bad qualified option would silently empty
EVERY lookup (no briefs, no skills, no hints). Degrade to the BARE name, never to nothing. Measured,
not assumed: `get-string claude-spt:nosuchprofile session_color` → exit 1, stdout `''`.

⭐⭐ **A test that supplies the value it is checking cannot see a resolution defect.**
`REQ-SESSION-COLOR`'s unit test fed `rename_payload(name, "purple")` in and asserted the JSON shape
out — it never exercised the RESOLUTION, so it stayed green through a defect that made the feature
inert for every profiled endpoint. Mutation-check any test written for a resolution bug: revert the
fix and watch it fail. Same family as `REQ-HAZARD-DIGEST-ROOT-BLIND` and [[v0261-hints-never-called]].

## ⭐⭐ How to observe what a BOUNDARY emitted (reusable recipe)

The colour is emitted as a `/color` half of the post-clear rename — a TUI display fact I cannot see
from inside the session. Two instruments, only one of which covers it:

- **The hook trace does NOT work.** Grepping `hook-trace.log` for `color` returns ZERO — and that
  zero means NOTHING. Enumerating the trace's line shapes shows it carries only `TRACE
  UserPromptSubmit`, commune-frame, and NOTIFICATION lines. It has never carried SessionStart
  rename/colour output at all. Population never covered ⇒ a clean-looking result.
- **The io-funnel DOES.** The boundary rename ships via `spt send --json-payload`, so it is a
  `MSG_OUT` frame. `spt api io-events <id> --after <seq> --json` replays it and the payload is
  directly readable. `--after` writes no cursor, so the replay disturbs nothing.

Field proof captured this way, same endpoint, same payload shape, two consecutive boundaries:

    seq 25  @ 2026-09-09T02:03Z  (v0.39.0)  {"rename":"v1","name":"…","color":"purple"}   ← the defect
    seq 154 @ 2026-09-09T03:59Z  (v0.39.1)  {"rename":"v1","name":"…","color":"green"}    ← :alt overlay

Plus `$SPT_ADAPTER` reading `claude-spt:alt` (was bare `claude-spt`). NOT observed, and stated as
such on the issue: the RENDERED TUI colour. Emission is proven; painting is the operator's eye.

## adapter_label — checked, not discovered

v0.39.1 made `adapter_label` live at the same time as `session_color`, and the plan flagged that the
second overlay wanted checking. A repo-wide scan (every file, not a `src/`-only grep) finds NO hook
call site, NO runtime path, NO rendered surface — only three CI ints carrying four assertions, each
passing its option explicitly. It is the observability leaf the manifest already called it, so
nothing user-visible changed. Values: base `Claude Code (spt)` · `:alt` `Claude Code (spt, alt
account)` · `:ccs` `Claude Code (spt, ccs)`.

## ⭐ Instrument trap paid for again this session

`sh ci/run-gates.sh 2>&1 | tail -25` run in the BACKGROUND writes NOTHING until it exits — `tail`
buffers the whole stream. I read the zero-byte output file as a possible stall and killed a perfectly
healthy gate run. **Zero bytes from a pipe-through-`tail` is not evidence of a stall.** Redirect the
gate to a log file unbuffered and watch THAT (a Monitor greping `=== GATE|^FAIL|^EXIT=` covers
progress AND failure, not just the happy path). Same family as [[msys-grep-instrument-traps]] and
[[node-local-book-gate]]: validate the instrument before reading its silence.

Related: [[v0390-recharge-trust-trace]], [[v0300-seal-carry]], [[v0310-attr-passthrough]],
[[signature-match-is-not-root-cause]], [[list-vs-predicate-assertions]].
