remove deprecated agent-loop.sh — replaced by sovereign-orchestration
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user