2026-03-23 10:22:32 -04:00
#!/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 "
2026-04-05 19:33:36 +00:00
GITEA_URL = " ${ GITEA_URL :- https : //forge.alexanderwhitestone.com } "
2026-03-23 10:22:32 -04:00
GITEA_TOKEN = $( cat " $HOME /.config/gitea/token " 2>/dev/null)
CYCLE_INTERVAL = 300
2026-04-05 19:33:36 +00:00
HERMES_TIMEOUT = 300
2026-03-23 10:22:32 -04:00
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 "
}
2026-03-27 22:00:16 -04:00
REPOS = "Timmy_Foundation/hermes-agent Timmy_Foundation/the-nexus hermes/hermes-config Rockachopa/alexanderwhitestone.com"
2026-03-23 10:22:32 -04:00
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 "
2026-04-05 19:33:36 +00:00
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
2026-03-23 10:22:32 -04:00
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