feat: ops dashboard v2 + kimi-loop updates

New files:
  ops-dashboard-v2.sh — 3-pane tmux launcher
  ops-panel.sh — consolidated status view (services, kimi, PRs, queue, warnings)
  ops-helpers.sh — control functions (ops-wake-kimi, ops-merge, ops-assign, etc)
  ops-status.sh, ops-gitea.sh — v1 individual panels (kept for reference)

Updated:
  timmy-loop.sh — now uses Kimi Code CLI instead of Claude Code
  timmy-loop-prompt.md — VPS Gitea URL
  timmy-status.sh — VPS Gitea URL
This commit is contained in:
Alexander Whitestone
2026-03-21 12:00:18 -04:00
parent 26717d6fc1
commit 3a9c15a98f
8 changed files with 502 additions and 69 deletions

38
bin/ops-dashboard-v2.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# ── Hermes Ops Dashboard v2 ────────────────────────────────────────────
# Clean 3-pane layout:
# ┌───────────────────────────────────┬────────────────────────────────┐
# │ │ │
# │ STATUS + GITEA + QUEUE │ KIMI LIVE FEED │
# │ (auto-refresh 20s) │ (tail log, colored) │
# │ │ │
# │ │ │
# ├───────────────────────────────────┴────────────────────────────────┤
# │ CONTROLS (bash prompt with helpers loaded) │
# └────────────────────────────────────────────────────────────────────┘
# ───────────────────────────────────────────────────────────────────────
SESSION="ops"
tmux kill-session -t "$SESSION" 2>/dev/null
tmux new-session -d -s "$SESSION"
# Split: left status (60%) | right kimi log (40%)
tmux split-window -h -p 40 -t "$SESSION"
# Split left pane: top status (80%) | bottom controls (20%)
tmux split-window -v -p 20 -t "$SESSION:1.1"
# Pane 1 (top-left): consolidated status, auto-refresh
tmux send-keys -t "$SESSION:1.1" "watch -n 20 -t -c 'bash ~/.hermes/bin/ops-panel.sh'" Enter
# Pane 2 (right): kimi live feed with color
tmux send-keys -t "$SESSION:1.2" "tail -f ~/.hermes/logs/kimi-loop.log | GREP_COLOR='1;32' grep --color=always -E 'SUCCESS|$' | GREP_COLOR='1;31' grep --color=always -E 'FAILED|BACKOFF|$' | GREP_COLOR='1;36' grep --color=always -E 'ISSUE #[0-9]+|$'" Enter
# Pane 3 (bottom-left): controls with helpers sourced
tmux send-keys -t "$SESSION:1.3" "source ~/.hermes/bin/ops-helpers.sh && ops-help" Enter
# Focus on status pane
tmux select-pane -t "$SESSION:1.1"
tmux attach -t "$SESSION"

69
bin/ops-gitea.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# ── Gitea Feed Panel ───────────────────────────────────────────────────
# Shows open PRs, recent merges, and issue queue. Called by watch.
# ───────────────────────────────────────────────────────────────────────
B='\033[1m' ; D='\033[2m' ; R='\033[0m'
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m'
TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
echo -e "${B}${C} ◈ GITEA${R} ${D}$(date '+%H:%M:%S')${R}"
echo -e "${D}────────────────────────────────────────${R}"
# Open PRs
echo -e " ${B}Open PRs${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=10" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
if not prs: print(' (none)')
for p in prs:
age_h = ''
print(f' #{p[\"number\"]:3d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:45]}')
except: print(' (error)')
" 2>/dev/null
echo -e "${D}────────────────────────────────────────${R}"
# Recent merged (last 5)
echo -e " ${B}Recently Merged${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
merged = [p for p in prs if p.get('merged')]
if not merged: print(' (none)')
for p in merged[:5]:
t = p['merged_at'][:16].replace('T',' ')
print(f' ${G}${R} #{p[\"number\"]:3d} {p[\"title\"][:35]} ${D}{t}${R}')
except: print(' (error)')
" 2>/dev/null
echo -e "${D}────────────────────────────────────────${R}"
# Issue queue (assigned to kimi)
echo -e " ${B}Kimi Queue${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&assignee=kimi&limit=10&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
issues = json.loads(sys.stdin.read())
if not issues: print(' (empty — assign more!)')
for i in issues[:8]:
print(f' #{i[\"number\"]:3d} {i[\"title\"][:50]}')
if len(issues) > 8: print(f' ... +{len(issues)-8} more')
except: print(' (error)')
" 2>/dev/null
echo -e "${D}────────────────────────────────────────${R}"
# Unassigned issues
UNASSIGNED=$(curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
issues = json.loads(sys.stdin.read())
print(len([i for i in issues if not i.get('assignees')]))
except: print('?')
" 2>/dev/null)
echo -e " Unassigned issues: ${Y}$UNASSIGNED${R}"

111
bin/ops-helpers.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# ── Dashboard Control Helpers ──────────────────────────────────────────
# Source this in the controls pane: source ~/.hermes/bin/ops-helpers.sh
# ───────────────────────────────────────────────────────────────────────
export TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
export GITEA="http://143.198.27.163:3000"
export REPO_API="$GITEA/api/v1/repos/rockachopa/Timmy-time-dashboard"
ops-help() {
echo ""
echo -e "\033[1m\033[35m ◈ CONTROLS\033[0m"
echo -e "\033[2m ──────────────────────────────────────\033[0m"
echo ""
echo -e " \033[1mWake Up\033[0m"
echo " ops-wake-kimi Restart Kimi loop"
echo " ops-wake-gateway Restart gateway"
echo " ops-wake-all Restart everything"
echo ""
echo -e " \033[1mManage\033[0m"
echo " ops-merge PR_NUM Squash-merge a PR"
echo " ops-assign ISSUE Assign issue to Kimi"
echo " ops-audit Run efficiency audit now"
echo " ops-prs List open PRs"
echo " ops-queue Show Kimi's queue"
echo ""
echo -e " \033[1mEmergency\033[0m"
echo " ops-kill-kimi Stop Kimi loop"
echo " ops-kill-zombies Kill stuck git/pytest"
echo ""
echo -e " \033[2m Type ops-help to see this again\033[0m"
echo ""
}
ops-wake-kimi() {
pkill -f "kimi-loop.sh" 2>/dev/null
sleep 1
nohup bash ~/.hermes/bin/kimi-loop.sh >> ~/.hermes/logs/kimi-loop.log 2>&1 &
echo " Kimi loop started (PID $!)"
}
ops-wake-gateway() {
hermes gateway start 2>&1
}
ops-wake-all() {
ops-wake-gateway
sleep 1
ops-wake-kimi
echo " All services started"
}
ops-merge() {
local pr=$1
[ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER"; return 1; }
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$REPO_API/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c "
import json,sys
d=json.loads(sys.stdin.read())
if 'sha' in d: print(f' ✓ PR #{$pr} merged ({d[\"sha\"][:8]})')
else: print(f' ✗ {d.get(\"message\",\"unknown error\")}')
" 2>/dev/null
}
ops-assign() {
local issue=$1
[ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER"; return 1; }
curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$REPO_API/issues/$issue" -d '{"assignees":["kimi"]}' | python3 -c "
import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to kimi')
" 2>/dev/null
}
ops-audit() {
bash ~/.hermes/bin/efficiency-audit.sh
}
ops-prs() {
curl -s -H "Authorization: token $TOKEN" "$REPO_API/pulls?state=open&limit=20" | python3 -c "
import json,sys
prs=json.loads(sys.stdin.read())
for p in prs: print(f' #{p[\"number\"]:4d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:60]}')
if not prs: print(' (none)')
" 2>/dev/null
}
ops-queue() {
curl -s -H "Authorization: token $TOKEN" "$REPO_API/issues?state=open&assignee=kimi&limit=20&type=issues" | python3 -c "
import json,sys
issues=json.loads(sys.stdin.read())
for i in issues: print(f' #{i[\"number\"]:4d} {i[\"title\"][:60]}')
if not issues: print(' (empty)')
" 2>/dev/null
}
ops-kill-kimi() {
pkill -f "kimi-loop.sh" 2>/dev/null
pkill -f "kimi.*--print" 2>/dev/null
echo " Kimi stopped"
}
ops-kill-zombies() {
local killed=0
for pid in $(ps aux | grep "pytest tests/" | grep -v grep | awk '{print $2}'); do
kill "$pid" 2>/dev/null && killed=$((killed+1))
done
for pid in $(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | awk '{print $2}'); do
kill "$pid" 2>/dev/null && killed=$((killed+1))
done
echo " Killed $killed zombie processes"
}

158
bin/ops-panel.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# ── Consolidated Ops Panel ─────────────────────────────────────────────
# Everything in one view. Designed for a half-screen pane (~100x45).
# ───────────────────────────────────────────────────────────────────────
B='\033[1m' ; D='\033[2m' ; R='\033[0m' ; U='\033[4m'
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m' ; W='\033[37m'
OK="${G}${R}" ; WARN="${Y}${R}" ; FAIL="${RD}${R}" ; OFF="${D}${R}"
TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
# ── HEADER ─────────────────────────────────────────────────────────────
echo ""
echo -e " ${B}${M}◈ HERMES OPERATIONS${R} ${D}$(date '+%a %b %d %H:%M:%S')${R}"
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}"
echo ""
# ── SERVICES ───────────────────────────────────────────────────────────
echo -e " ${B}${U}SERVICES${R}"
echo ""
# Gateway
GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1)
[ -n "$GW_PID" ] && echo -e " ${OK} Gateway ${D}pid $GW_PID${R}" \
|| echo -e " ${FAIL} Gateway ${RD}DOWN — run: hermes gateway start${R}"
# Kimi Code loop
KIMI_PID=$(pgrep -f "kimi-loop.sh" 2>/dev/null | head -1)
[ -n "$KIMI_PID" ] && echo -e " ${OK} Kimi Loop ${D}pid $KIMI_PID${R}" \
|| echo -e " ${FAIL} Kimi Loop ${RD}DOWN — run: ops-wake-kimi${R}"
# Active Kimi Code worker
KIMI_WORK=$(pgrep -f "kimi.*--print" 2>/dev/null | head -1)
if [ -n "$KIMI_WORK" ]; then
echo -e " ${OK} Kimi Code ${D}pid $KIMI_WORK ${G}working${R}"
elif [ -n "$KIMI_PID" ]; then
echo -e " ${WARN} Kimi Code ${Y}between issues${R}"
else
echo -e " ${OFF} Kimi Code ${D}not running${R}"
fi
# Gitea VPS
if curl -s --max-time 3 "http://143.198.27.163:3000/api/v1/version" >/dev/null 2>&1; then
echo -e " ${OK} Gitea VPS ${D}143.198.27.163:3000${R}"
else
echo -e " ${FAIL} Gitea VPS ${RD}unreachable${R}"
fi
# Matrix staging
HTTP=$(curl -s --max-time 3 -o /dev/null -w "%{http_code}" "http://143.198.27.163/")
[ "$HTTP" = "200" ] && echo -e " ${OK} Matrix Staging ${D}143.198.27.163${R}" \
|| echo -e " ${FAIL} Matrix Staging ${RD}HTTP $HTTP${R}"
# Dev cycle cron
CRON_LINE=$(hermes cron list 2>&1 | grep -B1 "consolidated-dev-cycle" | head -1 2>/dev/null)
if echo "$CRON_LINE" | grep -q "active"; then
NEXT=$(hermes cron list 2>&1 | grep -A4 "consolidated-dev-cycle" | grep "Next" | awk '{print $NF}' | cut -dT -f2 | cut -d. -f1)
echo -e " ${OK} Dev Cycle ${D}every 30m, next ${NEXT:-?}${R}"
else
echo -e " ${FAIL} Dev Cycle Cron ${RD}MISSING${R}"
fi
echo ""
# ── KIMI STATS ─────────────────────────────────────────────────────────
echo -e " ${B}${U}KIMI${R}"
echo ""
KIMI_LOG="$HOME/.hermes/logs/kimi-loop.log"
if [ -f "$KIMI_LOG" ]; then
COMPLETED=$(grep -c "SUCCESS:" "$KIMI_LOG" 2>/dev/null || echo 0)
FAILED=$(grep -c "FAILED:" "$KIMI_LOG" 2>/dev/null || echo 0)
LAST_ISSUE=$(grep "=== ISSUE" "$KIMI_LOG" | tail -1 | sed 's/.*=== //' | sed 's/ ===//')
LAST_TIME=$(grep "=== ISSUE\|SUCCESS\|FAILED" "$KIMI_LOG" | tail -1 | cut -d']' -f1 | tr -d '[')
RATE=""
if [ "$COMPLETED" -gt 0 ] && [ "$FAILED" -gt 0 ]; then
TOTAL=$((COMPLETED + FAILED))
PCT=$((COMPLETED * 100 / TOTAL))
RATE=" (${PCT}% success)"
fi
echo -e " Completed ${G}${B}$COMPLETED${R} Failed ${RD}$FAILED${R}${D}$RATE${R}"
echo -e " Current ${C}$LAST_ISSUE${R}"
echo -e " Last seen ${D}$LAST_TIME${R}"
fi
echo ""
# ── OPEN PRS ───────────────────────────────────────────────────────────
echo -e " ${B}${U}PULL REQUESTS${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=8" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
if not prs: print(' \033[2m(none open)\033[0m')
for p in prs[:6]:
n = p['number']
t = p['title'][:55]
u = p['user']['login']
print(f' \033[33m#{n:<4d}\033[0m \033[2m{u:8s}\033[0m {t}')
if len(prs) > 6: print(f' \033[2m... +{len(prs)-6} more\033[0m')
except: print(' \033[31m(error fetching)\033[0m')
" 2>/dev/null
echo ""
# ── RECENTLY MERGED ────────────────────────────────────────────────────
echo -e " ${B}${U}RECENTLY MERGED${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
merged = [p for p in prs if p.get('merged')][:5]
if not merged: print(' \033[2m(none recent)\033[0m')
for p in merged:
n = p['number']
t = p['title'][:50]
when = p['merged_at'][11:16]
print(f' \033[32m✓ #{n:<4d}\033[0m {t} \033[2m{when}\033[0m')
except: print(' \033[31m(error)\033[0m')
" 2>/dev/null
echo ""
# ── KIMI QUEUE ─────────────────────────────────────────────────────────
echo -e " ${B}${U}KIMI QUEUE${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&assignee=kimi&limit=10&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
issues = json.loads(sys.stdin.read())
if not issues: print(' \033[33m⚠ Queue empty — assign more issues to kimi\033[0m')
for i in issues[:6]:
n = i['number']
t = i['title'][:55]
print(f' #{n:<4d} {t}')
if len(issues) > 6: print(f' \033[2m... +{len(issues)-6} more\033[0m')
except: print(' \033[31m(error)\033[0m')
" 2>/dev/null
echo ""
# ── WARNINGS ───────────────────────────────────────────────────────────
HERMES_PROCS=$(ps aux | grep -E "hermes.*python" | grep -v grep | wc -l | tr -d ' ')
STUCK_GIT=$(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | wc -l | tr -d ' ')
ORPHAN_PY=$(ps aux | grep "pytest tests/" | grep -v grep | wc -l | tr -d ' ')
UNASSIGNED=$(curl -s --max-time 3 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "import json,sys; issues=json.loads(sys.stdin.read()); print(len([i for i in issues if not i.get('assignees')]))" 2>/dev/null)
WARNS=""
[ "$STUCK_GIT" -gt 0 ] && WARNS+=" ${RD}$STUCK_GIT stuck git processes${R}\n"
[ "$ORPHAN_PY" -gt 0 ] && WARNS+=" ${Y}$ORPHAN_PY orphaned pytest runs${R}\n"
[ "${UNASSIGNED:-0}" -gt 10 ] && WARNS+=" ${Y}$UNASSIGNED unassigned issues — feed the queue${R}\n"
if [ -n "$WARNS" ]; then
echo -e " ${B}${U}WARNINGS${R}"
echo ""
echo -e "$WARNS"
fi
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}"
echo -e " ${D}hermes sessions: $HERMES_PROCS unassigned: ${UNASSIGNED:-?} ↻ 20s${R}"

93
bin/ops-status.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# ── Live Status Panel ──────────────────────────────────────────────────
# Single-screen health overview. Called by watch every 30s.
# ───────────────────────────────────────────────────────────────────────
B='\033[1m' ; D='\033[2m' ; R='\033[0m'
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m'
OK="${G}${R}" ; WARN="${Y}${R}" ; FAIL="${RD}${R}" ; OFF="${D}${R}"
echo -e "${B}${M} ◈ HERMES OPS${R} ${D}$(date '+%Y-%m-%d %H:%M:%S')${R}"
echo -e "${D}────────────────────────────────────────${R}"
# Gateway
GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1)
if [ -n "$GW_PID" ]; then
echo -e " ${OK} Gateway ${D}PID $GW_PID${R}"
else
echo -e " ${FAIL} Gateway ${RD}DOWN${R}"
fi
# Kimi loop
KIMI_PID=$(pgrep -f "kimi-loop.sh" 2>/dev/null | head -1)
if [ -n "$KIMI_PID" ]; then
echo -e " ${OK} Kimi Loop ${D}PID $KIMI_PID${R}"
else
echo -e " ${FAIL} Kimi Loop ${RD}DOWN${R}"
fi
# Active Claude Code (kimi's worker)
CC_PID=$(pgrep -f "claude.*--dangerously" 2>/dev/null | head -1)
if [ -n "$CC_PID" ]; then
echo -e " ${OK} Claude Code ${D}PID $CC_PID (working)${R}"
else
if [ -n "$KIMI_PID" ]; then
echo -e " ${WARN} Claude Code ${Y}idle/between issues${R}"
else
echo -e " ${OFF} Claude Code ${D}not running${R}"
fi
fi
# Gitea VPS
if curl -s --max-time 3 "http://143.198.27.163:3000/api/v1/version" >/dev/null 2>&1; then
echo -e " ${OK} Gitea VPS ${D}143.198.27.163:3000${R}"
else
echo -e " ${FAIL} Gitea VPS ${RD}UNREACHABLE${R}"
fi
# Dev cycle cron
CRON_LINE=$(hermes cron list 2>&1 | grep -B1 "consolidated-dev-cycle" | head -1 2>/dev/null)
if echo "$CRON_LINE" | grep -q "active"; then
echo -e " ${OK} Dev Cycle Cron ${D}active (every 30m, ∞)${R}"
elif echo "$CRON_LINE" | grep -q "paused"; then
echo -e " ${WARN} Dev Cycle Cron ${Y}PAUSED${R}"
else
echo -e " ${FAIL} Dev Cycle Cron ${RD}MISSING${R}"
fi
echo -e "${D}────────────────────────────────────────${R}"
# Kimi stats
KIMI_LOG="$HOME/.hermes/logs/kimi-loop.log"
if [ -f "$KIMI_LOG" ]; then
COMPLETED=$(grep -c "SUCCESS:" "$KIMI_LOG" 2>/dev/null || echo 0)
FAILED=$(grep -c "FAILED:" "$KIMI_LOG" 2>/dev/null || echo 0)
LAST=$(tail -1 "$KIMI_LOG" 2>/dev/null | cut -d']' -f1 | tr -d '[')
echo -e " ${B}Kimi Stats${R}"
echo -e " Completed: ${G}$COMPLETED${R} Failed: ${RD}$FAILED${R}"
echo -e " Last activity: ${D}$LAST${R}"
LAST_ISSUE=$(grep "=== ISSUE" "$KIMI_LOG" | tail -1 | sed 's/.*=== ISSUE //' | sed 's/ ===//' 2>/dev/null)
[ -n "$LAST_ISSUE" ] && echo -e " Working on: ${C}$LAST_ISSUE${R}"
fi
echo -e "${D}────────────────────────────────────────${R}"
# Process count
HERMES_PROCS=$(ps aux | grep -E "hermes.*python" | grep -v grep | wc -l | tr -d ' ')
PYTEST_PROCS=$(ps aux | grep "pytest" | grep -v grep | wc -l | tr -d ' ')
GIT_STUCK=$(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | wc -l | tr -d ' ')
echo -e " ${B}Processes${R}"
echo -e " Hermes sessions: $HERMES_PROCS"
[ "$PYTEST_PROCS" -gt 0 ] && echo -e " Pytest running: ${Y}$PYTEST_PROCS${R}"
[ "$GIT_STUCK" -gt 0 ] && echo -e " ${RD}Stuck git: $GIT_STUCK${R}"
echo -e "${D}────────────────────────────────────────${R}"
# Last audit
LAST_AUDIT=$(ls -t ~/.hermes/audits/audit-*.md 2>/dev/null | head -1)
if [ -n "$LAST_AUDIT" ]; then
AUDIT_TIME=$(basename "$LAST_AUDIT" | sed 's/audit-//' | sed 's/\.md//' | sed 's/_/ /')
FINDINGS=$(grep "issue(s) found" "$LAST_AUDIT" 2>/dev/null | head -1)
echo -e " ${B}Last Audit${R}: ${D}$AUDIT_TIME${R}"
[ -n "$FINDINGS" ] && echo -e " $FINDINGS"
fi

View File

@@ -1,7 +1,7 @@
You are the Timmy development loop orchestrator.
REPO: ~/Timmy-Time-dashboard
API: http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard
API: http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard
GITEA TOKEN: ~/.hermes/gitea_token (hermes user)
STATE: ~/Timmy-Time-dashboard/.loop/state.json
CLAIMS: ~/Timmy-Time-dashboard/.loop/claims.json

View File

@@ -20,6 +20,9 @@ CLAIMS="$REPO/.loop/claims.json"
PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md"
LOCKFILE="/tmp/timmy-loop.lock"
COOLDOWN=3
IDLE_COOLDOWN=60 # 1 min base when idle (no queue / no work)
IDLE_MAX_COOLDOWN=600 # 10 min max backoff when idle
IDLE_STREAK=0 # consecutive idle cycles
MAX_CYCLE_TIME=1200 # 20 min — enough for complex issues
CLAIM_TTL_SECONDS=3600 # 1 hour — stale claims auto-expire
FAST_TRIAGE_INTERVAL=5 # mechanical scoring every N cycles
@@ -65,58 +68,6 @@ log() {
echo "[$(date '+%H:%M:%S')] $*"
}
PAUSE_FILE="$REPO/.loop/PAUSED"
CONSECUTIVE_FAILURES=0
HEALTH_CHECK_INTERVAL=30
MAX_BACKOFF=300
# ── Backend health check (Anthropic) ─────────────────────────────────
check_backend() {
local result
result=$(hermes chat -q "ping" -Q 2>/dev/null) || true
if [ -n "$result" ] && [ "${#result}" -gt 2 ]; then
return 0
fi
return 1
}
enter_pause() {
local reason="${1:-backend unreachable}"
echo "$reason (since $(date '+%H:%M:%S'))" > "$PAUSE_FILE"
log "⏸ PAUSED: $reason"
update_state "status" '"paused"'
}
leave_pause() {
rm -f "$PAUSE_FILE"
CONSECUTIVE_FAILURES=0
log "▶ RESUMED: backend is back"
update_state "status" '"running"'
}
wait_for_backend() {
local wait_time=$HEALTH_CHECK_INTERVAL
while true; do
# Check for STOP file even while paused
if [ -f "$REPO/.loop/STOP" ]; then
log "STOP file found while paused. Halting."
update_state "status" '"stopped"'
exit 0
fi
sleep "$wait_time"
log "Probing backend..."
if check_backend; then
leave_pause
return 0
fi
log "Backend still down. Next probe in ${wait_time}s"
wait_time=$(( wait_time * 2 ))
if [ "$wait_time" -gt "$MAX_BACKOFF" ]; then
wait_time=$MAX_BACKOFF
fi
done
}
# ── Expire stale claims ──────────────────────────────────────────────
expire_claims() {
python3 -c "
@@ -294,6 +245,34 @@ $QUEUE_CONTEXT
$PROMPT"
# ── Idle detection: skip cycle if queue is empty ──────────────────
QUEUE_EMPTY=false
if [ -f "$QUEUE_FILE" ]; then
QUEUE_LEN=$(python3 -c "import json; q=json.load(open('$QUEUE_FILE')); print(len(q))" 2>/dev/null || echo "0")
if [ "$QUEUE_LEN" = "0" ]; then
QUEUE_EMPTY=true
fi
else
QUEUE_EMPTY=true
fi
if [ "$QUEUE_EMPTY" = "true" ]; then
IDLE_STREAK=$((IDLE_STREAK + 1))
# Exponential backoff: 60s, 120s, 240s, ... capped at 600s
WAIT=$((IDLE_COOLDOWN * (2 ** (IDLE_STREAK - 1))))
if [ "$WAIT" -gt "$IDLE_MAX_COOLDOWN" ]; then
WAIT=$IDLE_MAX_COOLDOWN
fi
log "Queue empty (idle streak: $IDLE_STREAK). Sleeping ${WAIT}s before next triage."
update_state "status" '"idle"'
# Do NOT log a retro entry — empty cycles are not cycles
sleep "$WAIT"
continue
fi
# Reset idle streak — we have work
IDLE_STREAK=0
log "Spawning hermes for cycle $CYCLE..."
# Run hermes with timeout — tee to log AND stdout for live visibility
@@ -322,21 +301,6 @@ with open('$STATE', 'w') as f: json.dump(s, f, indent=2)
# ── Cleanup on failure ───────────────────────────────────────
cleanup_cycle "$CYCLE"
# ── Backend down? Pause with backoff ─────────────────────────
CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
if [ "$CONSECUTIVE_FAILURES" -ge 2 ]; then
log "$CONSECUTIVE_FAILURES consecutive failures. Checking backend..."
if ! check_backend; then
enter_pause "backend down after $CONSECUTIVE_FAILURES consecutive failures"
wait_for_backend
fi
fi
fi
# Reset failure counter on success (already 0 path above)
if [ "${EXIT_CODE:-0}" -eq 0 ] 2>/dev/null; then
CONSECUTIVE_FAILURES=0
fi
log "Cooling down ${COOLDOWN}s before next cycle..."

View File

@@ -7,7 +7,7 @@
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"
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
# ── Colors ──
B='\033[1m' # bold