Files
hermes-config/bin/consolidated-cycle.sh
Alexander Whitestone 539969c45d feat: add all automation scripts to source control
New:
  kimi-loop.sh — Kimi Code CLI dispatch loop (fixed: no double logging,
    idle state logs once then goes quiet, uses kimi not claude)
  consolidated-cycle.sh — sonnet dev cycle (watchdog + dev + philosophy)
  efficiency-audit.sh — zombie cleanup, plateau detection, recommendations

Fixes in kimi-loop.sh:
  - log() writes to file only (no more double lines)
  - idle queue logs once then goes silent until work appears
  - all claude references removed, uses kimi CLI
2026-03-21 12:11:30 -04:00

251 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# ── Consolidated Cycle v1 ──────────────────────────────────────────────
# Single-execution cycle. Cron fires this. No while-true. No sleep.
#
# 3 phases, always in order:
# 1. Watchdog — bash only, zero tokens, always runs
# 2. Dev cycle — sonnet, skips if no work or plateau detected
# 3. Philosophy — opus, skips if ran in last 24h
#
# PLATEAU DETECTION:
# Tracks cycle outcomes in .loop/cycle-metrics.jsonl
# If last N cycles produced zero merged PRs and zero new issues filed,
# the loop is plateauing — it skips the LLM call and logs why.
# Plateau resets when: new issues appear, PRs merge, or owner comments.
# ───────────────────────────────────────────────────────────────────────
set -uo pipefail
export PATH="$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
REPO="$HOME/Timmy-Time-dashboard"
STATE="$REPO/.loop/state.json"
CLAIMS="$REPO/.loop/claims.json"
PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md"
LOG_DIR="$REPO/.loop/logs"
METRICS="$REPO/.loop/cycle-metrics.jsonl"
QUEUE_FILE="$REPO/.loop/queue.json"
TRIAGE_SCRIPT="$REPO/scripts/triage_score.py"
RETRO_SCRIPT="$REPO/scripts/cycle_retro.py"
GITEA_URL="http://143.198.27.163:3000"
GITEA_API="$GITEA_URL/api/v1"
GITEA_TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null || cat ~/.hermes/gitea_token 2>/dev/null)
REPO_API="$GITEA_API/repos/rockachopa/Timmy-time-dashboard"
MAX_CYCLE_TIME=1200
PHILOSOPHY_MARKER="/tmp/philosophy-last-run"
PLATEAU_THRESHOLD=3 # skip after N consecutive zero-output cycles
DEV_MODEL="claude-sonnet-4-20250514"
PHILOSOPHY_MODEL="claude-opus-4-6"
# macOS timeout fallback
if ! command -v timeout &>/dev/null; then
timeout() { local d="$1"; shift; perl -e "alarm $d; exec @ARGV" -- "$@"; }
fi
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# ── Plateau Detection ─────────────────────────────────────────────────
check_plateau() {
# Returns 0 (true/plateau) or 1 (false/work to do)
[ ! -f "$METRICS" ] && return 1 # no history = no plateau
local recent
recent=$(tail -n "$PLATEAU_THRESHOLD" "$METRICS" 2>/dev/null)
local zero_count=0
local total=0
while IFS= read -r line; do
total=$((total + 1))
local prs_merged issues_filed
prs_merged=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('prs_merged',0))" 2>/dev/null || echo 0)
issues_filed=$(echo "$line" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('issues_filed',0))" 2>/dev/null || echo 0)
if [ "$prs_merged" -eq 0 ] && [ "$issues_filed" -eq 0 ]; then
zero_count=$((zero_count + 1))
fi
done <<< "$recent"
if [ "$total" -ge "$PLATEAU_THRESHOLD" ] && [ "$zero_count" -ge "$PLATEAU_THRESHOLD" ]; then
return 0 # plateau detected
fi
return 1 # not plateauing
}
log_metric() {
# $1=prs_merged $2=issues_filed $3=outcome
local ts
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "{\"ts\":\"$ts\",\"prs_merged\":${1:-0},\"issues_filed\":${2:-0},\"outcome\":\"${3:-unknown}\"}" >> "$METRICS"
}
# ── Phase 1: Watchdog (bash only, zero tokens) ───────────────────────
phase_watchdog() {
log "── WATCHDOG ──"
# Kill orphaned pytest processes (> 20 min)
ps aux | grep "pytest tests/" | grep -v grep | while read -r line; do
local pid etime
pid=$(echo "$line" | awk '{print $2}')
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ "$etime" == *:*:* ]]; then
log " Killing stale pytest PID $pid ($etime)"
kill "$pid" 2>/dev/null
fi
done
# Kill stuck git pushes (> 10 min)
ps aux | grep "git.*push\|git-remote-http" | grep -v grep | while read -r line; do
local pid etime
pid=$(echo "$line" | awk '{print $2}')
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ "$etime" == *:*:* ]]; then
log " Killing stuck git PID $pid ($etime)"
kill "$pid" 2>/dev/null
fi
done
# Kill orphaned vi/vim editors
ps aux | grep "vi.*COMMIT_EDITMSG" | grep -v grep | awk '{print $2}' | xargs kill 2>/dev/null
# Expire stale claims
if [ -f "$CLAIMS" ]; then
python3 -c "
import json, time
try:
claims = json.load(open('$CLAIMS'))
now = time.time()
expired = [k for k,v in claims.items() if isinstance(v, dict) and now - v.get('ts', now) > 3600]
for k in expired:
del claims[k]
print(f' Expired claim: {k}')
if expired:
json.dump(claims, open('$CLAIMS', 'w'), indent=2)
except: pass
" 2>/dev/null
fi
# Gitea health
if curl -s --max-time 5 "$GITEA_URL/api/v1/version" >/dev/null 2>&1; then
log " Gitea: OK"
else
log " WARNING: Gitea unreachable"
fi
}
# ── Phase 2: Dev Cycle (sonnet) ───────────────────────────────────────
phase_dev() {
log "── DEV CYCLE (model: $DEV_MODEL) ──"
# Plateau check
if check_plateau; then
log " PLATEAU: Last $PLATEAU_THRESHOLD cycles produced no output. Skipping LLM call."
log " (Will resume when new issues appear or PRs need review)"
# But still check if there's new external activity that breaks plateau
local open_prs
open_prs=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$REPO_API/pulls?state=open&limit=5" 2>/dev/null | \
python3 -c "import sys,json; print(len(json.loads(sys.stdin.read())))" 2>/dev/null || echo 0)
if [ "$open_prs" -gt 0 ]; then
log " But $open_prs open PRs found — breaking plateau for review."
else
log_metric 0 0 "plateau_skip"
return
fi
fi
# Fast triage
if [ -f "$TRIAGE_SCRIPT" ]; then
GITEA_API="$GITEA_API" GITEA_TOKEN="$GITEA_TOKEN" \
python3 "$TRIAGE_SCRIPT" > "$QUEUE_FILE" 2>/dev/null || true
fi
local queue_size=0
if [ -f "$QUEUE_FILE" ]; then
queue_size=$(python3 -c "import json; print(len(json.load(open('$QUEUE_FILE'))))" 2>/dev/null || echo 0)
fi
if [ "$queue_size" -eq 0 ]; then
log " No work in queue. Skipping."
log_metric 0 0 "empty_queue"
return
fi
log " Queue: $queue_size items"
local PROMPT
PROMPT=$(cat "$PROMPT_FILE" 2>/dev/null)
[ -z "$PROMPT" ] && { log "ERROR: No prompt file"; return; }
local QUEUE_SUMMARY
QUEUE_SUMMARY=$(python3 -c "
import json
q = json.load(open('$QUEUE_FILE'))
lines = ['PRIORITIZED QUEUE ({} ready issues):'.format(len(q))]
for i, item in enumerate(q[:8]):
score = item.get('score', 0)
title = item.get('title', '?')[:70]
num = item.get('number', '?')
labels = ','.join(item.get('labels', []))
files = ', '.join(item.get('files', [])[:3])
lines.append(f' {i+1}. #{num} [{labels}] score={score} — {title}')
if files: lines.append(f' files: {files}')
if len(q) > 8: lines.append(f' ... +{len(q)-8} more')
print('\n'.join(lines))
" 2>/dev/null || echo "Queue: error")
local FULL_PROMPT="TIME BUDGET: 20 minutes. Be efficient — sonnet, not opus.
$QUEUE_SUMMARY
Pick from the TOP of this queue.
$PROMPT"
local CYCLE_LOG="$LOG_DIR/cycle-$(date +%Y%m%d_%H%M%S).log"
mkdir -p "$LOG_DIR"
if timeout "$MAX_CYCLE_TIME" hermes chat --yolo \
--provider anthropic \
--model "$DEV_MODEL" \
-q "$FULL_PROMPT" 2>&1 | tee "$CYCLE_LOG"; then
log " Cycle completed OK"
# TODO: parse cycle log for prs_merged / issues_filed counts
log_metric 0 0 "completed"
else
log " Cycle failed (exit $?)"
log_metric 0 0 "failed"
fi
}
# ── Phase 3: Philosophy (opus, daily) ─────────────────────────────────
phase_philosophy() {
if [ -f "$PHILOSOPHY_MARKER" ]; then
local last_run now elapsed
last_run=$(cat "$PHILOSOPHY_MARKER" 2>/dev/null || echo 0)
now=$(date +%s)
elapsed=$((now - last_run))
if [ "$elapsed" -lt 86400 ]; then
return # ran today already
fi
fi
log "── PHILOSOPHY (daily, model: $PHILOSOPHY_MODEL) ──"
timeout 600 hermes chat --yolo \
--provider anthropic \
--model "$PHILOSOPHY_MODEL" \
-q "You are Hermes Agent on a philosophy loop. Study the next influence from ~/philosophy-journal.md. Search web for a real primary source. Write 300-500 word reflection on agentic architecture. File Gitea issue at $REPO_API/issues (token from ~/.hermes/gitea_token_vps). Append to ~/philosophy-journal.md. Tag: [philosophy]." \
2>&1 | tee "$LOG_DIR/philosophy-$(date +%Y%m%d).log" || true
date +%s > "$PHILOSOPHY_MARKER"
log " Philosophy complete. Next: ~24h."
}
# ── Main (single execution) ───────────────────────────────────────────
log "=== CONSOLIDATED CYCLE START ==="
phase_watchdog
phase_dev
phase_philosophy
log "=== CONSOLIDATED CYCLE END ==="