- bin: add hermes-claim, hermes-dispatch, hermes-enqueue (queue scripts) - bin: update timmy-loop-prompt.md (Phase 1 fix-broken-PRs, --no-verify ban) - bin: update timmy-loop.sh (timeout cleanup, claim TTL) - bin: update timmy-status.sh (watchdog auto-restart for dead loop) - bin: update timmy-tmux.sh (pane layout fixes) - bin: update timmy-watchdog.sh (minor fixes) - skills: add hermes-agent skill (was missing from repo) - memories: sync MEMORY.md and USER.md to current state - cron/channel_directory: sync runtime state - .gitignore: whitelist new bin scripts, fix hermes-agent/ scope
285 lines
9.7 KiB
Bash
Executable File
285 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ── Timmy Loop Status Panel ────────────────────────────────────────────
|
|
# Compact, info-dense sidebar for the tmux development loop.
|
|
# Refreshes every 10s. Designed for ~40-col wide pane.
|
|
# ───────────────────────────────────────────────────────────────────────
|
|
|
|
STATE="$HOME/Timmy-Time-dashboard/.loop/state.json"
|
|
REPO="$HOME/Timmy-Time-dashboard"
|
|
TOKEN=$(cat ~/.hermes/gitea_token 2>/dev/null)
|
|
API="http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
|
|
|
|
# ── Colors ──
|
|
B='\033[1m' # bold
|
|
D='\033[2m' # dim
|
|
R='\033[0m' # reset
|
|
G='\033[32m' # green
|
|
Y='\033[33m' # yellow
|
|
RD='\033[31m' # red
|
|
C='\033[36m' # cyan
|
|
M='\033[35m' # magenta
|
|
W='\033[37m' # white
|
|
BG='\033[42;30m' # green bg
|
|
BY='\033[43;30m' # yellow bg
|
|
BR='\033[41;37m' # red bg
|
|
|
|
# How wide is our pane?
|
|
COLS=$(tput cols 2>/dev/null || echo 40)
|
|
|
|
hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$COLS"); printf "${R}\n"; }
|
|
|
|
while true; do
|
|
clear
|
|
|
|
# ── Header ──
|
|
echo -e "${B}${C} ⚙ TIMMY DEV LOOP${R} ${D}$(date '+%H:%M:%S')${R}"
|
|
hr
|
|
|
|
# ── Loop State ──
|
|
if [ -f "$STATE" ]; then
|
|
eval "$(python3 -c "
|
|
import json, sys
|
|
with open('$STATE') as f: s = json.load(f)
|
|
print(f'CYCLE={s.get(\"cycle\",\"?\")}')" 2>/dev/null)"
|
|
STATUS=$(python3 -c "import json; print(json.load(open('$STATE'))['status'])" 2>/dev/null || echo "?")
|
|
LAST_OK=$(python3 -c "
|
|
import json
|
|
from datetime import datetime, timezone
|
|
s = json.load(open('$STATE'))
|
|
t = s.get('last_completed','')
|
|
if t:
|
|
dt = datetime.fromisoformat(t.replace('Z','+00:00'))
|
|
delta = datetime.now(timezone.utc) - dt
|
|
mins = int(delta.total_seconds() / 60)
|
|
if mins < 60: print(f'{mins}m ago')
|
|
else: print(f'{mins//60}h {mins%60}m ago')
|
|
else: print('never')
|
|
" 2>/dev/null || echo "?")
|
|
CLOSED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_closed',[])))" 2>/dev/null || echo 0)
|
|
CREATED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_created',[])))" 2>/dev/null || echo 0)
|
|
ERRS=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('errors',[])))" 2>/dev/null || echo 0)
|
|
LAST_ISSUE=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_issue','—'))" 2>/dev/null || echo "—")
|
|
LAST_PR=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_pr','—'))" 2>/dev/null || echo "—")
|
|
TESTS=$(python3 -c "
|
|
import json
|
|
s = json.load(open('$STATE'))
|
|
t = s.get('test_results',{})
|
|
if t:
|
|
print(f\"{t.get('passed',0)} pass, {t.get('failed',0)} fail, {t.get('coverage','?')} cov\")
|
|
else:
|
|
print('no data')
|
|
" 2>/dev/null || echo "no data")
|
|
|
|
# Status badge
|
|
case "$STATUS" in
|
|
working) BADGE="${BY} WORKING ${R}" ;;
|
|
idle) BADGE="${BG} IDLE ${R}" ;;
|
|
error) BADGE="${BR} ERROR ${R}" ;;
|
|
*) BADGE="${D} $STATUS ${R}" ;;
|
|
esac
|
|
|
|
echo -e " ${B}Status${R} $BADGE ${D}cycle${R} ${B}$CYCLE${R}"
|
|
echo -e " ${B}Last OK${R} ${G}$LAST_OK${R} ${D}issue${R} #$LAST_ISSUE ${D}PR${R} #$LAST_PR"
|
|
echo -e " ${G}✓${R} $CLOSED closed ${C}+${R} $CREATED created ${RD}✗${R} $ERRS errs"
|
|
echo -e " ${D}Tests:${R} $TESTS"
|
|
else
|
|
echo -e " ${RD}No state file${R}"
|
|
fi
|
|
|
|
hr
|
|
|
|
# ── Ollama Status ──
|
|
echo -e " ${B}${M}◆ OLLAMA${R}"
|
|
OLLAMA_PS=$(curl -s http://localhost:11434/api/ps 2>/dev/null)
|
|
if [ -n "$OLLAMA_PS" ] && echo "$OLLAMA_PS" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then
|
|
python3 -c "
|
|
import json, sys
|
|
data = json.loads('''$OLLAMA_PS''')
|
|
models = data.get('models', [])
|
|
if not models:
|
|
print(' \033[2m(no models loaded)\033[0m')
|
|
for m in models:
|
|
name = m.get('name','?')
|
|
vram = m.get('size_vram', 0) / 1e9
|
|
exp = m.get('expires_at','')
|
|
print(f' \033[32m●\033[0m {name} \033[2m{vram:.1f}GB VRAM\033[0m')
|
|
" 2>/dev/null
|
|
else
|
|
echo -e " ${RD}● offline${R}"
|
|
fi
|
|
|
|
# ── Timmy Health ──
|
|
TIMMY_HEALTH=$(curl -s --max-time 2 http://localhost:8000/health 2>/dev/null)
|
|
if [ -n "$TIMMY_HEALTH" ]; then
|
|
python3 -c "
|
|
import json
|
|
h = json.loads('''$TIMMY_HEALTH''')
|
|
status = h.get('status','?')
|
|
ollama = h.get('services',{}).get('ollama','?')
|
|
model = h.get('llm_model','?')
|
|
agent_st = list(h.get('agents',{}).values())[0].get('status','?') if h.get('agents') else '?'
|
|
up = int(h.get('uptime_seconds',0))
|
|
hrs, rem = divmod(up, 3600)
|
|
mins = rem // 60
|
|
print(f' \033[1m\033[35m◆ TIMMY DASHBOARD\033[0m')
|
|
print(f' \033[32m●\033[0m {status} model={model}')
|
|
print(f' \033[2magent={agent_st} ollama={ollama} up={hrs}h{mins}m\033[0m')
|
|
" 2>/dev/null
|
|
else
|
|
echo -e " ${B}${M}◆ TIMMY DASHBOARD${R}"
|
|
echo -e " ${RD}● unreachable${R}"
|
|
fi
|
|
|
|
hr
|
|
|
|
# ── Open Issues ──
|
|
echo -e " ${B}${Y}▶ OPEN ISSUES${R}"
|
|
if [ -n "$TOKEN" ]; then
|
|
curl -s "${API}/issues?state=open&limit=10&sort=created&direction=desc" \
|
|
-H "Authorization: token $TOKEN" 2>/dev/null | \
|
|
python3 -c "
|
|
import json, sys
|
|
try:
|
|
issues = json.load(sys.stdin)
|
|
if not issues:
|
|
print(' \033[2m(none)\033[0m')
|
|
for i in issues[:10]:
|
|
num = i['number']
|
|
title = i['title'][:36]
|
|
labels = ','.join(l['name'][:8] for l in i.get('labels',[]))
|
|
lbl = f' \033[2m[{labels}]\033[0m' if labels else ''
|
|
print(f' \033[33m#{num:<4d}\033[0m {title}{lbl}')
|
|
if len(issues) > 10:
|
|
print(f' \033[2m... +{len(issues)-10} more\033[0m')
|
|
except: print(' \033[2m(fetch failed)\033[0m')
|
|
" 2>/dev/null
|
|
else
|
|
echo -e " ${RD}(no token)${R}"
|
|
fi
|
|
|
|
# ── Open PRs ──
|
|
echo -e " ${B}${G}▶ OPEN PRs${R}"
|
|
if [ -n "$TOKEN" ]; then
|
|
curl -s "${API}/pulls?state=open&limit=5" \
|
|
-H "Authorization: token $TOKEN" 2>/dev/null | \
|
|
python3 -c "
|
|
import json, sys
|
|
try:
|
|
prs = json.load(sys.stdin)
|
|
if not prs:
|
|
print(' \033[2m(none)\033[0m')
|
|
for p in prs[:5]:
|
|
num = p['number']
|
|
title = p['title'][:36]
|
|
print(f' \033[32mPR #{num:<4d}\033[0m {title}')
|
|
except: print(' \033[2m(fetch failed)\033[0m')
|
|
" 2>/dev/null
|
|
else
|
|
echo -e " ${RD}(no token)${R}"
|
|
fi
|
|
|
|
hr
|
|
|
|
# ── Git Log ──
|
|
echo -e " ${B}${D}▶ RECENT COMMITS${R}"
|
|
cd "$REPO" 2>/dev/null && git log --oneline --no-decorate -6 2>/dev/null | while read line; do
|
|
HASH=$(echo "$line" | cut -c1-7)
|
|
MSG=$(echo "$line" | cut -c9- | cut -c1-32)
|
|
echo -e " ${C}${HASH}${R} ${D}${MSG}${R}"
|
|
done
|
|
|
|
hr
|
|
|
|
# ── Claims ──
|
|
CLAIMS_FILE="$REPO/.loop/claims.json"
|
|
if [ -f "$CLAIMS_FILE" ]; then
|
|
CLAIMS=$(python3 -c "
|
|
import json
|
|
with open('$CLAIMS_FILE') as f: c = json.load(f)
|
|
active = [(k,v) for k,v in c.items() if v.get('status') == 'active']
|
|
if active:
|
|
for k,v in active:
|
|
print(f' \033[33m⚡\033[0m #{k} claimed by {v.get(\"agent\",\"?\")[:12]}')
|
|
else:
|
|
print(' \033[2m(none active)\033[0m')
|
|
" 2>/dev/null)
|
|
if [ -n "$CLAIMS" ]; then
|
|
echo -e " ${B}${Y}▶ CLAIMED${R}"
|
|
echo "$CLAIMS"
|
|
fi
|
|
fi
|
|
|
|
# ── System ──
|
|
echo -e " ${B}${D}▶ SYSTEM${R}"
|
|
# Disk
|
|
DISK=$(df -h / 2>/dev/null | tail -1 | awk '{print $4 " free / " $2}')
|
|
echo -e " ${D}Disk:${R} $DISK"
|
|
# Memory (macOS)
|
|
if command -v memory_pressure &>/dev/null; then
|
|
MEM_PRESS=$(memory_pressure 2>/dev/null | grep "System-wide" | head -1 | sed 's/.*: //')
|
|
echo -e " ${D}Mem:${R} $MEM_PRESS"
|
|
elif [ -f /proc/meminfo ]; then
|
|
MEM=$(awk '/MemAvailable/{printf "%.1fGB free", $2/1048576}' /proc/meminfo 2>/dev/null)
|
|
echo -e " ${D}Mem:${R} $MEM"
|
|
fi
|
|
# CPU load
|
|
LOAD=$(uptime | sed 's/.*averages: //' | cut -d',' -f1 | xargs)
|
|
echo -e " ${D}Load:${R} $LOAD"
|
|
|
|
hr
|
|
|
|
# ── Notes from last cycle ──
|
|
if [ -f "$STATE" ]; then
|
|
NOTES=$(python3 -c "
|
|
import json
|
|
s = json.load(open('$STATE'))
|
|
n = s.get('notes','')
|
|
if n:
|
|
lines = n[:150]
|
|
if len(n) > 150: lines += '...'
|
|
print(lines)
|
|
" 2>/dev/null)
|
|
if [ -n "$NOTES" ]; then
|
|
echo -e " ${B}${D}▶ LAST CYCLE NOTE${R}"
|
|
echo -e " ${D}${NOTES}${R}"
|
|
hr
|
|
fi
|
|
|
|
# Timmy observations
|
|
TIMMY_OBS=$(python3 -c "
|
|
import json
|
|
s = json.load(open('$STATE'))
|
|
obs = s.get('timmy_observations','')
|
|
if obs:
|
|
lines = obs[:120]
|
|
if len(obs) > 120: lines += '...'
|
|
print(lines)
|
|
" 2>/dev/null)
|
|
if [ -n "$TIMMY_OBS" ]; then
|
|
echo -e " ${B}${M}▶ TIMMY SAYS${R}"
|
|
echo -e " ${D}${TIMMY_OBS}${R}"
|
|
hr
|
|
fi
|
|
fi
|
|
|
|
# ── Watchdog: restart loop if it died ──────────────────────────────
|
|
LOOP_LOCK="/tmp/timmy-loop.lock"
|
|
if [ -f "$LOOP_LOCK" ]; then
|
|
LOOP_PID=$(cat "$LOOP_LOCK" 2>/dev/null)
|
|
if ! kill -0 "$LOOP_PID" 2>/dev/null; then
|
|
echo -e " ${BR} ⚠ LOOP DIED — RESTARTING ${R}"
|
|
rm -f "$LOOP_LOCK"
|
|
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
|
|
fi
|
|
else
|
|
# No lock file at all — loop never started or was killed
|
|
if ! pgrep -f "timmy-loop.sh" >/dev/null 2>&1; then
|
|
echo -e " ${BR} ⚠ LOOP NOT RUNNING — STARTING ${R}"
|
|
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
echo -e " ${D}↻ 8s${R}"
|
|
sleep 8
|
|
done
|