surrogate-1 / bin /auto-orchestrate-loop.sh
Ashira Pitchayapakayakul
fix: 8 shards (was 4) + round-robin axentx projects + 3min cooldown
8645e42
#!/usr/bin/env bash
# Auto-orchestrate loop β€” fires SA β†’ architect β†’ qa-tdd β†’ dev β†’ qa-verify β†’ reviewer chain.
#
# Strategy: pick a real TODO/FIXME from any axentx project, run the full pipeline,
# auto-commit on APPROVE. Runs every 20 min via cron.
# Pairs with surrogate-dev-loop (light/fast); this one does heavy multi-stage work.
#
# Linux + macOS compatible (auto-detects coreutils variants).
set -uo pipefail
set -a; source "$HOME/.hermes/.env" 2>/dev/null; set +a
LOG="$HOME/.surrogate/logs/auto-orchestrate-loop.log"
mkdir -p "$(dirname "$LOG")"
# ── Resource guard ──────────────────────────────────────────────────────────
# HF Space CPU has spiky load avg from ollama pulls + concurrent scrape workers.
# load >50 = real saturation; free_mb <100 = OOM risk.
# Previous threshold (load>8) was paused 90% of time during model pulls β€” too aggressive.
LOAD=$(uptime | sed -E 's/.*load average[s]?:[[:space:]]*//' | awk -F',' '{print int($1)}')
if [[ -r /proc/meminfo ]]; then
FREE_MB=$(awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo)
elif command -v vm_stat >/dev/null 2>&1; then
FREE_MB=$(vm_stat | awk '/Pages free/{gsub("[.]","",$3); printf "%d", ($3*16384)/1048576}')
else
FREE_MB=999
fi
if [[ ${LOAD:-0} -gt 50 ]] || [[ ${FREE_MB:-999} -lt 100 ]]; then
echo "[$(date +%H:%M:%S)] resource-pause: load=$LOAD free_mb=$FREE_MB β€” skip" >> "$LOG"
exit 0
fi
# ── Pick a real task: one TODO/FIXME from a randomly-chosen axentx project ──
TASK_INFO=$(python3 <<'PYEOF'
import os, random, re, subprocess, json
from pathlib import Path
# Real paths (verified via api.github.com 2026-04-28)
PROJECTS = [
Path.home() / 'axentx/Costinel',
Path.home() / 'axentx/vanguard',
Path.home() / 'axentx/arkship',
Path.home() / 'axentx/surrogate',
Path.home() / 'axentx/workio',
Path.home() / 'axentx/hermes-toolbelt',
]
PROJECTS = [p for p in PROJECTS if (p/'.git').exists()]
if not PROJECTS:
print("{}"); exit()
# ROUND-ROBIN across projects (instead of random.shuffle which kept hitting same repo).
# Persistent counter at ~/.surrogate/state/orchestrate-project-cursor β€” increments each run.
# Result: every 6 runs covers all 6 axentx repos evenly.
cursor_file = Path.home() / '.surrogate/state/orchestrate-project-cursor'
cursor_file.parent.mkdir(parents=True, exist_ok=True)
try:
cursor = int(cursor_file.read_text().strip())
except Exception:
cursor = 0
PROJECTS = sorted(PROJECTS, key=lambda p: p.name) # stable order
cursor = cursor % len(PROJECTS)
# Rotate so current cursor's project is first
PROJECTS = PROJECTS[cursor:] + PROJECTS[:cursor]
cursor_file.write_text(str((cursor + 1) % len(PROJECTS)))
for proj in PROJECTS:
cmd = ['rg', '--no-heading', '-n', '-m', '5',
'--type', 'py', '--type', 'ts', '--type', 'go', '--type', 'sh',
'-g', '!node_modules', '-g', '!.venv', '-g', '!__pycache__',
'-g', '!.git', '-g', '!dist', '-g', '!build',
r'(TODO|FIXME)[:\s]', str(proj)]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=8)
lines = [l for l in r.stdout.splitlines() if l.strip()]
if not lines: continue
line = random.choice(lines)
m = re.match(r'^([^:]+):(\d+):(.+)$', line)
if not m: continue
path, lineno, content = m.groups()
rel = os.path.relpath(path, proj)
c = content.strip().lower()
if any(skip in c for skip in ['#todo:', 'todo: fix', 'todo:', '// todo', 'todo()']) and len(content) < 30:
continue
print(json.dumps({
'project': str(proj),
'project_name': proj.name,
'file': rel,
'line': int(lineno),
'content': content.strip()[:300],
}))
exit()
except Exception:
continue
print("{}")
PYEOF
)
if [[ -z "$TASK_INFO" ]] || [[ "$TASK_INFO" == "{}" ]]; then
echo "[$(date +%H:%M:%S)] no task found β€” skip" >> "$LOG"
exit 0
fi
PROJECT=$(echo "$TASK_INFO" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['project'])")
PROJ_NAME=$(echo "$TASK_INFO" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['project_name'])")
FILE=$(echo "$TASK_INFO" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['file'])")
LINE=$(echo "$TASK_INFO" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['line'])")
CONTENT=$(echo "$TASK_INFO" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['content'])")
# ── Per-task throttle: don't redo same TODO within 4 hours ─────────────────
# md5 (macOS) vs md5sum (Linux); stat -f%m (macOS) vs stat -c%Y (Linux)
if command -v md5sum >/dev/null 2>&1; then
TASK_HASH=$(echo "${PROJ_NAME}:${FILE}:${LINE}" | md5sum | cut -c1-12)
else
TASK_HASH=$(echo "${PROJ_NAME}:${FILE}:${LINE}" | md5 | cut -c1-12)
fi
LOCK_DIR="$HOME/.hermes/workspace/auto-orchestrate-locks"
mkdir -p "$LOCK_DIR"
LOCK="$LOCK_DIR/${TASK_HASH}"
if [[ -f "$LOCK" ]]; then
if stat -c %Y "$LOCK" >/dev/null 2>&1; then
LOCK_TS=$(stat -c %Y "$LOCK")
else
LOCK_TS=$(stat -f %m "$LOCK" 2>/dev/null || echo 0)
fi
AGE=$(( $(date +%s) - LOCK_TS ))
if [[ $AGE -lt 14400 ]]; then
echo "[$(date +%H:%M:%S)] task ${TASK_HASH} done ${AGE}s ago β€” skip" >> "$LOG"
exit 0
fi
fi
touch "$LOCK"
# ── Run orchestrate (auto-commits on APPROVE) ──────────────────────────────
START=$(date +%s)
echo "[$(date +%H:%M:%S)] orchestrate start: $PROJ_NAME/$FILE:$LINE" >> "$LOG"
echo " task: $CONTENT" >> "$LOG"
TASK_DESC="Resolve this TODO/FIXME in $PROJ_NAME at $FILE:$LINE: \"$CONTENT\". Implement a real fix (not stub), keep changes scoped to the file/function. Match existing code style."
cd "$PROJECT" || { echo "[$(date +%H:%M:%S)] cd failed" >> "$LOG"; exit 1; }
bash "$HOME/.surrogate/bin/surrogate-orchestrate.sh" "$TASK_DESC" >> "$LOG" 2>&1
RC=$?
DUR=$(( $(date +%s) - START ))
echo "[$(date +%H:%M:%S)] orchestrate done in ${DUR}s rc=$RC" >> "$LOG"
# ── Push to GitHub if commit was created ───────────────────────────────────
if [[ $RC -eq 0 ]]; then
LATEST_COMMIT=$(git -C "$PROJECT" log -1 --format=%H 2>/dev/null)
LATEST_AGE=$(( $(date +%s) - $(git -C "$PROJECT" log -1 --format=%ct 2>/dev/null || echo 0) ))
if [[ $LATEST_AGE -lt 600 ]]; then # commit within last 10 min = was just made
if git -C "$PROJECT" push origin HEAD:main >> "$LOG" 2>&1; then
echo "[$(date +%H:%M:%S)] βœ… pushed $LATEST_COMMIT to $PROJ_NAME" >> "$LOG"
else
echo "[$(date +%H:%M:%S)] ⚠ push failed for $PROJ_NAME" >> "$LOG"
fi
fi
fi
# ── Discord notification ───────────────────────────────────────────────────
NOTIFY="$HOME/.surrogate/bin/notify-discord.sh"
if [[ -x "$NOTIFY" ]]; then
if [[ $RC -eq 0 ]]; then
"$NOTIFY" task "Auto-orchestrate: $PROJ_NAME" "$FILE:$LINE β€” \`$(echo "$CONTENT" | head -c 80)\` Β· ${DUR}s" 2>/dev/null &
else
"$NOTIFY" warn "Auto-orchestrate failed" "$PROJ_NAME Β· $FILE:$LINE Β· rc=$RC Β· ${DUR}s" 2>/dev/null &
fi
fi