app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
harness: T5 derives the expected thread slug from ADOM_AI_THREAD (a hardcoded name produced a false 'stamp path broken'…
781d1b2
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
#!/usr/bin/env bash
# mode-ralph.sh — the taskbar-state switching ralph (John, 2026-08-16: "loop forever until you have
# it fixed. figure out how to take screenshots to fix yourself and analyze").
#
# Drives taskbarMode/overlayBadges through the SAME /dashboard/action endpoint the Settings dialog
# uses (via pup_eval on the cn-dash window), waits for the apply-progress tracker to report done,
# then verifies the GLASS with UIA button names and captures the tray. Includes RAPID double-click
# rounds that reproduce the in-flight-stamp race (the 2.0.216 generation-token fix's regression
# test). Exit code = number of failed rounds.
#
# Usage: tools/mode-ralph.sh [TARGET] [FINAL_MODE] [FINAL_OV]
set -u
TARGET="${1:-AdomLapper}"
FINAL_MODE="${2:-plain}"
FINAL_OV="${3:-on}"
export ADOM_AI_THREAD="${ADOM_AI_THREAD:-pup bridge maintenance}"
export ADOM_CONTAINER_NAME="${ADOM_CONTAINER_NAME:-galliaApril}"
CLI="adom-bridge --target $TARGET"
PASS=0; FAIL=0
SHOTDIR="${SHOTDIR:-/tmp/mode-ralph-shots}"; mkdir -p "$SHOTDIR"
# v2: the expected thread-tile names come from the LIVE sessions, never a hardcoded list. A stale
# list made every tiles round fail with "only 1 thread-name buttons" while the bar was perfect.
OWNERS=$(timeout 40 $CLI pup_status '{}' 2>&1 | python3 -c "
import sys,json
raw=sys.stdin.read();i=raw.find('{')
d=json.loads(raw[i:]) if i>=0 else {}
seen=[]
for s in d.get('sessions',[]):
o=(s.get('owner') or '').strip()
if o and o not in seen: seen.append(o)
print('|'.join(seen))" 2>/dev/null)
[ -z "$OWNERS" ] && OWNERS="pup bridge maintenance"
echo "live threads expected as tiles: $OWNERS"
export OWNERS
evaljs() { # evaljs <expression> -> prints result
timeout 60 $CLI pup_eval "{\"sessionId\":\"cn-dash\",\"expression\":$(python3 -c 'import json,sys;print(json.dumps(sys.argv[1]))' "$1")}" 2>&1 \
| python3 -c "import sys,json;raw=sys.stdin.read();i=raw.find('{');print(json.loads(raw[i:]).get('result',''))" 2>/dev/null
}
switch_state() { # switch_state <mode> <ov on|off>
evaljs "(fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'set-setting',key:'taskbarMode',value:'$1'})}).then(()=>fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'set-setting',key:'overlayBadges',value:'$2'})})), 'sent')" >/dev/null
}
wait_done() { # poll apply-progress until done (max ~40s)
for i in $(seq 1 40); do
D=$(evaljs "fetch('/dashboard/apply-progress').then(r=>r.json()).then(j=>String(j&&j.apply&&j.apply.done))")
[ "$D" = "true" ] && return 0
sleep 1
done
return 1
}
uia_buttons() { # $1 = rect file basename (default rects.txt); prints name lines; writes x|y|w|h|name rows
local PS='Add-Type -AssemblyName UIAutomationClient; $tray=[System.Windows.Automation.AutomationElement]::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Children,(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty,"Shell_TrayWnd"))); $btns=$tray.FindAll([System.Windows.Automation.TreeScope]::Descendants,(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty,[System.Windows.Automation.ControlType]::Button))); foreach($b in $btns){$r=$b.Current.BoundingRectangle; Write-Output ("{0}|{1}|{2}|{3}|{4}" -f [int]$r.X,[int]$r.Y,[int]$r.Width,[int]$r.Height,$b.Current.Name)}'
local B64; B64=$(python3 -c "import base64,sys;print(base64.b64encode(sys.stdin.read().encode('utf-16-le')).decode())" <<< "$PS")
timeout 90 $CLI shell_execute "{\"command\":\"powershell -NoProfile -EncodedCommand $B64\",\"reason\":\"mode-ralph: UIA taskbar button names + rects\"}" 2>&1 \
| python3 -c "
import sys,json
raw=sys.stdin.read();i=raw.find('{');d=json.loads(raw[i:]) if i>=0 else {}
out=d.get('stdout') or ''
import os
open(os.path.join('$SHOTDIR', '${1:-rects.txt}'),'w').write(out)
for l in out.splitlines():
p=l.split('|',4)
print(p[4] if len(p)==5 else l)"
}
pixel_gate() { # pixel_gate <mode> <label> — crop the button under test by its UIA rect, red-box the
# tray shot, and CLASSIFY the art: plain must show NO pup teal on the CfT group button; tiles and
# aumid must show pup teal on a pup button. Names are not pixels (John's red-box rule).
local mode="$1" label="$2"
python3 - "$mode" "$label" <<'PYEOF'
import sys, re
from PIL import Image, ImageDraw
mode, label = sys.argv[1], sys.argv[2]
import os
SHOTDIR = os.environ.get("SHOTDIR","/tmp/mode-ralph-shots")
def load(f):
return [l.split('|',4) for l in open(f).read().splitlines() if l.count('|')>=4]
def pick(rows):
if mode=='plain': return [r for r in rows if 'Chrome for Testing' in r[4]]
owners=[o for o in (os.environ.get('OWNERS') or '').split('|') if o]
return [r for r in rows if r[4].startswith('Adom Pup') or any(r[4].startswith(o) for o in owners)]
rows=load(f'{SHOTDIR}/rects.txt')
tgt=pick(rows)
if not tgt: print('PIXELGATE FAIL: no target button found'); sys.exit(1)
# the taskbar REFLOWS for seconds after a mode switch (buttons materialize as stamps land) — a rect
# captured before the screenshot can point at a NEIGHBOR button by capture time (the RustDesk-swirl
# incident). rects2.txt is read AFTER the shot; if the target moved, the bar is still settling.
try:
tgt2=pick(load(f'{SHOTDIR}/rects2.txt'))
if not tgt2 or tgt2[0][4]!=tgt[0][4] or abs(int(tgt2[0][0])-int(tgt[0][0]))>4:
print('PIXELGATE UNSTABLE: taskbar reflowed between rect read and screenshot'); sys.exit(2)
tgt=tgt2
except FileNotFoundError:
pass
# judge the MODE, not one unlucky button: a single straggler (the #53 hwnd-flaky class) can lag
# for tens of seconds while every sibling wears correct art — sample up to 4 targets, keep the best.
cands = tgt[:4] if mode!='plain' else tgt[:1]
import os.path
if not os.path.exists(f'{SHOTDIR}/{label}.png'):
print('PIXELGATE FAIL: no fresh tray capture'); sys.exit(1)
im=Image.open(f'{SHOTDIR}/{label}.png')
import json as _json
def _map(x,w):
try:
# imageToScreen describes the SAFE image, not the full.png we open — map via screenRect
# against THIS image's width (the 3-button-drift bug caught in the six-state evidence walk)
cm=_json.load(open(f'{SHOTDIR}/{label}.json')).get('coordMap') or dict()
sr=cm.get('screenRect') or dict()
srw=float(sr.get('w') or 0); srx=float(sr.get('x') or 0)
if not srw: raise ValueError('no screenRect')
sc=im.width/srw
a,b=int((x-srx)*sc),int((x+w-srx)*sc)
except Exception:
sc=im.width/2560.0; a,b=int(x*sc),int((x+w)*sc)
a=max(0,min(im.width-2,a)); b=max(a+2,min(im.width,b))
return a,b
best=None
for t in cands:
x,y,w,h=(int(t[i]) for i in range(4))
cx0,cx1=_map(x,w)
crop=im.crop((cx0,0,cx1,im.height)).convert('RGB')
W,H=crop.size
px=crop.load(); teal=0; total=0
for yy in range(H):
for xx in range(W):
if xx > W*0.45 and yy < H*0.62: continue
r,g,b=px[xx,yy][:3]; total+=1
# pup teal is #00b8b1: near-zero red, green~blue. CfT's own BLUE logo has cyan pixels
# that a loose rule counted as teal (the 0.056-forever plain fail) — keep it tight.
if g>120 and b>110 and r<70 and abs(int(g)-int(b))<45: teal+=1
frac=teal/max(1,total)
take = (best is None) or (frac > best[0] if mode != 'plain' else frac < best[0])
if take:
best=(frac,t,cx0,cx1)
frac,t,cx0,cx1=best
ann=im.convert('RGB').copy(); d=ImageDraw.Draw(ann)
d.rectangle([cx0-2,0,cx1+2,im.height-1],outline=(255,0,0),width=2)
ann.resize((ann.width*2,ann.height*2),Image.LANCZOS).save(f'{SHOTDIR}/{label}-redbox.png')
im.crop((cx0,0,cx1,im.height)).resize(((cx1-cx0)*10,im.height*10),Image.LANCZOS).save(f'{SHOTDIR}/{label}-btn10x.png')
ok = (frac < 0.02) if mode=='plain' else (frac > 0.04)
print(f"PIXELGATE {'PASS' if ok else 'FAIL'}: teal fraction {frac:.3f} on '{t[4][:50]}' ({'must be ~0 in plain' if mode=='plain' else 'best of ' + str(len(cands)) + ' pup buttons'})")
sys.exit(0 if ok else 1)
PYEOF
}
shot_tray() { # shot_tray <label> — MUST produce a FRESH capture; a stale file poisons every gate
local HW; HW=$(cat /tmp/tray_hwnd 2>/dev/null || echo 0)
if [ "$HW" = "0" ] || [ -z "$HW" ]; then HW=$(resolve_tray_hwnd); fi
timeout 60 $CLI desktop_screenshot_window "{\"hwnd\":$HW}" >/dev/null 2>&1
local P; P=$(find ~/project/screenshots/adom-bridge/window -name "*-${HW}.full.png" -newermt "$(date -u -d '-45 seconds' '+%Y-%m-%dT%H:%M:%S')" 2>/dev/null | head -1)
if [ -z "$P" ]; then
# capture failed or hwnd stale (Explorer restart) — re-resolve once and retry
HW=$(resolve_tray_hwnd); echo "$HW" > /tmp/tray_hwnd
timeout 60 $CLI desktop_screenshot_window "{\"hwnd\":$HW}" >/dev/null 2>&1
P=$(find ~/project/screenshots/adom-bridge/window -name "*-${HW}.full.png" -newermt "$(date -u -d '-45 seconds' '+%Y-%m-%dT%H:%M:%S')" 2>/dev/null | head -1)
fi
if [ -z "$P" ]; then echo " SHOT_TRAY FAILED: no fresh capture (hwnd $HW) — pixel gate will be skipped as FAIL" >&2; rm -f "$SHOTDIR/$1.png"; return 1; fi
cp "$P" "$SHOTDIR/$1.png"
}
resolve_tray_hwnd() {
local PS='Add-Type -AssemblyName UIAutomationClient; $tray=[System.Windows.Automation.AutomationElement]::RootElement.FindFirst([System.Windows.Automation.TreeScope]::Children,(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty,"Shell_TrayWnd"))); $tray.Current.NativeWindowHandle'
local B64; B64=$(python3 -c "import base64,sys;print(base64.b64encode(sys.stdin.read().encode('utf-16-le')).decode())" <<< "$PS")
timeout 60 $CLI shell_execute "{\"command\":\"powershell -NoProfile -EncodedCommand $B64\",\"reason\":\"mode-ralph: resolve tray hwnd\"}" 2>&1 \
| python3 -c "import sys,json;raw=sys.stdin.read();i=raw.find('{');d=json.loads(raw[i:]) if i>=0 else {};print((d.get('stdout') or '0').strip())"
}
verify_state() { # verify_state <mode> <label> — retries while the bar is still reflowing
local mode="$1" label="$2"
local B PG PGOK attempt
local N_PUP N_THREAD N_CFT
for attempt in 1 2 3 4 5; do
B=$(uia_buttons rects.txt)
N_PUP=$(echo "$B" | grep -c "Adom Pup" || true)
N_THREAD=$(echo "$B" | python3 -c "
import sys,os
owners=[o for o in (os.environ.get('OWNERS') or '').split('|') if o]
n=sum(1 for l in sys.stdin if any(l.startswith(o) for o in owners))
print(n)")
N_CFT=$(echo "$B" | grep -c "Chrome for Testing" || true)
shot_tray "$label"
uia_buttons rects2.txt >/dev/null
PG=$(SHOTDIR="$SHOTDIR" pixel_gate "$mode" "$label"); PGOK=$?
# names must agree with the mode too — a switch still landing shows the PREVIOUS mode's buttons
NAMEOK=1
case "$mode" in
plain) { [ "$N_PUP" -eq 0 ] && [ "$N_THREAD" -eq 0 ] && [ "$N_CFT" -eq 1 ]; } && NAMEOK=0 ;;
aumid) WANTA=$(python3 -c "import os;o=[x for x in (os.environ.get('OWNERS') or '').split('|') if x];print(max(1,len(o)))")
[ "$N_PUP" -ge "$WANTA" ] && NAMEOK=0 ;;
tiles) WANT=$(python3 -c "import os;o=[x for x in (os.environ.get('OWNERS') or '').split('|') if x];print(max(1,len(o)-1))")
[ "$N_THREAD" -ge "$WANT" ] && NAMEOK=0 ;;
esac
[ $PGOK -eq 0 ] && [ $NAMEOK -eq 0 ] && break
echo " (attempt $attempt: names $( [ $NAMEOK -eq 0 ] && echo ok || echo "still showing the old mode" ), $PG — settling 8s)"
sleep 8
done
echo " $PG"
case "$mode" in
plain)
if [ "$N_PUP" -eq 0 ] && [ "$N_THREAD" -eq 0 ] && [ "$N_CFT" -eq 1 ] && [ $PGOK -eq 0 ]; then PASS=$((PASS+1)); echo " PASS [$label] plain: 0 branded buttons, exactly ONE CfT group + pixel gate"; return 0
else FAIL=$((FAIL+1)); echo " FAIL [$label] plain: $N_PUP 'Adom Pup' + $N_THREAD thread buttons, $N_CFT CfT groups (must be exactly 1)"; echo "$B" | grep -E "Adom Pup|^(ti-crawl|mouser-run|digikey-run|wiki-in|wiki-out|dash-app|reclaimed|pup bridge)" | sed 's/^/ /'; return 1; fi ;;
aumid)
WANTA=$(python3 -c "import os;o=[x for x in (os.environ.get('OWNERS') or '').split('|') if x];print(max(1,len(o)))")
if [ "$N_PUP" -ge "$WANTA" ] && [ $PGOK -eq 0 ]; then PASS=$((PASS+1)); echo " PASS [$label] aumid: $N_PUP 'Adom Pup' per-window buttons + pixel gate"; return 0
else FAIL=$((FAIL+1)); echo " FAIL [$label] aumid: $N_PUP 'Adom Pup' button(s), wanted >= $WANTA"; return 1; fi ;;
tiles)
WANT=$(python3 -c "import os;o=[x for x in (os.environ.get('OWNERS') or '').split('|') if x];print(max(1,len(o)-1))")
if [ "$N_THREAD" -ge "$WANT" ] && [ $PGOK -eq 0 ]; then PASS=$((PASS+1)); echo " PASS [$label] tiles: $N_THREAD thread-name buttons + pixel gate"; return 0
else FAIL=$((FAIL+1)); echo " FAIL [$label] tiles: $N_THREAD thread-name button(s), wanted >= $WANT of [$OWNERS]"; return 1; fi ;;
esac
}
round() { # round <mode> <ov> <label> — clean switch, wait, verify
echo "== ROUND $3: switch to $1 + overlay $2 =="
switch_state "$1" "$2"
wait_done || echo " (apply-progress never reported done — verifying anyway)"
sleep 3
verify_state "$1" "$3"
}
rapid_round() { # rapid_round <modeA> <modeB> <label> — the race: click A, then B ~1.2s later; B must win.
# B lands with overlay OFF: the group badge favicon is racey (most-recent window wins), so a teal
# badge would poison the base-art teal measurement nondeterministically.
echo "== RAPID $3: $1 then $2 after 1.2s (the in-flight-stamp race) =="
switch_state "$1" "on"
sleep 1.2
switch_state "$2" "off"
wait_done || echo " (apply-progress never reported done — verifying anyway)"
sleep 5
verify_state "$2" "$3"
}
round tiles on r1-tiles
round aumid on r2-aumid
round plain off r3-plain
rapid_round aumid plain r4-rapid-aumid-then-plain
round tiles on r5-tiles
rapid_round tiles plain r6-rapid-tiles-then-plain
round aumid on r7-aumid
round "$FINAL_MODE" off r8-final-$FINAL_MODE
# restore the user's chosen overlay setting (measurement used overlay-off for determinism)
switch_state "$FINAL_MODE" "$FINAL_OV"
echo "== restored final state: $FINAL_MODE + overlay $FINAL_OV =="
echo
echo "== MODE-RALPH RESULT: $PASS passed, $FAIL failed (shots in $SHOTDIR) =="
[ "$FAIL" -eq 0 ] && echo "GREEN" || echo "RED"
exit $FAIL