remove deprecated agent-loop.sh — replaced by sovereign-orchestration

This commit is contained in:
2026-03-25 14:41:30 +00:00
parent d4c79d47a6
commit 7d6df802d7

View File

@@ -1,373 +0,0 @@
#!/usr/bin/env bash
# agent-loop.sh — Universal agent dev loop
# One script for all agents. Config via agent-specific .conf files.
#
# Usage: agent-loop.sh <agent-name> [num-workers]
# agent-loop.sh groq
# agent-loop.sh claude 10
# agent-loop.sh grok 1
set -uo pipefail
AGENT="${1:?Usage: agent-loop.sh <agent-name> [num-workers]}"
NUM_WORKERS="${2:-1}"
CONF="$HOME/.hermes/agents/${AGENT}.conf"
if [ ! -f "$CONF" ]; then
echo "No config at $CONF — create it first." >&2
exit 1
fi
# Load agent config
source "$CONF"
# === DEFAULTS (overridable in .conf) ===
: "${GITEA_URL:=http://143.198.27.163:3000}"
: "${WORKTREE_BASE:=$HOME/worktrees}"
: "${TIMEOUT:=600}"
: "${COOLDOWN:=30}"
: "${MAX_WORKERS:=10}"
: "${REPOS:=Timmy_Foundation/the-nexus rockachopa/hermes-agent}"
LOG_DIR="$HOME/.hermes/logs"
LOG="$LOG_DIR/${AGENT}-loop.log"
PIDFILE="$LOG_DIR/${AGENT}-loop.pid"
SKIP_FILE="$LOG_DIR/${AGENT}-skip-list.json"
LOCK_DIR="$LOG_DIR/${AGENT}-locks"
mkdir -p "$LOG_DIR" "$WORKTREE_BASE" "$LOCK_DIR"
[ -f "$SKIP_FILE" ] || echo '{}' > "$SKIP_FILE"
export BROWSER=echo # never open a browser
# === Single instance guard ===
if [ -f "$PIDFILE" ]; then
old_pid=$(cat "$PIDFILE")
if kill -0 "$old_pid" 2>/dev/null; then
echo "${AGENT} loop already running (PID $old_pid)" >&2
exit 0
fi
fi
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE"' EXIT
AGENT_UPPER=$(echo "$AGENT" | tr '[:lower:]' '[:upper:]')
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${AGENT_UPPER}: $*" >> "$LOG"
}
mark_skip() {
local issue_num="$1" reason="$2"
python3 -c "
import json, time, fcntl
with open('${SKIP_FILE}', 'r+') as f:
fcntl.flock(f, fcntl.LOCK_EX)
try: skips = json.load(f)
except: skips = {}
failures = skips.get(str($issue_num), {}).get('failures', 0) + 1
skip_hours = 6 if failures >= 3 else 1
skips[str($issue_num)] = {
'until': time.time() + (skip_hours * 3600),
'reason': '$reason', 'failures': failures
}
f.seek(0); f.truncate()
json.dump(skips, f, indent=2)
" 2>/dev/null
}
lock_issue() {
local key="$1"
mkdir "$LOCK_DIR/$key.lock" 2>/dev/null && echo $$ > "$LOCK_DIR/$key.lock/pid"
}
unlock_issue() {
rm -rf "$LOCK_DIR/$1.lock" 2>/dev/null
}
get_next_issue() {
python3 -c "
import json, sys, time, urllib.request, os
token = '${GITEA_TOKEN}'
base = '${GITEA_URL}'
repos = '${REPOS}'.split()
agent = '${AGENT}'
try:
with open('${SKIP_FILE}') as f: skips = json.load(f)
except: skips = {}
for repo in repos:
url = f'{base}/api/v1/repos/{repo}/issues?state=open&type=issues&limit=30&sort=created'
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
try:
resp = urllib.request.urlopen(req, timeout=10)
issues = json.loads(resp.read())
except: continue
for i in issues:
assignees = [a['login'] for a in (i.get('assignees') or [])]
if assignees and agent not in assignees: continue
title = i['title'].lower()
if '[epic]' in title or '[meta]' in title or '[audit]' in title: continue
num = str(i['number'])
entry = skips.get(num, {})
if entry and entry.get('until', 0) > time.time(): continue
lock = '${LOCK_DIR}/' + repo.replace('/','-') + '-' + num + '.lock'
if os.path.isdir(lock): continue
owner, name = repo.split('/')
if not assignees:
try:
data = json.dumps({'assignees': [agent]}).encode()
req2 = urllib.request.Request(
f'{base}/api/v1/repos/{repo}/issues/{i[\"number\"]}',
data=data, method='PATCH',
headers={'Authorization': f'token {token}', 'Content-Type': 'application/json'})
urllib.request.urlopen(req2, timeout=5)
except: pass
print(json.dumps({
'number': i['number'], 'title': i['title'],
'repo_owner': owner, 'repo_name': name, 'repo': repo}))
sys.exit(0)
print('null')
" 2>/dev/null
}
# === MERGE OWN PRs FIRST ===
merge_own_prs() {
# Before new work: find our open PRs, rebase if needed, merge them.
local open_prs
open_prs=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=20" 2>/dev/null | \
python3 -c "
import sys, json
prs = json.loads(sys.stdin.buffer.read())
ours = [p for p in prs if p['user']['login'] == '${AGENT}']
for p in ours:
print(f'{p[\"number\"]}|{p[\"head\"][\"ref\"]}|{p.get(\"mergeable\",False)}')
" 2>/dev/null)
[ -z "$open_prs" ] && return 0
local count=0
echo "$open_prs" | while IFS='|' read pr_num branch mergeable; do
[ -z "$pr_num" ] && continue
count=$((count + 1))
if [ "$mergeable" = "True" ]; then
# Try to squash merge directly
local result
result=$(curl -sf -w "%{http_code}" -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do":"squash","delete_branch_after_merge":true}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}/merge" 2>/dev/null)
local code="${result: -3}"
if [ "$code" = "200" ] || [ "$code" = "405" ]; then
log "MERGE: PR #${pr_num} merged"
else
log "MERGE: PR #${pr_num} merge failed (HTTP $code)"
fi
else
# Conflicts — clone, rebase, force push, then merge
local tmpdir="/tmp/${AGENT}-rebase-${pr_num}"
cd "$HOME"
rm -rf "$tmpdir" 2>/dev/null
local CLONE_URL="http://${AGENT}:${GITEA_TOKEN}@143.198.27.163:3000/Timmy_Foundation/the-nexus.git"
git clone -q --depth=50 -b "$branch" "$CLONE_URL" "$tmpdir" 2>/dev/null
if [ -d "$tmpdir/.git" ]; then
cd "$tmpdir"
git fetch origin main 2>/dev/null
if git rebase origin/main 2>/dev/null; then
git push -f origin "$branch" 2>/dev/null
log "REBASE: PR #${pr_num} rebased and pushed"
sleep 3
# Now try merge
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do":"squash","delete_branch_after_merge":true}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}/merge" 2>/dev/null
log "MERGE: PR #${pr_num} merged after rebase"
else
git rebase --abort 2>/dev/null
# Rebase impossible — close the PR, issue stays open for redo
curl -sf -X PATCH \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}" 2>/dev/null
log "CLOSE: PR #${pr_num} unrebaseable, closed"
fi
cd "$HOME"; rm -rf "$tmpdir"
fi
fi
sleep 2
done
return $count
}
# === WORKER FUNCTION ===
run_worker() {
local wid="$1"
log "WORKER-${wid}: started"
while true; do
# RULE: Merge existing PRs BEFORE creating new work.
merge_own_prs
local issue_json
issue_json=$(get_next_issue)
if [ "$issue_json" = "null" ] || [ -z "$issue_json" ]; then
sleep 120
continue
fi
local issue_num repo_owner repo_name repo branch workdir issue_key
issue_num=$(echo "$issue_json" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['number'])")
issue_title=$(echo "$issue_json" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['title'])")
repo_owner=$(echo "$issue_json" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['repo_owner'])")
repo_name=$(echo "$issue_json" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['repo_name'])")
repo="${repo_owner}/${repo_name}"
branch="${AGENT}/issue-${issue_num}"
workdir="${WORKTREE_BASE}/${AGENT}-w${wid}-${issue_num}"
issue_key="${repo_owner}-${repo_name}-${issue_num}"
lock_issue "$issue_key" || { sleep "$COOLDOWN"; continue; }
log "WORKER-${wid}: #${issue_num}${issue_title}"
# Clone
cd "$HOME"
rm -rf "$workdir" 2>/dev/null || true
local CLONE_URL="http://${AGENT}:${GITEA_TOKEN}@143.198.27.163:3000/${repo}.git"
if git ls-remote --heads "$CLONE_URL" "$branch" 2>/dev/null | grep -q "$branch"; then
git clone -q --depth=50 -b "$branch" "$CLONE_URL" "$workdir" 2>/dev/null
if [ -d "$workdir/.git" ]; then
cd "$workdir"
git fetch origin main 2>/dev/null
if ! git rebase origin/main 2>/dev/null; then
log "WORKER-${wid}: rebase failed, starting fresh"
cd "$HOME"; rm -rf "$workdir"
git clone -q --depth=1 -b main "$CLONE_URL" "$workdir" 2>/dev/null
cd "$workdir"; git checkout -b "$branch" 2>/dev/null
fi
fi
else
git clone -q --depth=1 -b main "$CLONE_URL" "$workdir" 2>/dev/null
cd "$workdir" 2>/dev/null && git checkout -b "$branch" 2>/dev/null
fi
if [ ! -d "$workdir/.git" ]; then
log "WORKER-${wid}: clone failed for #${issue_num}"
mark_skip "$issue_num" "clone_failed"
unlock_issue "$issue_key"
sleep "$COOLDOWN"; continue
fi
cd "$workdir"
# Read issue context
local issue_body issue_comments
issue_body=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${repo}/issues/${issue_num}" 2>/dev/null | \
python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('body',''))" 2>/dev/null || echo "")
issue_comments=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${repo}/issues/${issue_num}/comments" 2>/dev/null | \
python3 -c "
import sys,json
comments = json.loads(sys.stdin.read())
for c in comments[-3:]:
print(f'{c[\"user\"][\"login\"]}: {c[\"body\"][:150]}')
" 2>/dev/null || echo "")
# === RUN THE AGENT-SPECIFIC CLI ===
# This is the ONLY part that differs between agents.
# The run_agent function is defined in the .conf file.
run_agent "$issue_num" "$issue_title" "$issue_body" "$issue_comments" "$workdir" "$repo_owner" "$repo_name" "$branch"
# === COMMIT + PUSH (universal) ===
cd "$workdir" 2>/dev/null || { unlock_issue "$issue_key"; continue; }
git add -A 2>/dev/null
if ! git diff --cached --quiet 2>/dev/null; then
git commit -m "feat: ${issue_title} (#${issue_num})
Refs #${issue_num}
Agent: ${AGENT}" 2>/dev/null
fi
# Check for any local commits (agent may have committed directly)
local has_commits=false
if ! git diff --quiet HEAD origin/main 2>/dev/null; then
has_commits=true
fi
# Also check for new branch with no remote
git log --oneline -1 2>/dev/null | grep -q . && has_commits=true
if [ "$has_commits" = true ]; then
git push origin "$branch" 2>/dev/null || git push -f origin "$branch" 2>/dev/null || {
log "WORKER-${wid}: push failed for #${issue_num}"
mark_skip "$issue_num" "push_failed"
cd "$HOME"; rm -rf "$workdir"; unlock_issue "$issue_key"
sleep "$COOLDOWN"; continue
}
# Create or update PR
local existing_pr pr_num
existing_pr=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${repo}/pulls?state=open&head=${branch}&limit=1" 2>/dev/null | \
python3 -c "import sys,json; prs=json.loads(sys.stdin.read()); print(prs[0]['number'] if prs else '')" 2>/dev/null)
if [ -n "$existing_pr" ]; then
pr_num="$existing_pr"
log "WORKER-${wid}: updated PR #${pr_num}"
else
local pr_result
pr_result=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"title\": \"[${AGENT}] ${issue_title} (#${issue_num})\", \"body\": \"Refs #${issue_num}\n\nAgent: ${AGENT}\", \"head\": \"${branch}\", \"base\": \"main\"}" \
"${GITEA_URL}/api/v1/repos/${repo}/pulls" 2>/dev/null || echo "{}")
pr_num=$(echo "$pr_result" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('number','?'))" 2>/dev/null)
log "WORKER-${wid}: PR #${pr_num} created for #${issue_num}"
fi
# Only comment once per agent per issue — check before posting
existing_comment=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/${repo}/issues/${issue_num}/comments" 2>/dev/null \
| python3 -c "import sys,json; cs=json.loads(sys.stdin.read()); print('yes' if any('PR #' in c.get('body','') and '${AGENT}' in c.get('body','') for c in cs) else 'no')" 2>/dev/null)
if [ "$existing_comment" != "yes" ]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"PR #${pr_num}${AGENT}\"}" \
"${GITEA_URL}/api/v1/repos/${repo}/issues/${issue_num}/comments" >/dev/null 2>&1
fi
else
log "WORKER-${wid}: no changes for #${issue_num}"
mark_skip "$issue_num" "no_changes"
fi
cd "$HOME"; rm -rf "$workdir"
unlock_issue "$issue_key"
log "WORKER-${wid}: #${issue_num} complete"
sleep "$COOLDOWN"
done
}
# === MAIN ===
log "=== ${AGENT} loop started (PID $$, ${NUM_WORKERS} workers) ==="
if [ "$NUM_WORKERS" -gt 1 ]; then
for i in $(seq 1 "$NUM_WORKERS"); do
run_worker "$i" &
sleep 2
done
wait
else
run_worker 1
fi