From 7d6df802d754c24d7a461e51e3abe01a5faed7d2 Mon Sep 17 00:00:00 2001 From: Timmy Time Date: Wed, 25 Mar 2026 14:41:30 +0000 Subject: [PATCH] =?UTF-8?q?remove=20deprecated=20agent-loop.sh=20=E2=80=94?= =?UTF-8?q?=20replaced=20by=20sovereign-orchestration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/agent-loop.sh | 373 ---------------------------------------------- 1 file changed, 373 deletions(-) delete mode 100755 bin/agent-loop.sh diff --git a/bin/agent-loop.sh b/bin/agent-loop.sh deleted file mode 100755 index 8fed9578..00000000 --- a/bin/agent-loop.sh +++ /dev/null @@ -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 [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 [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