diff --git a/bin/ops-dashboard-v2.sh b/bin/ops-dashboard-v2.sh new file mode 100755 index 0000000..f18400d --- /dev/null +++ b/bin/ops-dashboard-v2.sh @@ -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" diff --git a/bin/ops-gitea.sh b/bin/ops-gitea.sh new file mode 100755 index 0000000..10a9f4e --- /dev/null +++ b/bin/ops-gitea.sh @@ -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}" diff --git a/bin/ops-helpers.sh b/bin/ops-helpers.sh new file mode 100755 index 0000000..70c1063 --- /dev/null +++ b/bin/ops-helpers.sh @@ -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" +} diff --git a/bin/ops-panel.sh b/bin/ops-panel.sh new file mode 100755 index 0000000..886bcfe --- /dev/null +++ b/bin/ops-panel.sh @@ -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}" diff --git a/bin/ops-status.sh b/bin/ops-status.sh new file mode 100755 index 0000000..367a56a --- /dev/null +++ b/bin/ops-status.sh @@ -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 diff --git a/bin/timmy-loop-prompt.md b/bin/timmy-loop-prompt.md index e1ef171..c1d49e2 100644 --- a/bin/timmy-loop-prompt.md +++ b/bin/timmy-loop-prompt.md @@ -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 diff --git a/bin/timmy-loop.sh b/bin/timmy-loop.sh index d211886..d7a94bd 100755 --- a/bin/timmy-loop.sh +++ b/bin/timmy-loop.sh @@ -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..." diff --git a/bin/timmy-status.sh b/bin/timmy-status.sh index c4b34fb..fb7cca3 100755 --- a/bin/timmy-status.sh +++ b/bin/timmy-status.sh @@ -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