#!/bin/sh
# Integration proof for the UserPromptSubmit message-drain path against a REAL spt-core (>= v0.7.1):
# the published `api poll` surface emits the canonical self-delimiting <EVENT> envelope,
# and our hook parser (render_frames) decodes it correctly. This is the confirm-match that closes
# REQ-MSG-ENVELOPE end-to-end: design -> impl -> ship -> real-surface-verify. F-002 (no inter-frame
# delimiter) is dissolved — multi-message drains split cleanly on </EVENT>, no __REPLY_TO__ relic.
# [int->REQ-DIST-HOOKS-API] [int->REQ-UPS-INJECTION]
#
# v0.7.1 is the floor: the <EVENT> poll surface ships in 0.7.1; 0.7.0 still emits the __REPLY_TO__
# relic, so this SKIPs there. Mutates node-local perch/spool state (a throwaway perch, torn down on
# exit), so it is gated behind SPTC_ACCEPTANCE=1. Idempotent.
# Run: SPTC_ACCEPTANCE=1 sh ci/hooks/poll-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
. "$ROOT/ci/lib/spt-probe.sh"   # sptc_flag_supported — capability probes WITH a positive control
ADAPTER=claude-spt
# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION: a colliding id tears
# down the live agent's perch + poll stream, name-keyed last-establish-wins).
BID=sptc-poll-int
BSID=sptc-poll-int-sess

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates perch/spool state)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
case "$ver" in
  0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.1 (<EVENT> poll surface ships in 0.7.1; older emits the __REPLY_TO__ relic)"; exit 0 ;;
esac

fail=0
SPILLHOME=
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Tear down the throwaway perch — and the sandboxed spill HOME — however we exit.
trap 'spt endpoint shutdown "$BID" >/dev/null 2>&1 || true; spt endpoint stop "$BID" >/dev/null 2>&1 || true; [ -n "$SPILLHOME" ] && rm -rf "$SPILLHOME"' EXIT INT TERM

# Establish a pollable perch (non-blocking: seed + bind, no listener -> sends spool).
# A node holding more than one subnet cannot infer a NEW endpoint's home, and spt refuses to guess
# (HOME_REFUSED) — so declare one for the throwaway perch: $SPTC_INT_SUBNET when the operator names
# it, else the first subnet this node holds (any is correct for a node-local probe).
spt api --adapter "$ADAPTER" seed --pid $$ --session-id "$BSID" >/dev/null 2>&1
subnets=$(spt subnet --json 2>/dev/null | sed -n 's/.*"name": *"\([^"]*\)".*/\1/p')
subnet="${SPTC_INT_SUBNET:-}"
if [ -z "$subnet" ] && [ "$(printf '%s\n' "$subnets" | grep -c .)" -gt 1 ]; then
  subnet=$(printf '%s\n' "$subnets" | head -1)
fi
if [ -n "$subnet" ]; then
  bound=$(spt api --adapter "$ADAPTER" bind "$BID" --subnet "$subnet" --set-session-id "$BSID" --session-id "$BSID" 2>&1)
else
  bound=$(spt api --adapter "$ADAPTER" bind "$BID" --set-session-id "$BSID" --session-id "$BSID" 2>&1)
fi
case "$bound" in *BOUND:"$BID"*) ok "perch bound (non-blocking)" ;; *) bad "bind failed: $bound"; echo "MSG-ENVELOPE-INT FAIL"; exit 1 ;; esac

# Drain & discard any resurfaced notifications so the assertion sees only our test message.
spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" >/dev/null 2>&1

# Send one message with body specials (newline + < > & ") from a known sender, then drain raw.
printf 'hello from probe<NL>second <line> & "stuff"' | sed 's/<NL>/\
/' | spt send "$BID" --from probe-int >/dev/null 2>&1
raw=$(spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" 2>&1)

# 1. Canonical <EVENT type="msg" from=…> envelope with correct body escaping; no relic.
expected='<EVENT type="msg" from="probe-int">hello from probe<br>second &lt;line&gt; &amp; &quot;stuff&quot;</EVENT>'
case "$raw" in *"$expected"*) ok "api poll emits canonical <EVENT> msg envelope (escaped body)" ;; *) bad "envelope mismatch; raw=[$raw]" ;; esac
case "$raw" in *__REPLY_TO__*) bad "raw drain still carries the __REPLY_TO__ relic" ;; *) ok "no __REPLY_TO__ relic (canonical poll envelope shipped)" ;; esac

# 2. Hook BINARY confirm-match (D1): the real `claude-spt hook UserPromptSubmit` drains the live perch
#    and renders the canonical <EVENT> drain to our <sptc_messages> additionalContext shape — the same
#    parser, now in the binary (was render_frames in _common.sh). Send a fresh message with body
#    specials, run the hook (it resolves the perch via whoami off OWL_SESSION_ID, marks busy, polls
#    --include-deferred, renders), then re-idle + drain residue so step 3's plain poll stays clean.
#    [int->REQ-DIST-HOOK-BINARY] [int->REQ-UPS-INJECTION]
HOOKBIN="$ROOT/tools/claude-spt/target/release/claude-spt.exe"
[ -x "$HOOKBIN" ] || HOOKBIN="$ROOT/tools/claude-spt/target/release/claude-spt"
# Run the hook binary with the AMBIENT perch env scrubbed. Whoever runs this script is very often
# an spt-hosted agent, and its shell exports $SPT_ENDPOINT_ID / $OWL_SESSION_ID for its OWN perch:
# inherited, the hook resolves the RUNNER's identity, marks it busy, and drains its inbox and msg
# park into this script's discarded stdout — the probe eats the operator's mail. Scrubbed, the
# binary resolves the throwaway perch the way a real session does (whoami off the stdin session id).
# `$HOME` is overridable per call so the spill step can sandbox the files it writes.
hookrun() { # hookrun <event> <stdin-json> [home]
  if [ -n "${3:-}" ]; then
    printf '%s' "$2" | env -u SPT_ENDPOINT_ID -u OWL_SESSION_ID -u SPT_AGENT_ID -u SPT_SESSION_NAME \
      -u SPT_HOST_PID -u SPT_ADAPTER HOME="$3" "$HOOKBIN" hook "$1" --host-pid $$ 2>/dev/null
  else
    printf '%s' "$2" | env -u SPT_ENDPOINT_ID -u OWL_SESSION_ID -u SPT_AGENT_ID -u SPT_SESSION_NAME \
      -u SPT_HOST_PID -u SPT_ADAPTER "$HOOKBIN" hook "$1" --host-pid $$ 2>/dev/null
  fi
}
if [ -x "$HOOKBIN" ]; then
  printf 'hello from probe<NL>second <line> & "stuff"' | sed 's/<NL>/\
/' | spt send "$BID" --from probe-int >/dev/null 2>&1
  # The binary resolves the perch via `spt whoami` with OWL_SESSION_ID set from the stdin session_id.
  rendered=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}")
  want=$(printf '<sptc_messages from="probe-int">\nhello from probe\nsecond <line> & "stuff"\n</sptc_messages>')
  case "$rendered" in *"$want"*) ok "binary hook confirm-match: live drain -> <sptc_messages>" ;; *) bad "binary render mismatch; got=[$rendered]" ;; esac
  # Re-idle the perch (the hook marked it busy) and drain any residue so step 3 is unaffected.
  spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
  spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred >/dev/null 2>&1
else
  echo "SKIP: claude-spt binary not built (cargo build --release) — binary hook confirm-match needs it"
fi

# 3. Multi-message: two sends drain as two whole envelopes (self-delimiting; F-002 dissolved).
printf 'one' | spt send "$BID" --from alice >/dev/null 2>&1
printf 'two' | spt send "$BID" --from bob >/dev/null 2>&1
multi=$(spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" 2>&1)
n=$(printf '%s\n' "$multi" | grep -c 'type="msg"')
[ "$n" -eq 2 ] && ok "multi-message: 2 whole <EVENT> envelopes split on </EVENT>" || bad "expected 2 msg envelopes, got $n"

# 4. TWO oversized deliveries in ONE session — the end-to-end proof for the spill pair, driven
#    through the SHIPPED binary against the REAL perch and asserted against the files that actually
#    landed on disk (the unit tests hold the same properties against a fake env).
#    REQ-HAZARD-SPILL-CLOBBER: every spill gets its OWN file, so a second oversized delivery cannot
#    destroy the first — the field loss was a SessionStart brief overwritten UNREAD by a later
#    broadcast, with nothing to say it had ever existed.
#    REQ-HAZARD-SPILL-NOTICE-LOST: the pointer that makes an unread spill visible is itself always
#    delivered — it NAMES the earlier spill, it fits under CC's real additionalContext limit, and
#    the PreToolUse mid-turn drain (the highest-volume spill path) carries the same earlier-spills
#    line the UserPromptSubmit path does.
#    [int->REQ-HAZARD-SPILL-CLOBBER] [int->REQ-HAZARD-SPILL-NOTICE-LOST]
CC_LIMIT=2000   # CC's real additionalContext ceiling; the adapter caps under it (DEFAULT_CAP 1800).
spilled_to() { # the spilled_to="…" path of a pointer (the PreToolUse envelope is JSON — unescape first)
  printf '%s\n' "$1" | sed 's/\\\\/\\/g; s/\\"/"/g' | grep -o 'spilled_to="[^"]*"' | head -1 | sed 's/^spilled_to="//;s/"$//'
}
topath() { cygpath -u "$1" 2>/dev/null || printf '%s' "$1"; }
# Windows paths reach these assertions three ways in one run — as typed into $HOME, as Rust's
# Path::join wrote them (mixed separators), and JSON-escaped inside the PreToolUse envelope. Compare
# on a separator- and escape-normalized form so the assertion is about WHICH FILE is named, not
# about how the path was spelled.
slashed() { printf '%s\n' "$1" | sed 's/\\\\/\//g; s/\\/\//g'; }
if [ -x "$HOOKBIN" ]; then
  # Spills land in $HOME/.claude — sandbox HOME so the probe never writes into the operator's real
  # ~/.claude and the directory scan sees only this run's files. Native (mixed) path: the binary is
  # a Windows build when this runs under Git Bash, and an MSYS /c/… path is not one it can write.
  SPILLHOME=$(mktemp -d 2>/dev/null || printf '%s' "${TMPDIR:-/tmp}/sptc-spill-int.$$")
  mkdir -p "$SPILLHOME/.claude"
  NHOME=$(cygpath -m "$SPILLHOME" 2>/dev/null || printf '%s' "$SPILLHOME")
  # ~3 KB of body per delivery — over the adapter's cap, so each one spills.
  pad=$(awk 'BEGIN{s="";while(length(s)<3000)s=s "spill-pad-";print substr(s,1,3000)}')
  drained() { spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1; }

  printf 'SPILL-ONE %s' "$pad" | spt send "$BID" --from probe-int >/dev/null 2>&1
  out1=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
  drained
  printf 'SPILL-TWO %s' "$pad" | spt send "$BID" --from probe-int >/dev/null 2>&1
  out2=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
  drained
  # Seed 40 NEWER sibling spills before the third delivery: a long live session accumulates them,
  # and naming all of them is what pushed the pointer past CC's limit (measured: 40 priors -> a
  # 4310-byte notice). The notice has to bound ITSELF — oldest names kept, the rest counted — so the
  # two files that matter here (the real ones) stay named. Newer than the real spills on purpose:
  # the bound drops the NEWEST first. (route (c) of the notice hazard)
  i=0
  while [ "$i" -lt 40 ]; do
    : > "$SPILLHOME/.claude/sptc-drain-$BSID-19999999999$i-4242.txt"
    i=$((i + 1))
  done
  # The third rides PreToolUse — the mid-turn drain used to be the one path whose pointer never
  # mentioned an earlier spill (route (b) of the notice hazard).
  printf 'SPILL-THREE %s' "$pad" | spt send "$BID" --from probe-int >/dev/null 2>&1
  out3=$(hookrun PreToolUse "{\"session_id\":\"$BSID\",\"tool_name\":\"Read\",\"tool_input\":{}}" "$NHOME")
  drained
  spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred >/dev/null 2>&1

  f1=$(spilled_to "$out1"); f2=$(spilled_to "$out2"); f3=$(spilled_to "$out3")
  p1=$(topath "$f1"); p2=$(topath "$f2"); p3=$(topath "$f3")
  if [ -z "$f1" ] || [ -z "$f2" ] || [ -z "$f3" ]; then
    bad "an oversized delivery did not spill (pointers: 1=[$f1] 2=[$f2] 3=[$f3]); out1=[$out1]"
  else
    # Unique paths, all three files on disk: no delivery overwrote another's.
    if [ "$f1" != "$f2" ] && [ "$f2" != "$f3" ] && [ "$f1" != "$f3" ]; then
      ok "three oversized deliveries -> three distinct spill files"
    else
      bad "spill paths collided: [$f1] [$f2] [$f3]"
    fi
    if [ -f "$p1" ] && [ -f "$p2" ] && [ -f "$p3" ]; then
      ok "every spill file exists on disk (the pointer names something readable)"
    else
      bad "a pointer names a file that is not there: [$p1] [$p2] [$p3]"
    fi
    # The first delivery's body is still intact AFTER two later spills — the clobber itself.
    if grep -q 'SPILL-ONE' "$p1" 2>/dev/null && grep -q 'SPILL-TWO' "$p2" 2>/dev/null; then
      ok "the first spill survives two later oversized deliveries, body intact"
    else
      bad "an earlier spill was destroyed or truncated by a later delivery"
    fi
    # The pointer resurfaces what came before it — on BOTH channels.
    # Both halves are load-bearing: the earlier-spills SENTENCE (a pointer that merely repeats its
    # own path names the first spill for free once the paths collide) and the first spill's path.
    n_f1=$(slashed "$f1"); h2=$(slashed "$out2"); h3=$(slashed "$out3")
    case "$h2" in *"earlier deliver"*"$n_f1"*) ok "the second pointer NAMES the first spill as an earlier one" ;; *) bad "second pointer never surfaces [$f1] as an earlier spill; out2=[$out2]" ;; esac
    case "$h3" in *"earlier deliver"*"$n_f1"*) ok "the PreToolUse pointer carries the earlier spills too" ;; *) bad "mid-turn pointer never surfaces [$f1] as an earlier spill; out3=[$out3]" ;; esac
  fi
  # And the notice itself is deliverable: over CC's limit and CC truncates the very notice whose
  # job is to prevent silent truncation.
  n1=${#out1}; n2=${#out2}; n3=${#out3}
  if [ "$n1" -le "$CC_LIMIT" ] && [ "$n2" -le "$CC_LIMIT" ] && [ "$n3" -le "$CC_LIMIT" ]; then
    ok "every pointer fits under CC's ${CC_LIMIT}-byte additionalContext limit ($n1/$n2/$n3)"
  else
    bad "a pointer exceeds CC's ${CC_LIMIT}-byte limit ($n1/$n2/$n3) — CC would truncate the notice"
  fi
  # …and it fits by BOUNDING, not by dropping the priors it could not name: the remainder is counted
  # and the recovery glob named, so an unnameable spill is still an observable one.
  case "$out3" in *"more not listed here"*"sptc-drain-*"*) ok "the over-full pointer counts the spills it cannot name" ;; *) bad "42-prior pointer neither named nor counted the remainder; out3=[$out3]" ;; esac
  # 5. An INHERITED identity does not act. This probe's own first version was the specimen: run from
  #    a live agent's shell, the hook took the RUNNER's ambient perch env and drained the runner's
  #    inbox instead of the throwaway perch's. Here the ambient env names a REAL perch (this
  #    script's own) while the payload names a different session — the shape of a Claude Code
  #    session launched from a perched agent's tool call. The hook must refuse the inherited
  #    identity and touch nothing; the message must still be there for the session that owns it.
  #    [int->REQ-HAZARD-INHERITED-IDENTITY-ADOPTION]
  printf 'inheritance-probe-body' | spt send "$BID" --from probe-int >/dev/null 2>&1
  stolen=$(printf '%s' "{\"session_id\":\"a-session-that-owns-no-perch\",\"prompt\":\"\"}" \
    | env -u SPT_AGENT_ID -u SPT_ADAPTER SPT_ENDPOINT_ID="$BID" OWL_SESSION_ID="$BSID" \
      SPT_HOST_PID=999999 HOME="$NHOME" "$HOOKBIN" hook UserPromptSubmit --host-pid $$ 2>/dev/null)
  case "$stolen" in
    *inheritance-probe-body*) bad "an inherited identity drained the perch's mail into a foreign session: [$stolen]" ;;
    *) ok "an inherited identity is refused — the foreign session drains nothing" ;;
  esac
  spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
  owned=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
  case "$owned" in
    *inheritance-probe-body*) ok "and the message is still there for the session that owns the perch" ;;
    *) bad "the refusal cost the legitimate session its message; got=[$owned]" ;;
  esac
  spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
else
  echo "SKIP: claude-spt binary not built (cargo build --release) — the spill two-overflow probe needs it"
fi

# 6. A MATCHED MONIC IS REVEALED, on BOTH drain legs. The field defect (hertz, 2026-08-05): core
#    suppresses the stranger trust-warning because it matched a monic, rides the matched records on
#    the envelope as `mnemonics-json`, and our re-render dropped every attribute but `from` — so a
#    classified peer arrived with no warning AND no judgement. This drives a REAL core-composed
#    envelope (core matches the monic itself; we never scan its store) through the SHIPPED binary on
#    the busy poll leg AND the idle stub/park leg, and asserts the reveal, the ordering, and the
#    negative arm. [int->REQ-MONIC-REVEAL]
if [ -x "$HOOKBIN" ]; then
  # Capability probe with a positive control (§2.14): `endpoint --help` MUST answer, and then either
  # advertises `monic` (proceed) or genuinely lacks it on an older spt (an honest SKIP). A probe that
  # could not read a help body at all is case (3) — nothing was learned — and FAILS.
  sptc_flag_supported 'monic' spt endpoint; mrc=$?
  MONIC=sptc-poll-int-monic
  MONIC_TEXT='probe monic: this sender is classified'
  case "$mrc" in
    2) bad "monic capability probe FAILED (spt endpoint --help did not answer) — nothing known, not skipping" ;;
    1) echo 'SKIP: this spt has no "endpoint monic" surface (monics are newer) — monic reveal unproven here' ;;
    0)
      # The monic goes in the THROWAWAY perch's own mind and is withdrawn on exit — never the runner's.
      trap 'spt endpoint monic remove --owner "$BID" --target "$MONIC" >/dev/null 2>&1 || true; spt endpoint shutdown "$BID" >/dev/null 2>&1 || true; spt endpoint stop "$BID" >/dev/null 2>&1 || true; [ -n "$SPILLHOME" ] && rm -rf "$SPILLHOME"' EXIT INT TERM
      madd=$(printf '%s' "$MONIC_TEXT" | spt endpoint monic add --owner "$BID" --target "$MONIC" \
        --triggers '[{"kind":"sender","pattern":"probe-int"}]' 2>&1)
      case "$madd" in
        *"$MONIC"*) ok "monic written into the throwaway perch's mind" ;;
        *) bad "could not write the probe monic: [$madd]" ;;
      esac
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred >/dev/null 2>&1

      # (a) BUSY POLL LEG — the mid-turn/UPS drain of a live core delivery.
      printf 'monic-probe-body-busy' | spt send "$BID" --from probe-int >/dev/null 2>&1
      mbusy=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      case "$mbusy" in
        *"$MONIC_TEXT"*) ok "poll leg: the matched monic's text reaches additionalContext" ;;
        *) bad "poll leg: monic text MISSING (the field defect) — got=[$mbusy]" ;;
      esac
      case "$mbusy" in
        *'<sptc_monics from="probe-int">'*'<sptc_messages from="probe-int">'*'monic-probe-body-busy'*)
          ok "poll leg: monic block LEADS its message, sender + body intact" ;;
        *) bad "poll leg: block/ordering wrong — got=[$mbusy]" ;;
      esac

      # (b) IDLE STUB/PARK LEG — custody. Take the REAL envelope core composed (attribute and all),
      #     park it exactly as the translation binary does, and drain it on a msg-stub turn.
      printf 'monic-probe-body-park' | spt send "$BID" --from probe-int >/dev/null 2>&1
      praw=$(spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred 2>&1)
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      case "$praw" in
        *mnemonics-json=*) ok "core composed the mnemonics-json attribute onto the live envelope" ;;
        *) bad "no mnemonics-json on a matched delivery — the probe never had the attribute to carry: [$praw]" ;;
      esac
      PARKDIR="$SPILLHOME/.spt-claude/msgpark/$BID"
      mkdir -p "$PARKDIR"
      printf '%s' "$praw" > "$PARKDIR/0000000000001-000000.park"
      mpark=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"<msg from=\\\"probe-int\\\"/>\"}" "$NHOME")
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      case "$mpark" in
        *"$MONIC_TEXT"*'<sptc_messages from="probe-int">'*'monic-probe-body-park'*)
          ok "park leg: custody holds — the parked envelope's monic renders identically" ;;
        *) bad "park leg: monic lost between park and render — got=[$mpark]" ;;
      esac

      # (c) NEGATIVE ARM. A sender no monic matches renders no block — and (a) above is its positive
      #     control, so a renderer that reveals nothing cannot pass this pair.
      printf 'unclassified-body' | spt send "$BID" --from probe-stranger >/dev/null 2>&1
      mno=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      case "$mno" in
        *'<sptc_monics'*) bad "an UNMATCHED delivery grew a monic block: [$mno]" ;;
        *unclassified-body*) ok "negative arm: no match, no block, message still delivered" ;;
        *) bad "negative arm: the unmatched message did not arrive at all: [$mno]" ;;
      esac

      # (d) OVERSIZED DELIVERY. The block leads, so the inlined head keeps the judgement even when the
      #     body spills to a file.
      bigpad=$(awk 'BEGIN{s="";while(length(s)<3000)s=s "monic-spill-pad-";print substr(s,1,3000)}')
      printf 'BIG %s' "$bigpad" | spt send "$BID" --from probe-int >/dev/null 2>&1
      mbig=$(hookrun UserPromptSubmit "{\"session_id\":\"$BSID\",\"prompt\":\"\"}" "$NHOME")
      spt api --adapter "$ADAPTER" state idle "$BID" --session-id "$BSID" >/dev/null 2>&1
      bigfile=$(spilled_to "$mbig"); bigpath=$(topath "$bigfile")
      if [ -n "$bigfile" ] && grep -q "$MONIC_TEXT" "$bigpath" 2>/dev/null; then
        ok "oversized delivery: the spilled copy retains the monic"
      else
        bad "oversized delivery lost the monic (spill=[$bigfile]); head=[$mbig]"
      fi
      spt api --adapter "$ADAPTER" poll "$BID" --session-id "$BSID" --include-deferred >/dev/null 2>&1
      ;;
  esac
else
  echo "SKIP: claude-spt binary not built (cargo build --release) — the monic reveal probe needs it"
fi

[ "$fail" -eq 0 ] && { echo "MSG-ENVELOPE-INT OK"; exit 0; } || { echo "MSG-ENVELOPE-INT FAIL"; exit 1; }
