cp "$DIGEST" "$STAGE/claude-spt-digest$EXE"
cp "$PSYCHE" "$STAGE/claude-spt-psyche$EXE"
cp "$IDLE" "$STAGE/cc-spt-idle-translate$EXE"

mkdir -p "$(dirname "$OUT")" || { echo "FATAL: cannot create $(dirname "$OUT")"; exit 2; }
( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings claude-spt-digest$EXE claude-spt-psyche$EXE cc-spt-idle-translate$EXE ) \
  || { echo "FATAL: tar failed"; exit 2; }

#!/bin/sh
# Pack the `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` (doyle's --release source, spt v0.7.3+).
# The archive ROOT must hold `manifest.toml` (named EXACTLY that) + `strings/` + the tool binaries;
# spt-core fetches it from the repo's GitHub release, extracts to the durable home, registers the
# root. DRY-RUN by default. Never uploads — the release/tag/upload is the operator's step.
# [impl->REQ-DIST-ADAPTER-RELEASE]
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# PER-OS asset name: the .spt carries native binaries, so it is platform-specific and ships as
# `adapter-<os>-<arch>.spt`. /sptc:setup acquires the matching one via spt's caller-named `--asset`.
# Host-derived from uname; override SPTC_OS/SPTC_ARCH for a cross build / CI. [impl->REQ-DIST-ADAPTER-PEROS]
SPTC_OS="${SPTC_OS:-}"; SPTC_ARCH="${SPTC_ARCH:-}"
if [ -z "$SPTC_OS" ]; then case "$(uname -s 2>/dev/null)" in
  MINGW*|MSYS*|CYGWIN*|Windows*) SPTC_OS=windows ;;
  Linux) SPTC_OS=linux ;;
  Darwin) SPTC_OS=macos ;;
  *) SPTC_OS=unknown ;;
esac; fi
if [ -z "$SPTC_ARCH" ]; then case "$(uname -m 2>/dev/null)" in
  x86_64|amd64) SPTC_ARCH=x86_64 ;;
  arm64|aarch64) SPTC_ARCH=aarch64 ;;
  *) SPTC_ARCH="$(uname -m 2>/dev/null || echo unknown)" ;;
esac; fi
# Native executable suffix, keyed off the DECLARED os (windows → .exe, else bare) — the archive
# carries the binary under its native name so a bare-name template (`claude-spt-digest …`) resolves.
case "$SPTC_OS" in windows) EXE=".exe" ;; *) EXE="" ;; esac
# Binary location: a cross build sets SPTC_TARGET (e.g. x86_64-unknown-linux-gnu → cargo-zigbuild) and
# the binaries land under target/<triple>/release; a native build uses target/release.
if [ -n "${SPTC_TARGET:-}" ]; then RELSUB="$SPTC_TARGET/release"; else RELSUB="release"; fi
DIGEST="$ROOT/tools/claude-spt-digest/target/$RELSUB/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/$RELSUB/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/$RELSUB/cc-spt-idle-translate$EXE"
ASSET="adapter-${SPTC_OS}-${SPTC_ARCH}.spt"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/$ASSET}"   # overridable so the unit test writes to a tmp file

# Validate the manifest first — refuse to ship an invalid adapter.
echo "== validate manifest =="
if ! sh "$ROOT/ci/manifest/check-manifest.sh"; then
  echo "REFUSING to package: manifest failed schema validation (fix above, then retry)." >&2
  exit 1
fi

# Require the release binaries (NOT auto-copied by register — they must ride the archive; doyle).
echo
echo "== tool binaries =="
missing=0
for b in "$DIGEST" "$PSYCHE" "$IDLE"; do
  if [ -f "$b" ]; then echo "  ok    $b"; else echo "  MISS  $b"; missing=1; fi
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build the release binaries first — sh ci/digest/build.sh && sh ci/psyche/build.sh && sh ci/idle-translate/build.sh" >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (archive root) manifest.toml"
echo "strings  : $STRINGS/  ->  (archive root) strings/"
echo "binaries : claude-spt-digest$EXE, claude-spt-psyche$EXE, cc-spt-idle-translate$EXE  ->  (archive root)"
echo "os/arch  : $SPTC_OS/$SPTC_ARCH"
echo "asset    : $OUT  (-> $ASSET)"
echo "NOTE: the archive is PLATFORM-SPECIFIC (native binaries: '$EXE' build) — hence the per-OS name."
echo "      Ship one asset per OS on the same release; /sptc:setup picks the host's via --asset. Build"
echo "      each OS native, OR cross-build Linux from Windows via cargo-zigbuild + SPTC_TARGET (bare"
echo "      'cargo build --target' fails 'cc not found'; zig provides the linker). On v0.8.0+ Feature B"
echo "      resolves the bundled binaries from the install dir (no PATH copy); pre-0.8.0 they need PATH."

if [ "$APPLY" -ne 1 ]; then
  echo
  echo "DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it to a GitHub"
  echo "release on the monorepo. End users acquire it with 'spt adapter add --release"
  echo "SaberMage/spt-claude-code --asset $ASSET' (see docs/RELEASE-RUNBOOK.md). Needs spt v0.7.3+."
  exit 0
fi

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
cp "$DIGEST" "$STAGE/claude-spt-digest$EXE"
cp "$PSYCHE" "$STAGE/claude-spt-psyche$EXE"

mkdir -p "$(dirname "$OUT")" || { echo "FATAL: cannot create $(dirname "$OUT")"; exit 2; }
( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings claude-spt-digest$EXE claude-spt-psyche$EXE ) \
  || { echo "FATAL: tar failed"; exit 2; }

# Self-validate the produced archive: manifest.toml must be at the ROOT (no leading path component).
echo
echo "== validate archive =="
if tar -tzf "$OUT" | grep -qx "manifest.toml"; then
  echo "ok   manifest.toml at archive root"
else
  echo "FATAL: manifest.toml is not at the archive root — adapter add --release would reject it" >&2
  exit 1
fi

echo
echo "WROTE $OUT. Next (operator): attach to a GitHub release on SaberMage/spt-claude-code (alongside"
echo "the other per-OS assets), then end users 'spt adapter add --release SaberMage/spt-claude-code"
echo "--asset $ASSET' (needs spt v0.7.3+). /reload as needed."
exit 0

-( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings claude-spt-digest$EXE claude-spt-psyche$EXE ) \

+( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings claude-spt-digest$EXE claude-spt-psyche$EXE cc-spt-idle-translate$EXE ) \


The archive unit test (`tests/adapter-archive.sh`) asserts the packed root contents — check if it enumerates an exact binary set.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -nE "digest|psyche|cc-spt|idle|binar|tar -t|count|-eq|root" tests/adapter-archive.sh | head -40

Inspect adapter-archive test expectations


4:# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.
13:[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
14:DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
15:PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
18:  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
21:    fail "packer should refuse (exit!=0) when release binaries are absent"
23:    echo "ok   packer refuses when binaries absent (guard works)"
25:  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"
43:LIST=$(tar -tzf "$OUT")
44:# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
46:  && echo "ok   manifest.toml at archive root" \
47:  || fail "manifest.toml not at archive root (add --release would reject)"
52:# both tool binaries present at root.
53:echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
54:  && echo "ok   claude-spt-digest$EXE at root" \
55:  || fail "claude-spt-digest$EXE missing from archive"
56:echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
57:  && echo "ok   claude-spt-psyche$EXE at root" \
58:  || fail "claude-spt-psyche$EXE missing from archive"
61:  fail "manifest.toml appears under a subdir — archive root is wrong"
82:[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"


start with A, yes. -the objective for [message-idle-translation-binary] is for it to output this set of key events per message: ctrl+s (stash any existing message), delay 50ms, message text+`\r` (to s…


Implement message-idle-translation-binary key events


10	fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
11
12	EXE=""
13	[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
14	DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
15	PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
16
17	if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ]; then
18	  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
19	  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
20	  if sh "$PACKER" >/dev/null 2>&1; then
21	    fail "packer should refuse (exit!=0) when release binaries are absent"
22	  else
23	    echo "ok   packer refuses when binaries absent (guard works)"
24	  fi
25	  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"
26	  exit "$rc"
27	fi
28
29	# Binaries present → build a real archive to a temp file and assert its structure.
30	TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
31	trap 'rm -rf "$TMP"' EXIT
32	OUT="$TMP/adapter.spt"
33
34	if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
35	  echo "ok   packer --apply succeeded"
36	else
37	  fail "packer --apply exited non-zero"
38	  exit "$rc"
39	fi
40
41	[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }
42
43	LIST=$(tar -tzf "$OUT")
44	# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
45	echo "$LIST" | grep -qx "manifest.toml" \
46	  && echo "ok   manifest.toml at archive root" \
47	  || fail "manifest.toml not at archive root (add --release would reject)"
48	# strings/ present (any entry under strings/).
49	echo "$LIST" | grep -q "^strings/" \
50	  && echo "ok   strings/ present" \
51	  || fail "strings/ missing from archive"
52	# both tool binaries present at root.
53	echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
54	  && echo "ok   claude-spt-digest$EXE at root" \
55	  || fail "claude-spt-digest$EXE missing from archive"
56	echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
57	  && echo "ok   claude-spt-psyche$EXE at root" \
58	  || fail "claude-spt-psyche$EXE missing from archive"
59	# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
60	if echo "$LIST" | grep -q "/manifest.toml"; then
61	  fail "manifest.toml appears under a subdir — archive root is wrong"
62	else
63	  echo "ok   no nested manifest.toml"
64	fi
65
66	# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
67	# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
68	# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
69	# [unit->REQ-DIST-ADAPTER-PEROS]
70	plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
71	echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \

fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

EXE=""
[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ]; then
  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
  if sh "$PACKER" >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when release binaries are absent"
  else
    echo "ok   packer refuses when binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"
  exit "$rc"
fi

# Binaries present → build a real archive to a temp file and assert its structure.
TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
OUT="$TMP/adapter.spt"

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi

[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

LIST=$(tar -tzf "$OUT")
# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# strings/ present (any entry under strings/).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   strings/ present" \
  || fail "strings/ missing from archive"
# both tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
if echo "$LIST" | grep -q "/manifest.toml"; then
  fail "manifest.toml appears under a subdir — archive root is wrong"
else
  echo "ok   no nested manifest.toml"
fi

# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
# [unit->REQ-DIST-ADAPTER-PEROS]
plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \


Update the archive test for the third binary.


Edit

DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ]; then

DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then


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

DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then

#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the archive-ROOT invariant that `spt adapter add --release` depends on: the packed .spt
# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.
# [unit->REQ-DIST-ADAPTER-RELEASE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

EXE=""
[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ]; then
  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
  if sh "$PACKER" >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when release binaries are absent"
  else
    echo "ok   packer refuses when binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"
  exit "$rc"
fi

# Binaries present → build a real archive to a temp file and assert its structure.
TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
OUT="$TMP/adapter.spt"

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi

[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

LIST=$(tar -tzf "$OUT")
# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# strings/ present (any entry under strings/).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   strings/ present" \
  || fail "strings/ missing from archive"
# both tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
if echo "$LIST" | grep -q "/manifest.toml"; then
  fail "manifest.toml appears under a subdir — archive root is wrong"
else
  echo "ok   no nested manifest.toml"
fi

# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
# [unit->REQ-DIST-ADAPTER-PEROS]
plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \
  && echo "ok   per-OS asset name honors SPTC_OS/SPTC_ARCH (adapter-linux-aarch64.spt)" \
  || fail "per-OS override name wrong; plan=[$plan]"
echo "$plan" | grep -q -- "--asset adapter-linux-aarch64\.spt" \
  && echo "ok   dry-run advertises the end-user --asset for that OS" \
  || fail "dry-run missing '--asset adapter-linux-aarch64.spt'; plan=[$plan]"
hostplan=$(sh "$PACKER" 2>&1)
echo "$hostplan" | grep -qE "adapter-(windows|linux|macos|unknown)-[A-Za-z0-9_]+\.spt" \
  && echo "ok   host default asset name matches adapter-<os>-<arch>.spt" \
  || fail "host default asset name not adapter-<os>-<arch>.spt; plan=[$hostplan]"

[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
exit "$rc"

-if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ]; then


echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"

echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche,idle-translate}/build.sh)"


#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the archive-ROOT invariant that `spt adapter add --release` depends on: the packed .spt
# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.
# [unit->REQ-DIST-ADAPTER-RELEASE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

EXE=""
[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then
  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
  if sh "$PACKER" >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when release binaries are absent"
  else
    echo "ok   packer refuses when binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"
  exit "$rc"
fi

# Binaries present → build a real archive to a temp file and assert its structure.
TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
OUT="$TMP/adapter.spt"

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi

[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

LIST=$(tar -tzf "$OUT")
# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# strings/ present (any entry under strings/).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   strings/ present" \
  || fail "strings/ missing from archive"
# both tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
if echo "$LIST" | grep -q "/manifest.toml"; then
  fail "manifest.toml appears under a subdir — archive root is wrong"
else
  echo "ok   no nested manifest.toml"
fi

# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
# [unit->REQ-DIST-ADAPTER-PEROS]
plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \
  && echo "ok   per-OS asset name honors SPTC_OS/SPTC_ARCH (adapter-linux-aarch64.spt)" \
  || fail "per-OS override name wrong; plan=[$plan]"
echo "$plan" | grep -q -- "--asset adapter-linux-aarch64\.spt" \
  && echo "ok   dry-run advertises the end-user --asset for that OS" \
  || fail "dry-run missing '--asset adapter-linux-aarch64.spt'; plan=[$plan]"
hostplan=$(sh "$PACKER" 2>&1)
echo "$hostplan" | grep -qE "adapter-(windows|linux|macos|unknown)-[A-Za-z0-9_]+\.spt" \
  && echo "ok   host default asset name matches adapter-<os>-<arch>.spt" \
  || fail "host default asset name not adapter-<os>-<arch>.spt; plan=[$hostplan]"

[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
exit "$rc"

-  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche}/build.sh)"

+  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche,idle-translate}/build.sh)"


# both tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"

# all three tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
echo "$LIST" | grep -qx "cc-spt-idle-translate$EXE" \
  && echo "ok   cc-spt-idle-translate$EXE at root" \
  || fail "cc-spt-idle-translate$EXE missing from archive"


# all three tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
echo "$LIST" | grep -qx "cc-spt-idle-translate$EXE" \
  && echo "ok   cc-spt-idle-translate$EXE at root" \
  || fail "cc-spt-idle-translate$EXE missing from archive"

#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the archive-ROOT invariant that `spt adapter add --release` depends on: the packed .spt
# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.
# [unit->REQ-DIST-ADAPTER-RELEASE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

EXE=""
[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then
  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
  if sh "$PACKER" >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when release binaries are absent"
  else
    echo "ok   packer refuses when binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche,idle-translate}/build.sh)"
  exit "$rc"
fi

# Binaries present → build a real archive to a temp file and assert its structure.
TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
OUT="$TMP/adapter.spt"

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi

[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

LIST=$(tar -tzf "$OUT")
# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# strings/ present (any entry under strings/).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   strings/ present" \
  || fail "strings/ missing from archive"
# both tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
if echo "$LIST" | grep -q "/manifest.toml"; then
  fail "manifest.toml appears under a subdir — archive root is wrong"
else
  echo "ok   no nested manifest.toml"
fi

# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
# [unit->REQ-DIST-ADAPTER-PEROS]
plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \
  && echo "ok   per-OS asset name honors SPTC_OS/SPTC_ARCH (adapter-linux-aarch64.spt)" \
  || fail "per-OS override name wrong; plan=[$plan]"
echo "$plan" | grep -q -- "--asset adapter-linux-aarch64\.spt" \
  && echo "ok   dry-run advertises the end-user --asset for that OS" \
  || fail "dry-run missing '--asset adapter-linux-aarch64.spt'; plan=[$plan]"
hostplan=$(sh "$PACKER" 2>&1)
echo "$hostplan" | grep -qE "adapter-(windows|linux|macos|unknown)-[A-Za-z0-9_]+\.spt" \
  && echo "ok   host default asset name matches adapter-<os>-<arch>.spt" \
  || fail "host default asset name not adapter-<os>-<arch>.spt; plan=[$hostplan]"

[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
exit "$rc"

+# all three tool binaries present at root.

+echo "$LIST" | grep -qx "cc-spt-idle-translate$EXE" \

+  && echo "ok   cc-spt-idle-translate$EXE at root" \

+  || fail "cc-spt-idle-translate$EXE missing from archive"


# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.

# holds manifest.toml (named exactly) + strings/ + the three tool binaries at the archive root.


#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the archive-ROOT invariant that `spt adapter add --release` depends on: the packed .spt
# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.
# [unit->REQ-DIST-ADAPTER-RELEASE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

EXE=""
[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"

if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then
  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
  if sh "$PACKER" >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when release binaries are absent"
  else
    echo "ok   packer refuses when binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche,idle-translate}/build.sh)"
  exit "$rc"
fi

# Binaries present → build a real archive to a temp file and assert its structure.
TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
trap 'rm -rf "$TMP"' EXIT
OUT="$TMP/adapter.spt"

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi

[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

LIST=$(tar -tzf "$OUT")
# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# strings/ present (any entry under strings/).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   strings/ present" \
  || fail "strings/ missing from archive"
# all three tool binaries present at root.
echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
  && echo "ok   claude-spt-digest$EXE at root" \
  || fail "claude-spt-digest$EXE missing from archive"
echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
  && echo "ok   claude-spt-psyche$EXE at root" \
  || fail "claude-spt-psyche$EXE missing from archive"
echo "$LIST" | grep -qx "cc-spt-idle-translate$EXE" \
  && echo "ok   cc-spt-idle-translate$EXE at root" \
  || fail "cc-spt-idle-translate$EXE missing from archive"
# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
if echo "$LIST" | grep -q "/manifest.toml"; then
  fail "manifest.toml appears under a subdir — archive root is wrong"
else
  echo "ok   no nested manifest.toml"
fi

# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
# [unit->REQ-DIST-ADAPTER-PEROS]
plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \
  && echo "ok   per-OS asset name honors SPTC_OS/SPTC_ARCH (adapter-linux-aarch64.spt)" \
  || fail "per-OS override name wrong; plan=[$plan]"
echo "$plan" | grep -q -- "--asset adapter-linux-aarch64\.spt" \
  && echo "ok   dry-run advertises the end-user --asset for that OS" \
  || fail "dry-run missing '--asset adapter-linux-aarch64.spt'; plan=[$plan]"
hostplan=$(sh "$PACKER" 2>&1)
echo "$hostplan" | grep -qE "adapter-(windows|linux|macos|unknown)-[A-Za-z0-9_]+\.spt" \
  && echo "ok   host default asset name matches adapter-<os>-<arch>.spt" \
  || fail "host default asset name not adapter-<os>-<arch>.spt; plan=[$hostplan]"

[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
exit "$rc"

-# holds manifest.toml (named exactly) + strings/ + both tool binaries at the archive root.

+# holds manifest.toml (named exactly) + strings/ + the three tool binaries at the archive root.


Now the traceability registry. Update the skip-perms hazard REQ to include `[session.resume]`, then add the two new REQs.


id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin) AND both [session.self] bringup commands (base claude + ccs profile, broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."
required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.

id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."
required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).


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)

id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin) AND both [session.self] bringup commands (base claude + ccs profile, broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."
required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.

id = "REQ-HAZARD-PSYCHE-PERMS-DEADLOCK"
title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."
required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).

-title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin) AND both [session.self] bringup commands (base claude + ccs profile, broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."

-required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (both [session.self] commands carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity.

+title = "Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded."

+required_stages = ["doc", "unit"]  # doc: docs/KNOWN-HAZARDS.md §2.2 + docs/adr/0003-*. unit: tools/claude-spt-psyche/src/main.rs every_turn_is_sandboxed_to_legacy_owl_parity (psyche turns) + tests/manifest-shortcut.sh (all THREE CC spawn commands — both [session.self] + [session.resume] — carry the flag). Legacy parity: claude_skill_owl src/live/wrapper/claude.rs (init/resume/final all skip-permissions). Identified 2026-06-18 reaching psyche parity; extended to [session.resume] 2026-06-20 (v0.13.0 native-resume seam).

# ── 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.


226	id = "REQ-DIST-ADAPTER-RELEASE"
227	title = "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/spt-claude-code` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)"
228	required_stages = ["doc", "impl", "unit", "int"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + both binaries; reject a malformed archive). int: ci/publish/release-acquire-int.sh — real `adapter add --release SaberMage/spt-claude-code` acquire on a clean target: claude-spt active, sourced from the _github install dir, profiles/strings from the .spt resolve (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.3, network); dogfood-proven on spt 0.7.3.
229
230	[[requirements]]
231	id = "REQ-DIST-ADAPTER-PEROS"
232	title = "The adapter.spt asset is the tool binaries' platform, so it ships PER-OS: the packer emits `adapter-<os>-<arch>.spt` (os ∈ windows|linux|macos, arch ∈ x86_64|aarch64) and /sptc:setup detects the host os+arch and passes the matching `--asset adapter-<os>-<arch>.spt` to `adapter add --release` (spt's `--asset` flag is caller-named; default adapter.spt). v1 OSes = windows + linux."
233	required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.

id = "REQ-DIST-ADAPTER-RELEASE"
title = "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/spt-claude-code` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)"
required_stages = ["doc", "impl", "unit", "int"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + both binaries; reject a malformed archive). int: ci/publish/release-acquire-int.sh — real `adapter add --release SaberMage/spt-claude-code` acquire on a clean target: claude-spt active, sourced from the _github install dir, profiles/strings from the .spt resolve (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.3, network); dogfood-proven on spt 0.7.3.

[[requirements]]
id = "REQ-DIST-ADAPTER-PEROS"
title = "The adapter.spt asset is the tool binaries' platform, so it ships PER-OS: the packer emits `adapter-<os>-<arch>.spt` (os ∈ windows|linux|macos, arch ∈ x86_64|aarch64) and /sptc:setup detects the host os+arch and passes the matching `--asset adapter-<os>-<arch>.spt` to `adapter add --release` (spt's `--asset` flag is caller-named; default adapter.spt). v1 OSes = windows + linux."
required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.


unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + both binaries; reject a malformed archive).

unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + the three binaries [digest, psyche, cc-spt-idle-translate]; reject a malformed archive).


title = "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/spt-claude-code` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)"

-required_stages = ["doc", "impl", "unit", "int"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + both binaries; reject a malformed archive). int: ci/publish/release-acquire-int.sh — real `adapter add --release SaberMage/spt-claude-code` acquire on a clean target: claude-spt active, sourced from the _github install dir, profiles/strings from the .spt resolve (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.3, network); dogfood-proven on spt 0.7.3.

+required_stages = ["doc", "impl", "unit", "int"]  # doc: SETUP-SLICE-PLAN.md Wave C' + docs/RELEASE-RUNBOOK.md (the adapter.spt release-asset publish target). impl: ci/publish/package-adapter.sh (validate manifest + pack manifest.toml[renamed from claude-spt.toml]+strings/+release tool binaries at archive ROOT; dry-run default, --apply writes dist/adapter.spt; never uploads — the release/tag/upload stays the operator's step). unit: tests/adapter-archive.sh (the packed .spt root holds manifest.toml named exactly + strings/ + the three binaries [digest, psyche, cc-spt-idle-translate]; reject a malformed archive). int: ci/publish/release-acquire-int.sh — real `adapter add --release SaberMage/spt-claude-code` acquire on a clean target: claude-spt active, sourced from the _github install dir, profiles/strings from the .spt resolve (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.3, network); dogfood-proven on spt 0.7.3.


Now append the two new v0.13.0 REQs.


required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.

required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.

# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from
# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
# harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Two new RUN-group
# seams are buildable against the contract NOW; their RUNTIME dogfood (the `int` stage) is GATED on
# the v0.13.0 daemon — so both activate ["impl","unit"] and add "int" when doyle pings the binary
# ship. doc DEFERRED to the docs-site harness-contract RUN-group slice (mirrors REQ-DIST-DIGEST-
# EXTRACTOR's doc-deferred precedent — documented in the manifest/module comments meanwhile). See
# V13-ADAPTER-PLAN.md.

[[requirements]]
id = "REQ-DIST-SESSION-RESUME"
title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."
required_stages = ["impl", "unit"]  # impl: [session.resume] in adapter/claude-spt.toml (the v0.13.0 native-resume role; validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED: a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.

[[requirements]]
id = "REQ-DIST-IDLE-TRANSLATE"
title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft. Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."
required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol.


required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.

# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from
# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
# harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Two new RUN-group
# seams are buildable against the contract NOW; their RUNTIME dogfood (the `int` stage) is GATED on
# the v0.13.0 daemon — so both activate ["impl","unit"] and add "int" when doyle pings the binary
# ship. doc DEFERRED to the docs-site harness-contract RUN-group slice (mirrors REQ-DIST-DIGEST-
# EXTRACTOR's doc-deferred precedent — documented in the manifest/module comments meanwhile). See
# V13-ADAPTER-PLAN.md.

[[requirements]]
id = "REQ-DIST-SESSION-RESUME"
title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."
required_stages = ["impl", "unit"]  # impl: [session.resume] in adapter/claude-spt.toml (the v0.13.0 native-resume role; validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED: a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.

[[requirements]]
id = "REQ-DIST-IDLE-TRANSLATE"
title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft. Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."
required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol.

title = "The adapter.spt asset is the tool binaries' platform, so it ships PER-OS: the packer emits `adapter-<os>-<arch>.spt` (os ∈ windows|linux|macos, arch ∈ x86_64|aarch64) and /sptc:setup detects the host os+arch and passes the matching `--asset adapter-<os>-<arch>.spt` to `adapter add --release` (spt's `--asset` flag is caller-named; default adapter.spt). v1 OSes = windows + linux."

+# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from

+# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/

+# harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Two new RUN-group

+# seams are buildable against the contract NOW; their RUNTIME dogfood (the `int` stage) is GATED on

+# the v0.13.0 daemon — so both activate ["impl","unit"] and add "int" when doyle pings the binary

+# ship. doc DEFERRED to the docs-site harness-contract RUN-group slice (mirrors REQ-DIST-DIGEST-

+# EXTRACTOR's doc-deferred precedent — documented in the manifest/module comments meanwhile). See

+title = "The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=[\"session_id\",\"id\"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd."

+required_stages = ["impl", "unit"]  # impl: [session.resume] in adapter/claude-spt.toml (the v0.13.0 native-resume role; validated against CC's real `-r`/`--remote-control` flags — both exist in `claude --help`). unit: tests/manifest-shortcut.sh — [session.resume] present, the `-r {session_id}` reload-fill + `--remote-control {id}` id-fill, and the 3rd skip-perms CC spawn (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). int DEFERRED: a real native-resume launch (resumed-transcript reload + RC drive) needs the v0.13.0 daemon (docs lead the binary, still v0.12.x on spt-releases) — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (the seam is fully documented in the manifest [session.resume] comment meanwhile; mirrors REQ-DIST-DIGEST-EXTRACTOR's doc-deferred precedent). Public-surface-complete: the contract page (harness-contract/manifest.html#session.resume, since v0.13.0) is the authority; no spt-core internals.

+title = "The adapter ships the [message-idle-translation-binary] (path=\"cc-spt-idle-translate\") — a lifecycle-managed stdin→stdout JSON-lines filter that turns each idle inbound <EVENT> envelope into the keystroke choreography spt-core applies ATOMICALLY to the broker PTY: ctrl+s (stash any draft) · 50ms · <envelope>\\r (the CR submits) · 50ms · ctrl+s (restore the draft) — so an inbound message never clobbers a half-typed operator draft. Idle delivery ONLY (the spt-hosted complement to the busy/mid-turn [inject] hook path); coexists with a live `spt rc` controller."

+required_stages = ["impl", "unit"]  # impl: tools/cc-spt-idle-translate (Rust JSON-lines filter — event→5-step choreography, init/input/unknown-type emit nothing, CR/LF in the envelope neutralized so only the trailing submit \r fires, forward-compat unknown fields ignored) + [message-idle-translation-binary] path="cc-spt-idle-translate" in adapter/claude-spt.toml + ci/idle-translate/build.sh gate (wired into ci/run-gates.sh) + packed into adapter.spt (ci/publish/package-adapter.sh, the 3rd binary). unit: the crate's 9 cargo tests (run by ci/idle-translate/build.sh: the choreography order, text+\r submit-not-enter-key, stash/restore brackets, CR/LF neutralization, dispatch, no-output types, envelope-less event, unknown fields, single-field command shape) + tests/{manifest-shortcut.sh (manifest path), adapter-archive.sh (binary packs at root)}. int DEFERRED: the spawn-on-up/reap-on-down lifecycle + atomic PTY apply need the v0.13.0 daemon — add "int" when doyle ships it. doc DEFERRED to the docs-site harness-contract RUN-group slice (documented in the manifest + module comment meanwhile). Public-surface-complete: the contract page (harness-contract/manifest.html, message-idle-translation-binary since v0.13.0) is the authority; the operator-specified ctrl+s choreography is the adapter's own UX layer atop the generic key/text/delay_ms protocol.


Now the JIT plan file, then run gates.


Write

# V13-ADAPTER-PLAN — build the two v0.13.0 harness-contract seams ahead of the binary

> **Scope.** spt-core's v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
> harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Build + structure
> the two new RUN-group adapter seams against the contract NOW; defer the runtime dogfood (`int`) to
> the v0.13.0 daemon. Public-surface-only (AGENTS.md): the contract pages are the authority.

## Seams

### 1. `[session.resume]` — native-resume (REQ-DIST-SESSION-RESUME)
- CC verb: `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`,
  keys `["session_id","id"]`. Both flags verified real (`claude --help`: `-r/--resume`,
  `--remote-control [name]`).
- `{session_id}` reloads the real transcript; `{id}` = the remote-control session name (RC is the
  channel native-resume drives — same channel `spt rc` + the idle-translation-binary use). Omitting
  the role → silent fallback to `[session.self]` = blank fresh session (the documented footgun).
- PTY lands in the resumed session's recorded project cwd.

### 2. `[message-idle-translation-binary]` — idle PTY delivery (REQ-DIST-IDLE-TRANSLATE)
- `path = "cc-spt-idle-translate"` — Rust stdin→stdout JSON-lines filter (`tools/cc-spt-idle-translate`).
- Input: `{"type":"init",…}` (no output) · `{"type":"event","envelope":"<EVENT…>"}` · `{"type":"input"}` (no output).
- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"delay_ms":50}` · `{"key":"ctrl+s"}`
  (stash draft → submit message → restore draft, so an inbound message never eats a half-typed draft).
- Idle-only; busy/mid-turn delivery stays the `[inject]` hook path. spt-core spawns on endpoint-up,
  reaps on down, applies keystrokes atomically (coexists with a live `spt rc` controller).

## Status — DONE (impl + unit)
- ✅ `tools/cc-spt-idle-translate` (Rust, 9 cargo tests green)
- ✅ `[session.resume]` + `[message-idle-translation-binary]` in `adapter/claude-spt.toml`
- ✅ `tests/manifest-shortcut.sh` (resume shape + 3rd skip-perms + idle path); `tests/adapter-archive.sh` (3rd binary at root)
- ✅ `ci/idle-translate/build.sh` wired into `ci/run-gates.sh`; packer packs the 3rd binary
- ✅ traceable-reqs: REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE (`["impl","unit"]`); REQ-HAZARD-PSYCHE-PERMS-DEADLOCK extended to resume

## Deferred — `int` (GATED on v0.13.0 daemon — doyle pings on ship)
- Native-resume launch: real `--resume` reloads the transcript + RC drive → add `int` to REQ-DIST-SESSION-RESUME.
- Idle-translation lifecycle: spawn-on-up / reap-on-down + atomic PTY apply → add `int` to REQ-DIST-IDLE-TRANSLATE.
- `doc` for both: docs-site harness-contract RUN-group slice (documented in manifest/module comments meanwhile).

## Gate
`sh ci/run-gates.sh` green + `traceable-reqs check` green.


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

# V13-ADAPTER-PLAN — build the two v0.13.0 harness-contract seams ahead of the binary

> **Scope.** spt-core's v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
> harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Build + structure
> the two new RUN-group adapter seams against the contract NOW; defer the runtime dogfood (`int`) to
> the v0.13.0 daemon. Public-surface-only (AGENTS.md): the contract pages are the authority.

## Seams

### 1. `[session.resume]` — native-resume (REQ-DIST-SESSION-RESUME)
- CC verb: `claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`,
  keys `["session_id","id"]`. Both flags verified real (`claude --help`: `-r/--resume`,
  `--remote-control [name]`).
- `{session_id}` reloads the real transcript; `{id}` = the remote-control session name (RC is the
  channel native-resume drives — same channel `spt rc` + the idle-translation-binary use). Omitting
  the role → silent fallback to `[session.self]` = blank fresh session (the documented footgun).
- PTY lands in the resumed session's recorded project cwd.

### 2. `[message-idle-translation-binary]` — idle PTY delivery (REQ-DIST-IDLE-TRANSLATE)
- `path = "cc-spt-idle-translate"` — Rust stdin→stdout JSON-lines filter (`tools/cc-spt-idle-translate`).
- Input: `{"type":"init",…}` (no output) · `{"type":"event","envelope":"<EVENT…>"}` · `{"type":"input"}` (no output).
- Output per event = the **operator choreography**:
  `{"key":"ctrl+s"}` · `{"delay_ms":50}` · `{"text":"<envelope>\r"}` · `{"delay_ms":50}` · `{"key":"ctrl+s"}`
  (stash draft → submit message → restore draft, so an inbound message never eats a half-typed draft).
- Idle-only; busy/mid-turn delivery stays the `[inject]` hook path. spt-core spawns on endpoint-up,
  reaps on down, applies keystrokes atomically (coexists with a live `spt rc` controller).

## Status — DONE (impl + unit)
- ✅ `tools/cc-spt-idle-translate` (Rust, 9 cargo tests green)
- ✅ `[session.resume]` + `[message-idle-translation-binary]` in `adapter/claude-spt.toml`
- ✅ `tests/manifest-shortcut.sh` (resume shape + 3rd skip-perms + idle path); `tests/adapter-archive.sh` (3rd binary at root)
- ✅ `ci/idle-translate/build.sh` wired into `ci/run-gates.sh`; packer packs the 3rd binary
- ✅ traceable-reqs: REQ-DIST-SESSION-RESUME + REQ-DIST-IDLE-TRANSLATE (`["impl","unit"]`); REQ-HAZARD-PSYCHE-PERMS-DEADLOCK extended to resume

## Deferred — `int` (GATED on v0.13.0 daemon — doyle pings on ship)
- Native-resume launch: real `--resume` reloads the transcript + RC drive → add `int` to REQ-DIST-SESSION-RESUME.
- Idle-translation lifecycle: spawn-on-up / reap-on-down + atomic PTY apply → add `int` to REQ-DIST-IDLE-TRANSLATE.
- `doc` for both: docs-site harness-contract RUN-group slice (documented in manifest/module comments meanwhile).

## Gate
`sh ci/run-gates.sh` green + `traceable-reqs check` green.


Now the full gate run.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/run-gates.sh 2>&1 | tail -45; echo "GATES_EXIT=${PIPESTATUS[0]:-$?}"

Run full CI gates


       SessionStart injects agent-facing briefs as additionalContext: an identity brief (who + perch-live/don't-re-arm + send/reply/roster) for perched sessions (bind+boundary), and a peer-gated ring brief (spt ring + roster) for no-perch seed sessions; all prose is adapter-string-backed ([strings.briefs]), the hook only composes + {id}-substitutes, never authors prose; subagent (agent_type) sessions are skipped
  [OK] REQ-DIST-SHORTCUT-BASENAME  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       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)
  [OK] REQ-DOCS-DRIFT  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       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
  [OK] REQ-DOCS-SITE  required: [doc, impl]  stages: +doc +impl -unit -int
       docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)
  [OK] REQ-HAZARD-MSYS-PATHCONV  required: [unit]  stages: -doc -impl +unit -int
       Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)
  [OK] REQ-HAZARD-PERCH-COLLISION  required: [unit]  stages: +doc +impl +unit +int
       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)
  [OK] REQ-HAZARD-PSYCHE-PERMS-DEADLOCK  required: [doc, unit]  stages: +doc -impl +unit -int
       Every CC process spt-core spawns NON-INTERACTIVELY carries --dangerously-skip-permissions: each claude-spt-psyche turn (seed + every pulse, detached/null-stdin), both [session.self] bringup commands (base claude + ccs profile, broker PTY), AND [session.resume] (native-resume into the broker PTY). An interactive permission prompt with no operator deadlocks the turn (silently, for the detached Psyche). The Psyche additionally runs inside a Read/Edit/Write tool sandbox so auto-approve is bounded.
  [OK] REQ-PARITY-AUDIT  required: [doc]  stages: +doc -impl -unit -int
       The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on
  [OK] REQ-SETUP-ACTIVATE  required: [doc, int]  stages: +doc -impl -unit +int
       /sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.
  [OK] REQ-SETUP-CCS  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).
  [OK] REQ-SETUP-SUBNET  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       /sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)
  [OK] REQ-SKILL-SUBNET  required: [doc, int]  stages: +doc -impl -unit +int
       The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [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

Findings:
  [must] missing_stage REQ-DIST-SESSION-RESUME stage=impl — Requirement is missing implementation evidence

=== GATE: skeleton-validate (cplugs installability) ===
ok  skeleton installable

=== GATE: manifest-schema ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json

=== GATE: docs-drift ===
ok   mdbook build
ok   llms.txt in sync

=== RESULT: FAIL ===
GATES_EXIT=1


One finding: `REQ-DIST-SESSION-RESUME` missing impl evidence. My impl tag is in the TOML comment — verify it's there and well-formed (shortcut-basename's impl tag is also a TOML comment and resolves).


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -rn "REQ-DIST-SESSION-RESUME\|REQ-DIST-IDLE-TRANSLATE" adapter/ tools/ tests/ ci/ | grep -iE "impl->|unit->"

Find impl/unit evidence tags for new reqs


adapter/claude-spt.toml:184:# [impl->REQ-DIST-SESSION-RESUME]
adapter/claude-spt.toml:230:# lead the binary — still v0.12.x on spt-releases). [impl->REQ-DIST-IDLE-TRANSLATE]
tools/cc-spt-idle-translate/src/main.rs:35://! JSON fields are ignored (forward-compat with newer spt-core). [impl->REQ-DIST-IDLE-TRANSLATE]
tools/cc-spt-idle-translate/src/main.rs:48:/// submit. [impl->REQ-DIST-IDLE-TRANSLATE]
tests/manifest-shortcut.sh:38:# [unit->REQ-DIST-SESSION-RESUME]
tests/manifest-shortcut.sh:54:# [unit->REQ-DIST-IDLE-TRANSLATE]
ci/idle-translate/build.sh:7:# [impl->REQ-DIST-IDLE-TRANSLATE]


164	[session.self]
165	command = "claude --dangerously-skip-permissions"
166	keys = ["id"]
167
168	# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
169	# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
170	# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
171	# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
172	# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
173	# {session_id} + cwd.
174	#
175	# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
176	# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
177	# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
178	# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
179	# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
180	# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
181	# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
182	# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
183	# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
184	# [impl->REQ-DIST-SESSION-RESUME]
185	[session.resume]
186	command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
187	keys = ["session_id", "id"]
188
189	# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────

[session.self]
command = "claude --dangerously-skip-permissions"
keys = ["id"]

# ── [session.resume] — native-resume sibling of [session.self] (harness-contract v0.13.0+) ─────
# spt-core selects THIS over [session.self] when a bringup carries a prior session (`spt endpoint run
# --resume <session>` or the picker's "Resume from history") AND this role is declared. OMITTING it
# silently falls back to [session.self] → a fresh session with a BLANK transcript (the documented
# footgun). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by
# {session_id} + cwd.
#
# Command = Claude Code's NATIVE-RESUME verb: `claude -r {session_id}` (-r/--resume reloads the real
# transcript by id). Unlike [session.self] — which threads the endpoint {id} via $SPT_ENDPOINT_ID
# because bare `claude` has no id flag — resume threads {id} as the REMOTE-CONTROL name
# (`--remote-control {id}`, a real CC flag): native-resume drives the resumed PTY over Remote Control,
# the same channel a live `spt rc` controller / the [message-idle-translation-binary] use, so the
# endpoint id IS the RC session name. `--dangerously-skip-permissions` for the same non-interactive
# broker-PTY reason as [session.self] (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). keys = the two fills
# spt-core substitutes: {session_id} (the resumed id) + {id} (the endpoint). spt tokenizes on
# whitespace then fills (tokenize-then-fill, v0.8.2+), so each flag is a clean argv element.
# [impl->REQ-DIST-SESSION-RESUME]
[session.resume]
command = "claude -r {session_id} --remote-control {id} --dangerously-skip-permissions"
keys = ["session_id", "id"]

# ── [env.<VAR>] ────────────────────────────────────────────────────────────────────────────


Tag placement matches the working shortcut-basename pattern. Check the scanner's config — maybe `adapter/*.toml` impl tags need a path glob, or run verbose.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && sed -n '1,20p' traceable-reqs.toml && echo "===CONFIG===" && grep -niE "glob|path|scan|include|exclude|source|\.toml|comment" traceable-reqs.toml | head -20

Inspect scanner config block


# 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] -->
#
===CONFIG===
8:# milestone (NOT up front). The registry below is intentionally EMPTY (commented shapes only).
27:[scan]
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".
84:required_stages = ["doc", "impl", "unit"]  # doc: ADR-0001 (cc launcher brand, decoupled from the s/sptc/spt/ plugin-name succession). impl: the cc-launcher BRINGUP machinery — adapter [session.self] (spawns `claude`) + [env.SPT_ENDPOINT_ID] (broker injects the endpoint id) + the SessionStart bind branch (sptc_register_verb -> `api bind`, the spt-hosted self-register path). unit: tests/manifest-shortcut.sh (brand-value guard) + tests/hooks-parse.sh sptc_register_verb (bind/seed/boundary branch). int (real `spt endpoint run` picker emits cc-<id> + real-claude bringup-bind) deferred — the shortcut-emitting picker "lands in a later spt-core wave" (per `endpoint run --help`); the bringup-bind is now contract-unblocked (doyle confirmed 2026-06-15: bind is establishing/intrinsic-auth, --set-session-id only, no token) but still needs a real-claude acceptance spawn (heavier tier). adapter add accepting the [session.self]/[env] blocks is covered by registration-int.sh step 4d (REQ-DIST-MANIFEST-SCHEMA int).
89:required_stages = ["int"]  # doc (ADR-0001 cc-launcher) + impl (adapter [session.self] + [env.SPT_ENDPOINT_ID] + session-start.sh `bind` branch) + unit (tests/hooks-parse.sh sptc_register_verb bind) are owned by REQ-DIST-SHORTCUT-BASENAME; this REQ owns the now-unblocked real-claude bringup-bind INT (its sibling, the picker `cc-<id>` emission, stays deferred under REQ-DIST-SHORTCUT-BASENAME — a later spt-core wave). int: ci/launcher/bind-int.sh — real `spt endpoint run --adapter claude-spt --id <disp> --start` on spt >=0.11.0 → SessionStart fills SPT_ENDPOINT_ID + BINDS → owlery/<disp> bound perch on disk (info.json + ready) → `spt send <disp>` reaches it (SENT live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED accepted) → full teardown. Was F-013-blocked (endpoint run did not substitute [env].value="{id}" → seed-not-bind → zero perch = the operator's wall-b NO_PERCH); doyle's v0.11.0 REQ-HAZARD-ENV-SUBST fix unblocked it. GREEN 2026-06-17 (disposable id, REQ-HAZARD-PERCH-COLLISION; box left clean). Self-gates SKIP on spt <0.11.0 / no SPTC_ACCEPTANCE. NB the spt-hosted bind path requires spt >=0.11.0 to bind (silently seeds on older) — manifest min_spt_core stays 0.9.0 for the ready/live flows; 0.11.0-floor-bump for the endpoint-run path is a flagged scope question, not yet taken.
94:required_stages = ["impl", "unit", "int"]  # impl: tools/claude-spt-digest (Rust — CC JSONL -> digest NDJSON; per-session locate of <slug>/<session_id>.jsonl under the projects root via the {session_id} the command receives; native UTF-8 stdout; defensive ~ expansion) + ci/digest/build.sh gate. unit: the crate's cargo tests (9: input/agent/tool mapping, arg-key preference, thinking+tool_result+noise+bad-json drops, UTF-8 round-trip, contract invariants, tilde expansion) run by ci/digest/build.sh. int: ci/digest/digest-proof-int.sh — `spt adapter digest-proof claude-spt --sample` on live v0.7.2 ran the published-shape extractor command (`--session {session_id} --in {source}`) and rendered DIGEST_PROOF_OK (5 parsed / 0 dropped). GREEN 2026-06-15 (registry left clean via soft-remove). Was F-004-blocked (digest-proof --sample passed an empty substitution-key map, cli.rs:5135) — doyle's spt-core PR#14 fix (v0.7.2, release counter 14) now fills {id}+{session_id} + an optional --session, matching runtime. doc deferred (the mapping is documented in the module + manifest [digest] comment; a docs-site section lands with the doc slice).
99:required_stages = ["doc", "impl", "unit", "int"]  # doc: adapter/strings/skills/live.md (bare-listen bringup + poll/relay reconcile + Psyche-is-daemon-managed; Option A, no :live profile). impl: [session.psyche_init] in the BASE adapter/claude-spt.toml (the LiveAgent seam, 4 daemon keys, base-not-overlay) + min_spt_core_version=0.9.0 + the bare SessionStart seed (session-start.sh) + tools/claude-spt-psyche (the resident runner). unit: claude-spt-psyche cargo tests (arg parse, seed/pulse/poll command construction, blank-pulse guard) run by ci/psyche/build.sh. int: ci/psyche/live-relay-int.sh — plays CC's Monitor role (spawns a persistent CHILD `spt api ... listen <id>`, NOT --once) under a disposable id and asserts on real spt >=0.9.0: PSYCHE-SPAWN (the daemon livehost hosts the Psyche off the perch's live_agent online status — resident claude-spt-psyche runner + nested {id}-psyche perch dir; livehost.rs:282 gates on live_agent state) + RELAY (BOUND/READY + a probe relayed as the escaped <EVENT> envelope + live_agent kind). PSYCHE hosts off the BASE manifest (Option A — psyche_init in base; the live listen path stamps live_agent which actualizes it, poll stays ready). Gotchas baked in (doyle): anchor on the WINDOWS pid not git-bash `$$` (else STALE_SEED); `bind` before `send` (else NO_PERCH, no queue). The psyche binary resolves by bare name from the adapter install dir (REQ-INSTALL-11, Feature B v0.8.0+). Bare-flow (no --adapter; host_binaries resolution) is the spt-core v0.9.0 legacy-parity path — F-007 (how-to redirect) retired, the body is self-contained.
109:required_stages = ["doc", "impl", "unit"]  # doc: docs-site harness-contract §SessionStart briefs. impl: session-start.sh emit-block + _common.sh (sptc_brief/assemble_perch/assemble_noperch/perch_brief/noperch_brief/is_subagent/has_peers_lines/node_has_peers/json_escape/emit_additional_context) + adapter/strings/briefs/*.md + [strings.briefs] in claude-spt.toml. unit: tests/hooks-parse.sh (assemble structure + {id} global subst + is_subagent + peer-gate line-count + json_escape + emit shape/empty). int PENDING: committed brief round-trip script (get-string file-backed resolve -> compose -> valid JSON) — manually proven 2026-06-19 via a renamed local probe adapter (probe-sptc), add "int" when the script lands + the published claude-spt adapter carries [strings.briefs]. NOTE: live-vs-ready flavor DEFERRED (public liveness-query gap, logged w/ doyle, post-v0.13.0 spt whoami --json).
137:title = "The spt messaging (bus) binary is located robustly at run time (per-version plugins path / PATH / configured) — never a hard-coded versioned path"
151:id = "REQ-HAZARD-MSYS-PATHCONV"
152:title = "Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)"
166:required_stages = ["doc", "impl"]  # doc: DOCS-STRATEGY. impl: docs-site/book.toml + src/ pages.
192:# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml
203:# ── Milestone: SETUP-SLICE (the /sptc:setup activation bridge + LOCKED v1 setup paths) — minted
206:# REQ-SETUP-ACTIVATE activates to ["doc","impl","int"] (Wave 1); the {1..7} feature paths get their
212:title = "/sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave."
217:title = "/sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected)."
218:required_stages = ["doc"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the ccs-wiring step (detect ~/.ccs; present -> reference the :ccs profile + verify ccs on PATH; absent -> offer install w/ value prop). Doc-class instruction prose, no adapter-side code of its own (same model as REQ-SETUP-ACTIVATE/REQ-SKILL-SUBNET). The :ccs profile's existence + leaf-replace resolution is covered separately by REQ-CCS-PROFILES (registration-int.sh); this REQ is only the SETUP-TIME detection/offer step. Public-surface-complete: `~/.ccs` detection + the existing :ccs profile, no unpublished dep.
223:required_stages = ["doc", "int"]  # doc: adapter/strings/skills/setup.md + plugin/sptc/skills/setup/SKILL.md — the activation step (probe `adapter list`; if missing/deregistered, `adapter add` local-file-form for dev | `--release` for end-user; verify active; PATH-binary caveat). A setup skill body is doc-class instruction prose (no adapter-side code of its own — same model as REQ-SKILL-SUBNET). int: ci/setup/activate-int.sh — the deregistered->active RE-ACTIVATE assertion (local file-form) + profiles/strings go live (slow lane, SPTC_ACCEPTANCE=1, spt>=0.7.0); dogfood-proven on spt 0.7.3. The END-USER `--release` acquisition leg is proven separately by REQ-DIST-ADAPTER-RELEASE int (ci/publish/release-acquire-int.sh). Acquisition RESOLVED (doyle owl 2026-06-15): monorepo file-form for dev, `adapter add --release SaberMage/spt-claude-code` (adapter.spt release asset) for end-user — no dedicated repo. See SETUP-SLICE-PLAN.md + REQ-DIST-ADAPTER-RELEASE.
227:title = "The adapter ships to end users as an `adapter.spt` GitHub release asset (tar ROOT = manifest.toml + strings/ + the tool binaries), acquired via `spt adapter add --release SaberMage/spt-claude-code` — distribution straight from the monorepo, no dedicated repo (doyle's --release acquisition source, spt v0.7.3/counter-15)"


Let me read the `[scan]` config block to see which file extensions/paths are scanned for evidence tags.


27	[scan]
28	# Every evidence location. A missing root makes evidence SILENTLY vanish from the trace —
29	# audit this whenever a new evidence dir appears. ADD `plugin`, `skills`, `hooks`, `tests`
30	# the moment those dirs are created (skeleton milestone, body #2) — until they exist a
31	# nonexistent root is a hard manifest_error, so only list what is on disk now.
32	roots = ["plugin", "ci", "ci/acceptance", "ci/publish", "ci/docs", "ci/manifest", "ci/digest", "adapter", "tools", "tests", "docs", "docs-site", "CONTEXT.md", "SCOPE.md", "AGENTS.md", "HANDOFF.md"]
33
34	[policy]
35	# Default policy applied at activation: doc + impl + unit. Networking/lifecycle/cross-node
36	# reqs add "int". Until a req is activated, its per-req `required_stages = []` keeps it out
37	# of the coverage gate.
38	required_stages = []
39
40	# ───────────────────────────── Requirements ─────────────────────────────────
41	# Add requirements as you scope milestones, seeded from SCOPE.md "Parity-trim". Shape:
42	#
43	# [[requirements]]
44	# id = "REQ-EXAMPLE-1"
45	# title = "One-line statement of the requirement"
46	# required_stages = []  # activate (e.g. ["doc","impl","unit"]) when the milestone starts
47	#
48	# KNOWN-HAZARDS invariants are first-class requirements — one REQ-HAZARD-* per invariant,
49	# each requiring at least "unit" (add "int" where cross-process/cross-node):
50	#
51	# [[requirements]]
52	# id = "REQ-HAZARD-EXAMPLE"
53	# title = "The invariant, stated so a test can prove it"
54	# required_stages = []
55
56	# ── Milestone: SKELETON (thin cplugs plugin) — minted at kickoff, per-milestone (not

[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", "ci/digest", "adapter", "tools", "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


`[scan]` is shallow per-root (note: `ci/digest`, `ci/manifest` listed separately despite `ci` being a root). My `ci/idle-translate/build.sh` impl tag isn't scanned — but idle-translate's impl is covered via `tools/`. The resume tag is in `adapter/` (a root), so it *should* resolve. Let me query the scanner directly.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs --help 2>&1 | head -30

traceable-reqs CLI help


Trace requirements via [stage->REQ-ID] tags

Usage: traceable-reqs.exe <COMMAND>

Commands:
  list
  trace
  check
  lint
  review
  help    Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version


cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs trace REQ-DIST-SESSION-RESUME 2>&1 | head -30; echo "---SHORTCUT(works)---"; traceable-reqs trace REQ-DIST-SHORTCUT-BASENAME 2>&1 | head -30

Trace evidence for resume vs working shortcut req


REQ-DIST-SESSION-RESUME — The adapter manifest declares [session.resume] — Claude Code's NATIVE-RESUME verb (`claude -r {session_id} --remote-control {id} --dangerously-skip-permissions`, keys=["session_id","id"]) — so a spt-hosted resume (`spt endpoint run --resume` / picker Resume-from-history) reloads the REAL transcript by id instead of silently re-running [session.self] = a fresh blank session. {session_id} reloads the transcript (-r); {id} threads the endpoint as the remote-control session name (the RC channel native-resume drives, the same one `spt rc` / the idle-translation-binary use); skip-permissions for the non-interactive broker PTY (REQ-HAZARD-PSYCHE-PERMS-DEADLOCK). The PTY lands in the resumed session's recorded project cwd; CC resolves the transcript by {session_id}+cwd.
  required stages: [impl, unit]
  [--] doc (not required)
  [--] impl
  [OK] unit
        tests/manifest-shortcut.sh:38
  [--] int (not required)
  findings:
    [must] missing_stage REQ-DIST-SESSION-RESUME stage=impl — Requirement is missing implementation evidence
---SHORTCUT(works)---
REQ-DIST-SHORTCUT-BASENAME — 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, impl, unit]
  [OK] doc
        docs/adr/0001-distribution-splits-by-volatility.md:103
  [OK] impl
        plugin/sptc/hooks/_common.sh:32
  [OK] unit
        tests/hooks-parse.sh:59
        tests/manifest-shortcut.sh:7
  [--] int (not required)


Revealing — shortcut-basename's impl resolves from `plugin/sptc/hooks/_common.sh`, **not** the `.toml` comment. The scanner likely skips `.toml`. Confirm whether any req draws evidence from `claude-spt.toml`.