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)

#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024
# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the
# archive ROOT, plus each recognized target-triple's binaries under a <triple>/ dir mirroring the
# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into
# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]
# [unit->REQ-DIST-ADAPTER-PEROS]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

# The fat archive needs BOTH platforms' release binaries. Probe both triples.
have_all=1
for b in $BINS; do
  [ -f "$ROOT/tools/$b/target/release/$b.exe" ] || have_all=0
  [ -f "$ROOT/tools/$b/target/$LINUX_TRIPLE/release/$b" ] || have_all=0
done

if [ "$have_all" -ne 1 ]; then
  # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the
  # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.
  if sh "$PACKER" --apply >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when a platform's binaries are absent"
  else
    echo "ok   packer refuses --apply when a platform's binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present"
  exit "$rc"
fi

# Both platforms present → build a real fat 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")
# SHARED manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   shared manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# SHARED strings/ at root (not under a triple).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   shared strings/ at root" \
  || fail "strings/ missing from archive root"
# Each triple carries all three binaries, under its <triple>/ dir, mirroring the flat-root tree.
for b in $BINS; do
  echo "$LIST" | grep -qx "$WIN_TRIPLE/$b.exe" \
    && echo "ok   $WIN_TRIPLE/$b.exe present" \
    || fail "$WIN_TRIPLE/$b.exe missing from archive"
  echo "$LIST" | grep -qx "$LINUX_TRIPLE/$b" \
    && echo "ok   $LINUX_TRIPLE/$b present" \
    || fail "$LINUX_TRIPLE/$b missing from archive"
done
# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.
if echo "$LIST" | grep -qE "^$WIN_TRIPLE/(manifest\.toml|strings/)"; then
  fail "manifest/strings duplicated under a triple — they must be shared at root only"
else
  echo "ok   manifest/strings not duplicated under a triple"
fi
# Negative: no UNRECOGNIZED top-level dir (spt-core would silently flatten it as a shared-root entry).
badtop=$(echo "$LIST" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
if [ -n "$badtop" ]; then
  fail "unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat (silent footgun)"
else
  echo "ok   no stray/unrecognized top-level dirs (only strings/ + the two recognized triples)"
fi

# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.
# (Smoke the guard by reading the dry-run plan's advertised floor.)
plan=$(sh "$PACKER" 2>&1)
echo "$plan" | grep -qE "min_spt_core 0\.(1[3-9]|[2-9][0-9])" \
  && echo "ok   packer advertises a fat-capable floor (>= 0.13.2)" \
  || fail "packer floor advertisement missing/too low; plan=[$plan]"

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

+BINS="claude-spt cc-spt-idle-translate"  # consolidated claude-spt (digest/psyche/post-update) + the separate idle-translate (ADR-0006/U2)


Edit

# Each triple carries all three binaries, under its <triple>/ dir, mirroring the flat-root tree.

# Each triple carries all the tool binaries, under its <triple>/ dir, mirroring the flat-root tree.


#!/bin/sh
# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024
# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the
# archive ROOT, plus each recognized target-triple's binaries under a <triple>/ dir mirroring the
# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into
# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]
# [unit->REQ-DIST-ADAPTER-PEROS]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
PACKER="$ROOT/ci/publish/package-adapter.sh"
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
BINS="claude-spt cc-spt-idle-translate"  # consolidated claude-spt (digest/psyche/post-update) + the separate idle-translate (ADR-0006/U2)
rc=0
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

# The fat archive needs BOTH platforms' release binaries. Probe both triples.
have_all=1
for b in $BINS; do
  [ -f "$ROOT/tools/$b/target/release/$b.exe" ] || have_all=0
  [ -f "$ROOT/tools/$b/target/$LINUX_TRIPLE/release/$b" ] || have_all=0
done

if [ "$have_all" -ne 1 ]; then
  # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the
  # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.
  if sh "$PACKER" --apply >/dev/null 2>&1; then
    fail "packer should refuse (exit!=0) when a platform's binaries are absent"
  else
    echo "ok   packer refuses --apply when a platform's binaries absent (guard works)"
  fi
  echo "SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present"
  exit "$rc"
fi

# Both platforms present → build a real fat 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")
# SHARED manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
echo "$LIST" | grep -qx "manifest.toml" \
  && echo "ok   shared manifest.toml at archive root" \
  || fail "manifest.toml not at archive root (add --release would reject)"
# SHARED strings/ at root (not under a triple).
echo "$LIST" | grep -q "^strings/" \
  && echo "ok   shared strings/ at root" \
  || fail "strings/ missing from archive root"
# Each triple carries all three binaries, under its <triple>/ dir, mirroring the flat-root tree.
for b in $BINS; do
  echo "$LIST" | grep -qx "$WIN_TRIPLE/$b.exe" \
    && echo "ok   $WIN_TRIPLE/$b.exe present" \
    || fail "$WIN_TRIPLE/$b.exe missing from archive"
  echo "$LIST" | grep -qx "$LINUX_TRIPLE/$b" \
    && echo "ok   $LINUX_TRIPLE/$b present" \
    || fail "$LINUX_TRIPLE/$b missing from archive"
done
# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.
if echo "$LIST" | grep -qE "^$WIN_TRIPLE/(manifest\.toml|strings/)"; then
  fail "manifest/strings duplicated under a triple — they must be shared at root only"
else
  echo "ok   manifest/strings not duplicated under a triple"
fi
# Negative: no UNRECOGNIZED top-level dir (spt-core would silently flatten it as a shared-root entry).
badtop=$(echo "$LIST" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
if [ -n "$badtop" ]; then
  fail "unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat (silent footgun)"
else
  echo "ok   no stray/unrecognized top-level dirs (only strings/ + the two recognized triples)"
fi

# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.
# (Smoke the guard by reading the dry-run plan's advertised floor.)
plan=$(sh "$PACKER" 2>&1)
echo "$plan" | grep -qE "min_spt_core 0\.(1[3-9]|[2-9][0-9])" \
  && echo "ok   packer advertises a fat-capable floor (>= 0.13.2)" \
  || fail "packer floor advertisement missing/too low; plan=[$plan]"

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

&& echo "ok   shared strings/ at root" \

-# Each triple carries all three binaries, under its <triple>/ dir, mirroring the flat-root tree.

+# Each triple carries all the tool binaries, under its <triple>/ dir, mirroring the flat-root tree.


Now the digest-proof-int. Read it:


1	#!/bin/sh
2	# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor and renders a
3	# non-empty digest from a real CC-shaped sample (the M10 acceptance criterion carried
4	# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
5	#
6	# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
7	# straight from its build dir against the bare-file manifest: --dir resolves the binary (before PATH)
8	# exactly as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior
9	# form `adapter add`/`adapter remove claude-spt` soft-removed the REAL registered claude-spt on
10	# cleanup, AND the bare-file add went stale on spt 0.13.x (a gh_release manifest registers
11	# GhReleaseManaged but unextracted -> F-011 manifest-not-present -> digest-proof failed). Read-only
12	# now, so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.13.2 (the --dir/--manifest options) + a built
13	# extractor. Idempotent. Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).
14	#
15	# GREEN since spt v0.7.2 (doyle's F-004 fix): digest-proof fills the runtime substitution keys
16	# ({id}+{session_id}, + optional --session) matching the daemon, so the published-shape extractor
17	# command (`--session {session_id} --in {source}`) proofs instead of hard-failing {session_id}.
18	set -u
19	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
20	MANIFEST="$ROOT/adapter/claude-spt.toml"
21	SAMPLE="$ROOT/ci/digest/sample.jsonl"
22	RELDIR="$ROOT/tools/claude-spt-digest/target/release"
23
24	command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
25
26	# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
27	if ! spt adapter digest-proof --help 2>&1 | grep -q -- '--dir'; then
28	  echo "SKIP: spt 'adapter digest-proof' has no --dir/--manifest (needs v0.13.2). Extractor itself: cargo tests green."
29	  exit 0
30	fi
31
32	[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }
33
34	# Proof the dev extractor in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
35	# pins the bare-file gh_release manifest — no extracted install, no registry touch.
36	out=$(spt adapter digest-proof claude-spt --sample "$SAMPLE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
37	case "$out" in
38	  *DIGEST_PROOF_OK*)
39	    echo "ok  digest-proof: DIGEST_PROOF_OK"
40	    printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/    /'
41	    echo "DIGEST-PROOF-INT OK"; exit 0 ;;
42	  *"no value for substitution key {session_id}"*)
43	    echo "SKIP: spt < v0.7.2 — digest-proof --sample doesn't fill {session_id} (pre-F-004-fix binary)."
44	    echo "      upgrade to spt >= 0.7.2 (release counter 14) to run this int. Extractor itself: cargo tests green."
45	    exit 0 ;;
46	  *)
47	    echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
48	esac
49

#!/bin/sh
# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor and renders a
# non-empty digest from a real CC-shaped sample (the M10 acceptance criterion carried
# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
# straight from its build dir against the bare-file manifest: --dir resolves the binary (before PATH)
# exactly as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior
# form `adapter add`/`adapter remove claude-spt` soft-removed the REAL registered claude-spt on
# cleanup, AND the bare-file add went stale on spt 0.13.x (a gh_release manifest registers
# GhReleaseManaged but unextracted -> F-011 manifest-not-present -> digest-proof failed). Read-only
# now, so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.13.2 (the --dir/--manifest options) + a built
# extractor. Idempotent. Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).
#
# GREEN since spt v0.7.2 (doyle's F-004 fix): digest-proof fills the runtime substitution keys
# ({id}+{session_id}, + optional --session) matching the daemon, so the published-shape extractor
# command (`--session {session_id} --in {source}`) proofs instead of hard-failing {session_id}.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
SAMPLE="$ROOT/ci/digest/sample.jsonl"
RELDIR="$ROOT/tools/claude-spt-digest/target/release"

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter digest-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter digest-proof' has no --dir/--manifest (needs v0.13.2). Extractor itself: cargo tests green."
  exit 0
fi

[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

# Proof the dev extractor in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter digest-proof claude-spt --sample "$SAMPLE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$out" in
  *DIGEST_PROOF_OK*)
    echo "ok  digest-proof: DIGEST_PROOF_OK"
    printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/    /'
    echo "DIGEST-PROOF-INT OK"; exit 0 ;;
  *"no value for substitution key {session_id}"*)
    echo "SKIP: spt < v0.7.2 — digest-proof --sample doesn't fill {session_id} (pre-F-004-fix binary)."
    echo "      upgrade to spt >= 0.7.2 (release counter 14) to run this int. Extractor itself: cargo tests green."
    exit 0 ;;
  *)
    echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
esac


RELDIR="$ROOT/tools/claude-spt/target/release"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)


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

#!/bin/sh
# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor and renders a
# non-empty digest from a real CC-shaped sample (the M10 acceptance criterion carried
# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
# straight from its build dir against the bare-file manifest: --dir resolves the binary (before PATH)
# exactly as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior
# form `adapter add`/`adapter remove claude-spt` soft-removed the REAL registered claude-spt on
# cleanup, AND the bare-file add went stale on spt 0.13.x (a gh_release manifest registers
# GhReleaseManaged but unextracted -> F-011 manifest-not-present -> digest-proof failed). Read-only
# now, so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.13.2 (the --dir/--manifest options) + a built
# extractor. Idempotent. Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).
#
# GREEN since spt v0.7.2 (doyle's F-004 fix): digest-proof fills the runtime substitution keys
# ({id}+{session_id}, + optional --session) matching the daemon, so the published-shape extractor
# command (`--session {session_id} --in {source}`) proofs instead of hard-failing {session_id}.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
SAMPLE="$ROOT/ci/digest/sample.jsonl"
RELDIR="$ROOT/tools/claude-spt-digest/target/release"

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter digest-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter digest-proof' has no --dir/--manifest (needs v0.13.2). Extractor itself: cargo tests green."
  exit 0
fi

[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

# Proof the dev extractor in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter digest-proof claude-spt --sample "$SAMPLE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$out" in
  *DIGEST_PROOF_OK*)
    echo "ok  digest-proof: DIGEST_PROOF_OK"
    printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/    /'
    echo "DIGEST-PROOF-INT OK"; exit 0 ;;
  *"no value for substitution key {session_id}"*)
    echo "SKIP: spt < v0.7.2 — digest-proof --sample doesn't fill {session_id} (pre-F-004-fix binary)."
    echo "      upgrade to spt >= 0.7.2 (release counter 14) to run this int. Extractor itself: cargo tests green."
    exit 0 ;;
  *)
    echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
esac

+RELDIR="$ROOT/tools/claude-spt/target/release"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)


[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

[ -x "$RELDIR/claude-spt" ] || [ -x "$RELDIR/claude-spt.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }


#!/bin/sh
# Integration proof: `spt adapter digest-proof` runs the claude-spt [digest] extractor and renders a
# non-empty digest from a real CC-shaped sample (the M10 acceptance criterion carried
# forward for spt-claude-code). [int->REQ-DIST-DIGEST-EXTRACTOR]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV extractor
# straight from its build dir against the bare-file manifest: --dir resolves the binary (before PATH)
# exactly as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior
# form `adapter add`/`adapter remove claude-spt` soft-removed the REAL registered claude-spt on
# cleanup, AND the bare-file add went stale on spt 0.13.x (a gh_release manifest registers
# GhReleaseManaged but unextracted -> F-011 manifest-not-present -> digest-proof failed). Read-only
# now, so NO SPTC_ACCEPTANCE gate. Needs spt >= 0.13.2 (the --dir/--manifest options) + a built
# extractor. Idempotent. Run: sh ci/digest/digest-proof-int.sh   (exit 0 = pass).
#
# GREEN since spt v0.7.2 (doyle's F-004 fix): digest-proof fills the runtime substitution keys
# ({id}+{session_id}, + optional --session) matching the daemon, so the published-shape extractor
# command (`--session {session_id} --in {source}`) proofs instead of hard-failing {session_id}.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
SAMPLE="$ROOT/ci/digest/sample.jsonl"
RELDIR="$ROOT/tools/claude-spt/target/release"   # consolidated binary; the `digest` subcommand is the extractor (ADR-0006/U2)

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# --dir/--manifest landed in spt v0.13.2 — capability-detect rather than version-parse.
if ! spt adapter digest-proof --help 2>&1 | grep -q -- '--dir'; then
  echo "SKIP: spt 'adapter digest-proof' has no --dir/--manifest (needs v0.13.2). Extractor itself: cargo tests green."
  exit 0
fi

[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

# Proof the dev extractor in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter digest-proof claude-spt --sample "$SAMPLE" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$out" in
  *DIGEST_PROOF_OK*)
    echo "ok  digest-proof: DIGEST_PROOF_OK"
    printf '%s\n' "$out" | grep -E 'parsed|dropped' | sed 's/^/    /'
    echo "DIGEST-PROOF-INT OK"; exit 0 ;;
  *"no value for substitution key {session_id}"*)
    echo "SKIP: spt < v0.7.2 — digest-proof --sample doesn't fill {session_id} (pre-F-004-fix binary)."
    echo "      upgrade to spt >= 0.7.2 (release counter 14) to run this int. Extractor itself: cargo tests green."
    exit 0 ;;
  *)
    echo "FAIL: unexpected digest-proof output:"; printf '%s\n' "$out"; exit 1 ;;
esac

-[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }

+[ -x "$RELDIR/claude-spt" ] || [ -x "$RELDIR/claude-spt.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }


Now the live-int scripts match the psyche process by exe name (now `claude-spt.exe` running the `psyche` subcommand). These are SPTC_ACCEPTANCE-gated but must stay correct. Read them to update:


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

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

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a Psyche + mutates perch state)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
case "$ver" in
  0.7.2|0.7.1|0.7.0|0.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.3 (daemon live path / counter-15)"; exit 0 ;;
esac
[ -f "$MAN" ] || { echo "SKIP: no manifest at $MAN"; exit 0; }

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

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

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

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

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

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

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

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

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

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


command -v claude-spt-psyche >/dev/null 2>&1 && return 0

command -v claude-spt >/dev/null 2>&1 && return 0


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

# Can the psyche runner be resolved on THIS host? (PATH now, or manifest-dir on v0.8.0+ Feature B.)

-  command -v claude-spt-psyche >/dev/null 2>&1 && return 0

+  command -v claude-spt >/dev/null 2>&1 && return 0

case "$ver" in 0.7.*) return 1 ;; *) return 0 ;; esac  # Feature B (manifest-dir resolution) lands in v0.8.0; >=0.8.0 resolvable


for p in $(tasklist 2>/dev/null | grep -i claude-spt-psyche | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done

for p in $(tasklist 2>/dev/null | grep -i claude-spt | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done


# Kill the spawned Psyche subtree first (the runner + its headless claude), by marker pid then name.

pp=$(grep -oE 'PSYCHE_SPAWNED:[^ ]+ pid=[0-9]+' "$LF" 2>/dev/null | grep -oE '[0-9]+' | tail -1)

[ -n "$pp" ] && taskkill //PID "$pp" //T //F >/dev/null 2>&1

-  for p in $(tasklist 2>/dev/null | grep -i claude-spt-psyche | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done

+  for p in $(tasklist 2>/dev/null | grep -i claude-spt | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done

[ -n "$CHILD" ] && kill "$CHILD" >/dev/null 2>&1

spt endpoint shutdown "$ID" >/dev/null 2>&1 || true

spt endpoint stop "$ID" >/dev/null 2>&1 || true


resident() { tasklist 2>/dev/null | grep -qi "claude-spt-psyche"; }

    # The runner exe is now claude-spt.exe (running the `psyche` subcommand; ADR-0006/U2). In this
    # disposable CI session the only claude-spt process is the psyche runner, so the image-name match
    # is unambiguous (tasklist shows the image name, not the subcommand).
    resident() { tasklist 2>/dev/null | grep -qi "claude-spt"; }


    # The runner exe is now claude-spt.exe (running the `psyche` subcommand; ADR-0006/U2). In this
    # disposable CI session the only claude-spt process is the psyche runner, so the image-name match
    # is unambiguous (tasklist shows the image name, not the subcommand).
    resident() { tasklist 2>/dev/null | grep -qi "claude-spt"; }

# the RESIDENT runner process + the nested perch dir for THIS id. This is also the REQ-INSTALL-11

# install-dir-resolution proof: the runner resolved by bare name FROM the adapter install dir.

-    resident() { tasklist 2>/dev/null | grep -qi "claude-spt-psyche"; }

+    # The runner exe is now claude-spt.exe (running the `psyche` subcommand; ADR-0006/U2). In this

+    # disposable CI session the only claude-spt process is the psyche runner, so the image-name match

+    # is unambiguous (tasklist shows the image name, not the subcommand).

+    resident() { tasklist 2>/dev/null | grep -qi "claude-spt"; }

j=0; while [ "$j" -lt 20 ]; do resident && [ -f "$OWL" ] && break; sleep 1; j=$((j+1)); done

procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")


procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")

procs=$(tasklist 2>/dev/null | grep -ci "claude-spt")


# is unambiguous (tasklist shows the image name, not the subcommand).

resident() { tasklist 2>/dev/null | grep -qi "claude-spt"; }

-    procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")

+    procs=$(tasklist 2>/dev/null | grep -ci "claude-spt")

ok "Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)"


Now the two wmic process-name checks in bind-int + multi-subnet ints:


43	  # 1. Kill the broker-spawned claude (the [session.self] target) by its run pid, subtree.
44	  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
45	  # 2. Kill the disposable Psyche ONLY (cmdline carries this run's unique id — never wall-a's).
46	  for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
47	    taskkill //PID "$p" //T //F >/dev/null 2>&1
48	  done

  # 1. Kill the broker-spawned claude (the [session.self] target) by its run pid, subtree.
  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
  # 2. Kill the disposable Psyche ONLY (cmdline carries this run's unique id — never wall-a's).
  for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
    taskkill //PID "$p" //T //F >/dev/null 2>&1
  done


53	  for id in $MADE; do spt endpoint purge "$id" --yes --force >/dev/null 2>&1 || true; done
54	  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
55	  if [ -n "${C3_ID:-}" ]; then
56	    for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
57	      taskkill //PID "$p" //T //F >/dev/null 2>&1
58	    done

  for id in $MADE; do spt endpoint purge "$id" --yes --force >/dev/null 2>&1 || true; done
  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
  if [ -n "${C3_ID:-}" ]; then
    for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
      taskkill //PID "$p" //T //F >/dev/null 2>&1
    done


for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do

for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do


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

#!/bin/sh
# Integration proof for the spt-hosted cc-launcher BRINGUP-BIND path — the gap that hid the operator's
# wall-b zero-perch (F-013). Drives the operator's EXACT flow against a real spt-core:
#
#   spt endpoint run --adapter claude-spt --id <disp> --start
#       -> broker spawns [session.self] (`claude`) into a held PTY + injects [env.SPT_ENDPOINT_ID]={id}
#       -> CC's SessionStart hook reads a POPULATED $SPT_ENDPOINT_ID -> sptc_register_verb=`bind`
#       -> `spt api bind <id>` self-registers a BOUND perch on disk
#   spt send <disp>  ->  reaches it (SENT = live PTY inject, REQ-SEND-SPT-HOSTED; QUEUED also accepted)
#
# This is the int that WOULD HAVE CAUGHT the bug: pre-v0.11.0, `endpoint run` did NOT substitute
# [env.<VAR>].value="{id}" (injected EMPTY), so SessionStart fell to `seed`-by-PPID instead of `bind`
# -> ZERO perch -> `spt send` NO_PERCH. doyle's v0.11.0 REQ-HAZARD-ENV-SUBST fix populates it.
# [int->REQ-CC-LAUNCHER-BIND]
#
# Spawns a REAL claude (the [session.self] target) into a broker PTY + the daemon livehost spawns its
# Psyche (base manifest is live-capable) + mutates node-local perch state — ALL torn down on exit.
# Gated behind SPTC_ACCEPTANCE=1 and spt >=0.11.0 (older silently seeds-not-binds). Disposable, per-run
# unique id (REQ-HAZARD-PERCH-COLLISION — NEVER a live agent's id). Idempotent.
# Run: SPTC_ACCEPTANCE=1 sh ci/launcher/bind-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
A=claude-spt
OWLERY="${LOCALAPPDATA:-$HOME/AppData/Local}/spt-core/owlery"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a real claude + Psyche, mutates perch state)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
case "$ver" in
  0.0.*|0.1.*|0.2.*|0.3.*|0.4.*|0.5.*|0.6.*|0.7.*|0.8.*|0.9.*|0.10.*)
    echo "SKIP: spt $ver < 0.11.0 (endpoint-run [env].value substitution = REQ-HAZARD-ENV-SUBST/F-013, counter 24)"; exit 0 ;;
esac
spt adapter list 2>/dev/null | grep -q "claude-spt" || { echo "SKIP: claude-spt not registered (spt adapter add)"; exit 0; }

RUN=$$
ID=ccbind-int-$RUN
RUNPID=""
fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }

cleanup() {
  # 1. Kill the broker-spawned claude (the [session.self] target) by its run pid, subtree.
  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
  # 2. Kill the disposable Psyche ONLY (cmdline carries this run's unique id — never wall-a's).
  for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
    taskkill //PID "$p" //T //F >/dev/null 2>&1
  done
  # 3. Take the endpoint offline + erase its perch.
  spt endpoint shutdown "$ID" >/dev/null 2>&1 || true
  spt endpoint stop "$ID" >/dev/null 2>&1 || true
  tok=$(cat "$OWLERY/$ID/api.token" 2>/dev/null)
  [ -n "$tok" ] && spt api session-end "$ID" --erase --token "$tok" >/dev/null 2>&1 || true
  rm -rf "$OWLERY/$ID" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM

# Bring up the spt-hosted endpoint (returns immediately; the PTY session keeps running).
runout=$(spt endpoint run --adapter "$A" --id "$ID" --start 2>&1)
echo "$runout" | grep -q "ENDPOINT_RUN_STARTED:$ID" && ok "endpoint run started ($ID)" || bad "endpoint run did not start: [$runout]"
RUNPID=$(printf '%s' "$runout" | grep -oE 'pid=Some\([0-9]+\)' | grep -oE '[0-9]+' | head -1)

# Wait (bounded ~50s) for SessionStart to BIND the perch (the bug = this never appearing).
i=0; while [ "$i" -lt 25 ]; do [ -f "$OWLERY/$ID/info.json" ] && break; sleep 2; i=$((i+1)); done

# 1. The spt-hosted SessionStart bind produced a BOUND perch on disk (info.json + api.token + ready).
if [ -f "$OWLERY/$ID/info.json" ] && [ -f "$OWLERY/$ID/api.token" ]; then
  ok "SessionStart BOUND a perch on disk (owlery/$ID — the F-013 zero-perch is fixed)"
else
  bad "no bound perch at owlery/$ID after ~50s (SessionStart seeded-not-bound? SPT_ENDPOINT_ID empty?)"
fi

# 2. The bound perch is registered/online in this node's local roster.
spt endpoint list --local 2>/dev/null | grep -q "$ID" && ok "endpoint registered in local roster" || bad "$ID not in endpoint list --local"

# 3. The bound spt-hosted endpoint is REACHABLE — `spt send` reaches it (SENT = live PTY inject per
#    REQ-SEND-SPT-HOSTED; QUEUED = spooled, still reachable). NO_PERCH would mean no perch behind it.
sendout=$(printf 'ccbind-int reachability probe' | spt send "$ID" --from "$ID" 2>&1)
case "$sendout" in
  SENT:*)   ok "send reached the spt-hosted endpoint LIVE (SENT — PTY inject)" ;;
  QUEUED:*) ok "send reached the spt-hosted endpoint (QUEUED — spooled, reachable)" ;;
  *)        bad "send did not reach $ID: [$sendout]" ;;
esac

[ "$fail" -eq 0 ] && { echo "CC-LAUNCHER-BIND-INT OK"; exit 0; } || { echo "CC-LAUNCHER-BIND-INT FAIL"; exit 1; }

-  for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do

+  for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do


for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do

for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do


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

#!/bin/sh
# Validation for F-017 — the multi-subnet bringup gap, FIXED in spt-core v0.14.0 (the
# endpoint-creation-flow milestone, REQ-RUN-MULTISUBNET-HOME / ADRs 0026-0027). This started life as
# the W6 regression SEED that pinned the gap on <= 0.13.2; on v0.14.0 the expected outcomes FLIP from
# "gap reproduces" to "fix confirmed". Still multi-subnet-gated (single-subnet auto-homes + never sees
# any of this). Validates spt-core's REQ-RUN-MULTISUBNET-HOME (their registry, not ours) — kept
# untagged here, like the original finding-repro seed.
#
# THE FIX (v0.14.0). `spt endpoint run` now HOMES an endpoint to one subnet at creation:
#   - multi-subnet node, no --subnet, non-interactive  -> REFUSES clear (MULTI_SUBNET_HOME + the subnet
#     list) INSTANTLY — replacing the old silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT (the F-017 gap).
#   - --subnet <name>                                  -> homes there; the harness binds (UNBOUND ->
#     ONLINE), no HOME_REFUSED.
# The underlying home-assignment POLICY is unchanged + still correct: a NEW-endpoint `api bind` without
# a home still HOME_REFUSEs (Case 2) — that policy is WHY `endpoint run` had to grow `--subnet`.
#
# Cases 1 & 2 are fast (a refuse + a bare bind probe, no harness spawn) and run on any multi-subnet
# node. Case 3 is the full E2E of the fix: it spawns a REAL claude into a broker PTY (the daemon also
# hosts a Psyche) and asserts the endpoint HOMES + BINDS (UNBOUND -> online) — gated behind
# SPTC_ACCEPTANCE=1. All disposable, per-run-unique ids (REQ-HAZARD-PERCH-COLLISION — NEVER a live
# agent's id); everything torn down on exit. Idempotent.
# Run: sh ci/subnet/multi-subnet-bringup-int.sh                      (cases 1+2)
#      SPTC_ACCEPTANCE=1 sh ci/subnet/multi-subnet-bringup-int.sh    (+ case 3 E2E)   exit 0 = pass.
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
A=claude-spt
OWLERY="${LOCALAPPDATA:-$HOME/AppData/Local}/spt-core/owlery"

command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }

# v0.14.0 is the carrying release (endpoint run gained --subnet + the MULTI_SUBNET_HOME refuse).
if ! spt endpoint run --help 2>&1 | grep -q -- '--subnet'; then
  echo "SKIP: spt 'endpoint run' has no --subnet (needs v0.14.0 — the F-017 fix). On <= 0.13.2 the gap is unfixed."; exit 0
fi

# Multi-subnet GATE — the fix (and the gap it replaced) is invisible on a single-subnet node.
SUBNETS=$(spt endpoint list 2>/dev/null | grep -E '^SUBNET ' | awk '{print $2}')
NSUB=$(printf '%s\n' "$SUBNETS" | grep -c .)
if [ "${NSUB:-0}" -lt 2 ]; then
  echo "SKIP: node holds ${NSUB:-0} subnet(s); F-017 only manifests on a multi-subnet node (>=2)"; exit 0
fi
HOME_SUB=$(printf '%s\n' "$SUBNETS" | head -1)
echo "multi-subnet node: [$(printf '%s' "$SUBNETS" | tr '\n' ' ')] — home = $HOME_SUB"

RUN=$$
fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }

MADE=""
RUNPID=""
cleanup() {
  for id in $MADE; do spt endpoint purge "$id" --yes --force >/dev/null 2>&1 || true; done
  [ -n "$RUNPID" ] && taskkill //PID "$RUNPID" //T //F >/dev/null 2>&1
  if [ -n "${C3_ID:-}" ]; then
    for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
      taskkill //PID "$p" //T //F >/dev/null 2>&1
    done
    spt endpoint shutdown "$C3_ID" >/dev/null 2>&1 || true
    spt endpoint stop "$C3_ID" >/dev/null 2>&1 || true
    spt endpoint purge "$C3_ID" --yes --force >/dev/null 2>&1 || true
    rm -rf "$OWLERY/$C3_ID" >/dev/null 2>&1 || true
  fi
}
trap cleanup EXIT INT TERM

# ── Case 1 — THE FIX: spt-hosted `endpoint run` without --subnet REFUSES clear + INSTANTLY ────
# (was the silent ~25s ENDPOINT_RUN_ONLINE_TIMEOUT). Non-interactive --start; expect no perch.
C1_ID=f017fix-run-$RUN
start=$(date +%s 2>/dev/null || echo 0)
out=$(spt endpoint run --adapter "$A" --id "$C1_ID" --start 2>&1)
end=$(date +%s 2>/dev/null || echo 0)
case "$out" in
  MULTI_SUBNET_HOME:*) ok "Case 1: endpoint run w/o --subnet -> MULTI_SUBNET_HOME refuse ([$out])" ;;
  *ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) MADE="$MADE $C1_ID"; bad "Case 1: endpoint run w/o --subnet STARTED a bringup — should refuse on a multi-subnet node [$out]" ;;
  *) bad "Case 1: unexpected endpoint-run result (expected MULTI_SUBNET_HOME): [$out]" ;;
esac
# The refuse must be IMMEDIATE — the gap it replaced was a ~25s hang. Allow generous slack (< 10s).
if [ "$start" -gt 0 ] && [ "$end" -gt 0 ]; then
  el=$((end - start))
  [ "$el" -lt 10 ] && ok "Case 1: refuse was immediate (${el}s, not the old ~25s timeout)" \
    || bad "Case 1: refuse took ${el}s — suspiciously close to the old ONLINE_TIMEOUT hang"
fi
[ -f "$OWLERY/$C1_ID/info.json" ] && { MADE="$MADE $C1_ID"; bad "Case 1: a perch was created despite the refuse"; } || ok "Case 1: no perch created (clean refuse, nothing to reap)"

# ── Case 2 — underlying POLICY (unchanged): a NEW-endpoint bind needs a home ──────────────────
C2_ID=f017fix-bind-$RUN
out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" 2>&1)
case "$out" in
  HOME_REFUSED:*) ok "Case 2a: bind w/o --subnet -> HOME_REFUSED (home-assignment policy intact) ([$out])" ;;
  BOUND:*)        MADE="$MADE $C2_ID"; bad "Case 2a: bind w/o --subnet BOUND unexpectedly [$out]" ;;
  *)              bad "Case 2a: unexpected bind result: [$out]" ;;
esac
out=$(spt api bind "$C2_ID" --set-session-id "sess-$RUN" --subnet "$HOME_SUB" 2>&1)
case "$out" in
  BOUND:*) MADE="$MADE $C2_ID"; ok "Case 2b: bind WITH --subnet $HOME_SUB -> BOUND ([$out])" ;;
  *)       bad "Case 2b: bind WITH --subnet did not BIND: [$out]" ;;
esac

# ── Case 3 (E2E, gated) — `endpoint run --subnet` HOMES + the harness BINDS (UNBOUND -> online) ─
if [ "${SPTC_ACCEPTANCE:-0}" = "1" ]; then
  if spt adapter list 2>/dev/null | grep -q "$A"; then
    C3_ID=f017fix-home-$RUN
    runout=$(spt endpoint run --adapter "$A" --id "$C3_ID" --subnet "$HOME_SUB" --start 2>&1)
    case "$runout" in
      *ENDPOINT_RUN_STARTED*|*ENDPOINT_RUN:*) ok "Case 3: endpoint run --subnet $HOME_SUB STARTED ([$runout])" ;;
      *) bad "Case 3: endpoint run --subnet did not start: [$runout]" ;;
    esac
    RUNPID=$(printf '%s' "$runout" | grep -oE 'pid=[0-9]+' | grep -oE '[0-9]+' | head -1)
    # Poll ~40s for the harness to self-bind (UNBOUND -> bound/online). The gap = no perch ever bound.
    bound=0
    i=0; while [ "$i" -lt 20 ]; do
      row=$(spt endpoint list 2>/dev/null | grep "$C3_ID" | head -1)
      case "$row" in *alive=true*) bound=1; break ;; esac
      sleep 2; i=$((i+1))
    done
    [ "$bound" -eq 1 ] && ok "Case 3: harness HOMED $HOME_SUB + BOUND (UNBOUND -> online, no HOME_REFUSED)" \
      || bad "Case 3: endpoint never bound within ~40s (regressed to the no-perch gap?)"
  else
    echo "Case 3: SKIP (claude-spt not registered)"
  fi
else
  echo "Case 3: SKIP (set SPTC_ACCEPTANCE=1 — spawns a real claude + Psyche, mutates perch state)"
fi

[ "$fail" -eq 0 ] && { echo "MULTI-SUBNET-BRINGUP-INT OK (F-017 fix confirmed on v0.14.0)"; exit 0; } || { echo "MULTI-SUBNET-BRINGUP-INT FAIL"; exit 1; }

-    for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do

+    for p in $(wmic process where "name='claude-spt.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do


Now shipped strings + operative docs. Read setup.md and the doc references:


30	       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->
31
32	3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no
33	   `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`
34	   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
35	   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the
36	   adapter install dir** (the `from …` path in `spt adapter list`), where activation extracted them —
37	   no PATH copy needed. If either fails to start, confirm both are present in that dir (a packaging
38	   defect, not a PATH problem).
39
40	4. **ccs wiring (optional).** Detect `~/.ccs`:
41	   - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a

       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->

3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no
   `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`
   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the
   adapter install dir** (the `from …` path in `spt adapter list`), where activation extracted them —
   no PATH copy needed. If either fails to start, confirm both are present in that dir (a packaging
   defect, not a PATH problem).

4. **ccs wiring (optional).** Detect `~/.ccs`:
   - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a


   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the
   adapter install dir** (the `from …` path in `spt adapter list`), where activation extracted them —
   no PATH copy needed. If either fails to start, confirm both are present in that dir (a packaging
   defect, not a PATH problem).

   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
   `claude-spt` tool (its `digest` / `psyche` / `post-update` subcommands) and `cc-spt-idle-translate`
   are invoked by bare name and resolve **from the adapter install dir** (the `from …` path in
   `spt adapter list`), where activation extracted them — no PATH copy needed. If either fails to
   start, confirm both are present in that dir (a packaging defect, not a PATH problem).


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

   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
   `claude-spt` tool (its `digest` / `psyche` / `post-update` subcommands) and `cc-spt-idle-translate`
   are invoked by bare name and resolve **from the adapter install dir** (the `from …` path in
   `spt adapter list`), where activation extracted them — no PATH copy needed. If either fails to
   start, confirm both are present in that dir (a packaging defect, not a PATH problem).

# /sptc:setup — operative instructions

**Goal:** install or repair spt-core for this session **and activate the claude-spt adapter** — the
mid-session installer covering the gap when no SessionStart bootstrap has fired. Installing the
binary is only half the job: a present binary with a **deregistered** adapter has no
profiles/strings/hints/`[digest]`, so the `/sptc:*` surface is inert until activation.

<!-- [doc->REQ-SETUP-ACTIVATE] -->

**Do this:**

1. **Binary.** If spt-core is absent (`command -v spt && spt --version` reports nothing), run the
   published install-on-demand bootstrap (spt-releases `harness-contract/install-on-demand.md`):
   - **POSIX:** `curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh`
   - **Windows (PowerShell):** `irm https://sabermage.github.io/spt-releases/install.ps1 | iex`

   `PATH` is not reloaded in this shell after a fresh install — verify with the absolute path:
   `"$HOME/.local/bin/spt" --version`. After this, `spt update` handles signed self-updates.

2. **Activate the adapter.** Run `spt adapter list` and find `claude-spt`:
   - Listed and **not** `deregistered` → already active; report it and skip to step 3.
   - Missing or `deregistered` → activate it:
     - **Local dev / dogfooding a repo checkout** (an `adapter/claude-spt.toml` near cwd):
       `spt adapter add ./adapter/claude-spt.toml`.
     - **End-user (plugin only):** `spt adapter add --release SaberMage/spt-claude-code` — fetches the
       single multi-platform `adapter.spt` (one archive bundling every supported platform's binaries
       beside a shared manifest; install auto-resolves the host's), extracts to the durable home,
       registers. No `--asset` / os-detection needed — the fat archive is host-agnostic (ADR-0024 W1).
       `--tag <ver>` pins a version. Needs spt **v0.13.2+** (the version that reads a fat archive).
       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->

3. **Verify + set active.** Re-run `spt adapter list` — `claude-spt` must read **active** (no
   `deregistered`). Then `spt adapter use claude-spt` so bare resolution lands here for the `claude`
   host (the legacy-parity bare flow — `/sptc:live`/`/sptc:ready` need no `--adapter`). The
   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the
   adapter install dir** (the `from …` path in `spt adapter list`), where activation extracted them —
   no PATH copy needed. If either fails to start, confirm both are present in that dir (a packaging
   defect, not a PATH problem).

4. **ccs wiring (optional).** Detect `~/.ccs`:
   - Present → the shipped `claude-spt:ccs` profile leaf-replaces the session command with `ccs` (a
     drop-in for `claude`), so live/ready agents can run on ccs backends via `--adapter
     claude-spt:ccs`. Check `command -v ccs`; if `~/.ccs` exists but `ccs` isn't on PATH, point the
     user at their ccs bin dir.
   - Absent → ccs is an optional CLI router for alternate model backends in place of `claude`. To
     enable: install ccs, then re-run `/sptc:setup`. Skip if unwanted.
   <!-- [doc->REQ-SETUP-CCS] -->

5. **Subnet onboarding (optional).** A subnet is the private group of paired machines that makes
   `/sptc:send`, `/sptc:ready`, and live agents work cross-machine (local use needs none). Check
   `spt subnet status`:
   - In a subnet → invite a machine with `spt subnet show-code`; on the joiner, `spt subnet join`.
   - Not in one → offer **create** (`spt subnet create` — this node becomes seed-holder) or **join**
     (`spt subnet join`). Full verb guidance → **/sptc:subnet**.
   - **Elevation:** create/join/show-code are OS-elevation-gated — Windows: elevated (UAC) shell;
     Linux desktop: pkexec/polkit or sudo terminal; Linux TTY: inline sudo; headless: print the exact
     command for the user to run elevated.
   <!-- [doc->REQ-SETUP-SUBNET] -->

Idempotent and safe to re-run — the same bootstrap + activation the SessionStart hook performs.

-   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the

-   adapter install dir** (the `from …` path in `spt adapter list`), where activation extracted them —

-   no PATH copy needed. If either fails to start, confirm both are present in that dir (a packaging

+   `claude-spt` tool (its `digest` / `psyche` / `post-update` subcommands) and `cc-spt-idle-translate`

+   are invoked by bare name and resolve **from the adapter install dir** (the `from …` path in

+   `spt adapter list`), where activation extracted them — no PATH copy needed. If either fails to

+   start, confirm both are present in that dir (a packaging defect, not a PATH problem).


# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
# whose own turns are asserted in tools/claude-spt-psyche unit tests).

# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt psyche` (the runner;
# `claude-spt` has `-`/no-space after `claude`, so the (claude|ccs)(space|") match correctly excludes
# it — its turns are asserted in the tools/claude-spt crate's psyche unit tests).


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

# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt psyche` (the runner;
# `claude-spt` has `-`/no-space after `claude`, so the (claude|ccs)(space|") match correctly excludes
# it — its turns are asserted in the tools/claude-spt crate's psyche unit tests).

#!/bin/sh
# Brand-value regression guard for the `spt endpoint run` shortcut: the manifest MUST declare
# adapter.shortcut_basename = "cc" (→ cc-<id>, the M12 cc launcher). A drift to "sptc"/"spt"/default
# is caught here, distinct from the structural manifest-schema check. (The launcher brand is
# decoupled from the plugin name `sptc`; the s/sptc/spt/ succession renames the plugin, not this.)
# Run: sh tests/manifest-shortcut.sh (exit 0 = pass).
# [unit->REQ-DIST-SHORTCUT-BASENAME]
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
fail=0

# The active assignment line (not a comment): a TOML key at column 0. grep -E, exclude `#`-leading.
line=$(grep -E '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ -z "$line" ]; then echo "FAIL no shortcut_basename assignment in manifest"; exit 1; fi

case "$line" in
  *'"cc"'*) echo "ok   shortcut_basename = \"cc\" (cc-<id> brand intact)" ;;
  *) echo "FAIL shortcut_basename is not \"cc\": $line"; fail=1 ;;
esac

# Defensive: exactly one active assignment (a stray second would make the picker value ambiguous).
n=$(grep -Ec '^[[:space:]]*shortcut_basename[[:space:]]*=' "$MANIFEST")
if [ "$n" -eq 1 ]; then echo "ok   single shortcut_basename assignment"; else echo "FAIL $n shortcut_basename assignments (want 1)"; fail=1; fi

# [unit->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK]
# Non-interactive bringup: ALL THREE CC spawn commands — the two [session.self] (base `claude`, the
# `ccs` profile) AND [session.resume] (`claude -r … --remote-control …`) — MUST carry
# --dangerously-skip-permissions. The broker spawns CC into a PTY with no operator, so an interactive
# permission prompt would deadlock the launch (docs/KNOWN-HAZARDS.md §2.2). Match the active
# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
# whose own turns are asserted in tools/claude-spt-psyche unit tests).
spawn=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"(claude|ccs)([[:space:]]|")' "$MANIFEST")
nspawn=$(printf '%s' "$spawn" | grep -c .)
if [ "$nspawn" -eq 3 ]; then echo "ok   3 CC spawn commands ([session.self] base claude + ccs, [session.resume])"; else echo "FAIL expected 3 CC spawn commands, found $nspawn"; fail=1; fi
nomiss=$(printf '%s\n' "$spawn" | grep -c -v -- '--dangerously-skip-permissions')
if [ "$nspawn" -gt 0 ] && [ "$nomiss" -eq 0 ]; then echo "ok   every CC spawn command carries --dangerously-skip-permissions"; else echo "FAIL a CC spawn command lacks --dangerously-skip-permissions ($nomiss without it)"; fail=1; fi

# [unit->REQ-DIST-SESSION-RESUME]
# [session.resume] declares CC's native-resume verb — `-r {session_id}` (reload the real transcript,
# else a resume re-runs [session.self] → blank) and `--remote-control {id}` (thread the endpoint id
# as the RC session name). A drift that drops either fill silently breaks native resume / RC drive.
resume=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -r ' "$MANIFEST")
if [ -z "$resume" ]; then echo 'FAIL no [session.resume] claude -r … command in manifest'; fail=1; else
  case "$resume" in
    *'-r {session_id}'*) echo "ok   [session.resume] reloads by {session_id} (-r)";;
    *) echo "FAIL [session.resume] missing -r {session_id}: $resume"; fail=1;;
  esac
  case "$resume" in
    *'--remote-control {id}'*) echo "ok   [session.resume] threads {id} as the remote-control name";;
    *) echo "FAIL [session.resume] missing --remote-control {id}: $resume"; fail=1;;
  esac
fi

# [unit->REQ-DIST-UPDATE-MESSAGE]
# U1: [update] carries a `message` field (markdown spt-core prints on a real apply). It MUST mention
# the unavoidable /reload-plugins manual residual — that is the field's whole reason to exist. Match
# the active `message =` assignment and the reload-plugins notice anywhere in the manifest body.
if grep -Eq '^[[:space:]]*message[[:space:]]*=' "$MANIFEST"; then echo "ok   [update].message present"; else echo "FAIL [update] has no message field"; fail=1; fi
if grep -q 'reload-plugins' "$MANIFEST"; then echo "ok   [update].message points at /reload-plugins"; else echo "FAIL [update].message lacks the /reload-plugins notice"; fail=1; fi

# [unit->REQ-DIST-RC-STARTUP]
# U6: {id} threads the DISPLAY name (`-n {id}`) on BOTH bringup paths and the REMOTE-CONTROL channel
# (`--remote-control {id}`) on BOTH, so a FRESH ([session.self]) and a RESUMED ([session.resume])
# endpoint are identified + RC-controlled identically. self currently `claude -n {id} …`, resume
# `claude -r {session_id} -n {id} …`. A drift that drops -n on either, or --remote-control on self,
# silently breaks the {id} display/RC parity. ({id}-only now; -> {id}@{node} when a {node} key lands.)
self_cmd=$(grep -E '^[[:space:]]*command[[:space:]]*=[[:space:]]*"claude -n ' "$MANIFEST")
if [ -z "$self_cmd" ]; then echo 'FAIL no [session.self] "claude -n {id} …" command in manifest'; fail=1; else
  case "$self_cmd" in
    *'-n {id}'*) echo "ok   [session.self] sets {id} as the display name (-n)";;
    *) echo "FAIL [session.self] missing -n {id}: $self_cmd"; fail=1;;
  esac
  case "$self_cmd" in
    *'--remote-control {id}'*) echo "ok   [session.self] threads {id} as the remote-control name";;
    *) echo "FAIL [session.self] missing --remote-control {id}: $self_cmd"; fail=1;;
  esac
fi
# resume carries -n {id} too (the U6 addition — parity with self; --remote-control {id} already
# asserted by the REQ-DIST-SESSION-RESUME block above, $resume reused).
case "$resume" in
  *'-n {id}'*) echo "ok   [session.resume] sets {id} as the display name (-n, U6 parity)";;
  *) echo "FAIL [session.resume] missing -n {id} (U6 display-name parity): $resume"; fail=1;;
esac

# [unit->REQ-DIST-IDLE-TRANSLATE]
# [message-idle-translation-binary] declares the idle-delivery filter by its bare binary name; the
# value MUST match the packed binary (tools/cc-spt-idle-translate) so spt-core can resolve+spawn it.
idle=$(grep -E '^[[:space:]]*path[[:space:]]*=[[:space:]]*"cc-spt-idle-translate"' "$MANIFEST")
if [ -n "$idle" ]; then echo "ok   [message-idle-translation-binary] path = \"cc-spt-idle-translate\""; else echo "FAIL [message-idle-translation-binary] path != \"cc-spt-idle-translate\""; fail=1; fi

[ "$fail" -eq 0 ] && { echo "MANIFEST-SHORTCUT OK"; exit 0; } || { echo "MANIFEST-SHORTCUT FAIL"; exit 1; }

-# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,

-# whose own turns are asserted in tools/claude-spt-psyche unit tests).

+# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt psyche` (the runner;

+# `claude-spt` has `-`/no-space after `claude`, so the (claude|ccs)(space|") match correctly excludes

+# it — its turns are asserted in the tools/claude-spt crate's psyche unit tests).


Operative docs (build instructions + hazard mapping must be accurate). Read the regions:


145	  Never uploads (operator's step).
146	<!-- [doc->REQ-DIST-ADAPTER-PEROS] -->
147	- **Fat layout (REQ-DIST-ADAPTER-PEROS).** Recognized triples = **`x86_64-pc-windows-msvc`** (win
148	  `.exe`) + **`x86_64-unknown-linux-gnu`** (linux ELF) — the ONLY two spt-core classifies in a fat
149	  archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install
150	  dir**, so the bare-name command tokens (`claude-spt-digest`, …) still resolve at `<install_dir>/`
151	  (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root
152	  entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform
153	  beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.
154	- **Build both platforms first** (the packer needs both binary sets present):
155	  - **Windows (native):** `sh ci/digest/build.sh && sh ci/psyche/build.sh && sh
156	    ci/idle-translate/build.sh` → `tools/*/target/release/*.exe`.
157	  - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target
158	    x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the
159	    link needs a Linux linker), so use **`cargo-zigbuild`** (zig supplies the cross-linker):
160	    ```sh
161	    # one-time: install zig (a self-contained binary) + cargo-zigbuild
162	    cargo install cargo-zigbuild
163	    # download zig (e.g. 0.14.1) to a dir OUTSIDE the repo (do NOT keep it under tools/ — it's a
164	    # traceable-reqs scan root and zig's bundled libc C-headers trip the scanner), put zig.exe on PATH:
165	    #   e.g. ~/.sptc-zig/zig-x86_64-windows-0.14.1/zig.exe  →  export PATH="$HOME/.sptc-zig/...:$PATH"
166	    rustup target add x86_64-unknown-linux-gnu
167	    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml
168	    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml
169	    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml
170	    ```
171	    → `tools/*/target/x86_64-unknown-linux-gnu/release/*` carrying real `ELF x86-64 GNU/Linux`
172	    binaries (verified). Then `sh ci/publish/package-adapter.sh --apply` packs BOTH into one
173	    `dist/adapter.spt`.
174	- **Publish:** attach the single `dist/adapter.spt` to the GitHub release on `SaberMage/spt-claude-code`.
175	  End users `spt adapter add --release SaberMage/spt-claude-code` (default asset `adapter.spt`, or
176	  `--tag <ver>` to pin) — the fat archive auto-resolves the host's binaries, no `--asset` needed. The
177	  `[update] avenue = "gh_release"` self-update fetches the same default `adapter.spt`, host-agnostic.
178	  - **F-015 note (Windows):** a binary-changing update still can't overwrite a binary a running live
179	    agent locks — but spt-core ≥ 0.13.2's W3 fix runs the live psyche from a `<perch>/.live-bin`
180	    own-copy, so the install-dir binary stays lock-free (validated 2026-06-22). Strings/hook-only
181	    releases always land fine.
182	  - **Local dogfood before upload (recommended):** extract `dist/adapter.spt`, flatten this node's
183	    triple into a dir (`cp <triple>/* .`), then `spt adapter {translate,digest}-proof claude-spt --dir
184	    <dir> --manifest <dir>/manifest.toml` — proves the flattened binaries resolve + run without any
185	    upload or registry mutation.
186	  - **`/sptc:setup` coupling.** The fat default is host-agnostic, so setup needs no os-detection /
187	    `--asset` — a bare `adapter add --release` suffices. (The current setup body still detects os/arch
188	    + passes `--asset`; harmless, simplifying it to the bare default is a follow-on.)
189	- **Binaries (install-dir resolution — REQ-INSTALL-11).** A `--release`/`--github` acquisition extracts
190	  the `.spt` archive's `manifest.toml` + `strings/` **+ both tool binaries** into the adapter install
191	  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,
192	  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /
193	  REQ-INSTALL-11) — **no PATH placement needed** (dogfood-proven on v0.8.1 for both binaries; F-006
194	  RESOLVED, interim retired). The pack therefore MUST carry both binaries (the resolution source). Note:

  Never uploads (operator's step).
<!-- [doc->REQ-DIST-ADAPTER-PEROS] -->
- **Fat layout (REQ-DIST-ADAPTER-PEROS).** Recognized triples = **`x86_64-pc-windows-msvc`** (win
  `.exe`) + **`x86_64-unknown-linux-gnu`** (linux ELF) — the ONLY two spt-core classifies in a fat
  archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install
  dir**, so the bare-name command tokens (`claude-spt-digest`, …) still resolve at `<install_dir>/`
  (REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root
  entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform
  beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.
- **Build both platforms first** (the packer needs both binary sets present):
  - **Windows (native):** `sh ci/digest/build.sh && sh ci/psyche/build.sh && sh
    ci/idle-translate/build.sh` → `tools/*/target/release/*.exe`.
  - **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target
    x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the
    link needs a Linux linker), so use **`cargo-zigbuild`** (zig supplies the cross-linker):
    ```sh
    # one-time: install zig (a self-contained binary) + cargo-zigbuild
    cargo install cargo-zigbuild
    # download zig (e.g. 0.14.1) to a dir OUTSIDE the repo (do NOT keep it under tools/ — it's a
    # traceable-reqs scan root and zig's bundled libc C-headers trip the scanner), put zig.exe on PATH:
    #   e.g. ~/.sptc-zig/zig-x86_64-windows-0.14.1/zig.exe  →  export PATH="$HOME/.sptc-zig/...:$PATH"
    rustup target add x86_64-unknown-linux-gnu
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml
    ```
    → `tools/*/target/x86_64-unknown-linux-gnu/release/*` carrying real `ELF x86-64 GNU/Linux`
    binaries (verified). Then `sh ci/publish/package-adapter.sh --apply` packs BOTH into one
    `dist/adapter.spt`.
- **Publish:** attach the single `dist/adapter.spt` to the GitHub release on `SaberMage/spt-claude-code`.
  End users `spt adapter add --release SaberMage/spt-claude-code` (default asset `adapter.spt`, or
  `--tag <ver>` to pin) — the fat archive auto-resolves the host's binaries, no `--asset` needed. The
  `[update] avenue = "gh_release"` self-update fetches the same default `adapter.spt`, host-agnostic.
  - **F-015 note (Windows):** a binary-changing update still can't overwrite a binary a running live
    agent locks — but spt-core ≥ 0.13.2's W3 fix runs the live psyche from a `<perch>/.live-bin`
    own-copy, so the install-dir binary stays lock-free (validated 2026-06-22). Strings/hook-only
    releases always land fine.
  - **Local dogfood before upload (recommended):** extract `dist/adapter.spt`, flatten this node's
    triple into a dir (`cp <triple>/* .`), then `spt adapter {translate,digest}-proof claude-spt --dir
    <dir> --manifest <dir>/manifest.toml` — proves the flattened binaries resolve + run without any
    upload or registry mutation.
  - **`/sptc:setup` coupling.** The fat default is host-agnostic, so setup needs no os-detection /
    `--asset` — a bare `adapter add --release` suffices. (The current setup body still detects os/arch
    + passes `--asset`; harmless, simplifying it to the bare default is a follow-on.)
- **Binaries (install-dir resolution — REQ-INSTALL-11).** A `--release`/`--github` acquisition extracts
  the `.spt` archive's `manifest.toml` + `strings/` **+ both tool binaries** into the adapter install
  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,
  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /
  REQ-INSTALL-11) — **no PATH placement needed** (dogfood-proven on v0.8.1 for both binaries; F-006
  RESOLVED, interim retired). The pack therefore MUST carry both binaries (the resolution source). Note:


dir**, so the bare-name command tokens (`claude-spt-digest`, …) still resolve at `<install_dir>/`

dir**, so the bare-name command tokens (`claude-spt`, `cc-spt-idle-translate`) still resolve at `<install_dir>/`


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

- **Fat layout (REQ-DIST-ADAPTER-PEROS).** Recognized triples = **`x86_64-pc-windows-msvc`** (win

`.exe`) + **`x86_64-unknown-linux-gnu`** (linux ELF) — the ONLY two spt-core classifies in a fat

archive. On install it places the shared root + **flattens this node's `<triple>/*` into the install

-  dir**, so the bare-name command tokens (`claude-spt-digest`, …) still resolve at `<install_dir>/`

+  dir**, so the bare-name command tokens (`claude-spt`, `cc-spt-idle-translate`) still resolve at `<install_dir>/`

(REQ-INSTALL-11). **Footgun:** an unrecognized top-level dir is silently treated as a shared-root

entry and lands flat — the packer guards this (refuses any stray top-level dir); for a platform

beyond the two triples, ship a *separate* single-triple asset via `--asset`, never a third dir here.


  - **Windows (native):** `sh ci/digest/build.sh && sh ci/psyche/build.sh && sh
    ci/idle-translate/build.sh` → `tools/*/target/release/*.exe`.

  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →
    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate
    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)


  - **Windows (native):** `sh ci/digest/build.sh && sh ci/psyche/build.sh && sh
    ci/idle-translate/build.sh` → `tools/*/target/release/*.exe`.

  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →
    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate
    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)

- **Build both platforms first** (the packer needs both binary sets present):

-  - **Windows (native):** `sh ci/digest/build.sh && sh ci/psyche/build.sh && sh

-    ci/idle-translate/build.sh` → `tools/*/target/release/*.exe`.

+  - **Windows (native):** `sh ci/digest/build.sh && sh ci/idle-translate/build.sh` →

+    `tools/*/target/release/*.exe`. (`ci/digest/build.sh` builds the consolidated `claude-spt` crate

+    — digest/psyche/post-update; `ci/psyche/build.sh` is a shim that defers to it — ADR-0006/U2.)

- **Linux cross-built FROM Windows (proven 2026-06-16):** bare `cargo build --target

x86_64-unknown-linux-gnu` fails (`error: linker 'cc' not found` — the crate compiles, only the

link needs a Linux linker), so use **`cargo-zigbuild`** (zig supplies the cross-linker):


    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml

    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml


    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml

    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt/Cargo.toml
    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/cc-spt-idle-translate/Cargo.toml

# traceable-reqs scan root and zig's bundled libc C-headers trip the scanner), put zig.exe on PATH:

#   e.g. ~/.sptc-zig/zig-x86_64-windows-0.14.1/zig.exe  →  export PATH="$HOME/.sptc-zig/...:$PATH"

rustup target add x86_64-unknown-linux-gnu

-    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml

-    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml

+    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt/Cargo.toml

→ `tools/*/target/x86_64-unknown-linux-gnu/release/*` carrying real `ELF x86-64 GNU/Linux`


  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,
  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /

  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt`,
  `cc-spt-idle-translate`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /


  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,
  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /

  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt`,
  `cc-spt-idle-translate`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /

+ passes `--asset`; harmless, simplifying it to the bare default is a follow-on.)

- **Binaries (install-dir resolution — REQ-INSTALL-11).** A `--release`/`--github` acquisition extracts

the `.spt` archive's `manifest.toml` + `strings/` **+ both tool binaries** into the adapter install

-  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,

-  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /

+  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt`,

+  `cc-spt-idle-translate`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /

REQ-INSTALL-11) — **no PATH placement needed** (dogfood-proven on v0.8.1 for both binaries; F-006

RESOLVED, interim retired). The pack therefore MUST carry both binaries (the resolution source). Note:

a plain local-dev `spt adapter add <manifest.toml>` copies manifest + strings only (no binaries in the


Now KNOWN-HAZARDS mapping path + ADR-0003 + PARITY:


88
89	<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->
90
91	- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
92	  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
93	  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that
94	  requires permission approval, the interactive permission gate has **no operator / no stdin** to
95	  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio
96	  discarded): the daemon believes it hosts a working companion that never produces a commune.
97	- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate
98	  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**
99	  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`
100	  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox
101	  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket
102	  trust (see `docs/adr/0003-*`).
103	- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and
104	  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test
105	  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a

<!-- [doc->REQ-HAZARD-PSYCHE-PERMS-DEADLOCK] -->

- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
  (broker-held **PTY**, no operator attached at spawn). If such a CC turn hits a tool-use that
  requires permission approval, the interactive permission gate has **no operator / no stdin** to
  approve it — the turn blocks indefinitely. For the Psyche this is silent (detached, stdio
  discarded): the daemon believes it hosts a working companion that never produces a commune.
- **Invariant:** Every CC process spt-core spawns non-interactively MUST bypass the permission gate
  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**
  every pulse) carries it, and **both** `[session.self]` bringup commands (base `claude`, the `ccs`
  profile) carry it. The Psyche additionally runs **inside** a Read/Edit/Write tool sandbox
  (`--tools Read,Edit,Write --disable-slash-commands`), so auto-approve is bounded, not blanket
  trust (see `docs/adr/0003-*`).
- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and
  the tool cap) to both `seed_cmd` and `pulse_cmd`; the unit test
  `every_turn_is_sandboxed_to_legacy_owl_parity` asserts it on every turn. The bringup leg is a


- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup

- **Failure:** spt-core spawns Claude Code **non-interactively** on two paths — the Psyche runner
  (`claude-spt psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup