cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== win binaries (target/release) ==="; ls tools/*/target/release/*.exe 2>&1 | grep -vE '\.d$'; echo "=== linux binaries (target/x86_64-unknown-linux-gnu/release) ==="; ls tools/*/target/x86_64-unknown-linux-gnu/release/ 2>&1 | grep -vE '\.d$|^$' | head; echo "=== manifest version + floor ==="; grep -n -E 'min_spt_core_version|^version = ' adapter/claude-spt.toml; echo "=== dist .spt ==="; ls -la dist/

Check binary availability + manifest floor + dist


=== win binaries (target/release) ===
tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe
tools/claude-spt-digest/target/release/claude-spt-digest.exe
tools/claude-spt-psyche/target/release/claude-spt-psyche.exe
=== linux binaries (target/x86_64-unknown-linux-gnu/release) ===
tools/cc-spt-idle-translate/target/x86_64-unknown-linux-gnu/release/:
build
cc-spt-idle-translate
deps
examples
incremental
tools/claude-spt-digest/target/x86_64-unknown-linux-gnu/release/:
build
claude-spt-digest
deps
=== manifest version + floor ===
35:# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays
38:version = "0.6.0"
43:min_spt_core_version = "0.9.0"
333:version = { file = "skills/version.md" }
=== dist .spt ===
total 1436
drwxr-xr-x 1 decid 197609      0 Jun 20 05:32 .
drwxr-xr-x 1 decid 197609      0 Jun 22 03:40 ..
-rw-r--r-- 1 decid 197609 699080 Jun 22 03:41 adapter-linux-x86_64.spt
-rw-r--r-- 1 decid 197609 380568 Jun 22 03:40 adapter-windows-x86_64.spt
-rw-r--r-- 1 decid 197609 380568 Jun 22 03:41 adapter.spt


go ahead and apply (C) to the existing release


Both platforms' binaries are present locally (win `.exe` in `target/release`, linux ELF in `target/x86_64-unknown-linux-gnu/release`) — I can build the fat `.spt` directly. First bump the manifest floor (mandatory for fat). Read the floor section to edit precisely.


26	# [impl->REQ-DIST-MANIFEST-SCHEMA]
27
28	[adapter]
29	name = "claude-spt"
30	kind = "harness"
31	# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
32	# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
33	# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
34	# 2026-06-21). The new seams are ADDITIVE — older spt-core (<0.13.0) ignores both tables (resume
35	# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays
36	# 0.9.0 (a 0.13.0 floor would needlessly block the ready/live flows on older nodes — same additive
37	# philosophy as host_binaries / F-013).
38	version = "0.6.0"
39	# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
40	# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
41	# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
42	# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
43	min_spt_core_version = "0.9.0"
44	hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
45	# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
46	# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
47	# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
48	# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
49	# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
50	# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
51	host_binaries = ["claude"]
52	# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
53	# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
54	# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
55	# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
56	# (unit->REQ-DIST-SHORTCUT-BASENAME).
57	shortcut_basename = "cc"
58
59	# ── [update] ───────────────────────────────────────────────────────────────────────────────────
60	# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
61	# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
62	# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
63	# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
64	# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
65	[update]
66	avenue = "gh_release"
67	repo = "SaberMage/spt-claude-code"
68

# [impl->REQ-DIST-MANIFEST-SCHEMA]

[adapter]
name = "claude-spt"
kind = "harness"
# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
# 2026-06-21). The new seams are ADDITIVE — older spt-core (<0.13.0) ignores both tables (resume
# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays
# 0.9.0 (a 0.13.0 floor would needlessly block the ready/live flows on older nodes — same additive
# philosophy as host_binaries / F-013).
version = "0.6.0"
# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
min_spt_core_version = "0.9.0"
hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]
# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for
# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking
# parent pid → exe basename → host_binaries match → active-profile pointer → registered_at_ms
# fallback (CONTEXT §170-186). Additive + N-1-safe: harmless before spt-core reads it (the
# vendored v0.7.0 schema's [adapter] allows additional props), live once v0.9.0 + `spt adapter use`
# land. Claude Code's host process basename is `claude`. [impl->REQ-SKILL-LIVE]
host_binaries = ["claude"]
# Brands the `spt endpoint run` picker shortcut as `cc-<id>` (default would be `spt-<id>`) — this is
# the M12 `cc` launcher, and matches the schema's own example (spt-claude-code → `cc`). The launcher
# brand is DECOUPLED from the plugin name (`sptc`, skills `/sptc:*`): the s/sptc/spt/ succession seam
# (ADR-0001) renames the plugin, not this shortcut. Guarded by tests/manifest-shortcut.sh
# (unit->REQ-DIST-SHORTCUT-BASENAME).
shortcut_basename = "cc"

# ── [update] ───────────────────────────────────────────────────────────────────────────────────
# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]
[update]
avenue = "gh_release"
repo = "SaberMage/spt-claude-code"


Edit the manifest floor → 0.13.2 (mandatory for the fat archive) with the reasoning, and update the `[update]`/version comments to reflect dropping the F-014 stopgap.


Edit

# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
# 2026-06-21). The new seams are ADDITIVE — older spt-core (<0.13.0) ignores both tables (resume
# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays
# 0.9.0 (a 0.13.0 floor would needlessly block the ready/live flows on older nodes — same additive
# philosophy as host_binaries / F-013).
version = "0.6.0"
# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
min_spt_core_version = "0.9.0"

# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
# 2026-06-21). The seams themselves are ADDITIVE, but the v0.6.0 release now ships a single
# MULTI-PLATFORM fat .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract
# — so the floor moves to 0.13.2 (see below). Packaging-only modernization; the [adapter].version
# stays 0.6.0 (the seam surface is unchanged).
version = "0.6.0"
# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
min_spt_core_version = "0.13.2"


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)

# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
# 2026-06-21). The new seams are ADDITIVE — older spt-core (<0.13.0) ignores both tables (resume
# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays
# 0.9.0 (a 0.13.0 floor would needlessly block the ready/live flows on older nodes — same additive
# philosophy as host_binaries / F-013).
version = "0.6.0"
# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's
# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).
# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires
# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.
min_spt_core_version = "0.9.0"

# 0.6.0 (2026-06-21): adds the v0.13.0 harness-contract RUN-group seams [session.resume] +
# [message-idle-translation-binary]. Bumped off 0.5.0 so nothing that resolves a manifest by VERSION
# (rather than by registered path) can shadow-pick the pre-seam 0.5.0 _github copy (doyle note,
# 2026-06-21). The seams themselves are ADDITIVE, but the v0.6.0 release now ships a single
# MULTI-PLATFORM fat .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract
# — so the floor moves to 0.13.2 (see below). Packaging-only modernization; the [adapter].version
# stays 0.6.0 (the seam surface is unchanged).
version = "0.6.0"
# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS
# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that
# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify
# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name
# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset
# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the
# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)
min_spt_core_version = "0.13.2"

-# 2026-06-21). The new seams are ADDITIVE — older spt-core (<0.13.0) ignores both tables (resume

-# falls back to [session.self]; the idle-translation table is unread), so min_spt_core_version stays

-# 0.9.0 (a 0.13.0 floor would needlessly block the ready/live flows on older nodes — same additive

-# philosophy as host_binaries / F-013).

+# 2026-06-21). The seams themselves are ADDITIVE, but the v0.6.0 release now ships a single

+# MULTI-PLATFORM fat .spt (per-triple subdirs, ADR-0024 W1) which only spt-core >= 0.13.2 can extract

+# — so the floor moves to 0.13.2 (see below). Packaging-only modernization; the [adapter].version

+# stays 0.6.0 (the seam surface is unchanged).

-# Floor BUMPED to 0.9.0 (2026-06-17, PREP-4): the legacy-parity bringup requires spt-core's

-# adapter-agnostic seed + bind-time resolution by [adapter] host_binaries (shipped v0.9.0).

-# On <0.9.0 `api seed`/`listen` still mandate --adapter, so the bare flow this manifest now wires

-# would break — the compat gate refuses the adapter on older spt-core rather than misbehave.

+# Floor BUMPED to 0.13.2 (2026-06-22, C / F-014 drop): the release packaging moved from the per-OS

+# stopgap (`adapter.spt` = a windows-binary copy — F-014; broke on a linux host) to ONE fat .spt that

+# bundles both x86_64 triples beside a shared manifest. spt-core extracts a fat archive (classify

+# top-level triple dirs → flatten this node's triple into the install dir, preserving bare-name

+# resolution) ONLY on >= 0.13.2 (doyle, extract_release_archive); an older node cannot read the asset

+# at all, so declaring the floor is honest, not a needless block. (Prior floor 0.9.0 was the

+# adapter-agnostic seed/listen-by-host_binaries requirement, PREP-4 — still satisfied, just superseded.)

hostable_types = ["LiveAgent", "ReadyAgent", "Worker"]

# host_binaries — STAGED ahead of spt-core v0.9.0 (doyle 2026-06-16). The pid→binary match-key for

# adapter-agnostic resolution: `spt api listen <id>` resolves the owning adapter/profile by walking


# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]

# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
# [impl->REQ-DIST-ADAPTER-RELEASE]


# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged
# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]

# Adapter self-update via OUR GitHub releases (gh_release avenue, spt v0.8.0). `spt adapter update`
# pulls the adapter `.spt` asset from this repo's releases through the same `--release` machinery —
# no signing infra (closes the F-005 file_pull/delegated blockers). `asset` omitted ⇒ spt's default
# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared
# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014
# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a
# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.
# [impl->REQ-DIST-ADAPTER-RELEASE]

-# (adapter.spt); the per-OS auto-update asset selection (REQ-DIST-ADAPTER-PEROS) is a flagged

-# question to doyle, so we do not pin a single-OS asset here. [impl->REQ-DIST-ADAPTER-RELEASE]

+# (adapter.spt), which is now the SINGLE MULTI-PLATFORM fat archive (both x86_64 triples + shared

+# manifest, ADR-0024 W1) — so one asset name auto-resolves the host's binaries. This RETIRES the F-014

+# per-OS stopgap (the old `asset`-less default fetched a windows-only `adapter.spt` that broke on a

+# linux host); no `{os}/{arch}` placeholder is needed since the fat archive is host-agnostic.


Manifest floor + comments updated. Now rewrite `package-adapter.sh` as the fat-archive packer (shared root + two triple subdirs).


Write

#!/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 its 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


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


<owl_messages>
<owl_message from="doyle" priority="high">Correct on every point — including the root-binary nuance: your flat layout has binaries at root, so under each triple it's <triple>/claude-spt-digest.exe (no bin/), and the flatten lands them at install_dir root → bare-name resolves. The dogfood plan is exactly right: build fat .spt → install on the win node → confirm spt-core flattens the win triple + manifest 0.6.0 resolves + translate-proof OK, BEFORE any upload. That local install IS the real-extract proof. Good call simplifying both proof-int scripts to --dir/--manifest (dropping the soft-remove-the-real-adapter staging — that was the F-011-adjacent footgun). Ping on repack result; I'm landing the published-doc layout fix in parallel.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


1	#!/bin/sh
2	# Pack the `adapter.spt` release asset — the end-user distribution acquired by
3	# `spt adapter add --release SaberMage/spt-claude-code` (doyle's --release source, spt v0.7.3+).
4	# The archive ROOT must hold `manifest.toml` (named EXACTLY that) + `strings/` + the tool binaries;
5	# spt-core fetches it from the repo's GitHub release, extracts to the durable home, registers the

#!/bin/sh
# Pack the `adapter.spt` release asset — the end-user distribution acquired by
# `spt adapter add --release SaberMage/spt-claude-code` (doyle's --release source, spt v0.7.3+).
# The archive ROOT must hold `manifest.toml` (named EXACTLY that) + `strings/` + the tool binaries;
# spt-core fetches it from the repo's GitHub release, extracts to the durable home, registers the


doyle confirms the plan on every point. Quick ACK, then continue the repack.


Bash

$OWL send doyle <<'EOF'
perri — ACK, locked. <triple>/claude-spt-*.exe (no bin/), flatten → install_dir root → bare-name resolves. Local install on the win node = the real-extract proof; doing that before any upload. Manifest floor already bumped to 0.13.2 (mandatory for fat), version stays 0.6.0 (packaging-only). Rewriting the packer now → fat .spt → dogfood install → gates → then I hold at the outward release step for the operator. Ping incoming with the repack result.
EOF
echo OK

ACK doyle plan confirmation


➜ SENT:doyle
OK


Now write the fat-archive packer.


Write

#!/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


<owl_messages>
<owl_message from="doyle" priority="high">Locked, all correct — and holding the OUTWARD release step for the operator is exactly right (that's the standing rule: outward-facing actions need operator GO). Version 0.6.0 stays (packaging-only, not a feature bump) ✓, floor 0.13.2 mandatory ✓. Dogfood-install-then-gates-then-hold is the right sequence. Ping me the repack+dogfood result; I'll relay the publish-readiness to the operator when you're at the hold.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


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)

#!/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

-# Pack the `adapter.spt` release asset — the end-user distribution acquired by

-# `spt adapter add --release SaberMage/spt-claude-code` (doyle's --release source, spt v0.7.3+).

-# The archive ROOT must hold `manifest.toml` (named EXACTLY that) + `strings/` + the tool binaries;

-# spt-core fetches it from the repo's GitHub release, extracts to the durable home, registers the

-# root. DRY-RUN by default. Never uploads — the release/tag/upload is the operator's step.

+# 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):

+#   ├── 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

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

#   package-adapter.sh            # dry-run: validate + print the archive plan, write nothing

#   package-adapter.sh --apply    # write dist/adapter.spt (validated)

+OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/adapter.spt}"   # overridable so the unit test writes to a tmp file

-# PER-OS asset name: the .spt carries native binaries, so it is platform-specific and ships as

-# `adapter-<os>-<arch>.spt`. /sptc:setup acquires the matching one via spt's caller-named `--asset`.

-# Host-derived from uname; override SPTC_OS/SPTC_ARCH for a cross build / CI. [impl->REQ-DIST-ADAPTER-PEROS]

-if [ -z "$SPTC_OS" ]; then case "$(uname -s 2>/dev/null)" in

-  MINGW*|MSYS*|CYGWIN*|Windows*) SPTC_OS=windows ;;

-if [ -z "$SPTC_ARCH" ]; then case "$(uname -m 2>/dev/null)" in

-  *) SPTC_ARCH="$(uname -m 2>/dev/null || echo unknown)" ;;

-# Native executable suffix, keyed off the DECLARED os (windows → .exe, else bare) — the archive

-# carries the binary under its native name so a bare-name template (`claude-spt-digest …`) resolves.

-case "$SPTC_OS" in windows) EXE=".exe" ;; *) EXE="" ;; esac

-# Binary location: a cross build sets SPTC_TARGET (e.g. x86_64-unknown-linux-gnu → cargo-zigbuild) and

-# the binaries land under target/<triple>/release; a native build uses target/release.

-if [ -n "${SPTC_TARGET:-}" ]; then RELSUB="$SPTC_TARGET/release"; else RELSUB="release"; fi

-OUT="${ADAPTER_SPT_OUT:-$ROOT/dist/$ASSET}"   # overridable so the unit test writes to a tmp file

+# 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/.

# Validate the manifest first — refuse to ship an invalid adapter.

-# Require the release binaries (NOT auto-copied by register — they must ride the archive; doyle).

+# 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/')

+  *) echo "REFUSING to package: min_spt_core_version is '$floor' but a multi-platform fat .spt needs >= 0.13.2 (doyle)." >&2; exit 1 ;;

+# Require BOTH platforms' release binaries (NOT auto-copied by register — they ride the archive).

-for b in "$DIGEST" "$PSYCHE" "$IDLE"; do

-  if [ -f "$b" ]; then echo "  ok    $b"; else echo "  MISS  $b"; missing=1; fi

+win_path()   { echo "$TOOLS/$1/target/$WIN_RELSUB/$1.exe"; }

+linux_path() { echo "$TOOLS/$1/target/$LINUX_RELSUB/$1"; }

+  for p in "$(win_path "$b")" "$(linux_path "$b")"; do

+    if [ -f "$p" ]; then echo "  ok    $p"; else echo "  MISS  $p"; missing=1; fi

-  echo "REFUSING to package: build the release binaries first — sh ci/digest/build.sh && sh ci/psyche/build.sh && sh ci/idle-translate/build.sh" >&2

+  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

echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="

-echo "manifest : $MANIFEST  ->  (archive root) manifest.toml"

-echo "strings  : $STRINGS/  ->  (archive root) strings/"

-echo "binaries : claude-spt-digest$EXE, claude-spt-psyche$EXE, cc-spt-idle-translate$EXE  ->  (archive root)"

-echo "NOTE: the archive is PLATFORM-SPECIFIC (native binaries: '$EXE' build) — hence the per-OS name."

-echo "      Ship one asset per OS on the same release; /sptc:setup picks the host's via --asset. Build"

-echo "      each OS native, OR cross-build Linux from Windows via cargo-zigbuild + SPTC_TARGET (bare"

-echo "      'cargo build --target' fails 'cc not found'; zig provides the linker). On v0.8.0+ Feature B"

-echo "      resolves the bundled binaries from the install dir (no PATH copy); pre-0.8.0 they need PATH."

+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)"

-  echo "DRY-RUN: nothing written. Re-run with --apply to write $OUT, then attach it to a GitHub"

-  echo "release on the monorepo. End users acquire it with 'spt adapter add --release"

-  echo "SaberMage/spt-claude-code --asset $ASSET' (see docs/RELEASE-RUNBOOK.md). Needs spt v0.7.3+."

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

+mkdir -p "$STAGE/$WIN_TRIPLE" "$STAGE/$LINUX_TRIPLE"

+  cp "$(win_path "$b")"   "$STAGE/$WIN_TRIPLE/$b.exe"

+  cp "$(linux_path "$b")" "$STAGE/$LINUX_TRIPLE/$b"

mkdir -p "$(dirname "$OUT")" || { echo "FATAL: cannot create $(dirname "$OUT")"; exit 2; }

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

+( cd "$STAGE" && tar -czf "$OUT" manifest.toml strings "$WIN_TRIPLE" "$LINUX_TRIPLE" ) \

|| { echo "FATAL: tar failed"; exit 2; }

-# Self-validate the produced archive: manifest.toml must be at the ROOT (no leading path component).

+# Self-validate the produced archive: shared manifest at ROOT + BOTH triple dirs present.

-if tar -tzf "$OUT" | grep -qx "manifest.toml"; then

-  echo "ok   manifest.toml at archive root"

-  echo "FATAL: manifest.toml is not at the archive root — adapter add --release would reject it" >&2

+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; }

+echo "ok   manifest.toml + strings/ at root; $WIN_TRIPLE/ + $LINUX_TRIPLE/ binaries present; no stray top-level dirs"

-echo "WROTE $OUT. Next (operator): attach to a GitHub release on SaberMage/spt-claude-code (alongside"

-echo "the other per-OS assets), then end users 'spt adapter add --release SaberMage/spt-claude-code"

-echo "--asset $ASSET' (needs spt v0.7.3+). /reload as needed."

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

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

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

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

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

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

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

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

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

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

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


ACK doyle — proceeding to build + dogfood.


Bash

$OWL send doyle <<'EOF'
perri — ACK. Holding outward release for operator GO (standing rule). Building fat .spt now → dogfood install on win node → gates → hold. Result ping incoming.
EOF
cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== DRY-RUN packer ==="; sh ci/publish/package-adapter.sh 2>&1; echo "DRY_RC=$?"

ACK doyle + run packer dry-run


➜ SENT:doyle
=== DRY-RUN packer ===
== validate manifest ==
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json

== tool binaries ==
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt-digest/target/release/claude-spt-digest.exe
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt-digest/target/x86_64-unknown-linux-gnu/release/claude-spt-digest
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt-psyche/target/release/claude-spt-psyche.exe
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt-psyche/target/x86_64-unknown-linux-gnu/release/claude-spt-psyche
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate.exe
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate/target/x86_64-unknown-linux-gnu/release/cc-spt-idle-translate

== plan (DRY-RUN) ==
manifest : /c/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml  ->  (root) manifest.toml          [min_spt_core 0.13.2]
strings  : /c/Users/decid/Documents/projects/spt-claude-code/adapter/strings/  ->  (root) strings/
win bins : -> x86_64-pc-windows-msvc/{claude-spt-digest,claude-spt-psyche,cc-spt-idle-translate}.exe
linux bins: -> x86_64-unknown-linux-gnu/{claude-spt-digest,claude-spt-psyche,cc-spt-idle-translate}
asset    : /c/Users/decid/Documents/projects/spt-claude-code/dist/adapter.spt  (the single fat adapter.spt — auto-resolves the host triple on install)

DRY-RUN: nothing written. Re-run with --apply to write /c/Users/decid/Documents/projects/spt-claude-code/dist/adapter.spt, then attach it as 'adapter.spt'
to a GitHub release on the monorepo. End users: 'spt adapter add --release SaberMage/spt-claude-code'
(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.
DRY_RC=0


Dry-run clean (manifest validates at floor 0.13.2, both platforms found). Build it.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/publish/package-adapter.sh --apply 2>&1 | tail -12; echo "APPLY_RC=${PIPESTATUS[0]}"; echo "=== fat .spt full listing ==="; tar -tzf dist/adapter.spt

Build the fat adapter.spt and list contents


manifest : /c/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml  ->  (root) manifest.toml          [min_spt_core 0.13.2]
strings  : /c/Users/decid/Documents/projects/spt-claude-code/adapter/strings/  ->  (root) strings/
win bins : -> x86_64-pc-windows-msvc/{claude-spt-digest,claude-spt-psyche,cc-spt-idle-translate}.exe
linux bins: -> x86_64-unknown-linux-gnu/{claude-spt-digest,claude-spt-psyche,cc-spt-idle-translate}
asset    : /c/Users/decid/Documents/projects/spt-claude-code/dist/adapter.spt  (the single fat adapter.spt — auto-resolves the host triple on install)

== validate archive ==
ok   manifest.toml + strings/ at root; x86_64-pc-windows-msvc/ + x86_64-unknown-linux-gnu/ binaries present; no stray top-level dirs

WROTE /c/Users/decid/Documents/projects/spt-claude-code/dist/adapter.spt (single fat adapter.spt). Next (operator): attach as 'adapter.spt' to a GitHub release
on SaberMage/spt-claude-code; end users 'spt adapter add --release SaberMage/spt-claude-code' (spt
v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.
APPLY_RC=0
=== fat .spt full listing ===
manifest.toml
strings/
strings/briefs/
strings/briefs/identity.md
strings/briefs/messaging-no-perch.md
strings/briefs/messaging-perch.md
strings/skills/
strings/skills/force-stop.md
strings/skills/list-agents.md
strings/skills/live.md
strings/skills/ready.md
strings/skills/setup.md
strings/skills/subnet.md
strings/skills/version.md
x86_64-pc-windows-msvc/
x86_64-pc-windows-msvc/cc-spt-idle-translate.exe
x86_64-pc-windows-msvc/claude-spt-digest.exe
x86_64-pc-windows-msvc/claude-spt-psyche.exe
x86_64-unknown-linux-gnu/
x86_64-unknown-linux-gnu/cc-spt-idle-translate
x86_64-unknown-linux-gnu/claude-spt-digest
x86_64-unknown-linux-gnu/claude-spt-psyche


Fat `.spt` built with the exact layout doyle specified. Now the dogfood: spt-core must really extract+flatten it. doyle says "install on the win node = the real-extract proof" — but `adapter add` takes a dir/manifest, and `--release` pulls from GitHub. Find the local path to make spt-core extract a local `.spt` (without any GitHub mutation, and without clobbering my live `claude-spt`).


SD="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/e2a44c8e-df24-4cba-8ede-fff02a618f5e/scratchpad"; echo "=== SPT_INSTALL_ASSET_BASE context ==="; sed -n '1893,1905p' "$SD/llms-full.txt"; echo "=== adapter add local-archive / file options ==="; spt adapter add --help 2>&1 | grep -iE "local|file|archive|\.spt|asset|base"; echo "=== any SPT_*ASSET*/SPT_*BASE env honored ==="; spt adapter add --release SaberMage/spt-claude-code --tag nonexistent-xyz --asset adapter.spt --https 2>&1 | head -3

Probe local asset-base install path


<owl_messages>
<owl_message from="doyle" priority="high">👍 Standing by for the dogfood result — go quiet till then, no need to ack further.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


=== SPT_INSTALL_ASSET_BASE context ===

| Env var | Meaning |
|---|---|
| `SPT_INSTALL_VERSION` | Install a specific release tag instead of latest |
| `SPT_INSTALL_DIR` | Override the install directory |
| `SPT_INSTALL_ASSET_BASE` | A URL or local directory holding the release assets + `SHA256SUMS` directly (CI, air-gap, mirrors) |
| `SPT_INSTALL_NO_PATH` | `1` = skip PATH registration |

Example — pin a version inside a CI job:

```sh
SPT_INSTALL_VERSION=v0.1.0 \
  curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
=== adapter add local-archive / file options ===
Register an adapter from a local path (a dir holding manifest.toml, or the manifest file itself)
  [PATH]  Local manifest source (omit when using --github or --release)
      --release <RELEASE>  GitHub release source user/repo — fetch the adapter archive asset from
      --asset <ASSET>      Release asset name for --release (default: adapter.spt — a tar archive
=== any SPT_*ASSET*/SPT_*BASE env honored ===
Fetching adapter release asset adapter.spt from SaberMage/spt-claude-code via Https…
ADAPTER_RELEASE_FAIL: fetch adapter.spt: HTTP 404 Not Found


doyle said go quiet — won't ack, continuing. Confirmed: spt-core has **no local-`.spt` install path** (`adapter add` takes dir/manifest only; `--release` pulls from GitHub; `SPT_INSTALL_ASSET_BASE` is installer-only). The real spt-core extract needs a release upload — an outward action I'm holding. So I'll do a **faithful local flatten-proof** now (replicating spt-core's documented classify→flatten-this-triple, then proving the flattened install resolves), and hold the true release-extract for operator GO.


SD="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/e2a44c8e-df24-4cba-8ede-fff02a618f5e/scratchpad"; cd "$SD" && rm -rf fatextract && mkdir fatextract && cd fatextract && \
tar -xzf "C:/Users/decid/Documents/projects/spt-claude-code/dist/adapter.spt" && \
echo "=== raw extracted (fat) ==="; ls -1; echo "=== simulate spt-core win-triple flatten ==="; \
cp x86_64-pc-windows-msvc/* . && rm -rf x86_64-pc-windows-msvc x86_64-unknown-linux-gnu && \
echo "=== flattened install dir (what spt-core lands on win) ==="; ls -1; echo "=== manifest version/floor ==="; grep -E '^version|^min_spt_core_version' manifest.toml

Extract fat .spt + simulate win-triple flatten


Error: Exit code 2
tar (child): Cannot connect to C: resolve failed

gzip: stdin: unexpected end of file
tar: Child returned status 128
tar: Error is not recoverable: exiting now
=== simulate spt-core win-triple flatten ===
cp: cannot stat 'x86_64-pc-windows-msvc/*': No such file or directory
=== manifest version/floor ===
grep: manifest.toml: No such file or directory


Bash `tar` mis-reads the `C:` path again. Use PowerShell tar for extract+flatten.


PowerShell

$SD = "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-claude-code\e2a44c8e-df24-4cba-8ede-fff02a618f5e\scratchpad"; $ex = "$SD\fatextract"; Remove-Item -Recurse -Force $ex -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Force $ex | Out-Null; tar -xzf "C:\Users\decid\Documents\projects\spt-claude-code\dist\adapter.spt" -C $ex; Write-Output "extract rc=$LASTEXITCODE"; Write-Output "=== raw fat ==="; Get-ChildItem $ex | Select-Object -ExpandProperty Name; $win = Join-Path $ex 'x86_64-pc-windows-msvc'; Copy-Item "$win\*" $ex; Remove-Item -Recurse -Force (Join-Path $ex 'x86_64-pc-windows-msvc'),(Join-Path $ex 'x86_64-unknown-linux-gnu'); Write-Output "=== flattened (win install dir) ==="; Get-ChildItem $ex | Select-Object -ExpandProperty Name

Extract fat .spt and simulate win-triple flatten via PowerShell


extract rc=0
=== raw fat ===
strings
x86_64-pc-windows-msvc
x86_64-unknown-linux-gnu
manifest.toml
=== flattened (win install dir) ===
strings
cc-spt-idle-translate.exe
claude-spt-digest.exe
claude-spt-psyche.exe
manifest.toml


Flattened win install dir = `{manifest.toml, strings/, 3×.exe}` — exactly spt-core's expected post-extract layout. Now prove those flattened win binaries (from the fat `.spt`) resolve + work, via the proofs against this dir.


Bash

FE="C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/e2a44c8e-df24-4cba-8ede-fff02a618f5e/scratchpad/fatextract"; SAMPLE="C:/Users/decid/Documents/projects/spt-claude-code/ci/digest/sample.jsonl"; \
echo "=== translate-proof (fat win binary, flattened) ==="; spt adapter translate-proof claude-spt --dir "$FE" --manifest "$FE/manifest.toml" --event '<EVENT type="msg" from="ci">fat .spt dogfood</EVENT>' 2>&1 | tail -5; \
echo "=== digest-proof (fat win binary, flattened) ==="; spt adapter digest-proof claude-spt --dir "$FE" --manifest "$FE/manifest.toml" --sample "$SAMPLE" 2>&1 | grep -E 'parsed|DIGEST_PROOF_OK'

Run both proofs against flattened fat-win install


=== translate-proof (fat win binary, flattened) ===
  key   ctrl+s -> [13]
  delay 50ms
  text  "<EVENT type=\"msg\" from=\"ci\">fat .spt dogfood</EVENT>\r"
  commit true
TRANSLATE_PROOF_OK:claude-spt
=== digest-proof (fat win binary, flattened) ===
parsed 5 record(s), dropped 0
--- parsed records ---
DIGEST_PROOF_OK:claude-spt


**Local dogfood PASSES** — both proofs green against the flattened fat-win install: the fat `.spt`'s win-triple binaries, flattened per spt-core's documented rule, resolve at install-dir root and work. This validates my archive structure end-to-end against the documented extract. (The only piece needing a release is spt-core's actual extract *code* — held for operator GO.)

Now verify the change didn't break gates + check for a packaging unit test.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== packaging tests / dist gitignore ==="; grep -rln "package-adapter\|ADAPTER_SPT_OUT" tests/ ci/ 2>/dev/null; grep -n "dist" .gitignore 2>/dev/null; echo "=== traceable-reqs check ==="; traceable-reqs check 2>&1 | tail -4; echo "=== check-manifest ==="; sh ci/manifest/check-manifest.sh 2>&1 | tail -3

Find packaging tests, gitignore dist, run quick gates


=== packaging tests / dist gitignore ===
tests/adapter-archive.sh
ci/publish/package-adapter.sh
ci/publish/release-acquire-int.sh
20:/dist/
=== traceable-reqs check ===
  [OK] REQ-SKILL-VERSION  required: []  stages: -doc -impl -unit -int
       /spt:version reports the spt-core-tracked manifest/binary version (the version-of-truth, not the marketplace skeleton version)
  [OK] REQ-UPS-INJECTION  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       UserPromptSubmit hook detects /sptc:X and injects X's real instructions as additionalContext (SKILL.md files stay skeletons); must be empirically confirmed UPS fires on slash-commands
=== check-manifest ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json


dist/ is gitignored (correct — `.spt` is a release artifact). traceable-reqs + check-manifest green. But there's a packaging unit test `tests/adapter-archive.sh` (and `release-acquire-int.sh`) that likely asserts the old per-OS layout. Read them.


1	#!/bin/sh
2	# Unit test for the adapter.spt packer (ci/publish/package-adapter.sh).
3	# Asserts the archive-ROOT invariant that `spt adapter add --release` depends on: the packed .spt
4	# holds manifest.toml (named exactly) + strings/ + the three tool binaries at the archive root.
5	# [unit->REQ-DIST-ADAPTER-RELEASE]
6	set -u
7	ROOT=$(CDPATH= cd "$(dirname "$0")/.." && pwd)
8	PACKER="$ROOT/ci/publish/package-adapter.sh"
9	rc=0
10	fail() { printf 'FAIL: %s\n' "$1"; rc=1; }
11
12	EXE=""
13	[ -f "$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest.exe" ] && EXE=".exe"
14	DIGEST="$ROOT/tools/claude-spt-digest/target/release/claude-spt-digest$EXE"
15	PSYCHE="$ROOT/tools/claude-spt-psyche/target/release/claude-spt-psyche$EXE"
16	IDLE="$ROOT/tools/cc-spt-idle-translate/target/release/cc-spt-idle-translate$EXE"
17
18	if [ ! -f "$DIGEST" ] || [ ! -f "$PSYCHE" ] || [ ! -f "$IDLE" ]; then
19	  # No release binaries on this host yet → assert the packer's REFUSE guard fires (no silent pass),
20	  # and announce the skip of the build-the-archive assertion (no hidden coverage gap).
21	  if sh "$PACKER" >/dev/null 2>&1; then
22	    fail "packer should refuse (exit!=0) when release binaries are absent"
23	  else
24	    echo "ok   packer refuses when binaries absent (guard works)"
25	  fi
26	  echo "SKIP: archive-build assertion — release binaries not present (run ci/{digest,psyche,idle-translate}/build.sh)"
27	  exit "$rc"
28	fi
29
30	# Binaries present → build a real archive to a temp file and assert its structure.
31	TMP=$(mktemp -d "${TMPDIR:-/tmp}/sptc-archtest.XXXXXX") || { echo "FAIL: mktemp"; exit 1; }
32	trap 'rm -rf "$TMP"' EXIT
33	OUT="$TMP/adapter.spt"
34
35	if ADAPTER_SPT_OUT="$OUT" sh "$PACKER" --apply >/dev/null 2>&1; then
36	  echo "ok   packer --apply succeeded"
37	else
38	  fail "packer --apply exited non-zero"
39	  exit "$rc"
40	fi
41
42	[ -f "$OUT" ] || { fail "no archive written at $OUT"; exit "$rc"; }
43
44	LIST=$(tar -tzf "$OUT")
45	# manifest.toml MUST be at the root (exact, no leading path) — the add --release contract.
46	echo "$LIST" | grep -qx "manifest.toml" \
47	  && echo "ok   manifest.toml at archive root" \
48	  || fail "manifest.toml not at archive root (add --release would reject)"
49	# strings/ present (any entry under strings/).
50	echo "$LIST" | grep -q "^strings/" \
51	  && echo "ok   strings/ present" \
52	  || fail "strings/ missing from archive"
53	# all three tool binaries present at root.
54	echo "$LIST" | grep -qx "claude-spt-digest$EXE" \
55	  && echo "ok   claude-spt-digest$EXE at root" \
56	  || fail "claude-spt-digest$EXE missing from archive"
57	echo "$LIST" | grep -qx "claude-spt-psyche$EXE" \
58	  && echo "ok   claude-spt-psyche$EXE at root" \
59	  || fail "claude-spt-psyche$EXE missing from archive"
60	echo "$LIST" | grep -qx "cc-spt-idle-translate$EXE" \
61	  && echo "ok   cc-spt-idle-translate$EXE at root" \
62	  || fail "cc-spt-idle-translate$EXE missing from archive"
63	# negative: no stray leading-dir wrapper (e.g. "adapter/manifest.toml").
64	if echo "$LIST" | grep -q "/manifest.toml"; then
65	  fail "manifest.toml appears under a subdir — archive root is wrong"
66	else
67	  echo "ok   no nested manifest.toml"
68	fi
69
70	# Per-OS asset NAMING (REQ-DIST-ADAPTER-PEROS): the default asset is adapter-<os>-<arch>.spt and the
71	# name follows SPTC_OS/SPTC_ARCH overrides; the dry-run plan advertises the matching end-user
72	# `--asset`. (Dry-run = no write; the cross-OS WARN to stderr on a mismatched host is non-fatal.)
73	# [unit->REQ-DIST-ADAPTER-PEROS]
74	plan=$(SPTC_OS=linux SPTC_ARCH=aarch64 sh "$PACKER" 2>&1)
75	echo "$plan" | grep -q "adapter-linux-aarch64\.spt" \
76	  && echo "ok   per-OS asset name honors SPTC_OS/SPTC_ARCH (adapter-linux-aarch64.spt)" \
77	  || fail "per-OS override name wrong; plan=[$plan]"
78	echo "$plan" | grep -q -- "--asset adapter-linux-aarch64\.spt" \
79	  && echo "ok   dry-run advertises the end-user --asset for that OS" \
80	  || fail "dry-run missing '--asset adapter-linux-aarch64.spt'; plan=[$plan]"
81	hostplan=$(sh "$PACKER" 2>&1)
82	echo "$hostplan" | grep -qE "adapter-(windows|linux|macos|unknown)-[A-Za-z0-9_]+\.spt" \
83	  && echo "ok   host default asset name matches adapter-<os>-<arch>.spt" \
84	  || fail "host default asset name not adapter-<os>-<arch>.spt; plan=[$hostplan]"
85
86	[ "$rc" -eq 0 ] && echo "PASS: adapter-archive"
87	exit "$rc"
88

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

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

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

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

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

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

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

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

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


The unit test `tests/adapter-archive.sh` asserts the OLD per-OS layout (binaries at root, `adapter-<os>-<arch>.spt` naming) — it'll fail against the fat packer. Rewrite it. Also check `release-acquire-int.sh`.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && echo "=== release-acquire-int.sh: asset/per-os refs ==="; grep -n -iE "asset|per-os|adapter-|\.spt|SPTC_OS|SPTC_ARCH|x86_64" ci/publish/release-acquire-int.sh | head -20; echo "=== REQ-DIST-ADAPTER-PEROS in traceable-reqs.toml ==="; grep -n -A6 'REQ-DIST-ADAPTER-PEROS' traceable-reqs.toml | head -16

Check release-acquire-int and the PEROS requirement


=== release-acquire-int.sh: asset/per-os refs ===
3:# <user/repo>` fetches the `adapter.spt` GitHub release asset (packed by package-adapter.sh),
7:# The LLM is never in the loop. [int->REQ-DIST-ADAPTER-RELEASE]
57:# 3. Profiles/strings shipped inside the .spt resolve through the registry (the asset was complete).
58:case "$list" in *claude-spt:ccs*) ok "shipped profile resolves from the .spt: claude-spt:ccs" ;; *) bad "ccs profile not resolved from --release asset" ;; esac
60:[ "$lbl" = "Claude Code (spt)" ] && ok "file-backed strings shipped in the .spt resolve: adapter_label" || bad "adapter_label='$lbl'"
=== REQ-DIST-ADAPTER-PEROS in traceable-reqs.toml ===
231:id = "REQ-DIST-ADAPTER-PEROS"
232-title = "The adapter.spt asset is the tool binaries' platform, so it ships PER-OS: the packer emits `adapter-<os>-<arch>.spt` (os ∈ windows|linux|macos, arch ∈ x86_64|aarch64) and /sptc:setup detects the host os+arch and passes the matching `--asset adapter-<os>-<arch>.spt` to `adapter add --release` (spt's `--asset` flag is caller-named; default adapter.spt). v1 OSes = windows + linux."
233-required_stages = ["doc", "impl", "unit"]  # doc: docs/RELEASE-RUNBOOK.md per-OS publish section (native per-OS build OR cross-build Linux FROM Windows via cargo-zigbuild + SPTC_TARGET — proven 2026-06-16, real ELF x86-64 binaries; bare `cargo build --target` fails `cc not found`, zig supplies the linker) + the setup body's os-detection step. impl: ci/publish/package-adapter.sh emits adapter-<os>-<arch>.spt (host-derived os/arch, overridable via SPTC_OS/SPTC_ARCH; SPTC_TARGET points at target/<triple>/release for a cross build) + adapter/strings/skills/setup.md & plugin/sptc/skills/setup/SKILL.md detect os/arch (uname) → `--asset`. unit: tests/adapter-archive.sh asserts the per-OS asset NAME (adapter-<os>-<arch>.spt) + the root invariant holds for it. int (real per-OS `--asset` acquire) folds into release-acquire-int.sh once the 2nd-OS asset is published on a release; both adapter-windows-x86_64.spt + adapter-linux-x86_64.spt now pack locally (linux verified = ELF GNU/Linux). See REQ-DIST-ADAPTER-RELEASE.
234-
235-# ── Milestone: V13-ADAPTER (spt-core harness-contract v0.13.0 seams) — minted 2026-06-20 from
236-# doyle's owl: the v0.13.0 harness contract is PUBLISHED (sabermage.github.io/spt-releases/
237-# harness-contract) ahead of the v0.13.0 binary (still v0.12.x on spt-releases). Two new RUN-group


`REQ-DIST-ADAPTER-PEROS` and its unit test encode the old per-OS-asset model; the fat archive supersedes it (host-correct binaries now via one archive). I'll keep blast radius tight: rewrite the unit test for the fat layout, update the requirement text, and leave the setup-skill `--asset` simplification + final per-OS-asset disposition as a **shape decision for the operator** at the hold. Rewrite the unit test.


Write

#!/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"