1	#!/bin/sh
2	# Integration proof for /sptc:live's LiveAgent bringup against a REAL spt-core (>= v0.7.3): the
3	# `claude-spt:live` profile's [session.psyche_init] makes spt spawn the Psyche, and the resident
4	# relay (`spt api listen <id>`, the production delivery pipe — run inside CC's Monitor tool, heir to
5	# legacy `$LIVE start <id>`) binds the live perch and streams inbound as the canonical <EVENT>
6	# envelope. We play Monitor's role: spawn `api listen` as a CHILD PROCESS (persistent, NOT --once —
7	# --once is the degenerate no-Monitor path) with `--manifest` (the manifest loads ONLY via --manifest;
8	# `--adapter <name>` is just a name → without it LiveHost is None and no Psyche spawns), capture its
9	# stdout/stderr, send a probe, and assert the bringup markers. The LLM is never judged. [int->REQ-SKILL-LIVE]
10	#
11	# Covers BOTH legs of REQ-SKILL-LIVE int: (1) PSYCHE-SPAWN — version-dependent marker (M11 restructure,
12	# doyle 2026-06-16): on <0.8.0 `api listen` spawns the Psyche IN-PROCESS (`PSYCHE_SPAWNED:{id}-psyche`
13	# off the listen child, startup.rs spawn_psyche pre once/loop split); on >=0.8.0 the DAEMON livehost
14	# hosts it by spawning claude-spt-psyche (resolved by bare name from the adapter install dir) — assert
15	# the RESIDENT runner process + nested `{id}-psyche` perch dir (FINALIZED on v0.8.1 + the adapter
16	# greedy-prompt fix, 2026-06-16; see the >=0.8.0 leg below). (2) RELAY — the resident listen pipe delivers the probe (BOUND/READY/<EVENT> off the child,
17	# unchanged across versions). The per-pulse runner command construction is additionally covered by
18	# claude-spt-psyche unit tests (ci/psyche/build.sh).
19	#
20	# PSYCHE-SPAWN binary resolution: the psyche_init command invokes `claude-spt-psyche` by bare name.
21	# On v0.8.0+ (Feature B / REQ-INSTALL-11) spt resolves it FROM the adapter install dir (proven: the
22	# binary registration-copies into adapters/_github/<safe>/, no PATH interim needed). On 0.7.3 it must
23	# be on PATH (the F-006 interim copy). The >=0.8.0 leg now ASSERTS a resident runner (was skip-with-note
24	# pre-v0.8.1); the <0.8.0 leg still SKIPs when the runner is unresolvable. The relay leg asserts always.
25	#
26	# Spawns a real claude-spt-psyche (which launches a headless claude) + mutates node-local perch state,
27	# all torn down on exit. Gated behind SPTC_ACCEPTANCE=1. Idempotent.
28	# Run: SPTC_ACCEPTANCE=1 sh ci/psyche/live-relay-int.sh   (exit 0 = pass).
29	set -u
30	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
31	A=claude-spt:live
32	MAN="$ROOT/adapter/claude-spt.toml"
33	# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity
34	# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):
35	# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a
36	# rerun (the first run's hosted-session memory suppresses it) — each CI run must be a fresh session.
37	RUN=$$
38	ID=sptc-ci-liverelay-$RUN
39	SID=sptc-ci-liverelay-$RUN-sess
40	export SPTC_CI_ID="$ID" SPT_AGENT_ID="$ID" OWL_SESSION_ID="$ID"
41
42	if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a Psyche + mutates perch state)"; exit 0; fi
43	command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
44	ver=$(spt --version 2>/dev/null | awk '{print $NF}')
45	case "$ver" in
46	  0.7.2|0.7.1|0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.3 (daemon live path / counter-15)"; exit 0 ;;
47	esac
48	[ -f "$MAN" ] || { echo "SKIP: no manifest at $MAN"; exit 0; }
49
50	# Anchor pid: spt probes WINDOWS pids — from git-bash `$$` is the MSYS pid and reads as a dead anchor
51	# (STALE_SEED/NO_SEED). Use the WINPID column from `ps` when present (git-bash col 4); elsewhere `$$`
52	# is already the real pid spt probes.
53	ANCHOR=$(ps -p $$ 2>/dev/null | awk 'NR==2{print $4}'); case "$ANCHOR" in ''|*[!0-9]*) ANCHOR=$$;; esac
54
55	# Can the psyche runner be resolved on THIS host? (PATH now, or manifest-dir on v0.8.0+ Feature B.)
56	psyche_resolvable() {
57	  command -v claude-spt-psyche >/dev/null 2>&1 && return 0
58	  case "$ver" in 0.7.*) return 1 ;; *) return 0 ;; esac  # Feature B (manifest-dir resolution) lands in v0.8.0; >=0.8.0 resolvable
59	}
60
61	LF=$(mktemp 2>/dev/null) || { echo "FATAL: mktemp"; exit 2; }
62	CHILD=""
63	fail=0
64	ok()  { echo "ok   $1"; }
65	bad() { echo "FAIL $1"; fail=1; }
66	skip(){ echo "skip $1"; }
67	cleanup() {
68	  # Kill the spawned Psyche subtree first (the runner + its headless claude), by marker pid then name.
69	  pp=$(grep -oE 'PSYCHE_SPAWNED:[^ ]+ pid=[0-9]+' "$LF" 2>/dev/null | grep -oE '[0-9]+' | tail -1)
70	  [ -n "$pp" ] && taskkill //PID "$pp" //T //F >/dev/null 2>&1
71	  for p in $(tasklist 2>/dev/null | grep -i claude-spt-psyche | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done
72	  [ -n "$CHILD" ] && kill "$CHILD" >/dev/null 2>&1
73	  spt endpoint shutdown "$ID" >/dev/null 2>&1 || true
74	  spt endpoint stop "$ID" >/dev/null 2>&1 || true
75	  rm -f "$LF" 2>/dev/null || true
76	}
77	trap cleanup EXIT INT TERM
78
79	# Seed the harness-hosted startup (anchored to the pid that will parent the listen child).
80	spt api --adapter "$A" --manifest "$MAN" seed --pid "$ANCHOR" --session-id "$SID" >/dev/null 2>&1
81
82	# Spawn the resident relay as a CHILD (Monitor surrogate). timeout bounds it so a hang can't wedge CI.
83	timeout 30 spt api --adapter "$A" --manifest "$MAN" listen --parent-pid "$ANCHOR" "$ID" > "$LF" 2>&1 &
84	CHILD=$!
85
86	# Wait (bounded) for the bringup to announce readiness.
87	i=0; while [ "$i" -lt 25 ]; do grep -q "READY:$ID" "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done
88
89	# 1. The live listen path binds the perch.
90	grep -q "BOUND:$ID" "$LF" 2>/dev/null && ok "live perch bound" || bad "no BOUND:$ID; log=[$(cat "$LF")]"
91	# 2. The live listen path announces readiness (the relay heartbeat is up).
92	grep -q "READY:$ID" "$LF" 2>/dev/null && ok "live listen READY:$ID" || bad "no READY:$ID; log=[$(cat "$LF")]"
93	# 3. PSYCHE-SPAWN — the marker model differs by version (M11 restructure; doyle 2026-06-16):
94	case "$ver" in
95	  0.7.*)
96	    # <0.8.0: `api listen` spawns the Psyche IN-PROCESS → `PSYCHE_SPAWNED:{id}-psyche pid=` off the
97	    # listen child's stderr. The bare `claude-spt-psyche` resolves via the F-006 PATH interim here.
98	    if grep -q "PSYCHE_SPAWNED:$ID-psyche pid=" "$LF" 2>/dev/null; then
99	      ok "Psyche spawned in-process ($(grep -oE "PSYCHE_SPAWNED:$ID-psyche pid=[0-9]+" "$LF" | head -1))"
100	    elif grep -q "PSYCHE_SPAWN_FAIL:" "$LF" 2>/dev/null && ! psyche_resolvable; then
101	      skip "psyche-spawn: runner unresolvable on spt $ver without the F-006 PATH interim (Feature B/REQ-INSTALL-11 lands v0.8.0) — $(grep -oE 'PSYCHE_SPAWN_FAIL:[^]]*' "$LF" | head -1)"
102	    else
103	      bad "no PSYCHE_SPAWNED marker (manifest declares psyche_init? runner resolvable?); log=[$(cat "$LF")]"
104	    fi
105	    ;;
106	  *)
107	    # >=0.8.0: the in-process spawn is gone (M11 restructure) — `api listen` emits only BOUND/READY +
108	    # stamps the perch `status="online"` IFF the resolved manifest declares [session.psyche_init]
109	    # (startup.rs:283 live_capable guard); the DAEMON reconcile then hosts the Psyche off that online
110	    # status (the `{id}-psyche` perch comes online; `LIVEHOST_PSYCHE:{id}` on the daemon's stderr).
111	    # RESOLVED (v0.8.1 + adapter greedy-prompt fix, 2026-06-16): hosting succeeds iff the daemon
112	    # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core
113	    # <0.8.1 livehost did not reconcile (no spawn at all); v0.8.1 fixed it. (2) spt-core substitutes
114	    # `{psyche_prompt}` into the psyche_init command STRING then whitespace-SPLITS, so the multi-word
115	    # prompt arrived as stray argv tokens — the runner's non-greedy --prompt rejected the 2nd word
116	    # ("unknown arg") and exited 2 instantly → the daemon recorded a phantom hosted perch (nested
117	    # info.json status=online, real-looking pid) with NO live process and NO psyche_host_error. The
118	    # runner now parses --prompt greedily (slurps trailing tokens). DETECTION: the nested {id}-psyche
119	    # perch does NOT surface in `endpoint list` (it lives under the parent in the owlery), so assert on
120	    # the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11
121	    # install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.
122	    OWL="${SPT_HOME:-$HOME/AppData/Local/spt-core}/owlery/$ID/nested/$ID-psyche/info.json"
123	    resident() { tasklist 2>/dev/null | grep -qi "claude-spt-psyche"; }
124	    j=0; while [ "$j" -lt 20 ]; do resident && [ -f "$OWL" ] && break; sleep 1; j=$((j+1)); done
125	    procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")
126	    nested=no; [ -f "$OWL" ] && nested=yes
127	    if resident && [ -f "$OWL" ]; then
128	      ok "Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)"
129	    else
130	      bad "psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs"
131	    fi
132	    ;;
133	esac
134
135	# Deliver a probe with body specials; the resident relay streams it live to the child's stdout.
136	printf 'live relay probe <a> & "b"' | spt send "$ID" --from relay-probe >/dev/null 2>&1
137	i=0; while [ "$i" -lt 20 ]; do grep -q 'type="msg"' "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done
138
139	# 4. RELAY: the probe arrives as the canonical <EVENT> envelope with correct body escaping.
140	expected='<EVENT type="msg" from="relay-probe">live relay probe <a> & "b"</EVENT>'
141	grep -qF "$expected" "$LF" 2>/dev/null && ok "probe relayed through the live pipe as escaped <EVENT>" || bad "envelope mismatch; log=[$(cat "$LF")]"
142	# 5. The endpoint registered as a live_agent (the :live kind was honored on bringup).
143	kind=$(spt daemon status 2>&1 | sed -n '/local endpoints/,$p' | grep -E "(^| )$ID( |$)")
144	case "$kind" in *live_agent*) ok "endpoint registered as live_agent (:live kind honored)" ;; *) bad "endpoint not live_agent: [$kind]" ;; esac
145
146	[ "$fail" -eq 0 ] && { echo "LIVE-RELAY-INT OK"; exit 0; } || { echo "LIVE-RELAY-INT FAIL"; exit 1; }
147

#!/bin/sh
# Integration proof for /sptc:live's LiveAgent bringup against a REAL spt-core (>= v0.7.3): the
# `claude-spt:live` profile's [session.psyche_init] makes spt spawn the Psyche, and the resident
# relay (`spt api listen <id>`, the production delivery pipe — run inside CC's Monitor tool, heir to
# legacy `$LIVE start <id>`) binds the live perch and streams inbound as the canonical <EVENT>
# envelope. We play Monitor's role: spawn `api listen` as a CHILD PROCESS (persistent, NOT --once —
# --once is the degenerate no-Monitor path) with `--manifest` (the manifest loads ONLY via --manifest;
# `--adapter <name>` is just a name → without it LiveHost is None and no Psyche spawns), capture its
# stdout/stderr, send a probe, and assert the bringup markers. The LLM is never judged. [int->REQ-SKILL-LIVE]
#
# Covers BOTH legs of REQ-SKILL-LIVE int: (1) PSYCHE-SPAWN — version-dependent marker (M11 restructure,
# doyle 2026-06-16): on <0.8.0 `api listen` spawns the Psyche IN-PROCESS (`PSYCHE_SPAWNED:{id}-psyche`
# off the listen child, startup.rs spawn_psyche pre once/loop split); on >=0.8.0 the DAEMON livehost
# hosts it by spawning claude-spt-psyche (resolved by bare name from the adapter install dir) — assert
# the RESIDENT runner process + nested `{id}-psyche` perch dir (FINALIZED on v0.8.1 + the adapter
# greedy-prompt fix, 2026-06-16; see the >=0.8.0 leg below). (2) RELAY — the resident listen pipe delivers the probe (BOUND/READY/<EVENT> off the child,
# unchanged across versions). The per-pulse runner command construction is additionally covered by
# claude-spt-psyche unit tests (ci/psyche/build.sh).
#
# PSYCHE-SPAWN binary resolution: the psyche_init command invokes `claude-spt-psyche` by bare name.
# On v0.8.0+ (Feature B / REQ-INSTALL-11) spt resolves it FROM the adapter install dir (proven: the
# binary registration-copies into adapters/_github/<safe>/, no PATH interim needed). On 0.7.3 it must
# be on PATH (the F-006 interim copy). The >=0.8.0 leg now ASSERTS a resident runner (was skip-with-note
# pre-v0.8.1); the <0.8.0 leg still SKIPs when the runner is unresolvable. The relay leg asserts always.
#
# Spawns a real claude-spt-psyche (which launches a headless claude) + mutates node-local perch state,
# all torn down on exit. Gated behind SPTC_ACCEPTANCE=1. Idempotent.
# Run: SPTC_ACCEPTANCE=1 sh ci/psyche/live-relay-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
A=claude-spt:live
MAN="$ROOT/adapter/claude-spt.toml"
# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Override BOTH identity
# env vars; pin OWL_SESSION_ID for the auth-gated seed/send/listen surfaces. PER-RUN UNIQUE ($$ suffix):
# the daemon hosts a Psyche at most ONCE per session_id, so a FIXED id/session would not re-host on a
# rerun (the first run's hosted-session memory suppresses it) — each CI run must be a fresh session.
RUN=$$
ID=sptc-ci-liverelay-$RUN
SID=sptc-ci-liverelay-$RUN-sess
export SPTC_CI_ID="$ID" SPT_AGENT_ID="$ID" OWL_SESSION_ID="$ID"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a Psyche + mutates perch 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.2|0.7.1|0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.3 (daemon live path / counter-15)"; exit 0 ;;
esac
[ -f "$MAN" ] || { echo "SKIP: no manifest at $MAN"; exit 0; }

# Anchor pid: spt probes WINDOWS pids — from git-bash `$$` is the MSYS pid and reads as a dead anchor
# (STALE_SEED/NO_SEED). Use the WINPID column from `ps` when present (git-bash col 4); elsewhere `$$`
# is already the real pid spt probes.
ANCHOR=$(ps -p $$ 2>/dev/null | awk 'NR==2{print $4}'); case "$ANCHOR" in ''|*[!0-9]*) ANCHOR=$$;; esac

# Can the psyche runner be resolved on THIS host? (PATH now, or manifest-dir on v0.8.0+ Feature B.)
psyche_resolvable() {
  command -v claude-spt-psyche >/dev/null 2>&1 && return 0
  case "$ver" in 0.7.*) return 1 ;; *) return 0 ;; esac  # Feature B (manifest-dir resolution) lands in v0.8.0; >=0.8.0 resolvable
}

LF=$(mktemp 2>/dev/null) || { echo "FATAL: mktemp"; exit 2; }
CHILD=""
fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
skip(){ echo "skip $1"; }
cleanup() {
  # Kill the spawned Psyche subtree first (the runner + its headless claude), by marker pid then name.
  pp=$(grep -oE 'PSYCHE_SPAWNED:[^ ]+ pid=[0-9]+' "$LF" 2>/dev/null | grep -oE '[0-9]+' | tail -1)
  [ -n "$pp" ] && taskkill //PID "$pp" //T //F >/dev/null 2>&1
  for p in $(tasklist 2>/dev/null | grep -i claude-spt-psyche | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done
  [ -n "$CHILD" ] && kill "$CHILD" >/dev/null 2>&1
  spt endpoint shutdown "$ID" >/dev/null 2>&1 || true
  spt endpoint stop "$ID" >/dev/null 2>&1 || true
  rm -f "$LF" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

# Seed the harness-hosted startup (anchored to the pid that will parent the listen child).
spt api --adapter "$A" --manifest "$MAN" seed --pid "$ANCHOR" --session-id "$SID" >/dev/null 2>&1

# Spawn the resident relay as a CHILD (Monitor surrogate). timeout bounds it so a hang can't wedge CI.
timeout 30 spt api --adapter "$A" --manifest "$MAN" listen --parent-pid "$ANCHOR" "$ID" > "$LF" 2>&1 &
CHILD=$!

# Wait (bounded) for the bringup to announce readiness.
i=0; while [ "$i" -lt 25 ]; do grep -q "READY:$ID" "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done

# 1. The live listen path binds the perch.
grep -q "BOUND:$ID" "$LF" 2>/dev/null && ok "live perch bound" || bad "no BOUND:$ID; log=[$(cat "$LF")]"
# 2. The live listen path announces readiness (the relay heartbeat is up).
grep -q "READY:$ID" "$LF" 2>/dev/null && ok "live listen READY:$ID" || bad "no READY:$ID; log=[$(cat "$LF")]"
# 3. PSYCHE-SPAWN — the marker model differs by version (M11 restructure; doyle 2026-06-16):
case "$ver" in
  0.7.*)
    # <0.8.0: `api listen` spawns the Psyche IN-PROCESS → `PSYCHE_SPAWNED:{id}-psyche pid=` off the
    # listen child's stderr. The bare `claude-spt-psyche` resolves via the F-006 PATH interim here.
    if grep -q "PSYCHE_SPAWNED:$ID-psyche pid=" "$LF" 2>/dev/null; then
      ok "Psyche spawned in-process ($(grep -oE "PSYCHE_SPAWNED:$ID-psyche pid=[0-9]+" "$LF" | head -1))"
    elif grep -q "PSYCHE_SPAWN_FAIL:" "$LF" 2>/dev/null && ! psyche_resolvable; then
      skip "psyche-spawn: runner unresolvable on spt $ver without the F-006 PATH interim (Feature B/REQ-INSTALL-11 lands v0.8.0) — $(grep -oE 'PSYCHE_SPAWN_FAIL:[^]]*' "$LF" | head -1)"
    else
      bad "no PSYCHE_SPAWNED marker (manifest declares psyche_init? runner resolvable?); log=[$(cat "$LF")]"
    fi
    ;;
  *)
    # >=0.8.0: the in-process spawn is gone (M11 restructure) — `api listen` emits only BOUND/READY +
    # stamps the perch `status="online"` IFF the resolved manifest declares [session.psyche_init]
    # (startup.rs:283 live_capable guard); the DAEMON reconcile then hosts the Psyche off that online
    # status (the `{id}-psyche` perch comes online; `LIVEHOST_PSYCHE:{id}` on the daemon's stderr).
    # RESOLVED (v0.8.1 + adapter greedy-prompt fix, 2026-06-16): hosting succeeds iff the daemon
    # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core
    # <0.8.1 livehost did not reconcile (no spawn at all); v0.8.1 fixed it. (2) spt-core substitutes
    # `{psyche_prompt}` into the psyche_init command STRING then whitespace-SPLITS, so the multi-word
    # prompt arrived as stray argv tokens — the runner's non-greedy --prompt rejected the 2nd word
    # ("unknown arg") and exited 2 instantly → the daemon recorded a phantom hosted perch (nested
    # info.json status=online, real-looking pid) with NO live process and NO psyche_host_error. The
    # runner now parses --prompt greedily (slurps trailing tokens). DETECTION: the nested {id}-psyche
    # perch does NOT surface in `endpoint list` (it lives under the parent in the owlery), so assert on
    # the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11
    # install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.
    OWL="${SPT_HOME:-$HOME/AppData/Local/spt-core}/owlery/$ID/nested/$ID-psyche/info.json"
    resident() { tasklist 2>/dev/null | grep -qi "claude-spt-psyche"; }
    j=0; while [ "$j" -lt 20 ]; do resident && [ -f "$OWL" ] && break; sleep 1; j=$((j+1)); done
    procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")
    nested=no; [ -f "$OWL" ] && nested=yes
    if resident && [ -f "$OWL" ]; then
      ok "Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)"
    else
      bad "psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs"
    fi
    ;;
esac

# Deliver a probe with body specials; the resident relay streams it live to the child's stdout.
printf 'live relay probe <a> & "b"' | spt send "$ID" --from relay-probe >/dev/null 2>&1
i=0; while [ "$i" -lt 20 ]; do grep -q 'type="msg"' "$LF" 2>/dev/null && break; sleep 1; i=$((i+1)); done

# 4. RELAY: the probe arrives as the canonical <EVENT> envelope with correct body escaping.
expected='<EVENT type="msg" from="relay-probe">live relay probe <a> & "b"</EVENT>'
grep -qF "$expected" "$LF" 2>/dev/null && ok "probe relayed through the live pipe as escaped <EVENT>" || bad "envelope mismatch; log=[$(cat "$LF")]"
# 5. The endpoint registered as a live_agent (the :live kind was honored on bringup).
kind=$(spt daemon status 2>&1 | sed -n '/local endpoints/,$p' | grep -E "(^| )$ID( |$)")
case "$kind" in *live_agent*) ok "endpoint registered as live_agent (:live kind honored)" ;; *) bad "endpoint not live_agent: [$kind]" ;; esac

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


Full contract clear. SEEDED/NO_SEED/BOUND/READY = spt-core parse markers, internal. Reply to doyle with plan, then execute.


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && "$OWL" send --reply-to doyle doyle perri <<'EOF'
perri — tail received, message fully reassembled (item-4: `spt adapter use claude-spt[:profile]` once-per-host-binary in /sptc:setup; CLOSING confirms interim chain is fine + dies with agnostic seed). PLAN:

#1 SELF-CONTAINED INJECTION (kill how-to redirect):
- Rewrite adapter/strings/skills/live.md. DELETE step 2 ("run `spt how-to live` and follow it") entirely — no redirect, ever.
- Fold the FULL bringup recipe inline: persistent-child `spt api --adapter claude-spt:live --manifest <man> listen --parent-pid <ANCHOR> <id>` under CC Monitor; WINPID anchor (ps col-4, not MSYS $$); seed-then-listen interim chain (anchored to same pid); bind-before-send; reconcile relay-vs-poll (relay authoritative). Tag the chain as INTERIM with a one-liner: "removed once spt-core agnostic seed lands (PREP-4)."
- SKILL.md skeleton stays a stub (ADR-0001); skeleton floor unchanged.

#2 HUMANIZE OUTPUT:
- Add an OUTPUT DISCIPLINE block to live.md: SEEDED:/NO_SEED:/STALE_SEED:/BOUND:/READY: + the raw PowerShell-step token lines are an INTERNAL PARSE CONTRACT — agent parses them, NEVER echoes them to the user.
- Sole user-facing surface = the final "LIVE. Session <id> is now a LiveAgent…" block. I'll pin its canonical shape in the skill so it's reproducible, not improvised.

PREP 3 (stage): add `host_binaries = ["claude"]` to [adapter] in adapter/claude-spt.toml. Will verify manifest.schema.json accepts the additive field (or note a finding if schema rejects unknown keys).

PREP 4: HELD for your v0.9.0 ping.

Governance: this is REQ-SKILL-LIVE doc-stage — re-tag evidence in-commit, `traceable-reqs check` green before done. Co-author trailer = perri. Starting now.

Q (1): the final humanized LIVE block — want a doyle-blessed canonical template, or my call? I'll draft it; flag if you want sign-off before I lock it.
EOF

Send plan to doyle


➜ REPLIED:doyle


Plan sent. Execute. First check manifest [adapter] + schema for PREP 3.


1	# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
2	#
3	# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
4	# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
5	# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
6	# marketplace `plugin.json` version.
7	#
8	# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
9	# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
10	# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
11	# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
12	#
13	# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
14	# yet built, or needs a capability not on the public surface — see notes inline):
15	#   * [session.<role>] for psyche_init/psyche_resume/echo_commune/signoff/notif — the live-agent /
16	#     Psyche layer (owl/$LIVE), absent from the core `spt` surface. Open scope question (does
17	#     claude-spt reach the Psyche layer?). [session.self] IS authored below (the endpoint-run/bind
18	#     bringup that hosts a fresh CC session — the M12 `cc` launcher target).
19	#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
20	#   * [update]                      — NOW DECLARED below via the `gh_release` avenue (spt v0.8.0):
21	#     spt-core's `--release` machinery auto-updates the adapter from THIS repo's GitHub releases on
22	#     `spt adapter update`, with NO Ed25519 signing infra (closes the F-005 parked `file_pull`/
23	#     `delegated` legs). Per-OS interaction flagged to doyle (gh_release `asset` is a single name; how
24	#     does auto-update pick the host's `adapter-<os>-<arch>.spt`?).
25	#
26	# [impl->REQ-DIST-MANIFEST-SCHEMA]
27
28	[adapter]
29	name = "claude-spt"
30	kind = "harness"
31	version = "0.2.0"
32	# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
33	# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
34	min_spt_core_version = "0.7.0"
35	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
36	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
37	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
38	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
39	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
40	# (unit->REQ-DIST-SHORTCUT-BASENAME).
41	shortcut_basename = "cc"
42
43	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
44	# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
45	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
46	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
47	# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
48	# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
49	[update]
50	avenue = "gh_release"
51	repo = "SaberMage/spt-claude-code"
52
53	# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
54	# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
55	# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
56	# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
57	# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.
58
59	[hooks.SessionStart]
60	# Seed the endpoint. {parent_pid} + {adapter_name} are spt-filled keys (confirmed against the
61	# published mock-adapter manifest); session_id is a CC stdin field. NOT the blocking poll/listen
62	# loop — that is /sptc:ready|live, never SessionStart.
63	fires = "api seed --session-id {session_id} --pid {parent_pid} --adapter {adapter_name}"
64	reads = ["session_id", "parent_pid"]
65	can_inject = true
66
67	[hooks.UserPromptSubmit]
68	# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
69	fires = "api poll {session_id}"
70	reads = ["session_id", "prompt"]
71	can_inject = true
72
73	[hooks.Stop]
74	# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
75	fires = "api state idle"
76	reads = ["session_id"]
77	can_inject = false
78
79	[hooks.SessionEnd]
80	fires = "api session-end {session_id}"
81	reads = ["session_id"]
82	can_inject = false
83
84	[hooks.SubagentStart]
85	fires = "api worker-start {session_id}"
86	reads = ["session_id"]
87	can_inject = false
88
89	[hooks.SubagentStop]
90	fires = "api worker-stop {session_id}"
91	reads = ["session_id"]
92	can_inject = false
93
94	# ── [identity] ─────────────────────────────────────────────────────────────────────────────
95	[identity]
96	# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
97	session_id_source = "post_spawn"
98	# Process-tree anchor when session_id is absent: the Claude Code binary.
99	parent_ancestor_name = "claude"
100
101	# ── [session] ──────────────────────────────────────────────────────────────────────────────
102	# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
103	# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
104	# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
105	# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
106	# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
107	# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
108	# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
109	# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
110	[session]
111	commune_dir = ".claude"
112	signoff_dir = ".claude"
113
114	# ── [session.self] ─────────────────────────────────────────────────────────────────────────
115	# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
116	# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
117	# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
118	# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
119	#
120	# command is `claude` bare: CC mints its OWN session id internally (identity.session_id_source =
121	# post_spawn), so spt's {session_id} is not threaded on argv — only the endpoint {id} matters, and
122	# CC has no native flag for it, so it rides the ENV instead (see [env.SPT_ENDPOINT_ID] below; the
123	# published sanctioned pattern for a flagless harness). SessionStart reads $SPT_ENDPOINT_ID and binds.
124	# [impl->REQ-DIST-SHORTCUT-BASENAME]
125	[session.self]
126	command = "claude"
127	keys = ["id"]
128
129	# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
130	# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
131	# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
132	# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
133	# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
134	[env.SPT_ENDPOINT_ID]
135	direction = "inject"
136	value = "{id}"
137
138	# ── [inject] ───────────────────────────────────────────────────────────────────────────────
139	[inject]
140	# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
141	# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
142	activity = ["hook"]
143	idle = ["hook"]
144
145	# ── [digest] ───────────────────────────────────────────────────────────────────────────────
146	# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
147	# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
148	# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
149	# divergence from the deliberately log-less mock adapter, which declares no [digest].)
150	#
151	# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
152	# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
153	# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
154	# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
155	# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
156	# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
157	# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
158	# within the root itself (the slug is the harness's own business). This matches the published example
159	# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
160	# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
161	[digest]
162	extractor = "claude-spt-digest --session {session_id} --in {source}"
163	source = "~/.claude/projects"
164	window_turns = 6
165	arg_truncation = 240
166	sprint_collapse = true
167
168	# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
169	# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
170	# `deep` widens the digest window for long-context review sessions without forking the base, and
171	# overrides a [strings] leaf so the overlay is observable via `spt adapter get-string` (base vs
172	# `:deep` differ). (Node-local user profiles are created with `spt adapter create-profile` and
173	# live in the registry, never in this manifest.)
174	[profiles.deep.digest]
175	window_turns = 20
176
177	[profiles.deep.strings]
178	adapter_label = "Claude Code (spt, deep)"
179
180	# `live` — the LiveAgent overlay. Selected as `claude-spt:live`; /sptc:live activates this profile so
181	# the daemon resolves the MERGED manifest, sees [session.psyche_init], and spawns the endpoint's
182	# Psyche companion (base claude-spt declares no psyche_init = ReadyAgent — doyle 2026-06-15, traced:
183	# api seed stores the composite → info.json adapter → daemon resolve_option_in applies the overlay →
184	# BrainLifecycle checks psyche_init on the merged view). Psyche keys the daemon fills are EXACTLY
185	# {id, session_id, psyche_dir, psyche_prompt} — NOT {session_name} (a [session.self] fill; the
186	# published example's use of it is a bug doyle fixed) and NOT {psyche_context} (the resume-preload
187	# seam, not first spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached
188	# (Stdio::null, unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at
189	# session end. The runner (claude-spt-psyche, tools/) is BUILT: a resident wrapper — `claude -p` is
190	# one-shot, so it seeds the Psyche `claude` session once from {psyche_prompt} then drives one
191	# `claude --continue` turn per daemon pulse (NOT a bare `claude -p`), the claude authoring commune
192	# drops. Activation: /sptc:live brings the perch up under the `claude-spt:live` composite (the overlay
193	# the daemon resolves to see psyche_init) + runs the delivery relay via Monitor (skills/live.md).
194	# [impl->REQ-SKILL-LIVE]
195	[profiles.live.session.psyche_init]
196	command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
197	cwd = "{psyche_dir}"
198	detach = true
199	keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]
200
201	# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
202	# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
203	# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
204	# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account
205	# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
206	# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
207	# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
208	# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
209	# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
210	#
211	# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
212	# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
213	# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
214	# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
215	# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
216	# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
217	# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
218	# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
219	[profiles.ccs.session.self]
220	command = "ccs"
221
222	# Observability leaf (mirrors :deep): makes the overlay visible via `spt adapter get-string
223	# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
224	[profiles.ccs.strings]
225	adapter_label = "Claude Code (spt, ccs)"
226
227	# ── [strings] ──────────────────────────────────────────────────────────────────────────────
228	# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
229	# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
230	# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
231	[strings]
232	adapter_label = "Claude Code (spt)"
233
234	[strings.notice]
235	not_installed = "spt-core not found on PATH — run /sptc:setup to install it."
236
237	[strings.skills]
238	# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
239	# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
240	# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
241	# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
242	# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
243	# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
244	# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
245	# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
246	setup = { file = "skills/setup.md" }
247	ready = { file = "skills/ready.md" }
248	send = { file = "skills/send.md" }
249	version = { file = "skills/version.md" }
250	signoff = { file = "skills/signoff.md" }       # -> spt endpoint shutdown (graceful + final save)
251	list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
252	commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
253	force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
254	subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
255	# `live` -> upgrade THIS session to a LiveAgent via the `claude-spt:live` profile (overlays
256	# [session.psyche_init] -> daemon spawns the Psyche) + a resident Monitor relay as the delivery pipe.
257	# Profile propagation CONFIRMED in spt-core code (doyle 2026-06-15: api seed stores the composite ->
258	# info.json adapter -> daemon resolve_option_in applies the overlay -> BrainLifecycle checks
259	# psyche_init on the merged view). Body authored; runner (claude-spt-psyche) built (tools/).
260	live = { file = "skills/live.md" }             # -> activate claude-spt:live + Monitor relay (LiveAgent)
261	# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
262	# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
263	# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
264	# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).
265
266	# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
267	# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
268	[[hints]]
269	keywords = ["go live", "live agent", "start live"]
270	text = "Tip: /sptc:live starts or resumes a live agent session."
271
272	[[hints]]
273	keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
274	text = "Tip: run `spt whoami` to see this session's spt endpoint id."
275
276	[[hints]]
277	keywords = ["message another agent", "send to agent", "tell the other agent"]
278	text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."
279
280	[[hints]]
281	keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
282	text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."
283

# claude-spt — Claude Code adapter manifest (publish target #2: the spt-core adapter registry side).
#
# This is the spt-core-conducted layer of the distribution split (ADR-0001): the volatile
# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
# Registered with `spt adapter add <path>`; the version-of-truth is `spt adapter list`, not the
# marketplace `plugin.json` version.
#
# Authored against spt-core's PUBLISHED v0.7.0 surface ONLY (spt-releases + sabermage.github.io/
# spt-releases) — `manifest.schema.json` is vendored beside this file (adapter/manifest.schema.json,
# provenance: https://sabermage.github.io/spt-releases/manifest.schema.json). Validated by the
# manifest-schema CI gate (ci/manifest/). NEVER copy spt-core internals (AGENTS.md).
#
# Deferred to later parity slices (not in this slice; each declares an adapter seam we have not
# yet built, or needs a capability not on the public surface — see notes inline):
#   * [session.<role>] for psyche_init/psyche_resume/echo_commune/signoff/notif — the live-agent /
#     Psyche layer (owl/$LIVE), absent from the core `spt` surface. Open scope question (does
#     claude-spt reach the Psyche layer?). [session.self] IS authored below (the endpoint-run/bind
#     bringup that hosts a fresh CC session — the M12 `cc` launcher target).
#   * [history]                     — transcript-access seam. Lands with [digest]'s sibling fetcher.
#   * [update]                      — NOW DECLARED below via the `gh_release` avenue (spt v0.8.0):
#     spt-core's `--release` machinery auto-updates the adapter from THIS repo's GitHub releases on
#     `spt adapter update`, with NO Ed25519 signing infra (closes the F-005 parked `file_pull`/
#     `delegated` legs). Per-OS interaction flagged to doyle (gh_release `asset` is a single name; how
#     does auto-update pick the host's `adapter-<os>-<arch>.spt`?).
#
# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
version = "0.2.0"
# Local spt binary is 0.6.0; the published contract we build to is v0.7.0 (manifest.schema.json,
# shortcut_basename). 0.7.0 is the floor this manifest's fields require.
min_spt_core_version = "0.7.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
# (unit->REQ-DIST-SHORTCUT-BASENAME).
shortcut_basename = "cc"

# ── [update] ───────────────────────────────────────────────────────────────────────────────────
# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"

# ── [hooks.<event>] ──────────────────────────────────────────────────────────────────────────
# CC hook event → the opaque `api …` command it fires. These mirror the hand-written plugin
# hooks.json (plugin/sptc/hooks/hooks.json) one-for-one; the plugin shells `spt api`, this table
# is the registry-side declaration (ADR-0002, F-001 resolution). `reads` = the CC hook-stdin
# fields we map; `can_inject` = whether the event can surface additionalContext back to the agent.

[hooks.SessionStart]
# Seed the endpoint. {parent_pid} + {adapter_name} are spt-filled keys (confirmed against the
# published mock-adapter manifest); session_id is a CC stdin field. NOT the blocking poll/listen
# loop — that is /sptc:ready|live, never SessionStart.
fires = "api seed --session-id {session_id} --pid {parent_pid} --adapter {adapter_name}"
reads = ["session_id", "parent_pid"]
can_inject = true

[hooks.UserPromptSubmit]
# Drain the inbox (api poll → additionalContext) and run keyword hints (api hint over the prompt).
fires = "api poll {session_id}"
reads = ["session_id", "prompt"]
can_inject = true

[hooks.Stop]
# Idle marker on turn-end. Stop cannot inject → relay/sentinel fallback (echo-gate, F-001 §Gap3).
fires = "api state idle"
reads = ["session_id"]
can_inject = false

[hooks.SessionEnd]
fires = "api session-end {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStart]
fires = "api worker-start {session_id}"
reads = ["session_id"]
can_inject = false

[hooks.SubagentStop]
fires = "api worker-stop {session_id}"
reads = ["session_id"]
can_inject = false

# ── [identity] ─────────────────────────────────────────────────────────────────────────────
[identity]
# CC hands the session_id on hook stdin once running (discovered post-spawn, not injected).
session_id_source = "post_spawn"
# Process-tree anchor when session_id is absent: the Claude Code binary.
parent_ancestor_name = "claude"

# ── [session] ──────────────────────────────────────────────────────────────────────────────
# Watched-dir seams for the file-drop lifecycle (doyle 2026-06-15: commune/signoff are FILE-DROPS,
# not api verbs — "there is deliberately no api commune"). The agent writes `<endpoint_id>-commune.md`
# / `<endpoint_id>-signoff.md` into this dir; spt-core's daemon watcher ingests it into the endpoint's
# tracked mind, then deletes it (daemon = single writer). The filenames are fixed by the contract;
# only the dir is adapter-declared. `.claude` is CC's per-project state dir — where a live CC session
# already drops these (matches the live-agent reorientation), resolved per-endpoint against its cwd.
# /sptc:commune writes the commune file here; /sptc:signoff's graceful path is `endpoint shutdown`
# (the signoff file-drop is the watcher's equivalent). [impl->REQ-DIST-MANIFEST-SCHEMA]
[session]
commune_dir = ".claude"
signoff_dir = ".claude"

# ── [session.self] ─────────────────────────────────────────────────────────────────────────
# The spt-hosted bringup template (publish target #2's launcher seam). `spt endpoint run --adapter
# claude-spt --id <id>` spawns this command into a broker-held PTY; CC self-registers its perch
# post-spawn (`api bind <id>`, the spt-hosted path — NOT the seed→listen path /sptc:ready|live uses).
# This is what the `cc-<id>` shortcut (shortcut_basename="cc") bakes into a non-interactive launch.
#
# command is `claude` bare: CC mints its OWN session id internally (identity.session_id_source =
# post_spawn), so spt's {session_id} is not threaded on argv — only the endpoint {id} matters, and
# CC has no native flag for it, so it rides the ENV instead (see [env.SPT_ENDPOINT_ID] below; the
# published sanctioned pattern for a flagless harness). SessionStart reads $SPT_ENDPOINT_ID and binds.
# [impl->REQ-DIST-SHORTCUT-BASENAME]
[session.self]
command = "claude"
keys = ["id"]

# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────
# Broker-injected env for spt-hosted (bind-path) sessions. The endpoint {id} is injected so CC's
# SessionStart hook can self-register the right perch via `api bind $SPT_ENDPOINT_ID`. spt-hosted
# sessions inherit env from the spawning broker, so no `channel` is needed (that field is for the
# harness-hosted injection path). [impl->REQ-DIST-SHORTCUT-BASENAME]
[env.SPT_ENDPOINT_ID]
direction = "inject"
value = "{id}"

# ── [inject] ───────────────────────────────────────────────────────────────────────────────
[inject]
# v1 delivery is the hook channel only (UserPromptSubmit/SessionStart additionalContext).
# PTY injection is spt-core M3 roadmap (F-001 resolution); not claimed here.
activity = ["hook"]
idle = ["hook"]

# ── [digest] ───────────────────────────────────────────────────────────────────────────────
# Session-digest extractor seam (ADR-0019). CC's transcript is JSONL (one event per line); the
# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
# divergence from the deliberately log-less mock adapter, which declares no [digest].)
#
# spt-core's registration cross-field rule requires [digest] to name where it reads from (`source`
# or a [history] `locate_template`) — verified against the live v0.7.0 `spt adapter add`. CC stores
# transcripts at `~/.claude/projects/<cwd-slug>/<session_id>.jsonl`; the cwd-slug subdir is a
# CC-internal encoding with NO published key (doyle 2026-06-15: spt-core stays harness-agnostic and
# must not bake a harness's directory scheme into the catalog — by design there is no {cwd_slug}).
# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)
# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
# within the root itself (the slug is the harness's own business). This matches the published example
# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
[digest]
extractor = "claude-spt-digest --session {session_id} --in {source}"
source = "~/.claude/projects"
window_turns = 6
arg_truncation = 240
sprint_collapse = true

# ── [profiles.<name>] ────────────────────────────────────────────────────────────────────────
# Shipped profile overlays (sparse leaf-replace; selected as the composite `claude-spt:<name>`).
# `deep` widens the digest window for long-context review sessions without forking the base, and
# overrides a [strings] leaf so the overlay is observable via `spt adapter get-string` (base vs
# `:deep` differ). (Node-local user profiles are created with `spt adapter create-profile` and
# live in the registry, never in this manifest.)
[profiles.deep.digest]
window_turns = 20

[profiles.deep.strings]
adapter_label = "Claude Code (spt, deep)"

# `live` — the LiveAgent overlay. Selected as `claude-spt:live`; /sptc:live activates this profile so
# the daemon resolves the MERGED manifest, sees [session.psyche_init], and spawns the endpoint's
# Psyche companion (base claude-spt declares no psyche_init = ReadyAgent — doyle 2026-06-15, traced:
# api seed stores the composite → info.json adapter → daemon resolve_option_in applies the overlay →
# BrainLifecycle checks psyche_init on the merged view). Psyche keys the daemon fills are EXACTLY
# {id, session_id, psyche_dir, psyche_prompt} — NOT {session_name} (a [session.self] fill; the
# published example's use of it is a bug doyle fixed) and NOT {psyche_context} (the resume-preload
# seam, not first spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached
# (Stdio::null, unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at
# session end. The runner (claude-spt-psyche, tools/) is BUILT: a resident wrapper — `claude -p` is
# one-shot, so it seeds the Psyche `claude` session once from {psyche_prompt} then drives one
# `claude --continue` turn per daemon pulse (NOT a bare `claude -p`), the claude authoring commune
# drops. Activation: /sptc:live brings the perch up under the `claude-spt:live` composite (the overlay
# the daemon resolves to see psyche_init) + runs the delivery relay via Monitor (skills/live.md).
# [impl->REQ-SKILL-LIVE]
[profiles.live.session.psyche_init]
command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
cwd = "{psyche_dir}"
detach = true
keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]

# `ccs` — the LOCKED-ADD ccs profile template. Selected as `claude-spt:ccs`. ccs is the model/billing
# multiplexer (kaitranntt/ccs): structurally Claude Code, only the launch binary + backend differ —
# exactly the profile seam (SCOPE §ccs, LOCKED). This overlay leaf-replaces ONE leaf: the spawn
# command. `ccs` (bare) is a DROP-IN for `claude` and launches the user's ccs *default* account
# (set via `ccs auth default <name>`); SPT_ENDPOINT_ID rides the inherited env through the wrapper
# exactly as for bare `claude`. Validated against the known-good sister project claude_skill_owl,
# which latches `cli_binary = "ccs"` as a same-argv `claude` substitute (live/wrapper/claude.rs).
# This profile IS the worked example of adapter-profile authoring + the operator's own SPT-ecosystem
# hook; a user wanting a *specific* ccs profile clones it with `command = "ccs glm"` etc.
#
# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the
# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at
# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log
# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the
# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base
# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.
# (REQ-CCS-PROFILES doc evidence lives in docs/PARITY.md; int = registration-int :ccs assertions.)
[profiles.ccs.session.self]
command = "ccs"

# Observability leaf (mirrors :deep): makes the overlay visible via `spt adapter get-string
# claude-spt:ccs adapter_label` (differs from base) — the registration-int resolve proof.
[profiles.ccs.strings]
adapter_label = "Claude Code (spt, ccs)"

# ── [strings] ──────────────────────────────────────────────────────────────────────────────
# Opaque adapter data (spt-core never executes it), dot-path-readable via `spt adapter get-string`.
# Seed only this slice — the file-backed skill-instruction BODIES (UPS-injection source) land in
# the M12 file-backed-strings parity slice. A profile may leaf-replace any key here.
[strings]
adapter_label = "Claude Code (spt)"

[strings.notice]
not_installed = "spt-core not found on PATH — run /sptc:setup to install it."

[strings.skills]
# Operative UPS-injection skill bodies. File-backed values (`{ file = "skills/<x>.md" }`) live in the
# adapter aux dir `adapter/strings/skills/` (copied to `adapters/<adapter>/strings/` on `adapter add`,
# resolved lazily at `spt adapter get-string` — proven vs live v0.7.0, F-003). File-backed so the
# manifest does not bloat (ADR-0001). Skills not yet authored keep a one-line inline summary until
# their body lands; converting an inline summary to a `{ file }` pointer is the per-skill increment.
# whoami SKILL dropped (SCOPE LOCKED parity-trim; operator-confirmed 2026-06-15): core `spt whoami`
# stays in the hot path (-> M12 `endpoint list` alias), so the adapter ships no whoami skill. The
# "who am i" hint below points at the core CLI. (docs/PARITY.md.)
setup = { file = "skills/setup.md" }
ready = { file = "skills/ready.md" }
send = { file = "skills/send.md" }
version = { file = "skills/version.md" }
signoff = { file = "skills/signoff.md" }       # -> spt endpoint shutdown (graceful + final save)
list-agents = { file = "skills/list-agents.md" } # -> spt endpoint list (the roster)
commune = { file = "skills/commune.md" }       # -> file-drop <id>-commune.md into [session].commune_dir
force-stop = { file = "skills/force-stop.md" } # -> spt endpoint shutdown (graceful, tears down Psyche)
subnet = { file = "skills/subnet.md" }         # -> spt subnet {status,create,show-code,join} (cross-machine); REQ-SKILL-SUBNET
# `live` -> upgrade THIS session to a LiveAgent via the `claude-spt:live` profile (overlays
# [session.psyche_init] -> daemon spawns the Psyche) + a resident Monitor relay as the delivery pipe.
# Profile propagation CONFIRMED in spt-core code (doyle 2026-06-15: api seed stores the composite ->
# info.json adapter -> daemon resolve_option_in applies the overlay -> BrainLifecycle checks
# psyche_init on the merged view). Body authored; runner (claude-spt-psyche) built (tools/).
live = { file = "skills/live.md" }             # -> activate claude-spt:live + Monitor relay (LiveAgent)
# `new-alarm` -> EXPLICIT OUT OF SCOPE for v1: no core `spt` alarm/timed-pulse primitive exists
# (legacy $LIVE TIMED PULSE is owl/Psyche-layer, not lifted into core; adapters don't add core
# features). doyle carries a parity-gap finding to mint a core deferred-pulse primitive; the skill
# lands only after that ships. No `[strings.skills].new-alarm` entry by design (visible-not-silent).

# ── [[hints]] ──────────────────────────────────────────────────────────────────────────────
# Once-per-session keyword hints (order significant, first match wins; case-insensitive substrings).
[[hints]]
keywords = ["go live", "live agent", "start live"]
text = "Tip: /sptc:live starts or resumes a live agent session."

[[hints]]
keywords = ["who am i", "my owl name", "my agent name", "my endpoint id"]
text = "Tip: run `spt whoami` to see this session's spt endpoint id."

[[hints]]
keywords = ["message another agent", "send to agent", "tell the other agent"]
text = "Tip: /sptc:send messages another spt agent; /sptc:ready first to receive replies."

[[hints]]
keywords = ["another machine", "pair machine", "across machines", "subnet", "join my network"]
text = "Tip: /sptc:subnet pairs machines into a private network so agents reach each other across nodes."


Now check schema accepts `host_binaries` (additive) before staging PREP 3.


cd C:/Users/decid/Documents/projects/spt-claude-code && grep -n 'host_binaries\|additionalProperties\|"adapter"' adapter/manifest.schema.json | head -40

Check schema for host_binaries and additionalProperties


7:    "adapter": {
13:      "additionalProperties": {
24:      "additionalProperties": {
93:      "additionalProperties": true
98:      "additionalProperties": true
109:    "adapter"
666:          "additionalProperties": {


1	{
2	  "$schema": "https://json-schema.org/draft/2020-12/schema",
3	  "title": "spt-core runtime manifest",
4	  "description": "Per-adapter runtime manifest for the spt-core harness contract. Authored as TOML (this schema describes the equivalent data model). A manifest declares only what varies per harness/shell; command templates are opaque strings spt-core never parses. Cross-field invariants (kind<->[shell] agreement, strategy/avenue field requirements) are enforced by spt-core's validate step beyond this schema.",
5	  "type": "object",
6	  "properties": {
7	    "adapter": {
8	      "$ref": "#/$defs/Adapter"
9	    },
10	    "hooks": {
11	      "description": "`[hooks.<event>]` — inbound hook table, keyed by harness event name.",
12	      "type": "object",
13	      "additionalProperties": {
14	        "$ref": "#/$defs/Hook"
15	      }
16	    },
17	    "session": {
18	      "description": "`[session]` — watched-dir keys plus the `[session.<role>]` templates.",
19	      "$ref": "#/$defs/Session"
20	    },
21	    "env": {
22	      "description": "`[env.<VAR>]` — env-var inject/read table.",
23	      "type": "object",
24	      "additionalProperties": {
25	        "$ref": "#/$defs/EnvVar"
26	      }
27	    },
28	    "history": {
29	      "anyOf": [
30	        {
31	          "$ref": "#/$defs/History"
32	        },
33	        {
34	          "type": "null"
35	        }
36	      ]
37	    },
38	    "digest": {
39	      "description": "`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).",
40	      "anyOf": [
41	        {
42	          "$ref": "#/$defs/Digest"
43	        },
44	        {
45	          "type": "null"
46	        }
47	      ]
48	    },
49	    "inject": {
50	      "anyOf": [
51	        {
52	          "$ref": "#/$defs/Inject"
53	        },
54	        {
55	          "type": "null"
56	        }
57	      ]
58	    },
59	    "identity": {
60	      "anyOf": [
61	        {
62	          "$ref": "#/$defs/Identity"
63	        },
64	        {
65	          "type": "null"
66	        }
67	      ]
68	    },
69	    "update": {
70	      "anyOf": [
71	        {
72	          "$ref": "#/$defs/Update"
73	        },
74	        {
75	          "type": "null"
76	        }
77	      ]
78	    },
79	    "shell": {
80	      "description": "`[shell]` body — present iff `adapter.kind = \"shell\"` (validated).",
81	      "anyOf": [
82	        {
83	          "$ref": "#/$defs/Shell"
84	        },
85	        {
86	          "type": "null"
87	        }
88	      ]
89	    },
90	    "profiles": {
91	      "description": "`[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace\noverlays declared by the adapter dev inside the parent manifest, updating\nas one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`]\nmerges one onto the base and re-validates the complete manifest. A bare\n`adapter_name` ignores these (parent unmodified); the composite\n`<adapter>:<profile>` selects one. **Local** (node-local, user-authored)\nprofiles live beside the adapter in the registry, never here.\n(CONTEXT.md §adapter profile.)",
92	      "type": "object",
93	      "additionalProperties": true
94	    },
95	    "strings": {
96	      "description": "`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core\nnever executes a string; command templates live in their own sections\nbehind registration). Dot-path-readable via `spt adapter get-string`, and\nit rides the same leaf-replace profile overlay as the rest of the manifest\n(a shipped or local profile may override base strings). Node-local; no\ncross-node sync. (CONTEXT.md §adapter strings.)",
97	      "type": "object",
98	      "additionalProperties": true
99	    },
100	    "hints": {
101	      "description": "`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints).\n**Order is significant** (first match wins). A profile overlays this by\nleaf-replace like any section — the array is replaced wholesale, never\nspliced (override/extend = re-declare).",
102	      "type": "array",
103	      "items": {
104	        "$ref": "#/$defs/Hint"
105	      }
106	    }
107	  },
108	  "required": [
109	    "adapter"
110	  ],
111	  "$defs": {
112	    "Adapter": {
113	      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
114	      "type": "object",
115	      "properties": {

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "spt-core runtime manifest",
  "description": "Per-adapter runtime manifest for the spt-core harness contract. Authored as TOML (this schema describes the equivalent data model). A manifest declares only what varies per harness/shell; command templates are opaque strings spt-core never parses. Cross-field invariants (kind<->[shell] agreement, strategy/avenue field requirements) are enforced by spt-core's validate step beyond this schema.",
  "type": "object",
  "properties": {
    "adapter": {
      "$ref": "#/$defs/Adapter"
    },
    "hooks": {
      "description": "`[hooks.<event>]` — inbound hook table, keyed by harness event name.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/Hook"
      }
    },
    "session": {
      "description": "`[session]` — watched-dir keys plus the `[session.<role>]` templates.",
      "$ref": "#/$defs/Session"
    },
    "env": {
      "description": "`[env.<VAR>]` — env-var inject/read table.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/EnvVar"
      }
    },
    "history": {
      "anyOf": [
        {
          "$ref": "#/$defs/History"
        },
        {
          "type": "null"
        }
      ]
    },
    "digest": {
      "description": "`[digest]` — the adapter-declared session-digest extractor seam (ADR-0019).",
      "anyOf": [
        {
          "$ref": "#/$defs/Digest"
        },
        {
          "type": "null"
        }
      ]
    },
    "inject": {
      "anyOf": [
        {
          "$ref": "#/$defs/Inject"
        },
        {
          "type": "null"
        }
      ]
    },
    "identity": {
      "anyOf": [
        {
          "$ref": "#/$defs/Identity"
        },
        {
          "type": "null"
        }
      ]
    },
    "update": {
      "anyOf": [
        {
          "$ref": "#/$defs/Update"
        },
        {
          "type": "null"
        }
      ]
    },
    "shell": {
      "description": "`[shell]` body — present iff `adapter.kind = \"shell\"` (validated).",
      "anyOf": [
        {
          "$ref": "#/$defs/Shell"
        },
        {
          "type": "null"
        }
      ]
    },
    "profiles": {
      "description": "`[profiles.<name>]` — **shipped** profile overlays: sparse leaf-replace\noverlays declared by the adapter dev inside the parent manifest, updating\nas one unit with it. Stored raw ([`toml::Value`]); [`crate::profile::resolve`]\nmerges one onto the base and re-validates the complete manifest. A bare\n`adapter_name` ignores these (parent unmodified); the composite\n`<adapter>:<profile>` selects one. **Local** (node-local, user-authored)\nprofiles live beside the adapter in the registry, never here.\n(CONTEXT.md §adapter profile.)",
      "type": "object",
      "additionalProperties": true
    },
    "strings": {
      "description": "`[strings]` — an adapter-authored KV tree of **opaque data** (spt-core\nnever executes a string; command templates live in their own sections\nbehind registration). Dot-path-readable via `spt adapter get-string`, and\nit rides the same leaf-replace profile overlay as the rest of the manifest\n(a shipped or local profile may override base strings). Node-local; no\ncross-node sync. (CONTEXT.md §adapter strings.)",
      "type": "object",
      "additionalProperties": true
    },
    "hints": {
      "description": "`[[hints]]` — once-per-session keyword hints (CONTEXT.md §keyword hints).\n**Order is significant** (first match wins). A profile overlays this by\nleaf-replace like any section — the array is replaced wholesale, never\nspliced (override/extend = re-declare).",
      "type": "array",
      "items": {
        "$ref": "#/$defs/Hint"
      }
    }
  },
  "required": [
    "adapter"
  ],
  "$defs": {
    "Adapter": {
      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
      "type": "object",
      "properties": {


111	  "$defs": {
112	    "Adapter": {
113	      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
114	      "type": "object",
115	      "properties": {
116	        "name": {
117	          "type": "string"
118	        },
119	        "kind": {
120	          "$ref": "#/$defs/AdapterKind",
121	          "default": "harness"
122	        },
123	        "version": {
124	          "type": "string"
125	        },
126	        "min_spt_core_version": {
127	          "description": "Lowest spt-core version this adapter tolerates (compat gate).",
128	          "type": "string"
129	        },
130	        "hostable_types": {
131	          "description": "Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).",
132	          "type": "array",
133	          "items": {
134	            "type": "string"
135	          }
136	        },
137	        "shortcut_basename": {
138	          "description": "Optional basename for the `spt endpoint run` picker's `<basename>-<id>`\nlauncher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default\n`spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts\n(spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted\nfrom serialization when absent). The picker reads it from the RESOLVED\nmanifest of the selected adapter.",
139	          "type": [
140	            "string",
141	            "null"
142	          ]
143	        }
144	      },
145	      "required": [
146	        "name",
147	        "version",
148	        "min_spt_core_version"
149	      ]
150	    },
151	    "AdapterKind": {
152	      "description": "The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven\nsurface (MANIFEST §Shell adapters).",
153	      "type": "string",
154	      "enum": [
155	        "harness",
156	        "shell"
157	      ]
158	    },
159	    "Hook": {
160	      "description": "`[hooks.<event>]` — one harness event → the `api` command it fires, the\nstdin fields it maps, and whether it can surface context to the agent.",
161	      "type": "object",
162	      "properties": {
163	        "fires": {
164	          "description": "Opaque `api …` command line the harness invokes for this event.",
165	          "type": "string"
166	        },
167	        "reads": {
168	          "type": "array",
169	          "items": {
170	            "type": "string"
171	          }
172	        },
173	        "can_inject": {
174	          "description": "Whether this hook can inject context (false ⇒ sentinel/relay fallback).",
175	          "type": "boolean",
176	          "default": false
177	        }
178	      },
179	      "required": [
180	        "fires"
181	      ]
182	    },
183	    "Session": {
184	      "description": "`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located\nwith the fixed set of `[session.<role>]` command templates.",
185	      "type": "object",
186	      "properties": {
187	        "commune_dir": {
188	          "type": [
189	            "string",
190	            "null"
191	          ]
192	        },
193	        "signoff_dir": {
194	          "type": [
195	            "string",
196	            "null"
197	          ]
198	        },
199	        "self": {
200	          "anyOf": [

  "$defs": {
    "Adapter": {
      "description": "`[adapter]` — the manifest header, readable before any update (compat gate).",
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "kind": {
          "$ref": "#/$defs/AdapterKind",
          "default": "harness"
        },
        "version": {
          "type": "string"
        },
        "min_spt_core_version": {
          "description": "Lowest spt-core version this adapter tolerates (compat gate).",
          "type": "string"
        },
        "hostable_types": {
          "description": "Endpoint types this adapter can host (`LiveAgent`, `Worker`, …).",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "shortcut_basename": {
          "description": "Optional basename for the `spt endpoint run` picker's `<basename>-<id>`\nlauncher shortcut (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic default\n`spt` (→ `spt-<id>`); an adapter sets this to brand its shortcuts\n(spt-claude-code → `cc`, giving `cc-<id>`). Additive + N-1-safe (omitted\nfrom serialization when absent). The picker reads it from the RESOLVED\nmanifest of the selected adapter.",
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "name",
        "version",
        "min_spt_core_version"
      ]
    },
    "AdapterKind": {
      "description": "The two adapter kinds. A `harness` hosts agents; a `shell` provides a driven\nsurface (MANIFEST §Shell adapters).",
      "type": "string",
      "enum": [
        "harness",
        "shell"
      ]
    },
    "Hook": {
      "description": "`[hooks.<event>]` — one harness event → the `api` command it fires, the\nstdin fields it maps, and whether it can surface context to the agent.",
      "type": "object",
      "properties": {
        "fires": {
          "description": "Opaque `api …` command line the harness invokes for this event.",
          "type": "string"
        },
        "reads": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "can_inject": {
          "description": "Whether this hook can inject context (false ⇒ sentinel/relay fallback).",
          "type": "boolean",
          "default": false
        }
      },
      "required": [
        "fires"
      ]
    },
    "Session": {
      "description": "`[session]` — the watched-dir keys (`commune_dir`/`signoff_dir`) co-located\nwith the fixed set of `[session.<role>]` command templates.",
      "type": "object",
      "properties": {
        "commune_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "signoff_dir": {
          "type": [
            "string",
            "null"
          ]
        },
        "self": {
          "anyOf": [


Schema `Adapter` def has no `additionalProperties: false` → defaults true → `host_binaries` validates as additive. PREP 3 safe (source manifest only; dist .spt stay as v0.2.0 release assets, untouched).

PREP 3 — stage `host_binaries` in source manifest:


Edit

hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]

hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]


The file C:\Users\decid\Documents\projects\spt-claude-code\adapter\claude-spt.toml has been updated successfully. (file state is current in your context — no need to Read it back)

hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]

# shortcut_basename). 0.7.0 is the floor this manifest's fields require.

+# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for

+# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking

+# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms

+# fallback (ADR-0021, CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the

+# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`

+# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]

# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is

# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher

# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam


Now author live.md (#1 self-contained + #2 humanize). First pull the canonical recipe from the published `spt how-to live` topic so I inline its content (read-only — embedding it, not redirecting the agent to it).


Bash

cd C:/Users/decid/Documents/projects/spt-claude-code && spt how-to live 2>&1 | head -120

Read published spt how-to live topic content


<owl_messages>
<owl_message from="doyle" priority="high">PLAN APPROVED — all correct. #1 (delete the how-to redirect, fold full recipe inline, tag the chain INTERIM/PREP-4), #2 (output-discipline block: tokens are internal parse contract, never echoed; sole user surface = the LIVE block). Good.

Q1 — the humanized LIVE block = YOUR CALL, no sign-off needed. spt-core stays harness-agnostic; the adapter owns its user-facing composition (CONTEXT harness-boundary). Draft + lock it. Light steer only: dense-but-clean, zero raw tokens, surface id · status · reachability · how-to-reply. Pin the canonical shape so it's reproducible, exactly as you said.

PREP-3 CAVEAT (verify before you ship it to the live manifest): the host_binaries field doesn't exist in 0.8.3's manifest parser yet (todlando adds it in v0.9.0). serde ignores unknown fields by default → 0.8.3 SHOULD silently ignore host_binaries = safe to stage. BUT confirm it: run `spt adapter add` (or your manifest load) against 0.8.3 — if it parses clean, stage it; if it REJECTS (strict/deny-unknown), HOLD PREP-3 with PREP-4 and log the finding (I'll fold it into todlando's wave). Don't ship a manifest 0.8.3 chokes on.

Governance noted: REQ-SKILL-LIVE doc-stage, re-tag in-commit, traceable green before done, perri trailer. Go. Ping me with the finding if PREP-3's schema check fails; otherwise carry on.
-- doyle</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


# how-to: live — run as a live agent (a Self + a Psyche companion)

A LIVE agent is a persistent being: a Self (this endpoint) plus a Psyche
companion process that carries its context across resets. A READY agent is the
lighter form — a perch with a poll/listen listener and no Psyche
(`how-to ready`). Go live for a companion that persists and communes; stay
ready for plain messaging.

Bringup — run the PERSISTENT relay loop (this is the live path):

    spt api listen <id> --manifest <manifest>

- Fire it through your harness's LONG-RUNNING-listener mechanism — a tool that
  keeps the process alive and re-fires per event WITHOUT terminating it. The
  loop binds the live perch, brings up your Psyche companion, and streams
  `<EVENT>` lines to stdout for the life of the session.
- Stderr signals: `BOUND:<id>` (perch established) then `READY:<id>` (the live
  relay is streaming). The stdout delivery shape is the same envelope as
  `how-to ready`.
- (v0.8.0+) `--adapter <name:profile>` resolves the registered manifest, so
  `--manifest` becomes optional once the adapter is registered.

Do NOT use `--once` for a live agent. `--once` drains the backlog and forwards
one delivery, then exits — the degenerate path for a harness that LACKS a
long-running listener. It never sustains a live session.

What makes the agent live is the manifest: it must declare
`[session.psyche_init]` (the Psyche-spawn seam). A manifest without that
section brings up a ready agent, not a live one. Liveness is
daemon-authoritative — the daemon hosts the Psyche off the perch's online
status, so the companion survives a relay restart.

Reset and reconcile: on a session boundary (a `/clear`-equivalent) re-fire the
same `spt api listen <id>` — the seed bridges the new session to the same
perch and the relay resumes.

Going offline: end the session (stop the listen process) to drop the relay;
`spt endpoint shutdown <id>` is the graceful ending — it tears the Psyche down
with the perch.


doyle approved + added PREP-3 caveat: must confirm live 0.8.x parser doesn't REJECT `host_binaries` (serde should ignore unknown, but verify via real manifest load; if reject → HOLD + log finding). ACK + check the validation path.


cd C:/Users/decid/Documents/projects/spt-claude-code && "$OWL" send --reply-to doyle doyle perri <<'EOF'
perri — APPROVED noted, executing. LIVE block = my call (dense/clean, zero raw tokens, surface id · status · reachability · reply-how), locking canonical shape. PREP-3 caveat understood: I'll verify host_binaries against the LIVE parser (not just the vendored JSON schema) via a non-mutating manifest load/validate — if it parses clean I stage; if it rejects (deny-unknown) I HOLD PREP-3 with PREP-4 and ping you the finding for todlando's wave. Won't ship a manifest 0.8.x chokes on. Proceeding.
EOF
echo "=== spt version ==="; spt --version 2>&1
echo "=== adapter subcommands ==="; spt adapter --help 2>&1 | head -40

ACK doyle + probe spt version and adapter validate path


➜ REPLIED:doyle
=== spt version ===
spt 0.8.2
=== adapter subcommands ===
Adapter registration: what this node can drive/launch.

The node-local registered set (one command for harness and shell adapters). Feeds creation-time
adapter selection, shell discovery, and the self-update ripple.

Usage: spt.exe adapter <COMMAND>

Commands:
  add             Register an adapter from a local path (a dir holding `manifest.toml`, or the
                  manifest file itself) or from GitHub (`--github user/repo`, cloned under
                  `adapters/_github/`). Manifest-first: an invalid manifest registers nothing.
                  Install is the first update — the declared `[update]` avenue is conducted once
                  after recording
  remove          Soft-deregister: hidden from new-creation/discovery; existing and live instances
                  keep running. The manifest's optional `uninstall` template is conducted only with
                  --force until quiesce detection lands
  list            List registered adapters (active and soft-deregistered), each followed by its
                  shipped + local profiles as composite options
  create-profile  Create (or overwrite) a **local** profile — a node-local sparse overlay registered
                  beside the adapter that survives `adapter add` re-registration. The overlay TOML
                  is read from `--from <file>` or piped stdin (empty = a placeholder profile to
                  populate later with `set-string`). Refuses a name shadowing a shipped profile, an
                  invalid name, or an overlay that loosens a consent floor — nothing is written
                  unless every check passes
  delete-profile  Delete a **local** profile. Refuses a shipped profile name (adapter-owned,
                  immutable) and errors if no local file exists
  get-string      Read a `[strings]` dot-path from an adapter option's merged view
                  (`<adapter>[:profile] <key.path>`). Resolves through the profile overlay like
                  every other consumer; prints the value (strings raw, else JSON). Exit 1 if the key
                  is unset. Strings are **data** — never executed
  digest-proof    Prove an adapter's `[digest]` extractor against a real log sample (ADR-0019). Runs
                  the declared extractor over `--sample <log>` (or the declared source) and prints
                  the parsed contract records, the rendered digest, and **every dropped line with
                  its reason** — the author-time answer to "`spt endpoint digest` returns nothing"
                  (no silent empty). Exit 1 if any line drops or nothing parses
  set-string      Set a `[strings]` dot-path on a **local** profile (`<adapter>:<profile>`). Sugar
                  over editing the overlay file; refuses a shipped profile and a bare option (a
                  local target is required — `create-profile` first)
  update          Update registered adapters that ship from their own GitHub releases: compare each
                  `[update] avenue = "gh_release"` adapter's latest release version against the


spt 0.8.2. Need a NON-mutating manifest parse test (avoid re-register → psyche-storm risk). Probe for a dry-run/validate path.


cd C:/Users/decid/Documents/projects/spt-claude-code && echo "=== adapter add help ==="; spt adapter add --help 2>&1 | head -40; echo "=== top-level: validate/manifest? ==="; spt --help 2>&1 | grep -iE 'valid|manifest|check|lint'

Probe adapter add flags and top-level validate commands


=== adapter add help ===
Register an adapter from a local path (a dir holding `manifest.toml`, or the manifest file itself)
or from GitHub (`--github user/repo`, cloned under `adapters/_github/`). Manifest-first: an invalid
manifest registers nothing. Install is the first update — the declared `[update]` avenue is
conducted once after recording

Usage: spt.exe adapter add [OPTIONS] [PATH]

Arguments:
  [PATH]  Local manifest source (omit when using --github or --release)

Options:
      --github <GITHUB>    GitHub source `user/repo` — shallow-clone the repo and register the clone
                           root. Manifest-first, then install via the declared `[update]` avenue
      --release <RELEASE>  GitHub release source `user/repo` — fetch the adapter archive asset from
                           the release and register it: ships built binaries, source-free and
                           versioned (the pattern for a monorepo whose adapter is a subdir)
      --tag <TAG>          Release tag for --release (default: the latest release)
      --asset <ASSET>      Release asset name for --release (default: adapter.spt — a tar archive
                           whose root holds manifest.toml + strings/ + binaries)
  -h, --help               Print help
=== top-level: validate/manifest? ===


No dry-run flag. Use doyle's sanctioned path — `spt adapter add` (manifest-first: invalid manifest registers nothing) — but against a throwaway probe NAME with `[update]` stripped, so the real `claude-spt` registration and its live perches are untouched (no re-host storm). Tests the actual 0.8.2 serde parser, not just the JSON schema.


cd C:/Users/decid/Documents/projects/spt-claude-code
TMP=$(mktemp -d)
cp -r adapter/* "$TMP"/
# Rename adapter to a throwaway probe name; strip the [update] block so `adapter add` won't fetch from GH.
# (host_binaries is already present in the copied toml.)
python - "$TMP/claude-spt.toml" <<'PY'
import re,sys
p=sys.argv[1]
s=open(p,encoding='utf-8').read()
s=s.replace('name = "claude-spt"','name = "claude-spt-hbprobe"',1)
# Remove the [update] table (from the line '[update]' up to the next blank-line-separated section start).
s=re.sub(r'\n\[update\]\n(?:.*\n)*?(?=\n# ──|\n\[)', '\n', s, count=1)
open(p,'w',encoding='utf-8').write(s)
print("probe manifest written")
PY
echo "=== host_binaries present in probe? ==="; grep -n 'host_binaries\|^\[update\]\|name = ' "$TMP/claude-spt.toml" | head
echo "=== attempt parse via adapter add (probe name) ==="
spt adapter add "$TMP/claude-spt.toml" 2>&1 | head -30
echo "EXIT=$?"
echo "=== cleanup probe ==="
spt adapter remove claude-spt-hbprobe 2>&1 | head -5
rm -rf "$TMP"

Validate host_binaries against live 0.8.2 parser via throwaway probe registration


probe manifest written
=== host_binaries present in probe? ===
29:name = "claude-spt-hbprobe"
36:# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
38:# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
42:host_binaries = ["claude"]
48:shortcut_basename = "cc"
103:parent_ancestor_name = "claude"
=== attempt parse via adapter add (probe name) ===
ADAPTER_ADD:claude-spt-hbprobe:Harness:Copy (registered)
ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)
EXIT=0
=== cleanup probe ===
ADAPTER_REMOVE:claude-spt-hbprobe (soft-deregistered; live instances unaffected)


PREP-3 **verified clean**: spt 0.8.2 parser accepted `host_binaries` — `ADAPTER_ADD:...:Copy (registered)`, EXIT=0. serde ignored the unknown field exactly as doyle predicted. Probe removed. No finding needed; staging stands.

Now #1+#2 — rewrite live.md self-contained with output discipline + canonical LIVE block.


Write

# /sptc:live — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].live = { file = "skills/live.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001). **This injection is self-contained** — everything you
> need to go live is below; you never need to run a `how-to` topic or any external lookup.

**Goal:** upgrade THIS Claude Code session to a **LiveAgent** — a reachable agent whose context is
backed by a **Psyche** (the daemon-managed companion that briefs your resume across `/clear`/compact
via commune deltas). This is NOT spawning a separate or nested session; it is making the session you
are in right now live.

**How a session becomes live (no "go-live" verb):** LiveAgent vs ReadyAgent is gated entirely by the
resolved manifest declaring `[session.psyche_init]`. Base `claude-spt` declares none (= ReadyAgent,
like `/sptc:ready`); the `claude-spt:live` profile overlays it (= LiveAgent). Bringing the perch up
under the `:live` composite is what tells the spt-core daemon to host this endpoint's Psyche. The
Psyche is **daemon-managed by contract** — do NOT try to start, poll, or tear down the Psyche
yourself; the daemon owns its whole lifecycle off the perch's online status (so the companion
survives a relay restart).

---

## Bringup — do this

1. **Ensure `spt` is available.** If `spt` is not on `PATH`, run `/sptc:setup` first, then continue.

2. **Pick the perch id.** Use a short lowercase id, or reuse this session's id from `spt whoami`. Call
   it `<id>`. Use this session's id from `spt whoami` for `<sid>` (the session-id).

3. **Run the live relay as a single PERSISTENT background task** — Claude Code's **Monitor** tool (a
   long-running listener that keeps the process alive and re-fires per event WITHOUT terminating).
   This one resident process IS the live delivery pipe. It seeds the `:live` composite and binds the
   relay in the SAME process so both share one parent-pid anchor (see the INTERIM note below). The
   task body:

   ```sh
   # Anchor pid: spt probes WINDOWS pids. From git-bash, $$ is the MSYS pid (reads as a dead anchor);
   # use the WINPID column from `ps` (col 4) when present, else $$ is already the real pid.
   ANCHOR=$(ps -p $$ 2>/dev/null | awk 'NR==2{print $4}'); case "$ANCHOR" in ''|*[!0-9]*) ANCHOR=$$;; esac
   spt api --adapter claude-spt:live seed   --pid        "$ANCHOR" --session-id "<sid>"
   spt api --adapter claude-spt:live listen --parent-pid "$ANCHOR" "<id>"
   ```

   - The `listen` line **blocks for the session's life**: the spooled backlog drains first, then each
     delivery streams to stdout as it arrives.
   - Do **not** use `--once` — that drains the backlog and forwards a single delivery, then exits (the
     degenerate path for a harness with no long-running listener). It never sustains a live session.
   - On a registered adapter, `--adapter claude-spt:live` resolves the registered manifest, so no
     `--manifest` is needed.

4. **Bind-before-send / readiness.** Watch the relay's output for the bringup markers (see OUTPUT
   DISCIPLINE): `BOUND:<id>` (perch established) then `READY:<id>` (the relay is streaming). Only once
   `READY:<id>` appears is the live pipe up — do not announce LIVE before then.

5. **Poll/relay reconcile (avoids double-delivery).** While the Monitor relay is up it is the *single*
   delivery pipe — treat it as authoritative. The `UserPromptSubmit` `api poll` hook must NOT also
   drain; the per-prompt poll no-ops while the relay is live and resumes only if the relay goes down.

6. **Delivery + reply shape.** Inbound arrives on the relay stream as the
   `<EVENT type="msg" from="<sender>">body</EVENT>` envelope (body HTML-escaped, newlines as `<br>`).
   To reply, pipe the body to `spt send --reply-to <sender>`.

7. **Across boundaries.** As a live agent you now have a Psyche — use `/sptc:commune` to push a
   context delta before a `/clear` or compact so your resume is briefed. On a session boundary,
   re-fire the same relay (step 3) — the seed bridges the new session to the same perch and the relay
   resumes. To go offline gracefully (tears the Psyche down with the perch), use `/sptc:force-stop`
   (`spt endpoint shutdown <id>`).

> **INTERIM (PREP-4 / spt-core v0.9.0).** The seed+listen-chained-in-one-process step exists only
> because today's seed and listen must share a parent-pid anchor. Once spt-core ships the
> adapter-agnostic seed (SessionStart seeds by pid; `api listen` resolves the owning adapter/profile
> by pid), this collapses to a bare `spt api listen <id>` under Monitor — no shared-pid chaining and
> no `--adapter`. Do not pre-build that path; it activates on doyle's v0.9.0 ship.

---

## OUTPUT DISCIPLINE — what the user sees

The relay and seed emit machine markers on stdout/stderr: `SEEDED:<pid>`, `NO_SEED:<pid>`,
`STALE_SEED:<pid>`, `BOUND:<id>`, `READY:<id>`, plus any raw token / step lines. **These are an
internal parse contract — you read them to drive bringup; you NEVER echo them to the user.** Do not
surface raw spt-core plumbing.

**The ONLY user-facing surface for going live is the canonical LIVE block below.** Emit it verbatim
(substituting `<id>`) once `READY:<id>` is observed — and nothing else from the bringup:

```
**LIVE.** Session `<id>` is now a LiveAgent (Psyche-backed).
- Perch `<id>` — status online; Psyche companion hosted by the daemon.
- Reachable — other agents reach you with `/sptc:send <id>` (or `spt send <id>`).
- Inbound — streams into this session live; the Monitor relay is your delivery pipe.
- Reply — pipe a reply body to `spt send --reply-to <sender>`.
- Across resets — `/sptc:commune` before a `/clear`/compact; `/sptc:force-stop` to go offline (tears down the Psyche).
```

If bringup fails (e.g. `NO_SEED`/`STALE_SEED` and no `READY:<id>` within ~25s), do NOT show the LIVE
block — report a short plain-language failure ("could not bring the live perch up") and the most
likely cause (stale pid anchor, or run `/sptc:setup`), still without dumping the raw markers.

> The Psyche runner (`claude-spt-psyche`, declared in `[profiles.live.session.psyche_init]`) is the
> resident headless-`claude` companion the daemon launches detached — it keeps a Psyche `claude`
> session alive (one `--continue` turn per daemon pulse) and authors commune drops. You never invoke
> it directly; the daemon does.
<!-- [doc->REQ-SKILL-LIVE] -->