← back to Ticket System
TK-11855: read-only Stage 1 post-reboot verifier for pm2 boot path B (+ negative test)
d7dcc35d0f2ecbd0c703b0e139a3fc7c81dc13ce · 2026-09-26 09:07:10 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbhSvxcSSmWHjSbbU37J6j
Files touched
A data/tk-11855-audit/verify-stage1-reboot.sh
Diff
commit d7dcc35d0f2ecbd0c703b0e139a3fc7c81dc13ce
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 09:07:10 2026 -0700
TK-11855: read-only Stage 1 post-reboot verifier for pm2 boot path B (+ negative test)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VbhSvxcSSmWHjSbbU37J6j
---
data/tk-11855-audit/verify-stage1-reboot.sh | 82 +++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/data/tk-11855-audit/verify-stage1-reboot.sh b/data/tk-11855-audit/verify-stage1-reboot.sh
new file mode 100755
index 00000000..d35a6f9d
--- /dev/null
+++ b/data/tk-11855-audit/verify-stage1-reboot.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+# verify-stage1-reboot.sh — TK-11855 Stage 1 gate (READ-ONLY).
+#
+# Steve's ruling 2026-09-26: stage the serializer wiring. Stage 1 = boot-path callers,
+# validated across ONE real reboot, before Stage 2 (the rest) is wired.
+#
+# Stage 1 needed zero plist edits: the only spawn-eligible boot caller is
+# com.steve.pm2-resurrect (already fronted by pm2-boot-serialize.sh); the RunAtLoad
+# canaries never fork a God daemon (they read dump.pm2 or refuse jlist without a live
+# daemon). So the remaining Stage 1 step is OBSERVING a reboot on that path.
+#
+# Verdicts (three states, never two):
+# PASS booted AFTER path B went live, exactly 1 God daemon on PM2_HOME, it came up
+# within MAX_UP_S of boot, resurrect job ran exit 0 this boot, com.PM2 not
+# loaded, managed count within 10% of dump.pm2
+# FAIL any of those measured and wrong (0 or >1 God, resurrect non-zero, twin loaded)
+# NOT_MEASURED no reboot since path B went live, or an input could not be read — never PASS
+#
+# Usage: verify-stage1-reboot.sh (live, read-only)
+# verify-stage1-reboot.sh --test (negative test: injects faults via fixtures)
+set -u
+PM2_HOME="${PM2_HOME:-$HOME/.pm2}"
+PATH_B_LIVE_EPOCH=1790437598 # 2026-09-26T15:46:38Z — com.steve.pm2-resurrect loaded (TK-11854)
+MAX_UP_S=300
+
+measure() {
+ # Each input may be overridden ONLY under --test (TEST=1); the scheduled/live path never reads them.
+ if [ "${TEST:-0}" = 1 ]; then
+ BOOT=$F_BOOT; GODS=$F_GODS; GOD_START=$F_GOD_START; RES_EXIT=$F_RES_EXIT; RES_RUNS=$F_RES_RUNS
+ TWIN=$F_TWIN; MANAGED=$F_MANAGED; DUMPN=$F_DUMPN
+ else
+ BOOT=$(sysctl -n kern.boottime | sed -E 's/^\{ sec = ([0-9]+),.*/\1/')
+ GODLINES=$(ps -axo pid=,lstart=,command= | grep "God Daemon ($PM2_HOME)" | grep -v grep)
+ GODS=$(printf '%s' "$GODLINES" | grep -c . )
+ GOD_START=""
+ if [ "$GODS" = 1 ]; then
+ GOD_START=$(date -j -f "%a %b %d %T %Y" "$(printf '%s' "$GODLINES" | awk '{print $2,$3,$4,$5,$6}')" +%s 2>/dev/null)
+ fi
+ P=$(launchctl print "gui/$(id -u)/com.steve.pm2-resurrect" 2>/dev/null)
+ RES_RUNS=$(printf '%s' "$P" | awk -F'= ' '/runs =/{print $2; exit}')
+ RES_EXIT=$(printf '%s' "$P" | awk -F'= ' '/last exit code =/{print $2; exit}')
+ TWIN=0; launchctl print "gui/$(id -u)/com.PM2" >/dev/null 2>&1 && TWIN=1
+ MANAGED=$(pgrep -P "$(printf '%s' "$GODLINES" | awk 'NR==1{print $1}')" 2>/dev/null | grep -c .)
+ DUMPN=$(/opt/homebrew/bin/node -e 'try{console.log(JSON.parse(require("fs").readFileSync(process.argv[1])).length)}catch{console.log("")}' "$PM2_HOME/dump.pm2")
+ fi
+}
+
+judge() {
+ V=PASS; R=()
+ if [ -z "$BOOT" ]; then echo "NOT_MEASURED|boot time unreadable"; return; fi
+ if [ "$BOOT" -lt "$PATH_B_LIVE_EPOCH" ]; then
+ echo "NOT_MEASURED|no reboot since path B went live ($(date -r "$BOOT" '+%F %T') < $(date -r $PATH_B_LIVE_EPOCH '+%F %T')) — Stage 1 still unvalidated"; return
+ fi
+ [ "$GODS" != 1 ] && { V=FAIL; R+=("God daemons=$GODS (want 1)"); }
+ [ "$TWIN" = 1 ] && { V=FAIL; R+=("com.PM2 twin loaded — duplicate resurrect path"); }
+ if [ -z "$RES_RUNS" ] || [ -z "$RES_EXIT" ]; then [ "$V" = PASS ] && V=NOT_MEASURED; R+=("resurrect job state unreadable")
+ elif [ "$RES_RUNS" -lt 1 ] || [ "$RES_EXIT" != 0 ]; then V=FAIL; R+=("resurrect runs=$RES_RUNS exit=$RES_EXIT"); fi
+ if [ -n "$GOD_START" ] && [ $((GOD_START - BOOT)) -gt $MAX_UP_S ]; then
+ [ "$V" = PASS ] && V=FAIL; R+=("God daemon up $((GOD_START - BOOT))s after boot (> ${MAX_UP_S}s — likely a manual resurrect)"); fi
+ if [ -z "$DUMPN" ] || [ -z "$MANAGED" ]; then [ "$V" = PASS ] && V=NOT_MEASURED; R+=("managed/dump count unreadable")
+ elif [ "$MANAGED" -lt $((DUMPN * 9 / 10)) ]; then V=FAIL; R+=("managed=$MANAGED vs dump=$DUMPN (<90%)"); fi
+ echo "$V|${R[*]:-1 God, resurrect exit 0, no twin, managed=$MANAGED/dump=$DUMPN}"
+}
+
+if [ "${1:-}" = --test ]; then
+ TEST=1; rc=0
+ run() { export F_BOOT=$1 F_GODS=$2 F_GOD_START=$3 F_RES_EXIT=$4 F_RES_RUNS=$5 F_TWIN=$6 F_MANAGED=$7 F_DUMPN=$8
+ measure; out=$(judge); got=${out%%|*}; [ "$got" = "$9" ] && s=ok || { s=WRONG; rc=1; }; echo " [$s] want $9 got $out"; }
+ B=$((PATH_B_LIVE_EPOCH + 3600))
+ run $B 1 $((B+40)) 0 1 0 150 151 PASS # healthy reboot
+ run $((PATH_B_LIVE_EPOCH-60)) 1 0 0 1 0 150 151 NOT_MEASURED # no reboot yet
+ run $B 2 "" 0 1 0 150 151 FAIL # fracture
+ run $B 0 "" 0 1 0 0 151 FAIL # nothing resurrected
+ run $B 1 $((B+40)) 0 1 1 150 151 FAIL # twin loaded
+ run $B 1 $((B+400)) 0 1 0 150 151 FAIL # manual resurrect later
+ run $B 1 $((B+40)) 1 1 0 150 151 FAIL # resurrect exit 1
+ run $B 1 $((B+40)) "" "" 0 150 151 NOT_MEASURED # unreadable job state
+ [ $rc = 0 ] && echo "NEGATIVE TEST: all cases correct" || echo "NEGATIVE TEST: FAILED"
+ exit $rc
+fi
+measure; out=$(judge); echo "TK-11855 stage1: ${out%%|*} — ${out#*|}"
+case ${out%%|*} in PASS) exit 0;; NOT_MEASURED) exit 3;; *) exit 1;; esac
← 547d69dc board: spinning working-orb on blocked tickets with live age
·
back to Ticket System
·
TK-11861: PR sell-mode segmentation evidence (read-only, 13, c5597b86 →