case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable

#!/bin/sh
# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the
# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
#
# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; 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.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
esac

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Clean up registry residue however we exit.
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 1. Registration cross-field validation accepts the manifest.
out=$(spt adapter add "$MANIFEST" 2>&1)
case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac

# 2. Listed, with the shipped profile resolved as a composite option.
list=$(spt adapter list 2>&1)
case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
# step 4d below, not a composite resolve.
# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs profile not resolved" ;; esac

# 3. [strings] read through the registry (base value).
base=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"

# 4. Profile overlay is observable: :deep leaf-replaces the base string.
deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"

# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
body=$(spt adapter get-string claude-spt skills.ready 2>&1)
case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready not resolved to file body: '$(printf %.40s "$body")'" ;; esac
# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
case "$cbody" in "# /sptc:commune"*) ok "file-backed string resolves: skills.commune -> body" ;; *) bad "skills.commune not resolved to file body: '$(printf %.40s "$cbody")'" ;; esac
fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
# `api listen`; REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac

# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
( . "$ROOT/plugin/sptc/hooks/_common.sh"
  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:ready listen up')")
  case "$inj" in
    '<sptc_skill name="ready">'*'# /sptc:ready'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:ready -> wrapped body" || bad "skill-injection did not emit wrapped body"

# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac

# 5. Soft-deregister cleanly.
rm=$(spt adapter remove claude-spt 2>&1)
list2=$(spt adapter list 2>&1)
case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac

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

-case "$list" in *claude-spt:deep*) ok "shipped profile resolves: claude-spt:deep" ;; *) bad "deep profile not resolved" ;; esac


Update Claude-spt adapter configuration and flags


Edit

# 4. Profile overlay is observable: :deep leaf-replaces the base string.
deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]

# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep
#    profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)
# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]


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

# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep
#    profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)
# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]

#!/bin/sh
# Integration proof for the claude-spt adapter manifest against a REAL spt-core (>= v0.7.0): the
# 2nd validation layer beyond JSON Schema — spt-core's `spt adapter add` cross-field registration —
# accepts the manifest, the shipped profile resolves, [strings] read through the registry, and the
# profile overlay is observable (base vs :ccs differ). This is the v1 acceptance proof for the
# adapter manifest (LLM never in the loop; pure CLI assertions). [int->REQ-DIST-MANIFEST-SCHEMA]
#
# Mutates the node-local adapter registry (add + soft-remove), so it is gated behind SPTC_ACCEPTANCE=1
# and a present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes
# what it adds. Run: SPTC_ACCEPTANCE=1 sh ci/manifest/registration-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; 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.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (shortcut_basename + adapter add are v0.7.0)"; exit 0 ;;
esac

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Clean up registry residue however we exit.
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 1. Registration cross-field validation accepts the manifest.
out=$(spt adapter add "$MANIFEST" 2>&1)
case "$out" in *registered*) ok "spt adapter add: registered" ;; *) bad "adapter add rejected: $out" ;; esac

# 2. Listed, with the shipped profile resolved as a composite option.
list=$(spt adapter list 2>&1)
case "$list" in *claude-spt*) ok "listed: claude-spt active" ;; *) bad "claude-spt not listed" ;; esac
# NOTE: there is no `:live` profile (Option A, PREP-4) — base claude-spt is live-capable
# ([session.psyche_init] in base); the LiveAgent capability is asserted via `spt api capability` at
# step 4d below, not a composite resolve.
# The ccs overlay (:ccs leaf-replaces [session.self].command -> `ccs`, a drop-in for `claude`) —
# the LOCKED-ADD ccs profile template (REQ-CCS-PROFILES; validated vs sister project claude_skill_owl).
case "$list" in *claude-spt:ccs*) ok "shipped profile resolves: claude-spt:ccs (ccs overlay)" ;; *) bad "ccs profile not resolved" ;; esac

# 3. [strings] read through the registry (base value).
base=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$base" = "Claude Code (spt)" ] && ok "strings resolve: adapter_label (base)" || bad "base adapter_label='$base'"

# 4. Profile overlay is observable: :deep leaf-replaces the base string.
deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)
[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"
# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]
ccs=$(spt adapter get-string claude-spt:ccs adapter_label 2>&1)
[ "$ccs" = "Claude Code (spt, ccs)" ] && ok "overlay observable: :ccs adapter_label differs" || bad "ccs adapter_label='$ccs'"

# 4b. File-backed [strings] pointer resolves to FILE CONTENTS (not the table, not raw); an inline
#     sibling still prints as-is. Proves `{ file = "skills/<x>.md" }` over adapter/strings/ (F-003).
body=$(spt adapter get-string claude-spt skills.ready 2>&1)
case "$body" in "# /sptc:ready"*) ok "file-backed string resolves: skills.ready -> body" ;; *) bad "skills.ready not resolved to file body: '$(printf %.40s "$body")'" ;; esac
# Newly file-backed skills (commune = file-drop body; force-stop = endpoint shutdown body).
cbody=$(spt adapter get-string claude-spt skills.commune 2>&1)
case "$cbody" in "# /sptc:commune"*) ok "file-backed string resolves: skills.commune -> body" ;; *) bad "skills.commune not resolved to file body: '$(printf %.40s "$cbody")'" ;; esac
fbody=$(spt adapter get-string claude-spt "skills.force-stop" 2>&1)
case "$fbody" in "# /sptc:force-stop"*) ok "file-backed string resolves: skills.force-stop -> body" ;; *) bad "skills.force-stop not resolved to file body: '$(printf %.40s "$fbody")'" ;; esac
# `live` is file-backed too (the LiveAgent bringup body — base [session.psyche_init] + bare
# `api listen`; REQ-SKILL-LIVE).
lbody=$(spt adapter get-string claude-spt skills.live 2>&1)
case "$lbody" in "# /sptc:live"*) ok "file-backed string resolves: skills.live -> body" ;; *) bad "skills.live not resolved to file body: '$(printf %.40s "$lbody")'" ;; esac
# subnet (LOCKED-ADD cross-machine membership skill — wraps `spt subnet`). [int->REQ-SKILL-SUBNET]
sbody=$(spt adapter get-string claude-spt skills.subnet 2>&1)
case "$sbody" in "# /sptc:subnet"*) ok "file-backed string resolves: skills.subnet -> body" ;; *) bad "skills.subnet not resolved to file body: '$(printf %.40s "$sbody")'" ;; esac

# 4c. UPS skill-injection end-to-end: the hook helper resolves a /sptc:<skill> prompt to the wrapped
#     operative body via get-string (REQ-UPS-INJECTION impl, on the registered adapter).
( . "$ROOT/plugin/sptc/hooks/_common.sh"
  inj=$(sptc_inject_skill "$(sptc_skill_key '/sptc:ready listen up')")
  case "$inj" in
    '<sptc_skill name="ready">'*'# /sptc:ready'*'</sptc_skill>'*) exit 0 ;;
    *) printf 'inject got: %.60s\n' "$inj"; exit 1 ;;
  esac ) && ok "UPS skill-injection: /sptc:ready -> wrapped body" || bad "skill-injection did not emit wrapped body"

# 4d. The spt-hosted bringup blocks ([session.self] + [env.SPT_ENDPOINT_ID]) cross-field-validate:
#     `adapter add` (step 1) is manifest-first ("an invalid manifest registers nothing"), so the
#     manifest carrying these blocks registering at all proves spt-core accepted their shape. Confirm
#     the registered adapter advertises a hostable harness — the thing `spt endpoint run` spawns via
#     [session.self] (the M12 cc-launcher target). [int->REQ-DIST-MANIFEST-SCHEMA]
cap=$(spt api --adapter claude-spt --manifest "$MANIFEST" capability 2>&1)
case "$cap" in *LiveAgent*) ok "bringup blocks accepted; capability reports hostable harness" ;; *) bad "capability missing LiveAgent: $cap" ;; esac

# 5. Soft-deregister cleanly.
rm=$(spt adapter remove claude-spt 2>&1)
list2=$(spt adapter list 2>&1)
case "$list2" in *"claude-spt"*"active"*) bad "claude-spt still active after remove" ;; *) ok "removed (soft-deregistered)" ;; esac

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

-# 4. Profile overlay is observable: :deep leaf-replaces the base string.

-deep=$(spt adapter get-string claude-spt:deep adapter_label 2>&1)

-[ "$deep" = "Claude Code (spt, deep)" ] && ok "overlay observable: :deep adapter_label differs" || bad "deep adapter_label='$deep'"

-# :ccs overlay observable the same way (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]

+# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep

+#    profile was removed 2026-06-18; :ccs is the sole shipped overlay and proves the same seam.)

+# :ccs overlay observable (leaf-replaced adapter_label). [int->REQ-CCS-PROFILES]


Now activate-int.sh and release-acquire-int.sh.


50	# 2. Verify active — deregistered -> active is the transition setup guarantees.
51	post=$(spt adapter list 2>&1)
52	postline=$(printf '%s\n' "$post" | grep '^claude-spt:')
53	case "$postline" in *active*) ok "verified: claude-spt is now active" ;; *) bad "claude-spt not active after re-activate ($postline)" ;; esac
54
55	# 3. Profiles/strings went live with activation (the point of activating, not just registering).
56	case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac
57	lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
58	[ "$lbl" = "Claude Code (spt)" ] && ok "strings live post-activate: adapter_label" || bad "adapter_label='$lbl'"
59
60	[ "$fail" -eq 0 ] && { echo "SETUP-ACTIVATE-INT OK"; exit 0; } || { echo "SETUP-ACTIVATE-INT FAIL"; exit 1; }
61

# 2. Verify active — deregistered -> active is the transition setup guarantees.
post=$(spt adapter list 2>&1)
postline=$(printf '%s\n' "$post" | grep '^claude-spt:')
case "$postline" in *active*) ok "verified: claude-spt is now active" ;; *) bad "claude-spt not active after re-activate ($postline)" ;; esac

# 3. Profiles/strings went live with activation (the point of activating, not just registering).
case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac
lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$lbl" = "Claude Code (spt)" ] && ok "strings live post-activate: adapter_label" || bad "adapter_label='$lbl'"

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


case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac

case "$post" in *claude-spt:ccs*) ok "shipped profile resolves post-activate: claude-spt:ccs" ;; *) bad "ccs profile not resolved post-activate" ;; esac


The file C:\Users\decid\Documents\projects\spt-claude-code\ci\setup\activate-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 /sptc:setup ACTIVATION bridge (F-005): binary-present is NOT a no-op —
# when the adapter is `deregistered`, setup RE-ACTIVATES it. This encodes, as deterministic CLI
# assertions on a real spt-core (>= v0.7.0), the exact deregistered->active transition the setup
# skill body performs in local-dev (file-form) mode: probe `adapter list`; if deregistered ->
# `adapter add <manifest>` -> verify active + shipped profile resolves. The LLM is never in the
# loop. [int->REQ-SETUP-ACTIVATE]
#
# Mutates the node-local adapter registry (add/remove), so it is gated behind SPTC_ACCEPTANCE=1 and a
# present spt>=0.7.0 — never runs in the default `tests/*.sh` unit sweep. Idempotent: removes what it
# adds. Run: SPTC_ACCEPTANCE=1 sh ci/setup/activate-int.sh   (exit 0 = pass).
#
# NOTE: this is the LOCAL file-form (dev) activation path. The END-USER `--release` acquisition leg
# is proven separately by ci/publish/release-acquire-int.sh (REQ-DIST-ADAPTER-RELEASE).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates the adapter registry)"; 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.6.*|0.5.*|0.4.*|0.3.*|0.2.*|0.1.*|0.0.*) echo "SKIP: spt $ver < 0.7.0 (adapter add is v0.7.0)"; exit 0 ;;
esac
[ -f "$MANIFEST" ] || { echo "SKIP: no manifest at $MANIFEST"; exit 0; }

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Clean up registry residue however we exit (leave no claude-spt registration behind).
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 0. Seed a registration, then soft-remove it to reach the F-005 trigger state: a `deregistered`
#    adapter (the residue a prior install/uninstall leaves — binary present, adapter NOT active).
spt adapter add "$MANIFEST" >/dev/null 2>&1
spt adapter remove claude-spt >/dev/null 2>&1
pre=$(spt adapter list 2>&1)
preline=$(printf '%s\n' "$pre" | grep '^claude-spt:')
case "$preline" in
  *deregistered*) ok "precondition: claude-spt is deregistered (F-005 trigger state)" ;;
  "")             ok "precondition: claude-spt absent (also a non-active start state)" ;;
  *active*)       bad "precondition not reached: claude-spt still active before re-activate ($preline)" ;;
  *)              ok "precondition: claude-spt not active ($preline)" ;;
esac

# 1. RE-ACTIVATE — the setup bridge step: `adapter add` the local manifest.
out=$(spt adapter add "$MANIFEST" 2>&1)
case "$out" in *registered*) ok "re-activate: adapter add registered the manifest" ;; *) bad "adapter add rejected: $out" ;; esac

# 2. Verify active — deregistered -> active is the transition setup guarantees.
post=$(spt adapter list 2>&1)
postline=$(printf '%s\n' "$post" | grep '^claude-spt:')
case "$postline" in *active*) ok "verified: claude-spt is now active" ;; *) bad "claude-spt not active after re-activate ($postline)" ;; esac

# 3. Profiles/strings went live with activation (the point of activating, not just registering).
case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac
lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$lbl" = "Claude Code (spt)" ] && ok "strings live post-activate: adapter_label" || bad "adapter_label='$lbl'"

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

-case "$post" in *claude-spt:deep*) ok "shipped profile resolves post-activate: claude-spt:deep" ;; *) bad "deep profile not resolved post-activate" ;; esac

+case "$post" in *claude-spt:ccs*) ok "shipped profile resolves post-activate: claude-spt:ccs" ;; *) bad "ccs profile not resolved post-activate" ;; esac


53	# safe-name of the repo (user/repo -> user-repo) appears in the install path.
54	safe=$(printf '%s' "$REPO" | tr '/' '-')
55	case "$line" in *"$safe"*) ok "install dir carries the repo safe-name ($safe)" ;; *) bad "repo safe-name '$safe' not in source path: $line" ;; esac
56
57	# 3. Profiles/strings shipped inside the .spt resolve through the registry (the asset was complete).
58	case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac
59	lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
60	[ "$lbl" = "Claude Code (spt)" ] && ok "file-backed strings shipped in the .spt resolve: adapter_label" || bad "adapter_label='$lbl'"
61
62	[ "$fail" -eq 0 ] && { echo "RELEASE-ACQUIRE-INT OK"; exit 0; } || { echo "RELEASE-ACQUIRE-INT FAIL"; exit 1; }

# safe-name of the repo (user/repo -> user-repo) appears in the install path.
safe=$(printf '%s' "$REPO" | tr '/' '-')
case "$line" in *"$safe"*) ok "install dir carries the repo safe-name ($safe)" ;; *) bad "repo safe-name '$safe' not in source path: $line" ;; esac

# 3. Profiles/strings shipped inside the .spt resolve through the registry (the asset was complete).
case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac
lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$lbl" = "Claude Code (spt)" ] && ok "file-backed strings shipped in the .spt resolve: adapter_label" || bad "adapter_label='$lbl'"

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


case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac

case "$list" in *claude-spt:ccs*) ok "shipped profile resolves from the .spt: claude-spt:ccs" ;; *) bad "ccs profile not resolved from --release asset" ;; esac


The file C:\Users\decid\Documents\projects\spt-claude-code\ci\publish\release-acquire-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 END-USER adapter acquisition leg: `spt adapter add --release
# <user/repo>` fetches the `adapter.spt` GitHub release asset (packed by package-adapter.sh),
# extracts it, and leaves claude-spt ACTIVE sourced from the per-adapter `_github/<safe>` install
# dir — distribution straight from the monorepo, no dedicated repo. Encodes the dogfood-proven
# acquire as deterministic CLI assertions on a real spt-core (>= v0.7.3, where `--release` lands).
# The LLM is never in the loop. [int->REQ-DIST-ADAPTER-RELEASE]
#
# Mutates the node-local adapter registry AND hits the network (GitHub release download), so it is
# gated behind SPTC_ACCEPTANCE=1, spt>=0.7.3, and GitHub reachability — it SKIPs (rc 0) otherwise so
# offline/old-spt hosts stay green. Idempotent: removes what it adds. Run:
#   SPTC_ACCEPTANCE=1 sh ci/publish/release-acquire-int.sh   (exit 0 = pass).
# Override target with SPTC_RELEASE_REPO / SPTC_RELEASE_TAG.
set -u
REPO="${SPTC_RELEASE_REPO:-SaberMage/spt-claude-code}"
TAG="${SPTC_RELEASE_TAG:-v0.1.0}"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (mutates registry + downloads from GitHub)"; 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 (\`adapter add --release\` lands in v0.7.3 / counter-15)"; exit 0 ;;
esac
# Network preflight — SKIP (not FAIL) on an offline host so the slow lane stays green there.
if command -v curl >/dev/null 2>&1; then
  curl -sfI "https://github.com/$REPO/releases" >/dev/null 2>&1 || { echo "SKIP: github.com/$REPO unreachable (offline)"; exit 0; }
else
  echo "SKIP: no curl for the reachability preflight"; exit 0
fi

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
# Leave no registration behind however we exit.
trap 'spt adapter remove claude-spt >/dev/null 2>&1 || true' EXIT INT TERM

# 0. Clean start — drop any prior claude-spt registration so the acquire is the thing under test.
spt adapter remove claude-spt >/dev/null 2>&1

# 1. ACQUIRE from the GitHub release — the end-user one-liner.
out=$(spt adapter add --release "$REPO" --tag "$TAG" 2>&1)
case "$out" in
  *registered*|*active*|*fetched*) ok "acquire: adapter add --release $REPO@$TAG succeeded" ;;
  *) bad "adapter add --release failed: $out" ;;
esac

# 2. Verify ACTIVE and sourced from the per-adapter _github install dir (NOT the local monorepo).
list=$(spt adapter list 2>&1)
line=$(printf '%s\n' "$list" | grep '^claude-spt:')
case "$line" in *active*) ok "verified: claude-spt active after --release acquire" ;; *) bad "claude-spt not active: $line" ;; esac
case "$line" in *_github*) ok "sourced from the _github install dir (real --release extract)" ;; *) bad "not sourced from _github: $line" ;; esac
# safe-name of the repo (user/repo -> user-repo) appears in the install path.
safe=$(printf '%s' "$REPO" | tr '/' '-')
case "$line" in *"$safe"*) ok "install dir carries the repo safe-name ($safe)" ;; *) bad "repo safe-name '$safe' not in source path: $line" ;; esac

# 3. Profiles/strings shipped inside the .spt resolve through the registry (the asset was complete).
case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac
lbl=$(spt adapter get-string claude-spt adapter_label 2>&1)
[ "$lbl" = "Claude Code (spt)" ] && ok "file-backed strings shipped in the .spt resolve: adapter_label" || bad "adapter_label='$lbl'"

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

-case "$list" in *claude-spt:deep*) ok "shipped profile resolves from the .spt: claude-spt:deep" ;; *) bad "deep profile not resolved from --release asset" ;; esac

+case "$list" in *claude-spt:ccs*) ok "shipped profile resolves from the .spt: claude-spt:ccs" ;; *) bad "ccs profile not resolved from --release asset" ;; esac


Now setup.md skill, PARITY.md, traceable comment.


46	       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->
47
48	3. **Verify activation.** Re-run `spt adapter list`: `claude-spt` must now appear **active** (no
49	   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`
50	   (any shipped profile — `:deep` or `:ccs`). Report the active state.
51
52	   Then **set the active-profile pointer** so bare resolution lands on this adapter for the `claude`
53	   host binary (the legacy-parity bare flow — `/sptc:live` and `/sptc:ready` need no `--adapter`):
54	   `spt adapter use claude-spt`. (With a single claude-hosting adapter registered this is the implicit
55	   default, but setting it is explicit + durable. There is **no `:live` profile** — base claude-spt is
56	   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
57	   only to select one of those override profiles.)
58
59	4. **Tool binaries — resolved from the install dir (REQ-INSTALL-11; no PATH copy).** The `[digest]`

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

3. **Verify activation.** Re-run `spt adapter list`: `claude-spt` must now appear **active** (no
   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`
   (any shipped profile — `:deep` or `:ccs`). Report the active state.

   Then **set the active-profile pointer** so bare resolution lands on this adapter for the `claude`
   host binary (the legacy-parity bare flow — `/sptc:live` and `/sptc:ready` need no `--adapter`):
   `spt adapter use claude-spt`. (With a single claude-hosting adapter registered this is the implicit
   default, but setting it is explicit + durable. There is **no `:live` profile** — base claude-spt is
   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
   only to select one of those override profiles.)

4. **Tool binaries — resolved from the install dir (REQ-INSTALL-11; no PATH copy).** The `[digest]`


   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`
   (any shipped profile — `:deep` or `:ccs`). Report the active state.

   `deregistered`). Spot-check the profile resolves: `spt adapter get-string claude-spt:ccs adapter_label`
   (the shipped overlay — `:ccs`). Report the active state.


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)

   `deregistered`). Spot-check the profile resolves: `spt adapter get-string claude-spt:ccs adapter_label`
   (the shipped overlay — `:ccs`). Report the active state.

# /sptc:setup — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].setup = { file = "skills/setup.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** install or repair spt-core for this session **and ACTIVATE the claude-spt adapter** — the
mid-session installer that covers the gap when no SessionStart bootstrap has fired (a user who
installs the plugin mid-session). Installing the binary is only half the job: a present binary with
a **deregistered** adapter has no profiles/strings/hints/`[digest]`, so `/sptc:*` injection has
nothing to source. Setup must leave the adapter **active**.

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

**Do this:**

1. **Binary.** Check whether spt-core is present: `command -v spt && spt --version`.
   - If absent, run the published install-on-demand bootstrap (verbatim against the spt-releases
     contract, `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 the current shell after a fresh install — verify with the absolute
     path for the first call: `"$HOME/.local/bin/spt" --version` (POSIX). Report the version.

2. **Adapter activation (the step a present binary still needs).** Run `spt adapter list` and look
   for `claude-spt`:
   - If `claude-spt` is listed and **not** marked `deregistered` → the adapter is **active**; report
     "adapter active" and go to step 4.
   - If `claude-spt` is **missing or `deregistered`** → **activate** it. Pick the source:
     - **Local dev / dogfooding from a repo checkout** (an `adapter/claude-spt.toml` is present near
       cwd): `spt adapter add ./adapter/claude-spt.toml` (the file-form takes any path + filename).
     - **End-user (plugin only, no repo checkout):** the `.spt` asset carries native binaries, so it
       ships **per-OS** — select the one matching this host. Detect os + arch:
       ```sh
       os=$(uname -s);  case "$os" in MINGW*|MSYS*|CYGWIN*|Windows*) os=windows;; Linux) os=linux;; Darwin) os=macos;; esac
       arch=$(uname -m); case "$arch" in x86_64|amd64) arch=x86_64;; arm64|aarch64) arch=aarch64;; esac
       ```
       then `spt adapter add --release SaberMage/spt-claude-code --asset adapter-$os-$arch.spt` —
       fetches the per-OS `adapter-<os>-<arch>.spt` release asset (a tar whose **root** holds
       `manifest.toml` + `strings/` + the native tool binaries) from the repo's GitHub release,
       extracts it to the durable adapter home, and registers it. Add `--tag <ver>` to pin a version
       (omit for latest). Re-running with a newer `--tag` is a manual re-acquire. `adapter add` is
       manifest-first (an invalid manifest registers nothing). (v1 ships **windows + linux** assets.
       Recommended path — straight from the monorepo, no dedicated repo. Needs the spt release that
       carries `--release`; the older `--github <root-manifest-repo>` is the alternative.)
       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->

3. **Verify activation.** Re-run `spt adapter list`: `claude-spt` must now appear **active** (no
   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`
   (any shipped profile — `:deep` or `:ccs`). Report the active state.

   Then **set the active-profile pointer** so bare resolution lands on this adapter for the `claude`
   host binary (the legacy-parity bare flow — `/sptc:live` and `/sptc:ready` need no `--adapter`):
   `spt adapter use claude-spt`. (With a single claude-hosting adapter registered this is the implicit
   default, but setting it is explicit + durable. There is **no `:live` profile** — base claude-spt is
   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
   only to select one of those override profiles.)

4. **Tool binaries — resolved from the install dir (REQ-INSTALL-11; no PATH copy).** The `[digest]`
   extractor (`claude-spt-digest`) and the Psyche runner (`claude-spt-psyche`) are invoked by **bare
   name**. spt-core resolves them **from the adapter install dir** — the `from …` path in
   `spt adapter list` (e.g. `…/adapters/_github/<safe>/`), where `--release` activation already
   extracted them beside the manifest. **Nothing to do here:** step 2 placed them; no PATH copy is
   needed. Verified on spt **v0.8.1** — session-digest and the daemon-hosted Psyche both resolve the
   binary straight from the install dir with nothing on PATH.
   - *(The legacy F-006 interim PATH-copy step is **retired**: spt-core's install-dir resolution —
     REQ-INSTALL-11, Feature B, landed in spt v0.8.0 — supersedes it. If session-digest or the Psyche
     ever fails to start, confirm the two `.exe`s are present in the install dir above; their absence
     is a packaging defect, not a PATH problem.)*

5. **ccs wiring (optional — SCOPE setup #7).** Detect `~/.ccs`:
   - **Present** → ccs is installed. The shipped **`claude-spt:ccs`** profile leaf-replaces the live
     session command with `ccs` (a drop-in for `claude`), so live/ready agents can run on ccs's
     configured backends (glm/kimi/custom from `~/.ccs/config.json`) instead of `claude`. Use it by
     selecting the `:ccs` composite: e.g. `/sptc:live` / `/sptc:ready` under `--adapter claude-spt:ccs`,
     or `spt endpoint run --adapter claude-spt:ccs`. Sanity-check `command -v ccs`; if `~/.ccs` exists
     but `ccs` is not on PATH, point the user at their ccs install's bin dir. (No action needed if
     they don't want ccs — base `claude-spt` is unaffected.)
   - **Absent** → ccs is an optional CLI router that lets your spt sessions drive alternate model
     backends (glm/kimi/custom) in place of `claude`. To enable: install ccs (see its docs), then
     re-run `/sptc:setup`. Skip if not wanted.

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

6. **Subnet onboarding (optional — SCOPE setup #3/#4).** A **subnet** is the private group of paired
   machines that makes `/sptc:send`, `/sptc:ready`, and live agents work **cross-machine** (local
   single-machine use needs none). Check membership: `spt subnet status` (or bare `spt subnet`).
   - **Already in a subnet** → report it. To invite another machine: `spt subnet show-code` (prints
     the current 6-digit code + `otpauth://` URI + a terminal QR). On the joining machine run
     `spt subnet join <name> --code <code>`.
   - **Not in a subnet** → offer either: **create** the first one — `spt subnet create <name>` (this
     node becomes the seed-holder; prints code/URI/QR) — or **join** an existing one —
     `spt subnet join <name> --code <code>`. Skip if single-machine use is fine.
   - Full verb guidance (status/create/show-code/join + lifecycle) lives in **/sptc:subnet** — route
     there rather than re-explaining.
   - **Elevation:** `create`/`join`/`show-code` are **OS-elevation-gated** (seed-reveal /
     machine-enrollment). Run where elevation can be granted: **Windows** → an elevated (UAC) shell;
     **Linux desktop** → pkexec/polkit or a sudo-capable terminal; **Linux TTY** → inline `sudo`;
     **headless / no-TTY** → print the exact command for the user to run elevated. (Automated
     context-aware elevation is a deeper wave; today, surface the requirement + the exact command.)

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

After this initial bootstrap + activation, `spt update` handles signed self-updates automatically —
the user does not run setup again. The whole flow is idempotent and safe to re-run.

-   `deregistered`). Spot-check a profile resolves: `spt adapter get-string claude-spt:deep adapter_label`

-   (any shipped profile — `:deep` or `:ccs`). Report the active state.

+   `deregistered`). Spot-check the profile resolves: `spt adapter get-string claude-spt:ccs adapter_label`

+   (the shipped overlay — `:ccs`). Report the active state.


   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
   only to select one of those override profiles.)

   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`
   only to select the ccs override profile.)


   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`
   only to select the ccs override profile.)

# /sptc:setup — operative instructions

> Delivered file-backed via the `claude-spt` adapter `[strings]`
> (`[strings.skills].setup = { file = "skills/setup.md" }`), resolved at injection time. The cplugs
> SKILL.md stays a thin skeleton (ADR-0001).

**Goal:** install or repair spt-core for this session **and ACTIVATE the claude-spt adapter** — the
mid-session installer that covers the gap when no SessionStart bootstrap has fired (a user who
installs the plugin mid-session). Installing the binary is only half the job: a present binary with
a **deregistered** adapter has no profiles/strings/hints/`[digest]`, so `/sptc:*` injection has
nothing to source. Setup must leave the adapter **active**.

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

**Do this:**

1. **Binary.** Check whether spt-core is present: `command -v spt && spt --version`.
   - If absent, run the published install-on-demand bootstrap (verbatim against the spt-releases
     contract, `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 the current shell after a fresh install — verify with the absolute
     path for the first call: `"$HOME/.local/bin/spt" --version` (POSIX). Report the version.

2. **Adapter activation (the step a present binary still needs).** Run `spt adapter list` and look
   for `claude-spt`:
   - If `claude-spt` is listed and **not** marked `deregistered` → the adapter is **active**; report
     "adapter active" and go to step 4.
   - If `claude-spt` is **missing or `deregistered`** → **activate** it. Pick the source:
     - **Local dev / dogfooding from a repo checkout** (an `adapter/claude-spt.toml` is present near
       cwd): `spt adapter add ./adapter/claude-spt.toml` (the file-form takes any path + filename).
     - **End-user (plugin only, no repo checkout):** the `.spt` asset carries native binaries, so it
       ships **per-OS** — select the one matching this host. Detect os + arch:
       ```sh
       os=$(uname -s);  case "$os" in MINGW*|MSYS*|CYGWIN*|Windows*) os=windows;; Linux) os=linux;; Darwin) os=macos;; esac
       arch=$(uname -m); case "$arch" in x86_64|amd64) arch=x86_64;; arm64|aarch64) arch=aarch64;; esac
       ```
       then `spt adapter add --release SaberMage/spt-claude-code --asset adapter-$os-$arch.spt` —
       fetches the per-OS `adapter-<os>-<arch>.spt` release asset (a tar whose **root** holds
       `manifest.toml` + `strings/` + the native tool binaries) from the repo's GitHub release,
       extracts it to the durable adapter home, and registers it. Add `--tag <ver>` to pin a version
       (omit for latest). Re-running with a newer `--tag` is a manual re-acquire. `adapter add` is
       manifest-first (an invalid manifest registers nothing). (v1 ships **windows + linux** assets.
       Recommended path — straight from the monorepo, no dedicated repo. Needs the spt release that
       carries `--release`; the older `--github <root-manifest-repo>` is the alternative.)
       <!-- [doc->REQ-DIST-ADAPTER-PEROS] -->

3. **Verify activation.** Re-run `spt adapter list`: `claude-spt` must now appear **active** (no
   `deregistered`). Spot-check the profile resolves: `spt adapter get-string claude-spt:ccs adapter_label`
   (the shipped overlay — `:ccs`). Report the active state.

   Then **set the active-profile pointer** so bare resolution lands on this adapter for the `claude`
   host binary (the legacy-parity bare flow — `/sptc:live` and `/sptc:ready` need no `--adapter`):
   `spt adapter use claude-spt`. (With a single claude-hosting adapter registered this is the implicit
   default, but setting it is explicit + durable. There is **no `:live` profile** — base claude-spt is
   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`
   only to select one of those override profiles.)

4. **Tool binaries — resolved from the install dir (REQ-INSTALL-11; no PATH copy).** The `[digest]`
   extractor (`claude-spt-digest`) and the Psyche runner (`claude-spt-psyche`) are invoked by **bare
   name**. spt-core resolves them **from the adapter install dir** — the `from …` path in
   `spt adapter list` (e.g. `…/adapters/_github/<safe>/`), where `--release` activation already
   extracted them beside the manifest. **Nothing to do here:** step 2 placed them; no PATH copy is
   needed. Verified on spt **v0.8.1** — session-digest and the daemon-hosted Psyche both resolve the
   binary straight from the install dir with nothing on PATH.
   - *(The legacy F-006 interim PATH-copy step is **retired**: spt-core's install-dir resolution —
     REQ-INSTALL-11, Feature B, landed in spt v0.8.0 — supersedes it. If session-digest or the Psyche
     ever fails to start, confirm the two `.exe`s are present in the install dir above; their absence
     is a packaging defect, not a PATH problem.)*

5. **ccs wiring (optional — SCOPE setup #7).** Detect `~/.ccs`:
   - **Present** → ccs is installed. The shipped **`claude-spt:ccs`** profile leaf-replaces the live
     session command with `ccs` (a drop-in for `claude`), so live/ready agents can run on ccs's
     configured backends (glm/kimi/custom from `~/.ccs/config.json`) instead of `claude`. Use it by
     selecting the `:ccs` composite: e.g. `/sptc:live` / `/sptc:ready` under `--adapter claude-spt:ccs`,
     or `spt endpoint run --adapter claude-spt:ccs`. Sanity-check `command -v ccs`; if `~/.ccs` exists
     but `ccs` is not on PATH, point the user at their ccs install's bin dir. (No action needed if
     they don't want ccs — base `claude-spt` is unaffected.)
   - **Absent** → ccs is an optional CLI router that lets your spt sessions drive alternate model
     backends (glm/kimi/custom) in place of `claude`. To enable: install ccs (see its docs), then
     re-run `/sptc:setup`. Skip if not wanted.

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

6. **Subnet onboarding (optional — SCOPE setup #3/#4).** A **subnet** is the private group of paired
   machines that makes `/sptc:send`, `/sptc:ready`, and live agents work **cross-machine** (local
   single-machine use needs none). Check membership: `spt subnet status` (or bare `spt subnet`).
   - **Already in a subnet** → report it. To invite another machine: `spt subnet show-code` (prints
     the current 6-digit code + `otpauth://` URI + a terminal QR). On the joining machine run
     `spt subnet join <name> --code <code>`.
   - **Not in a subnet** → offer either: **create** the first one — `spt subnet create <name>` (this
     node becomes the seed-holder; prints code/URI/QR) — or **join** an existing one —
     `spt subnet join <name> --code <code>`. Skip if single-machine use is fine.
   - Full verb guidance (status/create/show-code/join + lifecycle) lives in **/sptc:subnet** — route
     there rather than re-explaining.
   - **Elevation:** `create`/`join`/`show-code` are **OS-elevation-gated** (seed-reveal /
     machine-enrollment). Run where elevation can be granted: **Windows** → an elevated (UAC) shell;
     **Linux desktop** → pkexec/polkit or a sudo-capable terminal; **Linux TTY** → inline `sudo`;
     **headless / no-TTY** → print the exact command for the user to run elevated. (Automated
     context-aware elevation is a deeper wave; today, surface the requirement + the exact command.)

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

After this initial bootstrap + activation, `spt update` handles signed self-updates automatically —
the user does not run setup again. The whole flow is idempotent and safe to re-run.

-   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`/`:deep`

-   only to select one of those override profiles.)

+   live-capable and the command decides ready vs live; reach for `--adapter claude-spt:ccs`

+   only to select the ccs override profile.)


| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |

| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |


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

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


27	| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |


| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |

| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |


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

# Parity audit — claude-spt vs the legacy owl surface

> The reconciliation record: the adapter's operative surface measured against the **LOCKED
> parity-trim** in `SCOPE.md` (KEEP / ADD / DROP / TRANSFORM). This is the **"proven parity"**
> gate the `sptc`→`spt` succession flip depends on (see `docs/RELEASE-RUNBOOK.md` +
> `docs/KNOWN-HAZARDS.md`): the name flip happens only once this audit shows no unresolved
> divergence AND legacy owl is retired. Audited 2026-06-15 against published spt v0.7.2.
>
> Public-surface-only binds (AGENTS.md): a capability missing from the published `spt` surface is
> a **finding**, never a reason to reach into spt-core source.

<!-- [doc->REQ-PARITY-AUDIT] -->

## Disposition matrix

| Surface | SCOPE bucket | Adapter today | Status |
|---|---|---|---|
| `/live` | KEEP | operative — `skills/live.md` (base-manifest `[session.psyche_init]` + bare `api listen` + Monitor relay; Option A, no `:live` profile) | ✅ parity |
| `/commune` | KEEP | operative — `skills/commune.md` (file-drop) | ✅ parity |
| `/ready` | KEEP | operative — `skills/ready.md` | ✅ parity |
| `/send` | KEEP | operative — `skills/send.md` | ✅ parity |
| `/list-agents` | KEEP (→ `spt endpoint list`) | operative — `skills/list-agents.md` | ✅ parity |
| `/signoff` | KEEP (harness-hosted v1) | operative — `skills/signoff.md` | ✅ parity |
| `/force-stop` | KEEP (topology-aware) | operative — `skills/force-stop.md` | ✅ parity |
| `/spt:setup` | ADD | operative — `skills/setup.md` | ✅ parity |
| `/spt:version` | ADD | operative — `skills/version.md` | ✅ parity |
| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |
| subnet skill (status/create/show-code/join) | ADD (LOCKED) | operative — `skills/subnet.md` (wraps `spt subnet`) | ✅ parity (REQ-SKILL-SUBNET) |
| ccs profile (`claude-spt:ccs`) | ADD (LOCKED) | operative — `[profiles.ccs]` overlay + CLAUDE_CONFIG_DIR-aware extractor | ✅ parity (REQ-CCS-PROFILES) |
| `/whoami` skill | **DROP** (core `spt whoami` stays → M12 `endpoint list` alias) | removed (was carry-over) | ✅ dropped |
| `/new-alarm` | accepted gap | dead stub, no manifest entry | ⏸ DEFER + remove stub |
| `/fork` | DROP | absent | ✅ confirmed absent |
| `/amend-signoff` | DROP | absent | ✅ confirmed absent |
| `/revive` | DROP (daemon owns liveness) | absent | ✅ confirmed absent |
| `/clear-psyche` | DROP (= delete+recreate endpoint) | absent | ✅ confirmed absent |

TRANSFORM bucket (already realised, not skills): capsule→spt-hosted+`cc` (`[session.self]` +
`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→
`/spt:setup`; doctor→setup-verify; working-perches→`api worker-start/stop` hooks. ✅

## Open divergences and dispositions

### subnet skills — LOCKED-ADD gap → BUILD
The published surface carries the verbs (`spt subnet {create,join,show-code,status,…}`), and v1
topology scope is **mandatory** (operator vetoed harness-hosted-only — cross-subnet/PTY proof is
spt-core's central value prop, SCOPE). So this is a real gap to fill, not defer. → `REQ-SKILL-SUBNET`.

### ccs profile — LOCKED-ADD gap → BUILT ✅
<!-- [doc->REQ-CCS-PROFILES] -->
SCOPE §ccs (LOCKED): ccs ships as a **profile template** under the adapter that retargets the spawn
command template through `ccs`, with ccs's relocated log dir honored. Templates only — the user
supplies their own ccs config/keys. **Operator ruling (2026-06-15):** the shipped profile is
`claude-spt:ccs` invoking **bare `ccs`** (the account set via `ccs auth default <name>`), NOT
`glm`/`kimi` — those were SCOPE *examples*; this profile IS the worked example of adapter-profile
authoring **and** the operator's own SPT-ecosystem hook. Built + validated against the known-good
sister project **claude_skill_owl**:
- **Spawn seam.** `[profiles.ccs.session.self].command = "ccs"` leaf-replaces the base `claude`.
  owl proves ccs is a drop-in for the `claude` binary on the same argv (`live/wrapper/claude.rs`
  Tier-2 PULSE recovery latches `cli_binary = "ccs"`); `SPT_ENDPOINT_ID` rides inherited env through
  the wrapper unchanged.
- **Log-dir seam → in the extractor, not a manifest leaf.** ccs relocates CC's whole state tree
  (incl. `projects/`) via the **`CLAUDE_CONFIG_DIR`** env var (`~/.ccs/instances/<account>/.claude`),
  a per-account runtime value with no static catalog path. SCOPE's "per-profile `~/.ccs` log dir"
  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
  projects` over the `--in` root) — the owl-validated `owlery::claude_projects_root` pattern. The
  base `[digest]` config thus serves base **and** ccs sessions transparently; no `[profiles.ccs.
  digest]` leaf. This env-aware resolver is REQ-CCS-PROFILES's `impl`/`unit` evidence (a `.toml`
  profile leaf alone does not register as `impl`).
- **Finding (refines SCOPE's model).** SCOPE conflated ccs *profiles* (glm/kimi = API-provider
  env-swap, NO transcript relocation) with ccs *accounts* (isolated instances that DO relocate via
  `CLAUDE_CONFIG_DIR`). The operator runs the account/default path, which relocates — so the digest
  seam is real, but its mechanism is `CLAUDE_CONFIG_DIR`, not a fixed `~/.ccs` path. → `REQ-CCS-PROFILES`.

### `/whoami` skill — DROP (honoring LOCKED)
SCOPE DROPs the whoami **skill**; core `spt whoami` stays in the hot path (and becomes the
`endpoint list` alias in M12). The adapter currently still ships a whoami skill (a carry-over from
an early slice). Operator confirmed the drop 2026-06-15. Action: remove `[strings.skills].whoami` +
the file body + the plugin stub; redirect the "who am i" hint to core `spt whoami`. No REQ rides
whoami evidence (the file-backed-string + UPS-injection int examples repoint to `skills.ready`;
the hook-parser unit examples repoint to a surviving skill name). Core `spt whoami` calls in the
hooks (`session-start.sh`, `_common.sh`) are the CLI, not the skill — they STAY.

### `/new-alarm` — ACCEPTED GAP (deferred-pending-arrangement-system)
**Not missing — deliberately deferred.** doyle triage (2026-06-15): spt-core carries only the
alarm *event shape* (spt-proto) + relay handling (psyrelay.rs) + a test fixture; the durable
"fire-at-target-time" timer still lives in legacy owl in-memory, and an in-daemon durable scheduler
is a deliberate deferral (DEFERRED.md:43, ADR-0018 Q4/V3 — deferred because the daemon had no
one-shot consumer; building pre-consumer = untested dead code). The **operator has a superseding
"arrangement" system** in mind that would replace the alarm port. Disposition: **do not build an
adapter-side stopgap timer** (throwaway vs the coming model); **no REQ minted**; **remove the dead
`/sptc:new-alarm` stub** from the published surface (deferred ≠ shipping a no-op skill). doyle loops
me when the arrangement design surfaces.

## Parity verdict
**PROVEN (adapter surface).** All three open divergences are closed this slice: subnet skill added
(`skills/subnet.md`, REQ-SKILL-SUBNET), ccs profile added (`[profiles.ccs]` + CLAUDE_CONFIG_DIR-aware
extractor, REQ-CCS-PROFILES), whoami skill dropped. The full KEEP/ADD set is ✅ and the DROP set is
confirmed absent. `/new-alarm` is an accepted, documented gap (deferred-pending-arrangement-system)
and does **not** block the verdict.

The `sptc`→`spt` succession flip remains separately gated (NOT unblocked by this verdict alone):
it also requires **legacy owl retired** (KNOWN-HAZARDS / RELEASE-RUNBOOK) and rides the live-relay
int held for spt-core M11 counter-15. This audit clears only the parity precondition.

-| profiles + strings + hints | ADD | wired (`:deep`, `:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |

+| profiles + strings + hints | ADD | wired (`:ccs`, `[strings]`, `[[hints]]`; live-capability is in base, not a profile) | ✅ parity |


# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).

# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).


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

# impl/unit = the CLAUDE_CONFIG_DIR-aware projects-root resolver in claude-spt-digest (a .toml

# profile leaf does NOT register as impl evidence; the real impl is the env-aware extractor).

# doc = manifest [profiles.ccs] overlay + docs/PARITY.md. int = registration-int profile-resolves

-# assertion (claude-spt:ccs resolves + adapter_label overlay observable; mirrors :deep).

+# assertion (claude-spt:ccs resolves + adapter_label overlay observable; sole shipped overlay).

required_stages = ["doc", "impl", "unit", "int"]


Update dead-profile example in tips doc.


40	## Profiles: sparse leaf-replace overlays
41
42	- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It
43	  **leaf-replaces only the leaves you declare**; everything else inherits from base.
44	- Override exactly what differs:

## Profiles: sparse leaf-replace overlays

- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It
  **leaf-replaces only the leaves you declare**; everything else inherits from base.
- Override exactly what differs:


- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It

- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:ccs`). It


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

## Profiles: sparse leaf-replace overlays

-- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:deep`). It

+- A profile is selected as the composite `<adapter>:<profile>` (e.g. `claude-spt:ccs`). It

**leaf-replaces only the leaves you declare**; everything else inherits from base.

- `[profiles.<name>.session.self].command` — retarget the spawn/bringup command.


Now verify no live-wiring deep refs remain + manifest still valid.


MANIFEST-PARITY-PLAN.md:27:   shortcut_basename, [hooks.*], [identity], [inject], [digest], [profiles.deep], [strings],
MANIFEST-PARITY-PLAN.md:43:     accepted (2nd-layer cross-field validation), shipped `:deep` profile resolves, `[strings]`
PARITY-PUBLISH-PLAN.md:97:  get-string claude-spt:glm …` + resolves in registration-int (mirror the :deep/:live assertions).
PREP4-PLAN.md:14:Zero `--adapter` on the default path. `:ccs`/`:deep` stay as sanctioned override profiles (genuine
PREP4-PLAN.md:34:   (`get-string claude-spt:live` → a live profile that still exists, e.g. `:deep`). ccs `:ccs` refs
PREP4-PLAN.md:43:   psyche_init + command-decides, no :live profile). Fix line-180 ":deep/:live" comment.
adapter\claude-spt.toml:155:# command is `claude` (+ `--dangerously-skip-permissions`): CC mints its OWN session id internally
adapter\claude-spt.toml:159:# reads $SPT_ENDPOINT_ID and binds. `--dangerously-skip-permissions` is required for the spt-hosted
adapter\claude-spt.toml:165:command = "claude --dangerously-skip-permissions"
adapter\claude-spt.toml:220:# `ccs` is the ONLY shipped overlay (the placeholder `:deep` digest-window profile was removed
adapter\claude-spt.toml:232:# command — carrying the same `--dangerously-skip-permissions` as base (ccs is a drop-in for `claude`
adapter\claude-spt.toml:250:command = "ccs --dangerously-skip-permissions"
SKILLS-SLICE-PLAN.md:24:       (Empirically `spt api --adapter claude-spt:deep seed` → `SEEDED`.)
ci\manifest\registration-int.sh:46:# 4. Profile overlay is observable: :ccs leaf-replaces the base string. (The placeholder :deep


Remaining `:deep` = historical plan docs + the intentional "placeholder removed" note. Leave those. Now validate: manifest schema + traceable-reqs.


ci\manifest\validate_manifest.py
ci\manifest\check-manifest.sh
ci\manifest\registration-int.sh


Bash

sh ci/manifest/check-manifest.sh 2>&1 | tail -20

Run manifest schema validation gate


command -v traceable-reqs >/dev/null 2>&1 && traceable-reqs check 2>&1 | tail -30 || echo "traceable-reqs not on PATH"

Run traceable-reqs check


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


  [OK] REQ-DIST-MANIFEST-SCHEMA  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       CC adapter manifest validates against spt-core's published manifest.schema.json (from spt-releases)
  [OK] REQ-DIST-PLUGIN-SKELETON  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Marketplace artifact on cplugs is a thin skeleton: namespaced /sptc:* skill stubs + hooks.json + plugin.json, no embedded logic
  [OK] REQ-DIST-SHORTCUT-BASENAME  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       The adapter manifest brands the `spt endpoint run` launcher shortcut as `cc-<id>` via adapter.shortcut_basename = "cc" (the M12 cc launcher; decoupled from the sptc plugin name, ADR-0001)
  [OK] REQ-DOCS-DRIFT  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Generated docs (llms.txt) are CI-gated against drift: a deterministic generator's output must match what's checked in, and the book must build
  [OK] REQ-DOCS-SITE  required: [doc, impl]  stages: +doc +impl -unit -int
       docs-site/ builds with mdBook from src/ (themed, Diátaxis-shaped, all-real-content — no placeholders)
  [OK] REQ-HAZARD-MSYS-PATHCONV  required: [unit]  stages: -doc -impl +unit -int
       Hook wrappers read the CC payload from stdin, never from a /-leading positional argv (Git-Bash/MSYS path-mangles those on Windows)
  [OK] REQ-HAZARD-PERCH-COLLISION  required: [unit]  stages: +doc +impl +unit +int
       The acceptance harness spawns every nested claude SUT under a DISPOSABLE identity (SPT_AGENT_ID=sptc-ci-<n>), never a live agent's perch id — a colliding id tears down the live agent's perch + poll stream (name-keyed, last-establish-wins)
  [OK] REQ-PARITY-AUDIT  required: [doc]  stages: +doc -impl -unit -int
       The adapter's operative skill/profile surface is reconciled against the SCOPE.md LOCKED parity-trim (KEEP/ADD/DROP/TRANSFORM): every divergence is either resolved or logged as an accepted gap in docs/PARITY.md — the 'proven parity' record the sptc->spt succession flip gates on
  [OK] REQ-SETUP-ACTIVATE  required: [doc, int]  stages: +doc -impl -unit +int
       /sptc:setup, after confirming the spt binary is present, ACTIVATES the claude-spt adapter (`spt adapter add` — the manifest file in local dev; `--github <published-adapter-repo>` for end-users) so profiles/strings/hints/[digest] go live, then verifies (adapter no longer `deregistered`). Binary-present is NOT a no-op — this is the F-005 post-install activation bridge.
  [OK] REQ-SETUP-CCS  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup wires the ccs integration (SCOPE LOCKED setup #7): if `~/.ccs` is present, point the user at the shipped `claude-spt:ccs` profile (routes live/ready sessions through `ccs`, a drop-in for `claude`, via `--adapter claude-spt:ccs`) + sanity-check `ccs` on PATH; if absent, offer to install ccs with a one-sentence value prop (optional — base claude-spt is unaffected).
  [OK] REQ-SETUP-SUBNET  required: [doc]  stages: +doc -impl -unit -int
       /sptc:setup offers subnet onboarding (SCOPE LOCKED setup #3/#4): detect membership (`spt subnet status`); not-in-a-subnet -> offer create (`spt subnet create`, prints code/URI/QR) or join (`spt subnet join`); in-a-subnet -> show-code to invite / add-this-machine. Delegates the verb mechanics to /sptc:subnet and surfaces the OS-elevation requirement (create/join/show-code are seed-reveal/enroll-gated) with the per-context elevation paths; automated context-aware elevation is a deeper wave.
  [OK] REQ-SKILL-LIVE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       /sptc:live upgrades THIS CC session to a LiveAgent: base claude-spt is live-capable ([session.psyche_init] in the BASE manifest, Option A) so ready-vs-live is the COMMAND not a profile — bare `spt api listen <id>` stamps the perch state=live_agent and the daemon hosts the Psyche (poll stays ready, livehost.rs:282 gate); the claude-spt-psyche runner keeps the Psyche claude alive (one --continue turn per pulse, NOT one-shot `claude -p`); a resident Monitor relay is the single delivery pipe; zero --adapter (host_binaries resolution, spt-core v0.9.0+)
  [OK] REQ-SKILL-SUBNET  required: [doc, int]  stages: +doc -impl -unit +int
       The adapter ships subnet membership skill(s) (create/join/show-code) wrapping the published `spt subnet <verb>` surface — the LOCKED-ADD cross-subnet entrypoint (v1 topology is mandatory, SCOPE)
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands


Both gates green. Check no unit test pins the bare `command = "claude"`.


No matches found


for t in tests/*.sh; do sh "$t" >/dev/null 2>&1 && echo "ok   $t" || echo "FAIL $t"; done

Run unit test sweep


ok   tests/acceptance-harness.sh
ok   tests/adapter-archive.sh
ok   tests/ci-gates.sh
ok   tests/docs-gen.sh
ok   tests/hooks-parse.sh
ok   tests/manifest-schema.sh
ok   tests/manifest-shortcut.sh
ok   tests/msys-hazard.sh
ok   tests/skeleton-validate.sh


Done. Both changes in, all gates green.

**Removed `claude-spt:deep`** — placeholder profile (digest-window-20 + label leaf), only ever a demo. Gone from manifest; all live wiring swapped to `:ccs` (proves same overlay seam):
- `adapter/claude-spt.toml` — blocks + comments
- `ci/manifest/registration-int.sh`, `ci/setup/activate-int.sh`, `ci/publish/release-acquire-int.sh` — deep asserts → ccs
- `adapter/strings/skills/setup.md`, `docs/PARITY.md`, `traceable-reqs.toml` comment, tips-doc example

**Added flag** to both harness invocations (`[session.self]`):
- base: `command = "claude --dangerously-skip-permissions"`
- ccs: `command = "ccs --dangerously-skip-permissions"`

Note: real CC flag = `--dangerously-skip-permissions` (hyphenated), not "--dangerously skip permissions". Used real one. Rationale baked in comment: broker spawns CC non-interactive into PTY → perm-prompt would deadlock, no operator to approve.

Plan-doc `:deep` mentions left as historical.

Gates: manifest schema ✅ · `traceable-reqs check` all-OK ✅ · 9/9 unit tests ✅.

Not committed (no ask). Want commit?


yes, commit and push. --- does psyche have all the same tool restrictions here as it does in legacy spt (`claude_skill_owl` sibling project)?


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


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


On it — commit/push + dig into the psyche tool-restriction question (claude_skill_owl is a readable sibling, not spt-core). Parallel recon first.


git diff plugin/sptc/.claude-plugin/plugin.json && echo "=== last commit trailer ===" && git log -1 --format='%b'

Inspect pre-existing plugin.json change + last commit trailer


diff --git a/plugin/sptc/.claude-plugin/plugin.json b/plugin/sptc/.claude-plugin/plugin.json
index dcb9805..fbfbc19 100644
--- a/plugin/sptc/.claude-plugin/plugin.json
+++ b/plugin/sptc/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
 {
   "name": "sptc",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",
   "author": {
     "name": "perri"
=== last commit trailer ===
doyle floor-bump ruling: keep min_spt_core 0.9.0 (ready/live work on 0.9.0; a blanket
floor is too broad). Handle the narrow <0.11.0 endpoint-run silent-seed with a clear
SIGNAL not a floor — and (a) doc-note over (b) runtime guard, because a <0.11.0
endpoint-run spawn is ENV-INDISTINGUISHABLE from a normal user seed (both have
SPT_RELEASE_SEED + OWL_SESSION_ID set, SPT_ENDPOINT_ID empty — verified on this node), so
a guard would false-positive "update spt" on every normal seed. Narrow + transient (fleet
updates forward). Doc-note added to [env.SPT_ENDPOINT_ID]; F-013 marked CLOSED.

Co-authored by: perri


ls tools/ 2>/dev/null; echo "=== psyche runner files ==="; find . -iname '*psyche*' -not -path './.git/*' 2>/dev/null

Locate psyche runner sources


ls ../ 2>/dev/null | grep -i owl; echo "=== try common locs ==="; ls ../claude_skill_owl 2>/dev/null && echo FOUND_SIBLING

Find claude_skill_owl sibling project


claude_skill_owl
=== try common locs ===
AGENTS.md
CHANGELOG.md
CLAUDE.md
CONTEXT.md
Cargo.lock
Cargo.toml
build.rs
docs
mcp_stderr.txt
owl_agents.json
plugin
psyche.md
scripts
src
target
tests
FOUND_SIBLING


Both found. Committing + reading psyche sources in parallel.


git add adapter/claude-spt.toml ci/manifest/registration-int.sh ci/setup/activate-int.sh ci/publish/release-acquire-int.sh adapter/strings/skills/setup.md docs/PARITY.md traceable-reqs.toml SPT_HARNESS_ADAPTER_TIPS.md plugin/sptc/.claude-plugin/plugin.json
git commit -q -F - <<'EOF'
feat(adapter): drop placeholder :deep profile; --dangerously-skip-permissions on bringup

Remove the `claude-spt:deep` profile — a digest-window-20 + label demo that forked
nothing real and only existed to prove overlay-observability, which `:ccs` already
covers. All live wiring (registration/activate/release-acquire ints, setup.md,
PARITY.md, traceable comment) re-pointed to `:ccs`; `:ccs` is now the sole shipped
overlay. Historical plan docs keep their `:deep` references.

Add `--dangerously-skip-permissions` to both `[session.self]` bringup commands
(base `claude`, `:ccs` profile `ccs`): the broker spawns CC non-interactively into a
PTY, so the permission-prompt gate would deadlock the launch with no operator to
approve. spt tokenizes the command on whitespace (v0.8.2+), so the flag is a clean
second argv element; ccs is a `claude` drop-in and forwards the flag.

Gates: manifest schema OK; traceable-reqs check all-OK; 9/9 unit tests OK.

(plugin.json: pre-existing 0.1.2->0.1.3 cplugs bump, folded in.)

Co-authored by: perri
EOF
echo "=== committed ===" && git log -1 --format='%h %s'

Stage and commit the changes


find tools/claude-spt-psyche -name '*.rs' -not -path '*/target/*' | head; echo "=== psyche skill body ==="; ls plugin/sptc/skills/ 2>/dev/null; cat ci/psyche 2>/dev/null | head -5

List psyche runner rust sources


=== committed ===
166e122 feat(adapter): drop placeholder :deep profile; --dangerously-skip-permissions on bringup


tools/claude-spt-psyche/src/main.rs
=== psyche skill body ===
commune
force-stop
list-agents
live
ready
send
setup
signoff
subnet
version


Committed. Pushing + reading both psyche runners.


Push to remote


To https://github.com/SaberMage/spt-claude-code.git
   e8e90d8..166e122  main -> main


1	//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
2	//!
3	//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
4	//! spt-core daemon's livehost hosts it — only when the resolved manifest declares
5	//! `[session.psyche_init]` AND the perch came up as state=live_agent (the live `api listen` path;
6	//! Option A 2026-06-17 — psyche_init lives in the BASE claude-spt manifest, no `:live` profile; a
7	//! ready/poll perch is skipped regardless, livehost.rs:282). The Psyche owns its OWN perch
8	//! (`<parent>-psyche`), receives daemon PULSES
9	//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
10	//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
11	//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
12	//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
13	//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
14	//!
15	//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
16	//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
17	//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
18	//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
19	//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
20	//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
21	//!
22	//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
23	//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt…>
24	//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
25	//! NOTE on `--prompt` parsing: spt-core **<0.8.2** substituted `{psyche_prompt}` into the command
26	//! STRING then whitespace-split it, so the multi-word prompt reached us as many trailing argv tokens.
27	//! spt-core **0.8.2+** fills each `{key}` as exactly ONE argv element (tokenize-then-fill — re-validated
28	//! 2026-06-17: the prompt arrives as a single element, newlines intact). We parse `--prompt` GREEDILY
29	//! (slurp the rest) anyway — defensive: it reconstructs a split prompt on <0.8.2 AND passes the single
30	//! 0.8.2 element through unchanged. Keep `--prompt` LAST in the command template. [impl->REQ-SKILL-LIVE]
31	//!
32	//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
33	//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
34	//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
35	//! command's own job (ready.md), so no separate establish step.
36	//! [impl->REQ-SKILL-LIVE]
37
38	#[cfg(not(test))]
39	use std::process::{Command, ExitCode, Stdio};
40
41	/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
42	/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
43	#[derive(Debug, PartialEq)]
44	struct Args {
45	    id: String,
46	    session_id: String,
47	    prompt: String,
48	}
49
50	impl Args {
51	    /// Parse `--id V --session-id V --prompt <rest…>`. `--id`/`--session-id` are single-value and
52	    /// order-independent; `--prompt` is TERMINAL and GREEDY — it slurps every remaining token as the
53	    /// prompt (rejoined with single spaces). History: spt-core **<0.8.2** substituted `{psyche_prompt}`
54	    /// into the `[session.psyche_init]` command STRING then whitespace-split it, so a multi-word prompt
55	    /// (it always is — "PSYCHE REVIVAL time: … incoming event: …") arrived as many tokens, NOT one; a
56	    /// non-greedy `--prompt` read only the first word and rejected the second as "unknown arg" → instant
57	    /// exit 2 → the daemon recorded a phantom hosted Psyche (diagnosed v0.8.1, 2026-06-16). spt-core
58	    /// **0.8.2** fixed it (fills each `{key}` as ONE argv element; re-validated 2026-06-17). Greedy is
59	    /// KEPT as defensive: with one element, `collect().join(" ")` returns it unchanged; with the old
60	    /// split, it reconstructs. Our manifest places `--prompt` last. Returns the offending flag on a
61	    /// missing value or an unknown arg seen BEFORE `--prompt`, so a real misfire is still loud.
62	    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
63	        let (mut id, mut session_id, mut prompt) = (None, None, None);
64	        let mut it = argv.into_iter();
65	        while let Some(flag) = it.next() {
66	            let want = |it: &mut dyn Iterator<Item = String>| {
67	                it.next().ok_or_else(|| format!("{flag} expects a value"))
68	            };
69	            match flag.as_str() {
70	                "--id" => id = Some(want(&mut it)?),
71	                "--session-id" => session_id = Some(want(&mut it)?),
72	                // Terminal + greedy: consume ALL remaining tokens (spt-core whitespace-split the prompt).
73	                "--prompt" => {
74	                    prompt = Some(it.by_ref().collect::<Vec<_>>().join(" "));
75	                }
76	                other => return Err(format!("unknown arg: {other}")),
77	            }
78	        }
79	        Ok(Args {
80	            id: id.ok_or("missing --id")?,
81	            session_id: session_id.ok_or("missing --session-id")?,
82	            prompt: prompt.ok_or("missing --prompt")?,
83	        })
84	    }
85	}
86
87	/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
88	/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
89	fn seed_cmd(prompt: &str) -> Vec<String> {
90	    vec!["-p".into(), prompt.into()]
91	}
92
93	/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
94	/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
95	/// claude author a commune delta for this pulse.
96	fn pulse_cmd(pulse: &str) -> Vec<String> {
97	    vec!["--continue".into(), "-p".into(), pulse.into()]
98	}
99
100	/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
101	/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
102	/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
103	fn poll_cmd(id: &str) -> Vec<String> {
104	    vec!["ready".into(), id.into(), "--once".into()]
105	}
106
107	/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
108	/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
109	fn is_actionable(pulse: &str) -> bool {
110	    !pulse.trim().is_empty()
111	}
112
113	#[cfg(not(test))]
114	fn run() -> ExitCode {
115	    let args = match Args::parse(std::env::args().skip(1)) {
116	        Ok(a) => a,
117	        Err(e) => {
118	            eprintln!("claude-spt-psyche: {e}");
119	            return ExitCode::from(2);
120	        }
121	    };
122
123	    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
124	    // poll still defines our lifetime; we don't abort the runner over one failed turn).
125	    let _ = Command::new("claude")
126	        .args(seed_cmd(&args.prompt))
127	        .stdin(Stdio::null())
128	        .status();
129
130	    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
131	    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
132	    loop {
133	        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
134	            Ok(o) => o,
135	            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
136	        };
137	        if !out.status.success() {
138	            return ExitCode::SUCCESS; // perch gone => session ended
139	        }
140	        let pulse = String::from_utf8_lossy(&out.stdout);
141	        if !is_actionable(&pulse) {
142	            continue; // online signal / empty drain — no turn
143	        }
144	        let _ = Command::new("claude")
145	            .args(pulse_cmd(&pulse))
146	            .stdin(Stdio::null())
147	            .status();
148	    }
149	}
150
151	#[cfg(not(test))]
152	fn main() -> ExitCode {
153	    run()
154	}
155
156	#[cfg(test)]
157	fn main() {}
158
159	// [unit->REQ-SKILL-LIVE]
160	#[cfg(test)]
161	mod tests {
162	    use super::*;
163
164	    fn argv(parts: &[&str]) -> Vec<String> {
165	        parts.iter().map(|s| s.to_string()).collect()
166	    }
167
168	    #[test]
169	    fn id_and_session_are_order_independent_prompt_is_terminal() {
170	        // --id/--session-id may precede --prompt in any order; --prompt is last and slurps the rest.
171	        let a = Args::parse(argv(&[
172	            "--session-id", "sess-9", "--id", "perri-psyche", "--prompt", "be the psyche",
173	        ]))
174	        .unwrap();
175	        assert_eq!(
176	            a,
177	            Args {
178	                id: "perri-psyche".into(),
179	                session_id: "sess-9".into(),
180	                prompt: "be the psyche".into(),
181	            }
182	        );
183	    }
184
185	    #[test]
186	    fn prompt_slurps_whitespace_split_tokens() {
187	        // spt-core whitespace-splits the substituted {psyche_prompt}; we must rejoin every trailing
188	        // token, not reject the second word as "unknown arg" (the v0.8.1 phantom-Psyche bug).
189	        let a = Args::parse(argv(&[
190	            "--id", "p-psyche", "--session-id", "s",
191	            "--prompt", "PSYCHE", "REVIVAL", "time:", "epoch-ms:123", "incoming", "event:", "(none)",
192	        ]))
193	        .unwrap();
194	        assert_eq!(a.id, "p-psyche");
195	        assert_eq!(a.session_id, "s");
196	        assert_eq!(a.prompt, "PSYCHE REVIVAL time: epoch-ms:123 incoming event: (none)");
197	    }
198
199	    #[test]
200	    fn missing_flag_is_an_error_not_a_default() {
201	        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
202	        assert!(e.contains("--prompt"), "got: {e}");
203	    }
204
205	    #[test]
206	    fn unknown_arg_before_prompt_still_rejected() {
207	        // Greedy --prompt must not mask a genuine misfire earlier in argv.
208	        let e = Args::parse(argv(&["--id", "x", "--bogus", "v", "--prompt", "hi"])).unwrap_err();
209	        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
210	    }
211
212	    #[test]
213	    fn flag_without_value_errors() {
214	        let e = Args::parse(argv(&["--id"])).unwrap_err();
215	        assert!(e.contains("--id expects a value"), "got: {e}");
216	    }
217
218	    #[test]
219	    fn unknown_arg_rejected() {
220	        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
221	        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
222	    }
223
224	    #[test]
225	    fn seed_is_headless_print_of_the_prompt() {
226	        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
227	    }
228
229	    #[test]
230	    fn pulse_resumes_then_prints() {
231	        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
232	        assert_eq!(
233	            pulse_cmd("commune now"),
234	            vec!["--continue", "-p", "commune now"]
235	        );
236	    }
237
238	    #[test]
239	    fn poll_is_ready_once_on_the_psyche_perch() {
240	        assert_eq!(
241	            poll_cmd("perri-psyche"),
242	            vec!["ready", "perri-psyche", "--once"]
243	        );
244	    }
245
246	    #[test]
247	    fn blank_pulses_do_not_trigger_a_turn() {
248	        assert!(!is_actionable(""));
249	        assert!(!is_actionable("   \n\t "));
250	        assert!(is_actionable("READY backlog: resume the build"));
251	    }
252	}
253

//! claude-spt-psyche — the `[session.psyche_init]` Psyche runner for the claude-spt LiveAgent.
//!
//! WHAT A PSYCHE IS (doyle 2026-06-15, traced spt-core): a LiveAgent's detached companion. The
//! spt-core daemon's livehost hosts it — only when the resolved manifest declares
//! `[session.psyche_init]` AND the perch came up as state=live_agent (the live `api listen` path;
//! Option A 2026-06-17 — psyche_init lives in the BASE claude-spt manifest, no `:live` profile; a
//! ready/poll perch is skipped regardless, livehost.rs:282). The Psyche owns its OWN perch
//! (`<parent>-psyche`), receives daemon PULSES
//! on that perch, and on each pulse authors a COMMUNE delta (a context brief for the parent's
//! resume across `/clear`/compact) — it NEVER replies/notifies (the echo-commune is a distinct,
//! cheaper actor). It exits at session end. The Psyche is daemon-MANAGED by contract (decoupled
//! from message delivery); this runner does NOT orchestrate its own lifecycle — it is the dumb
//! resident wrapper the daemon launches and the daemon tears down (perch close = our exit signal).
//!
//! WHY A RESIDENT WRAPPER (not bare `claude -p`): `claude -p` is ONE-SHOT (one turn, then exits;
//! Stop hooks cannot re-loop it), and CC is migrating off `-p` billing-wise. So the Psyche-as-an-
//! LLM is a headless `claude` session in `{psyche_dir}` that we KEEP ALIVE: seed it once with the
//! daemon-supplied `{psyche_prompt}` (which tells that claude its Psyche role + the commune-on-pulse
//! contract), then drive one `claude --continue` turn per pulse. The COMMUNE authoring lives in
//! that claude (driven by the prompt) — this runner stays pure orchestration glue.
//!
//! Invoked by the daemon (detached, cwd=`{psyche_dir}`, stdio null):
//!   claude-spt-psyche --id <parent>-psyche --session-id <session_id> --prompt <psyche_prompt…>
//! ({id} is the daemon-OVERRIDDEN `<parent>-psyche`, NOT the parent endpoint id — see spawn_psyche.)
//! NOTE on `--prompt` parsing: spt-core **<0.8.2** substituted `{psyche_prompt}` into the command
//! STRING then whitespace-split it, so the multi-word prompt reached us as many trailing argv tokens.
//! spt-core **0.8.2+** fills each `{key}` as exactly ONE argv element (tokenize-then-fill — re-validated
//! 2026-06-17: the prompt arrives as a single element, newlines intact). We parse `--prompt` GREEDILY
//! (slurp the rest) anyway — defensive: it reconstructs a split prompt on <0.8.2 AND passes the single
//! 0.8.2 element through unchanged. Keep `--prompt` LAST in the command template. [impl->REQ-SKILL-LIVE]
//!
//! Loop:  seed `claude -p <prompt>`  ->  forever { `spt ready <id> --once` (blocks for one pulse;
//! its stdout is the pulse body) ; perch-closed => exit ; empty drain => next ; else feed the pulse
//! to `claude --continue -p <pulse>` }. The perch register + backlog drain are the `spt ready`
//! command's own job (ready.md), so no separate establish step.
//! [impl->REQ-SKILL-LIVE]

#[cfg(not(test))]
use std::process::{Command, ExitCode, Stdio};

/// Parsed launch args. Only the three daemon-filled `psyche_init.keys` we template
/// (`{id, session_id, psyche_prompt}`; `{psyche_dir}` is consumed as cwd by the daemon, not argv).
#[derive(Debug, PartialEq)]
struct Args {
    id: String,
    session_id: String,
    prompt: String,
}

impl Args {
    /// Parse `--id V --session-id V --prompt <rest…>`. `--id`/`--session-id` are single-value and
    /// order-independent; `--prompt` is TERMINAL and GREEDY — it slurps every remaining token as the
    /// prompt (rejoined with single spaces). History: spt-core **<0.8.2** substituted `{psyche_prompt}`
    /// into the `[session.psyche_init]` command STRING then whitespace-split it, so a multi-word prompt
    /// (it always is — "PSYCHE REVIVAL time: … incoming event: …") arrived as many tokens, NOT one; a
    /// non-greedy `--prompt` read only the first word and rejected the second as "unknown arg" → instant
    /// exit 2 → the daemon recorded a phantom hosted Psyche (diagnosed v0.8.1, 2026-06-16). spt-core
    /// **0.8.2** fixed it (fills each `{key}` as ONE argv element; re-validated 2026-06-17). Greedy is
    /// KEPT as defensive: with one element, `collect().join(" ")` returns it unchanged; with the old
    /// split, it reconstructs. Our manifest places `--prompt` last. Returns the offending flag on a
    /// missing value or an unknown arg seen BEFORE `--prompt`, so a real misfire is still loud.
    fn parse<I: IntoIterator<Item = String>>(argv: I) -> Result<Args, String> {
        let (mut id, mut session_id, mut prompt) = (None, None, None);
        let mut it = argv.into_iter();
        while let Some(flag) = it.next() {
            let want = |it: &mut dyn Iterator<Item = String>| {
                it.next().ok_or_else(|| format!("{flag} expects a value"))
            };
            match flag.as_str() {
                "--id" => id = Some(want(&mut it)?),
                "--session-id" => session_id = Some(want(&mut it)?),
                // Terminal + greedy: consume ALL remaining tokens (spt-core whitespace-split the prompt).
                "--prompt" => {
                    prompt = Some(it.by_ref().collect::<Vec<_>>().join(" "));
                }
                other => return Err(format!("unknown arg: {other}")),
            }
        }
        Ok(Args {
            id: id.ok_or("missing --id")?,
            session_id: session_id.ok_or("missing --session-id")?,
            prompt: prompt.ok_or("missing --prompt")?,
        })
    }
}

/// argv for the one-shot SEED turn: establishes the Psyche's headless `claude` session in the cwd
/// (`{psyche_dir}`) from the daemon-supplied psyche prompt. `-p` is the headless/print mode.
fn seed_cmd(prompt: &str) -> Vec<String> {
    vec!["-p".into(), prompt.into()]
}

/// argv for a per-PULSE turn: resume the seeded session (`--continue` picks the most-recent session
/// in the cwd) and feed the pulse body as the turn's prompt. The seeded role-prompt makes that
/// claude author a commune delta for this pulse.
fn pulse_cmd(pulse: &str) -> Vec<String> {
    vec!["--continue".into(), "-p".into(), pulse.into()]
}

/// argv for one perch poll: `spt ready <id> --once` registers the perch (first call), drains the
/// spooled backlog, blocks for exactly one delivery, then exits — re-run to stay reachable
/// (ready.md). Non-zero exit = perch gone (session end) = our cue to stop.
fn poll_cmd(id: &str) -> Vec<String> {
    vec!["ready".into(), id.into(), "--once".into()]
}

/// A pulse body is actionable only if non-blank: `spt ready` may print just its `READY:<id>` online
/// signal (on stderr) or an empty drain, which must NOT trigger a (billed) claude turn.
fn is_actionable(pulse: &str) -> bool {
    !pulse.trim().is_empty()
}

#[cfg(not(test))]
fn run() -> ExitCode {
    let args = match Args::parse(std::env::args().skip(1)) {
        Ok(a) => a,
        Err(e) => {
            eprintln!("claude-spt-psyche: {e}");
            return ExitCode::from(2);
        }
    };

    // Seed the resident Psyche claude session (best-effort: if claude is absent the daemon's perch
    // poll still defines our lifetime; we don't abort the runner over one failed turn).
    let _ = Command::new("claude")
        .args(seed_cmd(&args.prompt))
        .stdin(Stdio::null())
        .status();

    // Resident pulse loop. The perch IS the lifecycle: when the daemon tears the LiveAgent down at
    // session end the perch closes, `spt ready --once` exits non-zero, and we return.
    loop {
        let out = match Command::new("spt").args(poll_cmd(&args.id)).output() {
            Ok(o) => o,
            Err(_) => return ExitCode::SUCCESS, // spt vanished — nothing left to serve
        };
        if !out.status.success() {
            return ExitCode::SUCCESS; // perch gone => session ended
        }
        let pulse = String::from_utf8_lossy(&out.stdout);
        if !is_actionable(&pulse) {
            continue; // online signal / empty drain — no turn
        }
        let _ = Command::new("claude")
            .args(pulse_cmd(&pulse))
            .stdin(Stdio::null())
            .status();
    }
}

#[cfg(not(test))]
fn main() -> ExitCode {
    run()
}

#[cfg(test)]
fn main() {}

// [unit->REQ-SKILL-LIVE]
#[cfg(test)]
mod tests {
    use super::*;

    fn argv(parts: &[&str]) -> Vec<String> {
        parts.iter().map(|s| s.to_string()).collect()
    }

    #[test]
    fn id_and_session_are_order_independent_prompt_is_terminal() {
        // --id/--session-id may precede --prompt in any order; --prompt is last and slurps the rest.
        let a = Args::parse(argv(&[
            "--session-id", "sess-9", "--id", "perri-psyche", "--prompt", "be the psyche",
        ]))
        .unwrap();
        assert_eq!(
            a,
            Args {
                id: "perri-psyche".into(),
                session_id: "sess-9".into(),
                prompt: "be the psyche".into(),
            }
        );
    }

    #[test]
    fn prompt_slurps_whitespace_split_tokens() {
        // spt-core whitespace-splits the substituted {psyche_prompt}; we must rejoin every trailing
        // token, not reject the second word as "unknown arg" (the v0.8.1 phantom-Psyche bug).
        let a = Args::parse(argv(&[
            "--id", "p-psyche", "--session-id", "s",
            "--prompt", "PSYCHE", "REVIVAL", "time:", "epoch-ms:123", "incoming", "event:", "(none)",
        ]))
        .unwrap();
        assert_eq!(a.id, "p-psyche");
        assert_eq!(a.session_id, "s");
        assert_eq!(a.prompt, "PSYCHE REVIVAL time: epoch-ms:123 incoming event: (none)");
    }

    #[test]
    fn missing_flag_is_an_error_not_a_default() {
        let e = Args::parse(argv(&["--id", "x", "--session-id", "s"])).unwrap_err();
        assert!(e.contains("--prompt"), "got: {e}");
    }

    #[test]
    fn unknown_arg_before_prompt_still_rejected() {
        // Greedy --prompt must not mask a genuine misfire earlier in argv.
        let e = Args::parse(argv(&["--id", "x", "--bogus", "v", "--prompt", "hi"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn flag_without_value_errors() {
        let e = Args::parse(argv(&["--id"])).unwrap_err();
        assert!(e.contains("--id expects a value"), "got: {e}");
    }

    #[test]
    fn unknown_arg_rejected() {
        let e = Args::parse(argv(&["--bogus", "v"])).unwrap_err();
        assert!(e.contains("unknown arg: --bogus"), "got: {e}");
    }

    #[test]
    fn seed_is_headless_print_of_the_prompt() {
        assert_eq!(seed_cmd("hello psyche"), vec!["-p", "hello psyche"]);
    }

    #[test]
    fn pulse_resumes_then_prints() {
        // --continue MUST precede -p so the turn resumes the seeded session rather than starting fresh.
        assert_eq!(
            pulse_cmd("commune now"),
            vec!["--continue", "-p", "commune now"]
        );
    }

    #[test]
    fn poll_is_ready_once_on_the_psyche_perch() {
        assert_eq!(
            poll_cmd("perri-psyche"),
            vec!["ready", "perri-psyche", "--once"]
        );
    }

    #[test]
    fn blank_pulses_do_not_trigger_a_turn() {
        assert!(!is_actionable(""));
        assert!(!is_actionable("   \n\t "));
        assert!(is_actionable("READY backlog: resume the build"));
    }
}


Pushed (`166e122`). Now compare legacy owl psyche launch.


cd ../claude_skill_owl && grep -rniE 'allowedtools|disallowedtools|permission-mode|dangerously|allowed_tools|disallowed|permission_mode' src/ --include='*.rs' 2>/dev/null | head -40

Search legacy owl for tool-restriction flags


src/live/wrapper/claude.rs:72:            "--dangerously-skip-permissions",
src/live/wrapper/claude.rs:298:            "--dangerously-skip-permissions",
src/live/wrapper/claude.rs:504:            "--dangerously-skip-permissions",
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-claude-code


cd ../claude_skill_owl && grep -nE 'fn |--dangerously|allowedtools|--continue|"-p"|psyche|Psyche|cli_binary|append_setting|--settings|--mcp-config|tools' src/live/wrapper/claude.rs 2>/dev/null | head -60; echo "RESET"

Map claude.rs functions and flags


12:/// Psyche template embedded at compile time
13:const PSYCHE_TEMPLATE: &str = include_str!("../../../psyche.md");
15:/// Build the --agents JSON string with substituted psyche.md template.
16:fn build_agents_json(self_id: &str, psyche_id: &str, period: u64) -> String {
19:    // modes. psyche.md line 6 is exactly `Pulse period: {{period}} seconds`.
27:        .replace("{{psyche_id}}", psyche_id)
31:        "owl-psyche": {
32:            "description": "Psyche agent for owl live system",
34:            "tools": ["Read", "Write", "Edit"]
42:    pub fn init_session(&mut self) {
43:        let agents_json = build_agents_json(&self.self_id, &self.psyche_id, self.period);
55:        let mut init_cmd = Command::new(&self.cli_binary);
56:        init_cmd.current_dir(owlery::psyche_dir());
59:            "-p",
63:            "owl-psyche",
72:            "--dangerously-skip-permissions",
74:            "--tools",
155:                git_commit_context(&owlery::psyche_dir(), &self.self_id);
187:                    // 260523-648: include pulse_psyche on every re-publish so
189:                    pulse_psyche: self.pulse_psyche,
193:                // psyche_id, state)` — the central perch_path resolver places
194:                // the file at `owlery/<self>/nested/<psyche>/wrapper-state.json`
201:                    &self.psyche_id,
227:    pub fn resume_session(&self, msg: &str) {
251:    pub fn resume_session_with_exit(&self, msg: &str) -> (Option<String>, i32) {
285:        let mut resume_cmd = Command::new(&self.cli_binary);
286:        resume_cmd.current_dir(owlery::psyche_dir());
289:            "-p",
298:            "--dangerously-skip-permissions",
300:            "--tools",
338:                        git_commit_context(&owlery::psyche_dir(), &self.self_id);
398:    pub fn resume_session_checked(&self, msg: &str) -> Option<String> {
414:    pub(crate) fn emit_inwrapper_boot(self_id: &str, session_uuid: &str) {
434:    pub fn handle_pulse_trigger_recovery(&mut self, msg: &str) {
451:        let original_cli = self.cli_binary.clone();
452:        self.cli_binary = "ccs".to_string();
460:            // cli_binary stays as "ccs" -- latched
465:        self.cli_binary = original_cli;
473:    pub fn final_session(&self, msg: &str) -> bool {
491:        let mut final_cmd = Command::new(&self.cli_binary);
492:        final_cmd.current_dir(owlery::psyche_dir());
495:            "-p",
504:            "--dangerously-skip-permissions",
506:            "--tools",
552:        git_commit_context(&owlery::psyche_dir(), &self.self_id);
567:        self.log("Final psyche invocation complete, wrapper exiting");
573:    /// pattern can never recur silently (debug: psyche-final-note-not-logged).
574:    fn log_block(&self, header: &str, body: &str) {
592:/// Parse [REPLY], [NOTIFY], and [COMMUNE] markers from Psyche's response text.
594:pub fn parse_markers(response: &str) -> Vec<(String, String)> {
628:pub(crate) fn extract_claude_response(output: &str) -> String {
694:fn extract_session_uuid(output: &str) -> Option<String> {
727:fn is_uuid_like(s: &str) -> bool {
755:        fn capture() -> Self { Self(std::env::var("SPT_HOME").ok()) }
758:        fn drop(&mut self) {
766:    fn git_available() -> bool {
787:    fn emit_inwrapper_boot_first_call_appends_single_boot_row() {
798:        // Pre-seed a PsycheStatus so read_status returns Some.
801:            &crate::live::context::PsycheStatus {
803:                session_name: "doyle-psyche-1-00-00".to_string(),
RESET
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-claude-code


Legacy owl **restricts** psyche tools. Reading exact config.