# MONIC-REVEAL — the dropped `mnemonics-json` attribute

Routed 2026-08-05 by doyle (adapter DRI protocol, category 1 — the public contract already answers
it). Field RCA by hertz on `claude-spt` 0.26.1 (lia ↔ ball-b, both directions symmetric).

## The defect

spt-core composes a matched monic onto the delivery envelope as a `mnemonics-json` attribute
(docs-site `networking/monics.md`, `REQ-MONIC-DELIVERY-TRIGGER`). The published invariant:

> the two never disagree, so you can never be warned about a stranger whose monic you were handed in
> the same envelope

Our re-render manufactures exactly that disagreement. Core's warning suppression works (it read the
store and matched), and the adapter then renders the delivery with `from` + body only — so the
session sees **no trust warning and no monic**: a classified peer reads as an ordinary sender.

**Where it actually is** (measured, not assumed): custody is fine — `translate::park_envelope` writes
the RAW envelope and the drains concat it verbatim. The single drop site is `hook::render_frames`,
which keeps `from` and the body and discards every other attribute. That one function serves BOTH the
busy poll drain and the idle stub/park drain (call sites: hook.rs 2382, 2402, 2839, 2852), so parity
is structural once it is fixed, not a second thing to maintain.

## The record shape

Observed live on this node (spt 0.55.0) before asking, by adding a content-trigger monic to `perri`
and draining the raw frame:

```
mnemonics-json="[{&quot;id&quot;:&quot;…&quot;,&quot;triggers&quot;:[{&quot;kind&quot;:&quot;content&quot;,&quot;pattern&quot;:&quot;…&quot;,&quot;regex&quot;:false}],&quot;text&quot;:&quot;…&quot;,&quot;set_ms&quot;:…,&quot;origin&quot;:&quot;manual&quot;}]"
```

XML-escaped `serde_json` of the matched `Vec<Monic>`, records WHOLE — `{id, triggers[], text, set_ms,
origin}`, no delivery projection. doyle confirmed the same shape from source and corrected the docs
page (the published example showed a superseded `{"peer": …}` key that does not exist in the record
— docs-site main @898f16f).

## Shape of the fix (doyle/hertz, with the interim boundary)

1. **Custody — durable.** The full parsed attribute set survives stub/park and reaches the renderer.
2. **Revelation — interim, minimal, SEPARABLE.** The renderer reveals the already-matched monic with
   its message. Operator ruling (routed today): the envelope attribute is the monic's *interim*
   adapter surface; when the now-signal funnel ships (milestone D), its MONICS category replaces this.
   Build it so the D migration is a REMOVAL, not surgery: one helper, one call site, one digest entry.
3. **Never re-scan the monic store adapter-side.** The match already rode the envelope; a second
   evaluation would be a second match rule, and core stores are not adapter surface.
4. **Exactly-once park commit and spill semantics unchanged.**
5. The rendered block carries the record's `text` **verbatim** — the agent's own words, never
   summarized, never renderer-truncated (spill owns size).

Rendering decision: a SIBLING `<sptc_monics from="…">` block emitted immediately BEFORE its
`<sptc_messages>` block — never spliced into the peer's body. Same reasoning core gives for the trust
warning: judgement text inside the sender's own text is indistinguishable from text the sender wrote.
Leading placement also means an over-cap spill keeps the monic in the inlined head.

Malformed/unknown payload (unparseable JSON, no `text` key) renders the raw attribute verbatim in the
same block rather than dropping it — the defect being fixed is a silent drop; the fix may not add one.

## Tasks

- [x] REQ minted: `REQ-MONIC-REVEAL` in `traceable-reqs.toml` (doc/impl/unit/int).
- [x] `hook.rs`: `monic_reveal_block()` + one call in `render_frames`.
- [x] `digest.rs`: `frame_spans` learns `<sptc_monics` (else the revelation is invisible to the RC
      view and to my own digest — the same half-population trap as before).
- [x] Units: reveal with text verbatim; ordering before the message; negative arm (no attr → no
      block, byte-identical to today); malformed JSON → loud verbatim; multi-record; multi-frame
      independence; parity by construction (one renderer).
- [x] Int (`ci/hooks/poll-int.sh`, SPTC_ACCEPTANCE): real core-composed envelope carrying a real
      matched monic, driven through BOTH the busy poll drain and the park/stub drain, asserting
      monic text + from + body in additionalContext; negative arm; oversized spill retains it.
- [x] `docs/KNOWN-HAZARDS.md` §2.15 + the hazard's REQ evidence.
- [x] Gate: `sh ci/run-gates.sh` PASS + `traceable-reqs check` exit 0 before the commit lands.
- [x] Shipped **v0.26.2** (`533da1d`, tag `v0.26.2`, private release + public mirror), installed on
      HFENDULEAM, and FIELD-VERIFIED on the shipped binary through a live perch: a content-trigger
      monic matched, and the block led the message with the record's text verbatim.
- [x] **The together-assertion — RUN AND GREEN** (2026-08-05, cross-node ENLYZEAM → HFENDULEAM,
      `ball-b` → disposable `perri-monic-e2e`, entry-admitted, same peer both legs; hertz coordinated
      ball-b, doyle second-read the reveal on his own perch).

## The together-e2e, as run (repeatable)

Why it cannot be built node-locally: a same-node arrival is ratified never-warning, and so is a
posture-open one — a local probe has no warning whose suppression it could observe. It needs a peer
admitted by an ACCESS ENTRY, from another node. Do NOT restrict a live perch to get this; a
restriction denies every unlisted peer for the window. Use a disposable:

```sh
spt api --adapter claude-spt seed --pid $$ --session-id perri-monic-e2e-sess
spt api --adapter claude-spt bind perri-monic-e2e --subnet SPT_MANTLE \
  --set-session-id perri-monic-e2e-sess --session-id perri-monic-e2e-sess
spt endpoint access allow perri-monic-e2e --endpoint <cross-node-sender> --surfaces ALL
# LEG 1 — no monic held. Peer sends. Drain through the SHIPPED hook binary (a bound perch spools;
# no listener is needed, and the hook binary IS the real render path):
printf '{"session_id":"perri-monic-e2e-sess","prompt":""}' | env -u SPT_ENDPOINT_ID -u OWL_SESSION_ID \
  … tools/claude-spt/target/release/claude-spt.exe hook UserPromptSubmit --host-pid $$
spt endpoint monic add --owner perri-monic-e2e --target e2e-<peer> \
  --triggers '[{"kind":"sender","pattern":"<peer>"}]' <<< "<classification>"
# LEG 2 — same peer sends again. Capture RAW first, then park it and drain, so both the wire bytes
# and the rendered result are observed:
raw=$(spt api --adapter claude-spt poll perri-monic-e2e --session-id perri-monic-e2e-sess --include-deferred)
printf '%s' "$raw" > "$HOME/.spt-claude/msgpark/perri-monic-e2e/0000000000002-000000.park"
# …then the same hook drain with prompt "<msg from=\"<peer>\"/>"
echo y | spt endpoint purge perri-monic-e2e --force     # teardown: purge, NEVER rm
```

**Measured:**

| leg | monic held | raw wire | rendered |
|---|---|---|---|
| 1 | none | warning frame `from="spt-access"` + the body frame | `<sptc_messages from="spt-access">` TRUST WARNING naming the peer, then the body. **No monic block** |
| 2 | `e2e-ball-b`, sender trigger | **ONE frame only — no warning frame** — carrying `mnemonics-json` | `<sptc_monics from="ball-b">` at FRAME level leading, text verbatim, then the body |

Suppression and revelation are therefore observed in ONE delivery, on the shipped 0.26.2 binary, and
leg 2 was drained through the PARK leg — so cross-node custody rides with it. The rendering half of
the pairing is additionally pinned by a local unit (`render_frames` renders a warning frame and a
monic-bearing frame independently and in order), so a renderer refactor cannot silently undo it
between cross-node runs.

## Not in scope

- Re-scanning or reading the monic store (explicitly forbidden).
- Rendering the OTHER dropped attributes (`type`, `notif_id`, `subnet`, `path`) — they carry no
  recipient-facing meaning today. §2.15 names the seam so the next one is not silently dropped.
