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

# ── Checkpoint detection (REQ-DIST-CHECKPOINT-COMMUNE) ──────────────────────────────────────────
# A live agent requests a CHECKPOINT (self-initiated context reset) by embedding the literal
# `!!checkpoint!!` trigger in its commune. The PostToolUse hook detects it at Write-time and self-sends
# a structured signal that loops back through the translation binary's clear+wake macro. spt-core
# v0.15.0 strips the marker from the durable/pending context on ingest, so it is one-shot. The pure
# predicates below run on a single physical line (the raw hook JSON, whose content newlines are `\n`
# escapes) so the paired-marker scan works even for a multi-line commune body.

# PURE predicate: does this commune content ($1) carry the checkpoint trigger? exit 0 iff present.
# [unit->REQ-DIST-CHECKPOINT-COMMUNE]
sptc_has_checkpoint() {
  case "$1" in *'!!checkpoint!!'*) return 0 ;; *) return 1 ;; esac
}

# PURE: extract a CUSTOM wake directive from commune content ($1) — the text between the FIRST pair of
# `!!checkpoint!!` markers, trimmed. Prints nothing when there is only one marker (single = default
# wake; the translation binary supplies the default) or none. The pair must be inline (same physical
# line); a lone or unmatched marker yields the default. [unit->REQ-DIST-CHECKPOINT-COMMUNE]
sptc_checkpoint_wake() {
  _n=$(printf '%s' "$1" | grep -o '!!checkpoint!!' | wc -l | tr -d ' ')
  [ "${_n:-0}" -ge 2 ] || return 0
  printf '%s' "$1" | sed -n 's/.*!!checkpoint!!\(.*\)!!checkpoint!!.*/\1/p' | head -n1 \
    | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}

# PURE predicate: is this tool call ($1=tool_name, $2=file_path, $3=endpoint id) a Write to THIS
# agent's own commune file `<id>-commune.md`? Suffix match tolerates the JSON-escaped Windows path
# (doubled backslashes) and either separator. exit 0 iff it is. [unit->REQ-DIST-CHECKPOINT-COMMUNE]
sptc_is_commune_write() {
  [ "$1" = "Write" ] || return 1
  [ -n "$3" ] || return 1
  case "$2" in *"$3-commune.md") return 0 ;; *) return 1 ;; esac
}

# ── SessionStart agent-facing briefs (REQ-DIST-SESSIONSTART-BRIEF) ──────────────────────────────

+# ── Checkpoint detection (REQ-DIST-CHECKPOINT-COMMUNE) ──────────────────────────────────────────

+# A live agent requests a CHECKPOINT (self-initiated context reset) by embedding the literal

+# `!!checkpoint!!` trigger in its commune. The PostToolUse hook detects it at Write-time and self-sends

+# a structured signal that loops back through the translation binary's clear+wake macro. spt-core

+# v0.15.0 strips the marker from the durable/pending context on ingest, so it is one-shot. The pure

+# predicates below run on a single physical line (the raw hook JSON, whose content newlines are `\n`

+# escapes) so the paired-marker scan works even for a multi-line commune body.

+# PURE predicate: does this commune content ($1) carry the checkpoint trigger? exit 0 iff present.

+  case "$1" in *'!!checkpoint!!'*) return 0 ;; *) return 1 ;; esac

+# PURE: extract a CUSTOM wake directive from commune content ($1) — the text between the FIRST pair of

+# `!!checkpoint!!` markers, trimmed. Prints nothing when there is only one marker (single = default

+# wake; the translation binary supplies the default) or none. The pair must be inline (same physical

+# line); a lone or unmatched marker yields the default. [unit->REQ-DIST-CHECKPOINT-COMMUNE]

+  _n=$(printf '%s' "$1" | grep -o '!!checkpoint!!' | wc -l | tr -d ' ')

+# PURE predicate: is this tool call ($1=tool_name, $2=file_path, $3=endpoint id) a Write to THIS

+# agent's own commune file `<id>-commune.md`? Suffix match tolerates the JSON-escaped Windows path

+# (doubled backslashes) and either separator. exit 0 iff it is. [unit->REQ-DIST-CHECKPOINT-COMMUNE]

+  case "$2" in *"$3-commune.md") return 0 ;; *) return 1 ;; esac

# The hook COMPOSES adapter-string briefs into a SessionStart additionalContext block; it never

# authors agent-facing prose (single-source throughline — bodies live in [strings.briefs]). Pure


Create the `post-tool-use.sh` wrapper:


Write

#!/bin/sh
# PostToolUse (Write matcher): the CHECKPOINT detector (Feature 2). On a Write to THIS spt-hosted live
# agent's own .claude/<id>-commune.md whose content carries the literal !!checkpoint!! trigger, mark
# the perch idle and self-send a reserved {"checkpoint":"v1",...} signal that loops back through the
# endpoint's own [message-idle-translation-binary], firing the clear+wake macro (the agent-driven
# context reset — the operator's manual /clear, done by the agent itself). spt-hosted live sessions
# ONLY (the loopback needs the broker-held PTY + the spawned translation binary). The !!checkpoint!!
# file-content marker is the WRITE-side trigger; spt-core v0.15.0 strips it from the durable + pending
# context on ingest so it is one-shot. The --json-payload self-send is the DELIVERY-side trigger.
# [impl->REQ-DIST-CHECKPOINT-COMMUNE]
. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"

input=$(cat)

# spt-hosted only: the endpoint id rides $SPT_ENDPOINT_ID (injected by [env.SPT_ENDPOINT_ID] on the
# broker-PTY bringup). A harness-hosted session has no translation binary, so checkpoint is inert there.
id="${SPT_ENDPOINT_ID:-}"
[ -z "$id" ] && exit 0

# Only a Write to this agent's own commune file.
sptc_is_commune_write "$(json_str "$input" tool_name)" "$(json_str "$input" file_path)" "$id" || exit 0

# Trigger present in the written content? (Scanned from the hook payload, not a file re-read.)
sptc_has_checkpoint "$input" || exit 0

sid=$(json_str "$input" session_id)
SPT=$(spt_bin)

# Mark idle so the loopback delivery lands on an idle input box, not mid-turn.
"$SPT" api --adapter "$ADAPTER" state idle "$id" --session-id "$sid" >/dev/null 2>&1 || true

# Build the structured checkpoint payload. A custom wake (text between a !!checkpoint!! pair) rides as
# "wake"; otherwise omit it and let the translation binary apply its default ("Proceed with next
# steps") — the default lives in ONE place (the binary). JSON-escape the wake value.
wake=$(sptc_checkpoint_wake "$input")
if [ -n "$wake" ]; then
  payload=$(printf '{"checkpoint":"v1","wake":"%s"}' "$(printf '%s' "$wake" | sptc_json_escape)")
else
  payload='{"checkpoint":"v1"}'
fi

# Self-send the signal back through our own endpoint's translation binary (the proven loopback). The
# body is a harmless human-readable note; the structured trigger lives in the json attr (collision-
# proof). QUEUED and SENT are both success. --json-payload needs spt-core v0.15.0; on an older node the
# send fails and is swallowed (the commune is still saved — checkpoint just won't auto-fire).
printf '%s' "checkpoint requested" | "$SPT" send --from "$id" "$id" --json-payload "$payload" >/dev/null 2>&1 || true
exit 0


File created successfully at: C:\Users\decid\Documents\projects\spt-claude-code\plugin\sptc\hooks\post-tool-use.sh (file state is current in your context — no need to Read it back)

#!/bin/sh
# PostToolUse (Write matcher): the CHECKPOINT detector (Feature 2). On a Write to THIS spt-hosted live
# agent's own .claude/<id>-commune.md whose content carries the literal !!checkpoint!! trigger, mark
# the perch idle and self-send a reserved {"checkpoint":"v1",...} signal that loops back through the
# endpoint's own [message-idle-translation-binary], firing the clear+wake macro (the agent-driven
# context reset — the operator's manual /clear, done by the agent itself). spt-hosted live sessions
# ONLY (the loopback needs the broker-held PTY + the spawned translation binary). The !!checkpoint!!
# file-content marker is the WRITE-side trigger; spt-core v0.15.0 strips it from the durable + pending
# context on ingest so it is one-shot. The --json-payload self-send is the DELIVERY-side trigger.
# [impl->REQ-DIST-CHECKPOINT-COMMUNE]
. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"

input=$(cat)

# spt-hosted only: the endpoint id rides $SPT_ENDPOINT_ID (injected by [env.SPT_ENDPOINT_ID] on the
# broker-PTY bringup). A harness-hosted session has no translation binary, so checkpoint is inert there.
id="${SPT_ENDPOINT_ID:-}"
[ -z "$id" ] && exit 0

# Only a Write to this agent's own commune file.
sptc_is_commune_write "$(json_str "$input" tool_name)" "$(json_str "$input" file_path)" "$id" || exit 0

# Trigger present in the written content? (Scanned from the hook payload, not a file re-read.)
sptc_has_checkpoint "$input" || exit 0

sid=$(json_str "$input" session_id)
SPT=$(spt_bin)

# Mark idle so the loopback delivery lands on an idle input box, not mid-turn.
"$SPT" api --adapter "$ADAPTER" state idle "$id" --session-id "$sid" >/dev/null 2>&1 || true

# Build the structured checkpoint payload. A custom wake (text between a !!checkpoint!! pair) rides as
# "wake"; otherwise omit it and let the translation binary apply its default ("Proceed with next
# steps") — the default lives in ONE place (the binary). JSON-escape the wake value.
wake=$(sptc_checkpoint_wake "$input")
if [ -n "$wake" ]; then
  payload=$(printf '{"checkpoint":"v1","wake":"%s"}' "$(printf '%s' "$wake" | sptc_json_escape)")
else
  payload='{"checkpoint":"v1"}'
fi

# Self-send the signal back through our own endpoint's translation binary (the proven loopback). The
# body is a harmless human-readable note; the structured trigger lives in the json attr (collision-
# proof). QUEUED and SENT are both success. --json-payload needs spt-core v0.15.0; on an older node the
# send fails and is swallowed (the commune is still saved — checkpoint just won't auto-fire).
printf '%s' "checkpoint requested" | "$SPT" send --from "$id" "$id" --json-payload "$payload" >/dev/null 2>&1 || true
exit 0


Make it executable, then add unit tests for the new helpers.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && chmod +x plugin/sptc/hooks/post-tool-use.sh && ls -la plugin/sptc/hooks/post-tool-use.sh


-rwxr-xr-x 1 decid 197609 2694 Jun 24 04:14 plugin/sptc/hooks/post-tool-use.sh


Edit

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

# --- checkpoint detection: trigger presence, custom-wake extraction, commune-write guard ---
# [unit->REQ-DIST-CHECKPOINT-COMMUNE]
check "has_checkpoint single"  "yes" "$(sptc_has_checkpoint 'delta ... !!checkpoint!!'        && echo yes || echo no)"
check "has_checkpoint pair"    "yes" "$(sptc_has_checkpoint '!!checkpoint!! wake !!checkpoint!!' && echo yes || echo no)"
check "has_checkpoint absent"  "no"  "$(sptc_has_checkpoint 'an ordinary commune delta'        && echo yes || echo no)"
check "has_checkpoint empty"   "no"  "$(sptc_has_checkpoint ''                                  && echo yes || echo no)"

# Single marker -> no custom wake (binary applies the default).
check "wake single -> default(empty)" "" "$(sptc_checkpoint_wake 'work ... !!checkpoint!!')"
# Paired markers -> the trimmed inner text is the custom wake.
check "wake pair -> custom" "Resume T2c now" \
  "$(sptc_checkpoint_wake 'body !!checkpoint!! Resume T2c now !!checkpoint!! more')"
# Pair embedded in a realistic single-line JSON content blob.
check "wake pair in json blob" "wire the hook" \
  "$(sptc_checkpoint_wake '{"content":"delta\n!!checkpoint!! wire the hook !!checkpoint!!\nend"}')"
check "wake none -> empty"     "" "$(sptc_checkpoint_wake 'no markers here')"

# commune-write guard: Write to <id>-commune.md only (tolerates JSON-escaped Windows path).
check "commune_write match unix"  "yes" \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write match win"   "yes" \
  "$(sptc_is_commune_write Write 'C:\\\\Users\\\\d\\\\.claude\\\\perri-commune.md' perri && echo yes || echo no)"
check "commune_write wrong file"  "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/notes.md' perri && echo yes || echo no)"
check "commune_write wrong id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/doyle-commune.md' perri && echo yes || echo no)"
check "commune_write not Write"   "no"  \
  "$(sptc_is_commune_write Edit '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write empty id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' '' && echo yes || echo no)"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }


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

# --- checkpoint detection: trigger presence, custom-wake extraction, commune-write guard ---
# [unit->REQ-DIST-CHECKPOINT-COMMUNE]
check "has_checkpoint single"  "yes" "$(sptc_has_checkpoint 'delta ... !!checkpoint!!'        && echo yes || echo no)"
check "has_checkpoint pair"    "yes" "$(sptc_has_checkpoint '!!checkpoint!! wake !!checkpoint!!' && echo yes || echo no)"
check "has_checkpoint absent"  "no"  "$(sptc_has_checkpoint 'an ordinary commune delta'        && echo yes || echo no)"
check "has_checkpoint empty"   "no"  "$(sptc_has_checkpoint ''                                  && echo yes || echo no)"

# Single marker -> no custom wake (binary applies the default).
check "wake single -> default(empty)" "" "$(sptc_checkpoint_wake 'work ... !!checkpoint!!')"
# Paired markers -> the trimmed inner text is the custom wake.
check "wake pair -> custom" "Resume T2c now" \
  "$(sptc_checkpoint_wake 'body !!checkpoint!! Resume T2c now !!checkpoint!! more')"
# Pair embedded in a realistic single-line JSON content blob.
check "wake pair in json blob" "wire the hook" \
  "$(sptc_checkpoint_wake '{"content":"delta\n!!checkpoint!! wire the hook !!checkpoint!!\nend"}')"
check "wake none -> empty"     "" "$(sptc_checkpoint_wake 'no markers here')"

# commune-write guard: Write to <id>-commune.md only (tolerates JSON-escaped Windows path).
check "commune_write match unix"  "yes" \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write match win"   "yes" \
  "$(sptc_is_commune_write Write 'C:\\\\Users\\\\d\\\\.claude\\\\perri-commune.md' perri && echo yes || echo no)"
check "commune_write wrong file"  "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/notes.md' perri && echo yes || echo no)"
check "commune_write wrong id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/doyle-commune.md' perri && echo yes || echo no)"
check "commune_write not Write"   "no"  \
  "$(sptc_is_commune_write Edit '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write empty id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' '' && echo yes || echo no)"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

#!/bin/sh
# Unit tests for the sptc hook parsing glue (pure functions; no spt/CC needed).
# Run: sh tests/hooks-parse.sh   (exit 0 = pass)
. "$(dirname "$0")/../plugin/sptc/hooks/_common.sh"

fail=0
check() { # check  <expected> <actual>
  if [ "$2" = "$3" ]; then printf 'ok   %s\n' "$1"
  else printf 'FAIL %s\n  expected: [%s]\n  actual:   [%s]\n' "$1" "$2" "$3"; fail=1; fi
}

# --- json_str: top-level CC hook-input fields ---  [unit->REQ-DIST-HOOKS-API]
J='{"session_id":"sess-abc123","transcript_path":"/x","source":"startup","model":"m"}'
check "json_str session_id" "sess-abc123" "$(json_str "$J" session_id)"
check "json_str source"     "startup"     "$(json_str "$J" source)"
check "json_str prompt"      "/sptc:send doyle" \
  "$(json_str '{"session_id":"s1","prompt":"/sptc:send doyle"}' prompt)"
check "json_str missing key" "" "$(json_str "$J" nope)"

# --- sptc_unescape:
 -> newline, entities, & LAST --- [unit->REQ-UPS-INJECTION]
check "unescape br+entities" \
  "$(printf 'a \n"c" & <')" \
  "$(sptc_unescape 'a <b>
"c" & &lt;')"

# --- render_frames: canonical <EVENT> envelope --- [unit->REQ-UPS-INJECTION]
# Named single,
-escaped multiline body, sender preserved.
named=$(render_frames '<EVENT type="msg" from="doyle">hello
world</EVENT>')
check "render named <EVENT>" \
  "$(printf '<sptc_messages from="doyle">\nhello\nworld\n</sptc_messages>')" "$named"

# Entity-escaped body decodes.
ent=$(render_frames '<EVENT type="msg" from="kit">a <tag> & b</EVENT>')
check "render entity body" \
  "$(printf '<sptc_messages from="kit">\na <tag> & b\n</sptc_messages>')" "$ent"

# Multi-message drain: two self-delimiting envelopes -> two rendered blocks.
multi=$(render_frames '<EVENT type="msg" from="a">one</EVENT><EVENT type="msg" from="b">two</EVENT>')
check "render multi-message" \
  "$(printf '<sptc_messages from="a">\none\n</sptc_messages>\n<sptc_messages from="b">\ntwo\n</sptc_messages>')" \
  "$multi"

# Envelope without a from attr -> anonymous rendering.
anon=$(render_frames '<EVENT type="msg">sys note</EVENT>')
check "render no-from" "$(printf '<sptc_messages>\nsys note\n</sptc_messages>')" "$anon"

# Empty drain -> no output.
check "render empty" "" "$(render_frames '')"

# --- sptc_skill_key: detect /sptc:<skill> in a slash-command prompt --- [unit->REQ-UPS-INJECTION]
check "skill_key bare"        "ready"  "$(sptc_skill_key '/sptc:ready')"
check "skill_key with args"   "send"   "$(sptc_skill_key '/sptc:send doyle hi')"
check "skill_key leading ws"  "setup"  "$(sptc_skill_key '   /sptc:setup')"
check "skill_key hyphenated"  "list-agents" "$(sptc_skill_key '/sptc:list-agents')"
check "skill_key mid-prose"   ""       "$(sptc_skill_key 'please run /sptc:ready for me')"
check "skill_key other ns"    ""       "$(sptc_skill_key '/other:ready')"
check "skill_key not a cmd"   ""       "$(sptc_skill_key 'ready set go')"
check "skill_key empty"       ""       "$(sptc_skill_key '')"

# --- sptc_register_verb: SessionStart bind/seed/boundary branch --- [unit->REQ-DIST-SHORTCUT-BASENAME]
# spt-hosted (endpoint id injected) -> bind; harness-hosted (no id) -> seed; clear/compact -> boundary.
unset SPT_ENDPOINT_ID
check "verb harness-hosted startup"   "seed"     "$(sptc_register_verb startup)"
check "verb harness-hosted empty src" "seed"     "$(sptc_register_verb '')"
check "verb boundary clear"           "boundary" "$(sptc_register_verb clear)"
check "verb boundary compact"         "boundary" "$(sptc_register_verb compact)"
SPT_ENDPOINT_ID=cc-7; export SPT_ENDPOINT_ID
check "verb spt-hosted startup -> bind" "bind"     "$(sptc_register_verb startup)"
# A /clear inside an spt-hosted session still rebinds via boundary (never re-binds fresh).
check "verb spt-hosted clear -> boundary" "boundary" "$(sptc_register_verb clear)"
unset SPT_ENDPOINT_ID

# --- sptc_cap_output: additionalContext spill guard (ADR-0002 Open #2) --- [unit->REQ-UPS-INJECTION]
spill="${TMPDIR:-/tmp}/sptc-cap-$$.txt"
rm -f "$spill"
check "cap under -> passthrough verbatim" "small body" "$(sptc_cap_output 'small body' 9000 "$spill")"
check "cap under -> no spill written"     "absent"     "$([ -f "$spill" ] && echo present || echo absent)"

over=$(sptc_cap_output 'abcdefghij' 5 "$spill")   # 10 bytes > cap 5
case "$over" in *'<sptc_overflow'*) m=ok ;; *) m=no ;; esac
check "cap over -> emits overflow marker"  "ok"         "$m"
check "cap over -> full body spilled"      "abcdefghij" "$(cat "$spill" 2>/dev/null)"
case "$over" in *abcdefghij*) leak=leaked ;; *) leak=clean ;; esac
check "cap over -> body not inlined"       "clean"      "$leak"
check "cap empty -> no output"             ""           "$(sptc_cap_output '' 5 "$spill")"
rm -f "$spill"

# --- SessionStart briefs: pure composition --- [unit->REQ-DIST-SESSIONSTART-BRIEF]
# assemble_perch: {id} substitution + block structure.
pb=$(sptc_assemble_perch "perri" 'you are {id}.' 'send body' 'roster line')
check "assemble_perch structure+subst" \
  "$(printf '<sptc-active-perch id="perri">\nyou are perri.\n\nsend body\n\nroster line\n</sptc-active-perch>')" \
  "$pb"
case "$pb" in *'{id}'*) leftover=present ;; *) leftover=clean ;; esac
check "assemble_perch no {id} leftover" "clean" "$leftover"
# Every {id} occurrence is substituted (global).
multi_id=$(sptc_assemble_perch "kit" 'a {id} b {id}' 'm' 'r')
case "$multi_id" in *'a kit b kit'*) g=ok ;; *) g=no ;; esac
check "assemble_perch global subst" "ok" "$g"

# assemble_noperch: ring block structure (no id).
nb=$(sptc_assemble_noperch 'ring body' 'roster line')
check "assemble_noperch structure" \
  "$(printf '<sptc-reach>\nring body\n\nroster line\n</sptc-reach>')" "$nb"

# is_subagent: empty -> real session (inject); set -> subagent (skip).
check "is_subagent empty -> no"   "no"  "$(sptc_is_subagent ''               && echo yes || echo no)"
check "is_subagent set -> yes"    "yes" "$(sptc_is_subagent 'general-purpose' && echo yes || echo no)"

# has_peers_lines: >1 non-empty line on `subnet status` stdin = peers present.
check "peers: header+row -> yes" "yes" "$(printf 'SUBNET NODES ENDPOINTS\nSPT_DEV 3 4\n' | sptc_has_peers_lines && echo yes || echo no)"
check "peers: single line -> no" "no"  "$(printf 'no subnets\n'                            | sptc_has_peers_lines && echo yes || echo no)"
check "peers: empty -> no"       "no"  "$(printf ''                                          | sptc_has_peers_lines && echo yes || echo no)"

# --- SessionStart briefs: JSON encode + emit --- [unit->REQ-DIST-SESSIONSTART-BRIEF]
check "json_escape quotes"    'say \"hi\"' "$(printf '%s' 'say "hi"' | sptc_json_escape)"
check "json_escape backslash" 'a\\b'       "$(printf '%s' 'a\b'      | sptc_json_escape)"
check "json_escape newline"   'a\nb'       "$(printf 'a\nb'          | sptc_json_escape)"

check "emit empty -> silent" "" "$(sptc_emit_additional_context '')"
check "emit json shape" \
  '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"hi \"x\""}}' \
  "$(sptc_emit_additional_context 'hi "x"')"
check "emit json newline-escaped" \
  '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"a\nb"}}' \
  "$(sptc_emit_additional_context "$(printf 'a\nb')")"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

+# --- checkpoint detection: trigger presence, custom-wake extraction, commune-write guard ---

+check "has_checkpoint single"  "yes" "$(sptc_has_checkpoint 'delta ... !!checkpoint!!'        && echo yes || echo no)"

+check "has_checkpoint pair"    "yes" "$(sptc_has_checkpoint '!!checkpoint!! wake !!checkpoint!!' && echo yes || echo no)"

+check "has_checkpoint absent"  "no"  "$(sptc_has_checkpoint 'an ordinary commune delta'        && echo yes || echo no)"

+check "has_checkpoint empty"   "no"  "$(sptc_has_checkpoint ''                                  && echo yes || echo no)"

+# Single marker -> no custom wake (binary applies the default).

+check "wake single -> default(empty)" "" "$(sptc_checkpoint_wake 'work ... !!checkpoint!!')"

+# Paired markers -> the trimmed inner text is the custom wake.

+check "wake pair -> custom" "Resume T2c now" \

+  "$(sptc_checkpoint_wake 'body !!checkpoint!! Resume T2c now !!checkpoint!! more')"

+# Pair embedded in a realistic single-line JSON content blob.

+check "wake pair in json blob" "wire the hook" \

+  "$(sptc_checkpoint_wake '{"content":"delta\n!!checkpoint!! wire the hook !!checkpoint!!\nend"}')"

+check "wake none -> empty"     "" "$(sptc_checkpoint_wake 'no markers here')"

+# commune-write guard: Write to <id>-commune.md only (tolerates JSON-escaped Windows path).

+check "commune_write match unix"  "yes" \

+  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"

+check "commune_write match win"   "yes" \

+  "$(sptc_is_commune_write Write 'C:\\\\Users\\\\d\\\\.claude\\\\perri-commune.md' perri && echo yes || echo no)"

+check "commune_write wrong file"  "no"  \

+  "$(sptc_is_commune_write Write '/home/x/.claude/notes.md' perri && echo yes || echo no)"

+check "commune_write wrong id"    "no"  \

+  "$(sptc_is_commune_write Write '/home/x/.claude/doyle-commune.md' perri && echo yes || echo no)"

+check "commune_write not Write"   "no"  \

+  "$(sptc_is_commune_write Edit '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"

+check "commune_write empty id"    "no"  \

+  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' '' && echo yes || echo no)"


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh tests/hooks-parse.sh 2>&1 | tail -25


ok   is_subagent set -> yes
ok   peers: header+row -> yes
ok   peers: single line -> no
ok   peers: empty -> no
ok   json_escape quotes
ok   json_escape backslash
ok   json_escape newline
ok   emit empty -> silent
ok   emit json shape
ok   emit json newline-escaped
ok   has_checkpoint single
ok   has_checkpoint pair
ok   has_checkpoint absent
ok   has_checkpoint empty
ok   wake single -> default(empty)
ok   wake pair -> custom
ok   wake pair in json blob
ok   wake none -> empty
ok   commune_write match unix
ok   commune_write match win
ok   commune_write wrong file
ok   commune_write wrong id
ok   commune_write not Write
ok   commune_write empty id
ALL PASS


All hook tests pass. T2 int: emit-half via translate-proof (binary checkpoint branch) runnable now; the full self-send loopback needs 0.15.0 (capability-skip). Rebuild release + verify translate-proof fires the macro on a checkpoint envelope.


cd "C:/Users/decid/Documents/projects/spt-claude-code/tools/cc-spt-idle-translate" && cargo build --release 2>&1 | tail -2
ROOT="C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== translate-proof on a CHECKPOINT envelope ==="
ENV='<EVENT type="msg" from="self" json="{"checkpoint":"v1","wake":"go now"}">checkpoint requested</EVENT>'
spt adapter translate-proof claude-spt --event "$ENV" --manifest "$ROOT/adapter/claude-spt.toml" --dir "$ROOT/tools/cc-spt-idle-translate/target/release" 2>&1


   Compiling cc-spt-idle-translate v0.1.0 (C:\Users\decid\Documents\projects\spt-claude-code\tools\cc-spt-idle-translate)
    Finished `release` profile [optimized] target(s) in 0.74s
=== translate-proof on a CHECKPOINT envelope ===
=== translate-proof: claude-spt ===
emitted 8 command(s), commit: yes

--- emitted keystroke commands ---
  key   ctrl+s -> [13]
  delay 50ms
  text  "/clear"
  key   enter -> [0d]
  delay 500ms
  text  "go now"
  key   enter -> [0d]
  commit true
TRANSLATE_PROOF_OK:claude-spt


Binary fires the macro through the real harness (8 cmds: `/clear` · 500ms · wake · commit). Add checkpoint emit-half assertions to the int script:


Edit

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }

# ── CHECKPOINT branch (Feature 2) ────────────────────────────────────────────────────────────────
# An envelope carrying a `json="{"checkpoint":"v1",…}"` attr (what `spt send --json-payload` composes,
# v0.15.0) fires the clear+wake macro INSTEAD of normal delivery. This is the EMIT half of the binary's
# checkpoint branch — proven here by feeding translate-proof an envelope with the json attr directly
# (no --json-payload needed, so it runs on the pre-0.15.0 daemon). The send-side composition + the live
# self-send loopback stay the heavier real-claude int (deferred, like the multi-line APPLY half).
# [int->REQ-DIST-CHECKPOINT-COMMUNE]
CKPT='<EVENT type="msg" from="self" json="{"checkpoint":"v1","wake":"Resume now"}">checkpoint requested</EVENT>'
ck=$(spt adapter translate-proof claude-spt --event "$CKPT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$ck" in
  *'text  "/clear"'*) echo "ok  checkpoint envelope fires the /clear macro" ;;
  *) echo "FAIL: checkpoint envelope did not emit /clear:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'text  "Resume now"'*) echo "ok  checkpoint macro types the custom wake directive" ;;
  *) echo "FAIL: checkpoint macro missing the custom wake:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'delay 500ms'*) echo "ok  checkpoint macro waits for /clear to settle (500ms)" ;;
  *) echo "FAIL: checkpoint macro missing the post-/clear settle:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'commit: yes'*) echo "ok  checkpoint macro terminates with the mandatory {commit}" ;;
  *) echo "FAIL: checkpoint macro missing the {commit} terminator:"; printf '%s\n' "$ck"; rc=1 ;;
esac
# Default wake when the payload carries no `wake` field.
CKPT_DEF='<EVENT type="msg" from="self" json="{"checkpoint":"v1"}">checkpoint requested</EVENT>'
ckd=$(spt adapter translate-proof claude-spt --event "$CKPT_DEF" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$ckd" in
  *'text  "Proceed with next steps"'*) echo "ok  checkpoint without a wake uses the default" ;;
  *) echo "FAIL: default wake not applied:"; printf '%s\n' "$ckd"; rc=1 ;;
esac
# A normal (non-checkpoint) message must NOT fire the macro.
case "$out" in
  *'/clear'*) echo "FAIL: a normal message emitted /clear (checkpoint false-positive):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  normal delivery never fires the /clear macro" ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }


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

# ── CHECKPOINT branch (Feature 2) ────────────────────────────────────────────────────────────────
# An envelope carrying a `json="{"checkpoint":"v1",…}"` attr (what `spt send --json-payload` composes,
# v0.15.0) fires the clear+wake macro INSTEAD of normal delivery. This is the EMIT half of the binary's
# checkpoint branch — proven here by feeding translate-proof an envelope with the json attr directly
# (no --json-payload needed, so it runs on the pre-0.15.0 daemon). The send-side composition + the live
# self-send loopback stay the heavier real-claude int (deferred, like the multi-line APPLY half).
# [int->REQ-DIST-CHECKPOINT-COMMUNE]
CKPT='<EVENT type="msg" from="self" json="{"checkpoint":"v1","wake":"Resume now"}">checkpoint requested</EVENT>'
ck=$(spt adapter translate-proof claude-spt --event "$CKPT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$ck" in
  *'text  "/clear"'*) echo "ok  checkpoint envelope fires the /clear macro" ;;
  *) echo "FAIL: checkpoint envelope did not emit /clear:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'text  "Resume now"'*) echo "ok  checkpoint macro types the custom wake directive" ;;
  *) echo "FAIL: checkpoint macro missing the custom wake:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'delay 500ms'*) echo "ok  checkpoint macro waits for /clear to settle (500ms)" ;;
  *) echo "FAIL: checkpoint macro missing the post-/clear settle:"; printf '%s\n' "$ck"; rc=1 ;;
esac
case "$ck" in
  *'commit: yes'*) echo "ok  checkpoint macro terminates with the mandatory {commit}" ;;
  *) echo "FAIL: checkpoint macro missing the {commit} terminator:"; printf '%s\n' "$ck"; rc=1 ;;
esac
# Default wake when the payload carries no `wake` field.
CKPT_DEF='<EVENT type="msg" from="self" json="{"checkpoint":"v1"}">checkpoint requested</EVENT>'
ckd=$(spt adapter translate-proof claude-spt --event "$CKPT_DEF" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
case "$ckd" in
  *'text  "Proceed with next steps"'*) echo "ok  checkpoint without a wake uses the default" ;;
  *) echo "FAIL: default wake not applied:"; printf '%s\n' "$ckd"; rc=1 ;;
esac
# A normal (non-checkpoint) message must NOT fire the macro.
case "$out" in
  *'/clear'*) echo "FAIL: a normal message emitted /clear (checkpoint false-positive):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  normal delivery never fires the /clear macro" ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }

#!/bin/sh
# Integration proof: `spt adapter translate-proof` spawns + feeds the claude-spt
# [message-idle-translation-binary] (cc-spt-idle-translate) EXACTLY as the daemon does at idle
# delivery (init line then the --event envelope), reads back the emitted keystroke-command stream,
# and gates it. This is the EMIT half of REQ-DIST-IDLE-TRANSLATE — the cross-process author-time proof
# (ADR-0022, the EMIT-half mirror of digest-proof). It does NOT exercise the daemon's atomic PTY APPLY
# or controller buffering — that half stays the real-claude bringup int (deferred). [int->REQ-DIST-IDLE-TRANSLATE]
#
# Uses the v0.13.2 `--dir`/`--manifest` override (F-011 closed, W5) to proof the DEV binary straight
# from its build dir against the bare-file manifest: --dir resolves the binary (before PATH) exactly
# as the daemon does, --manifest pins the bare-file manifest. NO registry mutation — the prior form
# staged a disposable install dir and `adapter add`/`adapter remove claude-spt`, which soft-removed
# the REAL registered claude-spt on cleanup. Read-only now, so NO SPTC_ACCEPTANCE gate. Needs
# spt >= 0.13.2 (the --dir/--manifest options) + a built binary. Idempotent.
# Run: sh ci/idle-translate/translate-proof-int.sh   (exit 0 = pass).
#
# GREEN against the {commit}-terminated binary: emits the 6-command choreography ctrl+s · 50ms ·
# {text:"<envelope>"} · 50ms · {key:enter} · {commit:true}, exit 0 / TRANSLATE_PROOF_OK / "commit: yes".
# The submit is a discrete {key:enter} AFTER the text (a trailing \r byte does NOT submit a CC message,
# corrected 2026-06-23). The no-{commit} form FAULTs (the F-016 defect the proof's no-commit gate catches).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
MANIFEST="$ROOT/adapter/claude-spt.toml"
RELDIR="$ROOT/tools/cc-spt-idle-translate/target/release"
EVENT='<EVENT type="msg" from="ci">translate-proof int probe</EVENT>'

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

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

BIN="$RELDIR/cc-spt-idle-translate"
[ -x "$BIN" ] || BIN="$RELDIR/cc-spt-idle-translate.exe"
[ -x "$BIN" ] || { echo "SKIP: binary not built (run sh ci/idle-translate/build.sh)"; exit 0; }

# Proof the dev binary in-place: --dir resolves the binary (before PATH) like the daemon, --manifest
# pins the bare-file gh_release manifest — no extracted install, no registry touch.
out=$(spt adapter translate-proof claude-spt --event "$EVENT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)
rc=0
case "$out" in
  *TRANSLATE_PROOF_OK*) echo "ok  translate-proof: TRANSLATE_PROOF_OK" ;;
  *) echo "FAIL: translate-proof did not pass:"; printf '%s\n' "$out"; exit 1 ;;
esac
# The {commit} terminator is the F-016 fix — assert the proof saw it (no-commit would FAULT live).
case "$out" in
  *"commit: yes"*) echo "ok  emitted the mandatory {commit} terminator (commit: yes)" ;;
  *) echo "FAIL: proof reports no commit terminator (the F-016 fault condition):"; printf '%s\n' "$out"; rc=1 ;;
esac
# The submit is a discrete enter keypress (NOT a trailing \r in the text — a \r byte does not submit CC).
case "$out" in
  *enter*) echo "ok  emitted the discrete {key:enter} submit" ;;
  *) echo "FAIL: no {key:enter} submit in the command stream:"; printf '%s\n' "$out"; rc=1 ;;
esac
# ...and the text command must NOT carry a trailing \r anymore.
case "$out" in
  *'\r"'*) echo "FAIL: text still carries a trailing \\r (should be a discrete enter):"; printf '%s\n' "$out"; rc=1 ;;
  *) echo "ok  text command carries no trailing \\r" ;;
esac
# The envelope is FRAMED across multiple lines for visual distinction: a raw \n after the opening
# tag and before the closing </EVENT>. CC soft-newlines a bare \n (empirically gated 2026-06-24), so
# this renders as one user turn spanning lines, not an early submit. The emitted {text} carries the
# two deliberate framing newlines (JSON-escaped as \n in the command stream). [int->REQ-DIST-IDLE-MULTILINE]
case "$out" in
  *'>\n'*'\n</EVENT>'*) echo "ok  envelope framed across lines (\\n after opening tag and before </EVENT>)" ;;
  *) echo "FAIL: text is not framed across lines (multi-line envelope missing):"; printf '%s\n' "$out"; rc=1 ;;
esac

[ "$rc" -eq 0 ] && { echo "TRANSLATE-PROOF-INT OK"; exit 0; } || { echo "TRANSLATE-PROOF-INT FAIL"; exit 1; }

+# ── CHECKPOINT branch (Feature 2) ────────────────────────────────────────────────────────────────

+# An envelope carrying a `json="{"checkpoint":"v1",…}"` attr (what `spt send --json-payload` composes,

+# v0.15.0) fires the clear+wake macro INSTEAD of normal delivery. This is the EMIT half of the binary's

+# checkpoint branch — proven here by feeding translate-proof an envelope with the json attr directly

+# (no --json-payload needed, so it runs on the pre-0.15.0 daemon). The send-side composition + the live

+# self-send loopback stay the heavier real-claude int (deferred, like the multi-line APPLY half).

+CKPT='<EVENT type="msg" from="self" json="{"checkpoint":"v1","wake":"Resume now"}">checkpoint requested</EVENT>'

+ck=$(spt adapter translate-proof claude-spt --event "$CKPT" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)

+  *'text  "/clear"'*) echo "ok  checkpoint envelope fires the /clear macro" ;;

+  *) echo "FAIL: checkpoint envelope did not emit /clear:"; printf '%s\n' "$ck"; rc=1 ;;

+  *'text  "Resume now"'*) echo "ok  checkpoint macro types the custom wake directive" ;;

+  *) echo "FAIL: checkpoint macro missing the custom wake:"; printf '%s\n' "$ck"; rc=1 ;;

+  *'delay 500ms'*) echo "ok  checkpoint macro waits for /clear to settle (500ms)" ;;

+  *) echo "FAIL: checkpoint macro missing the post-/clear settle:"; printf '%s\n' "$ck"; rc=1 ;;

+  *'commit: yes'*) echo "ok  checkpoint macro terminates with the mandatory {commit}" ;;

+  *) echo "FAIL: checkpoint macro missing the {commit} terminator:"; printf '%s\n' "$ck"; rc=1 ;;

+# Default wake when the payload carries no `wake` field.

+CKPT_DEF='<EVENT type="msg" from="self" json="{"checkpoint":"v1"}">checkpoint requested</EVENT>'

+ckd=$(spt adapter translate-proof claude-spt --event "$CKPT_DEF" --manifest "$MANIFEST" --dir "$RELDIR" 2>&1)

+  *'text  "Proceed with next steps"'*) echo "ok  checkpoint without a wake uses the default" ;;

+  *) echo "FAIL: default wake not applied:"; printf '%s\n' "$ckd"; rc=1 ;;

+# A normal (non-checkpoint) message must NOT fire the macro.

+  *'/clear'*) echo "FAIL: a normal message emitted /clear (checkpoint false-positive):"; printf '%s\n' "$out"; rc=1 ;;

+  *) echo "ok  normal delivery never fires the /clear macro" ;;


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && sh ci/idle-translate/translate-proof-int.sh 2>&1


ok  translate-proof: TRANSLATE_PROOF_OK
ok  emitted the mandatory {commit} terminator (commit: yes)
ok  emitted the discrete {key:enter} submit
ok  text command carries no trailing \r
ok  envelope framed across lines (\n after opening tag and before </EVENT>)
ok  checkpoint envelope fires the /clear macro
ok  checkpoint macro types the custom wake directive
ok  checkpoint macro waits for /clear to settle (500ms)
ok  checkpoint macro terminates with the mandatory {commit}
ok  checkpoint without a wake uses the default
ok  normal delivery never fires the /clear macro
TRANSLATE-PROOF-INT OK


All int assertions green. Add the `doc` tag to the commune SKILL.md (the agent-facing contract), then activate the REQ.


Edit

# /sptc:commune

Brief your Psyche with a context DELTA

<!-- [doc->REQ-DIST-CHECKPOINT-COMMUNE] -->

# /sptc:commune

Brief your Psyche with a context DELTA


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

<!-- [doc->REQ-DIST-CHECKPOINT-COMMUNE] -->

# /sptc:commune

Brief your Psyche with a context DELTA

---
name: commune
description: |
  Push a context update to your Psyche so it can brief your resume across a reset. Use when the
  user says "commune" or "update psyche", or — as a live agent — after a significant body of work,
  before a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the
  freshest commune (an agent-driven context reset, no operator needed).
allowed-tools: [Bash, Write]
---

# /sptc:commune

Brief your Psyche with a context DELTA so it can carry you across a `/clear` or compact. **Live
agents only** — a ready agent has no Psyche and nothing to rebuild from.

This is a **file-drop, not an `api` verb**: you write the file, spt's daemon ingests it into your
tracked mind, then deletes it. The file disappearing is the success signal.

## Normal commune

1. Get your id: `spt whoami` (empty ⇒ no perch — run `/sptc:live` first).
2. Write `.claude/<id>-commune.md` in ONE atomic write — a concise delta, not a transcript:
   - current task + status,
   - decisions made since your last commune,
   - immediate next steps (so your post-reset self resumes without re-deriving).
3. If the file lingers instead of disappearing, you are not live (no daemon ingesting it).

## Checkpoint (`--checkpoint`) — self-initiated context reset

A **checkpoint** is a commune that ALSO asks spt to wipe and rebuild your working context from that
very commune — the agent-driven sibling of the operator manually running `/clear`. Use it when your
context is getting long but you are mid-task and want to keep going coherently on your own.

You request a checkpoint by embedding the literal **`!!checkpoint!!`** trigger in the commune body
(the `--checkpoint` flag is just the cue to do this). Author the commune INLINE, right now, in this
same turn — you are the authoring agent, pre-clear. Do NOT defer authoring to after the clear.

Wake directive — what your resumed self is told to do first:

- **One `!!checkpoint!!`** anywhere in the body ⇒ checkpoint with the DEFAULT wake,
  `Proceed with next steps`.
- **A PAIR of `!!checkpoint!!` markers** ⇒ the text BETWEEN them is your CUSTOM wake directive, e.g.

  ```
  !!checkpoint!! Resume T2c: wire the binary checkpoint branch against doyle's reply. !!checkpoint!!
  ```

  rebuilds your context, then wakes you with exactly that instruction.

Mechanics (you don't drive these — they fire automatically once the file lands): your perch is marked
idle, a reserved checkpoint signal self-sends back through your own endpoint, and the delivery layer
runs the clear + wake. The commune you just wrote is what your Psyche rebuilds you from, so make it
complete. **spt-hosted live sessions only** (the self-send loopback needs the broker-held PTY).

Steps for a checkpoint:

1. `spt whoami` for your id (empty ⇒ not live; checkpoint is unavailable).
2. Write `.claude/<id>-commune.md` with your full resume delta AND the `!!checkpoint!!` trigger
   (single for the default wake, or a pair bracketing your custom wake).
3. The clear + wake fire on their own. Your next turn is the post-checkpoint wake — continue from the
   commune.


Now activate REQ-DIST-CHECKPOINT-COMMUNE (doc+impl+unit+int):


required_stages = []  # Feature 2 / task T2. ACTIVATE at build. doc: ADR-0004 + CHECKPOINT-COMMUNE-PLAN.md. impl: full-fat /sptc:commune SKILL.md (+ --checkpoint flag + [[hints]]) + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json + detector reading tool_input.content) + cc-spt-idle-translate checkpoint branch (wire-sentinel filter -> clear+wake macro + double-trigger custom-wake parse). unit: binary macro tests + hook parse tests. int: live endpoint — flagged commune -> /clear fires -> wake turn (default + custom). Mechanics have ZERO spt-core dependency; self-send loopback PROVEN 2026-06-24.

required_stages = ["doc", "impl", "unit", "int"]  # Feature 2 / task T2 — ACTIVATED 2026-06-24. WIRE-SENTINEL is now spt-core v0.15.0 `spt send --json-payload '{"checkpoint":"v1","wake":…}'` (doyle, grounded in the gated W4 code) — a structured json ATTR on the <EVENT> envelope, NOT the old string-prefix sniff (collision-proof: the marker lives inside the opaque attr, can't forge from/type). doc: full-fat /sptc:commune SKILL.md (--checkpoint: single trigger=default wake, paired triggers bracket a custom wake) + ADR-0004 + the checkpoint [[hints]] entry. impl: cc-spt-idle-translate checkpoint branch (extract json attr -> xml-attr-unescape (& LAST) -> serde parse -> checkpoint=="v1" -> clear+wake macro ctrl+s·50ms·/clear·enter·500ms·wake·enter·commit, default "Proceed with next steps") + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json Write-matcher + post-tool-use.sh detector: !!checkpoint!! scan of tool_input content -> api state idle -> self-send --json-payload) + _common.sh pure helpers (sptc_has_checkpoint/sptc_checkpoint_wake/sptc_is_commune_write). unit: hooks-parse.sh checkpoint tests + the crate's checkpoint macro/parse/unescape tests. int: ci/idle-translate/translate-proof-int.sh checkpoint block (envelope w/ json attr fires the clear+wake macro, custom + default wake, normal delivery never clears) — the EMIT half on the pre-0.15.0 daemon; the send-side --json-payload composition + live self-send loopback stay the heavier real-claude int (deferred to the v0.15.0/counter-34 publish, capability-detect-skip). The write-side !!checkpoint!! marker is one-shot (spt-core v0.15.0 strips it from durable+pending on ingest). min_spt_core stays 0.13.2 in-tree until doyle's publish ping, then bumps to 0.15.0. Self-send loopback PROVEN 2026-06-24.


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

title = "Agent-driven checkpoint: a live agent flags a commune with the literal trigger !!checkpoint!! (one = default wake; a pair brackets a custom wake directive); a PostToolUse hook detects the trigger in the Write tool_input.content for .claude/<id>-commune.md, sets the perch idle (api state idle), and self-sends a reserved wire-sentinel (spt send --from <id> <id>); the message loops back through the endpoint's own translation binary, which fires the clear+wake macro (ctrl+s · 50ms · /clear · enter · 500ms · wake-text · enter · commit). spt-hosted-only. INVARIANT: the commune is authored INLINE pre-clear (ADR-0004, Shape 1), never via a post-clear resume-Self refresh."

-required_stages = []  # Feature 2 / task T2. ACTIVATE at build. doc: ADR-0004 + CHECKPOINT-COMMUNE-PLAN.md. impl: full-fat /sptc:commune SKILL.md (+ --checkpoint flag + [[hints]]) + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json + detector reading tool_input.content) + cc-spt-idle-translate checkpoint branch (wire-sentinel filter -> clear+wake macro + double-trigger custom-wake parse). unit: binary macro tests + hook parse tests. int: live endpoint — flagged commune -> /clear fires -> wake turn (default + custom). Mechanics have ZERO spt-core dependency; self-send loopback PROVEN 2026-06-24.

+required_stages = ["doc", "impl", "unit", "int"]  # Feature 2 / task T2 — ACTIVATED 2026-06-24. WIRE-SENTINEL is now spt-core v0.15.0 `spt send --json-payload '{"checkpoint":"v1","wake":…}'` (doyle, grounded in the gated W4 code) — a structured json ATTR on the <EVENT> envelope, NOT the old string-prefix sniff (collision-proof: the marker lives inside the opaque attr, can't forge from/type). doc: full-fat /sptc:commune SKILL.md (--checkpoint: single trigger=default wake, paired triggers bracket a custom wake) + ADR-0004 + the checkpoint [[hints]] entry. impl: cc-spt-idle-translate checkpoint branch (extract json attr -> xml-attr-unescape (& LAST) -> serde parse -> checkpoint=="v1" -> clear+wake macro ctrl+s·50ms·/clear·enter·500ms·wake·enter·commit, default "Proceed with next steps") + NEW PostToolUse hook (manifest [hooks.PostToolUse] + hooks.json Write-matcher + post-tool-use.sh detector: !!checkpoint!! scan of tool_input content -> api state idle -> self-send --json-payload) + _common.sh pure helpers (sptc_has_checkpoint/sptc_checkpoint_wake/sptc_is_commune_write). unit: hooks-parse.sh checkpoint tests + the crate's checkpoint macro/parse/unescape tests. int: ci/idle-translate/translate-proof-int.sh checkpoint block (envelope w/ json attr fires the clear+wake macro, custom + default wake, normal delivery never clears) — the EMIT half on the pre-0.15.0 daemon; the send-side --json-payload composition + live self-send loopback stay the heavier real-claude int (deferred to the v0.15.0/counter-34 publish, capability-detect-skip). The write-side !!checkpoint!! marker is one-shot (spt-core v0.15.0 strips it from durable+pending on ingest). min_spt_core stays 0.13.2 in-tree until doyle's publish ping, then bumps to 0.15.0. Self-send loopback PROVEN 2026-06-24.


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code" && traceable-reqs check 2>&1 | grep -E "REQ-DIST-CHECKPOINT-COMMUNE|REQ-DIST-IDLE-MULTILINE|FAIL|missing|EXIT" ; echo "EXIT=$?"
echo "=== full check exit ==="; traceable-reqs check >/dev/null 2>&1 && echo "GREEN (exit 0)" || echo "RED"


  [OK] REQ-DIST-CHECKPOINT-COMMUNE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
  [OK] REQ-DIST-IDLE-MULTILINE  required: [impl, unit, int]  stages: -doc +impl +unit +int
EXIT=0
=== full check exit ===
GREEN (exit 0)


Both REQs green, check exit 0. Run the build gates before committing T2.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== cargo test ==="; (cd tools/cc-spt-idle-translate && cargo test 2>&1 | grep "test result")
echo "=== hooks-parse ==="; sh tests/hooks-parse.sh 2>&1 | tail -1
echo "=== manifest schema ==="; sh ci/manifest/check-manifest.sh 2>&1 | tail -2
echo "=== manifest-shortcut ==="; sh tests/manifest-shortcut.sh 2>&1 | tail -1
echo "=== hooks.json valid JSON ==="; python -c "import json;json.load(open(r'plugin/sptc/hooks/hooks.json'));print('hooks.json OK')"


=== cargo test ===
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== hooks-parse ===
ALL PASS
=== manifest schema ===
ok   C:/Users/decid/Documents/projects/spt-claude-code/adapter/claude-spt.toml
ok   manifest validates against published manifest.schema.json
=== manifest-shortcut ===
MANIFEST-SHORTCUT OK
=== hooks.json valid JSON ===
hooks.json OK


All gates green. Let me verify the skeleton-validate gate doesn't flag the now-full-fat commune SKILL.md (commune is intentionally full-fat, not an injected skeleton), then run the full gate runner.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== skeleton-validate ==="; sh tests/skeleton-validate.sh 2>&1 | tail -15


=== skeleton-validate ===
ok   real skeleton validates
ok   clean fixture validates
ok   catches runtime state in published path
ok   catches planted binary
ok   catches missing SKILL.md
ok   catches wrong plugin name
SKELETON-VALIDATE OK


cd "C:/Users/decid/Documents/projects/spt-claude-code"
test -f ci/run-gates.sh && echo "run-gates exists" || ls ci/
echo "=== docs-gen drift check (commune SKILL changed; llms.txt may regen) ==="
test -f tests/docs-gen.sh && sh tests/docs-gen.sh 2>&1 | tail -8 || echo "no docs-gen test"


run-gates exists
=== docs-gen drift check (commune SKILL changed; llms.txt may regen) ===
ok   generator deterministic
ok   all SUMMARY pages covered
ok   has H1 title
ok   has summary blockquote
ok   check-docs valid shell
DOCS-GEN OK


Let me check what `ci/run-gates.sh` runs (avoid triggering the heavy acceptance harness), then commit T2.


cd "C:/Users/decid/Documents/projects/spt-claude-code" && grep -nE '\.sh|build|cargo|SPTC_ACCEPTANCE|translate' ci/run-gates.sh | head -40


15:for f in $(find plugin tests ci -name '*.sh' 2>/dev/null | sort); do
19:gate "unit tests (tests/*.sh)"
20:for t in tests/*.sh; do
25:gate "digest-extractor (cargo build + test)"
26:if [ -f "$ROOT/ci/digest/build.sh" ]; then
27:  if sh "$ROOT/ci/digest/build.sh"; then :; else fail "digest-extractor"; fi
29:  echo "SKIP: no digest extractor build yet"
32:gate "psyche-runner (cargo build + test)"
33:if [ -f "$ROOT/ci/psyche/build.sh" ]; then
34:  if sh "$ROOT/ci/psyche/build.sh"; then :; else fail "psyche-runner"; fi
36:  echo "SKIP: no psyche runner build yet"
39:gate "idle-translate (cargo build + test)"
40:if [ -f "$ROOT/ci/idle-translate/build.sh" ]; then
41:  if sh "$ROOT/ci/idle-translate/build.sh"; then :; else fail "idle-translate"; fi
43:  echo "SKIP: no idle-translate build yet"
54:if [ -x "$ROOT/ci/publish/validate-skeleton.sh" ] || [ -f "$ROOT/ci/publish/validate-skeleton.sh" ]; then
55:  if sh "$ROOT/ci/publish/validate-skeleton.sh" >/dev/null 2>&1; then echo "ok  skeleton installable"; else fail "skeleton-validate"; sh "$ROOT/ci/publish/validate-skeleton.sh" || true; fi
61:if [ -f "$ROOT/ci/manifest/check-manifest.sh" ]; then
62:  if sh "$ROOT/ci/manifest/check-manifest.sh"; then :; else fail "manifest-schema"; fi
68:if [ -f "$ROOT/ci/docs/check-docs.sh" ]; then
69:  if sh "$ROOT/ci/docs/check-docs.sh"; then :; else fail "docs-drift"; fi


`run-gates` shellchecks every `.sh`. Let me lint my new/changed shell files + run the idle-translate build gate.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== shellcheck ==="
if command -v shellcheck >/dev/null 2>&1; then
  shellcheck plugin/sptc/hooks/post-tool-use.sh plugin/sptc/hooks/_common.sh ci/idle-translate/translate-proof-int.sh tests/hooks-parse.sh && echo "SHELLCHECK CLEAN"
else echo "(shellcheck not on PATH — checking how run-gates invokes it)"; sed -n '10,18p' ci/run-gates.sh; fi
echo "=== idle-translate build gate ==="; sh ci/idle-translate/build.sh 2>&1 | tail -6


=== shellcheck ===
(shellcheck not on PATH — checking how run-gates invokes it)
rc=0
gate() { printf '\n=== GATE: %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; rc=1; }

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

=== idle-translate build gate ===

running 22 tests
......................
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

ok  cc-spt-idle-translate: cargo test + release build


The gate uses `sh -n` (syntax), not shellcheck. Verify my shell files pass `sh -n`:


cd "C:/Users/decid/Documents/projects/spt-claude-code"
for f in plugin/sptc/hooks/post-tool-use.sh plugin/sptc/hooks/_common.sh ci/idle-translate/translate-proof-int.sh tests/hooks-parse.sh; do
  sh -n "$f" && echo "ok  $f" || echo "FAIL $f"
done


ok  plugin/sptc/hooks/post-tool-use.sh
ok  plugin/sptc/hooks/_common.sh
ok  ci/idle-translate/translate-proof-int.sh
ok  tests/hooks-parse.sh


All clean. T2 is complete end-to-end and gated. Commit it.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
git add plugin/sptc/skills/commune/SKILL.md adapter/claude-spt.toml plugin/sptc/hooks/hooks.json plugin/sptc/hooks/post-tool-use.sh plugin/sptc/hooks/_common.sh tools/cc-spt-idle-translate/src/main.rs ci/idle-translate/translate-proof-int.sh tests/hooks-parse.sh traceable-reqs.toml
git status --short
git commit -q -F - <<'EOF'
feat(checkpoint): T2 — agent-driven checkpoint communes via self-send loopback

A live agent flags a commune with the literal !!checkpoint!! trigger to
request a self-initiated context reset: spt clears and rebuilds its working
context from that very commune, no operator /clear needed. Mechanism (grilled
with doyle against the frozen v0.15.0 contract):

- Write-side trigger: a PostToolUse (Write matcher) hook detects !!checkpoint!!
  in the agent's own .claude/<id>-commune.md at Write-time, marks the perch
  idle, and self-sends a structured signal
  `spt send --from <id> <id> --json-payload '{"checkpoint":"v1","wake":…}'`.
  spt-core v0.15.0 strips the marker from the durable+pending context on
  ingest, so it is one-shot (never persists / re-fires).
- Delivery-side trigger: the json rides as an opaque `json="…"` attr on the
  <EVENT> envelope; the self-send loops back through the endpoint's own
  translation binary, which parses the attr (xml-attr-unescape, & last),
  and on checkpoint=="v1" fires the clear+wake macro: ctrl+s · 50ms · /clear ·
  enter · 500ms · wake · enter · commit (default wake "Proceed with next
  steps"). Collision-proof: the marker lives inside the structured attr, a
  normal message can never forge it.

Single !!checkpoint!! = default wake; a PAIR brackets a custom wake directive.

- Binary: checkpoint branch + 7 crate tests (macro, custom/default wake,
  unescape-amp-last, attr extract, malformed-fallthrough, non-checkpoint).
- Hook: post-tool-use.sh + _common.sh pure helpers (has_checkpoint /
  checkpoint_wake / is_commune_write) + manifest [hooks.PostToolUse] +
  hooks.json Write matcher; 15 hooks-parse unit tests.
- Skill: full-fat /sptc:commune SKILL.md (--checkpoint semantics) + a
  checkpoint [[hints]] entry.
- Int: translate-proof checkpoint block (EMIT half on the pre-0.15.0 daemon);
  the live self-send loopback stays the heavier real-claude int, deferred to
  the v0.15.0/counter-34 publish (capability-detect-skip).
- Activate REQ-DIST-CHECKPOINT-COMMUNE (doc+impl+unit+int). check GREEN.

min_spt_core stays 0.13.2 in-tree until doyle's publish ping, then bumps to
0.15.0 and the live loopback int lights.

Co-authored by: perri
EOF
echo "commit exit=$?"; git log -3 --format='%h %s'


M  adapter/claude-spt.toml
M  ci/idle-translate/translate-proof-int.sh
M  plugin/sptc/hooks/_common.sh
M  plugin/sptc/hooks/hooks.json
A  plugin/sptc/hooks/post-tool-use.sh
M  plugin/sptc/skills/commune/SKILL.md
M  tests/hooks-parse.sh
M  tools/cc-spt-idle-translate/src/main.rs
M  traceable-reqs.toml
commit exit=0
e27e0d3 feat(checkpoint): T2 — agent-driven checkpoint communes via self-send loopback
befa592 feat(idle-translate): T1 — frame inbound <EVENT> across multiple lines
211a5cc docs(checkpoint-commune): land design — ADR-0004, F-020/21/22, 5 REQ stubs


T2 shipped (e27e0d3). Now **T3** — psyche-download resume wiring at SessionStart, code-now against frozen contract B. Let me read `session-start.sh` to place it correctly (the bind/boundary/seed branches).


1	#!/bin/sh
2	# SessionStart: ensure spt-core installed, then register the perch — bind (spt-hosted, broker
3	# spawned us via [session.self] + injected $SPT_ENDPOINT_ID), seed (harness-hosted, user-launched
4	# CC), or boundary-rebind (clear/compact) — and persist session env so whoami + per-prompt hooks
5	# resolve. Non-blocking (never `listen`).
6	# [impl->REQ-DIST-HOOKS-API]
7	. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"
8
9	input=$(cat)
10	sid=$(json_str "$input" session_id)
11	src=$(json_str "$input" source)
12
13	# Invisible-installer: bootstrap spt-core if absent (no-op when present).
14	sh "$CLAUDE_PLUGIN_ROOT/bootstrap.sh" >/dev/null 2>&1 || true
15	SPT=$(spt_bin)
16
17	case "$(sptc_register_verb "$src")" in
18	  boundary)
19	    id=$(sptc_self_id "$sid")
20	    [ -n "$id" ] && "$SPT" api --adapter "$ADAPTER" boundary "$src" "$id" \
21	      --to-session-id "$sid" >/dev/null 2>&1 || true
22	    ;;
23	  bind)
24	    # spt-hosted: `spt endpoint run` spawned us into the broker PTY and injected the endpoint id via
25	    # [env.SPT_ENDPOINT_ID]. The perch already exists — self-register post-spawn by binding our
26	    # discovered CC session id, NOT seed (no seed file in this path). `bind` is an ESTABLISHING call:
27	    # the broker parentage IS the credential (auth intrinsic), so --set-session-id alone, no proof
28	    # token — confirmed by doyle 2026-06-15 (harness-contract/api.md "Auth is intrinsic"). Later
29	    # mutating calls prove association with --session-id "$sid" against the record bind wrote.
30	    "$SPT" api --adapter "$ADAPTER" bind "$SPT_ENDPOINT_ID" \
31	      --set-session-id "$sid" >/dev/null 2>&1 || true
32	    ;;
33	  seed)
34	    # harness-hosted (seed→listen path): user-launched CC. Record the seed for /sptc:ready|live.
35	    # ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter — `api listen`/`poll` resolve the
36	    # owning adapter from this seed's parent pid via [adapter] host_binaries = ["claude"] (the legacy-
37	    # parity bare flow). $PPID is the CC host process (basename `claude`), the host_binaries match-key.
38	    # (bind/boundary above keep their explicit --adapter override — sanctioned, valid on 0.9.0.)
39	    "$SPT" api seed --pid "$PPID" --session-id "$sid" >/dev/null 2>&1 || true
40	    ;;
41	esac
42
43	# Persist for the rest of the session (whoami + per-prompt hooks read these).
44	if [ -n "$CLAUDE_ENV_FILE" ]; then
45	  {
46	    printf 'OWL_SESSION_ID=%s\n' "$sid"
47	    printf 'SPT_ADAPTER=%s\n' "$ADAPTER"
48	  } >> "$CLAUDE_ENV_FILE"
49	fi
50
51	# Relay an agent-facing brief as additionalContext (REQ-DIST-SESSIONSTART-BRIEF). Perched sessions
52	# (bind/boundary — an id already exists) get the identity brief: who they are + perch-is-live +
53	# how to message. No-perch seeds on a node WITH subnet peers get the ring brief (how to reach others
54	# without a perch). Seeds with no peers, and subagent (agent_type) sessions, get nothing. All prose
55	# is adapter-string-backed; this hook only selects + composes. [impl->REQ-DIST-SESSIONSTART-BRIEF]
56	if ! sptc_is_subagent "$(json_str "$input" agent_type)"; then
57	  brief=""
58	  case "$(sptc_register_verb "$src")" in
59	    bind|boundary)
60	      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
61	      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
62	      [ -n "$bid" ] && brief=$(sptc_perch_brief "$bid")
63	      ;;
64	    seed)
65	      sptc_node_has_peers && brief=$(sptc_noperch_brief)
66	      ;;
67	  esac
68	  sptc_emit_additional_context "$brief"
69	fi
70	exit 0
71

#!/bin/sh
# SessionStart: ensure spt-core installed, then register the perch — bind (spt-hosted, broker
# spawned us via [session.self] + injected $SPT_ENDPOINT_ID), seed (harness-hosted, user-launched
# CC), or boundary-rebind (clear/compact) — and persist session env so whoami + per-prompt hooks
# resolve. Non-blocking (never `listen`).
# [impl->REQ-DIST-HOOKS-API]
. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"

input=$(cat)
sid=$(json_str "$input" session_id)
src=$(json_str "$input" source)

# Invisible-installer: bootstrap spt-core if absent (no-op when present).
sh "$CLAUDE_PLUGIN_ROOT/bootstrap.sh" >/dev/null 2>&1 || true
SPT=$(spt_bin)

case "$(sptc_register_verb "$src")" in
  boundary)
    id=$(sptc_self_id "$sid")
    [ -n "$id" ] && "$SPT" api --adapter "$ADAPTER" boundary "$src" "$id" \
      --to-session-id "$sid" >/dev/null 2>&1 || true
    ;;
  bind)
    # spt-hosted: `spt endpoint run` spawned us into the broker PTY and injected the endpoint id via
    # [env.SPT_ENDPOINT_ID]. The perch already exists — self-register post-spawn by binding our
    # discovered CC session id, NOT seed (no seed file in this path). `bind` is an ESTABLISHING call:
    # the broker parentage IS the credential (auth intrinsic), so --set-session-id alone, no proof
    # token — confirmed by doyle 2026-06-15 (harness-contract/api.md "Auth is intrinsic"). Later
    # mutating calls prove association with --session-id "$sid" against the record bind wrote.
    "$SPT" api --adapter "$ADAPTER" bind "$SPT_ENDPOINT_ID" \
      --set-session-id "$sid" >/dev/null 2>&1 || true
    ;;
  seed)
    # harness-hosted (seed→listen path): user-launched CC. Record the seed for /sptc:ready|live.
    # ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter — `api listen`/`poll` resolve the
    # owning adapter from this seed's parent pid via [adapter] host_binaries = ["claude"] (the legacy-
    # parity bare flow). $PPID is the CC host process (basename `claude`), the host_binaries match-key.
    # (bind/boundary above keep their explicit --adapter override — sanctioned, valid on 0.9.0.)
    "$SPT" api seed --pid "$PPID" --session-id "$sid" >/dev/null 2>&1 || true
    ;;
esac

# Persist for the rest of the session (whoami + per-prompt hooks read these).
if [ -n "$CLAUDE_ENV_FILE" ]; then
  {
    printf 'OWL_SESSION_ID=%s\n' "$sid"
    printf 'SPT_ADAPTER=%s\n' "$ADAPTER"
  } >> "$CLAUDE_ENV_FILE"
fi

# Relay an agent-facing brief as additionalContext (REQ-DIST-SESSIONSTART-BRIEF). Perched sessions
# (bind/boundary — an id already exists) get the identity brief: who they are + perch-is-live +
# how to message. No-perch seeds on a node WITH subnet peers get the ring brief (how to reach others
# without a perch). Seeds with no peers, and subagent (agent_type) sessions, get nothing. All prose
# is adapter-string-backed; this hook only selects + composes. [impl->REQ-DIST-SESSIONSTART-BRIEF]
if ! sptc_is_subagent "$(json_str "$input" agent_type)"; then
  brief=""
  case "$(sptc_register_verb "$src")" in
    bind|boundary)
      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
      [ -n "$bid" ] && brief=$(sptc_perch_brief "$bid")
      ;;
    seed)
      sptc_node_has_peers && brief=$(sptc_noperch_brief)
      ;;
  esac
  sptc_emit_additional_context "$brief"
fi
exit 0


Clear. T3 wires psyche-download into the bind|boundary brief composition — this is also the checkpoint re-seed path (post-`/clear` boundary), so it must fire on both (the REQ title confirms "also the checkpoint re-seed fast-path"). Pre-0.15.0 the verb is absent → empty stdout → graceful skip. Add the helpers to `_common.sh`:


Edit

# Impure: resolve + assemble the no-perch ring brief. [impl->REQ-DIST-SESSIONSTART-BRIEF]
sptc_noperch_brief() {
  sptc_assemble_noperch "$(sptc_brief messaging-no-perch)" "$(sptc_brief endpoint-list)"
}

# Impure: resolve + assemble the no-perch ring brief. [impl->REQ-DIST-SESSIONSTART-BRIEF]
sptc_noperch_brief() {
  sptc_assemble_noperch "$(sptc_brief messaging-no-perch)" "$(sptc_brief endpoint-list)"
}

# Impure: pull the live-agent durable RESUME context for endpoint id $1 (optional session id $2) via
# `spt api psyche-download` — the spt-core v0.15.0 W5 verb that closes F-020 (claude-spt rehydrates NO
# durable context today). stdout = durable role/live/project tiers + the freshest not-yet-synthesized
# <pending-commune>/<pending-signoff> (trigger stripped core-side), to inject VERBATIM. Mirrors the
# id-scoped `api poll` auth shape (--session-id proves association; project resolves from the bound
# cwd, no --project). Empty on NO-CONTEXT (stderr) / the verb absent (pre-v0.15.0 node) / unregistered
# — caller appends nothing. Also the checkpoint re-seed fast-path. [impl->REQ-DIST-RESUME-CONTEXT]
sptc_psyche_download() {
  _id="$1"; [ -z "$_id" ] && return 0
  _psid="$2"; _spt="$(spt_bin)"
  if [ -n "$_psid" ]; then
    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" --session-id "$_psid" 2>/dev/null
  else
    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" 2>/dev/null
  fi
}

# PURE: append resume context ($2) below a SessionStart brief ($1), skipping cleanly when the resume
# is empty (NO-CONTEXT). Newline-joined so the verbatim XML resume sits below the identity brief.
# [impl->REQ-DIST-RESUME-CONTEXT]
sptc_append_resume() {
  if [ -n "$2" ]; then printf '%s\n%s' "$1" "$2"; else printf '%s' "$1"; fi
}


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

# Impure: resolve + assemble the no-perch ring brief. [impl->REQ-DIST-SESSIONSTART-BRIEF]
sptc_noperch_brief() {
  sptc_assemble_noperch "$(sptc_brief messaging-no-perch)" "$(sptc_brief endpoint-list)"
}

# Impure: pull the live-agent durable RESUME context for endpoint id $1 (optional session id $2) via
# `spt api psyche-download` — the spt-core v0.15.0 W5 verb that closes F-020 (claude-spt rehydrates NO
# durable context today). stdout = durable role/live/project tiers + the freshest not-yet-synthesized
# <pending-commune>/<pending-signoff> (trigger stripped core-side), to inject VERBATIM. Mirrors the
# id-scoped `api poll` auth shape (--session-id proves association; project resolves from the bound
# cwd, no --project). Empty on NO-CONTEXT (stderr) / the verb absent (pre-v0.15.0 node) / unregistered
# — caller appends nothing. Also the checkpoint re-seed fast-path. [impl->REQ-DIST-RESUME-CONTEXT]
sptc_psyche_download() {
  _id="$1"; [ -z "$_id" ] && return 0
  _psid="$2"; _spt="$(spt_bin)"
  if [ -n "$_psid" ]; then
    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" --session-id "$_psid" 2>/dev/null
  else
    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" 2>/dev/null
  fi
}

# PURE: append resume context ($2) below a SessionStart brief ($1), skipping cleanly when the resume
# is empty (NO-CONTEXT). Newline-joined so the verbatim XML resume sits below the identity brief.
# [impl->REQ-DIST-RESUME-CONTEXT]
sptc_append_resume() {
  if [ -n "$2" ]; then printf '%s\n%s' "$1" "$2"; else printf '%s' "$1"; fi
}

+# Impure: pull the live-agent durable RESUME context for endpoint id $1 (optional session id $2) via

+# `spt api psyche-download` — the spt-core v0.15.0 W5 verb that closes F-020 (claude-spt rehydrates NO

+# durable context today). stdout = durable role/live/project tiers + the freshest not-yet-synthesized

+# <pending-commune>/<pending-signoff> (trigger stripped core-side), to inject VERBATIM. Mirrors the

+# id-scoped `api poll` auth shape (--session-id proves association; project resolves from the bound

+# cwd, no --project). Empty on NO-CONTEXT (stderr) / the verb absent (pre-v0.15.0 node) / unregistered

+# — caller appends nothing. Also the checkpoint re-seed fast-path. [impl->REQ-DIST-RESUME-CONTEXT]

+    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" --session-id "$_psid" 2>/dev/null

+    "$_spt" api --adapter "$ADAPTER" psyche-download "$_id" 2>/dev/null

+# PURE: append resume context ($2) below a SessionStart brief ($1), skipping cleanly when the resume

+# is empty (NO-CONTEXT). Newline-joined so the verbatim XML resume sits below the identity brief.

# PURE predicate: should this SessionStart inject a brief? Subagent sessions (agent_type set) get

# none. $1 = the stdin agent_type value (empty = a real user session). [impl->REQ-DIST-SESSIONSTART-BRIEF]


Wire it into `session-start.sh`'s bind|boundary brief branch:


    bind|boundary)
      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
      [ -n "$bid" ] && brief=$(sptc_perch_brief "$bid")
      ;;

    bind|boundary)
      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
      if [ -n "$bid" ]; then
        brief=$(sptc_perch_brief "$bid")
        # Durable RESUME context (F-020 fix, REQ-DIST-RESUME-CONTEXT): pull the agent's role/live/
        # project tiers + freshest pending commune/signoff and inject it VERBATIM below the identity
        # brief. Fires on a fresh spt-hosted bind AND on a post-/clear boundary — boundary IS the
        # checkpoint re-seed path, where the just-cleared agent rebuilds from its latest commune. The
        # `api boundary` rotation above is unchanged; this is an additive context pull. Empty on
        # NO-CONTEXT or a pre-v0.15.0 node (verb absent) -> nothing appended.
        brief=$(sptc_append_resume "$brief" "$(sptc_psyche_download "$bid" "$sid")")
      fi
      ;;


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

    bind|boundary)
      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
      if [ -n "$bid" ]; then
        brief=$(sptc_perch_brief "$bid")
        # Durable RESUME context (F-020 fix, REQ-DIST-RESUME-CONTEXT): pull the agent's role/live/
        # project tiers + freshest pending commune/signoff and inject it VERBATIM below the identity
        # brief. Fires on a fresh spt-hosted bind AND on a post-/clear boundary — boundary IS the
        # checkpoint re-seed path, where the just-cleared agent rebuilds from its latest commune. The
        # `api boundary` rotation above is unchanged; this is an additive context pull. Empty on
        # NO-CONTEXT or a pre-v0.15.0 node (verb absent) -> nothing appended.
        brief=$(sptc_append_resume "$brief" "$(sptc_psyche_download "$bid" "$sid")")
      fi
      ;;

#!/bin/sh
# SessionStart: ensure spt-core installed, then register the perch — bind (spt-hosted, broker
# spawned us via [session.self] + injected $SPT_ENDPOINT_ID), seed (harness-hosted, user-launched
# CC), or boundary-rebind (clear/compact) — and persist session env so whoami + per-prompt hooks
# resolve. Non-blocking (never `listen`).
# [impl->REQ-DIST-HOOKS-API]
. "$CLAUDE_PLUGIN_ROOT/hooks/_common.sh"

input=$(cat)
sid=$(json_str "$input" session_id)
src=$(json_str "$input" source)

# Invisible-installer: bootstrap spt-core if absent (no-op when present).
sh "$CLAUDE_PLUGIN_ROOT/bootstrap.sh" >/dev/null 2>&1 || true
SPT=$(spt_bin)

case "$(sptc_register_verb "$src")" in
  boundary)
    id=$(sptc_self_id "$sid")
    [ -n "$id" ] && "$SPT" api --adapter "$ADAPTER" boundary "$src" "$id" \
      --to-session-id "$sid" >/dev/null 2>&1 || true
    ;;
  bind)
    # spt-hosted: `spt endpoint run` spawned us into the broker PTY and injected the endpoint id via
    # [env.SPT_ENDPOINT_ID]. The perch already exists — self-register post-spawn by binding our
    # discovered CC session id, NOT seed (no seed file in this path). `bind` is an ESTABLISHING call:
    # the broker parentage IS the credential (auth intrinsic), so --set-session-id alone, no proof
    # token — confirmed by doyle 2026-06-15 (harness-contract/api.md "Auth is intrinsic"). Later
    # mutating calls prove association with --session-id "$sid" against the record bind wrote.
    "$SPT" api --adapter "$ADAPTER" bind "$SPT_ENDPOINT_ID" \
      --set-session-id "$sid" >/dev/null 2>&1 || true
    ;;
  seed)
    # harness-hosted (seed→listen path): user-launched CC. Record the seed for /sptc:ready|live.
    # ADAPTER-AGNOSTIC since spt-core v0.9.0 (PREP-4): NO --adapter — `api listen`/`poll` resolve the
    # owning adapter from this seed's parent pid via [adapter] host_binaries = ["claude"] (the legacy-
    # parity bare flow). $PPID is the CC host process (basename `claude`), the host_binaries match-key.
    # (bind/boundary above keep their explicit --adapter override — sanctioned, valid on 0.9.0.)
    "$SPT" api seed --pid "$PPID" --session-id "$sid" >/dev/null 2>&1 || true
    ;;
esac

# Persist for the rest of the session (whoami + per-prompt hooks read these).
if [ -n "$CLAUDE_ENV_FILE" ]; then
  {
    printf 'OWL_SESSION_ID=%s\n' "$sid"
    printf 'SPT_ADAPTER=%s\n' "$ADAPTER"
  } >> "$CLAUDE_ENV_FILE"
fi

# Relay an agent-facing brief as additionalContext (REQ-DIST-SESSIONSTART-BRIEF). Perched sessions
# (bind/boundary — an id already exists) get the identity brief: who they are + perch-is-live +
# how to message. No-perch seeds on a node WITH subnet peers get the ring brief (how to reach others
# without a perch). Seeds with no peers, and subagent (agent_type) sessions, get nothing. All prose
# is adapter-string-backed; this hook only selects + composes. [impl->REQ-DIST-SESSIONSTART-BRIEF]
if ! sptc_is_subagent "$(json_str "$input" agent_type)"; then
  brief=""
  case "$(sptc_register_verb "$src")" in
    bind|boundary)
      # id: the spt-hosted endpoint id if injected, else resolve our perch via whoami.
      bid="${SPT_ENDPOINT_ID:-$(sptc_self_id "$sid")}"
      [ -n "$bid" ] && brief=$(sptc_perch_brief "$bid")
      ;;
    seed)
      sptc_node_has_peers && brief=$(sptc_noperch_brief)
      ;;
  esac
  sptc_emit_additional_context "$brief"
fi
exit 0

-      [ -n "$bid" ] && brief=$(sptc_perch_brief "$bid")

+        brief=$(sptc_perch_brief "$bid")

+        # Durable RESUME context (F-020 fix, REQ-DIST-RESUME-CONTEXT): pull the agent's role/live/

+        # project tiers + freshest pending commune/signoff and inject it VERBATIM below the identity

+        # brief. Fires on a fresh spt-hosted bind AND on a post-/clear boundary — boundary IS the

+        # checkpoint re-seed path, where the just-cleared agent rebuilds from its latest commune. The

+        # `api boundary` rotation above is unchanged; this is an additive context pull. Empty on

+        # NO-CONTEXT or a pre-v0.15.0 node (verb absent) -> nothing appended.

+        brief=$(sptc_append_resume "$brief" "$(sptc_psyche_download "$bid" "$sid")")


Add the pure `sptc_append_resume` unit tests:


[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

# --- resume-context append: skip cleanly on NO-CONTEXT (REQ-DIST-RESUME-CONTEXT) ---
# [unit->REQ-DIST-RESUME-CONTEXT]
# Non-empty resume -> appended below the brief, newline-joined (verbatim XML sits under the identity).
check "append resume present" \
  "$(printf '<brief/>\n<live-context>ctx</live-context>')" \
  "$(sptc_append_resume '<brief/>' '<live-context>ctx</live-context>')"
# Empty resume (NO-CONTEXT) -> brief unchanged, no trailing newline injected.
check "append resume empty -> brief verbatim" "<brief/>" "$(sptc_append_resume '<brief/>' '')"
# Both empty -> empty.
check "append resume both empty" "" "$(sptc_append_resume '' '')"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }


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

# --- resume-context append: skip cleanly on NO-CONTEXT (REQ-DIST-RESUME-CONTEXT) ---
# [unit->REQ-DIST-RESUME-CONTEXT]
# Non-empty resume -> appended below the brief, newline-joined (verbatim XML sits under the identity).
check "append resume present" \
  "$(printf '<brief/>\n<live-context>ctx</live-context>')" \
  "$(sptc_append_resume '<brief/>' '<live-context>ctx</live-context>')"
# Empty resume (NO-CONTEXT) -> brief unchanged, no trailing newline injected.
check "append resume empty -> brief verbatim" "<brief/>" "$(sptc_append_resume '<brief/>' '')"
# Both empty -> empty.
check "append resume both empty" "" "$(sptc_append_resume '' '')"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

#!/bin/sh
# Unit tests for the sptc hook parsing glue (pure functions; no spt/CC needed).
# Run: sh tests/hooks-parse.sh   (exit 0 = pass)
. "$(dirname "$0")/../plugin/sptc/hooks/_common.sh"

fail=0
check() { # check  <expected> <actual>
  if [ "$2" = "$3" ]; then printf 'ok   %s\n' "$1"
  else printf 'FAIL %s\n  expected: [%s]\n  actual:   [%s]\n' "$1" "$2" "$3"; fail=1; fi
}

# --- json_str: top-level CC hook-input fields ---  [unit->REQ-DIST-HOOKS-API]
J='{"session_id":"sess-abc123","transcript_path":"/x","source":"startup","model":"m"}'
check "json_str session_id" "sess-abc123" "$(json_str "$J" session_id)"
check "json_str source"     "startup"     "$(json_str "$J" source)"
check "json_str prompt"      "/sptc:send doyle" \
  "$(json_str '{"session_id":"s1","prompt":"/sptc:send doyle"}' prompt)"
check "json_str missing key" "" "$(json_str "$J" nope)"

# --- sptc_unescape:
 -> newline, entities, & LAST --- [unit->REQ-UPS-INJECTION]
check "unescape br+entities" \
  "$(printf 'a \n"c" & <')" \
  "$(sptc_unescape 'a <b>
"c" & &lt;')"

# --- render_frames: canonical <EVENT> envelope --- [unit->REQ-UPS-INJECTION]
# Named single,
-escaped multiline body, sender preserved.
named=$(render_frames '<EVENT type="msg" from="doyle">hello
world</EVENT>')
check "render named <EVENT>" \
  "$(printf '<sptc_messages from="doyle">\nhello\nworld\n</sptc_messages>')" "$named"

# Entity-escaped body decodes.
ent=$(render_frames '<EVENT type="msg" from="kit">a <tag> & b</EVENT>')
check "render entity body" \
  "$(printf '<sptc_messages from="kit">\na <tag> & b\n</sptc_messages>')" "$ent"

# Multi-message drain: two self-delimiting envelopes -> two rendered blocks.
multi=$(render_frames '<EVENT type="msg" from="a">one</EVENT><EVENT type="msg" from="b">two</EVENT>')
check "render multi-message" \
  "$(printf '<sptc_messages from="a">\none\n</sptc_messages>\n<sptc_messages from="b">\ntwo\n</sptc_messages>')" \
  "$multi"

# Envelope without a from attr -> anonymous rendering.
anon=$(render_frames '<EVENT type="msg">sys note</EVENT>')
check "render no-from" "$(printf '<sptc_messages>\nsys note\n</sptc_messages>')" "$anon"

# Empty drain -> no output.
check "render empty" "" "$(render_frames '')"

# --- sptc_skill_key: detect /sptc:<skill> in a slash-command prompt --- [unit->REQ-UPS-INJECTION]
check "skill_key bare"        "ready"  "$(sptc_skill_key '/sptc:ready')"
check "skill_key with args"   "send"   "$(sptc_skill_key '/sptc:send doyle hi')"
check "skill_key leading ws"  "setup"  "$(sptc_skill_key '   /sptc:setup')"
check "skill_key hyphenated"  "list-agents" "$(sptc_skill_key '/sptc:list-agents')"
check "skill_key mid-prose"   ""       "$(sptc_skill_key 'please run /sptc:ready for me')"
check "skill_key other ns"    ""       "$(sptc_skill_key '/other:ready')"
check "skill_key not a cmd"   ""       "$(sptc_skill_key 'ready set go')"
check "skill_key empty"       ""       "$(sptc_skill_key '')"

# --- sptc_register_verb: SessionStart bind/seed/boundary branch --- [unit->REQ-DIST-SHORTCUT-BASENAME]
# spt-hosted (endpoint id injected) -> bind; harness-hosted (no id) -> seed; clear/compact -> boundary.
unset SPT_ENDPOINT_ID
check "verb harness-hosted startup"   "seed"     "$(sptc_register_verb startup)"
check "verb harness-hosted empty src" "seed"     "$(sptc_register_verb '')"
check "verb boundary clear"           "boundary" "$(sptc_register_verb clear)"
check "verb boundary compact"         "boundary" "$(sptc_register_verb compact)"
SPT_ENDPOINT_ID=cc-7; export SPT_ENDPOINT_ID
check "verb spt-hosted startup -> bind" "bind"     "$(sptc_register_verb startup)"
# A /clear inside an spt-hosted session still rebinds via boundary (never re-binds fresh).
check "verb spt-hosted clear -> boundary" "boundary" "$(sptc_register_verb clear)"
unset SPT_ENDPOINT_ID

# --- sptc_cap_output: additionalContext spill guard (ADR-0002 Open #2) --- [unit->REQ-UPS-INJECTION]
spill="${TMPDIR:-/tmp}/sptc-cap-$$.txt"
rm -f "$spill"
check "cap under -> passthrough verbatim" "small body" "$(sptc_cap_output 'small body' 9000 "$spill")"
check "cap under -> no spill written"     "absent"     "$([ -f "$spill" ] && echo present || echo absent)"

over=$(sptc_cap_output 'abcdefghij' 5 "$spill")   # 10 bytes > cap 5
case "$over" in *'<sptc_overflow'*) m=ok ;; *) m=no ;; esac
check "cap over -> emits overflow marker"  "ok"         "$m"
check "cap over -> full body spilled"      "abcdefghij" "$(cat "$spill" 2>/dev/null)"
case "$over" in *abcdefghij*) leak=leaked ;; *) leak=clean ;; esac
check "cap over -> body not inlined"       "clean"      "$leak"
check "cap empty -> no output"             ""           "$(sptc_cap_output '' 5 "$spill")"
rm -f "$spill"

# --- SessionStart briefs: pure composition --- [unit->REQ-DIST-SESSIONSTART-BRIEF]
# assemble_perch: {id} substitution + block structure.
pb=$(sptc_assemble_perch "perri" 'you are {id}.' 'send body' 'roster line')
check "assemble_perch structure+subst" \
  "$(printf '<sptc-active-perch id="perri">\nyou are perri.\n\nsend body\n\nroster line\n</sptc-active-perch>')" \
  "$pb"
case "$pb" in *'{id}'*) leftover=present ;; *) leftover=clean ;; esac
check "assemble_perch no {id} leftover" "clean" "$leftover"
# Every {id} occurrence is substituted (global).
multi_id=$(sptc_assemble_perch "kit" 'a {id} b {id}' 'm' 'r')
case "$multi_id" in *'a kit b kit'*) g=ok ;; *) g=no ;; esac
check "assemble_perch global subst" "ok" "$g"

# assemble_noperch: ring block structure (no id).
nb=$(sptc_assemble_noperch 'ring body' 'roster line')
check "assemble_noperch structure" \
  "$(printf '<sptc-reach>\nring body\n\nroster line\n</sptc-reach>')" "$nb"

# is_subagent: empty -> real session (inject); set -> subagent (skip).
check "is_subagent empty -> no"   "no"  "$(sptc_is_subagent ''               && echo yes || echo no)"
check "is_subagent set -> yes"    "yes" "$(sptc_is_subagent 'general-purpose' && echo yes || echo no)"

# has_peers_lines: >1 non-empty line on `subnet status` stdin = peers present.
check "peers: header+row -> yes" "yes" "$(printf 'SUBNET NODES ENDPOINTS\nSPT_DEV 3 4\n' | sptc_has_peers_lines && echo yes || echo no)"
check "peers: single line -> no" "no"  "$(printf 'no subnets\n'                            | sptc_has_peers_lines && echo yes || echo no)"
check "peers: empty -> no"       "no"  "$(printf ''                                          | sptc_has_peers_lines && echo yes || echo no)"

# --- SessionStart briefs: JSON encode + emit --- [unit->REQ-DIST-SESSIONSTART-BRIEF]
check "json_escape quotes"    'say \"hi\"' "$(printf '%s' 'say "hi"' | sptc_json_escape)"
check "json_escape backslash" 'a\\b'       "$(printf '%s' 'a\b'      | sptc_json_escape)"
check "json_escape newline"   'a\nb'       "$(printf 'a\nb'          | sptc_json_escape)"

check "emit empty -> silent" "" "$(sptc_emit_additional_context '')"
check "emit json shape" \
  '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"hi \"x\""}}' \
  "$(sptc_emit_additional_context 'hi "x"')"
check "emit json newline-escaped" \
  '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"a\nb"}}' \
  "$(sptc_emit_additional_context "$(printf 'a\nb')")"

# --- checkpoint detection: trigger presence, custom-wake extraction, commune-write guard ---
# [unit->REQ-DIST-CHECKPOINT-COMMUNE]
check "has_checkpoint single"  "yes" "$(sptc_has_checkpoint 'delta ... !!checkpoint!!'        && echo yes || echo no)"
check "has_checkpoint pair"    "yes" "$(sptc_has_checkpoint '!!checkpoint!! wake !!checkpoint!!' && echo yes || echo no)"
check "has_checkpoint absent"  "no"  "$(sptc_has_checkpoint 'an ordinary commune delta'        && echo yes || echo no)"
check "has_checkpoint empty"   "no"  "$(sptc_has_checkpoint ''                                  && echo yes || echo no)"

# Single marker -> no custom wake (binary applies the default).
check "wake single -> default(empty)" "" "$(sptc_checkpoint_wake 'work ... !!checkpoint!!')"
# Paired markers -> the trimmed inner text is the custom wake.
check "wake pair -> custom" "Resume T2c now" \
  "$(sptc_checkpoint_wake 'body !!checkpoint!! Resume T2c now !!checkpoint!! more')"
# Pair embedded in a realistic single-line JSON content blob.
check "wake pair in json blob" "wire the hook" \
  "$(sptc_checkpoint_wake '{"content":"delta\n!!checkpoint!! wire the hook !!checkpoint!!\nend"}')"
check "wake none -> empty"     "" "$(sptc_checkpoint_wake 'no markers here')"

# commune-write guard: Write to <id>-commune.md only (tolerates JSON-escaped Windows path).
check "commune_write match unix"  "yes" \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write match win"   "yes" \
  "$(sptc_is_commune_write Write 'C:\\\\Users\\\\d\\\\.claude\\\\perri-commune.md' perri && echo yes || echo no)"
check "commune_write wrong file"  "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/notes.md' perri && echo yes || echo no)"
check "commune_write wrong id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/doyle-commune.md' perri && echo yes || echo no)"
check "commune_write not Write"   "no"  \
  "$(sptc_is_commune_write Edit '/home/x/.claude/perri-commune.md' perri && echo yes || echo no)"
check "commune_write empty id"    "no"  \
  "$(sptc_is_commune_write Write '/home/x/.claude/perri-commune.md' '' && echo yes || echo no)"

[ "$fail" -eq 0 ] && { echo "ALL PASS"; exit 0; } || { echo "FAILURES"; exit 1; }

check "commune_write empty id"    "no"  \

+# --- resume-context append: skip cleanly on NO-CONTEXT (REQ-DIST-RESUME-CONTEXT) ---

+# Non-empty resume -> appended below the brief, newline-joined (verbatim XML sits under the identity).

+  "$(printf '<brief/>\n<live-context>ctx</live-context>')" \

+  "$(sptc_append_resume '<brief/>' '<live-context>ctx</live-context>')"

+# Empty resume (NO-CONTEXT) -> brief unchanged, no trailing newline injected.

+check "append resume empty -> brief verbatim" "<brief/>" "$(sptc_append_resume '<brief/>' '')"

+check "append resume both empty" "" "$(sptc_append_resume '' '')"


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== syntax ==="; sh -n plugin/sptc/hooks/session-start.sh && sh -n plugin/sptc/hooks/_common.sh && echo "syntax ok"
echo "=== hooks-parse ==="; sh tests/hooks-parse.sh 2>&1 | tail -5


=== syntax ===
syntax ok
=== hooks-parse ===
ok   commune_write empty id
ok   append resume present
ok   append resume empty -> brief verbatim
ok   append resume both empty
ALL PASS


Now activate REQ-DIST-RESUME-CONTEXT (impl+unit; int deferred to W5 validation):


Edit

required_stages = []  # Parity wiring / task T3. ACTIVATE when spt-core v0.15.0 W5 ships the verb (F-020, frozen Tier-1 contract). impl: session-start.sh bind|boundary branch -> call `spt api psyche-download <self-id>` + inject stdout, skip on NO-CONTEXT (stderr). boundary stays rotation-only. unit: tests/hooks-parse.sh. int: real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 gated+built ping. No adapter-side drop handling (core owns the race-free append).

required_stages = ["impl", "unit"]  # Parity wiring / task T3 — impl+unit ACTIVATED 2026-06-24 (coded against the FROZEN Tier-1 contract B); int DEFERRED until spt-core v0.15.0 W5 ships the verb (then add "int" + validate on doyle's gated+built ping). impl: session-start.sh bind|boundary brief branch -> sptc_psyche_download (mirrors `api poll` id-scoped auth: --session-id, no --project — project resolves from the bound cwd) + sptc_append_resume injects stdout VERBATIM below the identity brief, skip on NO-CONTEXT/empty (pre-v0.15.0 the verb is absent -> empty -> graceful skip). Fires on bind AND boundary — boundary IS the checkpoint re-seed path (post-/clear rebuild from the latest commune); the `api boundary` rotation itself is unchanged (additive pull). unit: tests/hooks-parse.sh sptc_append_resume skip-on-NO-CONTEXT tests. int (DEFERRED): real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 ping. No adapter-side raw-drop handling (core owns the race-free append).


title = "claude-spt SessionStart pulls the live agent resume context via the v0.15.0 `spt api psyche-download <self-id> [--session-id <sid>]` verb and injects stdout as additionalContext (skip on NO-CONTEXT) — closing a pre-existing parity gap (claude-spt rehydrates NO durable context today; F-020). One call returns durable role/live/project tiers + the freshest not-yet-synthesized <pending-commune>/<pending-signoff> (trigger stripped core-side); it is also the checkpoint re-seed fast-path."

-required_stages = []  # Parity wiring / task T3. ACTIVATE when spt-core v0.15.0 W5 ships the verb (F-020, frozen Tier-1 contract). impl: session-start.sh bind|boundary branch -> call `spt api psyche-download <self-id>` + inject stdout, skip on NO-CONTEXT (stderr). boundary stays rotation-only. unit: tests/hooks-parse.sh. int: real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 gated+built ping. No adapter-side drop handling (core owns the race-free append).

+required_stages = ["impl", "unit"]  # Parity wiring / task T3 — impl+unit ACTIVATED 2026-06-24 (coded against the FROZEN Tier-1 contract B); int DEFERRED until spt-core v0.15.0 W5 ships the verb (then add "int" + validate on doyle's gated+built ping). impl: session-start.sh bind|boundary brief branch -> sptc_psyche_download (mirrors `api poll` id-scoped auth: --session-id, no --project — project resolves from the bound cwd) + sptc_append_resume injects stdout VERBATIM below the identity brief, skip on NO-CONTEXT/empty (pre-v0.15.0 the verb is absent -> empty -> graceful skip). Fires on bind AND boundary — boundary IS the checkpoint re-seed path (post-/clear rebuild from the latest commune); the `api boundary` rotation itself is unchanged (additive pull). unit: tests/hooks-parse.sh sptc_append_resume skip-on-NO-CONTEXT tests. int (DEFERRED): real psyche-download output injected at SessionStart, durable+pending present — validate on doyle's W5 ping. No adapter-side raw-drop handling (core owns the race-free append).


Bash

cd "C:/Users/decid/Documents/projects/spt-claude-code"
traceable-reqs check 2>&1 | grep -E "REQ-DIST-RESUME-CONTEXT"
traceable-reqs check >/dev/null 2>&1 && echo "CHECK GREEN (exit 0)" || echo "CHECK RED"


  [OK] REQ-DIST-RESUME-CONTEXT  required: [impl, unit]  stages: -doc +impl +unit -int
CHECK GREEN (exit 0)


T3 complete, check green. Commit it.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
git add plugin/sptc/hooks/_common.sh plugin/sptc/hooks/session-start.sh tests/hooks-parse.sh traceable-reqs.toml
git status --short
git commit -q -F - <<'EOF'
feat(resume): T3 — pull durable resume context at SessionStart (psyche-download)

Close F-020: claude-spt rehydrates NO durable resume context today (a
perched SessionStart only does `api boundary` rotation + the identity brief;
the <psyche-context> on a live session was LEGACY owl's hook). Wire the
spt-core v0.15.0 W5 `spt api psyche-download <id>` verb into the bind|boundary
brief branch and inject its stdout VERBATIM below the identity brief —
durable role/live/project tiers + the freshest not-yet-synthesized
<pending-commune>/<pending-signoff> (trigger stripped core-side).

- Fires on a fresh spt-hosted bind AND on a post-/clear boundary — boundary IS
  the checkpoint re-seed path (the just-cleared agent rebuilds from its latest
  commune). The `api boundary` rotation is unchanged; this is an additive pull.
- Mirrors `api poll`'s id-scoped auth (--session-id; project resolves from the
  bound cwd, no --project — frozen contract B).
- Graceful: empty on NO-CONTEXT (stderr) or a pre-v0.15.0 node (verb absent)
  -> nothing appended.
- sptc_psyche_download (impure resolver) + sptc_append_resume (pure, tested:
  skip-on-NO-CONTEXT).
- Activate REQ-DIST-RESUME-CONTEXT impl+unit; int DEFERRED to the v0.15.0 W5
  ship (real psyche-download output injected — validate on doyle's ping).

Coded against the frozen Tier-1 contract; un-validatable until W5 lands.

Co-authored by: perri
EOF
echo "commit exit=$?"; git log -4 --format='%h %s'


M  plugin/sptc/hooks/_common.sh
M  plugin/sptc/hooks/session-start.sh
M  tests/hooks-parse.sh
M  traceable-reqs.toml
commit exit=0
38044e2 feat(resume): T3 — pull durable resume context at SessionStart (psyche-download)
e27e0d3 feat(checkpoint): T2 — agent-driven checkpoint communes via self-send loopback
befa592 feat(idle-translate): T1 — frame inbound <EVENT> across multiple lines
211a5cc docs(checkpoint-commune): land design — ADR-0004, F-020/21/22, 5 REQ stubs


All three tasks committed. Run the consolidated gate to confirm the whole wave is green together.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
echo "=== traceable-reqs ==="; traceable-reqs check >/dev/null 2>&1 && echo GREEN || echo RED
echo "=== binary cargo test ==="; (cd tools/cc-spt-idle-translate && cargo test 2>&1 | grep "test result")
echo "=== all tests/*.sh syntax + hooks-parse + manifest ==="
sh tests/hooks-parse.sh >/dev/null 2>&1 && echo "hooks-parse: PASS" || echo "hooks-parse: FAIL"
sh tests/manifest-shortcut.sh >/dev/null 2>&1 && echo "manifest-shortcut: PASS" || echo "manifest-shortcut: FAIL"
sh ci/manifest/check-manifest.sh >/dev/null 2>&1 && echo "manifest-schema: PASS" || echo "manifest-schema: FAIL"
sh ci/idle-translate/translate-proof-int.sh >/dev/null 2>&1 && echo "translate-proof-int: PASS" || echo "translate-proof-int: FAIL"
echo "=== shell syntax all touched ==="
for f in plugin/sptc/hooks/*.sh tests/hooks-parse.sh ci/idle-translate/translate-proof-int.sh; do sh -n "$f" || echo "SYNTAX FAIL $f"; done; echo "syntax ok"
echo "=== git clean? ==="; git status --short


=== traceable-reqs ===
GREEN
=== binary cargo test ===
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== all tests/*.sh syntax + hooks-parse + manifest ===
hooks-parse: PASS
manifest-shortcut: PASS
manifest-schema: PASS
translate-proof-int: PASS
=== shell syntax all touched ===
syntax ok
=== git clean? ===


Whole wave green, tree clean. Let me notify doyle (he pings me on the counter-34 publish to trigger the `min_spt_core` bump + light the deferred ints) and record the wave in memory — independent, so in parallel.


cd "C:/Users/decid/Documents/projects/spt-claude-code"
$OWL send doyle perri <<'EOF'
T2 + T3 BUILT & committed (befa592 T1, e27e0d3 T2, 38044e2 T3). All gates GREEN: 22 binary tests, hooks-parse, manifest-schema, translate-proof-int (incl. checkpoint emit-half + multi-line frame). traceable-reqs GREEN.

- T2 checkpoint: binary json-attr branch + PostToolUse hook + full-fat /sptc:commune --checkpoint. REQ-DIST-CHECKPOINT-COMMUNE doc+impl+unit+int. EMIT-half proven via translate-proof on 0.14.2 (crafted the json= attr envelope directly). DEFERRED int = the live `spt send --json-payload` self-send loopback.
- T3 resume: psyche-download wired into session-start.sh bind|boundary brief (= checkpoint re-seed path). REQ-DIST-RESUME-CONTEXT impl+unit; int DEFERRED.

min_spt_core stays 0.13.2 in-tree. WAITING ON YOUR counter-34 / v0.15.0 publish ping → then I bump to 0.15.0 + light both deferred ints (json-payload self-send loopback + psyche-download SessionStart injection). Ping when deployah publishes the hashes. Thanks for the clean contract.
EOF
echo "sent=$?"


➜ SENT:doyle
sent=0