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
This commit is contained in:
Alexander Whitestone
2026-03-21 12:11:30 -04:00
parent 3a9c15a98f
commit 539969c45d
3 changed files with 772 additions and 0 deletions

250
bin/consolidated-cycle.sh Executable file
View File

@@ -0,0 +1,250 @@
#!/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 ==="

201
bin/efficiency-audit.sh Executable file
View File

@@ -0,0 +1,201 @@
#!/usr/bin/env bash
# ── Hermes Efficiency Audit ────────────────────────────────────────────
# Runs every 12h initially, backs off to 24h+ when plateau detected.
#
# WHAT IT CHECKS:
# 1. Zombie processes (stuck git, pytest, vi, hermes sessions)
# 2. Token spend estimate (cron run count × estimated tokens)
# 3. Plateau detection (are loops producing value?)
# 4. Stale resources (old worktrees, tmp files, logs)
# 5. Recommendations (evolve or cut)
#
# PLATEAU-AWARE SCHEDULING:
# This script tracks its own diminishing returns. If the last 3 audits
# found nothing actionable, it writes a "back-off" marker and the cron
# should extend its interval. The cron checks this marker.
# ───────────────────────────────────────────────────────────────────────
set -uo pipefail
export PATH="$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
AUDIT_DIR="$HOME/.hermes/audits"
AUDIT_LOG="$AUDIT_DIR/audit-$(date +%Y%m%d_%H%M%S).md"
BACKOFF_MARKER="$AUDIT_DIR/backoff-level"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null || cat ~/.hermes/gitea_token 2>/dev/null)
mkdir -p "$AUDIT_DIR"
findings=0
report=""
r() { report+="$1"$'\n'; }
r "# Hermes Efficiency Audit — $(date '+%Y-%m-%d %H:%M')"
r ""
# ── 1. Zombie Processes ───────────────────────────────────────────────
r "## 1. Zombie Processes"
zombies=0
# Stuck pytest (> 20 min)
while IFS= read -r line; do
if [ -n "$line" ]; then
pid=$(echo "$line" | awk '{print $2}')
r " KILL: stale pytest PID $pid"
kill "$pid" 2>/dev/null
zombies=$((zombies + 1))
fi
done < <(ps aux | grep "pytest tests/" | grep -v grep | while read -r l; do
pid=$(echo "$l" | awk '{print $2}')
et=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
[[ "$et" == *:*:* ]] && echo "$l"
done)
# Stuck git
while IFS= read -r line; do
if [ -n "$line" ]; then
pid=$(echo "$line" | awk '{print $2}')
r " KILL: stuck git PID $pid"
kill "$pid" 2>/dev/null
zombies=$((zombies + 1))
fi
done < <(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | while read -r l; do
pid=$(echo "$l" | awk '{print $2}')
et=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
[[ "$et" == *:*:* ]] && echo "$l"
done)
# Orphaned vi
vi_count=$(ps aux | grep "vi.*COMMIT_EDITMSG" | grep -v grep | wc -l | tr -d ' ')
if [ "$vi_count" -gt 0 ]; then
ps aux | grep "vi.*COMMIT_EDITMSG" | grep -v grep | awk '{print $2}' | xargs kill 2>/dev/null
r " KILL: $vi_count orphaned vi editors"
zombies=$((zombies + vi_count))
fi
# Count active hermes sessions
hermes_count=$(ps aux | grep "hermes" | grep python | grep -v grep | wc -l | tr -d ' ')
r " Active hermes sessions: $hermes_count"
r " Zombies killed: $zombies"
[ "$zombies" -gt 0 ] && findings=$((findings + 1))
r ""
# ── 2. Cron Job Status ───────────────────────────────────────────────
r "## 2. Cron Jobs"
python3 -c "
import json
data = json.load(open('$HOME/.hermes/cron/jobs.json'))
jobs = data.get('jobs', data) if isinstance(data, dict) else data
active = 0
for j in jobs:
if not isinstance(j, dict): continue
name = j.get('name', j.get('id','?'))
enabled = j.get('enabled', True)
paused = j.get('paused', False)
state = 'PAUSED' if (paused or not enabled) else 'ACTIVE'
sched = j.get('schedule', {})
mins = sched.get('minutes', '?') if isinstance(sched, dict) else '?'
if state == 'ACTIVE': active += 1
print(f' {state:7s} {name:40s} every {mins}m')
print(f'\n Total active: {active}')
" 2>/dev/null | while IFS= read -r line; do r "$line"; done
r ""
# ── 3. Stale Resources ───────────────────────────────────────────────
r "## 3. Stale Resources"
# Old worktrees
worktree_count=$(find ~/worktrees -maxdepth 1 -type d -mtime +3 2>/dev/null | wc -l | tr -d ' ')
if [ "$worktree_count" -gt 0 ]; then
r " Stale worktrees (>3 days): $worktree_count"
find ~/worktrees -maxdepth 1 -type d -mtime +3 -exec basename {} \; 2>/dev/null | while read -r w; do
r " $w"
done
findings=$((findings + 1))
else
r " Worktrees: clean"
fi
# Log disk usage
log_size=$(du -sh ~/.hermes/logs 2>/dev/null | awk '{print $1}')
r " Hermes logs: $log_size"
# Tmp files
tmp_size=$(du -sh /tmp/timmy-agents 2>/dev/null | awk '{print $1}')
r " /tmp/timmy-agents: ${tmp_size:-0}"
r ""
# ── 4. Plateau Detection ─────────────────────────────────────────────
r "## 4. Loop Plateau Analysis"
METRICS="$HOME/Timmy-Time-dashboard/.loop/cycle-metrics.jsonl"
if [ -f "$METRICS" ]; then
total_cycles=$(wc -l < "$METRICS" | tr -d ' ')
recent_zero=$(tail -5 "$METRICS" | python3 -c "
import sys, json
zero = sum(1 for l in sys.stdin if l.strip()
and json.loads(l).get('prs_merged',0) == 0
and json.loads(l).get('issues_filed',0) == 0)
print(zero)
" 2>/dev/null || echo "?")
r " Total cycles logged: $total_cycles"
r " Last 5 zero-output: $recent_zero/5"
if [ "$recent_zero" = "5" ]; then
r " STATUS: PLATEAUED — loop is spinning without producing value"
r " RECOMMENDATION: Increase interval or pause until new issues arrive"
findings=$((findings + 1))
elif [ "$recent_zero" -ge 3 ] 2>/dev/null; then
r " STATUS: APPROACHING PLATEAU"
else
r " STATUS: PRODUCTIVE"
fi
else
r " No cycle metrics yet."
fi
r ""
# ── 5. Gitea Health ──────────────────────────────────────────────────
r "## 5. Gitea Status"
if curl -s --max-time 5 "$GITEA_URL/api/v1/version" >/dev/null 2>&1; then
open_issues=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/rockachopa/Timmy-time-dashboard/issues?state=open&limit=1&type=issues" 2>/dev/null | \
python3 -c "import sys; print(len(__import__('json').loads(sys.stdin.read())))" 2>/dev/null || echo "?")
open_prs=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/rockachopa/Timmy-time-dashboard/pulls?state=open&limit=50" 2>/dev/null | \
python3 -c "import sys; print(len(__import__('json').loads(sys.stdin.read())))" 2>/dev/null || echo "?")
r " Gitea: ONLINE ($GITEA_URL)"
r " Open issues: $open_issues+"
r " Open PRs: $open_prs"
else
r " Gitea: OFFLINE"
findings=$((findings + 1))
fi
r ""
# ── 6. Recommendations ───────────────────────────────────────────────
r "## 6. Recommendations"
if [ "$findings" -eq 0 ]; then
r " All clear. No action needed."
else
r " $findings issue(s) found — see above."
fi
r ""
# ── Self-scheduling: plateau backoff ──────────────────────────────────
current_backoff=$(cat "$BACKOFF_MARKER" 2>/dev/null || echo 0)
if [ "$findings" -eq 0 ]; then
new_backoff=$((current_backoff + 1))
echo "$new_backoff" > "$BACKOFF_MARKER"
if [ "$new_backoff" -ge 3 ]; then
r "## Self-Schedule"
r " Audit backoff level: $new_backoff (no findings in $new_backoff consecutive runs)"
r " RECOMMENDATION: Extend audit interval to 24h or 48h"
fi
else
echo 0 > "$BACKOFF_MARKER"
fi
# ── Output ────────────────────────────────────────────────────────────
echo "$report" | tee "$AUDIT_LOG"
echo ""
echo "Audit saved to: $AUDIT_LOG"

321
bin/kimi-loop.sh Executable file
View File

@@ -0,0 +1,321 @@
#!/usr/bin/env bash
# kimi-loop.sh — Dropout-proof Kimi code agent dispatch loop
# Picks an open issue from Gitea, creates a worktree, runs Kimi Code CLI,
# handles failures gracefully, and loops forever.
#
# Dropout-proof means:
# - If Kimi Code crashes/hangs, we kill it and move on
# - If worktree creation fails, skip and retry
# - If push fails, log and continue
# - Exponential backoff on repeated failures
# - Clean up worktrees after PR is created
set -euo pipefail
# === CONFIG ===
REPO_DIR="$HOME/worktrees/kimi-repo"
WORKTREE_BASE="$HOME/worktrees"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/kimi_token")
REPO_OWNER="rockachopa"
REPO_NAME="Timmy-time-dashboard"
KIMI_TIMEOUT=600 # 10 min per issue
COOLDOWN=30 # seconds between issues
MAX_FAILURES=5 # consecutive failures before long sleep
LONG_SLEEP=300 # 5 min backoff on repeated failures
LOG_DIR="$HOME/.hermes/logs"
SKIP_FILE="$LOG_DIR/kimi-skip-list.json" # issues to skip temporarily
mkdir -p "$LOG_DIR" "$WORKTREE_BASE"
# Initialize skip file if missing
[ -f "$SKIP_FILE" ] || echo '{}' > "$SKIP_FILE"
# === STATE ===
failure_count=0
issues_completed=0
# === SKIP LIST FUNCTIONS ===
is_skipped() {
local issue_num="$1"
python3 -c "
import json, time, sys
try:
with open('$SKIP_FILE') as f: skips = json.load(f)
except: skips = {}
entry = skips.get(str($issue_num), {})
if entry and entry.get('until', 0) > time.time():
print('skip')
sys.exit(0)
# Expired or not found — clean up and allow
if str($issue_num) in skips:
del skips[str($issue_num)]
with open('$SKIP_FILE', 'w') as f: json.dump(skips, f)
print('ok')
" 2>/dev/null
}
mark_skip() {
local issue_num="$1"
local reason="$2"
local skip_hours="${3:-1}" # default 1 hour
python3 -c "
import json, time
try:
with open('$SKIP_FILE') as f: skips = json.load(f)
except: skips = {}
skips[str($issue_num)] = {
'until': time.time() + ($skip_hours * 3600),
'reason': '$reason',
'failures': skips.get(str($issue_num), {}).get('failures', 0) + 1
}
# If 3+ failures, skip for 6 hours instead
if skips[str($issue_num)]['failures'] >= 3:
skips[str($issue_num)]['until'] = time.time() + (6 * 3600)
with open('$SKIP_FILE', 'w') as f: json.dump(skips, f, indent=2)
" 2>/dev/null
log "SKIP: #${issue_num} added to skip list — ${reason}"
}
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >> "$LOG_DIR/kimi-loop.log"
}
cleanup_worktree() {
local wt="$1"
local branch="$2"
if [ -d "$wt" ]; then
cd "$REPO_DIR"
git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
git worktree prune 2>/dev/null
git branch -D "$branch" 2>/dev/null || true
log "Cleaned up worktree: $wt"
fi
}
get_next_issue() {
# Get open issues ASSIGNED TO KIMI only — Kimi works its own queue
# NOTE: Gitea's assignee filter is unreliable — we validate in Python
local skip_file="$SKIP_FILE"
curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open&type=issues&limit=50&sort=created" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys, json, time
issues = json.load(sys.stdin)
# Reverse to oldest-first (Gitea returns newest-first) — respects dependency order
issues.reverse()
# Load skip list
try:
with open('${skip_file}') as f: skips = json.load(f)
except: skips = {}
for i in issues:
# MUST be assigned to kimi (Gitea filter is broken, validate here)
assignees = [a['login'] for a in (i.get('assignees') or [])]
if 'kimi' not in assignees:
continue
title = i['title'].lower()
# Skip philosophy, epics, showcases, features (not 10-min code work)
if '[philosophy]' in title: continue
if '[epic]' in title or 'epic:' in title: continue
if '[showcase]' in title: continue
if '[feature]' in title: continue
# Check skip list
num_str = str(i['number'])
entry = skips.get(num_str, {})
if entry and entry.get('until', 0) > time.time():
continue
print(json.dumps({'number': i['number'], 'title': i['title']}))
sys.exit(0)
print('null')
" 2>/dev/null
}
build_prompt() {
local issue_num="$1"
local issue_title="$2"
local worktree="$3"
cat <<PROMPT
You are Kimi, an autonomous code agent on the Timmy-time-dashboard project.
YOUR ISSUE: #${issue_num} — "${issue_title}"
GITEA API: ${GITEA_URL}/api/v1
GITEA TOKEN: ${GITEA_TOKEN}
REPO: ${REPO_OWNER}/${REPO_NAME}
WORKING DIRECTORY: ${worktree}
== YOUR POWERS ==
You can do ANYTHING a developer can do. You are not limited to the narrow task.
1. READ the issue. Read any comments — they may have instructions.
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_num}"
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_num}/comments"
2. DO THE WORK. Code, test, fix, refactor — whatever the issue needs.
- tox -e format (auto-format first)
- tox -e unit (all tests must pass)
- tox -e lint (must be clean)
3. COMMIT with conventional commits: fix: / feat: / refactor: / test: / chore:
Include "Fixes #${issue_num}" or "Refs #${issue_num}" in the message.
4. PUSH to your branch (kimi/issue-${issue_num}) and CREATE A PR:
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"title": "[kimi] <description> (#${issue_num})", "body": "Fixes #${issue_num}\n\n<describe what you did>", "head": "kimi/issue-${issue_num}", "base": "main"}'
5. COMMENT on the issue when done:
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_num}/comments" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"body": "PR created. <summary of changes>"}'
6. FILE NEW ISSUES if you find bugs, missing tests, or improvements while working:
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"title": "[kimi-generated] <title>", "body": "<description>"}'
== RULES ==
- Read CLAUDE.md or project README first for conventions
- tox is the ONLY way to run tests/lint/format. Never run pytest/ruff directly.
- Never use --no-verify on git commands.
- If tests fail after 2 attempts, STOP and comment on the issue explaining why.
- Be thorough. If you see something broken nearby, file an issue for it.
PROMPT
}
# === MAIN LOOP ===
log "=== Kimi Loop Started ==="
log "Repo: ${REPO_DIR}"
log "Worktrees: ${WORKTREE_BASE}"
while true; do
# Check for too many consecutive failures
if [ "$failure_count" -ge "$MAX_FAILURES" ]; then
log "BACKOFF: ${failure_count} consecutive failures. Sleeping ${LONG_SLEEP}s..."
sleep "$LONG_SLEEP"
failure_count=0
fi
# Fetch latest main (resilient — never die on git errors)
cd "$REPO_DIR"
timeout 60 git fetch origin main 2>/dev/null || { log "WARN: git fetch failed, continuing anyway"; }
git checkout main 2>/dev/null || true
git reset --hard origin/main 2>/dev/null || true
# Get next issue
issue_json=$(get_next_issue)
if [ "$issue_json" = "null" ] || [ -z "$issue_json" ]; then
# Only log idle ONCE, then go quiet until work appears
if [ "${LAST_STATE:-}" != "idle" ]; then
log "Queue empty. Waiting for assignments..."
LAST_STATE="idle"
fi
sleep "$LONG_SLEEP"
continue
fi
LAST_STATE="working"
issue_num=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
issue_title=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['title'])")
branch="kimi/issue-${issue_num}"
worktree="${WORKTREE_BASE}/kimi-${issue_num}"
log "=== ISSUE #${issue_num}: ${issue_title} ==="
# Create worktree
if [ -d "$worktree" ]; then
log "Worktree already exists, cleaning..."
cleanup_worktree "$worktree" "$branch"
fi
cd "$REPO_DIR"
if ! git worktree add "$worktree" -b "$branch" origin/main 2>&1; then
log "ERROR: Failed to create worktree for #${issue_num}"
failure_count=$((failure_count + 1))
sleep "$COOLDOWN"
continue
fi
# Configure git remote with kimi's token so it can push
cd "$worktree"
git remote set-url origin "http://kimi:${GITEA_TOKEN}@143.198.27.163:3000/${REPO_OWNER}/${REPO_NAME}.git"
cd "$REPO_DIR"
# Build prompt
prompt=$(build_prompt "$issue_num" "$issue_title" "$worktree")
# Run Kimi Code CLI with timeout
log "Launching Kimi Code for #${issue_num} (timeout: ${KIMI_TIMEOUT}s)..."
set +e
cd "$worktree"
gtimeout "$KIMI_TIMEOUT" kimi \
--print \
--quiet \
-w "$worktree" \
-p "$prompt" \
</dev/null 2>&1 | tee "$LOG_DIR/kimi-${issue_num}.log"
exit_code=${PIPESTATUS[0]}
cd "$REPO_DIR"
set -e
if [ "$exit_code" -eq 0 ]; then
log "SUCCESS: #${issue_num} completed — attempting auto-merge..."
# Find and merge the PR kimi created
pr_num=$(curl -sf "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls?state=open&head=${REPO_OWNER}:${branch}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys,json
prs = json.load(sys.stdin)
if prs: print(prs[0]['number'])
else: print('')
" 2>/dev/null)
if [ -n "$pr_num" ]; then
merge_result=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' 2>&1) || true
log " PR #${pr_num} merge attempted"
# Close the issue (Gitea auto-close via "Fixes #N" is unreliable)
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log " Issue #${issue_num} closed"
else
log " WARN: No open PR found for branch ${branch}"
fi
failure_count=0
issues_completed=$((issues_completed + 1))
log "Stats: ${issues_completed} issues completed this session"
elif [ "$exit_code" -eq 124 ]; then
log "TIMEOUT: #${issue_num} exceeded ${KIMI_TIMEOUT}s"
mark_skip "$issue_num" "timeout" 1
failure_count=$((failure_count + 1))
else
log "FAILED: #${issue_num} exited with code ${exit_code}"
mark_skip "$issue_num" "exit_code_${exit_code}" 1
failure_count=$((failure_count + 1))
fi
# Clean up worktree
cleanup_worktree "$worktree" "$branch"
# Cooldown
log "Cooling down ${COOLDOWN}s before next issue..."
sleep "$COOLDOWN"
done