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

#!/bin/sh
# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain
# /-leading content (e.g. a prompt "/sptc:send doyle") — must reach the handler via STDIN (immune to
# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see
# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,
# …) + the numeric --host-pid — never message/prompt content, and never /-leading.
# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]
ROOT="$(CDPATH= cd "$(dirname "$0")/.." && pwd)"
HK="$ROOT/plugin/sptc/hooks"
HOOKRS="$ROOT/tools/claude-spt/src/hook.rs"
fail=0
yn() { [ "$1" -eq 0 ] && echo yes || echo no; }

# 1. hooks.json passes dispatch.sh only a bare, non-/-leading CC event-name token (no payload content).
#    Every `dispatch.sh <token>` token must be alphabetic (an event name), never /-leading.
bad_tokens=$(grep -oE 'dispatch\.sh\\?" [^"]+' "$HK/hooks.json" | sed 's#.*dispatch\.sh\\\?" ##' | grep -Ec '^[A-Za-z]' )
tot_tokens=$(grep -oE 'dispatch\.sh\\?" [^"]+' "$HK/hooks.json" | wc -l | tr -d ' ')
if [ "$bad_tokens" -ne "$tot_tokens" ] || [ "$tot_tokens" -eq 0 ]; then
  printf 'FAIL hooks.json: a dispatch token is not a bare event name (%s/%s alphabetic)\n' "$bad_tokens" "$tot_tokens"; fail=1
fi
if grep -qE 'dispatch\.sh\\?" +/' "$HK/hooks.json"; then
  printf 'FAIL hooks.json: a /-leading positional token is passed to dispatch.sh (MSYS-unsafe)\n'; fail=1
fi

# 2. dispatch.sh must NOT read the payload (no `cat`) — it inherits stdin straight through to the
#    binary — and must NOT reconstruct content from a positional argv ($1 is the event name only).
if grep -qE '\$\(\s*cat\b' "$HK/dispatch.sh"; then
  printf 'FAIL dispatch.sh: reads stdin itself (must pass it through to the binary)\n'; fail=1
fi
if grep -Eq '(prompt|msg|body|message|content|file_path)=("?\$[1-9])' "$HK/dispatch.sh"; then
  printf 'FAIL dispatch.sh: parses payload content from a positional argv (MSYS-unsafe)\n'; fail=1
fi

# 3. The binary reads the CC payload from STDIN and parses every field off that parsed object — never
#    from argv (which only carries the event name + --host-pid).
grep -q 'stdin().read_to_string' "$HOOKRS" && r=0 || r=1
[ "$r" -eq 0 ] || { printf 'FAIL hook.rs: does not read the CC payload from stdin\n'; fail=1; }
# The argv loop handles only --host-pid; the payload fields come from the parsed stdin Value (field/nested).
grep -q 'serde_json::from_str' "$HOOKRS" && r=0 || r=1
[ "$r" -eq 0 ] || { printf 'FAIL hook.rs: does not parse the stdin payload as JSON\n'; fail=1; }

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

-# REQ-HAZARD-MSYS-PATHCONV conformance: every hook wrapper must read the CC hook payload from

-# STDIN (immune to MSYS), never reconstruct message/prompt content from a /-leading positional

-# argv (Git-Bash path-mangles those on Windows — see docs/KNOWN-HAZARDS.md 1.1).

+# REQ-HAZARD-MSYS-PATHCONV conformance (D1 architecture). The CC hook payload — which can contain

+# /-leading content (e.g. a prompt "/sptc:send doyle") — must reach the handler via STDIN (immune to

+# MSYS), never as a positional argv (Git-Bash path-mangles a /-leading argv on Windows — see

+# docs/KNOWN-HAZARDS.md 1.1). After D1 the only argv passed anywhere is the CC EVENT NAME (SessionStart,

+# …) + the numeric --host-pid — never message/prompt content, and never /-leading.

+ROOT="$(CDPATH= cd "$(dirname "$0")/.." && pwd)"

-for w in session-start user-prompt-submit stop session-end subagent-start subagent-stop; do

-  # Must consume the payload from stdin.

-  if ! grep -q 'input=$(cat)' "$f"; then

-    printf 'FAIL %s: does not read the hook payload from stdin\n' "$w"; fail=1

-  # Must NOT parse prompt/message CONTENT from a positional argument.

-  if grep -Eq '(prompt|msg|body|message)=("?\$[1-9])' "$f"; then

-    printf 'FAIL %s: parses content from a positional argv (MSYS-unsafe)\n' "$w"; fail=1

+# 1. hooks.json passes dispatch.sh only a bare, non-/-leading CC event-name token (no payload content).

+#    Every `dispatch.sh <token>` token must be alphabetic (an event name), never /-leading.

+bad_tokens=$(grep -oE 'dispatch\.sh\\?" [^"]+' "$HK/hooks.json" | sed 's#.*dispatch\.sh\\\?" ##' | grep -Ec '^[A-Za-z]' )

+tot_tokens=$(grep -oE 'dispatch\.sh\\?" [^"]+' "$HK/hooks.json" | wc -l | tr -d ' ')

+if [ "$bad_tokens" -ne "$tot_tokens" ] || [ "$tot_tokens" -eq 0 ]; then

+  printf 'FAIL hooks.json: a dispatch token is not a bare event name (%s/%s alphabetic)\n' "$bad_tokens" "$tot_tokens"; fail=1

+if grep -qE 'dispatch\.sh\\?" +/' "$HK/hooks.json"; then

+  printf 'FAIL hooks.json: a /-leading positional token is passed to dispatch.sh (MSYS-unsafe)\n'; fail=1

+# 2. dispatch.sh must NOT read the payload (no `cat`) — it inherits stdin straight through to the

+#    binary — and must NOT reconstruct content from a positional argv ($1 is the event name only).

+if grep -qE '\$\(\s*cat\b' "$HK/dispatch.sh"; then

+  printf 'FAIL dispatch.sh: reads stdin itself (must pass it through to the binary)\n'; fail=1

+if grep -Eq '(prompt|msg|body|message|content|file_path)=("?\$[1-9])' "$HK/dispatch.sh"; then

+  printf 'FAIL dispatch.sh: parses payload content from a positional argv (MSYS-unsafe)\n'; fail=1

+# 3. The binary reads the CC payload from STDIN and parses every field off that parsed object — never

+#    from argv (which only carries the event name + --host-pid).

+grep -q 'stdin().read_to_string' "$HOOKRS" && r=0 || r=1

+[ "$r" -eq 0 ] || { printf 'FAIL hook.rs: does not read the CC payload from stdin\n'; fail=1; }

+# The argv loop handles only --host-pid; the payload fields come from the parsed stdin Value (field/nested).

+grep -q 'serde_json::from_str' "$HOOKRS" && r=0 || r=1

+[ "$r" -eq 0 ] || { printf 'FAIL hook.rs: does not parse the stdin payload as JSON\n'; fail=1; }

#!/bin/sh
# REQ-HAZARD-MSYS-PATHCONV conformance: every hook wrapper must read the CC hook payload from
# STDIN (immune to MSYS), never reconstruct message/prompt content from a /-leading positional
# argv (Git-Bash path-mangles those on Windows — see docs/KNOWN-HAZARDS.md 1.1).
# Run: sh tests/msys-hazard.sh   (exit 0 = pass).   [unit->REQ-HAZARD-MSYS-PATHCONV]
H="$(dirname "$0")/../plugin/sptc/hooks"
fail=0
for w in session-start user-prompt-submit stop session-end subagent-start subagent-stop; do
  f="$H/$w.sh"
  # Must consume the payload from stdin.
  if ! grep -q 'input=$(cat)' "$f"; then
    printf 'FAIL %s: does not read the hook payload from stdin\n' "$w"; fail=1
  fi
  # Must NOT parse prompt/message CONTENT from a positional argument.
  if grep -Eq '(prompt|msg|body|message)=("?\$[1-9])' "$f"; then
    printf 'FAIL %s: parses content from a positional argv (MSYS-unsafe)\n' "$w"; fail=1
  fi
done
[ "$fail" -eq 0 ] && { echo "MSYS-HAZARD OK"; exit 0; } || { echo "MSYS-HAZARD FAIL"; exit 1; }


proceed to build v0.9.0. see it through to release publish


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh tests/msys-hazard.sh

Run rewritten msys-hazard test


MSYS-HAZARD OK


cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh 2>&1 | grep -E "GATE:|FAIL|RESULT|^ok  |SKIP" | head -60

Re-run full gates, summary view


=== GATE: shell-syntax (sh -n) ===
ok  ci/acceptance/lib.sh
ok  ci/acceptance/run-acceptance.sh
ok  ci/digest/build.sh
ok  ci/digest/digest-proof-int.sh
ok  ci/docs/check-docs.sh
ok  ci/docs/gen-llms.sh
ok  ci/hooks/poll-int.sh
ok  ci/idle-translate/build.sh
ok  ci/idle-translate/translate-proof-int.sh
ok  ci/install-hooks.sh
ok  ci/launcher/bind-int.sh
ok  ci/lib/spt-bus.sh
ok  ci/manifest/check-manifest.sh
ok  ci/manifest/registration-int.sh
ok  ci/psyche-download/psyche-download-int.sh
ok  ci/psyche/build.sh
ok  ci/psyche/live-relay-int.sh
ok  ci/publish/package-adapter.sh
ok  ci/publish/package-skeleton.sh
ok  ci/publish/release-acquire-int.sh
ok  ci/publish/validate-skeleton.sh
ok  ci/run-gates.sh
ok  ci/setup/activate-int.sh
ok  ci/subnet/multi-subnet-bringup-int.sh
ok  plugin/sptc/bootstrap.sh
ok  plugin/sptc/hooks/dispatch.sh
ok  tests/acceptance-harness.sh
ok  tests/adapter-archive.sh
ok  tests/ci-gates.sh
ok  tests/docs-gen.sh
ok  tests/hooks-dispatch.sh
ok  tests/manifest-schema.sh
ok  tests/manifest-shortcut.sh
ok  tests/msys-hazard.sh
ok  tests/skeleton-validate.sh
=== GATE: unit tests (tests/*.sh) ===
ok  tests/acceptance-harness.sh
ok  tests/adapter-archive.sh
ok  tests/ci-gates.sh
ok  tests/docs-gen.sh
ok  tests/hooks-dispatch.sh
ok  tests/manifest-schema.sh
ok  tests/manifest-shortcut.sh
ok  tests/msys-hazard.sh
ok  tests/skeleton-validate.sh
=== GATE: digest-extractor (cargo build + test) ===
ok  claude-spt: cargo test + release build (digest/psyche/post-update subcommands)
=== GATE: psyche-runner (cargo build + test) ===
ok  claude-spt-psyche: folded into the consolidated claude-spt crate (built/tested by ci/digest/build.sh)
=== GATE: idle-translate (cargo build + test) ===
ok  cc-spt-idle-translate: folded into the consolidated claude-spt crate as the translate subcommand (built/tested by ci/digest/build.sh)
=== GATE: traceable-reqs check (requirement coverage) ===
ok  coverage green
=== GATE: skeleton-validate (cplugs installability) ===
ok  skeleton installable
=== GATE: manifest-schema ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json
=== GATE: docs-drift ===


cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh 2>&1 | grep -E "RESULT"

Check final gate result


=== RESULT: PASS ===


All default gates PASS (89 cargo tests, traceable-reqs green, msys-hazard green). Now honestly re-validate the D1 binary drain e2e via poll-int (safe: throwaway perch, no registry mutation).


cd /c/Users/decid/Documents/projects/spt-claude-code
SPTC_ACCEPTANCE=1 sh ci/hooks/poll-int.sh 2>&1

Run poll-int (binary drain e2e) on live spt 0.16.0


Error: Exit code 1
FAIL bind failed: HOME_REFUSED: this node holds 2 subnets (SPT_DEV, BIGNET) — pass --subnet <name> (home is assigned at creation, no guessing)
MSG-ENVELOPE-INT FAIL


poll-int's bind needs `--subnet` (this node holds 2 subnets — env constraint, not a D1 defect). The binary's render+drain is already covered by hook.rs unit tests + my earlier live skill-injection dogfood; the live multi-subnet perch e2e stays in the deferred harness-glue bucket. Verify no stray perch + my live perch intact.


cd /c/Users/decid/Documents/projects/spt-claude-code
spt endpoint list 2>&1 | grep -iE "poll-int|SPT_DEV|perri" | head; echo "---adapter still registered---"; spt adapter list 2>&1 | grep claude-spt | head -2

Check no stray perch + adapter + live perch intact


SUBNET SPT_DEV
---adapter still registered---
claude-spt: Harness Pointer active (from C:\Users\decid\AppData\Local\spt-core\adapters\_github\SaberMage-spt-claude-code)
  claude-spt:ccs (shipped)


Live perch intact, no collateral. Code done, gates green. Now the release. Read plugin.json + CHANGELOG head + check zig/linux build availability.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== plugin.json ==="; find plugin -name plugin.json -exec cat {} \;
echo "=== CHANGELOG head ==="; sed -n '1,30p' CHANGELOG.md
echo "=== zig / zigbuild / linux target ==="; command -v zig && zig version; command -v cargo-zigbuild && echo "zigbuild present"; rustup target list --installed 2>/dev/null | grep linux

Read plugin.json + CHANGELOG + linux toolchain status


=== plugin.json ===
{
  "name": "sptc",
  "version": "0.1.7",
  "description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",
  "author": {
    "name": "perri"
  },
  "keywords": ["spt", "spacetime", "messaging", "agents", "live", "claude-spt"]
}
=== CHANGELOG head ===
# Changelog

All notable user-facing changes to the **sptc** plugin (Spacetime adapter for Claude Code).
The format follows [Keep a Changelog](https://keepachangelog.com/); each release section is the
public release body verbatim.

> Each release below is keyed to the **adapter version of truth** (the version you see in
> `/sptc:version` and the GitHub release tag). The cplugs *plugin skeleton* version (`plugin.json`)
> moves on its own slower schedule and may differ.

## [0.8.0] - 2026-06-26

> Requires spt-core **v0.16.0 or newer** (was v0.15.0). This release unifies the project naming,
> turns updating into a single command, and consolidates the adapter to one tool binary — built on
> the update-arc + CLI features that arrive in spt-core v0.16.0.

### Added
- **One-command update — `spt adapter update claude-spt`.** A single command now keeps *everything*
  current: it pulls the new adapter (manifest + binary + strings) **and** reconciles the Claude Code
  plugin in the same step. The only manual residual is `/reload-plugins` (an unavoidable Claude Code
  TUI action), which the update prints a reminder to run.
- **Mid-turn reachability for live agents.** A live agent can now receive a message *while it is
  working* (not only between turns) — incoming messages surface mid-turn, with the endpoint honestly
  marked busy during a turn and idle when it finishes.

### Changed
- **Name unification.** The project/repo and adapter are now **`claude-spt`** everywhere spt-core
  sees them (the repo was renamed `spt-claude-code` → `claude-spt`; install and update both read
  `claude-spt`). The Claude Code plugin stays `sptc` / `/sptc:*` this release.
- **One tool binary.** The separate digest, psyche, and idle-translation binaries are consolidated
=== zig / zigbuild / linux target ===
/c/Users/decid/.cargo/bin/cargo-zigbuild
zigbuild present
x86_64-unknown-linux-gnu


cargo-zigbuild + linux target present, but `zig` not on PATH. Locate it (runbook says `~/.sptc-zig/...`).


ls -d "$HOME"/.sptc-zig/* 2>/dev/null; find "$HOME/.sptc-zig" -name "zig.exe" 2>/dev/null | head; echo "---PATH zig?---"; which zig 2>/dev/null || echo "not on PATH"

Locate zig binary


/c/Users/decid/.sptc-zig/zig-x86_64-windows-0.14.1
/c/Users/decid/.sptc-zig/zig-x86_64-windows-0.14.1/zig.exe
---PATH zig?---
not on PATH


Found zig. Build both platforms (win native + linux via zigbuild), then verify the artifacts.


cd /c/Users/decid/Documents/projects/spt-claude-code
export PATH="$HOME/.sptc-zig/zig-x86_64-windows-0.14.1:$PATH"
echo "=== win release (already built, rebuild clean) ==="
cargo build --release --quiet --manifest-path tools/claude-spt/Cargo.toml && echo "win ok"
echo "=== linux zigbuild ==="
cargo zigbuild --release --quiet --target x86_64-unknown-linux-gnu --manifest-path tools/claude-spt/Cargo.toml 2>&1 | tail -8 && echo "linux build returned $?"

Build win + linux binaries


=== win release (already built, rebuild clean) ===
win ok
=== linux zigbuild ===
linux build returned 0


cd /c/Users/decid/Documents/projects/spt-claude-code
file tools/claude-spt/target/release/claude-spt.exe 2>/dev/null
file tools/claude-spt/target/x86_64-unknown-linux-gnu/release/claude-spt 2>/dev/null
echo "=== linux binary has hook subcommand? (strings grep) ==="
grep -c "hook <event>" tools/claude-spt/target/x86_64-unknown-linux-gnu/release/claude-spt 2>/dev/null || echo "(binary - grep ok regardless)"

Verify both binaries are correct platform + carry hook


tools/claude-spt/target/release/claude-spt.exe: PE32+ executable (console) x86-64, for MS Windows, 5 sections
tools/claude-spt/target/x86_64-unknown-linux-gnu/release/claude-spt: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, stripped
=== linux binary has hook subcommand? (strings grep) ===
1


Both binaries correct (win PE + linux ELF, both carry `hook`). Package the adapter (dry-run then apply).


cd /c/Users/decid/Documents/projects/spt-claude-code
export PATH="$HOME/.sptc-zig/zig-x86_64-windows-0.14.1:$PATH"
sh ci/publish/package-adapter.sh 2>&1 | tail -25

Package adapter dry-run


== 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/target/release/claude-spt.exe
  ok    /c/Users/decid/Documents/projects/spt-claude-code/tools/claude-spt/target/x86_64-unknown-linux-gnu/release/claude-spt

== plan (DRY-RUN) ==
manifest : /c/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml  ->  (root) manifest.toml          [min_spt_core 0.16.0]
strings  : /c/Users/decid/Documents/projects/spt-claude-code/adapter/strings/  ->  (root) strings/
win bins : -> x86_64-pc-windows-msvc/{claude-spt}.exe
linux bins: -> x86_64-unknown-linux-gnu/{claude-spt}
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/claude-spt'
(default asset adapter.spt). Needs spt v0.13.2+. See docs/RELEASE-RUNBOOK.md.


cd /c/Users/decid/Documents/projects/spt-claude-code
export PATH="$HOME/.sptc-zig/zig-x86_64-windows-0.14.1:$PATH"
sh ci/publish/package-adapter.sh --apply 2>&1 | tail -8
echo "=== dist ==="; ls -la dist/
echo "=== archive contents ==="; tar tzf dist/adapter.spt 2>/dev/null || tar tf dist/adapter.spt 2>/dev/null

Package adapter --apply + inspect archive


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/claude-spt; end users 'spt adapter add --release SaberMage/claude-spt' (spt
v0.13.2+). The fat archive auto-resolves the host's binaries — no per-OS --asset needed.
=== dist ===
total 1608
drwxr-xr-x 1 decid 197609      0 Jun 20 05:32 .
drwxr-xr-x 1 decid 197609      0 Jun 28 01:23 ..
-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 549411 Jun 28 01:42 adapter.spt
=== archive contents ===
manifest.toml
strings/
strings/briefs/
strings/briefs/identity.md
strings/briefs/live-ops.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/claude-spt.exe
x86_64-unknown-linux-gnu/
x86_64-unknown-linux-gnu/claude-spt


Adapter.spt packed (manifest + strings + both triples). Local dogfood per runbook: extract, flatten this triple, run translate/digest-proof on the repacked binary.


cd /c/Users/decid/Documents/projects/spt-claude-code
D="$LOCALAPPDATA/Temp/sptc-v090-dogfood"; rm -rf "$D"; mkdir -p "$D"
tar xf dist/adapter.spt -C "$D"
cp "$D"/x86_64-pc-windows-msvc/* "$D"/ 2>/dev/null
echo "=== flattened ==="; ls "$D" | grep -E "claude-spt|manifest"
echo "=== translate-proof ==="
spt adapter translate-proof claude-spt --dir "$D" --manifest "$D/manifest.toml" 2>&1 | tail -12

Extract+flatten+translate-proof the repacked binary


=== flattened ===
claude-spt.exe
manifest.toml
=== translate-proof ===
error: the following required arguments were not provided:
  --event <EVENT>

Usage: spt.exe adapter translate-proof --event <EVENT> --dir <DIR> --manifest <MANIFEST> <OPTION>

For more information, try '--help'.


cd /c/Users/decid/Documents/projects/spt-claude-code
D="$LOCALAPPDATA/Temp/sptc-v090-dogfood"
echo "=== translate-proof (with --event) ==="
spt adapter translate-proof claude-spt --dir "$D" --manifest "$D/manifest.toml" --event '<EVENT type="msg" from="doyle">hi there</EVENT>' 2>&1 | tail -15

translate-proof with event arg


=== translate-proof (with --event) ===
=== translate-proof: claude-spt ===
emitted 6 command(s), commit: yes

--- emitted keystroke commands ---
  key   ctrl+s -> [13]
  delay 50ms
  text  "<EVENT type=\"msg\" from=\"doyle\">\nhi there\n</EVENT>"
  delay 50ms
  key   enter -> [0d]
  commit true
TRANSLATE_PROOF_OK:claude-spt


translate-proof OK (binary resolves + runs). Now digest-proof.


cd /c/Users/decid/Documents/projects/spt-claude-code
D="$LOCALAPPDATA/Temp/sptc-v090-dogfood"
spt adapter digest-proof claude-spt --dir "$D" --manifest "$D/manifest.toml" --sample ci/digest/sample.jsonl 2>&1 | tail -15

digest-proof the repacked binary


parsed 5 record(s), dropped 0

--- parsed records ---
  {"role":"input","text":"add a file at src/a.rs","ts":"2026-06-15T10:00:00.000Z"}
  {"role":"agent","text":"On it — writing src/a.rs now.","ts":"2026-06-15T10:00:01.000Z"}
  {"role":"tool","tool":{"arg":"src/a.rs","name":"Write"},"ts":"2026-06-15T10:00:01.000Z"}
  {"role":"tool","tool":{"arg":"cargo build","name":"Bash"},"ts":"2026-06-15T10:00:04.000Z"}
  {"role":"agent","text":"Done — built clean.","ts":"2026-06-15T10:00:05.000Z"}

--- rendered digest ---
» add a file at src/a.rs
  On it — writing src/a.rs now.
  used: Write(src/a.rs), Bash(cargo build)
  Done — built clean.
DIGEST_PROOF_OK:claude-spt


Both proofs pass from the repacked binary. Sanity-check the `hook` subcommand directly from the extracted binary.


cd /c/Users/decid/Documents/projects/spt-claude-code
D="$LOCALAPPDATA/Temp/sptc-v090-dogfood"
echo "=== extracted binary: hook skill-inject ==="
printf '%s' '{"session_id":"x","prompt":"/sptc:version"}' | "$D/claude-spt.exe" hook UserPromptSubmit --host-pid 1 | head -3
echo "=== extracted binary: hook help ==="
"$D/claude-spt.exe" hook 2>&1
rm -rf "$D"; echo "cleaned dogfood dir"

Verify hook subcommand in the packed binary


=== extracted binary: hook skill-inject ===
<sptc_skill name="version">
# /sptc:version — operative instructions

=== extracted binary: hook help ===
claude-spt hook: missing <event>
cleaned dogfood dir


Packed binary's `hook` works. Adapter.spt fully validated locally. Now release docs: CHANGELOG + plugin.json bump. Check gitignore for dist + stale artifacts.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== .gitignore ==="; cat .gitignore
echo "=== git status ==="; git status --short

Check gitignore + current git status


=== .gitignore ===
# spt runtime perch state (not source) — depth-safe (** ), so it's ignored under any nested
# .claude/ too (e.g. plugin/sptc/.claude/ when this repo is loaded as a live plugin). A
# root-anchored pattern previously leaked plugin/sptc/.claude/LIVE_AGENT_IDS.json into the tree.
**/.claude/LIVE_AGENT_IDS.json
**/.claude/*-commune.md
**/.claude/*-signoff.md

# mdBook build artifact (generated; source is docs-site/src)
docs-site/book/

# Rust build artifacts for the claude-spt-digest extractor crate (Cargo.lock is tracked, bin crate)
tools/**/target/
# generated launchers (capsule-style cc / cc-<id>)
/cc
/cc.bat
/cc.sh
/cc-*

# packed adapter release asset (generated by ci/publish/package-adapter.sh; uploaded to a GitHub release)
/dist/
=== git status ===
 M adapter/claude-spt.toml
 M ci/hooks/poll-int.sh
 M ci/manifest/registration-int.sh
 M docs/adr/0006-one-command-update-and-consolidated-binary.md
 D plugin/sptc/hooks/_common.sh
 M plugin/sptc/hooks/hooks.json
 D plugin/sptc/hooks/post-tool-use.sh
 D plugin/sptc/hooks/pre-tool-use.sh
 D plugin/sptc/hooks/session-end.sh
 D plugin/sptc/hooks/session-start.sh
 D plugin/sptc/hooks/stop.sh
 D plugin/sptc/hooks/subagent-start.sh
 D plugin/sptc/hooks/subagent-stop.sh
 D plugin/sptc/hooks/user-prompt-submit.sh
 D tests/hooks-parse.sh
 M tests/msys-hazard.sh
 M tools/claude-spt/src/main.rs
 M traceable-reqs.toml
?? V09-PLAN.md
?? plugin/sptc/hooks/dispatch.sh
?? tests/hooks-dispatch.sh
?? tools/claude-spt/src/hook.rs


dist/ is gitignored. Now write CHANGELOG [0.9.0] + bump plugin.json. Locate plugin.json.


cd /c/Users/decid/Documents/projects/spt-claude-code
find plugin -name plugin.json

Find plugin.json path


1	{
2	  "name": "sptc",
3	  "version": "0.1.7",
4	  "description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",
5	  "author": {
6	    "name": "perri"
7	  },
8	  "keywords": ["spt", "spacetime", "messaging", "agents", "live", "claude-spt"]
9	}
10

sptc

spt spacetime messaging agents live claude-spt


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

"description": "Spacetime (spt) adapter for Claude Code — agent messaging, live agents, and spt-core bootstrap. Thin skeleton; logic rides the spt binary + adapter manifest (spt-core-conducted).",


Now the CHANGELOG `## [0.9.0]` section (user-facing, no internal lingo).


Edit

## [0.9.0] - 2026-06-28

> Requires spt-core **v0.16.0 or newer** (unchanged from 0.8.0). This release is an internal
> re-plumbing of how Claude Code hooks run — there is **no change to what you do or see**. After
> updating, run `/reload-plugins` (or restart Claude Code) once, as the update reminds you.

### Changed
- **Hook behaviour now updates with `spt adapter update` — no plugin reinstall needed.** Previously,
  any change to how a hook works (message delivery, the live-agent checkpoint, the session briefing)
  required a separate Claude Code plugin update. Now that logic lives in the `claude-spt` program that
  `spt adapter update` already refreshes, so a single update keeps hooks current. The plugin's hook
  wiring is now fixed and rarely needs a marketplace bump again.

### Fixed
- Nothing user-visible. This is a behaviour-preserving refactor: messaging, the `/sptc:*` skills, the
  live-agent checkpoint (`/sptc:commune --checkpoint`), and the session briefing all work exactly as
  before — they are just driven by the consolidated `claude-spt` program instead of separate hook
  scripts shipped in the plugin.

## [0.8.0] - 2026-06-26


<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	# Changelog
2
3	All notable user-facing changes to the **sptc** plugin (Spacetime adapter for Claude Code).
4	The format follows [Keep a Changelog](https://keepachangelog.com/); each release section is the
5	public release body verbatim.
6
7	> Each release below is keyed to the **adapter version of truth** (the version you see in
8	> `/sptc:version` and the GitHub release tag). The cplugs *plugin skeleton* version (`plugin.json`)
9	> moves on its own slower schedule and may differ.
10
11	## [0.8.0] - 2026-06-26
12

# Changelog

All notable user-facing changes to the **sptc** plugin (Spacetime adapter for Claude Code).
The format follows [Keep a Changelog](https://keepachangelog.com/); each release section is the
public release body verbatim.

> Each release below is keyed to the **adapter version of truth** (the version you see in
> `/sptc:version` and the GitHub release tag). The cplugs *plugin skeleton* version (`plugin.json`)
> moves on its own slower schedule and may differ.

## [0.8.0] - 2026-06-26


## [0.9.0] - 2026-06-28

> Requires spt-core **v0.16.0 or newer** (unchanged from 0.8.0). This release is an internal
> re-plumbing of how Claude Code hooks run — there is **no change to what you do or see**. After
> updating, run `/reload-plugins` (or restart Claude Code) once, as the update reminds you.

### Changed
- **Hook behaviour now updates with `spt adapter update` — no plugin reinstall needed.** Previously,
  any change to how a hook works (message delivery, the live-agent checkpoint, the session briefing)
  required a separate Claude Code plugin update. Now that logic lives in the `claude-spt` program that
  `spt adapter update` already refreshes, so a single update keeps hooks current. The plugin's hook
  wiring is now fixed and rarely needs a marketplace bump again.

### Fixed
- Nothing user-visible. This is a behaviour-preserving refactor: messaging, the `/sptc:*` skills, the
  live-agent checkpoint (`/sptc:commune --checkpoint`), and the session briefing all work exactly as
  before — they are just driven by the consolidated `claude-spt` program instead of separate hook
  scripts shipped in the plugin.

## [0.8.0] - 2026-06-26


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

## [0.9.0] - 2026-06-28

> Requires spt-core **v0.16.0 or newer** (unchanged from 0.8.0). This release is an internal
> re-plumbing of how Claude Code hooks run — there is **no change to what you do or see**. After
> updating, run `/reload-plugins` (or restart Claude Code) once, as the update reminds you.

### Changed
- **Hook behaviour now updates with `spt adapter update` — no plugin reinstall needed.** Previously,
  any change to how a hook works (message delivery, the live-agent checkpoint, the session briefing)
  required a separate Claude Code plugin update. Now that logic lives in the `claude-spt` program that
  `spt adapter update` already refreshes, so a single update keeps hooks current. The plugin's hook
  wiring is now fixed and rarely needs a marketplace bump again.

### Fixed
- Nothing user-visible. This is a behaviour-preserving refactor: messaging, the `/sptc:*` skills, the
  live-agent checkpoint (`/sptc:commune --checkpoint`), and the session briefing all work exactly as
  before — they are just driven by the consolidated `claude-spt` program instead of separate hook
  scripts shipped in the plugin.

## [0.8.0] - 2026-06-26

> `/sptc:version` and the GitHub release tag). The cplugs *plugin skeleton* version (`plugin.json`)

> moves on its own slower schedule and may differ.

+> Requires spt-core **v0.16.0 or newer** (unchanged from 0.8.0). This release is an internal

+> re-plumbing of how Claude Code hooks run — there is **no change to what you do or see**. After

+> updating, run `/reload-plugins` (or restart Claude Code) once, as the update reminds you.

+- **Hook behaviour now updates with `spt adapter update` — no plugin reinstall needed.** Previously,

+  any change to how a hook works (message delivery, the live-agent checkpoint, the session briefing)

+  required a separate Claude Code plugin update. Now that logic lives in the `claude-spt` program that

+  `spt adapter update` already refreshes, so a single update keeps hooks current. The plugin's hook

+  wiring is now fixed and rarely needs a marketplace bump again.

+- Nothing user-visible. This is a behaviour-preserving refactor: messaging, the `/sptc:*` skills, the

+  live-agent checkpoint (`/sptc:commune --checkpoint`), and the session briefing all work exactly as

+  before — they are just driven by the consolidated `claude-spt` program instead of separate hook

> Requires spt-core **v0.16.0 or newer** (was v0.15.0). This release unifies the project naming,


CHANGELOG + plugin.json done. Check docs for stale references to the old hook wrappers (docs-drift won't catch stale prose).


Found 14 files
docs\adr\0006-one-command-update-and-consolidated-binary.md
V09-PLAN.md
UNIFY-CONSOLIDATE-PLAN.md
docs\PARITY.md
docs\SPT-CORE-FINDINGS.md
CHECKPOINT-COMMUNE-PLAN.md
docs-site\src\reference\harness-contract.md
SESSIONSTART-BRIEF-PLAN.md
SKELETON-PLAN.md
MANIFEST-PARITY-PLAN.md
docs\adr\0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md
CC-LAUNCHER-BIND-PLAN.md
PREP4-PLAN.md
docs\adr\0001-distribution-splits-by-volatility.md


Most are point-in-time plan docs (leave as history). The published `docs-site/src/reference/harness-contract.md` matters. Check its hook references.


1-<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
2:     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->
3-# Harness contract
4-
--
15-| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
16-| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
17:| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
18:| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
19:| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
20:| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
21:| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
22:| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |
23-
24-## Two invariants the wrappers hold


This published doc table is now stale. Read the full hook section to update for D1.


1	<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
2	     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->
3	# Harness contract
4
5	`claude-spt` is glue: it maps Claude Code **hook events** to the `spt` binary's harness-contract
6	inbound surface (`spt api --adapter claude-spt <verb>`). The binary is harness-agnostic; this
7	adapter is the Claude-Code-shaped edge of it.
8
9	The authoritative contract lives on `spt-core`'s published surface — the
10	[harness-contract + CLI reference](https://sabermage.github.io/spt-releases). This page documents
11	the **adapter's** wiring: which Claude Code hook drives which `spt api` verb.
12
13	## Hook → `spt api` mapping
14
15	| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
16	| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
17	| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
18	| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
19	| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
20	| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
21	| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
22	| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |
23
24	## Two invariants the wrappers hold
25
26	- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
27	  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read
28	  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.
29	- **Messages are self-delimiting `<EVENT>` envelopes.** `poll` output is rendered by splitting on
30	  the canonical `<EVENT type="msg" from="…">body</EVENT>` envelope, so a multi-message drain parses
31	  cleanly and each message keeps its sender for reply-correlation.
32
33	## Identity
34
35	Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
36	A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
37	no-ops rather than erroring.
38
39	## SessionStart briefs
40
41	`SessionStart` also relays an **agent-facing brief** as `additionalContext`, composed from the
42	adapter's `[strings.briefs]` (same file-backed/inline machinery as `[strings.skills]`). The hook
43	only selects + composes + `{id}`-substitutes — it never authors the prose.
44
45	| session state | trigger | brief |
46	| --- | --- | --- |
47	| has a perch | `bind` (`$SPT_ENDPOINT_ID`) or `boundary` (clear/compact) | **identity brief** — who it is (`{id}`), that its perch is already live (don't re-arm → `COLLISION`), and how to message (`spt send` + reply + the `spt endpoint list` roster). |
48	| no perch, node has subnet peers | `seed` / fresh startup | **ring brief** — how to reach other agents without a perch (`spt ring <target> --timeout 60`) + the roster. |
49	| no perch and no peers · subagent (`agent_type` set) | — | nothing. |
50
51	The brief is **liveness-agnostic** (no live-vs-ready distinction) pending a published machine-readable
52	liveness query on the spt surface. The peer gate is a line-count presence check on `spt subnet
53	status` — it never parses the human-formatted column values.
54
55	<!-- [doc->REQ-DIST-SESSIONSTART-BRIEF] -->
56
57	> The operative skill instructions are **not** in this plugin. They are delivered by the adapter
58	> manifest (conducted by spt-core) at invocation time; the `/sptc:*` `SKILL.md` files are
59	> deliberately thin skeletons. This split keeps the marketplace artifact low-churn while logic and
60	> instructions update through spt-core's signed adapter-update channel.
61

<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->
# Harness contract

`claude-spt` is glue: it maps Claude Code **hook events** to the `spt` binary's harness-contract
inbound surface (`spt api --adapter claude-spt <verb>`). The binary is harness-agnostic; this
adapter is the Claude-Code-shaped edge of it.

The authoritative contract lives on `spt-core`'s published surface — the
[harness-contract + CLI reference](https://sabermage.github.io/spt-releases). This page documents
the **adapter's** wiring: which Claude Code hook drives which `spt api` verb.

## Hook → `spt api` mapping

| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |

## Two invariants the wrappers hold

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read
  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.
- **Messages are self-delimiting `<EVENT>` envelopes.** `poll` output is rendered by splitting on
  the canonical `<EVENT type="msg" from="…">body</EVENT>` envelope, so a multi-message drain parses
  cleanly and each message keeps its sender for reply-correlation.

## Identity

Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
no-ops rather than erroring.

## SessionStart briefs

`SessionStart` also relays an **agent-facing brief** as `additionalContext`, composed from the
adapter's `[strings.briefs]` (same file-backed/inline machinery as `[strings.skills]`). The hook
only selects + composes + `{id}`-substitutes — it never authors the prose.

| session state | trigger | brief |
| --- | --- | --- |
| has a perch | `bind` (`$SPT_ENDPOINT_ID`) or `boundary` (clear/compact) | **identity brief** — who it is (`{id}`), that its perch is already live (don't re-arm → `COLLISION`), and how to message (`spt send` + reply + the `spt endpoint list` roster). |
| no perch, node has subnet peers | `seed` / fresh startup | **ring brief** — how to reach other agents without a perch (`spt ring <target> --timeout 60`) + the roster. |
| no perch and no peers · subagent (`agent_type` set) | — | nothing. |

The brief is **liveness-agnostic** (no live-vs-ready distinction) pending a published machine-readable
liveness query on the spt surface. The peer gate is a line-count presence check on `spt subnet
status` — it never parses the human-formatted column values.

<!-- [doc->REQ-DIST-SESSIONSTART-BRIEF] -->

> The operative skill instructions are **not** in this plugin. They are delivered by the adapter
> manifest (conducted by spt-core) at invocation time; the `/sptc:*` `SKILL.md` files are
> deliberately thin skeletons. This split keeps the marketplace artifact low-churn while logic and
> instructions update through spt-core's signed adapter-update channel.


Update the published harness-contract doc for the D1 architecture.


<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->
# Harness contract

<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against tools/claude-spt/src/hook.rs. Keep in lockstep with hooks.json + hook.rs. [doc->REQ-DOCS-SITE] -->
# Harness contract


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

<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against tools/claude-spt/src/hook.rs. Keep in lockstep with hooks.json + hook.rs. [doc->REQ-DOCS-SITE] -->
# Harness contract

<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->
# Harness contract

`claude-spt` is glue: it maps Claude Code **hook events** to the `spt` binary's harness-contract
inbound surface (`spt api --adapter claude-spt <verb>`). The binary is harness-agnostic; this
adapter is the Claude-Code-shaped edge of it.

The authoritative contract lives on `spt-core`'s published surface — the
[harness-contract + CLI reference](https://sabermage.github.io/spt-releases). This page documents
the **adapter's** wiring: which Claude Code hook drives which `spt api` verb.

## Hook → `spt api` mapping

| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |

## Two invariants the wrappers hold

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read
  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.
- **Messages are self-delimiting `<EVENT>` envelopes.** `poll` output is rendered by splitting on
  the canonical `<EVENT type="msg" from="…">body</EVENT>` envelope, so a multi-message drain parses
  cleanly and each message keeps its sender for reply-correlation.

## Identity

Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
no-ops rather than erroring.

## SessionStart briefs

`SessionStart` also relays an **agent-facing brief** as `additionalContext`, composed from the
adapter's `[strings.briefs]` (same file-backed/inline machinery as `[strings.skills]`). The hook
only selects + composes + `{id}`-substitutes — it never authors the prose.

| session state | trigger | brief |
| --- | --- | --- |
| has a perch | `bind` (`$SPT_ENDPOINT_ID`) or `boundary` (clear/compact) | **identity brief** — who it is (`{id}`), that its perch is already live (don't re-arm → `COLLISION`), and how to message (`spt send` + reply + the `spt endpoint list` roster). |
| no perch, node has subnet peers | `seed` / fresh startup | **ring brief** — how to reach other agents without a perch (`spt ring <target> --timeout 60`) + the roster. |
| no perch and no peers · subagent (`agent_type` set) | — | nothing. |

The brief is **liveness-agnostic** (no live-vs-ready distinction) pending a published machine-readable
liveness query on the spt surface. The peer gate is a line-count presence check on `spt subnet
status` — it never parses the human-formatted column values.

<!-- [doc->REQ-DIST-SESSIONSTART-BRIEF] -->

> The operative skill instructions are **not** in this plugin. They are delivered by the adapter
> manifest (conducted by spt-core) at invocation time; the `/sptc:*` `SKILL.md` files are
> deliberately thin skeletons. This split keeps the marketplace artifact low-churn while logic and
> instructions update through spt-core's signed adapter-update channel.

-     hand-authored against plugin/sptc/hooks/. Keep in lockstep with hooks.json. [doc->REQ-DOCS-SITE] -->

+     hand-authored against tools/claude-spt/src/hook.rs. Keep in lockstep with hooks.json + hook.rs. [doc->REQ-DOCS-SITE] -->


## Hook → `spt api` mapping

| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |

## Two invariants the wrappers hold

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read
  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.

## Hook → `spt api` mapping

The plugin ships a **static** `hooks.json` that routes every Claude Code hook event through one thin
wrapper, `hooks/dispatch.sh <EventName>`, which resolves the `claude-spt` program (from the adapter's
`[strings].hook_cmd = "{adapter_dir}/claude-spt hook"`, looked up once per session) and runs
`claude-spt hook <EventName>` with the Claude Code hook payload on stdin. The hook **logic lives in
the program** — so it updates with `spt adapter update`, and the plugin's hook wiring stays fixed.

| Claude Code hook   | handler                       | `spt api` verb (representative) | Purpose                                              |
| ------------------ | ----------------------------- | ------------------------------- | --------------------------------------------------- |
| `SessionStart`     | `claude-spt hook SessionStart`   | `seed` / `bind` / `boundary` | Bootstrap spt-core (via dispatch), register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `claude-spt hook UserPromptSubmit` | `state busy` + `poll`      | Mark the turn busy, drain delivered messages (incl. deferred) to the prompt as `additionalContext`, and inject a `/sptc:…` skill body when present. |
| `PreToolUse`       | `claude-spt hook PreToolUse`     | `state busy` + `poll`        | Mid-turn delivery: drain messages deferred while busy so a live agent receives them *while working*. |
| `Stop`             | `claude-spt hook Stop`           | `state` (idle)               | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `claude-spt hook SessionEnd`     | `session-end`                | Tear down session state cleanly.                    |
| `SubagentStart`    | `claude-spt hook SubagentStart`  | `worker-*`                   | Track a spawned subagent.                           |
| `SubagentStop`     | `claude-spt hook SubagentStop`   | `worker-*`                   | Track subagent completion.                          |
| `PostToolUse` (Write) | `claude-spt hook PostToolUse` | `state idle` + self-send     | Detect a `!!checkpoint!!` commune Write and self-send the agent-driven checkpoint signal (spt-hosted live agents). |

## Two invariants the handler holds

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The dispatch wrapper
  passes the payload straight through on **stdin** (its only argument is the event name) and the
  program reads the Claude Code hook payload as JSON from stdin, so a `/sptc:…` token is never corrupted.


## Hook → `spt api` mapping

The plugin ships a **static** `hooks.json` that routes every Claude Code hook event through one thin
wrapper, `hooks/dispatch.sh <EventName>`, which resolves the `claude-spt` program (from the adapter's
`[strings].hook_cmd = "{adapter_dir}/claude-spt hook"`, looked up once per session) and runs
`claude-spt hook <EventName>` with the Claude Code hook payload on stdin. The hook **logic lives in
the program** — so it updates with `spt adapter update`, and the plugin's hook wiring stays fixed.

| Claude Code hook   | handler                       | `spt api` verb (representative) | Purpose                                              |
| ------------------ | ----------------------------- | ------------------------------- | --------------------------------------------------- |
| `SessionStart`     | `claude-spt hook SessionStart`   | `seed` / `bind` / `boundary` | Bootstrap spt-core (via dispatch), register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `claude-spt hook UserPromptSubmit` | `state busy` + `poll`      | Mark the turn busy, drain delivered messages (incl. deferred) to the prompt as `additionalContext`, and inject a `/sptc:…` skill body when present. |
| `PreToolUse`       | `claude-spt hook PreToolUse`     | `state busy` + `poll`        | Mid-turn delivery: drain messages deferred while busy so a live agent receives them *while working*. |
| `Stop`             | `claude-spt hook Stop`           | `state` (idle)               | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `claude-spt hook SessionEnd`     | `session-end`                | Tear down session state cleanly.                    |
| `SubagentStart`    | `claude-spt hook SubagentStart`  | `worker-*`                   | Track a spawned subagent.                           |
| `SubagentStop`     | `claude-spt hook SubagentStop`   | `worker-*`                   | Track subagent completion.                          |
| `PostToolUse` (Write) | `claude-spt hook PostToolUse` | `state idle` + self-send     | Detect a `!!checkpoint!!` commune Write and self-send the agent-driven checkpoint signal (spt-hosted live agents). |

## Two invariants the handler holds

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The dispatch wrapper
  passes the payload straight through on **stdin** (its only argument is the event name) and the
  program reads the Claude Code hook payload as JSON from stdin, so a `/sptc:…` token is never corrupted.

<!-- Reference: the adapter's hook -> `spt api` mapping. Generated-from-truth target later; for now
     hand-authored against tools/claude-spt/src/hook.rs. Keep in lockstep with hooks.json + hook.rs. [doc->REQ-DOCS-SITE] -->
# Harness contract

`claude-spt` is glue: it maps Claude Code **hook events** to the `spt` binary's harness-contract
inbound surface (`spt api --adapter claude-spt <verb>`). The binary is harness-agnostic; this
adapter is the Claude-Code-shaped edge of it.

The authoritative contract lives on `spt-core`'s published surface — the
[harness-contract + CLI reference](https://sabermage.github.io/spt-releases). This page documents
the **adapter's** wiring: which Claude Code hook drives which `spt api` verb.

## Hook → `spt api` mapping

| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |
| ------------------ | --------------------------- | ------------------------ | --------------------------------------------------- |
| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |
| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |
| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |
| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |
| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |
| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |

## Two invariants the wrappers hold

- **Payload comes from stdin, never from a `/`-leading argument.** On Windows under Git Bash /
  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read
  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.
- **Messages are self-delimiting `<EVENT>` envelopes.** `poll` output is rendered by splitting on
  the canonical `<EVENT type="msg" from="…">body</EVENT>` envelope, so a multi-message drain parses
  cleanly and each message keeps its sender for reply-correlation.

## Identity

Every session resolves its own perch id via `spt whoami`, off `$OWL_SESSION_ID` / `$SPT_AGENT_ID`.
A session with no perch (never made reachable) simply delivers nothing — the per-prompt drain
no-ops rather than erroring.

## SessionStart briefs

`SessionStart` also relays an **agent-facing brief** as `additionalContext`, composed from the
adapter's `[strings.briefs]` (same file-backed/inline machinery as `[strings.skills]`). The hook
only selects + composes + `{id}`-substitutes — it never authors the prose.

| session state | trigger | brief |
| --- | --- | --- |
| has a perch | `bind` (`$SPT_ENDPOINT_ID`) or `boundary` (clear/compact) | **identity brief** — who it is (`{id}`), that its perch is already live (don't re-arm → `COLLISION`), and how to message (`spt send` + reply + the `spt endpoint list` roster). |
| no perch, node has subnet peers | `seed` / fresh startup | **ring brief** — how to reach other agents without a perch (`spt ring <target> --timeout 60`) + the roster. |
| no perch and no peers · subagent (`agent_type` set) | — | nothing. |

The brief is **liveness-agnostic** (no live-vs-ready distinction) pending a published machine-readable
liveness query on the spt surface. The peer gate is a line-count presence check on `spt subnet
status` — it never parses the human-formatted column values.

<!-- [doc->REQ-DIST-SESSIONSTART-BRIEF] -->

> The operative skill instructions are **not** in this plugin. They are delivered by the adapter
> manifest (conducted by spt-core) at invocation time; the `/sptc:*` `SKILL.md` files are
> deliberately thin skeletons. This split keeps the marketplace artifact low-churn while logic and
> instructions update through spt-core's signed adapter-update channel.

-| Claude Code hook   | sptc wrapper                | `spt api` verb           | Purpose                                              |

-| `SessionStart`     | `hooks/session-start.sh`    | `seed` / `bind` / `boundary` | Bootstrap spt-core, register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |

-| `UserPromptSubmit` | `hooks/user-prompt-submit.sh` | `poll`                 | Drain delivered messages and surface them to the prompt as `additionalContext`. |

-| `Stop`             | `hooks/stop.sh`             | `state` (idle)           | Mark the agent idle when a turn ends.               |

-| `SessionEnd`       | `hooks/session-end.sh`      | `session-end`            | Tear down session state cleanly.                    |

-| `SubagentStart`    | `hooks/subagent-start.sh`   | `worker-*`               | Track a spawned subagent.                           |

-| `SubagentStop`     | `hooks/subagent-stop.sh`    | `worker-*`               | Track subagent completion.                          |

+The plugin ships a **static** `hooks.json` that routes every Claude Code hook event through one thin

+wrapper, `hooks/dispatch.sh <EventName>`, which resolves the `claude-spt` program (from the adapter's

+`[strings].hook_cmd = "{adapter_dir}/claude-spt hook"`, looked up once per session) and runs

+`claude-spt hook <EventName>` with the Claude Code hook payload on stdin. The hook **logic lives in

+the program** — so it updates with `spt adapter update`, and the plugin's hook wiring stays fixed.

+| Claude Code hook   | handler                       | `spt api` verb (representative) | Purpose                                              |

+| `SessionStart`     | `claude-spt hook SessionStart`   | `seed` / `bind` / `boundary` | Bootstrap spt-core (via dispatch), register the perch (`bind` spt-hosted · `seed` harness-hosted · `boundary` on clear/compact), then relay an agent-facing brief (see below); non-blocking — never `listen`. |

+| `UserPromptSubmit` | `claude-spt hook UserPromptSubmit` | `state busy` + `poll`      | Mark the turn busy, drain delivered messages (incl. deferred) to the prompt as `additionalContext`, and inject a `/sptc:…` skill body when present. |

+| `PreToolUse`       | `claude-spt hook PreToolUse`     | `state busy` + `poll`        | Mid-turn delivery: drain messages deferred while busy so a live agent receives them *while working*. |

+| `Stop`             | `claude-spt hook Stop`           | `state` (idle)               | Mark the agent idle when a turn ends.               |

+| `SessionEnd`       | `claude-spt hook SessionEnd`     | `session-end`                | Tear down session state cleanly.                    |

+| `SubagentStart`    | `claude-spt hook SubagentStart`  | `worker-*`                   | Track a spawned subagent.                           |

+| `SubagentStop`     | `claude-spt hook SubagentStop`   | `worker-*`                   | Track subagent completion.                          |

+| `PostToolUse` (Write) | `claude-spt hook PostToolUse` | `state idle` + self-send     | Detect a `!!checkpoint!!` commune Write and self-send the agent-driven checkpoint signal (spt-hosted live agents). |

-  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The wrappers read

-  the Claude Code hook payload as JSON from **stdin**, so a `/sptc:…` token is never corrupted.

+  MSYS, any argument beginning with `/` is silently rewritten to a Windows path. The dispatch wrapper

+  passes the payload straight through on **stdin** (its only argument is the event name) and the

+  program reads the Claude Code hook payload as JSON from stdin, so a `/sptc:…` token is never corrupted.


Regenerate docs (llms.txt) + re-run the docs-drift gate.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/docs/gen-llms.sh 2>&1 | tail -5
echo "=== docs check ==="
sh ci/docs/check-docs.sh 2>&1 | tail -10

Regenerate llms.txt + run docs drift check


wrote /c/Users/decid/Documents/projects/spt-claude-code/docs-site/llms.txt
=== docs check ===
ok   mdbook build
ok   llms.txt in sync


Docs in sync. Update plan status, then final full-gate run before committing.


Edit

## Status: IN PROGRESS (started 2026-06-28, perri)

## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

- hook.rs (8 events + _common.sh ported), main.rs `hook` dispatch, dispatch.sh, static hooks.json,
  [strings].hook_cmd, 8 wrappers + _common.sh deleted. All default gates PASS (89 cargo tests,
  traceable-reqs green incl. new REQ-DIST-HOOK-BINARY doc/impl/unit, msys-hazard reworked, docs-drift).
- Adapter.spt repacked (win + linux); translate-proof + digest-proof + hook subcommand dogfooded from
  the extracted binary. CHANGELOG [0.9.0], plugin.json sptc 0.1.8, manifest 0.9.0.
- poll-int live binary-drain e2e blocked on THIS node (2 subnets → bind needs --subnet); covered by
  hook.rs unit tests + live skill-injection dogfood. Live full-perch e2e stays in the deferred
  harness-glue bucket (needs single-subnet/real-CC node). registration-int 4c reworked to the binary
  (not run live here — would soft-deregister the live node's adapter).
- Remaining: commit · tag v0.9.0 + push · gh release w/ dist/adapter.spt · cplugs sptc 0.1.8 push.


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

## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

- hook.rs (8 events + _common.sh ported), main.rs `hook` dispatch, dispatch.sh, static hooks.json,
  [strings].hook_cmd, 8 wrappers + _common.sh deleted. All default gates PASS (89 cargo tests,
  traceable-reqs green incl. new REQ-DIST-HOOK-BINARY doc/impl/unit, msys-hazard reworked, docs-drift).
- Adapter.spt repacked (win + linux); translate-proof + digest-proof + hook subcommand dogfooded from
  the extracted binary. CHANGELOG [0.9.0], plugin.json sptc 0.1.8, manifest 0.9.0.
- poll-int live binary-drain e2e blocked on THIS node (2 subnets → bind needs --subnet); covered by
  hook.rs unit tests + live skill-injection dogfood. Live full-perch e2e stays in the deferred
  harness-glue bucket (needs single-subnet/real-CC node). registration-int 4c reworked to the binary
  (not run live here — would soft-deregister the live node's adapter).
- Remaining: commit · tag v0.9.0 + push · gh release w/ dist/adapter.spt · cplugs sptc 0.1.8 push.

# v0.9.0 — D1: hook logic → `claude-spt hook <event>` (static-forever hooks.json)

> JIT plan. The DEFERRED-D1 milestone (UNIFY-CONSOLIDATE-PLAN §D1 / ADR-0006 ask #1).
> Goal: move all hook **logic** out of the cplugs plugin shell and into the consolidated
> `claude-spt` binary, so hook churn rides `spt adapter update` (NOT a cplugs republish).
> The plugin `hooks.json` + a thin dispatch wrapper become **static-forever**.

## Design (locked — ADR-0006, UPDATE-NAMING-DOYLE-ASKS Ask 1 → RESOLVE-NOT-EXECUTE)

spt-core stays a pure resolver (executes no new thing). Two v0.16.0 primitives carry D1 — BOTH
verified live on this node's spt 0.16.0 (throwaway probe-d1 adapter, 2026-06-28):
- **(a) `{adapter_dir}` / `{adapter_name}` substitution keys.** `get-string` of a `[strings]` value
  containing `{adapter_dir}` returns the install dir path. Confirmed: `hook_cmd = "{adapter_dir}/claude-spt hook"`
  → `<install_dir>/claude-spt hook`.
- **(b) lazy substitution inside `[strings]` at `get-string` read time** (adapter-static keys only;
  session-scoped `{session_id}` NOT available there — hooks still read those from CC stdin).

### Components

1. **`tools/claude-spt/src/hook.rs`** (NEW) — `claude-spt hook <event>` subcommand. Reads the event
   name from argv + the CC hook payload JSON from stdin; shells `spt api …`; emits additionalContext.
   Faithful port of all 8 hook wrappers + `_common.sh` (pure helpers → pure fns with unit tests;
   impure resolvers → `spt`-spawning fns). **Behaviour-preserving refactor**: same `spt api` command
   lines, same stdout contract per event (SessionStart = `hookSpecificOutput` JSON; UPS/PreToolUse =
   raw rendered text), same no-op-when-no-perch.
2. **`tools/claude-spt/src/main.rs`** — add `Sub::Hook`; dispatch `hook` → `hook::run()`.
3. **`plugin/sptc/hooks/dispatch.sh`** (NEW, static-forever) — the only plugin-resident hook shell.
   - SessionStart only: run `bootstrap.sh` first (invisible installer — the binary CANNOT exist before
     spt-core + the adapter are installed), then cache the resolved bin to `$CLAUDE_ENV_FILE`.
   - Resolve bin: `$SPTC_HOOK_BIN` (cached) → else `spt adapter get-string claude-spt hook_cmd`.
   - No-op (exit 0) if unresolvable (adapter not registered yet = pre-/sptc:setup window).
   - Pass `--host-pid "$PPID"` (the seed pid — Rust std has no portable getppid on Windows) and the
     event; the binary reads CC stdin directly (dispatch does NOT cat, so stdin passes through clean).
4. **`plugin/sptc/hooks/hooks.json`** — static-forever: every event → `sh dispatch.sh <event>`.
5. **`adapter/claude-spt.toml`** — add `[strings] hook_cmd = "{adapter_dir}/claude-spt hook"`;
   version → 0.9.0; min_spt_core stays 0.16.0 (primitives already shipped there); `[hooks.*]` comments
   note the binary handler.
6. **Delete** the 8 `*.sh` hook wrappers + `_common.sh` (logic now in the binary). **Keep** `bootstrap.sh`.
7. **Tests** — port `tests/hooks-parse.sh` coverage into `hook.rs` `#[cfg(test)]` (runs under
   `cargo test`, the digest-extractor gate). Replace `tests/hooks-parse.sh` with a thin
   `tests/hooks-dispatch.sh` (hooks.json static shape + dispatch wrapper sh -n + referenced files exist).
8. **traceable-reqs.toml** — add `REQ-DIST-HOOK-BINARY` (doc/impl/unit), activate; retag the migrated
   REQ evidence (REQ-DIST-HOOKS-API / REQ-UPS-INJECTION / REQ-DIST-* unit tags now point at hook.rs).
9. **cplugs skeleton** — STRUCTURAL change (hooks.json rewrite + .sh removal + dispatch.sh add) →
   `plugin.json` sptc 0.1.7 → 0.1.8.

### Bootstrap / no-op invariant (the one behavioural nuance)

A session that ran the spt-core bootstrap but is BEFORE `/sptc:setup` (adapter not yet registered)
has no `{adapter_dir}/claude-spt` → dispatch no-ops. Today's shell `api seed` would run there; the
binary path won't. Acceptable: a seed for an UNregistered adapter is unusable (ready/live resolve the
adapter by host_binaries from the registered set). Next session post-setup seeds normally. No real-world
regression (the perch was already a no-op pre-readiness today).

## Gate

`sh ci/run-gates.sh` green (shell-syntax, cargo test+build incl. new hook tests, traceable-reqs check,
skeleton-validate, manifest-schema) + a local idle/digest-proof dogfood of the repacked binary.

## Release (see it through — docs/RELEASE-RUNBOOK.md)

CHANGELOG `## [0.9.0]` · build win + linux (zigbuild) · `package-adapter.sh --apply` · gh release
v0.9.0 on SaberMage/claude-spt with `dist/adapter.spt` · cplugs sptc 0.1.8 push. gh = SaberMage
(repo+workflow scopes) — publish is doable from this node.

## Status: IN PROGRESS (started 2026-06-28, perri)

-## Status: IN PROGRESS (started 2026-06-28, perri)

+## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

+- hook.rs (8 events + _common.sh ported), main.rs `hook` dispatch, dispatch.sh, static hooks.json,

+  [strings].hook_cmd, 8 wrappers + _common.sh deleted. All default gates PASS (89 cargo tests,

+  traceable-reqs green incl. new REQ-DIST-HOOK-BINARY doc/impl/unit, msys-hazard reworked, docs-drift).

+- Adapter.spt repacked (win + linux); translate-proof + digest-proof + hook subcommand dogfooded from

+  the extracted binary. CHANGELOG [0.9.0], plugin.json sptc 0.1.8, manifest 0.9.0.

+- poll-int live binary-drain e2e blocked on THIS node (2 subnets → bind needs --subnet); covered by

+  hook.rs unit tests + live skill-injection dogfood. Live full-perch e2e stays in the deferred

+  harness-glue bucket (needs single-subnet/real-CC node). registration-int 4c reworked to the binary

+  (not run live here — would soft-deregister the live node's adapter).

+- Remaining: commit · tag v0.9.0 + push · gh release w/ dist/adapter.spt · cplugs sptc 0.1.8 push.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/run-gates.sh 2>&1 | grep -E "RESULT|FAIL"

Final full-gate confirmation


=== RESULT: PASS ===


All gates PASS. Commit (project convention: release commits land on main, tagged from main).


cd /c/Users/decid/Documents/projects/spt-claude-code
git add -A
git status --short | head -30

Stage all changes


warning: in the working copy of 'tools/claude-spt/src/main.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tools/claude-spt/src/hook.rs', LF will be replaced by CRLF the next time Git touches it
M  CHANGELOG.md
A  V09-PLAN.md
M  adapter/claude-spt.toml
M  ci/hooks/poll-int.sh
M  ci/manifest/registration-int.sh
M  docs-site/src/reference/harness-contract.md
M  docs/adr/0006-one-command-update-and-consolidated-binary.md
M  plugin/sptc/.claude-plugin/plugin.json
D  plugin/sptc/hooks/_common.sh
A  plugin/sptc/hooks/dispatch.sh
M  plugin/sptc/hooks/hooks.json
D  plugin/sptc/hooks/post-tool-use.sh
D  plugin/sptc/hooks/pre-tool-use.sh
D  plugin/sptc/hooks/session-end.sh
D  plugin/sptc/hooks/session-start.sh
D  plugin/sptc/hooks/stop.sh
D  plugin/sptc/hooks/subagent-start.sh
D  plugin/sptc/hooks/subagent-stop.sh
D  plugin/sptc/hooks/user-prompt-submit.sh
A  tests/hooks-dispatch.sh
D  tests/hooks-parse.sh
M  tests/msys-hazard.sh
A  tools/claude-spt/src/hook.rs
M  tools/claude-spt/src/main.rs
M  traceable-reqs.toml


cd /c/Users/decid/Documents/projects/spt-claude-code
git commit -q -F - <<'EOF'
feat(v0.9.0): D1 — hook logic → `claude-spt hook <event>` (static-forever hooks.json)

Move all CC hook logic out of the cplugs plugin shell into the consolidated
claude-spt binary so it rides `spt adapter update`, not a cplugs republish
(ADR-0006 ask #1 → resolve-not-execute; spt-core v0.16.0 {adapter_dir} + lazy
[strings] subst, both verified live on 0.16.0).

- tools/claude-spt/src/hook.rs: faithful port of the 8 hook wrappers + _common.sh
  (HookEnv side-effect seam; pure helpers + recorder-driven handler tests for
  ordering/gating/emit). main.rs gains the `hook` subcommand.
- plugin: static-forever hooks.json (every event -> dispatch.sh <event>) + thin
  dispatch.sh (resolve [strings].hook_cmd, cache SPTC_HOOK_BIN, bootstrap on
  SessionStart, --host-pid); deleted the 8 .sh wrappers + _common.sh.
- adapter: [strings].hook_cmd = "{adapter_dir}/claude-spt hook"; version 0.9.0.
- tests/hooks-dispatch.sh (static wiring) replaces tests/hooks-parse.sh; reworked
  tests/msys-hazard.sh; ci poll-int + registration-int repointed to the binary.
- REQ-DIST-HOOK-BINARY (doc/impl/unit); CHANGELOG [0.9.0]; cplugs sptc 0.1.8.

Behaviour-preserving refactor. 89 crate tests, all gates green; adapter.spt
repacked (win+linux) and translate/digest/hook dogfooded from the extracted binary.

Co-authored by: perri
EOF
git log --oneline -1

Commit v0.9.0


3a255c4 feat(v0.9.0): D1 — hook logic → `claude-spt hook <event>` (static-forever hooks.json)


Committed. Now push main, tag v0.9.0, and push the tag (triggers docs-publish).


cd /c/Users/decid/Documents/projects/spt-claude-code
git push origin main 2>&1 | tail -5
echo "=== tag ==="
git tag v0.9.0 && git push origin v0.9.0 2>&1 | tail -5

Push main + tag v0.9.0


hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
=== tag ===
To https://github.com/SaberMage/claude-spt.git
 * [new tag]         v0.9.0 -> v0.9.0


Main push rejected — remote main advanced (something pushed). Tag pushed but main didn't. Investigate before integrating.


cd /c/Users/decid/Documents/projects/spt-claude-code
git fetch origin 2>&1 | tail -3
echo "=== local vs remote main ==="
git log --oneline origin/main -5
echo "=== my local main ==="
git log --oneline main -3
echo "=== divergence ==="
git rev-list --left-right --count origin/main...main

Fetch + inspect divergence


   5a1c3f2..193efed  main       -> origin/main
 * [new tag]         v0.6.1     -> v0.6.1
 * [new tag]         v0.8.0     -> v0.8.0
=== local vs remote main ===
193efed Revise README for clarity and succinctness
5a1c3f2 docs(plan): v0.8.0 SHIPPED — D2/D3 + bumps published (adapter v0.8.0 + cplugs 0.1.7); D1 deferred
2f9de99 chore(release): v0.8.0 — bump version + min_spt_core 0.16.0 + CHANGELOG + skeleton 0.1.7
d7122ef feat(v0.8.0): D2 — wire [update.post] composite update (spt-core v0.16.0)
d0ab354 feat(v0.8.0): D3 — fold translate into claude-spt; one tool binary (spt-core v0.16.0)
=== my local main ===
3a255c4 feat(v0.9.0): D1 — hook logic → `claude-spt hook <event>` (static-forever hooks.json)
5a1c3f2 docs(plan): v0.8.0 SHIPPED — D2/D3 + bumps published (adapter v0.8.0 + cplugs 0.1.7); D1 deferred
2f9de99 chore(release): v0.8.0 — bump version + min_spt_core 0.16.0 + CHANGELOG + skeleton 0.1.7
=== divergence ===
1	1


Remote added a README revision (separate from my changes). Rebase my commit on top, then re-point the tag.


cd /c/Users/decid/Documents/projects/spt-claude-code
git rebase origin/main 2>&1 | tail -8
echo "=== new HEAD ==="
git log --oneline -2

Rebase my commit onto origin/main


Rebasing (1/1)

Successfully rebased and updated refs/heads/main.
=== new HEAD ===
d7fb440 feat(v0.9.0): D1 — hook logic → `claude-spt hook <event>` (static-forever hooks.json)
193efed Revise README for clarity and succinctness


Clean rebase. Quick traceable-reqs recheck (README is a scan root), then push main + re-point tag.


cd /c/Users/decid/Documents/projects/spt-claude-code
traceable-reqs check >/dev/null 2>&1 && echo "traceable-reqs: GREEN" || { echo "traceable-reqs: FAIL"; traceable-reqs check 2>&1 | tail -5; }
echo "=== push main ==="
git push origin main 2>&1 | tail -4
echo "=== re-point tag to rebased commit ==="
git tag -d v0.9.0 && git tag v0.9.0 && git push -f origin v0.9.0 2>&1 | tail -3

Recheck reqs, push main, re-point tag


traceable-reqs: GREEN
=== push main ===
To https://github.com/SaberMage/claude-spt.git
   193efed..d7fb440  main -> main
=== re-point tag to rebased commit ===
Deleted tag 'v0.9.0' (was 3a255c4)
To https://github.com/SaberMage/claude-spt.git
 + 3a255c4...d7fb440 v0.9.0 -> v0.9.0 (forced update)


Main + tag pushed. Now create the GitHub release with the adapter.spt asset, body = CHANGELOG [0.9.0] section.


cd /c/Users/decid/Documents/projects/spt-claude-code
SCRATCH="/c/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/7a919ad5-82a3-4e01-8754-d635c0702c15/scratchpad"
mkdir -p "$SCRATCH"
awk '/^## \[0.9.0\]/{f=1;next} /^## \[0.8.0\]/{f=0} f' CHANGELOG.md > "$SCRATCH/relnotes.md"
echo "=== release body preview ==="; cat "$SCRATCH/relnotes.md"
echo "=== asset ==="; ls -la dist/adapter.spt

Extract changelog section + verify asset


=== release body preview ===

> Requires spt-core **v0.16.0 or newer** (unchanged from 0.8.0). This release is an internal
> re-plumbing of how Claude Code hooks run — there is **no change to what you do or see**. After
> updating, run `/reload-plugins` (or restart Claude Code) once, as the update reminds you.

### Changed
- **Hook behaviour now updates with `spt adapter update` — no plugin reinstall needed.** Previously,
  any change to how a hook works (message delivery, the live-agent checkpoint, the session briefing)
  required a separate Claude Code plugin update. Now that logic lives in the `claude-spt` program that
  `spt adapter update` already refreshes, so a single update keeps hooks current. The plugin's hook
  wiring is now fixed and rarely needs a marketplace bump again.

### Fixed
- Nothing user-visible. This is a behaviour-preserving refactor: messaging, the `/sptc:*` skills, the
  live-agent checkpoint (`/sptc:commune --checkpoint`), and the session briefing all work exactly as
  before — they are just driven by the consolidated `claude-spt` program instead of separate hook
  scripts shipped in the plugin.

=== asset ===
-rw-r--r-- 1 decid 197609 549411 Jun 28 01:42 dist/adapter.spt


cd /c/Users/decid/Documents/projects/spt-claude-code
SCRATCH="/c/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/7a919ad5-82a3-4e01-8754-d635c0702c15/scratchpad"
gh release create v0.9.0 dist/adapter.spt \
  --repo SaberMage/claude-spt \
  --title "v0.9.0 — D1: hook logic in the binary" \
  --notes-file "$SCRATCH/relnotes.md" 2>&1 | tail -5

Create GitHub release v0.9.0 with adapter.spt


Release created with asset. Verify it, then handle cplugs sptc 0.1.8.


cd /c/Users/decid/Documents/projects/spt-claude-code
echo "=== release assets ==="
gh release view v0.9.0 --repo SaberMage/claude-spt --json assets --jq '.assets[].name' 2>&1
echo "=== cplugs marketplace clone present? ==="
ls -d ~/.claude/plugins/marketplaces/cplugs 2>/dev/null && echo "PRESENT" || echo "ABSENT"
echo "=== package-skeleton.sh head ==="
sed -n '1,30p' ci/publish/package-skeleton.sh

Verify release asset + locate cplugs clone + read skeleton packer


=== release assets ===
adapter.spt
=== cplugs marketplace clone present? ===
/c/Users/decid/.claude/plugins/marketplaces/cplugs
PRESENT
=== package-skeleton.sh head ===
#!/bin/sh
# Stage the sptc cplugs SKELETON into a marketplace clone — the runbook "per skeleton bump" cp
# mechanic as a real, idempotent, validated script. DRY-RUN by default: prints the plan and stages
# nothing. Never pushes (marketplace credentials + the commit/push are the operator's step).
# [impl->REQ-DIST-PLUGIN-SKELETON]
#
# Usage:
#   package-skeleton.sh                 # dry-run against the default $MARKET
#   package-skeleton.sh --apply         # actually copy the skeleton subset into $MARKET
#   MARKET=/path/to/cplugs/plugins/sptc package-skeleton.sh --apply
set -u
HERE=$(CDPATH= cd "$(dirname "$0")" && pwd)
ROOT=$(CDPATH= cd "$HERE/../.." && pwd)
PLUGIN="$ROOT/plugin/sptc"
MARKET="${MARKET:-$HOME/.claude/plugins/marketplaces/cplugs/plugins/sptc}"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

# Validate first — refuse to stage an invalid / leaky skeleton.
echo "== validate skeleton =="
if ! sh "$HERE/validate-skeleton.sh" "$PLUGIN"; then
  echo "REFUSING to package: skeleton failed validation (fix above, then retry)." >&2
  exit 1
fi

# The skeleton SUBSET — exactly what cplugs carries (no binary, no manifest, no runtime state).
SUBSET=".claude-plugin/plugin.json hooks skills bootstrap.sh bootstrap.ps1"

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


Confirmed: asset attached, cplugs clone present. Check whether the skeleton packer cleans stale destination files (old .sh wrappers must not linger in the cplugs hooks dir).


cd /c/Users/decid/Documents/projects/spt-claude-code
sed -n '30,75p' ci/publish/package-skeleton.sh
echo "=== current cplugs sptc hooks dir ==="
ls -la ~/.claude/plugins/marketplaces/cplugs/plugins/sptc/hooks/ 2>/dev/null

Read packer copy logic + inspect destination hooks


echo "== plan ($([ "$APPLY" -eq 1 ] && echo APPLY || echo DRY-RUN)) =="
echo "source : $PLUGIN"
echo "target : $MARKET"
for item in $SUBSET; do
  if [ -e "$PLUGIN/$item" ]; then echo "  stage  $item"; else echo "  WARN   missing source: $item"; fi
done

if [ "$APPLY" -ne 1 ]; then
  echo
  echo "DRY-RUN: nothing copied. Re-run with --apply to stage into \$MARKET, then commit+push the"
  echo "cplugs repo and 'claude plugin install sptc@cplugs' (see docs/RELEASE-RUNBOOK.md)."
  exit 0
fi

mkdir -p "$MARKET/.claude-plugin" "$MARKET/hooks" "$MARKET/skills" || { echo "FATAL: cannot create $MARKET"; exit 2; }
cp "$PLUGIN/.claude-plugin/plugin.json" "$MARKET/.claude-plugin/"
# Replace dir contents wholesale so removed skeleton files don't linger in the marketplace clone.
rm -rf "$MARKET/hooks" "$MARKET/skills"
cp -r "$PLUGIN/hooks" "$MARKET/hooks"
cp -r "$PLUGIN/skills" "$MARKET/skills"
for b in bootstrap.sh bootstrap.ps1; do [ -f "$PLUGIN/$b" ] && cp "$PLUGIN/$b" "$MARKET/"; done

echo
echo "STAGED into $MARKET. Next (operator): cd into the cplugs repo, 'git add plugins/sptc/',"
echo "commit + push, then 'claude plugin install sptc@cplugs' + /reload-plugins."
exit 0
=== current cplugs sptc hooks dir ===
total 48
drwxr-xr-x 1 decid 197609     0 Jun 26 03:13 .
drwxr-xr-x 1 decid 197609     0 Jun 26 03:13 ..
-rw-r--r-- 1 decid 197609 14203 Jun 26 03:13 _common.sh
-rw-r--r-- 1 decid 197609  1266 Jun 26 03:13 hooks.json
-rwxr-xr-x 1 decid 197609  2694 Jun 26 03:13 post-tool-use.sh
-rwxr-xr-x 1 decid 197609  2488 Jun 26 03:13 pre-tool-use.sh
-rwxr-xr-x 1 decid 197609   461 Jun 26 03:13 session-end.sh
-rwxr-xr-x 1 decid 197609  4083 Jun 26 03:13 session-start.sh
-rwxr-xr-x 1 decid 197609   429 Jun 26 03:13 stop.sh
-rwxr-xr-x 1 decid 197609   483 Jun 26 03:13 subagent-start.sh
-rwxr-xr-x 1 decid 197609   395 Jun 26 03:13 subagent-stop.sh
-rwxr-xr-x 1 decid 197609  2551 Jun 26 03:13 user-prompt-submit.sh


Packer does wholesale `rm -rf` + copy, so stale wrappers won't linger. Stage the skeleton.


cd /c/Users/decid/Documents/projects/spt-claude-code
sh ci/publish/package-skeleton.sh --apply 2>&1 | tail -8
echo "=== staged cplugs hooks ==="; ls ~/.claude/plugins/marketplaces/cplugs/plugins/sptc/hooks/
echo "=== staged plugin.json version ==="; grep version ~/.claude/plugins/marketplaces/cplugs/plugins/sptc/.claude-plugin/plugin.json

Stage skeleton into cplugs clone


  stage  .claude-plugin/plugin.json
  stage  hooks
  stage  skills
  stage  bootstrap.sh
  stage  bootstrap.ps1

STAGED into /c/Users/decid/.claude/plugins/marketplaces/cplugs/plugins/sptc. Next (operator): cd into the cplugs repo, 'git add plugins/sptc/',
commit + push, then 'claude plugin install sptc@cplugs' + /reload-plugins.
=== staged cplugs hooks ===
dispatch.sh
hooks.json
=== staged plugin.json version ===
  "version": "0.1.8",


Staged clean (dispatch.sh + hooks.json only, v0.1.8). Commit + push the cplugs marketplace.


CP=~/.claude/plugins/marketplaces/cplugs
git -C "$CP" status --short
echo "=== branch ==="; git -C "$CP" rev-parse --abbrev-ref HEAD
echo "=== pull latest first ==="; git -C "$CP" pull --ff-only 2>&1 | tail -3

Check cplugs repo status + pull latest


 M plugins/sptc/.claude-plugin/plugin.json
 D plugins/sptc/hooks/_common.sh
 M plugins/sptc/hooks/hooks.json
 D plugins/sptc/hooks/post-tool-use.sh
 D plugins/sptc/hooks/pre-tool-use.sh
 D plugins/sptc/hooks/session-end.sh
 D plugins/sptc/hooks/session-start.sh
 D plugins/sptc/hooks/stop.sh
 D plugins/sptc/hooks/subagent-start.sh
 D plugins/sptc/hooks/subagent-stop.sh
 D plugins/sptc/hooks/user-prompt-submit.sh
?? plugins/sptc/hooks/dispatch.sh
=== branch ===
main
=== pull latest first ===
Already up to date.


CP=~/.claude/plugins/marketplaces/cplugs
git -C "$CP" add plugins/sptc/
git -C "$CP" commit -q -F - <<'EOF'
sptc 0.1.8: static-forever hooks (D1) — hooks.json -> dispatch.sh -> claude-spt hook

Hook logic moved into the claude-spt binary (rides spt adapter update); the
skeleton now ships only a static hooks.json + dispatch.sh. Removed the 8 per-event
.sh wrappers + _common.sh. Pairs with adapter/release v0.9.0.

Co-authored by: perri
EOF
git -C "$CP" push origin main 2>&1 | tail -4
echo "=== cplugs HEAD ==="; git -C "$CP" log --oneline -1

Commit + push cplugs sptc 0.1.8


warning: in the working copy of 'plugins/sptc/.claude-plugin/plugin.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugins/sptc/hooks/hooks.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'plugins/sptc/hooks/dispatch.sh', LF will be replaced by CRLF the next time Git touches it
To https://github.com/SaberMage/cplugs.git
   bb1bbed..2989326  main -> main
=== cplugs HEAD ===
2989326 sptc 0.1.8: static-forever hooks (D1) — hooks.json -> dispatch.sh -> claude-spt hook


Both targets live: release v0.9.0 (adapter.spt) + cplugs sptc 0.1.8. Check the docs-publish workflow the tag triggered.


cd /c/Users/decid/Documents/projects/spt-claude-code
gh run list --repo SaberMage/claude-spt --limit 4 2>&1 | head -8

Check recent workflow runs


(Bash completed with no output)


cd /c/Users/decid/Documents/projects/spt-claude-code
ls -la .github/workflows/ 2>/dev/null || echo "no .github/workflows dir"
gh workflow list --repo SaberMage/claude-spt 2>&1 | head

Check for GitHub Actions workflows


no .github/workflows dir


No Actions workflow configured (project is "light" — local gates are the CI; docs-drift ran green locally). Release is complete. Final integrity check: download the *published* asset and confirm it carries the v0.9.0 manifest + hook_cmd.


cd /c/Users/decid/Documents/projects/spt-claude-code
SCRATCH="/c/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-claude-code/7a919ad5-82a3-4e01-8754-d635c0702c15/scratchpad"
gh release download v0.9.0 --repo SaberMage/claude-spt --dir "$SCRATCH/dl" --clobber 2>&1 | tail -2
echo "=== published asset: manifest version + hook_cmd ==="
tar xOf "$SCRATCH/dl/adapter.spt" manifest.toml | grep -E '^version|hook_cmd'
echo "=== published asset: triples + binary ==="
tar tf "$SCRATCH/dl/adapter.spt" | grep -E 'claude-spt(\.exe)?$'
rm -rf "$SCRATCH/dl"

Download published asset + verify manifest version + hook_cmd + binaries


=== published asset: manifest version + hook_cmd ===
# the lazily-substituted [strings].hook_cmd = "{adapter_dir}/claude-spt hook" (ADR-0006 ask #1 →
version = "0.9.0"
# (resolved via [strings].hook_cmd above). These declarations stay one-for-one with that wiring.
# hook_cmd (D1, REQ-DIST-HOOK-BINARY): the resolve-not-execute primitive that lets the plugin's
hook_cmd = "{adapter_dir}/claude-spt hook"
version = { file = "skills/version.md" }
=== published asset: triples + binary ===
x86_64-pc-windows-msvc/claude-spt.exe
x86_64-unknown-linux-gnu/claude-spt


Published asset verified end-to-end (v0.9.0, hook_cmd, both binaries). v0.9.0 fully shipped. Mark plan SHIPPED + commit.


Edit

## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

## Status: SHIPPED (2026-06-28, perri)

**PUBLISHED both targets.** GitHub release **v0.9.0** on SaberMage/claude-spt (commit d7fb440) with
the fat `adapter.spt` (win+linux, manifest 0.9.0 + hook_cmd verified from the downloaded asset) +
cplugs **sptc 0.1.8** (static hooks.json + dispatch.sh; 8 wrappers + _common.sh removed). main pushed.
No GitHub Actions workflow in this repo — local gates ARE the CI (all green). On-node update verify
(`spt adapter update claude-spt` ×2 for [update.post]) DEFERRED — would mutate this live node's
registration mid-session; safe to do on a non-live node.

### Build log


## Status: SHIPPED (2026-06-28, perri)

**PUBLISHED both targets.** GitHub release **v0.9.0** on SaberMage/claude-spt (commit d7fb440) with
the fat `adapter.spt` (win+linux, manifest 0.9.0 + hook_cmd verified from the downloaded asset) +
cplugs **sptc 0.1.8** (static hooks.json + dispatch.sh; 8 wrappers + _common.sh removed). main pushed.
No GitHub Actions workflow in this repo — local gates ARE the CI (all green). On-node update verify
(`spt adapter update claude-spt` ×2 for [update.post]) DEFERRED — would mutate this live node's
registration mid-session; safe to do on a non-live node.

### Build log

# v0.9.0 — D1: hook logic → `claude-spt hook <event>` (static-forever hooks.json)

> JIT plan. The DEFERRED-D1 milestone (UNIFY-CONSOLIDATE-PLAN §D1 / ADR-0006 ask #1).
> Goal: move all hook **logic** out of the cplugs plugin shell and into the consolidated
> `claude-spt` binary, so hook churn rides `spt adapter update` (NOT a cplugs republish).
> The plugin `hooks.json` + a thin dispatch wrapper become **static-forever**.

## Design (locked — ADR-0006, UPDATE-NAMING-DOYLE-ASKS Ask 1 → RESOLVE-NOT-EXECUTE)

spt-core stays a pure resolver (executes no new thing). Two v0.16.0 primitives carry D1 — BOTH
verified live on this node's spt 0.16.0 (throwaway probe-d1 adapter, 2026-06-28):
- **(a) `{adapter_dir}` / `{adapter_name}` substitution keys.** `get-string` of a `[strings]` value
  containing `{adapter_dir}` returns the install dir path. Confirmed: `hook_cmd = "{adapter_dir}/claude-spt hook"`
  → `<install_dir>/claude-spt hook`.
- **(b) lazy substitution inside `[strings]` at `get-string` read time** (adapter-static keys only;
  session-scoped `{session_id}` NOT available there — hooks still read those from CC stdin).

### Components

1. **`tools/claude-spt/src/hook.rs`** (NEW) — `claude-spt hook <event>` subcommand. Reads the event
   name from argv + the CC hook payload JSON from stdin; shells `spt api …`; emits additionalContext.
   Faithful port of all 8 hook wrappers + `_common.sh` (pure helpers → pure fns with unit tests;
   impure resolvers → `spt`-spawning fns). **Behaviour-preserving refactor**: same `spt api` command
   lines, same stdout contract per event (SessionStart = `hookSpecificOutput` JSON; UPS/PreToolUse =
   raw rendered text), same no-op-when-no-perch.
2. **`tools/claude-spt/src/main.rs`** — add `Sub::Hook`; dispatch `hook` → `hook::run()`.
3. **`plugin/sptc/hooks/dispatch.sh`** (NEW, static-forever) — the only plugin-resident hook shell.
   - SessionStart only: run `bootstrap.sh` first (invisible installer — the binary CANNOT exist before
     spt-core + the adapter are installed), then cache the resolved bin to `$CLAUDE_ENV_FILE`.
   - Resolve bin: `$SPTC_HOOK_BIN` (cached) → else `spt adapter get-string claude-spt hook_cmd`.
   - No-op (exit 0) if unresolvable (adapter not registered yet = pre-/sptc:setup window).
   - Pass `--host-pid "$PPID"` (the seed pid — Rust std has no portable getppid on Windows) and the
     event; the binary reads CC stdin directly (dispatch does NOT cat, so stdin passes through clean).
4. **`plugin/sptc/hooks/hooks.json`** — static-forever: every event → `sh dispatch.sh <event>`.
5. **`adapter/claude-spt.toml`** — add `[strings] hook_cmd = "{adapter_dir}/claude-spt hook"`;
   version → 0.9.0; min_spt_core stays 0.16.0 (primitives already shipped there); `[hooks.*]` comments
   note the binary handler.
6. **Delete** the 8 `*.sh` hook wrappers + `_common.sh` (logic now in the binary). **Keep** `bootstrap.sh`.
7. **Tests** — port `tests/hooks-parse.sh` coverage into `hook.rs` `#[cfg(test)]` (runs under
   `cargo test`, the digest-extractor gate). Replace `tests/hooks-parse.sh` with a thin
   `tests/hooks-dispatch.sh` (hooks.json static shape + dispatch wrapper sh -n + referenced files exist).
8. **traceable-reqs.toml** — add `REQ-DIST-HOOK-BINARY` (doc/impl/unit), activate; retag the migrated
   REQ evidence (REQ-DIST-HOOKS-API / REQ-UPS-INJECTION / REQ-DIST-* unit tags now point at hook.rs).
9. **cplugs skeleton** — STRUCTURAL change (hooks.json rewrite + .sh removal + dispatch.sh add) →
   `plugin.json` sptc 0.1.7 → 0.1.8.

### Bootstrap / no-op invariant (the one behavioural nuance)

A session that ran the spt-core bootstrap but is BEFORE `/sptc:setup` (adapter not yet registered)
has no `{adapter_dir}/claude-spt` → dispatch no-ops. Today's shell `api seed` would run there; the
binary path won't. Acceptable: a seed for an UNregistered adapter is unusable (ready/live resolve the
adapter by host_binaries from the registered set). Next session post-setup seeds normally. No real-world
regression (the perch was already a no-op pre-readiness today).

## Gate

`sh ci/run-gates.sh` green (shell-syntax, cargo test+build incl. new hook tests, traceable-reqs check,
skeleton-validate, manifest-schema) + a local idle/digest-proof dogfood of the repacked binary.

## Release (see it through — docs/RELEASE-RUNBOOK.md)

CHANGELOG `## [0.9.0]` · build win + linux (zigbuild) · `package-adapter.sh --apply` · gh release
v0.9.0 on SaberMage/claude-spt with `dist/adapter.spt` · cplugs sptc 0.1.8 push. gh = SaberMage
(repo+workflow scopes) — publish is doable from this node.

## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

- hook.rs (8 events + _common.sh ported), main.rs `hook` dispatch, dispatch.sh, static hooks.json,
  [strings].hook_cmd, 8 wrappers + _common.sh deleted. All default gates PASS (89 cargo tests,
  traceable-reqs green incl. new REQ-DIST-HOOK-BINARY doc/impl/unit, msys-hazard reworked, docs-drift).
- Adapter.spt repacked (win + linux); translate-proof + digest-proof + hook subcommand dogfooded from
  the extracted binary. CHANGELOG [0.9.0], plugin.json sptc 0.1.8, manifest 0.9.0.
- poll-int live binary-drain e2e blocked on THIS node (2 subnets → bind needs --subnet); covered by
  hook.rs unit tests + live skill-injection dogfood. Live full-perch e2e stays in the deferred
  harness-glue bucket (needs single-subnet/real-CC node). registration-int 4c reworked to the binary
  (not run live here — would soft-deregister the live node's adapter).
- Remaining: commit · tag v0.9.0 + push · gh release w/ dist/adapter.spt · cplugs sptc 0.1.8 push.

-## Status: BUILT + GATES GREEN (2026-06-28, perri) — releasing

+**PUBLISHED both targets.** GitHub release **v0.9.0** on SaberMage/claude-spt (commit d7fb440) with

+the fat `adapter.spt` (win+linux, manifest 0.9.0 + hook_cmd verified from the downloaded asset) +

+cplugs **sptc 0.1.8** (static hooks.json + dispatch.sh; 8 wrappers + _common.sh removed). main pushed.

+No GitHub Actions workflow in this repo — local gates ARE the CI (all green). On-node update verify

+(`spt adapter update claude-spt` ×2 for [update.post]) DEFERRED — would mutate this live node's

+registration mid-session; safe to do on a non-live node.


Bash

cd /c/Users/decid/Documents/projects/spt-claude-code
git add V09-PLAN.md
git commit -q -m "docs(plan): v0.9.0 SHIPPED — D1 published (adapter v0.9.0 + cplugs 0.1.8)

Co-authored by: perri"
git pull --rebase origin main 2>&1 | tail -3
git push origin main 2>&1 | tail -3

Commit plan status + push


From https://github.com/SaberMage/claude-spt
 * branch            main       -> FETCH_HEAD
Current branch main is up to date.
To https://github.com/SaberMage/claude-spt.git
   d7fb440..9a10b23  main -> main


v0.9.0 fully shipped + verified. Record to memory + commune to Psyche.