===== ci/digest/build.sh =====
#!/bin/sh
# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-digest: cargo test + release build"
===== ci/psyche/build.sh =====
#!/bin/sh
# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-psyche: cargo test + release build"


1	#!/bin/sh
2	# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
3	# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
4	# [impl->REQ-CI-MANUAL]
5	# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
6	# always announced so "green" never hides un-run coverage).
7	set -u
8	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
9	cd "$ROOT" || exit 2
10	rc=0
11	gate() { printf '\n=== GATE: %s ===\n' "$1"; }
12	fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
13
14	gate "shell-syntax (sh -n)"
15	for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
16	  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
17	done
18
19	gate "unit tests (tests/*.sh)"
20	for t in tests/*.sh; do
21	  [ -e "$t" ] || continue
22	  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
23	done
24
25	gate "digest-extractor (cargo build + test)"
26	if [ -f "$ROOT/ci/digest/build.sh" ]; then
27	  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
28	else
29	  echo "SKIP: no digest extractor build yet"
30	fi
31
32	gate "psyche-runner (cargo build + test)"
33	if [ -f "$ROOT/ci/psyche/build.sh" ]; then
34	  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
35	else
36	  echo "SKIP: no psyche runner build yet"
37	fi
38
39	gate "idle-translate (cargo build + test)"
40	if [ -f "$ROOT/ci/idle-translate/build.sh" ]; then
41	  if sh "$ROOT/ci/idle-translate/build.sh"; then :; else fail "idle-translate"; fi
42	else
43	  echo "SKIP: no idle-translate build yet"
44	fi
45
46	gate "traceable-reqs check (requirement coverage)"
47	if command -v traceable-reqs >/dev/null 2>&1; then
48	  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
49	else
50	  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
51	fi
52
53	gate "skeleton-validate (cplugs installability)"
54	if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
55	  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
56	else
57	  echo "SKIP: no skeleton validator yet"
58	fi
59
60	gate "manifest-schema"
61	if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
62	  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
63	else
64	  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
65	fi
66
67	gate "docs-drift"
68	if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
69	  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
70	else
71	  echo "SKIP: no docs check yet"
72	fi
73
74	printf '\n=== RESULT: %s ===\n' "$([ "$rc" -eq 0 ] && echo PASS || echo FAIL)"
75	exit "$rc"
76

#!/bin/sh
# Deterministic CI gates for spt-claude-code — each a binary pass/fail, no model in the loop.
# Runnable by hand on any fleet host: this IS the manual fallback. [impl->REQ-CI-GATES]
# [impl->REQ-CI-MANUAL]
# Gates whose artifact doesn't exist yet SKIP with a logged note (no silent caps — a skip is
# always announced so "green" never hides un-run coverage).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 2
rc=0
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

gate "shell-syntax (sh -n)"
for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
  if sh -n "$f"; then printf 'ok  %s\n' "$f"; else fail "syntax: $f"; fi
done

gate "unit tests (tests/*.sh)"
for t in tests/*.sh; do
  [ -e "$t" ] || continue
  if sh "$t" >/dev/null 2>&1; then printf 'ok  %s\n' "$t"; else fail "unit: $t"; sh "$t" || true; fi
done

gate "digest-extractor (cargo build + test)"
if [ -f "$ROOT/ci/digest/build.sh" ]; then
  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
else
  echo "SKIP: no digest extractor build yet"
fi

gate "psyche-runner (cargo build + test)"
if [ -f "$ROOT/ci/psyche/build.sh" ]; then
  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
else
  echo "SKIP: no psyche runner build yet"
fi

gate "idle-translate (cargo build + test)"
if [ -f "$ROOT/ci/idle-translate/build.sh" ]; then
  if sh "$ROOT/ci/idle-translate/build.sh"; then :; else fail "idle-translate"; fi
else
  echo "SKIP: no idle-translate build yet"
fi

gate "traceable-reqs check (requirement coverage)"
if command -v traceable-reqs >/dev/null 2>&1; then
  if traceable-reqs check >/dev/null 2>&1; then echo "ok  coverage green"; else fail "traceable-reqs check"; traceable-reqs check || true; fi
else
  echo "SKIP: traceable-reqs not on PATH (install per docs/TRACEABILITY.md)"
fi

gate "skeleton-validate (cplugs installability)"
if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
else
  echo "SKIP: no skeleton validator yet"
fi

gate "manifest-schema"
if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
else
  echo "SKIP: no adapter manifest yet — activates when the CC adapter manifest lands"
fi

gate "docs-drift"
if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi
else
  echo "SKIP: no docs check yet"
fi

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


Both build scripts now target one crate. Make `ci/digest/build.sh` the canonical consolidated build; `ci/psyche/build.sh` a shim (honors the "one build" goal, no redundant cargo run):


Write

#!/bin/sh
# Build + unit-test the CONSOLIDATED claude-spt tool crate (Rust) — the single binary carrying the
# `digest` / `psyche` / `post-update` subcommands (ADR-0006, U2; was three crates). The crate's cargo
# tests ARE the unit coverage for all three: the digest CC-JSONL->NDJSON mapping (drop cases, UTF-8,
# contract invariants), the psyche arg/command construction, the post-update CLI detection + plugin/
# marketplace reconciliation, and the subcommand dispatch. This is the canonical build of the binary;
# ci/psyche/build.sh defers to it (no redundant cargo run). SKIP if cargo is absent so a host without
# the Rust toolchain announces it rather than silently passing (no silent caps).
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
# [impl->REQ-DIST-DIGEST-EXTRACTOR]
# [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)"


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


1	#!/bin/sh
2	# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
3	# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
4	# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
5	# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
6	set -u
7	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
8	CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"
9
10	if ! command -v cargo >/dev/null 2>&1; then
11	  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
12	  exit 0
13	fi
14
15	cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
16	cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
17	echo "ok  claude-spt-digest: cargo test + release build"
18

#!/bin/sh
# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-digest: cargo test + release build"


1	#!/bin/sh
2	# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
3	# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
4	# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
5	# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
6	# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
7	set -u
8	ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
9	CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"
10
11	if ! command -v cargo >/dev/null 2>&1; then
12	  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
13	  exit 0
14	fi
15
16	cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
17	cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
18	echo "ok  claude-spt-psyche: cargo test + release build"
19

#!/bin/sh
# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-psyche: cargo test + release build"


#!/bin/sh
# Build + unit-test the CONSOLIDATED claude-spt tool crate (Rust) — the single binary carrying the
# `digest` / `psyche` / `post-update` subcommands (ADR-0006, U2; was three crates). The crate's cargo
# tests ARE the unit coverage for all three: the digest CC-JSONL->NDJSON mapping (drop cases, UTF-8,
# contract invariants), the psyche arg/command construction, the post-update CLI detection + plugin/
# marketplace reconciliation, and the subcommand dispatch. This is the canonical build of the binary;
# ci/psyche/build.sh defers to it (no redundant cargo run). SKIP if cargo is absent so a host without
# the Rust toolchain announces it rather than silently passing (no silent caps).
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
# [impl->REQ-DIST-DIGEST-EXTRACTOR]
# [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)"


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

#!/bin/sh
# Build + unit-test the CONSOLIDATED claude-spt tool crate (Rust) — the single binary carrying the
# `digest` / `psyche` / `post-update` subcommands (ADR-0006, U2; was three crates). The crate's cargo
# tests ARE the unit coverage for all three: the digest CC-JSONL->NDJSON mapping (drop cases, UTF-8,
# contract invariants), the psyche arg/command construction, the post-update CLI detection + plugin/
# marketplace reconciliation, and the subcommand dispatch. This is the canonical build of the binary;
# ci/psyche/build.sh defers to it (no redundant cargo run). SKIP if cargo is absent so a host without
# the Rust toolchain announces it rather than silently passing (no silent caps).
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
# [impl->REQ-DIST-DIGEST-EXTRACTOR]
# [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)"

-# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the

-# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract

-# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than

-# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]

+# Build + unit-test the CONSOLIDATED claude-spt tool crate (Rust) — the single binary carrying the

+# `digest` / `psyche` / `post-update` subcommands (ADR-0006, U2; was three crates). The crate's cargo

+# tests ARE the unit coverage for all three: the digest CC-JSONL->NDJSON mapping (drop cases, UTF-8,

+# contract invariants), the psyche arg/command construction, the post-update CLI detection + plugin/

+# marketplace reconciliation, and the subcommand dispatch. This is the canonical build of the binary;

+# ci/psyche/build.sh defers to it (no redundant cargo run). SKIP if cargo is absent so a host without

+# the Rust toolchain announces it rather than silently passing (no silent caps).

-  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"

+  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt)"

-echo "ok  claude-spt-digest: cargo test + release build"

+echo "ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)"

#!/bin/sh
# Build + unit-test the claude-spt-digest extractor crate (Rust). The crate's cargo tests ARE the
# extractor's unit coverage (CC-JSONL -> digest-NDJSON mapping, drop cases, UTF-8, contract
# invariants). SKIP if cargo is absent so a host without the Rust toolchain announces it rather than
# silently passing (no silent caps). [impl->REQ-DIST-DIGEST-EXTRACTOR]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-digest/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-digest)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-digest: cargo test + release build"


#!/bin/sh
# The psyche runner is now the `psyche` subcommand of the CONSOLIDATED claude-spt crate (ADR-0006,
# U2) — built + unit-tested by ci/digest/build.sh (the canonical build of the one binary; its cargo
# run covers the psyche arg/command-construction tests too). This gate is kept as a thin shim so the
# run-gates "psyche-runner" slot stays meaningful and announced, WITHOUT a redundant second cargo
# build/test of the same crate ("one build", U2). The runner's resident pulse loop + subprocess spawn
# remain integration (deferred behind the daemon Psyche loop + a real live CC session, REQ-SKILL-LIVE).
# [impl->REQ-SKILL-LIVE]
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
set -u
echo "ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)"


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

#!/bin/sh
# The psyche runner is now the `psyche` subcommand of the CONSOLIDATED claude-spt crate (ADR-0006,
# U2) — built + unit-tested by ci/digest/build.sh (the canonical build of the one binary; its cargo
# run covers the psyche arg/command-construction tests too). This gate is kept as a thin shim so the
# run-gates "psyche-runner" slot stays meaningful and announced, WITHOUT a redundant second cargo
# build/test of the same crate ("one build", U2). The runner's resident pulse loop + subprocess spawn
# remain integration (deferred behind the daemon Psyche loop + a real live CC session, REQ-SKILL-LIVE).
# [impl->REQ-SKILL-LIVE]
# [impl->REQ-DIST-BINARY-CONSOLIDATE]
set -u
echo "ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)"

-# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the

-# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the

-# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +

-# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust

-# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]

+# The psyche runner is now the `psyche` subcommand of the CONSOLIDATED claude-spt crate (ADR-0006,

+# U2) — built + unit-tested by ci/digest/build.sh (the canonical build of the one binary; its cargo

+# run covers the psyche arg/command-construction tests too). This gate is kept as a thin shim so the

+# run-gates "psyche-runner" slot stays meaningful and announced, WITHOUT a redundant second cargo

+# build/test of the same crate ("one build", U2). The runner's resident pulse loop + subprocess spawn

+# remain integration (deferred behind the daemon Psyche loop + a real live CC session, REQ-SKILL-LIVE).

-if ! command -v cargo >/dev/null 2>&1; then

-  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"

-cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }

-cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }

-echo "ok  claude-spt-psyche: cargo test + release build"

+echo "ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)"

#!/bin/sh
# Build + unit-test the claude-spt-psyche runner crate (Rust). The crate's cargo tests ARE the
# runner's unit coverage (arg parse, seed/pulse/poll command construction, blank-pulse guard); the
# resident pulse loop + subprocess spawn are integration (deferred behind the daemon Psyche loop +
# a real live CC session — see REQ-SKILL-LIVE). SKIP if cargo is absent so a host without the Rust
# toolchain announces it rather than silently passing (no silent caps). [impl->REQ-SKILL-LIVE]
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
CRATE="$ROOT/tools/claude-spt-psyche/Cargo.toml"

if ! command -v cargo >/dev/null 2>&1; then
  echo "SKIP: cargo not on PATH (Rust toolchain needed to build/test claude-spt-psyche)"
  exit 0
fi

cargo test  --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo test"; exit 1; }
cargo build --release --quiet --manifest-path "$CRATE" || { echo "FAIL: cargo build --release"; exit 1; }
echo "ok  claude-spt-psyche: cargo test + release build"


Now the manifest seams ([digest].extractor + [session.psyche_init].command) and package-adapter.sh + adapter-archive.sh. Read them:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "===== manifest [digest] + psyche_init seams ====="; grep -nE "extractor|^command = \"claude-spt|claude-spt-digest|claude-spt-psyche" adapter/claude-spt.toml; echo "===== package-adapter.sh binary handling ====="; grep -nE "claude-spt-digest|claude-spt-psyche|cc-spt-idle-translate|claude-spt|BINAR|binary|binaries|bin" ci/publish/package-adapter.sh | head -40

Locate manifest seams + package binary handling


===== manifest [digest] + psyche_init seams =====
4:# `[digest]` extractor / profiles / strings / hints ride here, NOT in the thin cplugs skeleton.
203:# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds
209:command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
298:# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
299:# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits
317:# Session-digest extractor seam. CC's transcript is JSONL (one event per line); the
318:# extractor maps that native log → the published {role,text,tool,ts} digest-record contract,
319:# one JSON record per output line. (CC has a rich log, so the extractor seam fits — a defensible
328:# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
330:# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
333:extractor = "claude-spt-digest --session {session_id} --in {source}"
365:# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers
===== package-adapter.sh binary handling =====
1:#!/bin/sh
8:#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
10:#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
11:#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe
13:#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate
33:MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
40:# The recognized triples and where each platform's release binaries live. A native Windows build lands
43:# non-default layout. The three tool binaries each live in their own crate under tools/.
48:BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"
64:# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
66:echo "== tool binaries =="
85:echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
86:echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
118:echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
119:echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
124:echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"
129:echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."


1	#!/bin/sh
2	# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
3	# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
4	# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
5	# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
6	#
7	#   adapter.spt (tar.gz)
8	#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
9	#   ├── strings/                      ← SHARED, at archive root
10	#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
11	#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe
12	#   └── x86_64-unknown-linux-gnu/
13	#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate
14	#
15	# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
16	# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
17	# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
18	# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
19	#
20	# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
21	# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
22	# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
23	# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
24	# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
25	#
26	# Usage:
27	#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
28	#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
29	set -u
30	HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
31	ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
32	ADAPTER="$ROOT/adapter"
33	MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
34	STRINGS="$ADAPTER/strings"
35	TOOLS="$ROOT/tools"
36	OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
37	APPLY=0
38	[ "${1:-}" = "--apply" ] && APPLY=1
39
40	# The recognized triples and where each platform's release binaries live. A native Windows build lands
41	# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
42	# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
43	# non-default layout. The three tool binaries each live in their own crate under tools/.
44	WIN_TRIPLE=x86_64-pc-windows-msvc
45	LINUX_TRIPLE=x86_64-unknown-linux-gnu
46	WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
47	LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
48	BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"
49
50	# Validate the manifest first — refuse to ship an invalid adapter.
51	echo "== validate manifest =="
52	if ! sh "$ROOT/ci/manifest/check-manifest.sh"; then
53	  echo "REFUSING to package: manifest failed schema validation (fix above, then retry)." >&2
54	  exit 1
55	fi
56
57	# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
58	floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
59	case "$floor" in
60	  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
61	  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
62	esac
63
64	# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
65	echo
66	echo "== tool binaries =="
67	missing=0
68	win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
69	linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
70	for b in $BINS; do
71	  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
72	    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
73	  done
74	done
75	if [ "$missing" -ne 0 ]; then
76	  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2
77	  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
78	  exit 1
79	fi
80
81	echo
82	echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
83	echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
84	echo "strings  : $STRINGS/  ->  (root) strings/"
85	echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
86	echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
87	echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"
88
89	if [ "$APPLY" -ne 1 ]; then
90	  echo
91	  echo "DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it as 'adapter.spt'"
92	  echo "to a GitHub release on the monorepo. End users: 'spt adapter add --release SaberMage/spt-claude-code'"
93	  echo "(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md."
94	  exit 0
95	fi
96
97	# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
98	STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
99	trap 'rm -rf "$STAGE"' EXIT
100	cp "$MANIFEST" "$STAGE/manifest.toml"
101	cp -r "$STRINGS" "$STAGE/strings"
102	mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
103	for b in $BINS; do
104	  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
105	  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
106	done
107
108	mkdir -p "$(dirname "$OUT")" || { echo "FATAL: cannot create $(dirname "$OUT")"; exit 2; }
109	( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings "$WIN_TRIPLE" "$LINUX_TRIPLE" ) \
110	  || { echo "FATAL: tar failed"; exit 2; }
111
112	# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
113	echo
114	echo "== validate archive =="
115	listing=$(tar -tzf "$OUT")
116	fatal=0
117	echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
118	echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
119	echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
120	# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
121	badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
122	[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
123	[ "$fatal" -eq 0 ] || exit 1
124	echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"
125
126	echo
127	echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
128	echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
129	echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
130	exit 0
131

#!/bin/sh
# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
#
#   adapter.spt (tar.gz)
#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
#   ├── strings/                      ← SHARED, at archive root
#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate
#
# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
#
# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# The recognized triples and where each platform's release binaries live. A native Windows build lands
# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
# non-default layout. The three tool binaries each live in their own crate under tools/.
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"

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

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
echo
echo "== tool binaries =="
missing=0
win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
  done
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2
  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "strings  : $STRINGS/  ->  (root) strings/"
echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

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

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
for b in $BINS; do
  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
done

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

# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
echo
echo "== validate archive =="
listing=$(tar -tzf "$OUT")
fatal=0
echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
[ "$fatal" -eq 0 ] || exit 1
echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
exit 0


Per-crate path scheme means new BINS `"claude-spt cc-spt-idle-translate"` works directly. Update package-adapter.sh:


Edit

#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate

#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt · cc-spt-idle-translate


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

#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt · cc-spt-idle-translate

#!/bin/sh
# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
#
#   adapter.spt (tar.gz)
#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
#   ├── strings/                      ← SHARED, at archive root
#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate
#
# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
#
# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# The recognized triples and where each platform's release binaries live. A native Windows build lands
# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
# non-default layout. The three tool binaries each live in their own crate under tools/.
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"

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

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
echo
echo "== tool binaries =="
missing=0
win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
  done
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2
  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "strings  : $STRINGS/  ->  (root) strings/"
echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

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

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
for b in $BINS; do
  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
done

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

# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
echo
echo "== validate archive =="
listing=$(tar -tzf "$OUT")
fatal=0
echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
[ "$fatal" -eq 0 ] || exit 1
echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
exit 0

-#   │   ├── claude-spt-digest.exe · claude-spt-psyche.exe · cc-spt-idle-translate.exe

+#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe

-#       └── claude-spt-digest · claude-spt-psyche · cc-spt-idle-translate

+#       └── claude-spt · cc-spt-idle-translate


# non-default layout. The three tool binaries each live in their own crate under tools/.
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"

# non-default layout. The two tool binaries each live in their own crate under tools/ (the former
# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;
# cc-spt-idle-translate stays separate until doyle ask #3 lets it fold in too — ADR-0006/U2).
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt cc-spt-idle-translate"


# non-default layout. The two tool binaries each live in their own crate under tools/ (the former
# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;
# cc-spt-idle-translate stays separate until doyle ask #3 lets it fold in too — ADR-0006/U2).
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt cc-spt-idle-translate"

#!/bin/sh
# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
#
#   adapter.spt (tar.gz)
#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
#   ├── strings/                      ← SHARED, at archive root
#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt · cc-spt-idle-translate
#
# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
#
# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# The recognized triples and where each platform's release binaries live. A native Windows build lands
# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
# non-default layout. The three tool binaries each live in their own crate under tools/.
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"

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

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
echo
echo "== tool binaries =="
missing=0
win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
  done
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2
  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "strings  : $STRINGS/  ->  (root) strings/"
echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

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

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
for b in $BINS; do
  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
done

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

# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
echo
echo "== validate archive =="
listing=$(tar -tzf "$OUT")
fatal=0
echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
[ "$fatal" -eq 0 ] || exit 1
echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
exit 0

-# non-default layout. The three tool binaries each live in their own crate under tools/.

+# non-default layout. The two tool binaries each live in their own crate under tools/ (the former

+# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;

+# cc-spt-idle-translate stays separate until doyle ask #3 lets it fold in too — ADR-0006/U2).


echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2

echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,idle-translate}/build.sh;" >&2


#!/bin/sh
# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
#
#   adapter.spt (tar.gz)
#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
#   ├── strings/                      ← SHARED, at archive root
#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt · cc-spt-idle-translate
#
# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
#
# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# The recognized triples and where each platform's release binaries live. A native Windows build lands
# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
# non-default layout. The two tool binaries each live in their own crate under tools/ (the former
# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;
# cc-spt-idle-translate stays separate until doyle ask #3 lets it fold in too — ADR-0006/U2).
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt cc-spt-idle-translate"

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

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
echo
echo "== tool binaries =="
missing=0
win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
  done
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2
  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "strings  : $STRINGS/  ->  (root) strings/"
echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

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

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
for b in $BINS; do
  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
done

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

# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
echo
echo "== validate archive =="
listing=$(tar -tzf "$OUT")
fatal=0
echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
[ "$fatal" -eq 0 ] || exit 1
echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
exit 0

-  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,psyche,idle-translate}/build.sh;" >&2

+  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,idle-translate}/build.sh;" >&2


echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }

echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }


echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }

#!/bin/sh
# Pack the SINGLE MULTI-PLATFORM `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` and carried by the `[update] gh_release`
# avenue. ONE fat archive covers every supported platform (ADR-0024 W1, spt-core >= 0.13.2):
# [impl->REQ-DIST-ADAPTER-RELEASE] [impl->REQ-DIST-ADAPTER-PEROS]
#
#   adapter.spt (tar.gz)
#   ├── manifest.toml                 ← SHARED, at archive root (renamed from claude-spt.toml)
#   ├── strings/                      ← SHARED, at archive root
#   ├── x86_64-pc-windows-msvc/       ← this triple's binaries, mirroring the flat-root tree
#   │   ├── claude-spt.exe · cc-spt-idle-translate.exe
#   └── x86_64-unknown-linux-gnu/
#       └── claude-spt · cc-spt-idle-translate
#
# spt-core CLASSIFIES on top-level entry names: a known target-triple dir ⇒ multi-platform; it places
# the shared-root entries + FLATTENS this node's <triple>/* into the install dir (so a bare-name
# command token still resolves at <install_dir>/<program>, REQ-INSTALL-11). This RETIRES the F-014
# per-OS stopgap (the old single-OS `adapter.spt`-by-default that broke on a foreign host).
#
# CONSTRAINTS (doyle, extract_release_archive): only x86_64-pc-windows-msvc + x86_64-unknown-linux-gnu
# are recognized triples — an UNRECOGNIZED dir name is silently treated as a shared-root entry and
# lands flat (no error), so for platforms beyond these two ship a separate single-triple asset via
# `--asset` instead of adding a dir here. A fat archive REQUIRES the manifest's
# min_spt_core_version >= 0.13.2 (older spt-core cannot read it). DRY-RUN by default; never uploads.
#
# Usage:
#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing
#   package-adapter.sh --apply    # write dist/adapter.spt (validated)
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
ADAPTER="$ROOT/adapter"
MANIFEST="$ADAPTER/claude-spt.toml"   # renamed to manifest.toml INSIDE the archive (root-only rule)
STRINGS="$ADAPTER/strings"
TOOLS="$ROOT/tools"
OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# The recognized triples and where each platform's release binaries live. A native Windows build lands
# in target/release; the Linux build is cross-compiled (cargo-zigbuild) into
# target/x86_64-unknown-linux-gnu/release. Override SPTC_WIN_RELSUB / SPTC_LINUX_RELSUB for a
# non-default layout. The two tool binaries each live in their own crate under tools/ (the former
# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;
# cc-spt-idle-translate stays separate until doyle ask #3 lets it fold in too — ADR-0006/U2).
WIN_TRIPLE=x86_64-pc-windows-msvc
LINUX_TRIPLE=x86_64-unknown-linux-gnu
WIN_RELSUB="${SPTC_WIN_RELSUB:-release}"
LINUX_RELSUB="${SPTC_LINUX_RELSUB:-$LINUX_TRIPLE/release}"
BINS="claude-spt cc-spt-idle-translate"

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

# A fat archive is only readable on spt-core >= 0.13.2 — the manifest MUST declare that floor.
floor=$(grep -E '^min_spt_core_version' "$MANIFEST" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
case "$floor" in
  0.13.2|0.13.[3-9]*|0.1[4-9].*|0.[2-9][0-9].*|[1-9].*) : ;;
  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;
esac

# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).
echo
echo "== tool binaries =="
missing=0
win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }
linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }
for b in $BINS; do
  for p in "$(win_path "$b")" "$(linux_path "$b")"; do
    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi
  done
done
if [ "$missing" -ne 0 ]; then
  echo "REFUSING to package: build BOTH platforms first — Windows: sh ci/{digest,idle-translate}/build.sh;" >&2
  echo "Linux (cross): cargo-zigbuild --release --target $LINUX_TRIPLE for each tool crate (see docs/RELEASE-RUNBOOK.md)." >&2
  exit 1
fi

echo
echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "manifest : $MANIFEST  ->  (root) manifest.toml          [min_spt_core $floor]"
echo "strings  : $STRINGS/  ->  (root) strings/"
echo "win bins : -> $WIN_TRIPLE/{$(echo $BINS | tr ' ' ',')}.exe"
echo "linux bins: -> $LINUX_TRIPLE/{$(echo $BINS | tr ' ' ',')}"
echo "asset    : $OUT  (the single fat adapter.spt — auto-resolves the host triple on install)"

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

# Stage the archive ROOT in a temp dir, then tar from it so paths are root-relative (no leading dirs).
STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sptc-adapter.XXXXXX") || { echo "FATAL: mktemp failed"; exit 2; }
trap 'rm -rf "$STAGE"' EXIT
cp "$MANIFEST" "$STAGE/manifest.toml"
cp -r "$STRINGS" "$STAGE/strings"
mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"
for b in $BINS; do
  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"
  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"
done

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

# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.
echo
echo "== validate archive =="
listing=$(tar -tzf "$OUT")
fatal=0
echo "$listing" | grep -qx "manifest.toml" || { echo "FATAL: manifest.toml not at archive root" >&2; fatal=1; }
echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }
echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }
# Guard the footgun: no UNRECOGNIZED top-level dir (would silently flatten as a shared-root entry).
badtop=$(echo "$listing" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
[ -n "$badtop" ] && { echo "FATAL: unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat" >&2; fatal=1; }
[ "$fatal" -eq 0 ] || exit 1
echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

echo
echo "WROTE $OUT (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release"
echo "on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt"
echo "v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed."
exit 0

-echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt-psyche.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }

-echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt-psyche$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }

+echo "$listing" | grep -q "^$WIN_TRIPLE/claude-spt.exe$" || { echo "FATAL: missing $WIN_TRIPLE/ binaries" >&2; fatal=1; }

+echo "$listing" | grep -q "^$LINUX_TRIPLE/claude-spt$"   || { echo "FATAL: missing $LINUX_TRIPLE/ binaries" >&2; fatal=1; }


Now the manifest seams (extractor + psyche_init command) + comment accuracy:


command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"

command = "claude-spt psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"


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

# as ONE argv element (tokenize-then-fill, v0.8.2+); the runner parses --prompt greedily as defense.

-command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"

+command = "claude-spt psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"

keys = ["id", "session_id", "psyche_dir", "psyche_prompt"]


extractor = "claude-spt-digest --session {session_id} --in {source}"

extractor = "claude-spt digest --session {session_id} --in {source}"


# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a

# Rust binary) ships in the spt-core-conducted layer; this is its declaration.

-extractor = "claude-spt-digest --session {session_id} --in {source}"

+extractor = "claude-spt digest --session {session_id} --in {source}"


Update manifest comment accuracy (old crate-name refs). Re-locate them:


203:# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds
298:# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
299:# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits
330:# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
365:# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers


# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds

# The runner (`claude-spt psyche`, the psyche subcommand of the consolidated tools/claude-spt crate —
# ADR-0006/U2) is a resident wrapper — `claude -p` is one-shot, so it seeds


# The runner (`claude-spt psyche`, the psyche subcommand of the consolidated tools/claude-spt crate —
# ADR-0006/U2) is a resident wrapper — `claude -p` is one-shot, so it seeds

# {session_name} (a [session.self] fill) and NOT {psyche_context} (the resume-preload seam, not first

# spawn). `detach=true` + cwd={psyche_dir}: the Psyche is fire-and-forget detached (Stdio::null,

# unsupervised), owns its `<parent>-psyche` perch, authors commune-drops only, exits at session end.

-# The runner (claude-spt-psyche, tools/) is a resident wrapper — `claude -p` is one-shot, so it seeds

+# The runner (`claude-spt psyche`, the psyche subcommand of the consolidated tools/claude-spt crate —

+# ADR-0006/U2) is a resident wrapper — `claude -p` is one-shot, so it seeds

# the Psyche `claude` session once from {psyche_prompt} then drives one `claude --continue` turn per

# daemon pulse (NOT a bare `claude -p`), the claude authoring commune drops. spt-core fills each {key}


# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# `path` = the binary's bare name (resolved from the adapter install dir like the consolidated
# claude-spt binary; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits


# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /
# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# `path` = the binary's bare name (resolved from the adapter install dir like the consolidated
# claude-spt binary; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# spt-hosted complement to the [inject] hook channel above: [inject] handles BUSY/mid-turn delivery

# (UPS additionalContext); this handles IDLE delivery into a session sitting at its input box.

-# `path` = the binary's bare name (resolved from the adapter install dir like claude-spt-digest /

-# claude-spt-psyche; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

+# `path` = the binary's bare name (resolved from the adapter install dir like the consolidated

+# claude-spt binary; it packs into adapter.spt). The binary (tools/cc-spt-idle-translate, Rust) emits

# the operator-specified choreography per message — ctrl+s (stash draft) · 50ms · <envelope>\r

# (submit line) · {"commit":true} (terminate sequence) — so an inbound message never clobbers a

# half-typed draft (CC AUTO-RESTORES the stashed draft after submit, so no trailing restore keystroke).


329	# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
330	# within the root itself (the slug is the harness's own business). This matches the published example
331	# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
332	# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
333	[digest]
334	extractor = "claude-spt digest --session {session_id} --in {source}"

# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`
# within the root itself (the slug is the harness's own business). This matches the published example
# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
# Rust binary) ships in the spt-core-conducted layer; this is its declaration.
[digest]
extractor = "claude-spt digest --session {session_id} --in {source}"


# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
# Rust binary) ships in the spt-core-conducted layer; this is its declaration.

# shape and what `spt endpoint digest` fills at runtime. The extractor (`claude-spt digest`, the
# digest subcommand of the consolidated tools/claude-spt Rust crate — ADR-0006/U2) ships in the
# spt-core-conducted layer; this is its declaration.


# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a
# Rust binary) ships in the spt-core-conducted layer; this is its declaration.

# shape and what `spt endpoint digest` fills at runtime. The extractor (`claude-spt digest`, the
# digest subcommand of the consolidated tools/claude-spt Rust crate — ADR-0006/U2) ships in the
# spt-core-conducted layer; this is its declaration.

# So `source` names the per-project ROOT dir (`~` for home expansion — {home} is NOT a catalog key)

# and the extractor receives `{session_id}` on the COMMAND and resolves `<slug>/<session_id>.jsonl`

# within the root itself (the slug is the harness's own business). This matches the published example

-# shape and what `spt endpoint digest` fills at runtime. The extractor (tools/claude-spt-digest, a

-# Rust binary) ships in the spt-core-conducted layer; this is its declaration.

+# shape and what `spt endpoint digest` fills at runtime. The extractor (`claude-spt digest`, the

+# digest subcommand of the consolidated tools/claude-spt Rust crate — ADR-0006/U2) ships in the

+# spt-core-conducted layer; this is its declaration.

extractor = "claude-spt digest --session {session_id} --in {source}"


# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers

# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (`claude-spt digest` prefers


# NO `[profiles.ccs.digest]` leaf by design: ccs relocates Claude Code's transcript tree via the

# CLAUDE_CONFIG_DIR env var (e.g. ~/.ccs/instances/<account>/.claude), whose value is set by ccs at

# runtime per-account and is NOT expressible as a static catalog path. The "per-profile ~/.ccs log

-# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (claude-spt-digest prefers

+# dir" SCOPE requirement is therefore honored IN THE EXTRACTOR (`claude-spt digest` prefers

# $CLAUDE_CONFIG_DIR/projects over the base ~/.claude/projects on its dir-locate branch — the

# owl-validated owlery::claude_projects_root pattern), so the base [digest] config serves both base

# and ccs sessions transparently. That env-aware resolver is REQ-CCS-PROFILES's impl evidence.


Now the proof-int + adapter-archive test references. Check both for old crate/binary names:


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -rnE "claude-spt-digest|claude-spt-psyche" tests/ ci/ adapter/ docs/ 2>/dev/null | grep -v "0006\|adr/0006"

Find remaining old crate-name references


tests/adapter-archive.sh:14:BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"
tests/manifest-shortcut.sh:30:# `command =` lines whose value is a bare claude/ccs CC spawn — NOT `claude-spt-psyche` (the runner,
tests/manifest-shortcut.sh:31:# whose own turns are asserted in tools/claude-spt-psyche unit tests).
ci/digest/digest-proof-int.sh:22:RELDIR="$ROOT/tools/claude-spt-digest/target/release"
ci/digest/digest-proof-int.sh:32:[ -x "$RELDIR/claude-spt-digest" ] || [ -x "$RELDIR/claude-spt-digest.exe" ] || { echo "SKIP: extractor not built (run sh ci/digest/build.sh)"; exit 0; }
ci/launcher/bind-int.sh:46:  for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
ci/psyche/build.sh:11:echo "ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)"
ci/psyche/live-relay-int.sh:14:# hosts it by spawning claude-spt-psyche (resolved by bare name from the adapter install dir) — assert
ci/psyche/live-relay-int.sh:18:# claude-spt-psyche unit tests (ci/psyche/build.sh).
ci/psyche/live-relay-int.sh:20:# PSYCHE-SPAWN binary resolution: the psyche_init command invokes `claude-spt-psyche` by bare name.
ci/psyche/live-relay-int.sh:26:# Spawns a real claude-spt-psyche (which launches a headless claude) + mutates node-local perch state,
ci/psyche/live-relay-int.sh:64:  command -v claude-spt-psyche >/dev/null 2>&1 && return 0
ci/psyche/live-relay-int.sh:78:  for p in $(tasklist 2>/dev/null | grep -i claude-spt-psyche | awk '{print $2}'); do taskkill //PID "$p" //T //F >/dev/null 2>&1; done
ci/psyche/live-relay-int.sh:104:    # listen child's stderr. The bare `claude-spt-psyche` resolves via the F-006 PATH interim here.
ci/psyche/live-relay-int.sh:119:    # spawned claude-spt-psyche AND the runner stays RESIDENT. Two bugs were in the way: (1) spt-core
ci/psyche/live-relay-int.sh:130:    resident() { tasklist 2>/dev/null | grep -qi "claude-spt-psyche"; }
ci/psyche/live-relay-int.sh:132:    procs=$(tasklist 2>/dev/null | grep -ci "claude-spt-psyche")
ci/psyche/live-relay-int.sh:135:      ok "Psyche daemon-hosted: claude-spt-psyche runner RESIDENT for $ID-psyche (v0.8.1 livehost + greedy-prompt fix; REQ-INSTALL-11 install-dir resolution proven)"
ci/psyche/live-relay-int.sh:137:      bad "psyche-spawn: no resident claude-spt-psyche for $ID (v0.8.1 host gap or prompt-split regression); nested=$nested procs=$procs"
ci/publish/package-adapter.sh:44:# claude-spt-digest + claude-spt-psyche crates are now the digest/psyche subcommands of claude-spt;
ci/subnet/multi-subnet-bringup-int.sh:56:    for p in $(wmic process where "name='claude-spt-psyche.exe' and commandline like '%$C3_ID%'" get processid 2>/dev/null | tr -dc '0-9 \n' | tr ' ' '\n' | grep -E '^[0-9]+$'); do
adapter/strings/skills/setup.md:35:   `claude-spt-digest` + `claude-spt-psyche` tools are invoked by bare name and resolve **from the
docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md:12:hosts alongside the parent agent. The daemon launches our `claude-spt-psyche` runner **detached,
docs/adr/0003-psyche-is-a-constrained-auto-approving-companion.md:37:legacy owl. `claude-spt-psyche` appends this flag set (`sandbox_flags()`) to both `seed_cmd` and
docs/KNOWN-HAZARDS.md:92:  (`claude-spt-psyche`, launched **detached** with `Stdio::null`) and the `[session.self]` bringup
docs/KNOWN-HAZARDS.md:98:  with `--dangerously-skip-permissions`. Concretely: each `claude-spt-psyche` turn (seed **and**
docs/KNOWN-HAZARDS.md:103:- **Mapping / notes:** `tools/claude-spt-psyche/src/main.rs` `sandbox_flags()` appends the flag (and
docs/PARITY.md:38:`shortcut_basename="cc"`); echo-commune→`[digest]` extractor (`claude-spt-digest`); psyche-sync→
docs/PARITY.md:64:  is therefore honored **in `claude-spt-digest`** (dir-locate branch prefers `$CLAUDE_CONFIG_DIR/
docs/RELEASE-RUNBOOK.md:150:  dir**, so the bare-name command tokens (`claude-spt-digest`, …) still resolve at `<install_dir>/`
docs/RELEASE-RUNBOOK.md:167:    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-digest/Cargo.toml
docs/RELEASE-RUNBOOK.md:168:    cargo zigbuild --release --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt-psyche/Cargo.toml
docs/RELEASE-RUNBOOK.md:191:  dir (`…/adapters/_github/<safe>/`). The command templates use **bare names** (`claude-spt-digest`,
docs/RELEASE-RUNBOOK.md:192:  `claude-spt-psyche`), which spt-core resolves **from that install dir** (v0.8.0 Feature B /
docs/SPT-CORE-FINDINGS.md:284:extractor = "claude-spt-digest --session {session_id} --in {source}"
docs/SPT-CORE-FINDINGS.md:480:`C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code\claude-spt-digest.exe`
docs/SPT-CORE-FINDINGS.md:481:+ `…\claude-spt-psyche.exe` (note: the `_github/<safe>` dir even for `--release`). But the registered
docs/SPT-CORE-FINDINGS.md:485:extractor = "claude-spt-digest --session {session_id} --in {source}"
docs/SPT-CORE-FINDINGS.md:486:command   = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
docs/SPT-CORE-FINDINGS.md:490:claude-spt-digest` / `-psyche` both **MISS**. Registration/activation succeed, but runtime
docs/SPT-CORE-FINDINGS.md:497:`claude-spt-digest --help` run. Unblocks runtime today.
docs/SPT-CORE-FINDINGS.md:509:- **psyche** — the daemon-hosted Psyche resolved + spawned `claude-spt-psyche` from the install dir
docs/SPT-CORE-FINDINGS.md:591:  - Binary resolution: the psyche_init command's bare `claude-spt-psyche` resolves via the F-006 PATH
docs/SPT-CORE-FINDINGS.md:691:0). Root cause: `claude-spt-psyche.exe` was **running** (pid for the live `wall-a` agent's Psyche), so
docs/SPT-CORE-FINDINGS.md:693:(install-dir `manifest.toml` = 0.5.0, `strings/briefs/` present + resolving, `claude-spt-digest.exe`
docs/SPT-CORE-FINDINGS.md:694:swapped); only the locked `claude-spt-psyche.exe` stayed the old build. spt-core reports the whole
docs/SPT-CORE-FINDINGS.md:709:    node SPT_DEV+BIGNET). Daemon hosted the Psyche; running `claude-spt-psyche.exe`
docs/SPT-CORE-FINDINGS.md:710:    ExecutablePath = `…\spt-core\owlery\w3accept\.live-bin\claude-spt-psyche.exe` — the perch
docs/SPT-CORE-FINDINGS.md:713:    open of `…\adapters\_github\…\claude-spt-psyche.exe` SUCCEEDED — the install copy is unlocked,
docs/SPT-CORE-FINDINGS.md:717:    `claude-spt-psyche.exe` process count went 1 → 0. No brain-less orphan left behind.
docs/SPT-CORE-FINDINGS.md:728:  the daemon spawned `claude-spt-psyche` but it exited instantly (phantom pid, `status=online`, no
docs/SPT-CORE-FINDINGS.md:802:  (mirrors the existing `claude-spt-psyche` runner) that captures `$1` as the endpoint id, exports
docs/SPT-CORE-FINDINGS.md:865:command = "claude-spt-psyche --id {id} --session-id {session_id} --prompt {psyche_prompt}"
docs/SPT-CORE-FINDINGS.md:870:`{psyche_prompt}` is always a sentence; argv-capture (instrumented `claude-spt-psyche` dumping its raw
docs/SPT-CORE-FINDINGS.md:884:**Adapter workaround (shipped, ddf1965).** `claude-spt-psyche` parses `--prompt` **greedily/terminally**


Many references — some operative (must fix), some historical findings-log (leave as record). Read the operative test/int files to update:


1	#!/bin/sh
2	# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
3	# Asserts the MULTI-PLATFORM fat-archive invariants `spt adapter add --release` depends on (ADR-0024
4	# W1, spt-core >= 0.13.2): one .spt holds the SHARED manifest.toml (named exactly) + strings/ at the
5	# archive ROOT, plus each recognized target-triple's binaries under a <triple>/ dir mirroring the
6	# flat-root tree. spt-core classifies on the top-level triple dir and flattens this node's triple into
7	# the install dir, so a bare-name command token still resolves. [unit->REQ-DIST-ADAPTER-RELEASE]
8	# [unit->REQ-DIST-ADAPTER-PEROS]
9	set -u
10	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
11	PACKER="$ROOT/ci/publish/package-adapter.sh"
12	WIN_TRIPLE=x86_64-pc-windows-msvc
13	LINUX_TRIPLE=x86_64-unknown-linux-gnu
14	BINS="claude-spt-digest claude-spt-psyche cc-spt-idle-translate"
15	rc=0
16	fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
17
18	# The fat archive needs BOTH platforms' release binaries. Probe both triples.
19	have_all=1
20	for b in $BINS; do
21	  [ -f "$ROOT/tools/$b/target/release/$b.exe" ] || have_all=0
22	  [ -f "$ROOT/tools/$b/target/$LINUX_TRIPLE/release/$b" ] || have_all=0
23	done
24
25	if [ "$have_all" -ne 1 ]; then
26	  # Missing a platform → assert the packer's REFUSE guard fires (no silent pass), then skip the
27	  # archive-build assertion (no hidden coverage gap). Both platforms are required for a fat archive.
28	  if sh "$PACKER" --apply >/dev/null 2>&1; then
29	    fail "packer should refuse (exit!=0) when a platform's binaries are absent"
30	  else
31	    echo "ok   packer refuses --apply when a platform's binaries absent (guard works)"
32	  fi
33	  echo "SKIP: archive-build assertion — need BOTH platforms (win native + linux cross-build) present"
34	  exit "$rc"
35	fi
36
37	# Both platforms present → build a real fat archive to a temp file and assert its structure.
38	TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
39	trap 'rm -rf "$TMP"' EXIT
40	OUT="$TMP/adapter.spt"
41
42	if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
43	  echo "ok   packer --apply succeeded"
44	else
45	  fail "packer --apply exited non-zero"
46	  exit "$rc"
47	fi
48	[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }
49
50	LIST=$(tar -tzf "$OUT")
51	# SHARED manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
52	echo "$LIST" | grep -qx "manifest.toml" \
53	  && echo "ok   shared manifest.toml at archive root" \
54	  || fail "manifest.toml not at archive root (add --release would reject)"
55	# SHARED strings/ at root (not under a triple).
56	echo "$LIST" | grep -q "^strings/" \
57	  && echo "ok   shared strings/ at root" \
58	  || fail "strings/ missing from archive root"
59	# Each triple carries all three binaries, under its <triple>/ dir, mirroring the flat-root tree.
60	for b in $BINS; do
61	  echo "$LIST" | grep -qx "$WIN_TRIPLE/$b.exe" \
62	    && echo "ok   $WIN_TRIPLE/$b.exe present" \
63	    || fail "$WIN_TRIPLE/$b.exe missing from archive"
64	  echo "$LIST" | grep -qx "$LINUX_TRIPLE/$b" \
65	    && echo "ok   $LINUX_TRIPLE/$b present" \
66	    || fail "$LINUX_TRIPLE/$b missing from archive"
67	done
68	# Negative: the SHARED files must NOT sit under a triple (would not be shared), and no nested wrapper.
69	if echo "$LIST" | grep -qE "^$WIN_TRIPLE/(manifest\.toml|strings/)"; then
70	  fail "manifest/strings duplicated under a triple — they must be shared at root only"
71	else
72	  echo "ok   manifest/strings not duplicated under a triple"
73	fi
74	# Negative: no UNRECOGNIZED top-level dir (spt-core would silently flatten it as a shared-root entry).
75	badtop=$(echo "$LIST" | awk -F/ 'NF>1 {print $1}' | sort -u | grep -vE "^(strings|$WIN_TRIPLE|$LINUX_TRIPLE)$" || true)
76	if [ -n "$badtop" ]; then
77	  fail "unrecognized top-level dir(s) [$badtop] — spt-core would mis-place them flat (silent footgun)"
78	else
79	  echo "ok   no stray/unrecognized top-level dirs (only strings/ + the two recognized triples)"
80	fi
81
82	# A fat archive REQUIRES min_spt_core_version >= 0.13.2 — the packer must refuse a lower floor.
83	# (Smoke the guard by reading the dry-run plan's advertised floor.)
84	plan=$(sh "$PACKER" 2>&1)
85	echo "$plan" | grep -qE "min_spt_core 0\.(1[3-9]|[2-9][0-9])" \
86	  && echo "ok   packer advertises a fat-capable floor (>= 0.13.2)" \
87	  || fail "packer floor advertisement missing/too low; plan=[$plan]"
88
89	[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
90	exit "$rc"
91

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

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

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

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

if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
  echo "ok   packer --apply succeeded"
else
  fail "packer --apply exited non-zero"
  exit "$rc"
fi
[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }

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

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

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


Edit

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