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:
38
bin/ops-dashboard-v2.sh
Executable file
38
bin/ops-dashboard-v2.sh
Executable 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
69
bin/ops-gitea.sh
Executable 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
111
bin/ops-helpers.sh
Executable 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
158
bin/ops-panel.sh
Executable 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
93
bin/ops-status.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user