#!/bin/sh
# LIVE wake-survival E2E — the checkpoint clear+wake macro fired END-TO-END on a real box, proven by
# a SIDE-EFFECT file the woken session writes. This is the graduated live successor to the deterministic
# EMIT int (ci/idle-translate/translate-proof-int.sh checkpoint block, which asserts ARM emits clear-only
# and FIRE emits wake-only in isolation): here a real CC session is bound, a `{"wake_arm":"v1","directive":…}`
# envelope is delivered, the /clear boundary rotates the session, and the wake directive is observed to
# EXECUTE (the woken agent writes the proof file). [int->REQ-HAZARD-CHECKPOINT-CLEAR-RACE]
#
# Also graduates the DEFERRED live assertion logged under REQ-HAZARD-EMPTY-RESPONSE-COMMIT ("the live
# every-clear boundary survival assert graduates with wake-repro.sh -> the wake-survival e2e once core
# C-1 lands"): the translate binary is asserted ALIVE after the /clear boundary (the B6 ghost —
# every-clear zero-record kill — would have terminated it). [int->REQ-HAZARD-EMPTY-RESPONSE-COMMIT]
#
# The full five-layer stack must hold for the proof to land: zero-record {commit} terminator (v0.13.1) +
# core C-1 miss!=fault + core C-2 ready-restrand at the boundary + enter-coalescing submit settle
# (v0.13.3). Requires spt-core >= 0.24.0 (C-1/C-2 = B6 closure) and an installed claude-spt >= 0.13.3.
#
# GROUND TRUTH IS THE SIDE-EFFECT FILE, never a transcript: broker-PTY probe sessions leave NO transcript
# jsonl in any config root (rig gap, banked) — assert via the file the wake writes, not by reading history.
#
# Spawns a real endpoint + headless claude, pre-trusts the disposable cwd + nested psyche dir in
# ~/.claude.json (ATOMICALLY: tmp + fsync + os.replace — NEVER open-truncate the trust store; ENOSPC
# mid-write wipes every project's trust) and removes those two keys again on exit. Mutates node-local
# perch state; all torn down on exit. Gated behind SPTC_ACCEPTANCE=1. Idempotent (per-run unique id).
# Run: SPTC_ACCEPTANCE=1 sh ci/idle-translate/wake-survival-int.sh   (exit 0 = pass).
set -u
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
. "$ROOT/ci/lib/spt-probe.sh"
. "$ROOT/ci/lib/spt-bringup.sh"

# Disposable perch id — NEVER a live agent's id (REQ-HAZARD-PERCH-COLLISION). Per-run unique ($$): the
# daemon hosts a session at most once per session_id, so a fixed id would not re-bind on a rerun.
PROBE=sptc-ci-wakesurv-$$
SUBNET="${SPTC_CI_SUBNET:-SPT_DEV}"
OWLERY="${SPT_HOME:-$HOME/AppData/Local/spt-core}/owlery"

if [ "${SPTC_ACCEPTANCE:-0}" != "1" ]; then echo "SKIP: set SPTC_ACCEPTANCE=1 to run (spawns a live CC session + mutates perch + trust state)"; exit 0; fi
command -v spt >/dev/null 2>&1 || { echo "SKIP: no spt on PATH"; exit 0; }
command -v python >/dev/null 2>&1 || { echo "SKIP: no python (trust-seed + info.json reads)"; exit 0; }
ver=$(spt --version 2>/dev/null | awk '{print $NF}')
# B6 closure (C-1 miss!=fault + C-2 ready-restrand) lands in spt-core 0.24.0 — below it the wake cannot
# survive the boundary and this proof is expected-red for reasons outside the adapter's control.
case "$ver" in
  0.24.*|0.25.*|0.26.*|0.27.*|0.28.*|0.29.*|0.3[0-9].*|0.[4-9][0-9].*|[1-9].*) : ;;
  *) echo "SKIP: spt $ver < 0.24.0 (B6 closure C-1/C-2 not present; wake cannot survive the boundary)"; exit 0 ;;
esac

RIG=$(mktemp -d 2>/dev/null) || { echo "FATAL: mktemp -d"; exit 2; }
CWD="$RIG/cwd"; PROOF="$RIG/WAKE-PROOF.txt"; EXPECT="WAKE-OK-$PROBE"
mkdir -p "$CWD/.claude"
NESTED="$OWLERY/$PROBE/nested/$PROBE-psyche"

fail=0
ok()  { echo "ok   $1"; }
bad() { echo "FAIL $1"; fail=1; }
ts()  { date -u +%H:%M:%S.%3NZ 2>/dev/null || date -u +%H:%M:%SZ; }
alive() { powershell -Command "[bool](Get-Process -Id $1 -ErrorAction SilentlyContinue)" 2>/dev/null | tr -d '\r'; }
# Windows pids of the running translate binaries (claude-spt.exe with a `translate` command line).
tpids() { powershell -Command "Get-CimInstance Win32_Process -Filter \"Name='claude-spt.exe'\" | Where-Object {\$_.CommandLine -match 'translate'} | Select-Object -ExpandProperty ProcessId" 2>/dev/null | tr -d '\r' | tr '\n' ' '; }
sid_of() { python -X utf8 -c "import json;print(json.load(open(r'$OWLERY/$PROBE/info.json'))['session_id'])" 2>/dev/null; }

cleanup() {
  # Kill the probe's CC + translate subtree (by matching command line to the disposable id).
  powershell -Command "Get-CimInstance Win32_Process -Filter \"Name='claude.exe' OR Name='claude-spt.exe'\" | Where-Object {\$_.CommandLine -match '$PROBE'} | ForEach-Object { Stop-Process -Id \$_.ProcessId -Force -Confirm:\$false -ErrorAction SilentlyContinue }" >/dev/null 2>&1
  spt endpoint stop "$PROBE" >/dev/null 2>&1 || true
  echo y | spt endpoint purge "$PROBE" --force >/dev/null 2>&1 || true
  # Remove the two trust keys we added (leave every OTHER project's trust untouched; atomic write).
  python -X utf8 - "$CWD" "$NESTED" <<'PY' 2>/dev/null || true
import json,os,sys
cwd,nested=sys.argv[1:3]
p=os.path.expanduser('~/.claude.json')
try: d=json.load(open(p,encoding='utf-8'))
except Exception: sys.exit(0)
pr=d.get('projects',{})
for k in (cwd,nested): pr.pop(k,None)
tmp=p+'.tmp'
with open(tmp,'w',encoding='utf-8') as f: json.dump(d,f,indent=2); f.flush(); os.fsync(f.fileno())
os.replace(tmp,p)
PY
  rm -rf "$RIG" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

echo "== WAKE-SURVIVAL INT  probe=$PROBE  spt=$ver  $(ts) =="
spt adapter list --json 2>/dev/null | python -X utf8 -c "import json,sys
try: a=[x for x in json.load(sys.stdin)['adapters'] if x['name']=='claude-spt'][0]; print('   adapter claude-spt', a['version'])
except Exception: print('   adapter version unknown')" 2>/dev/null

# Pre-trust the disposable cwd + the nested psyche dir (untrusted cwd = CC dies at spawn, KH §2.2).
# Copy the trust-relevant keys from an already-trusted project as a template; force hasTrustDialogAccepted.
python -X utf8 - "$CWD" "$NESTED" <<'PY'
import json,os,sys
cwd,nested=sys.argv[1:3]
p=os.path.expanduser('~/.claude.json'); d=json.load(open(p,encoding='utf-8'))
pr=d.setdefault('projects',{})
keys=('allowedTools','mcpServers','enabledMcpjsonServers','disabledMcpjsonServers','hasTrustDialogAccepted','projectOnboardingSeenCount','hasClaudeMdExternalIncludesApproved','hasClaudeMdExternalIncludesWarningShown')
tmpl={}
for v in pr.values():
    if isinstance(v,dict) and v.get('hasTrustDialogAccepted'):
        tmpl={k:v[k] for k in keys if k in v}; break
tmpl['hasTrustDialogAccepted']=True; tmpl.setdefault('allowedTools',[])
for k in (cwd,nested): pr[k]=dict(tmpl)
tmp=p+'.tmp'
with open(tmp,'w',encoding='utf-8') as f: json.dump(d,f,indent=2); f.flush(); os.fsync(f.fileno())
os.replace(tmp,p); print('   pretrusted (atomic tmp+fsync+replace)')
PY

BEFORE=" $(tpids) "
# Routed through sptc_bringup for the 0.54.0 lifecycle rename. NOTE the cwd is now PASSED, not
# implied by a subshell `cd`: on 0.54.0 `start` lands on the endpoint's REMEMBERED project folder
# and never on the folder the caller happens to be standing in, so the old `cd "$CWD"` would
# silently stop carrying and drop this probe somewhere else entirely — a bringup that looks broken.
sptc_bringup "$PROBE" claude-spt "$CWD" "$SUBNET" >/dev/null 2>&1

# Wait (bounded) for the endpoint to come alive; nudge the fresh CC session so it settles + stamps ready.
BOUND=no
for i in $(seq 1 16); do
  if spt daemon status 2>/dev/null | grep "$PROBE" | grep -q "alive=true"; then BOUND=yes; break; fi
  case "$i" in 3|5|7|9) { sleep 2; printf '1'; sleep 1; printf '\r'; sleep 3; } | timeout 10 spt rc "$PROBE" --take >/dev/null 2>&1 ;; esac
  sleep 5
done
[ "$BOUND" = yes ] && ok "endpoint bound (alive)" || { bad "endpoint never went alive in ~90s"; echo "WAKE-SURVIVAL-INT FAIL"; exit 1; }

sleep 2
AFTER="$(tpids)"; MYPID=""
for p in $AFTER; do case "$BEFORE" in *" $p "*) : ;; *) MYPID=$p ;; esac; done
[ -n "$MYPID" ] && ok "translate binary spawned (pid=$MYPID)" || bad "no fresh translate pid identified"

# Settle the session (drives CC past its first prompt) so the boundary machinery stamps ready.
{ sleep 2; printf 'standby'; sleep 1; printf '\r'; sleep 3; } | timeout 9 spt rc "$PROBE" >/dev/null 2>&1
RDY=no
for i in $(seq 1 30); do [ -f "$OWLERY/$PROBE/ready" ] && { RDY=yes; break; }; sleep 2; done
[ "$RDY" = yes ] && ok "ready stamped (C-2 leg present)" || bad "ready NEVER stamped in 60s (C-2 restrand missing?)"

sid0=$(sid_of)
[ -n "$sid0" ] && ok "pre-clear session id resolved" || bad "no pre-clear session id in info.json"

# ARM the checkpoint: envelope stashes the wake + emits CLEAR-ONLY; the clear boundary re-runs SessionStart
# which self-sends wake_fire; translate then emits WAKE-ONLY. The wake directive writes the proof file.
echo "-- ARM checkpoint $(ts) --"
spt send "$PROBE" --from "$PROBE" --force-native --json-payload \
  "{\"wake_arm\":\"v1\",\"directive\":\"Use the Write tool to create the file $PROOF with exactly the content $EXPECT and then stop.\"}" <<< "checkpoint" >/dev/null 2>&1 \
  && ok "checkpoint ARM accepted" || bad "checkpoint ARM send failed"

# The /clear boundary rotates the session id — proof the clear actually took effect.
ROT=no
for i in $(seq 1 45); do
  sid=$(sid_of)
  [ -n "$sid" ] && [ "$sid" != "$sid0" ] && { echo "   boundary: sid rotated t+$((i*2))s $(ts)"; ROT=yes; break; }
  sleep 2
done
[ "$ROT" = yes ] && ok "clear boundary took effect (session id rotated)" || bad "no session rotation in 90s (clear never fired)"

# THE PROOF: the woken session executed the wake directive and wrote the file.
FOUND=no
for i in $(seq 1 60); do
  [ -f "$PROOF" ] && { got=$(tr -d '\r\n' < "$PROOF"); FOUND=yes; break; }
  sleep 3
done
if [ "$FOUND" = yes ] && [ "$got" = "$EXPECT" ]; then
  ok "WAKE EXECUTED end-to-end (proof file = '$got')"
elif [ "$FOUND" = yes ]; then
  bad "wake wrote proof but content mismatch: got='$got' want='$EXPECT'"
else
  bad "wake NEVER executed in 180s (no proof file) — the checkpoint macro did not complete the boundary"
fi

# B6 survival: the translate binary must still be alive after the clear boundary (the every-clear
# zero-record kill would have terminated it). And the perch stays healthy (force-native still delivers).
[ -n "$MYPID" ] && case "$(alive "$MYPID")" in True) ok "translate binary SURVIVED the clear boundary (B6 closure)";; *) bad "translate binary died at/after the boundary (B6 ghost)";; esac
spt send "$PROBE" --from "$PROBE" --force-native <<< '[post]' >/dev/null 2>&1 && ok "perch healthy after wake (force-native delivers)" || bad "perch unhealthy after wake"

echo "== $( [ "$fail" -eq 0 ] && echo DONE || echo FAILED ) $(ts) =="
[ "$fail" -eq 0 ] && { echo "WAKE-SURVIVAL-INT OK"; exit 0; } || { echo "WAKE-SURVIVAL-INT FAIL"; exit 1; }
