#!/usr/bin/env bash # timmy-orchestrator.sh — Timmy's orchestration loop # Uses hermes (local Ollama) to triage, assign, review, and merge. # Timmy is the brain. Claude/Gemini/Kimi are the hands. set -uo pipefail LOG_DIR="$HOME/.hermes/logs" LOG="$LOG_DIR/timmy-orchestrator.log" PIDFILE="$LOG_DIR/timmy-orchestrator.pid" GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}" GITEA_TOKEN=$(cat "$HOME/.config/gitea/token" 2>/dev/null) CYCLE_INTERVAL=300 HERMES_TIMEOUT=300 mkdir -p "$LOG_DIR" # Single instance guard if [ -f "$PIDFILE" ]; then old_pid=$(cat "$PIDFILE") if kill -0 "$old_pid" 2>/dev/null; then echo "Timmy already running (PID $old_pid)" >&2 exit 0 fi fi echo $$ > "$PIDFILE" trap 'rm -f "$PIDFILE"' EXIT log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] TIMMY: $*" >> "$LOG" } REPOS="Timmy_Foundation/hermes-agent Timmy_Foundation/the-nexus hermes/hermes-config Rockachopa/alexanderwhitestone.com" gather_state() { local state_dir="/tmp/timmy-state-$$" mkdir -p "$state_dir" > "$state_dir/unassigned.txt" > "$state_dir/open_prs.txt" > "$state_dir/agent_status.txt" for repo in $REPOS; do local short=$(echo "$repo" | cut -d/ -f2) # Unassigned issues curl -sf -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/$repo/issues?state=open&type=issues&limit=50" 2>/dev/null | \ python3 -c " import sys,json for i in json.load(sys.stdin): if not i.get('assignees'): print(f'REPO={\"$repo\"} NUM={i[\"number\"]} TITLE={i[\"title\"]}')" >> "$state_dir/unassigned.txt" 2>/dev/null # Open PRs curl -sf -H "Authorization: token $GITEA_TOKEN" \ "$GITEA_URL/api/v1/repos/$repo/pulls?state=open&limit=30" 2>/dev/null | \ python3 -c " import sys,json for p in json.load(sys.stdin): print(f'REPO={\"$repo\"} PR={p[\"number\"]} BY={p[\"user\"][\"login\"]} TITLE={p[\"title\"]}')" >> "$state_dir/open_prs.txt" 2>/dev/null done echo "Claude workers: $(pgrep -f 'claude.*--print.*--dangerously' 2>/dev/null | wc -l | tr -d ' ')" >> "$state_dir/agent_status.txt" echo "Claude loop: $(pgrep -f 'claude-loop.sh' 2>/dev/null | wc -l | tr -d ' ') procs" >> "$state_dir/agent_status.txt" tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" | xargs -I{} echo "Recent successes: {}" >> "$state_dir/agent_status.txt" tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "FAILED" | xargs -I{} echo "Recent failures: {}" >> "$state_dir/agent_status.txt" echo "$state_dir" } run_triage() { local state_dir="$1" local unassigned_count=$(wc -l < "$state_dir/unassigned.txt" | tr -d ' ') local pr_count=$(wc -l < "$state_dir/open_prs.txt" | tr -d ' ') log "Cycle: $unassigned_count unassigned, $pr_count open PRs" # If nothing to do, skip the LLM call if [ "$unassigned_count" -eq 0 ] && [ "$pr_count" -eq 0 ]; then log "Nothing to triage" return fi # Phase 1: Bulk-assign unassigned issues to claude (no LLM needed) if [ "$unassigned_count" -gt 0 ]; then log "Assigning $unassigned_count issues to claude..." while IFS= read -r line; do local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/') local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*/\1/') curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$repo/issues/$num" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d '{"assignees":["claude"]}' >/dev/null 2>&1 && \ log " Assigned #$num ($repo) to claude" done < "$state_dir/unassigned.txt" fi # Phase 2: PR review via Timmy (LLM) if [ "$pr_count" -gt 0 ]; then run_pr_review "$state_dir" fi } run_pr_review() { local state_dir="$1" local prompt_file="/tmp/timmy-prompt-$$.txt" # Build a review prompt listing all open PRs cat > "$prompt_file" <<'HEADER' You are Timmy, the orchestrator. Review these open PRs from AI agents. For each PR, you will see the diff. Your job: - MERGE if changes look reasonable (most agent PRs are good, merge aggressively) - COMMENT if there is a clear problem - CLOSE if it is a duplicate or garbage Use these exact curl patterns (replace REPO, NUM): Merge: curl -sf -X POST "GITEA/api/v1/repos/REPO/pulls/NUM/merge" -H "Authorization: token TOKEN" -H "Content-Type: application/json" -d '{"Do":"squash"}' Comment: curl -sf -X POST "GITEA/api/v1/repos/REPO/pulls/NUM/comments" -H "Authorization: token TOKEN" -H "Content-Type: application/json" -d '{"body":"feedback"}' Close: curl -sf -X PATCH "GITEA/api/v1/repos/REPO/pulls/NUM" -H "Authorization: token TOKEN" -H "Content-Type: application/json" -d '{"state":"closed"}' HEADER # Replace placeholders sed -i '' "s|GITEA|$GITEA_URL|g; s|TOKEN|$GITEA_TOKEN|g" "$prompt_file" # Add each PR with its diff (up to 10 PRs per cycle) local count=0 while IFS= read -r line && [ "$count" -lt 10 ]; do local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/') local pr_num=$(echo "$line" | sed 's/.*PR=\([^ ]*\).*/\1/') local by=$(echo "$line" | sed 's/.*BY=\([^ ]*\).*/\1/') local title=$(echo "$line" | sed 's/.*TITLE=//') [ -z "$pr_num" ] && continue local diff diff=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \ -H "Accept: application/diff" \ "$GITEA_URL/api/v1/repos/$repo/pulls/$pr_num" 2>/dev/null | head -150) [ -z "$diff" ] && continue echo "" >> "$prompt_file" echo "=== PR #$pr_num in $repo by $by ===" >> "$prompt_file" echo "Title: $title" >> "$prompt_file" echo "Diff (first 150 lines):" >> "$prompt_file" echo "$diff" >> "$prompt_file" echo "=== END PR #$pr_num ===" >> "$prompt_file" count=$((count + 1)) done < "$state_dir/open_prs.txt" if [ "$count" -eq 0 ]; then rm -f "$prompt_file" return fi echo "" >> "$prompt_file" cat >> "$prompt_file" <<'FOOTER' INSTRUCTIONS: For EACH PR above, do ONE of the following RIGHT NOW using your terminal tool: - Run the merge curl command if the diff looks good - Run the close curl command if it is a duplicate or garbage - Run the comment curl command only if there is a clear bug IMPORTANT: Actually run the curl commands in your terminal. Do not just describe what you would do. Most PRs from agents are fine — merge aggressively. Output only the commands you ran and their results. FOOTER local prompt_text prompt_text=$(cat "$prompt_file") rm -f "$prompt_file" log "Reviewing $count PRs..." local result result=$(timeout "$HERMES_TIMEOUT" hermes chat -q "$prompt_text" -Q --yolo 2>&1) local exit_code=$? if [ "$exit_code" -eq 0 ]; then log "PR review complete" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $result" >> "$LOG_DIR/timmy-reviews.log" else log "PR review failed (exit $exit_code)" fi } # === MAIN LOOP === log "=== Timmy Orchestrator Started (PID $$) ===" log "Model: qwen3:30b via Ollama | Cycle: ${CYCLE_INTERVAL}s" while true; do state_dir=$(gather_state) run_triage "$state_dir" rm -rf "$state_dir" log "Sleeping ${CYCLE_INTERVAL}s" sleep "$CYCLE_INTERVAL" done