File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\ci\hooks\poll-int.sh (file state is current in your context — no need to Read it back)

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

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

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Tear down the throwaway perch however we exit.
trap 'spt endpoint shutdown "$BID" >/dev/null 2>&1 || true; spt endpoint stop "$BID" >/dev/null 2>&1 || true' EXIT INT TERM

# Establish a pollable perch (non-blocking: seed + bind, no listener -> sends spool).
spt api --adapter "$ADAPTER" seed --pid $$ --session-id "$BSID" >/dev/null 2>&1
bound=$(spt api --adapter "$ADAPTER" bind "$BID" --set-session-id "$BSID" --session-id "$BSID" 2>&1)
case "$bound" in *BOUND:"$BID"*) ok "perch bound (non-blocking)" ;; *) bad "bind failed: $bound"; echo "MSG-ENVELOPE-INT FAIL"; exit 1 ;; esac

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

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

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

# 2. Hook parser confirm-match: render_frames decodes the drain to our additionalContext shape.
rendered=$( . "$ROOT/plugin/sptc/hooks/_common.sh"; render_frames "$raw" )
want=$(printf '<sptc_messages from="probe-int">\nhello from probe\nsecond <line> & "stuff"\n</sptc_messages>')
case "$rendered" in *"$want"*) ok "render_frames confirm-match: drain -> <sptc_messages>" ;; *) bad "render mismatch; got=[$rendered]" ;; esac

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

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


doyle CAPSTONE CONFIRMED — terminal, "you're clear." Absorbed. Run the new int to verify it passes.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; SPTC_ACCEPTANCE=1 sh ci/hooks/poll-int.sh 2>&1

Run poll-int acceptance test


ok   perch bound (non-blocking)
ok   api poll emits canonical <EVENT> msg envelope (escaped body)
ok   no __REPLY_TO__ relic (ADR-0020 shipped)
ok   render_frames confirm-match: drain -> <sptc_messages>
ok   multi-message: 2 whole <EVENT> envelopes split on </EVENT>
MSG-ENVELOPE-INT OK


poll-int 5/5. Now activate the int stages + update docs. Read the relevant traceable-reqs entries + check acceptance wiring.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== HOOKS-API + UPS-INJECTION reqs ==="; grep -nB1 -A4 'REQ-DIST-HOOKS-API\|REQ-UPS-INJECTION\|REQ-MSG-ENVELOPE' traceable-reqs.toml
echo "=== how acceptance/int tests run in gates ==="; grep -rn "registration-int\|SPTC_ACCEPTANCE\|poll-int\|ci/hooks\|ci/manifest" ci/run-gates.sh ci/acceptance 2>/dev/null | head

Inspect req entries + acceptance wiring


=== HOOKS-API + UPS-INJECTION reqs ===
71-[[requirements]]
72:id = "REQ-DIST-HOOKS-API"
73-title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
74-required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.
75-
76-[[requirements]]
--
86-[[requirements]]
87:id = "REQ-UPS-INJECTION"
88-title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
89-required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.
90-
91-# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
=== how acceptance/int tests run in gates ===
ci/run-gates.sh:40:if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
ci/run-gates.sh:41:  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
ci/acceptance/run-acceptance.sh:6:# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so
ci/acceptance/run-acceptance.sh:13:if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then
ci/acceptance/run-acceptance.sh:14:  echo "SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)"


1	#!/bin/sh
2	# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired
3	# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this
4	# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]
5	#
6	# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so
7	# the deterministic gate run stays green on hosts without claude/auth. Real-claude execution is the
8	# cross-process evidence for REQ-CI-ACCEPTANCE: [int->REQ-CI-ACCEPTANCE]
9	set -u
10	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
11	. "$ROOT/ci/acceptance/lib.sh"
12
13	if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then
14	  echo "SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)"
15	  exit 0
16	fi
17	if ! command -v claude >/dev/null 2>&1; then
18	  echo "SKIP acceptance: no \`claude\` on PATH (fleet host without the harness)"
19	  exit 0
20	fi
21
22	# Disposable identity FIRST — before any scaffold/spawn — so nothing can resolve the operator perch.
23	# [int->REQ-HAZARD-PERCH-COLLISION]
24	sptc_ci_identity "$$"          # sets+exports SPTC_CI_ID / SPT_AGENT_ID / OWL_SESSION_ID in-shell
25	sptc_ci_is_disposable "$SPTC_CI_ID" || { echo "FATAL: refusing to spawn under non-disposable id '$SPTC_CI_ID'"; exit 2; }
26	echo "acceptance: SUT identity = $SPTC_CI_ID (operator perch protected)"
27
28	work=$(mktemp -d 2>/dev/null) || { echo "FATAL: mktemp -d failed"; exit 2; }
29	proj="$work/proj"
30	digest="$work/digest.txt"
31	: > "$digest"
32	cleanup() { rm -rf "$work" 2>/dev/null || true; }
33	trap cleanup EXIT INT TERM
34
35	sptc_ci_mkproject "$proj" "$digest" >/dev/null || { echo "FATAL: scaffold failed"; exit 2; }
36
37	probe="sptc-acceptance-probe-$$"
38	echo "acceptance: driving real claude -p in $proj (probe='$probe')"
39	# Drive the SUT. We assert on the hook side-effect, not stdout — model text is irrelevant.
40	( cd "$proj" && claude -p "$probe" >/dev/null 2>&1 ) || \
41	  echo "note: claude -p exited non-zero (model/auth) — asserting on hook side-effect regardless" >&2
42
43	rc=0
44	sptc_ci_assert "UPS hook fired in real claude" "UPS_FIRED:$probe" "$digest" || rc=1
45
46	printf '\n=== ACCEPTANCE: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
47	exit "$rc"
48

#!/bin/sh
# sptc CI ACCEPTANCE — slice 1: spawn a REAL `claude` as the system-under-test, assert it fired
# the UserPromptSubmit hook (a deterministic digest-marker side-effect). The LLM is the SUT; this
# orchestration is deterministic and never judges model text. [impl->REQ-CI-ACCEPTANCE]
#
# SLOW LANE — env-gated. Skips cleanly (rc 0) unless SPTC_ACCEPTANCE=1 AND `claude` is on PATH, so
# the deterministic gate run stays green on hosts without claude/auth. Real-claude execution is the
# cross-process evidence for REQ-CI-ACCEPTANCE: [int->REQ-CI-ACCEPTANCE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
. "$ROOT/ci/acceptance/lib.sh"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then
  echo "SKIP acceptance: set SPTC_ACCEPTANCE=1 to run the real-claude lane (deterministic gates unaffected)"
  exit 0
fi
if ! command -v claude >/dev/null 2>&1; then
  echo "SKIP acceptance: no \`claude\` on PATH (fleet host without the harness)"
  exit 0
fi

# Disposable identity FIRST — before any scaffold/spawn — so nothing can resolve the operator perch.
# [int->REQ-HAZARD-PERCH-COLLISION]
sptc_ci_identity "$$"          # sets+exports SPTC_CI_ID / SPT_AGENT_ID / OWL_SESSION_ID in-shell
sptc_ci_is_disposable "$SPTC_CI_ID" || { echo "FATAL: refusing to spawn under non-disposable id '$SPTC_CI_ID'"; exit 2; }
echo "acceptance: SUT identity = $SPTC_CI_ID (operator perch protected)"

work=$(mktemp -d 2>/dev/null) || { echo "FATAL: mktemp -d failed"; exit 2; }
proj="$work/proj"
digest="$work/digest.txt"
: > "$digest"
cleanup() { rm -rf "$work" 2>/dev/null || true; }
trap cleanup EXIT INT TERM

sptc_ci_mkproject "$proj" "$digest" >/dev/null || { echo "FATAL: scaffold failed"; exit 2; }

probe="sptc-acceptance-probe-$$"
echo "acceptance: driving real claude -p in $proj (probe='$probe')"
# Drive the SUT. We assert on the hook side-effect, not stdout — model text is irrelevant.
( cd "$proj" && claude -p "$probe" >/dev/null 2>&1 ) || \
  echo "note: claude -p exited non-zero (model/auth) — asserting on hook side-effect regardless" >&2

rc=0
sptc_ci_assert "UPS hook fired in real claude" "UPS_FIRED:$probe" "$digest" || rc=1

printf '\n=== ACCEPTANCE: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
exit "$rc"


Both int tests are manual (SPTC_ACCEPTANCE-gated), matching registration-int's convention; the `[int->...]` tags give traceable evidence. Activate int for both reqs.


Edit

id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope, render_frames confirm-match (ADR-0020 verified on the published surface, __REPLY_TO__ relic gone).


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


71	[[requirements]]
72	id = "REQ-DIST-HOOKS-API"
73	title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
74	required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.
75
76	[[requirements]]
77	id = "REQ-DIST-MANIFEST-SCHEMA"
78	title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
79	required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".
80
81	[[requirements]]
82	id = "REQ-DIST-SHORTCUT-BASENAME"
83	title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
84	required_stages = ["doc", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). unit: tests/manifest-shortcut.sh (brand-value regression guard on the declarative field). impl (the cc launcher) + int (real `spt endpoint run` picker emits cc-<id>) deferred to the endpoint-run/cc-launcher parity slice — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`) and needs `[session.self]`. The field itself is declarative config, not production code, so it carries no impl tag (the scanner classifies impl by file type — config is not impl).
85
86	[[requirements]]
87	id = "REQ-UPS-INJECTION"
88	title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
89	required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

[[requirements]]
id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

[[requirements]]
id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".

[[requirements]]
id = "REQ-DIST-SHORTCUT-BASENAME"
title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
required_stages = ["doc", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). unit: tests/manifest-shortcut.sh (brand-value regression guard on the declarative field). impl (the cc launcher) + int (real `spt endpoint run` picker emits cc-<id>) deferred to the endpoint-run/cc-launcher parity slice — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`) and needs `[session.self]`. The field itself is declarative config, not production code, so it carries no impl tag (the scanner classifies impl by file type — config is not impl).

[[requirements]]
id = "REQ-UPS-INJECTION"
title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.


required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope (ADR-0020), confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).


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

# traceable-reqs manifest for spt-claude-code — SEED.
#
# Authoritative requirement registry: every requirement and every KNOWN-HAZARDS
# invariant lives here as a REQ-* id (see docs/TRACEABILITY.md for the full contract).
#
# SEED YOUR REQs FROM SCOPE.md "Parity-trim" — the DROP / TRANSFORM / KEEP / ADD buckets are
# the parity reference. Mint a REQ-* per surface you port/transform/add as you START each
# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
#
# ACQUIRE the CLI (it is a released binary, not a crate dependency):
#   gh release download --repo BigscreenVR/traceable-reqs \
#     --pattern '*<your-platform>' --output traceable-reqs   # then chmod +x
# (see INSTANTIATE.md for per-platform patterns + CI wiring.) Validate this seed against
# the installed CLI before relying on it: `traceable-reqs check`.
#
# THE FOUR-STAGE CONTRACT: every requirement is traced from
#   doc  (prose/design)  ->  impl (production code)  ->  unit (unit test)  ->  int (cross-process/node)
# Tag evidence in the SAME commit, on/above the real evidence:
#   // [impl->REQ-FOO]   // [unit->REQ-FOO]   <!-- [doc->REQ-FOO] -->
#
# ACTIVATION MODEL: every requirement starts inactive (`required_stages = []`) so `check`
# stays green pre-code. A milestone ACTIVATES its requirements by setting their real
# `required_stages` when work on them begins. Deferred items stay [] until promoted.
# Never declare the full end-state stage list up front — activate incrementally as evidence
# lands (add "int" at the final integrating wave).

[scan]
# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
# nonexistent root is a hard manifest_error, so only list what is on disk now.
roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "adapter", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]

[policy]
# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
# of the coverage gate.
required_stages = []

# ───────────────────────────── Requirements ─────────────────────────────────
# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
#
# [[requirements]]
# id = "REQ-EXAMPLE-1"
# title = "One-line statement of the requirement"
# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
#
# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
# each requiring at least "unit" (add "int" where cross-process/cross-node):
#
# [[requirements]]
# id = "REQ-HAZARD-EXAMPLE"
# title = "The invariant, stated so a test can prove it"
# required_stages = []

# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not
# the whole registry up front). All inactive (required_stages = []) so `check` stays
# green until body #2 lands evidence and flips each to ["doc"] / ["doc","impl"] etc.
# Seeded from SCOPE.md "Parity-trim" (Distribution + ADD buckets).

[[requirements]]
id = "REQ-DIST-PLUGIN-SKELETON"
title = "Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 + RELEASE-RUNBOOK. impl: ci/publish/{validate,package}-skeleton.sh (installability gate + publish mechanic). unit: tests/skeleton-validate.sh (pass real + fail tampered).

[[requirements]]
id = "REQ-DIST-BOOTSTRAP-INSTALL"
title = "SessionStart bootstrap installs spt-core when absent (invisible-installer pattern)"
required_stages = ["doc", "impl"]  # doc: ADR-0001. impl: plugin/sptc/bootstrap.{sh,ps1}.

[[requirements]]
id = "REQ-DIST-HOOKS-API"
title = "hooks.json delegates to `spt api` rather than carrying adapter logic"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

[[requirements]]
id = "REQ-DIST-MANIFEST-SCHEMA"
title = "CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)"
required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001 (manifest layer + version-of-truth). impl: adapter/claude-spt.toml + vendored adapter/manifest.schema.json + ci/manifest/{validate_manifest.py,check-manifest.sh} (gate). unit: tests/manifest-schema.sh (pass real + fail tampered). int: ci/manifest/registration-int.sh — real `spt adapter add` (v0.7.0) cross-field registration-validation accepted the manifest, shipped profile resolved, [strings] read + overlay observable; 6/6 green 2026-06-15 (registry left clean via soft-remove). Cross-field discovery: [digest] requires source-or-[history].locate_template (not in JSON schema) — fixed with source="{home}/.claude/projects".

[[requirements]]
id = "REQ-DIST-SHORTCUT-BASENAME"
title = "The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = \"cc\" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)"
required_stages = ["doc", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). unit: tests/manifest-shortcut.sh (brand-value regression guard on the declarative field). impl (the cc launcher) + int (real `spt endpoint run` picker emits cc-<id>) deferred to the endpoint-run/cc-launcher parity slice — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`) and needs `[session.self]`. The field itself is declarative config, not production code, so it carries no impl tag (the scanner classifies impl by file type — config is not impl).

[[requirements]]
id = "REQ-UPS-INJECTION"
title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"
required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +
# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a
# later slice (doc-only now). All harness-agnostic shell; runs on the Win+Linux fleet.

[[requirements]]
id = "REQ-CI-GATES"
title = "Deterministic gates (shell-syntax + unit tests + traceable-reqs check + manifest-schema + docs-drift), each a binary pass/fail, runnable as one command"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-MANUAL"
title = "A manual 'run gates' command always exists — the same gate scripts runnable by hand on any fleet host"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-TRIGGER"
title = "A git push hook fires the gates: pings a fleet runner-agent over spt rather than polling"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-BUS"
title = "CI progress/results report over spt messaging (dogfood the product as its own CI nervous system)"
required_stages = ["doc", "impl"]

[[requirements]]
id = "REQ-CI-OWL-DISCOVERY"
title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
required_stages = ["doc", "impl", "unit"]

[[requirements]]
id = "REQ-CI-ACCEPTANCE"
title = "Acceptance = scripted orchestration spawning real claude/headless sessions as the system-under-test, asserting spt-state/digest output (LLM is SUT, never the runner)"
required_stages = ["doc", "impl", "int"]  # doc: docs/CI.md. impl: ci/acceptance/{lib,run-acceptance}.sh + harness unit. int: real `claude -p` spawn asserted UPS-hook-fires green 2026-06-15 under disposable id sptc-ci-40322 (operator perch perri survived — guard proven e2e).

[[requirements]]
id = "REQ-HAZARD-PERCH-COLLISION"
title = "The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)"
required_stages = ["unit"]  # unit: tests/acceptance-harness.sh. Diagnosed 2026-06-15 (operator): nested claude loading the spt plugin established a perch under "perri" and displaced the live poll stream.

[[requirements]]
id = "REQ-HAZARD-MSYS-PATHCONV"
title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
required_stages = ["unit"]  # unit: tests/msys-hazard.sh. Confirmed live 2026-06-15 (UPS validation run A).

# ── Milestone: DOCS-SITE (mdBook, drift-gated) — minted on-start. Dual-audience product docs
# in docs-site/, built with mdBook, gated against drift. Seeded from docs/DOCS-STRATEGY.md.

[[requirements]]
id = "REQ-DOCS-SITE"
title = "docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)"
required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.

[[requirements]]
id = "REQ-DOCS-DRIFT"
title = "Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build"
required_stages = ["doc", "impl", "unit"]  # doc: DOCS-STRATEGY #10. impl: ci/docs/{gen-llms,check-docs}.sh + run-gates wiring. unit: tests/docs-gen.sh.

[[requirements]]
id = "REQ-SKILL-VERSION"
title = "/spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)"
required_stages = []

-required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int (real-CC e2e) held for throwaway validation.

+required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0001/0002. impl: hooks.json + wrappers. unit: tests/hooks-parse.sh. int: ci/hooks/poll-int.sh — `spt api poll` on live v0.7.1 emits the canonical <EVENT> envelope (ADR-0020), confirm-matched through render_frames; __REPLY_TO__ relic gone, multi-drain splits on </EVENT>. 5/5 green 2026-06-15 (throwaway perch, torn down).


required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.


title = "UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands"

-required_stages = ["doc", "impl", "unit"]  # doc: ADR-0002. impl: user-prompt-submit.sh. unit: render_frames test. int (UPS-fires + multi-frame post-F-002) held for throwaway validation.

+required_stages = ["doc", "impl", "unit", "int"]  # doc: ADR-0002. impl: user-prompt-submit.sh (skill-injection + drain) + _common.sh. unit: render_frames + sptc_skill_key tests. int: ci/hooks/poll-int.sh — the poll->additionalContext drain round-trip vs live v0.7.1 (<EVENT> envelope, render_frames confirm-match, multi-message). UPS-fires-on-slash empirical = ADR-0002 validation (CC 2.1.177); skill-injection resolve = registration-int 4c.

# ── Milestone: CI (autonomous fleet) — minted on-start. Deterministic gates + git push-trigger +

# spt reporting bus. Seeded from SCOPE.md "CI model" + docs/CI.md. Real-claude acceptance is a


Verify traceable recognizes the int evidence.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; traceable-reqs check 2>&1 | grep -E "HOOKS-API|UPS-INJECTION|coverage|FAIL|missing" | head

Traceable check for int coverage


  [OK] REQ-DIST-HOOKS-API  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int


Both reqs now `+int`. Update the docs: F-002 → verified-shipped, F-003 docs-residual → closed, ADR-0002 open#1 → resolved, plan held-items.


Edit

- **Transitional (MERGED, not yet PUBLISHED):** the refactor making `api poll` (and worker-poll)
  emit `<EVENT>` (`REQ-MSG-ENVELOPE`, ADR-0020) is **merged to spt-core main** (`25ffd7a`,
  2026-06-15) — `__REPLY_TO__` relic deleted, `<EVENT>` is the sole arriving format at every surface.
  **But the published binary (v0.7.0) still carries the relic** at the poll surface. Our byte-capture
  tests the **shipped** surface, so the `REQ-MSG-ENVELOPE` int flip waits for the next **RELEASE**
  that carries ADR-0020 (operator-gated publish, bundling the update-apply fix) — **not** the merge.
  Until then we keep parsing the v0.7.0 surface as-is (relic present) and build for canonical
  `<EVENT>` without validating against current poll output. doyle pings with the hash on **publish**;
  the throwaway byte-capture then confirms `<EVENT>` and flips HOOKS-API / UPS-INJECTION `int`.

- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
    is the **exact** `render_frames` decode rule.
  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
    published binary**.
  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
    confirm-match PASS). `notify` events ride the same envelope.
  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.


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

- **Transitional (MERGED, not yet PUBLISHED):** the refactor making `api poll` (and worker-poll)
  emit `<EVENT>` (`REQ-MSG-ENVELOPE`, ADR-0020) is **merged to spt-core main** (`25ffd7a`,
  2026-06-15) — `__REPLY_TO__` relic deleted, `<EVENT>` is the sole arriving format at every surface.
  **But the published binary (v0.7.0) still carries the relic** at the poll surface. Our byte-capture
  tests the **shipped** surface, so the `REQ-MSG-ENVELOPE` int flip waits for the next **RELEASE**
  that carries ADR-0020 (operator-gated publish, bundling the update-apply fix) — **not** the merge.
  Until then we keep parsing the v0.7.0 surface as-is (relic present) and build for canonical
  `<EVENT>` without validating against current poll output. doyle pings with the hash on **publish**;
  the throwaway byte-capture then confirms `<EVENT>` and flips HOOKS-API / UPS-INJECTION `int`.

- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**
  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api
  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:
  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe
second <line>
    & "stuff"</EVENT>\n` — body escaping (`
` + `</>/"` + `&` last)
    is the **exact** `render_frames` decode rule.
  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook
    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the
    published binary**.
  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser
    confirm-match PASS). `notify` events ride the same envelope.
  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api
  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**
  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.

- **Our parser** now targets `<EVENT>` (the `from` attr → `<sptc_messages from="…">`, `<br>` →

newline, entity unescape with `&` last) — exactly the live-agent body-parsing rule. See

`render_frames` in `plugin/sptc/hooks/_common.sh` + `tests/hooks-parse.sh` (multi-message covered).

-- **Transitional (MERGED, not yet PUBLISHED):** the refactor making `api poll` (and worker-poll)

-  emit `<EVENT>` (`REQ-MSG-ENVELOPE`, ADR-0020) is **merged to spt-core main** (`25ffd7a`,

-  2026-06-15) — `__REPLY_TO__` relic deleted, `<EVENT>` is the sole arriving format at every surface.

-  **But the published binary (v0.7.0) still carries the relic** at the poll surface. Our byte-capture

-  tests the **shipped** surface, so the `REQ-MSG-ENVELOPE` int flip waits for the next **RELEASE**

-  that carries ADR-0020 (operator-gated publish, bundling the update-apply fix) — **not** the merge.

-  Until then we keep parsing the v0.7.0 surface as-is (relic present) and build for canonical

-  `<EVENT>` without validating against current poll output. doyle pings with the hash on **publish**;

-  the throwaway byte-capture then confirms `<EVENT>` and flips HOOKS-API / UPS-INJECTION `int`.

+- **VERIFIED ON THE PUBLISHED SURFACE (v0.7.1, 2026-06-15).** ADR-0020 shipped in **v0.7.1**

+  (counter 13); `__REPLY_TO__` relic gone. A throwaway byte-capture against the live 0.7.1 `spt api

+  poll` drain (`od`-verified) confirms the canonical envelope end-to-end:

+  - single msg: `<EVENT type="msg" from="probe-sender">hello from probe<br>second <line>

+    & "stuff"</EVENT>\n` — body escaping (`<br>` + `</>/"` + `&` last)

+    is the **exact** `render_frames` decode rule.

+  - whole envelopes, self-delimiting **and** `\n`-framed; **no `<EVENT-PART>`** on a normal hook

+    drain; **no `__REPLY_TO__`**. Multi-drain splits cleanly on `</EVENT>` → **F-002 dissolved on the

+  - raw drain piped through our `render_frames` → correct `<sptc_messages from=…>` (parser

+    confirm-match PASS). `notify` events ride the same envelope.

+  Locked in by `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated, ≥0.7.1, 5/5): bind→send→`api

+  poll`→`render_frames` assert. **`REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION` `int` flipped GREEN.**

+  ADR-0020 loop closed: design→impl→gate→ship→real-surface-verify.


| F-002 | 2026-06-15 | **resolved-by-design** (ADR-0020) — envelope self-delimits; impl refactor pending | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **resolved (capability shipped); residual = docs-visibility (doyle publishing)** | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |

| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |


| F-002 | 2026-06-15 | **resolved-by-design** (ADR-0020) — envelope self-delimits; impl refactor pending | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **resolved (capability shipped); residual = docs-visibility (doyle publishing)** | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |

| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |
| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |

| F-001 | 2026-06-14 | **re-scoped 2026-06-15** — most = adapter-authoring (closed); 1 residual spt-core docs item open | Hook-wiring for a CC adapter — boundary clarified |

-| F-002 | 2026-06-15 | **resolved-by-design** (ADR-0020) — envelope self-delimits; impl refactor pending | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |

-| F-003 | 2026-06-15 | **resolved (capability shipped); residual = docs-visibility (doyle publishing)** | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |

+| F-002 | 2026-06-15 | **RESOLVED-SHIPPED (v0.7.1)** — `<EVENT>` verified on the published `api poll` surface; F-002 dissolved; int flipped | `api poll` agent path has no inter-frame delimiter → multi-message drains are unsplittable |

+| F-003 | 2026-06-15 | **RESOLVED + docs CLOSED (v0.7.1)** — capability shipped; the file-pointer syntax is now on the published surface | File-backed `[strings]` IS shipped (value-position table pointer `key = { file = "rel" }`) but was **undocumented** on the published surface |


the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**doyle owns it** and is folding the exact byte behavior above into the docs-site manifest reference +
`MANIFEST.md` in the same docs batch. F-003 stays logged as a **DOCS finding** until that publishes.

the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
F-003 fully resolved.


the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**doyle owns it** and is folding the exact byte behavior above into the docs-site manifest reference +
`MANIFEST.md` in the same docs batch. F-003 stays logged as a **DOCS finding** until that publishes.

the pointer syntax was undocumented on the GH-Pages surface (it lived in spt-core-internal
`CONTEXT.md §adapter strings`) — same class as the `{key}` catalog + `[digest]` cross-field rule.
**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the
`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +
`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —
F-003 fully resolved.

"skills/<x>.md" }` over `adapter/strings/skills/<x>.md`. **Residual = published-docs visibility only:**

-**doyle owns it** and is folding the exact byte behavior above into the docs-site manifest reference +

-`MANIFEST.md` in the same docs batch. F-003 stays logged as a **DOCS finding** until that publishes.

+**CLOSED in v0.7.1 (2026-06-15):** doyle published the `[strings]` file-pointer syntax (plus the

+`{key}` substitution catalog and the `[digest]` register rule) to the docs-site manifest reference +

+`MANIFEST.md`. The next adapter author discovers the pointer form from the published surface alone —


Update ADR-0002 Open#1 (now verified) + the plan held-item notes.


127	## Open (remaining `int` — gated on the spt-core poll refactor)
128
129	1. **`api poll` → `additionalContext` round-trip** against a real spt perch, once `REQ-MSG-ENVELOPE`
130	   lands (poll emits `<EVENT>`): capture actual stdout bytes, confirm-match to doyle, flip `int`.
131	2. **Large-drain injection size** — CC `additionalContext` caps at 10k chars; add truncate/spill in
132	   the UPS wrapper (adapter-side; `int`).
133

## Open (remaining `int` — gated on the spt-core poll refactor)

1. **`api poll` → `additionalContext` round-trip** against a real spt perch, once `REQ-MSG-ENVELOPE`
   lands (poll emits `<EVENT>`): capture actual stdout bytes, confirm-match to doyle, flip `int`.
2. **Large-drain injection size** — CC `additionalContext` caps at 10k chars; add truncate/spill in
   the UPS wrapper (adapter-side; `int`).


## Open (remaining `int` — gated on the spt-core poll refactor)

1. **`api poll` → `additionalContext` round-trip** against a real spt perch, once `REQ-MSG-ENVELOPE`
   lands (poll emits `<EVENT>`): capture actual stdout bytes, confirm-match to doyle, flip `int`.
2. **Large-drain injection size** — CC `additionalContext` caps at 10k chars; add truncate/spill in
   the UPS wrapper (adapter-side; `int`).

## Open / resolved `int`

1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** ADR-0020 shipped
   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed
   the canonical `<EVENT type="msg" from=…>body</EVENT>\n` envelope (no `__REPLY_TO__`, no
   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-
   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`
   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.
2. **Large-drain injection size** — CC `additionalContext` caps at ~10k chars; add truncate/spill in
   the UPS wrapper (adapter-side; still open, lower priority — normal drains are well under the cap).


The file C:\Users\decid\Documents\projects\spt-claude-code\docs\adr\0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Open / resolved `int`

1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** ADR-0020 shipped
   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed
   the canonical `<EVENT type="msg" from=…>body</EVENT>\n` envelope (no `__REPLY_TO__`, no
   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-
   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`
   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.
2. **Large-drain injection size** — CC `additionalContext` caps at ~10k chars; add truncate/spill in
   the UPS wrapper (adapter-side; still open, lower priority — normal drains are well under the cap).

<!-- [doc->REQ-DIST-HOOKS-API] -->
<!-- [doc->REQ-UPS-INJECTION] -->
# Hook wiring: the sptc plugin hand-writes a CC `hooks.json` that shells `spt api`

## Status

accepted (2026-06-15) · grounds the F-001 resolution (`docs/SPT-CORE-FINDINGS.md`)

## Context

spt-core is **harness-independent** (`CONTEXT.md` L52/L181): it supplies the agnostic `spt api`
primitives + their I/O format; the **adapter** authors the harness-specific wiring and output
formatting (L112). Claude Code drives hooks via a `hooks/hooks.json` whose handlers receive a JSON
payload on **stdin** and may return `additionalContext`. `spt api` primitives are observable on the
public 0.6.0 binary (flags confirmed via `--help`); every call requires `--adapter claude-spt`
(the adapter_name — distinct from the plugin name `sptc`).

Two hard constraints shape the wiring:

1. **`api listen` blocks** ("consume a seed and hold the perch + relay loop (blocks)"). It therefore
   **cannot** run from `SessionStart` — that would hang CC startup. `listen` belongs to an explicit,
   backgrounded `/sptc:ready` / `/sptc:live` invocation (as legacy did).
2. **spt-core must not materialize a CC `hooks.json`** — doing so would be CC-catering, violating
   L52/L181. The plugin hand-writes it.

## Decision

**The `sptc` plugin ships a hand-written `plugin/sptc/hooks/hooks.json` whose handlers shell out to
`spt api --adapter claude-spt <subcommand>`.** Hook params are sourced from CC's native hook input
(stdin JSON / process), mapped by a thin adapter wrapper — *not* by spt-core `{placeholder}`
substitution (that is the spt-hosted-bringup path, where spt-core itself spawns the command).

### CC-event → `spt api` mapping (authored adapter glue)

| Claude Code hook | `spt api` (all prefixed `--adapter claude-spt`) | Notes |
|---|---|---|
| SessionStart (`startup`/`resume`) | `seed --pid <pid> --session-id <sid>` + write env aliases via `$CLAUDE_ENV_FILE` | non-blocking; records the pid↔session seed |
| SessionStart (`clear`/`compact`) | `boundary <clear\|compact> <id> --to-session-id <sid>` | rebind perch, preserve identity |
| UserPromptSubmit | `poll <id>` → stdout → `additionalContext` | the message-delivery + UPS-injection path (L149) |
| Stop | `state idle <id>` | arms echo-gate fallback (L137) since Stop can't inject |
| (activity) | `state busy <id>` | on prompt/tool start |
| SubagentStart / SubagentStop | `worker-start <parent> <id>` / `worker-stop <id>` | nested worker perches |
| SessionEnd | `session-end <id>` | soft teardown; `shutdown <id>` on graceful signoff |
| PreToolUse | — | **out of scope v1** (UPS covers delivery) |

`api listen <id>` is launched by `/sptc:ready` / `/sptc:live` as a backgrounded blocking poll loop,
**not** from a hook.

### Skill instructions (UPS-injection) ride the same UserPromptSubmit hook

The `UserPromptSubmit` hook's stdout becomes CC `additionalContext`. The same channel that delivers
queued messages (`api poll`) is where `/sptc:X` skill-instruction injection lands (from the adapter
`[strings]`, M12-dep). `api poll` emitting to stdout is by design (L149); formatting it for CC is
ours. PTY/relay inject methods are M3 roadmap (M2a = stdout/hook only) — not a gap.

### `api poll` parse contract — the `<EVENT>` envelope (ADR-0020, operator-ruled 2026-06-15)

> **Supersedes** the earlier `__REPLY_TO__` framing (a mis-elevated relic, now deleted from
> spt-core — see `docs/SPT-CORE-FINDINGS.md` F-002, resolved-by-design). Confirmed by doyle as the
> deliberate poll-surface contract (ADR-0020 §1). **Transitional:** the current 0.6.0 binary still
> emits the `__REPLY_TO__` relic at poll until `REQ-MSG-ENVELOPE` ships; build to `<EVENT>` but
> validate against poll only post-refactor.

The **canonical format at every surface, including `api poll`, is the `spt-proto::event` envelope**
(the ADR-0001 grammar the live listener already emits):

- **One whole, single-line `<EVENT type="msg" from="<sender>">body</EVENT>` per message.** Interior
  newlines are `<br>`-escaped, so the envelope never breaks across lines.
- **Self-delimiting** → a multi-message drain splits cleanly on `</EVENT>` (no delimiter, no
  `F-002` ambiguity).
- **No `<EVENT-PART>` chunking at the poll surface** (doyle Q2): `poll`/`worker-poll` emit whole
  `<EVENT>`s. `<EVENT-PART seq="N/M">` exists only for the *listener stream* (the `« spt event »`
  Monitor's ~500-char `EVENT_LINE_THRESHOLD`); the hook-drain injects via `additionalContext` which
  has no per-line cap. **No id+seq reassembly on the poll path.**

Parser (`render_frames`, `plugin/sptc/hooks/_common.sh`): split on `</EVENT>`; per envelope, read
the `from` attr → `<sptc_messages from="<sender>">`, decode the body (`<br>` → newline, then entity
unescape `< > "` then `&` **last**). **Sender preserved** (reply-correlation: access
gate `ADR-0009`, Psyche routing `ADR-0012`), never silently stripped. Covered by
`tests/hooks-parse.sh` (named / entity / multi-message / no-from / empty).

**Harness injection-size limit is ours** (harness-agnostic boundary — spt-core emits whole
`<EVENT>`s regardless): CC `additionalContext` caps at **10,000 chars** (larger output is spilled to
a file by CC). A large multi-message drain can exceed it → adapter-side follow-up: truncate with a
marker or spill. (`REQ-UPS-INJECTION` `int` item.)

### Portability

Handlers must map CC stdin JSON → `api` flags. The plugin ships **no binary**, so the mapping is a
thin portable wrapper (POSIX `sh` + PowerShell), selected per-platform. Exact wrapper packaging is
settled during impl (see Open).

### Hook-side id-resolution — RESOLVED (observed on the 0.6.0 binary)

`spt whoami` "Print this session's own perch id. **Resolved from `$OWL_SESSION_ID` /
`$SPT_AGENT_ID`.**" That is the id-resolver: the per-prompt hooks do not need a positional `<id>`
threaded in — they resolve it from the session env. Wiring:

- **SessionStart** writes the session env via `$CLAUDE_ENV_FILE`: `OWL_SESSION_ID=<session_id>`
  (from the hook stdin) + `SPT_ADAPTER=claude-spt`. (It also runs the bootstrap and `api seed`.)
- **Per-prompt hooks** (UserPromptSubmit/Stop/SessionEnd) resolve `id="$(spt whoami)"`; if empty
  (session never readied → no perch), they no-op. Auth: pass `--session-id "$OWL_SESSION_ID"`
  (`poll`/`state`/`session-end` all accept `--session-id` as the association proof — observed) so
  no capability token is needed from the hook.

The frame contract is corroborated at the binary level too: `spt send --from <FROM>` = "Sender id
**written into `__REPLY_TO__`**" — matching the `spool.rs` frame doyle provided.

## Validation results (throwaway `claude -p` session, 2026-06-15)

Ran an isolated temp-project rig (UserPromptSubmit marker hook + a registered `/send` project
skill) on the real CC 2.1.177 binary:

- ✅ **UPS fires on a `/`-slash-command.** `MSYS_NO_PATHCONV=1 claude -p "/send hi"` → the hook
  received `prompt:"/send hi"` (literal) **and** fired **and** the skill ran. Answers the
  SCOPE-flagged question: `UserPromptSubmit` fires on a `/sptc:X` invocation with the token intact,
  so the wrapper can detect `/sptc:X` in `prompt` and inject. (`REQ-UPS-INJECTION`.)
- ✅ **Windows `shell:"bash"` command hooks work** — the hook executed via Git-Bash,
  `$CLAUDE_PROJECT_DIR` resolved, exit-0 stdout honored. Resolves Open#2 for the POSIX-wrapper
  packaging on Windows (no per-OS branch needed for the hook to run).
- ✅ **Hook stdin schema confirmed**: `{session_id, transcript_path, cwd, permission_mode,
  hook_event_name, prompt}` — `json_str` targets the right fields; `session_id` sourcing confirmed.
- ⚠ **MSYS `/`-arg mangling** observed (run A): a `/send` passed as a Git-Bash *argument* became
  `C:/Program Files/Git/send`. Test artifact (wrappers read stdin, not argv) but a real Windows
  hazard → `docs/KNOWN-HAZARDS.md` 1.1 + `REQ-HAZARD-MSYS-PATHCONV` (test: `tests/msys-hazard.sh`).

## Open (remaining `int` — gated on the spt-core poll refactor)

1. **`api poll` → `additionalContext` round-trip** against a real spt perch, once `REQ-MSG-ENVELOPE`
   lands (poll emits `<EVENT>`): capture actual stdout bytes, confirm-match to doyle, flip `int`.
2. **Large-drain injection size** — CC `additionalContext` caps at 10k chars; add truncate/spill in
   the UPS wrapper (adapter-side; `int`).

## Consequences

- `REQ-DIST-HOOKS-API` / `REQ-UPS-INJECTION` gain a concrete, grounded `doc` design here; their
  `impl` activates when the validated `hooks.json` + wrappers land (gated on Open #1).
- Supersedes the `ADR-0001` "hooks.json delegates to `spt api`" stub with the full wiring; ADR-0001's
  UPS-injection open item (UPS-fires empirical) is carried here as Open #2.
- No spt-core dependency for the wiring itself — only the (non-blocking) residual docs publish of the
  `api poll` frame format, which we can substitute with observed behavior until it lands.

-## Open (remaining `int` — gated on the spt-core poll refactor)

-1. **`api poll` → `additionalContext` round-trip** against a real spt perch, once `REQ-MSG-ENVELOPE`

-   lands (poll emits `<EVENT>`): capture actual stdout bytes, confirm-match to doyle, flip `int`.

-2. **Large-drain injection size** — CC `additionalContext` caps at 10k chars; add truncate/spill in

-   the UPS wrapper (adapter-side; `int`).

+1. **`api poll` → `additionalContext` round-trip — RESOLVED (v0.7.1, 2026-06-15).** ADR-0020 shipped

+   in v0.7.1; a throwaway byte-capture against the live `spt api poll` drain (`od`-verified) confirmed

+   the canonical `<EVENT type="msg" from=…>body</EVENT>\n` envelope (no `__REPLY_TO__`, no

+   `<EVENT-PART>` on normal drains, multi-drain splits on `</EVENT>`), and `render_frames` confirm-

+   matched it. Locked by `ci/hooks/poll-int.sh` (5/5); `REQ-DIST-HOOKS-API` + `REQ-UPS-INJECTION`

+   `int` flipped green. See `docs/SPT-CORE-FINDINGS.md` F-002.

+2. **Large-drain injection size** — CC `additionalContext` caps at ~10k chars; add truncate/spill in

+   the UPS wrapper (adapter-side; still open, lower priority — normal drains are well under the cap).


Update the plan's two REQ-MSG-ENVELOPE held-item notes → resolved.


- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — ADR-0020 MERGED to spt-core main
  (`25ffd7a`) but NOT yet PUBLISHED; v0.7.0 still carries the `__REPLY_TO__` relic. Byte-capture tests
  the SHIPPED binary, so the int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with
  the hash on publish), NOT the merge. Parse v0.7.0 as-is until then; then confirm-match + flip
  HOOKS-API/UPS-INJECTION int.

- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — **DONE (v0.7.1, 2026-06-15).** ADR-0020
  published in v0.7.1; byte-capture confirmed `<EVENT>` on the live `api poll` drain (no `__REPLY_TO__`,
  F-002 dissolved), `render_frames` confirm-match. Locked by `ci/hooks/poll-int.sh` (5/5);
  REQ-DIST-HOOKS-API + REQ-UPS-INJECTION `int` flipped green.


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

- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — **DONE (v0.7.1, 2026-06-15).** ADR-0020
  published in v0.7.1; byte-capture confirmed `<EVENT>` on the live `api poll` drain (no `__REPLY_TO__`,
  F-002 dissolved), `render_frames` confirm-match. Locked by `ci/hooks/poll-int.sh` (5/5);
  REQ-DIST-HOOKS-API + REQ-UPS-INJECTION `int` flipped green.

# Adapter-manifest + M12 parity — JIT plan (next milestone)

> Fresh milestone after the CI/distribution slices. Authors the CC adapter **manifest** (publish
> target #2, the spt-core registry side) against the REAL v0.7.0 published surface, then the M12
> parity surfaces. NEEDS public-docs research first — run in clean context.

## Public-surface-only (hard constraint — re-read)

Build ONLY from `SaberMage/spt-releases` (v0.7.0) + `https://sabermage.github.io/spt-releases`
(harness-contract, CLI reference, manifest.schema.json, rustdoc, llms-full.txt). NEVER read
spt-core source. A missing capability = a FINDING for doyle/todlando, not a workaround. (AGENTS.md)

## Research first (before authoring)

1. Pull v0.7.0 `manifest.schema.json` from spt-releases — the authoritative shape the adapter
   manifest must validate against. Note required fields, `[adapter]` block, `[strings]` (file-backed
   large bodies), `shortcut_basename`, profiles/hints/`[digest]` extractor.
2. Read the published harness-contract + CLI reference for: `spt adapter` registration verbs,
   `spt endpoint run`, `shortcut_basename` semantics (emits `sptc-<id>` shortcuts), whoami-as-alias,
   adapter:profile fallback addressing.
3. Confirm where the manifest physically lives (spt-core-conducted registry, NOT plugin files —
   ADR-0001/SCOPE) and how it's validated locally.

## Slices (sequence; each its own commit + green gate)

1. **DONE (bd16644)** — **Adapter manifest authoring**. `adapter/claude-spt.toml` ([adapter]+
   shortcut_basename, [hooks.*], [identity], [inject], [digest], [profiles.deep], [strings],
   [[hints]]); vendored `adapter/manifest.schema.json`; manifest-schema gate
   (`ci/manifest/{validate_manifest.py,check-manifest.sh}`, py tomllib|tomli + jsonschema) wired
   into `run-gates.sh`. **REQ-DIST-MANIFEST-SCHEMA** → `[doc,impl,unit]` (int held: real
   `spt adapter add` registration-validation = 2nd layer). Deferred in-manifest with notes:
   `[session]`/roles, `[history]`, `[update]` (file_pull needs a signing key + registry repo not
   on the public surface — a finding).
2. **DONE (c93757d)** — **shortcut_basename** guard. `tests/manifest-shortcut.sh` asserts the
   `sptc` brand (succession seam). **REQ-DIST-SHORTCUT-BASENAME** → `[doc,unit]` (the field is
   declarative config, not impl — the scanner classifies impl by file type; launcher impl +
   real-picker int deferred to the endpoint-run/cc-launcher slice, gated on the v0.7.0 binary).
3. **M12 parity surfaces** (seed REQs per surface as each starts — activate-don't-pre-fail):
   - **DONE (8cd8372)** — `/sptc:whoami` + `/sptc:setup` skill skeletons (the manifest advertised
     both; skeletons now exist, covered by REQ-DIST-PLUGIN-SKELETON + skeleton-validate).
   - **DONE (435e1be)** — **manifest proven against live v0.7.0**. Local spt upgraded to 0.7.0;
     `ci/manifest/registration-int.sh` (SPTC_ACCEPTANCE-gated) green 6/6: `spt adapter add`
     accepted (2nd-layer cross-field validation), shipped `:deep` profile resolves, `[strings]`
     read + overlay observable via `get-string`, clean soft-remove. **REQ-DIST-MANIFEST-SCHEMA →
     [doc,impl,unit,int]**. Cross-field discovery fixed: `[digest]` needs `source` →
     `source="{home}/.claude/projects"`. Findings logged (key catalog + digest rule undocumented).
   - **IN PROGRESS — capability confirmed (F-003 resolved 2026-06-15)** — file-backed `[strings]`.
     Research conclusion REVERSED by live byte-test + doyle/todlando ruling: the mechanism **is
     shipped** in v0.7.0 as a value-position table pointer `key = { file = "rel" }` over
     `adapters/<adapter>/strings/` (lazy-resolved at `get-string`, containment-checked, copied on
     `adapter add`). My avenue search missed it (it's a value-table shape, not a CLI verb / `@file`).
     ADR-0001 dependency **SATISFIED**; residual is docs-only (doyle publishing, PR #13).
     - **DONE (this session)** — pattern proven end-to-end + first two bodies authored:
       `adapter/strings/skills/{whoami,setup}.md` + `[strings.skills].{whoami,setup} = { file = ... }`;
       `registration-int.sh` extended (8/8) — asserts a `{ file }` pointer resolves to FILE CONTENTS
       and an inline sibling prints as-is, vs live v0.7.0. `strings/` resolves next to the manifest
       FILE on `adapter add`. Bodies grounded in the public surface (`spt whoami`; shipped bootstrap).
     - **DONE (this session)** — ready, send, version authored as file-backed bodies + pointers; stubs
       refreshed. ready/send **delegate to `spt how-to ready|send`** (stay current vs published surface);
       version reports the version-of-truth via `spt --version` + `spt adapter list` (ADR-0001).
     - **DONE (this session, triaged vs the v0.7.1 surface)** — signoff (`spt endpoint shutdown` —
       graceful + final context save, exact match) + list-agents (`spt endpoint list` — the roster).
       7/11 skills now operative. (force-stop/list-agents/new-alarm had no `[strings.skills]` entry;
       list-agents now added.)
     - **TODO — remaining 4, classified (don't invent):**
       - **live** — `spt endpoint run` EXISTS but spawns the manifest `[session.self]` command, which
         is unauthored (deferred slice). GATED on `[session.self]` authoring + the run bringup.
       - **force-stop** — core spt has only soft `spt endpoint stop` (perch, spool kept) + graceful
         `endpoint shutdown`; NO force-kill / Psyche-teardown equivalent (the legacy `$LIVE stop`
         3-step). SEMANTICS MISMATCH → defer; candidate finding if force-kill is required.
       - **commune / new-alarm** — ABSENT from the core `spt` v0.7.1 surface (live-agent/Psyche layer,
         the owl/`$LIVE` binary, not core). Not authorable as core-spt skills → defer; likely a
         scope/finding question (does claude-spt reach the Psyche layer, or are these owl-only?).
   - **DONE (this session) — UPS skill-injection branch BUILT.** UPS-fires-on-slash was already
     proven (ADR-0002 validation 2026-06-15: `/sptc:X` fires with token intact on CC 2.1.177) — my
     earlier "still-open" framing was stale. Built: `_common.sh` `sptc_skill_key` (pure, detect
     `/sptc:<skill>` leading-only) + `sptc_inject_skill` (resolve body via `get-string skills.<x>`,
     wrap `<sptc_skill name>`); `user-prompt-submit.sh` now injects BEFORE the perch check (whoami/
     setup need no perch) then drains. Tests: hooks-parse unit (skill_key, 8 cases) + registration-int
     4c (end-to-end `/sptc:whoami` → wrapped body, 9/9 vs live v0.7.0). `setup` SKILL.md is
     self-contained (runs when spt may be absent → injection no-ops; stub is the floor). whoami/setup
     stubs refreshed (stale "not yet operative" removed). `REQ-UPS-INJECTION` `[doc,impl,unit]` stays
     covered; `int` still held — but ONLY on the message-DRAIN half (`api poll` → additionalContext
     round-trip, gated on REQ-MSG-ENVELOPE publish). The skill-injection int is satisfied (4c).
   - **TODO** `[session.self]` + `spt endpoint run` bringup + `cc`/`sptc` launcher → unblocks the
     held **REQ-DIST-SHORTCUT-BASENAME** impl+int (the `<basename>-<id>` picker "lands in a later
     spt-core wave" per `endpoint run --help`; `endpoint run` spawns `[session.self]`, still deferred).
   - **TODO** digest extractor binary `claude-spt-digest` → unblocks `spt adapter digest-proof`
     (the deeper [digest] int; the seam is declared + registration-accepted, extractor unbuilt).

### Still held on spt-core (non-blocking)
- `sptc-<id>` shortcut EMISSION — spt-core picker wave (not in 0.7.0) + needs `[session.self]`.
- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — ADR-0020 MERGED to spt-core main
  (`25ffd7a`) but NOT yet PUBLISHED; v0.7.0 still carries the `__REPLY_TO__` relic. Byte-capture tests
  the SHIPPED binary, so the int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with
  the hash on publish), NOT the merge. Parse v0.7.0 as-is until then; then confirm-match + flip
  HOOKS-API/UPS-INJECTION int.

### Note: claude-spt is now a REGISTERED adapter artifact on this node (the v1 acceptance proof).
The int test soft-removes it each run; re-add with `spt adapter add adapter/claude-spt.toml`.
Local `spt` is **0.7.0** (operator-upgraded 2026-06-15).

## Still gated on spt-core (non-blocking)

poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE. ADR-0020 MERGED to spt-core main
(`25ffd7a`) but NOT PUBLISHED; v0.7.0 still emits the `__REPLY_TO__` relic. Byte-capture tests the
SHIPPED binary → int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with the hash on
publish), not the merge. Then confirm-match + flip HOOKS-API/UPS-INJECTION `int`.

## Gate (every slice)

`sh ci/run-gates.sh` ⇒ PASS · `traceable-reqs check` green. Atomic commits, `Co-authored by: perri`.

## Session state at handoff (2026-06-15)

Shipped this session (all green, gates PASS, 16/16 traceable): CI ACCEPTANCE slice 1
(73677fd/d834611 — real-claude SUT + perch-collision guard, REQ-CI-ACCEPTANCE [doc,impl,int] +
REQ-HAZARD-PERCH-COLLISION); cplugs publish-prep (e28db92 — validate/package skeleton + untracked
a real perch-state leak, REQ-DIST-PLUGIN-SKELETON [doc,impl,unit]); mdBook docs-site
(d999c60/905c62c — drift-gated book + llms.txt, REQ-DOCS-SITE/REQ-DOCS-DRIFT). Collaborators:
doyle (design/gate) + todlando (builds spt-core) over the spt bus. v0.7.0/M12 is PUBLIC.

-- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — ADR-0020 MERGED to spt-core main

-  (`25ffd7a`) but NOT yet PUBLISHED; v0.7.0 still carries the `__REPLY_TO__` relic. Byte-capture tests

-  the SHIPPED binary, so the int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with

-  the hash on publish), NOT the merge. Parse v0.7.0 as-is until then; then confirm-match + flip

+- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — **DONE (v0.7.1, 2026-06-15).** ADR-0020

+  published in v0.7.1; byte-capture confirmed `<EVENT>` on the live `api poll` drain (no `__REPLY_TO__`,

+  F-002 dissolved), `render_frames` confirm-match. Locked by `ci/hooks/poll-int.sh` (5/5);

+  REQ-DIST-HOOKS-API + REQ-UPS-INJECTION `int` flipped green.


poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE. ADR-0020 MERGED to spt-core main
(`25ffd7a`) but NOT PUBLISHED; v0.7.0 still emits the `__REPLY_TO__` relic. Byte-capture tests the
SHIPPED binary → int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with the hash on
publish), not the merge. Then confirm-match + flip HOOKS-API/UPS-INJECTION `int`.

poll→additionalContext bus-delivery `int` — **DONE (v0.7.1).** ADR-0020 published; byte-capture
confirmed the `<EVENT>` envelope on the live `api poll` drain + `render_frames` confirm-match;
`ci/hooks/poll-int.sh` (5/5) locks it; HOOKS-API + UPS-INJECTION `int` green. (Remaining open: the
~10k additionalContext large-drain truncate/spill — lower priority, ADR-0002 Open #2.)


poll→additionalContext bus-delivery `int` — **DONE (v0.7.1).** ADR-0020 published; byte-capture
confirmed the `<EVENT>` envelope on the live `api poll` drain + `render_frames` confirm-match;
`ci/hooks/poll-int.sh` (5/5) locks it; HOOKS-API + UPS-INJECTION `int` green. (Remaining open: the
~10k additionalContext large-drain truncate/spill — lower priority, ADR-0002 Open #2.)

# Adapter-manifest + M12 parity — JIT plan (next milestone)

> Fresh milestone after the CI/distribution slices. Authors the CC adapter **manifest** (publish
> target #2, the spt-core registry side) against the REAL v0.7.0 published surface, then the M12
> parity surfaces. NEEDS public-docs research first — run in clean context.

## Public-surface-only (hard constraint — re-read)

Build ONLY from `SaberMage/spt-releases` (v0.7.0) + `https://sabermage.github.io/spt-releases`
(harness-contract, CLI reference, manifest.schema.json, rustdoc, llms-full.txt). NEVER read
spt-core source. A missing capability = a FINDING for doyle/todlando, not a workaround. (AGENTS.md)

## Research first (before authoring)

1. Pull v0.7.0 `manifest.schema.json` from spt-releases — the authoritative shape the adapter
   manifest must validate against. Note required fields, `[adapter]` block, `[strings]` (file-backed
   large bodies), `shortcut_basename`, profiles/hints/`[digest]` extractor.
2. Read the published harness-contract + CLI reference for: `spt adapter` registration verbs,
   `spt endpoint run`, `shortcut_basename` semantics (emits `sptc-<id>` shortcuts), whoami-as-alias,
   adapter:profile fallback addressing.
3. Confirm where the manifest physically lives (spt-core-conducted registry, NOT plugin files —
   ADR-0001/SCOPE) and how it's validated locally.

## Slices (sequence; each its own commit + green gate)

1. **DONE (bd16644)** — **Adapter manifest authoring**. `adapter/claude-spt.toml` ([adapter]+
   shortcut_basename, [hooks.*], [identity], [inject], [digest], [profiles.deep], [strings],
   [[hints]]); vendored `adapter/manifest.schema.json`; manifest-schema gate
   (`ci/manifest/{validate_manifest.py,check-manifest.sh}`, py tomllib|tomli + jsonschema) wired
   into `run-gates.sh`. **REQ-DIST-MANIFEST-SCHEMA** → `[doc,impl,unit]` (int held: real
   `spt adapter add` registration-validation = 2nd layer). Deferred in-manifest with notes:
   `[session]`/roles, `[history]`, `[update]` (file_pull needs a signing key + registry repo not
   on the public surface — a finding).
2. **DONE (c93757d)** — **shortcut_basename** guard. `tests/manifest-shortcut.sh` asserts the
   `sptc` brand (succession seam). **REQ-DIST-SHORTCUT-BASENAME** → `[doc,unit]` (the field is
   declarative config, not impl — the scanner classifies impl by file type; launcher impl +
   real-picker int deferred to the endpoint-run/cc-launcher slice, gated on the v0.7.0 binary).
3. **M12 parity surfaces** (seed REQs per surface as each starts — activate-don't-pre-fail):
   - **DONE (8cd8372)** — `/sptc:whoami` + `/sptc:setup` skill skeletons (the manifest advertised
     both; skeletons now exist, covered by REQ-DIST-PLUGIN-SKELETON + skeleton-validate).
   - **DONE (435e1be)** — **manifest proven against live v0.7.0**. Local spt upgraded to 0.7.0;
     `ci/manifest/registration-int.sh` (SPTC_ACCEPTANCE-gated) green 6/6: `spt adapter add`
     accepted (2nd-layer cross-field validation), shipped `:deep` profile resolves, `[strings]`
     read + overlay observable via `get-string`, clean soft-remove. **REQ-DIST-MANIFEST-SCHEMA →
     [doc,impl,unit,int]**. Cross-field discovery fixed: `[digest]` needs `source` →
     `source="{home}/.claude/projects"`. Findings logged (key catalog + digest rule undocumented).
   - **IN PROGRESS — capability confirmed (F-003 resolved 2026-06-15)** — file-backed `[strings]`.
     Research conclusion REVERSED by live byte-test + doyle/todlando ruling: the mechanism **is
     shipped** in v0.7.0 as a value-position table pointer `key = { file = "rel" }` over
     `adapters/<adapter>/strings/` (lazy-resolved at `get-string`, containment-checked, copied on
     `adapter add`). My avenue search missed it (it's a value-table shape, not a CLI verb / `@file`).
     ADR-0001 dependency **SATISFIED**; residual is docs-only (doyle publishing, PR #13).
     - **DONE (this session)** — pattern proven end-to-end + first two bodies authored:
       `adapter/strings/skills/{whoami,setup}.md` + `[strings.skills].{whoami,setup} = { file = ... }`;
       `registration-int.sh` extended (8/8) — asserts a `{ file }` pointer resolves to FILE CONTENTS
       and an inline sibling prints as-is, vs live v0.7.0. `strings/` resolves next to the manifest
       FILE on `adapter add`. Bodies grounded in the public surface (`spt whoami`; shipped bootstrap).
     - **DONE (this session)** — ready, send, version authored as file-backed bodies + pointers; stubs
       refreshed. ready/send **delegate to `spt how-to ready|send`** (stay current vs published surface);
       version reports the version-of-truth via `spt --version` + `spt adapter list` (ADR-0001).
     - **DONE (this session, triaged vs the v0.7.1 surface)** — signoff (`spt endpoint shutdown` —
       graceful + final context save, exact match) + list-agents (`spt endpoint list` — the roster).
       7/11 skills now operative. (force-stop/list-agents/new-alarm had no `[strings.skills]` entry;
       list-agents now added.)
     - **TODO — remaining 4, classified (don't invent):**
       - **live** — `spt endpoint run` EXISTS but spawns the manifest `[session.self]` command, which
         is unauthored (deferred slice). GATED on `[session.self]` authoring + the run bringup.
       - **force-stop** — core spt has only soft `spt endpoint stop` (perch, spool kept) + graceful
         `endpoint shutdown`; NO force-kill / Psyche-teardown equivalent (the legacy `$LIVE stop`
         3-step). SEMANTICS MISMATCH → defer; candidate finding if force-kill is required.
       - **commune / new-alarm** — ABSENT from the core `spt` v0.7.1 surface (live-agent/Psyche layer,
         the owl/`$LIVE` binary, not core). Not authorable as core-spt skills → defer; likely a
         scope/finding question (does claude-spt reach the Psyche layer, or are these owl-only?).
   - **DONE (this session) — UPS skill-injection branch BUILT.** UPS-fires-on-slash was already
     proven (ADR-0002 validation 2026-06-15: `/sptc:X` fires with token intact on CC 2.1.177) — my
     earlier "still-open" framing was stale. Built: `_common.sh` `sptc_skill_key` (pure, detect
     `/sptc:<skill>` leading-only) + `sptc_inject_skill` (resolve body via `get-string skills.<x>`,
     wrap `<sptc_skill name>`); `user-prompt-submit.sh` now injects BEFORE the perch check (whoami/
     setup need no perch) then drains. Tests: hooks-parse unit (skill_key, 8 cases) + registration-int
     4c (end-to-end `/sptc:whoami` → wrapped body, 9/9 vs live v0.7.0). `setup` SKILL.md is
     self-contained (runs when spt may be absent → injection no-ops; stub is the floor). whoami/setup
     stubs refreshed (stale "not yet operative" removed). `REQ-UPS-INJECTION` `[doc,impl,unit]` stays
     covered; `int` still held — but ONLY on the message-DRAIN half (`api poll` → additionalContext
     round-trip, gated on REQ-MSG-ENVELOPE publish). The skill-injection int is satisfied (4c).
   - **TODO** `[session.self]` + `spt endpoint run` bringup + `cc`/`sptc` launcher → unblocks the
     held **REQ-DIST-SHORTCUT-BASENAME** impl+int (the `<basename>-<id>` picker "lands in a later
     spt-core wave" per `endpoint run --help`; `endpoint run` spawns `[session.self]`, still deferred).
   - **TODO** digest extractor binary `claude-spt-digest` → unblocks `spt adapter digest-proof`
     (the deeper [digest] int; the seam is declared + registration-accepted, extractor unbuilt).

### Still held on spt-core (non-blocking)
- `sptc-<id>` shortcut EMISSION — spt-core picker wave (not in 0.7.0) + needs `[session.self]`.
- REQ-MSG-ENVELOPE poll→additionalContext bus-delivery int — **DONE (v0.7.1, 2026-06-15).** ADR-0020
  published in v0.7.1; byte-capture confirmed `<EVENT>` on the live `api poll` drain (no `__REPLY_TO__`,
  F-002 dissolved), `render_frames` confirm-match. Locked by `ci/hooks/poll-int.sh` (5/5);
  REQ-DIST-HOOKS-API + REQ-UPS-INJECTION `int` flipped green.

### Note: claude-spt is now a REGISTERED adapter artifact on this node (the v1 acceptance proof).
The int test soft-removes it each run; re-add with `spt adapter add adapter/claude-spt.toml`.
Local `spt` is **0.7.0** (operator-upgraded 2026-06-15).

## Still gated on spt-core (non-blocking)

poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE. ADR-0020 MERGED to spt-core main
(`25ffd7a`) but NOT PUBLISHED; v0.7.0 still emits the `__REPLY_TO__` relic. Byte-capture tests the
SHIPPED binary → int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with the hash on
publish), not the merge. Then confirm-match + flip HOOKS-API/UPS-INJECTION `int`.

## Gate (every slice)

`sh ci/run-gates.sh` ⇒ PASS · `traceable-reqs check` green. Atomic commits, `Co-authored by: perri`.

## Session state at handoff (2026-06-15)

Shipped this session (all green, gates PASS, 16/16 traceable): CI ACCEPTANCE slice 1
(73677fd/d834611 — real-claude SUT + perch-collision guard, REQ-CI-ACCEPTANCE [doc,impl,int] +
REQ-HAZARD-PERCH-COLLISION); cplugs publish-prep (e28db92 — validate/package skeleton + untracked
a real perch-state leak, REQ-DIST-PLUGIN-SKELETON [doc,impl,unit]); mdBook docs-site
(d999c60/905c62c — drift-gated book + llms.txt, REQ-DOCS-SITE/REQ-DOCS-DRIFT). Collaborators:
doyle (design/gate) + todlando (builds spt-core) over the spt bus. v0.7.0/M12 is PUBLIC.

-poll→additionalContext bus-delivery `int` — REQ-MSG-ENVELOPE. ADR-0020 MERGED to spt-core main

-(`25ffd7a`) but NOT PUBLISHED; v0.7.0 still emits the `__REPLY_TO__` relic. Byte-capture tests the

-SHIPPED binary → int flip waits for the next RELEASE carrying ADR-0020 (doyle pings with the hash on

-publish), not the merge. Then confirm-match + flip HOOKS-API/UPS-INJECTION `int`.

+poll→additionalContext bus-delivery `int` — **DONE (v0.7.1).** ADR-0020 published; byte-capture

+confirmed the `<EVENT>` envelope on the live `api poll` drain + `render_frames` confirm-match;

+`ci/hooks/poll-int.sh` (5/5) locks it; HOOKS-API + UPS-INJECTION `int` green. (Remaining open: the

+~10k additionalContext large-drain truncate/spill — lower priority, ADR-0002 Open #2.)


All notes updated. Run full gates + both acceptance ints + traceable.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh 2>&1 | tail -3
echo "=== poll-int ==="; SPTC_ACCEPTANCE=1 sh ci/hooks/poll-int.sh 2>&1 | tail -2
echo "=== registration-int ==="; SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh 2>&1 | tail -2
echo "=== traceable ==="; traceable-reqs check 2>&1 | tail -2

Full gates + both int tests + traceable


ok   llms.txt in sync

=== RESULT: PASS ===
=== poll-int ===
ok   multi-message: 2 whole <EVENT> envelopes split on </EVENT>
MSG-ENVELOPE-INT OK
=== registration-int ===
ok   removed (soft-deregistered)
MANIFEST-REGISTRATION-INT OK
=== traceable ===
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands


Everything green (RESULT: PASS includes traceable). Commit the int flip.


cd /c/Users/decid/Documents/projects/spt-claude-code
git add ci/hooks/poll-int.sh traceable-reqs.toml docs/SPT-CORE-FINDINGS.md docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md MANIFEST-PARITY-PLAN.md
git commit -q -F - <<'EOF'
test(hooks): poll-int verifies <EVENT> on v0.7.1 — flip HOOKS-API + UPS-INJECTION int

ADR-0020 shipped in v0.7.1 (counter 13). Byte-captured the live `spt api poll`
drain (od-verified): canonical `<EVENT type="msg" from=…>body</EVENT>\n` — body
escaping (<br> + entities + & last) is the exact render_frames decode rule;
whole self-delimiting + \n-framed envelopes; NO <EVENT-PART> on a normal hook
drain; NO __REPLY_TO__ relic; multi-drain splits cleanly on </EVENT>. Raw drain
through render_frames -> correct <sptc_messages from=…> (parser confirm-match).
F-002 dissolved on the published binary.

- ci/hooks/poll-int.sh (NEW): SPTC_ACCEPTANCE-gated, >=0.7.1, disposable perch
  (REQ-HAZARD-PERCH-COLLISION), bind->send->api poll->render_frames assert +
  no-relic + multi-message; torn down on exit. 5/5 green.
  [int->REQ-DIST-HOOKS-API] [int->REQ-UPS-INJECTION]
- traceable-reqs.toml: REQ-DIST-HOOKS-API + REQ-UPS-INJECTION required_stages
  -> +int (evidenced by poll-int.sh).
- SPT-CORE-FINDINGS.md: F-002 RESOLVED-SHIPPED (verified on published surface);
  F-003 docs-residual CLOSED (v0.7.1 publishes the [strings] file-pointer syntax
  + {key} catalog + [digest] rule).
- ADR-0002 Open #1 RESOLVED; plan held-item notes -> done.

ADR-0020 loop closed end-to-end: design->impl->gate->ship->real-surface-verify.

Gate: sh ci/run-gates.sh PASS, poll-int 5/5, registration-int 9/9, traceable green.

Co-authored by: perri
EOF
git log --oneline -1

Commit poll-int + int flips


c00b090 test(hooks): poll-int verifies <EVENT> on v0.7.1 — flip HOOKS-API + UPS-INJECTION int


c00b090 committed — **REQ-MSG-ENVELOPE / ADR-0020 loop closed end-to-end.** doyle said ping only if snags; none, so no ping. Commune the large delta for next-session accuracy.


Write

<live-context>
**BIG DELTA — REQ-MSG-ENVELOPE / ADR-0020 loop CLOSED + UPS skill-injection built + 7/11 skills operative.**

**Local spt is now 0.7.1** (operator-applied `spt update fetch && apply` this session, on trial;
daemon restarted, perch survived). v0.7.1 = counter 13, hashes logged in doyle's publish ping.

**REQ-MSG-ENVELOPE done (the big one I owed doyle).** ADR-0020 published in v0.7.1 (`<EVENT>` at every
surface, `__REPLY_TO__` relic GONE). I byte-captured the live `spt api poll` drain (od-verified):
`<EVENT type="msg" from="<sender>">body</EVENT>\n` — body escaping (<br>+entities+& LAST) = my
render_frames decode rule EXACTLY; whole self-delimiting + \n-framed envelopes; NO <EVENT-PART> on
normal drains; multi-drain splits on </EVENT> → F-002 DISSOLVED on the published binary. render_frames
confirm-match PASS. Sent doyle the confirm-match bytes; he confirmed CAPSTONE, loop closed
design→impl→gate→ship→real-surface-verify. Locked by NEW `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated,
≥0.7.1, disposable perch, 5/5). **Flipped REQ-DIST-HOOKS-API + REQ-UPS-INJECTION int GREEN.**

**F-002 = RESOLVED-SHIPPED. F-003 = RESOLVED + docs CLOSED** (v0.7.1 published the [strings]
file-pointer syntax + {key} catalog + [digest] register rule — my 3 flagged docs gaps, all on the
published surface now). No open findings.

**UPS skill-injection branch BUILT** (was wrongly framed as blocked — operator reminded me UPS-fires-
on-slash was already proven in ADR-0002, CC 2.1.177). `_common.sh`: sptc_skill_key (detect /sptc:<x>,
leading-only) + sptc_inject_skill (get-string skills.<x>, wrap <sptc_skill>). user-prompt-submit.sh
injects BEFORE the perch check (whoami/setup need no perch) then drains.

**Bus etiquette reminder to self:** doyle/todlando send MANY high-priority acks; a pure "ACK/carry on"
is TERMINAL — do NOT reply (it loops). Only reply when there's substance or a question.

**Open threads:** none owed. todlando flagged nothing pending. doyle said "you're clear."
</live-context>
<project-context>
**Adapter-manifest milestone — major progress. 7/11 skills operative; ADR-0020 verified on real surface.**

Commits since last commune (all gated PASS, traceable green; ints run vs live spt):
- `9d5516d` UPS skill-injection branch (sptc_skill_key + sptc_inject_skill; inject-before-drain).
- `c9abdf7` file-backed bodies: ready, send, version (ready/send delegate to `spt how-to`).
- `21f50be` file-backed bodies: signoff (`spt endpoint shutdown`), list-agents (`spt endpoint list`).
- `c00b090` ci/hooks/poll-int.sh + REQ-DIST-HOOKS-API/REQ-UPS-INJECTION int flipped; F-002/F-003 closed.

**Skills operative (7/11):** whoami, setup, ready, send, version, signoff, list-agents — file-backed
bodies in adapter/strings/skills/*.md via [strings.skills].<x> = { file = "skills/<x>.md" }, injected
by the UPS hook. setup is SELF-CONTAINED in its SKILL.md (runs when spt may be absent → injection
no-ops). registration-int extended (9/9): file-backed resolve + skill-injection end-to-end.

**Remaining 4 skills (classified vs v0.7.1, NOT authored — don't invent commands):**
- live → `spt endpoint run` exists but needs manifest [session.self] (deferred slice).
- force-stop → only soft `endpoint stop`/`shutdown` in core; no force-kill/Psyche teardown → defer/finding.
- commune, new-alarm → ABSENT from core spt v0.7.1 (live-agent/Psyche layer = owl/$LIVE, not core) →
  defer; scope/finding question (does claude-spt reach the Psyche layer at all?).

**REQ status:** REQ-DIST-MANIFEST-SCHEMA, REQ-DIST-HOOKS-API, REQ-UPS-INJECTION all [doc,impl,unit,int]
GREEN. REQ-DIST-SHORTCUT-BASENAME [doc,unit] (impl/int = cc launcher + picker, deferred spt-core wave).

**Next candidate slices:** (1) [session.self] + endpoint-run + cc launcher → unblocks live skill +
REQ-DIST-SHORTCUT-BASENAME impl/int; (2) claude-spt-digest extractor (contract {role,text,tool,ts}
JSONL, `claude-spt-digest --session {session_id} --in {source}`) → unblocks `spt adapter digest-proof`;
(3) the 4 deferred skills' live-layer scope question; (4) large-drain truncate/spill (ADR-0002 Open #2).
Then ship when parity proven.
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\.claude\perri-commune.md (file state is current in your context — no need to Read it back)

<live-context>
**BIG DELTA — REQ-MSG-ENVELOPE / ADR-0020 loop CLOSED + UPS skill-injection built + 7/11 skills operative.**

**Local spt is now 0.7.1** (operator-applied `spt update fetch && apply` this session, on trial;
daemon restarted, perch survived). v0.7.1 = counter 13, hashes logged in doyle's publish ping.

**REQ-MSG-ENVELOPE done (the big one I owed doyle).** ADR-0020 published in v0.7.1 (`<EVENT>` at every
surface, `__REPLY_TO__` relic GONE). I byte-captured the live `spt api poll` drain (od-verified):
`<EVENT type="msg" from="<sender>">body</EVENT>\n` — body escaping (<br>+entities+& LAST) = my
render_frames decode rule EXACTLY; whole self-delimiting + \n-framed envelopes; NO <EVENT-PART> on
normal drains; multi-drain splits on </EVENT> → F-002 DISSOLVED on the published binary. render_frames
confirm-match PASS. Sent doyle the confirm-match bytes; he confirmed CAPSTONE, loop closed
design→impl→gate→ship→real-surface-verify. Locked by NEW `ci/hooks/poll-int.sh` (SPTC_ACCEPTANCE-gated,
≥0.7.1, disposable perch, 5/5). **Flipped REQ-DIST-HOOKS-API + REQ-UPS-INJECTION int GREEN.**

**F-002 = RESOLVED-SHIPPED. F-003 = RESOLVED + docs CLOSED** (v0.7.1 published the [strings]
file-pointer syntax + {key} catalog + [digest] register rule — my 3 flagged docs gaps, all on the
published surface now). No open findings.

**UPS skill-injection branch BUILT** (was wrongly framed as blocked — operator reminded me UPS-fires-
on-slash was already proven in ADR-0002, CC 2.1.177). `_common.sh`: sptc_skill_key (detect /sptc:<x>,
leading-only) + sptc_inject_skill (get-string skills.<x>, wrap <sptc_skill>). user-prompt-submit.sh
injects BEFORE the perch check (whoami/setup need no perch) then drains.

**Bus etiquette reminder to self:** doyle/todlando send MANY high-priority acks; a pure "ACK/carry on"
is TERMINAL — do NOT reply (it loops). Only reply when there's substance or a question.

**Open threads:** none owed. todlando flagged nothing pending. doyle said "you're clear."
</live-context>
<project-context>
**Adapter-manifest milestone — major progress. 7/11 skills operative; ADR-0020 verified on real surface.**

Commits since last commune (all gated PASS, traceable green; ints run vs live spt):
- `9d5516d` UPS skill-injection branch (sptc_skill_key + sptc_inject_skill; inject-before-drain).
- `c9abdf7` file-backed bodies: ready, send, version (ready/send delegate to `spt how-to`).
- `21f50be` file-backed bodies: signoff (`spt endpoint shutdown`), list-agents (`spt endpoint list`).
- `c00b090` ci/hooks/poll-int.sh + REQ-DIST-HOOKS-API/REQ-UPS-INJECTION int flipped; F-002/F-003 closed.

**Skills operative (7/11):** whoami, setup, ready, send, version, signoff, list-agents — file-backed
bodies in adapter/strings/skills/*.md via [strings.skills].<x> = { file = "skills/<x>.md" }, injected
by the UPS hook. setup is SELF-CONTAINED in its SKILL.md (runs when spt may be absent → injection
no-ops). registration-int extended (9/9): file-backed resolve + skill-injection end-to-end.

**Remaining 4 skills (classified vs v0.7.1, NOT authored — don't invent commands):**
- live → `spt endpoint run` exists but needs manifest [session.self] (deferred slice).
- force-stop → only soft `endpoint stop`/`shutdown` in core; no force-kill/Psyche teardown → defer/finding.
- commune, new-alarm → ABSENT from core spt v0.7.1 (live-agent/Psyche layer = owl/$LIVE, not core) →
  defer; scope/finding question (does claude-spt reach the Psyche layer at all?).

**REQ status:** REQ-DIST-MANIFEST-SCHEMA, REQ-DIST-HOOKS-API, REQ-UPS-INJECTION all [doc,impl,unit,int]
GREEN. REQ-DIST-SHORTCUT-BASENAME [doc,unit] (impl/int = cc launcher + picker, deferred spt-core wave).

**Next candidate slices:** (1) [session.self] + endpoint-run + cc launcher → unblocks live skill +
REQ-DIST-SHORTCUT-BASENAME impl/int; (2) claude-spt-digest extractor (contract {role,text,tool,ts}
JSONL, `claude-spt-digest --session {session_id} --in {source}`) → unblocks `spt adapter digest-proof`;
(3) the 4 deferred skills' live-layer scope question; (4) large-drain truncate/spill (ADR-0002 Open #2).
Then ship when parity proven.
</project-context>


Commune queued.

## REQ-MSG-ENVELOPE / ADR-0020 — closed end-to-end

After 0.7.1 upgrade:
- **Byte-captured live `api poll`** (od-verified): `<EVENT type="msg" from="probe">body</EVENT>\n` — body escaping = my `render_frames` decode rule exactly; whole self-delimiting + `\n`-framed envelopes; **no `<EVENT-PART>`, no `__REPLY_TO__`**; multi-drain splits on `</EVENT>`. **F-002 dissolved** on the published binary. render_frames confirm-match PASS.
- **`ci/hooks/poll-int.sh`** (new, 5/5, SPTC_ACCEPTANCE-gated, disposable perch): bind→send→poll→render_frames assert.
- **Flipped REQ-DIST-HOOKS-API + REQ-UPS-INJECTION `int` green.**
- **F-002 RESOLVED-SHIPPED, F-003 docs CLOSED** (v0.7.1 published my 3 flagged doc gaps).
- doyle confirmed capstone.

## Full session tally — 8 commits, all gated
F-003 lifecycle → file-backed `[strings]` proven shipped → UPS skill-injection branch → **7/11 skills operative** (whoami/setup/ready/send/version/signoff/list-agents) → REQ-MSG-ENVELOPE int closed.

3 reqs now `[doc,impl,unit,int]` green. **No open findings, none owed.**

## Remaining (next session, in plan)
- `[session.self]` + endpoint-run + cc launcher → unblocks `live` skill + REQ-DIST-SHORTCUT-BASENAME impl/int
- `claude-spt-digest` extractor → `spt adapter digest-proof`
- 4 deferred skills' live-layer scope question (commune/new-alarm/force-stop/live)
- large-drain truncate/spill (ADR-0002 Open #2)

Clean stopping point. Perch live on 0.7.1.


<command-message>spt:commune</command-message>
<command-name>/spt:commune</command-name>
<command-args>with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session</command-args>


Base directory for this skill: C:\Users\decid\.ccs\instances\bigscreen\plugins\cache\cplugs\spt\1.11.25\skills\commune

# /spt:commune

All commands use `$LIVE` env var, auto-injected by the plugin's SessionStart hook. If commands fail with "command not found", restart the Claude Code session so the SessionStart hook re-runs.

> **Identity auto-detection:** Your identity is auto-detected from your session. Pass your ID explicitly only if auto-detection fails.

## Flow

1. **Download Psyche's *current* context** — it shifts during the session as background echo communes fire. Re-download right before composing; the gen-N snapshot you read at session start is already stale:
   ```bash
   $LIVE psyche-download <your-id>
   ```

   > **Output truncation.** If the Bash tool reports `Output too large (N KB)` and saves the full result to a `tool-results/<id>.txt` file, use the `Read` tool on that saved path to access the full content. Do NOT re-run `psyche-download` to grep/awk out sections — repeated invocations re-download the same context, waste tokens, and may duplicate `## Pending Commune` sections in `live_context.md`.

2. **Diff your knowledge against this fresh download** — only what's missing from *it* is the real delta (new work, decisions, intentions). Anything already absorbed is not. If the download includes a `<memformat>` section, use it as a guide for what topics to cover. A `## Pending Commune (uncommitted)` section means a previous commune is still queued — do NOT re-send it.

3. **Commune only the delta** -- use the **Write tool** to create
   `.claude/{your-id}-commune.md` with the two-slice body as its contents
   (see `## Two-slice body shape (Phase 25 D-10/D-11)` below for the required
   envelope wrapping). Do not use Bash/heredoc -- Write is the canonical path:
   no shell escaping, no `EOF` collision risk, and Write auto-creates the
   `.claude/` parent directory.

   Your Self listener detects the file on its next poll iteration, notifies
   your Psyche wrapper, and the wrapper ingests the content via the existing
   Psyche session. On success (subprocess exit code 0) the wrapper deletes the
   file. On error (token rate limit, etc) the file is retained and retried on
   the next consume cycle. If you write again before the wrapper consumes, the
   latest write wins (last-write-wins overwrite semantics).

If Psyche's context is already current, no commune is needed. Say so and move on.

Read `commune.md` (in this skill directory) for the full commune protocol -- when to send, what to include, and diligence triggers.

**Agent-type guard:** Only works on live agents. If target is not live, refuses with: "Communes require a live agent with Psyche."

## Two-slice body shape (Phase 25 D-10/D-11)

The body you write in `.claude/{your-id}-commune.md` MUST itself be wrapped in two-slice envelopes per Phase 25 D-10/D-11. The taxonomy — what belongs in `<live-context>` vs `<project-context>` — is defined canonically in `psyche.md` §`<output_envelope>` (lines 282–323). Read that section as the source of truth; this skill doc teaches only when and how to apply it.

**In-project detection rule (D-25.1-04).** Before composing your commune body, run `$LIVE psyche-download <your-id>` (Step 1 of `## Flow` above) and inspect the `<current ... />` tag at the top of the output. The `<current/>` tag carries a `project="..."` attribute whenever any payload is produced — its presence means you are inside a tracked project (inside a git repo, `project` is the repo name; outside a git repo per D-11, `project` is the cwd-basename fallback, which still counts as a project context for routing purposes). A `psyche-download` invocation that returns no payload at all (NO-CONTEXT exit) means no project resolved. So: `project="..."` populated on `<current/>` → emit BOTH envelopes (apply rule 1 or 2 below); no payload at all → emit ONLY `<live-context>` (rule 3). Secondary signal: the `<project-context-resolved name="..."/>` sentinel further down the output indicates the project file already has prior content — useful for spotting first-time-in-project pairings, but NOT the routing rule.

**Three rules (D-25.1-01..03):**

- **In-project, both slices populated** (marker present, you have project-specific content this cycle): emit `<live-context>...</live-context>` AND `<project-context>...</project-context>`, both with non-empty bodies.
- **In-project, no project-specific update this cycle** (marker present, but nothing project-bound to report): emit `<live-context>...</live-context>` AND `<project-context></project-context>` (empty body — the deliberate "in-project but quiet" signal per D-25.1-02). Distinguishable from "no project resolved".
- **Outside any tracked project** (marker absent): emit ONLY `<live-context>...</live-context>`. Do NOT emit an empty `<project-context>` envelope (missing tag means "no project resolved" per D-25.1-03).

**Nested envelopes.** The Phase 23 `<EVENT type="commune">` envelope (see `## Envelope shape (Phase 23 v1.8)` below) is composed by `$LIVE commune` automatically — you NEVER write `<EVENT>` tags. You write ONLY the inner two-slice body. The two envelope layers nest: outer EVENT wraps inner two-slice body.

(See `psyche.md` §`<output_envelope>` for the canonical taxonomy and per-slice content rules.)

## Envelope shape (Phase 23 v1.8)

As of v1.8 Phase 23, commune deliveries are wrapped in a typed EVENT envelope carrying a 5-field stamp (`machine`, `project`, `branch`, `head_sha`, `head_subject`); the prose `COMMUNE (timestamp): body` form is gone.

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="claude_skill_owl" branch="main" head_sha="abc123..." head_subject="subject line capped at 72 chars">body</EVENT>
```

Outside a git repo (D-11), `branch`, `head_sha`, and `head_subject` are omitted entirely (not empty-string-set):

```xml
<EVENT type="commune" timestamp="2026-05-20T06:00:00-07:00" machine="hostname" project="cwd-basename">body</EVENT>
```

`$LIVE commune` composes this envelope automatically — Self does NOT manually construct EVENT XML. The `body` placeholder in the example above is the two-slice body (see `## Two-slice body shape (Phase 25 D-10/D-11)` above); the two envelope layers nest. The wrapper-side parser ignores unknown attrs, so the stamp fields are forward-compatible with older receivers. For drift detection on resume, see the `live` skill.

ARGUMENTS: with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Download fresh Psyche context to diff against (it shifted — I queued a commune earlier this session).


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code; $LIVE psyche-download perri 2>&1 | head -60

Download current Psyche context