[object Object]

← 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

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 →