# JIT plan — PORTER bind seam (#205/#204) + trust-warning carry (releases#170)

Two independent bodies of work routed by doyle 2026-08-21. Both land in
`tools/claude-spt/src/hook.rs`; neither depends on the other. Gate for both:
`sh ci/run-gates.sh` PASS + `traceable-reqs check` exit 0 before the commit lands.

## W1 — the trust warning must survive our re-render (releases#170)

**Contract (as routed; the doc section is NOT yet on this node — see "open" below).** A
delivered message from a warning-eligible stranger now carries its trust warning on its own
envelope as a `trust-warning` attribute, composed by the RECEIVING node exactly as
`mnemonics-json` is. One arrival instead of two. Anything a *sender* writes into the attribute
is stripped at ingress, so the value is always the receiving node's own. Bodies that are already
typed envelopes are unchanged — the warning stays its own system-authored delivery, reserved
author, delivered first, no carrier.

**Why it is ours.** `hook::render_frames` re-renders every delivery (keeps `from` + body,
rebuilds as `<sptc_messages>`), so we hold custody of the envelope. This is the same seam that
dropped `mnemonics-json` in v0.26.2 (KNOWN-HAZARDS 2.15). For THIS attribute the routing
inverts the mnemonics-json precedent deliberately: **being ignored IS the failure**, because a
silently dropped trust warning is a security caution that never happened.

**Build.**

- `trust_warning_block(opening, from)` — mirror of `monic_reveal_block`: read the attribute,
  `attr_unescape`, emit a frame-level `<sptc_trust_warning from="…">` sibling block LEADING the
  message. Verbatim, never interpreted, never summarized, never renderer-truncated (spill owns
  size). Preserve, do not interpret — the same forward guidance the monics doc already gives.
- Ordering in `render_frames`: trust warning, then monics, then the message. The published
  invariant says the two can never co-occur (a matched monic suppresses the warning); ordering
  is fixed anyway so the co-occurrence case is deterministic rather than accidental.
- FAIL LOUD, never silent: an attribute that is present but empty/blank after unescape still
  renders a block, naming that the node composed an empty warning. The whole defect class here
  is a silent drop, so the fix may not introduce a quieter one.
- Digest: add `<sptc_trust_warning` to `digest::frame_spans` alongside `<sptc_monics>`. The
  caution is part of the DELIVERY, not decoration — a digest that shows the message without it
  shows the operator a delivery that never happened that way.
- Imitation surface: identical to the monic block and bounded the same way. A body can type the
  literal marker; the discriminant is STRUCTURAL — a genuine block sits at our frame level,
  ahead of and outside the peer's `<sptc_messages>` block, never inside it. Same reasoning core
  uses for the warning's reserved author. Documented, not defended against, per the accepted
  bounded risk already recorded for `REQ-MONIC-REVEAL`.
- Custody is already durable: `translate::park_envelope` writes the RAW envelope and both drains
  concat it, so the attribute reaches this renderer over the park/stub leg unchanged.

**ANSWERED by doyle 2026-08-21** (the contract as it will publish — the section is authored but
UNLANDED anywhere, riding an upcoming core assembly; re-check the page when it lands):

- **Value = the FULLY COMPOSED block, never a token.** The receiving node composes it whole:
  the identifying spine ("TRUST WARNING — this message is from `<peer>`, who reached you through
  an access rule rather than through anything you decided about them. You hold no note about
  them."), then the advisory line (a custom override may replace the advisory, NEVER the spine),
  then the how-to-silence line. Attr-escaped by the one composer. **Nothing to expand
  adapter-side, ever** — preserve, do not interpret, print verbatim after standard attr-unescape.
- **The cadence mark is made by the receiving node when the caution goes into the delivery
  channel** — the same act that hands the enveloped message over. There is no adapter-side
  acknowledgement in the loop, so an adapter that drops the attribute DOES silence that
  (session, peer) pair — **for that session only, not permanently**: the dedup marker lives in
  the per-session scratch, so any context reset that mints a new session owes the warning again.
  The marker is best-effort and fails toward warning again (a damaged marker costs a duplicate,
  never a silence), and a warning that could not be delivered at all claims nothing. Loss scope
  of an adapter drop = the remainder of that session for that peer. Scoped, still real.
- **SUPERSEDED, then RULED — newlines (Q3).** First answer: they ride LITERAL, guaranteed, and the
  defensive decode should be dropped. Re-measured at the lane tip that reversed: literal newlines are
  what the composer does TODAY but a **latent core defect**, because the envelope codec is line-framed
  and a multi-line attribute value tears it at any line-based reader (the "one line, always" pin only
  ever covered the monic attribute, whose newlines are a two-character JSON escape — so nothing had
  ever driven a real newline through the attribute layer). RULING, riding the same unlanded lane ahead
  of this feature: the attr codec gains exactly one entity - escape normalizes CRLF and CR to LF,
  then LF becomes `&#10;`, applied AFTER the `&` -> `&amp;` step; decode reverses it BEFORE the amp-last
  step, so literal `&#10;` content round-trips as `&amp;#10;`. Our decode is restored, scoped to the
  trust-warning attribute, and widens to the general attr unescape only when the docs-site attr-rule
  section publishes the fifth entity. `<br>` is never decoded in an attribute.
- **Keep the pairing.** The block must ride the SAME delivery as the message it is about — never
  batched, never reordered across deliveries. The caution must reach the agent WITH its message.

## W2 — the bind seam's two false surfaces (PORTER #205, member #204)

Full measurement: `BigscreenVR/spt-bs-releases` #204, `issuecomment-5367098271` (todlando),
v0.58.0 ER acceptance. Core half (bring-up anchors the endpoint skeleton) rides todlando's lane
in parallel; this half stands independently.

**Defect 1 — the perch claim is gated on the verb, not on registration.** SessionStart already
tracks `registered: Option<String>` (the id whose STRICT registration succeeded this
SessionStart, the sole license to advance the session carrier), but the brief below it branches
on `match verb { "bind" | "boundary" => … }`. So a REFUSED bind still emits the full
`<sptc-active-perch>` identity brief — a false claim, measured byte-for-byte in the field. The
brief arm also takes its id from `$SPT_ENDPOINT_ID` unverified, which is why this surface lies
where the every-turn reachability notice does not (that one resolves through `self_id` →
`whoami` keyed on the session, so a failed bind correctly yields no id and it self-gates).
Verified by inspection this session; **no widening beyond the brief is needed.**

- Fix: gate the perch claim on `registered`. Same rule the session carrier already follows.
- `bid.is_empty()` (no endpoint id resolvable) folds into the same no-perch outcome instead of
  emitting an empty brief.
- Also gate the bringup idle assert on registration: after a refused bind the perch still holds
  its PRIOR sid, so `state idle … --session-id <our sid>` can only AUTH_REFUSE. Skipping it
  costs the STUCK-ACTIVE protection nothing and removes a guaranteed-failing call.

**Defect 2 — a refused bind is silent at every surface anyone reads.** The refusal reason is
already in hand (`spt_strict` returns stderr text, `ANCHOR_REFUSED …` included) but goes only to
`env.log`, i.e. the spill file nobody reads. Measured case: `ANCHOR_REFUSED` asking for
`--subnet` on a 3-subnet node.

- Fix: on bind/boundary failure emit the **no-perch shape** carrying the refusal VERBATIM —
  `<sptc_perch_refused verb="…" endpoint="…">` naming that this session has no perch and why,
  then the ordinary `<sptc-reach>` ring brief. Carry the daemon's own bytes; invent no remedy
  (the measured refusal already names `--subnet` itself).
- A verdict may not assert more than it measured: the block says the registration was refused
  and quotes the refusal. It does not diagnose the node, and it does not claim the endpoint is
  gone.

**Contract sentences** (being added to the docs-site harness contract in todlando's lane, not
yet published on this node): the identity brief's perch claim is valid only after successful
registration; on bind failure the adapter emits the no-perch shape carrying the refusal. Build
against these; re-check the published page once todlando lands it.

## Requirements

Mint in `traceable-reqs.toml` BEFORE satisfying, activate on start (`doc`/`impl`/`unit`; `int`
added at the integrating wave):

- `REQ-TRUST-WARNING-CARRY` — the receiver-composed `trust-warning` envelope attribute survives
  the re-render and is surfaced at frame level. doc: KNOWN-HAZARDS. impl: `hook.rs`
  (`trust_warning_block` + the `render_frames` call site) + `digest.rs` `frame_spans`. unit:
  render cases (present/absent, verbatim incl. multi-line, empty-value loud, ordering with a
  monic, per-frame, body-imitation placement) + the digest span. int: a real warning-eligible
  delivery driven through BOTH the busy poll drain and the park/stub drain of the shipped hook
  binary — cross-node, since a warning-eligible stranger is by definition not same-node-trusted.
- `REQ-PORTER-PERCH-CLAIM-GATED` — the identity brief's perch claim is emitted only after a
  successful registration this SessionStart.
- `REQ-PORTER-BIND-REFUSAL-SURFACED` — a refused bind/boundary emits the no-perch shape naming
  the refusal, in the agent's context, not only the log.

## Tasks

1. Mint + activate the three REQs.
2. W2 impl + units (brief gated on `registered`; refusal block; idle assert gated).
3. W1 impl + units (`trust_warning_block`, call-site ordering, digest span).
4. KNOWN-HAZARDS entries for both (2.x carry-through invariant; a bind-seam claim invariant).
5. `sh ci/run-gates.sh` + `traceable-reqs check` → both green.
6. Commit. Release decision (and the `int` stages) separately — the cross-node int for W1 needs
   a warning-eligible stranger, which a single node cannot manufacture (same lesson as the
   v0.26.2 suppression measurement).
