Spaces:
Runtime error
Runtime error
Ashira Pitchayapakayakul
fix: 8 shards (was 4) + round-robin axentx projects + 3min cooldown
8645e42 | # 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 | |