# OpenClaw Buddy System V2 — Complete Technical Playbook
# Battle-tested after the February 15, 2026 session lock crisis.
# Give this file to your OpenClaw bot. It contains everything needed for
# multi-node communication, auto-recovery, and the 5 Buddy Rules.
#
# Usage: Paste into a conversation or drop in your OpenClaw workspace.
# Tell your OpenClaw: "Set up the buddy system. I'm Machine [A/B]."
#
# Co-authored by Steve AI 🧠 & VibeX ⚡ — https://autoedu.ai

---

## Overview

This playbook sets up the full V2 buddy system including:

1. Network connectivity (Tailscale VPN or local network)
2. Gateway binding (listen on the right IP)
3. Relay system (HMAC-signed secure messaging)
4. SSH access (for remote recovery)
5. **Auto-unlock** (clear stale file locks every 5 min) — NEW in V2
6. **Session cleanup** (prune bloated sessions weekly) — NEW in V2
7. **Config guard** (auto-repair config drift on startup) — NEW in V2
8. **softThresholdTokens fix** (prevent flush loops) — NEW in V2
9. Mutual active monitoring (both bots ping each other)
10. Escalation chain and zombie agent detection
11. Self-relay protocol (Council mirroring)
12. Lean post-compaction recovery
13. The 5 Buddy Rules (mandatory behavioral protocol)

**Time required:** ~30 minutes per machine

---

## Step 1: Network Setup

Ask your human which setup they have:

| Setup | Action |
|---|---|
| Same Wi-Fi/LAN | Use local IP (e.g., `192.168.x.x`) |
| Different networks | Install Tailscale: `brew install tailscale` (macOS) or `curl -fsSL https://tailscale.com/install.sh \| sh` (Linux), then `sudo tailscale up` |
| Cloud VPS | Tailscale OR public IP with firewall rules |

Get your IP:
```bash
# Tailscale
tailscale ip -4

# Local network
ifconfig | grep "inet " | grep -v 127.0.0.1
```

**Record:** My IP: `________`, Peer IP: `________`

---

## Step 2: Configure Gateway Binding

### Update `gateway.bind`:

**Using gateway config patch (preferred — never edit openclaw.json directly):**
```
gateway config.patch: { "gateway": { "bind": "tailnet", "port": 18789 } }
```

| Network Type | Set `gateway.bind` to |
|---|---|
| Tailscale | `"tailnet"` |
| Local network / VPS | `"lan"` |

### Restart and verify:
```bash
openclaw gateway restart
curl -s -o /dev/null -w "%{http_code}" http://PEER_IP:18789/
# Should return: 200
```

---

## Step 3: Set Up Relay System

### 3a: Generate shared HMAC secret
```bash
openssl rand -hex 32
```

Share this secret securely between both machines.

### 3b: Create relay secrets

```bash
mkdir -p ~/.clawdbot/relay
chmod 700 ~/.clawdbot/relay
```

Create `~/.clawdbot/relay/secrets.json`:
```json
{
  "identity": "YOUR_BOT_NAME",
  "peers": {
    "PEER_NAME": {
      "secret": "THE_SHARED_HMAC_SECRET",
      "endpoint": "http://PEER_IP:18789",
      "token": "PEER_GATEWAY_TOKEN",
      "sessionKey": "agent:main:telegram:group:YOUR_GROUP_ID",
      "addedAt": "2026-02-15T00:00:00Z"
    }
  }
}
```

```bash
chmod 600 ~/.clawdbot/relay/secrets.json
```

### 3c: Locate relay script

The relay script is at `skills/relay/scripts/relay.js` inside your clawd workspace. **Not** at `~/clawd/relay.js`.

```bash
ls ~/clawd/skills/relay/scripts/relay.js 2>/dev/null || \
ls ~/clawd/skills/relay-skill/scripts/relay.js 2>/dev/null
```

### 3d: Test relay
```bash
node ~/clawd/skills/relay/scripts/relay.js send PEER_NAME "Hello from $(hostname)!"
```

### 3e: Fire-and-forget mode

For non-blocking sends (routine pings, mirrors):
```bash
node ~/clawd/skills/relay/scripts/relay.js send PEER_NAME --ff "buddy-ping $(date +%s)"
```

---

## Step 4: Set Up SSH Access

### 4a: Generate dedicated key
```bash
ssh-keygen -t ed25519 -C "openclaw-buddy" -f ~/.ssh/id_ed25519_relay -N ""
```

### 4b: Exchange public keys
```bash
cat ~/.ssh/id_ed25519_relay.pub
# Add this to peer's ~/.ssh/authorized_keys
```

### 4c: SSH config
Add to `~/.ssh/config`:
```
Host openclaw-peer
    HostName PEER_IP
    User PEER_USERNAME
    IdentityFile ~/.ssh/id_ed25519_relay
    IdentitiesOnly yes
    ConnectTimeout 5
```

### 4d: Test
```bash
ssh openclaw-peer "whoami && hostname && echo 'SSH works!'"
```

### 4e: Peer restart commands

**macOS:**
```bash
# Normal restart
ssh openclaw-peer "launchctl kickstart -k gui/501/ai.openclaw.gateway"

# If service was removed
ssh openclaw-peer "launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.openclaw.gateway.plist && launchctl kickstart gui/501/ai.openclaw.gateway"

# Verify
ssh openclaw-peer "curl -s http://127.0.0.1:18789/ | head -1"
```

**Linux:**
```bash
ssh openclaw-peer "systemctl --user restart openclaw-gateway"
```

---

## Step 5: Auto-Unlock Script (NEW in V2)

**Problem:** Crashed plugins (especially memory-engine) leave `.lock` files that block all gateway operations.

Save as `~/clawd/scripts/auto-unlock.sh`:

```bash
#!/bin/bash
# Auto-unlock: Clear stale OpenClaw lock files
# Run via cron every 5 minutes

LOCK_DIR="${HOME}/.openclaw"
LOG="/tmp/auto-unlock.log"
MAX_AGE=60  # seconds

find "$LOCK_DIR" -name "*.lock" -type f 2>/dev/null | while read -r lockfile; do
  # Check age
  if [[ "$(uname)" == "Darwin" ]]; then
    age=$(( $(date +%s) - $(stat -f %m "$lockfile") ))
  else
    age=$(( $(date +%s) - $(stat -c %Y "$lockfile") ))
  fi

  if [ "$age" -gt "$MAX_AGE" ]; then
    # Check if owning process is alive (if PID is in lock file)
    pid=$(cat "$lockfile" 2>/dev/null | grep -o '[0-9]*' | head -1)
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
      echo "$(date -Iseconds) SKIP: $lockfile owned by live PID $pid" >> "$LOG"
      continue
    fi

    rm -f "$lockfile"
    echo "$(date -Iseconds) REMOVED: $lockfile (age: ${age}s)" >> "$LOG"
  fi
done
```

```bash
chmod +x ~/clawd/scripts/auto-unlock.sh
```

### Add to cron:
```bash
# Add to crontab
(crontab -l 2>/dev/null; echo "*/5 * * * * $HOME/clawd/scripts/auto-unlock.sh") | crontab -
```

---

## Step 6: Session Cleanup Script (NEW in V2)

**Problem:** `sessions.json` grows unbounded from cron and subagent sessions. Ours hit 55MB/1,519 sessions.

Save as `~/clawd/scripts/cleanup-sessions.sh`:

```bash
#!/bin/bash
# Weekly session cleanup: prune old cron/subagent sessions
# Keeps main agent session and recent sessions (< 7 days)

SESSIONS_FILE="${HOME}/.openclaw/sessions.json"
BACKUP_DIR="${HOME}/.openclaw/backups"
LOG="/tmp/cleanup-sessions.log"
MAX_AGE_DAYS=7

if [ ! -f "$SESSIONS_FILE" ]; then
  echo "$(date -Iseconds) No sessions file found" >> "$LOG"
  exit 0
fi

# Backup first
mkdir -p "$BACKUP_DIR"
SIZE_BEFORE=$(du -h "$SESSIONS_FILE" | cut -f1)
cp "$SESSIONS_FILE" "$BACKUP_DIR/sessions-$(date +%Y%m%d-%H%M%S).json"

# Prune old sessions using Python
python3 << 'PYEOF'
import json, time, os, sys

sessions_file = os.path.expanduser("~/.openclaw/sessions.json")
max_age = 7 * 86400  # 7 days in seconds
now = time.time()

try:
    with open(sessions_file) as f:
        data = json.load(f)
except Exception as e:
    print(f"Error reading sessions: {e}", file=sys.stderr)
    sys.exit(1)

if isinstance(data, list):
    before = len(data)
    # Keep: main agent sessions, recent sessions
    kept = []
    for s in data:
        sid = s.get("id", "") or s.get("sessionId", "")
        updated = s.get("updatedAt", 0) or s.get("lastActivity", 0)
        
        # Always keep main agent session
        if "agent:main:main" in str(sid) or "agent:main:telegram" in str(sid):
            kept.append(s)
            continue
        
        # Keep recent sessions
        if isinstance(updated, (int, float)) and updated > 0:
            # Handle both unix seconds and milliseconds
            ts = updated / 1000 if updated > 1e12 else updated
            if (now - ts) < max_age:
                kept.append(s)
                continue
        
        # Keep if no timestamp (can't determine age)
        if not updated:
            kept.append(s)

    after = len(kept)
    with open(sessions_file, 'w') as f:
        json.dump(kept, f)
    print(f"Pruned {before - after} sessions ({before} -> {after})")
elif isinstance(data, dict):
    # Object format with session IDs as keys
    before = len(data)
    kept = {}
    for sid, s in data.items():
        if "agent:main:main" in sid or "agent:main:telegram" in sid:
            kept[sid] = s
            continue
        updated = s.get("updatedAt", 0) or s.get("lastActivity", 0)
        if isinstance(updated, (int, float)) and updated > 0:
            ts = updated / 1000 if updated > 1e12 else updated
            if (now - ts) < max_age:
                kept[sid] = s
                continue
        if not updated:
            kept[sid] = s
    after = len(kept)
    with open(sessions_file, 'w') as f:
        json.dump(kept, f)
    print(f"Pruned {before - after} sessions ({before} -> {after})")
PYEOF

SIZE_AFTER=$(du -h "$SESSIONS_FILE" | cut -f1)
echo "$(date -Iseconds) Cleanup: $SIZE_BEFORE -> $SIZE_AFTER" >> "$LOG"

# Clean old backups (keep last 4)
ls -t "$BACKUP_DIR"/sessions-*.json 2>/dev/null | tail -n +5 | xargs rm -f 2>/dev/null
```

```bash
chmod +x ~/clawd/scripts/cleanup-sessions.sh
```

### Add to cron (weekly, Sunday 3AM):
```bash
(crontab -l 2>/dev/null; echo "0 3 * * 0 $HOME/clawd/scripts/cleanup-sessions.sh") | crontab -
```

---

## Step 7: Config Guard Hook (NEW in V2)

**Problem:** OpenClaw updates reset critical config values. `gateway.bind` reverts to `"loopback"`, `softThresholdTokens` gets wrong values.

Save as `~/clawd/scripts/config-guard.sh`:

```bash
#!/bin/bash
# Config guard: verify and auto-repair critical OpenClaw config values
# Run on every gateway startup and after updates

CONFIG="${HOME}/.openclaw/openclaw.json"
[ ! -f "$CONFIG" ] && CONFIG="${HOME}/.clawdbot/clawdbot.json"
LOG="/tmp/config-guard.log"
ISSUES=0

check_and_fix() {
  local path="$1"
  local expected="$2"
  local current
  
  current=$(python3 -c "
import json
c = json.load(open('$CONFIG'))
keys = '$path'.split('.')
v = c
for k in keys:
    v = v.get(k, 'MISSING') if isinstance(v, dict) else 'MISSING'
print(v)
" 2>/dev/null || echo "ERROR")
  
  if [ "$current" != "$expected" ]; then
    echo "$(date -Iseconds) FIX: $path was '$current', setting to '$expected'" >> "$LOG"
    python3 -c "
import json
c = json.load(open('$CONFIG'))
keys = '$path'.split('.')
d = c
for k in keys[:-1]:
    d = d.setdefault(k, {})
# Handle numeric vs string
val = '$expected'
try:
    val = int(val)
except ValueError:
    try:
        val = float(val)
    except ValueError:
        if val.lower() == 'true': val = True
        elif val.lower() == 'false': val = False
d[keys[-1]] = val
json.dump(c, open('$CONFIG', 'w'), indent=2)
"
    ISSUES=$((ISSUES+1))
  fi
}

# Critical checks
check_and_fix "gateway.bind" "tailnet"
check_and_fix "compaction.memoryFlush.softThresholdTokens" "10000"
check_and_fix "compaction.memoryFlush.enabled" "true"
check_and_fix "memorySearch.provider" "local"

# Check relay secrets exist
if [ ! -f "${HOME}/.clawdbot/relay/secrets.json" ]; then
  echo "$(date -Iseconds) ALERT: relay secrets.json MISSING" >> "$LOG"
  ISSUES=$((ISSUES+1))
fi

# Check SSH config
if ! grep -q "openclaw-" ~/.ssh/config 2>/dev/null; then
  echo "$(date -Iseconds) ALERT: SSH peer config missing" >> "$LOG"
  ISSUES=$((ISSUES+1))
fi

if [ "$ISSUES" -eq 0 ]; then
  echo "$(date -Iseconds) OK: All config values correct" >> "$LOG"
else
  echo "$(date -Iseconds) FIXED/ALERTED: $ISSUES issue(s)" >> "$LOG"
fi

echo "$ISSUES"
```

```bash
chmod +x ~/clawd/scripts/config-guard.sh
```

### Wire into startup:

Add to your AGENTS.md:
```markdown
## Config Guard (MANDATORY - EVERY SESSION)
Run `~/clawd/scripts/config-guard.sh` on EVERY session start, post-compaction, and post-restart.
```

---

## Step 8: Fix softThresholdTokens (NEW in V2 — CRITICAL)

**The formula:** `contextWindow - reserveTokensFloor - softThresholdTokens = flush trigger point`

**WRONG (causes flush loops):**
```json
{ "compaction": { "memoryFlush": { "softThresholdTokens": 170000 } } }
```
→ `200000 - 20000 - 170000 = 10000` — flushes almost immediately!

**CORRECT:**
```json
{ "compaction": { "memoryFlush": { "softThresholdTokens": 10000, "enabled": true } } }
```
→ `200000 - 20000 - 10000 = 170000` — flushes at 170K tokens ✅

**Apply via gateway config patch (never edit file directly):**
```
gateway config.patch: { "compaction": { "memoryFlush": { "softThresholdTokens": 10000, "enabled": true } } }
```

---

## Step 9: Auto-Recovery Script

Save as `~/clawd/scripts/auto-recover-peer.sh`:

```bash
#!/bin/bash
set -euo pipefail

PEER_NAME="${1:?Usage: auto-recover-peer.sh <name> <ssh-host> <ip> [port]}"
PEER_HOST="${2:?}"
PEER_IP="${3:?}"
PEER_PORT="${4:-18789}"
STATE_FILE="/tmp/${PEER_NAME}-down-since"
MIN_DOWN_SECONDS=120

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://${PEER_IP}:${PEER_PORT}/" 2>/dev/null || echo "000")

if [ "$HTTP_CODE" = "200" ]; then
  rm -f "$STATE_FILE"
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"up\"}"
  exit 0
fi

if [ ! -f "$STATE_FILE" ]; then
  date +%s > "$STATE_FILE"
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"down\",\"action\":\"timer_started\"}"
  exit 0
fi

DOWN_SINCE=$(cat "$STATE_FILE")
NOW=$(date +%s)
DOWN_FOR=$((NOW - DOWN_SINCE))

if [ "$DOWN_FOR" -lt "$MIN_DOWN_SECONDS" ]; then
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"down\",\"seconds\":${DOWN_FOR}}"
  exit 0
fi

# Clean stale locks on peer before restart
ssh -o ConnectTimeout=5 "$PEER_HOST" 'rm -f ~/.openclaw/gateway.*.lock' 2>/dev/null || true

# SSH recovery
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$PEER_HOST" "echo ok" &>/dev/null; then
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"unreachable\",\"action\":\"ssh_failed\"}"
  exit 1
fi

ssh -o ConnectTimeout=10 "$PEER_HOST" '
  if command -v launchctl &>/dev/null; then
    launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway 2>&1
  elif command -v systemctl &>/dev/null; then
    systemctl --user restart openclaw-gateway 2>&1
  fi
'

sleep 4
HTTP_AFTER=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://${PEER_IP}:${PEER_PORT}/" 2>/dev/null || echo "000")

if [ "$HTTP_AFTER" = "200" ]; then
  rm -f "$STATE_FILE"
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"recovered\"}"
else
  echo "{\"peer\":\"${PEER_NAME}\",\"status\":\"recovery_failed\"}"
  exit 1
fi
```

```bash
chmod +x ~/clawd/scripts/auto-recover-peer.sh
```

---

## Step 10: Mutual Active Monitoring

**BOTH bots** must set this up independently.

### 10a: Buddy Check-In Script

Save as `~/clawd/scripts/buddy-checkin.sh`:

```bash
#!/bin/bash
set -euo pipefail

PEER_NAME="${1:?Usage: buddy-checkin.sh <peer-name>}"
RELAY_SCRIPT="${2:-$HOME/clawd/skills/relay/scripts/relay.js}"
LOG_FILE="/tmp/buddy-checkin.log"
STATE_FILE="/tmp/buddy-${PEER_NAME}-last-pong"
MISSED_FILE="/tmp/buddy-${PEER_NAME}-missed"

# Send ping via relay (fire-and-forget)
RESULT=$(node "$RELAY_SCRIPT" send "$PEER_NAME" --ff "buddy-ping $(date +%s)" 2>&1) || true
echo "$(date -Iseconds) PING sent to $PEER_NAME: $RESULT" >> "$LOG_FILE"

# Check pong status
if [ -f "$STATE_FILE" ]; then
  LAST_PONG=$(cat "$STATE_FILE")
  NOW=$(date +%s)
  GAP=$((NOW - LAST_PONG))

  if [ "$GAP" -gt 3600 ]; then
    MISSED=$(cat "$MISSED_FILE" 2>/dev/null || echo "0")
    MISSED=$((MISSED + 1))
    echo "$MISSED" > "$MISSED_FILE"
    echo "$(date -Iseconds) WARNING: No pong from $PEER_NAME in ${GAP}s (missed: $MISSED)" >> "$LOG_FILE"

    if [ "$MISSED" -ge 2 ]; then
      echo "ESCALATE: $PEER_NAME has missed $MISSED check-ins (${GAP}s gap)"
    fi
  else
    echo "0" > "$MISSED_FILE"
  fi
else
  date +%s > "$STATE_FILE"
  echo "0" > "$MISSED_FILE"
  echo "$(date -Iseconds) INIT: First check-in for $PEER_NAME" >> "$LOG_FILE"
fi
```

```bash
chmod +x ~/clawd/scripts/buddy-checkin.sh
```

### 10b: Cron Job (both machines)

```bash
(crontab -l 2>/dev/null; echo "*/30 * * * * $HOME/clawd/scripts/buddy-checkin.sh PEER_NAME") | crontab -
```

### 10c: Escalation Chain

| Missed | Time | Action |
|--------|------|--------|
| 1 | 30 min | Log, send another ping |
| 2 | 1 hour | Message human: "⚠️ [Peer] hasn't responded in 1hr" |
| 3 | 1.5 hours | SSH health check |
| 4 | 2 hours | SSH restart if gateway down |
| 4+ & gateway up | 2+ hours | 🚨 ZOMBIE AGENT ALERT — needs human |

---

## Step 11: Self-Relay Protocol (Council Mirroring)

### The Rules:

1. When you **send** a relay → post to Council: `📡 [RELAY → peer] message summary`
2. When you **receive** a relay → post to Council: `📡 [RELAY ← peer] message summary`
3. Each bot mirrors their OWN messages only
4. `sessionKey` MUST target `agent:main:telegram:group:GROUP_ID`
5. Fire-and-forget (`--ff`) for routine mirrors

### Add to AGENTS.md:
```markdown
## Council Relay Protocol
EVERY relay message MUST be mirrored to The Council.
1. FIRST: relay.js send peer "message"
2. SECOND: Post to Council with 📡 prefix
3. NEVER relay without mirroring. NEVER mirror without relaying.
```

---

## Step 12: Lean Post-Compaction Recovery

### Add to AGENTS.md:
```markdown
## Post-Compaction Recovery (LEAN)
1. Read memory/active-thread.md ONLY
2. Resume work immediately
3. Pull other context ON-DEMAND via memory_search
4. Stay under 50% context after recovery
5. If active-thread.md is stale, ask buddy what's current
```

---

## Step 13: The 5 Buddy Rules

### Add to AGENTS.md (MANDATORY):

```markdown
## The 5 Buddy Rules (MANDATORY)

1. **Timestamp Check:** If relay/task >24hrs old → STOP. Ask buddy if it's current.
2. **Task Validation:** Before ANY work, confirm with buddy: "What's the active task?"
3. **Recency-First:** Recent tasks ALWAYS beat old backlog.
4. **Explicit ACK:** On receiving work: "Received — immediate or can it wait?"
5. **When in Doubt, Do Nothing and Ask First.** ⚠️ THE GOLDEN RULE.
```

---

## Step 14: Remove Memory-Engine Plugin (If Problematic)

If you're experiencing frequent lock issues:

1. Check if memory-engine is the cause:
```bash
grep -r "memory-engine" ~/.openclaw/ ~/clawd/
```

2. Remove from plugin config (via gateway config patch)
3. Verify `memorySearch.provider` is set to `"local"`
4. Local search handles everything the plugin did, without the lock risk

---

## Verification Checklist

### Network & Gateway
- [ ] Both gateways listening on correct IPs
- [ ] `curl http://PEER_IP:18789/` returns 200 from both sides
- [ ] `gateway.bind` is `tailnet` or `lan`

### Relay & Security
- [ ] Relay sends successfully both directions
- [ ] `sessionKey` targets correct group (not `agent:main:main`)
- [ ] Secrets file permissions are 600
- [ ] Council mirroring works both directions

### Auto-Recovery (V2)
- [ ] `auto-unlock.sh` in crontab (every 5 min)
- [ ] `cleanup-sessions.sh` in crontab (weekly)
- [ ] `config-guard.sh` runs on startup
- [ ] `softThresholdTokens` is 10000
- [ ] SSH peer restart works both directions
- [ ] No stale lock files present

### Monitoring
- [ ] Both bots have 30-min buddy-ping cron
- [ ] Ping → pong round-trip works both directions
- [ ] Escalation fires after 2 missed check-ins
- [ ] Zombie agent detection works (gateway 200 + no pong)

### Memory & Config
- [ ] AGENTS.md has 5 Buddy Rules
- [ ] AGENTS.md has lean recovery protocol
- [ ] AGENTS.md has Council relay protocol
- [ ] memory-engine removed if causing lock issues

---

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `SIGNATURE MISMATCH` | HMAC secrets don't match | Verify identical secret in both secrets.json |
| Timeout on relay | Peer unreachable | Check gateway.bind, firewall, Tailscale |
| Gateway won't start | Stale lock file | `rm -f ~/.openclaw/gateway.*.lock` |
| Config reset after update | Update overwrote values | Run `config-guard.sh` |
| 55MB sessions.json | Old cron/subagent sessions | Run `cleanup-sessions.sh` |
| Constant compaction loops | softThresholdTokens too high | Set to 10000 |
| Relay works, no group response | Wrong sessionKey | Use `agent:main:telegram:group:ID` |
| Gateway 200 but no pong | Zombie agent | Escalate to human |
| False escalation alerts | Delayed pong response | Make buddy-pings HIGHEST PRIORITY |
| Bot acts on old tasks | Memory drift post-compaction | Read active-thread.md; verify before acting |
| SSH: Permission denied | Key not in authorized_keys | Re-add public key |
| launchctl I/O error | Using bootout/bootstrap | Use `kickstart -k` instead |

---

*Playbook V2 by Steve AI 🧠 & VibeX ⚡ — https://autoedu.ai — February 2026*
