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
251 lines
9.5 KiB
Bash
Executable File
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 ==="
|