{"repo": "buagi/ace", "files": {"lib/autoloop.sh": {"a17581ae52126f3a7a611c442a972cffbd13db59": {"t": 1785745959.4212105, "n": 165016, "h": "5a49a918dd829a88", "c": "#!/usr/bin/env bash\n# autoloop.sh — the ACE autonomous loop (SINGLE SOURCE). Run in a project cwd:\n# bash \"$ACE_DIR/lib/autoloop.sh\" (projects exec this via a thin scripts/auto-loop.sh)\n# Reads only cwd-relative project files: ROADMAP.md, OBJECTIVES.md, .opencode/*, ci.sh.\n# Autonomous PR loop. Watch CI; on failure feed the failed-job log to opencode to fix the\n# ROOT CAUSE; repeat until green; then implement the next ROADMAP item. Capped, no auto-merge.\n# MAX_FIX=5 MAX_FEATURES=3 AUTOMERGE=0 GENERATE_IDEAS=0 bash scripts/auto-loop.sh\nset -uo pipefail\n# shared-state coordination helpers (swarm_aggregate_lessons — S6 per-worker lessons reduce). Pure\n# function defs, safe to source; guarded so a missing module never breaks the single-flow loop.\n# shellcheck source=/dev/null\n. \"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/swarm-policy.sh\" 2>/dev/null || true\n# item 3: RED-main circuit breaker — driven via the swarm.sh CLI (autoloop does NOT source swarm.sh; this keeps\n# them decoupled). No-op outside a swarm (SWARM_WORKER unset) or if swarm.sh is absent. SWARM_DIR flows in via env.\n_SWARM_SH=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/swarm.sh\"\n_swarm(){ [ -n \"${SWARM_WORKER:-}\" ] && [ -f \"$_SWARM_SH\" ] && bash \"$_SWARM_SH\" \"$@\" 2>/dev/null; }\n# per-subagent token/cost telemetry from opencode's session DB (subagent_report / ace stats). Fail-soft.\n# shellcheck source=/dev/null\n. \"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/telemetry.sh\" 2>/dev/null || true\n. \"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)/consistency.sh\" 2>/dev/null || true # ace_repo_hygiene (startup git-cleanliness)\ncd \"$(git rev-parse --show-toplevel)\" || exit 1\n# delivery policy: defaults come from .opencode/profile.yaml; env still overrides per run.\nprof_get(){ grep -E \"^[[:space:]]*$1:[[:space:]]*\" .opencode/profile.yaml 2>/dev/null | head -1 | sed -E \"s/^[^:]*:[[:space:]]*\\\"([^\\\"]*)\\\".*$/\\1/; t; s/^[^:]*:[[:space:]]*'([^']*)'.*$/\\1/; t; s/^[^:]*:[[:space:]]*//; s/^#.*$//; s/[[:space:]]+#.*$//; s/[[:space:]]+$//\"; }\ncase \"$(prof_get auto_merge)\" in true|yes|1) _pam=1 ;; *) _pam=0 ;; esac\n_pmg=\"$(prof_get merge_gate)\"; case \"$_pmg\" in local|remote|both) : ;; *) _pmg=remote ;; esac\n_pcc=\"$(prof_get ci_cd)\" # ci_cd: github-actions | none — only watch Actions when the project actually uses it\nAGENT=\"${AGENT:-orchestrator}\"\nACE_TELEMETRY=\"${ACE_TELEMETRY:-1}\"; export ACE_TELEMETRY # 1 = full per-subagent × worker × run token/cost logging (default) · 0 = OFF (max throughput, nothing logged)\nMAX_FIX=\"${MAX_FIX:-5}\"; MAX_FEATURES=\"${MAX_FEATURES:-3}\"; MAX_PLANS=\"${MAX_PLANS:-5}\" # caps: CI-fix attempts/red · features/run (0=∞) · re-plan attempts before \"stuck\"\nAUTOMERGE=\"${AUTOMERGE:-$_pam}\"; PLAN=\"${PLAN:-1}\"; DEPLOY=\"${DEPLOY:-0}\"\nMERGE_GATE=\"${MERGE_GATE:-$_pmg}\" # remote = wait for Actions green · local = merge on ./ci.sh --container green · both = require local AND remote green\n# Don't watch GitHub Actions unless the project actually USES it: ci_cd != github-actions ⇒ local gate is the authority.\n# 'both' also degrades to 'local' when there's no remote CI to wait on (there's no remote half to require).\n{ [ \"$MERGE_GATE\" = remote ] || [ \"$MERGE_GATE\" = both ]; } && [ -n \"$_pcc\" ] && [ \"$_pcc\" != github-actions ] && { printf '[auto-loop] ci_cd=%s (no GitHub Actions) -> merge_gate=local (was %s); not watching Actions.\\n' \"$_pcc\" \"$MERGE_GATE\"; MERGE_GATE=local; }\nDEPLOY_KIND=\"${DEPLOY_KIND:-$(prof_get deploy_kind)}\"; DEPLOY_KIND=\"${DEPLOY_KIND:-service}\" # service|artifact|none — what to do after a merge\n# Overseer-on-Claude (subscription) limit policy: wait | cancel | deepseek\nON_CLAUDE_LIMIT=\"${ON_CLAUDE_LIMIT:-wait}\" # on a Claude/OpenAI cap: WAIT for reset on your model (default) — never auto-downgrade. wait|cancel|deepseek (deepseek is opt-in)\nCLAUDE_RESET_WAIT=\"${CLAUDE_RESET_WAIT:-21600}\" # keep polling this long (6h — rides through a sub's reset window) then STOP for review; NEVER auto-downgrades the model\nCLAUDE_POLL=\"${CLAUDE_POLL:-120}\" # poll interval while waiting — resumes within ~this long of a reset\nOPENCODE_TIMEOUT=\"${OPENCODE_TIMEOUT:-7200}\" # base per-step budget (s) = 2h. A real feature can legitimately take this long — don't fear big tasks. Progress RESETS the clock (a new commit → charged=0), so a task that keeps committing runs to OPENCODE_WALL_MAX; only a genuinely-stuck step overruns and becomes a BIG TASK (one bounded SLICE retry, BIGTASK_SLICE_RETRIES). A frozen step is still caught fast + cheap by HANG_AFTER, independent of this.\nexport OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS=\"${OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS:-300000}\" # E4: raise the inner bash cap to 5m for legit-long commands (opencode env, verified present on 1.17.x). The 120s streamText step ceiling (#25509) may STILL fire — so keep the FULL/--container build OUT of the inner agent loop (FAST ci.sh inner; FULL at the merge gate).\nBIGTASK_SLICE_RETRIES=\"${BIGTASK_SLICE_RETRIES:-1}\" # P0.2: once a step overruns, it's oversized — do at most this many SLICE attempts (each at the BASE budget, NOT escalating), then stop. Prevents one item eating 45→90→135m and still parking. A slice, by definition, fits the base budget. (Replaced the old escalating OPENCODE_RETRIES / OPENCODE_TIMEOUT_MAX.)\nHEARTBEAT=\"${HEARTBEAT:-60}\" # seconds between live \"still running\" elapsed/remaining ticks\nWATCH_POLL=\"${WATCH_POLL:-10}\" # seconds between activity samples (budget-accounting granularity)\nOPENCODE_WALL_MAX=\"${OPENCODE_WALL_MAX:-16200}\" # HARD wall-clock ceiling (s) per attempt = 4.5h — bounds a stuck step even while slow steps pause the budget clock. Accommodates a 2h base + a slice retry; HANG_AFTER still kills a frozen step far sooner.\nSTALL_AFTER=\"${STALL_AFTER:-900}\" # s of ZERO new output (and no build running) before the rathole supervisor judges the step\nHANG_AFTER=\"${HANG_AFTER:-480}\" # s of ZERO opencode log growth (stdout AND internal) → deterministic hang-kill (deadlocked step / stalled parallel subagents). 8m: a frozen process (no stdout, no tool log, nothing building) won't recover — don't wait the old 15m. Run 260716 lost ~4.5h to 18× 15m hangs.\nHANG_WARN=\"${HANG_WARN:-300}\" # s of the same zero-growth → an EARLY one-shot warning + swarm bus event BEFORE the kill at HANG_AFTER, so a freeze is surfaced in ~5m instead of only when the step is killed.\nRATHOLE_JUDGE=\"${RATHOLE_JUDGE:-deepseek-v4-flash}\" # cheap model the driver curls (hard-timed + fail-open) for a stuck-vs-progress verdict\nRATHOLE_RETRIES=\"${RATHOLE_RETRIES:-2}\" # max autonomous fix-and-retry attempts on a CONFIRMED rathole, then hard-stop (no infinite fixing)\nRATHOLE_MAXCHECKS=\"${RATHOLE_MAXCHECKS:-6}\" # circuit-breaker: max supervisor checks per step before it stops re-checking (wall cap takes over)\nACE_FIXME=\"${ACE_FIXME:-$HOME/.config/ace/ace-fixme.log}\" # queue: persistent ratholes get filed here for the issue-filing inception meta-loop\nSLOW_STEPS=\"${SLOW_STEPS:-podman buildah docker pnpm npm yarn pip pip3 poetry uv cargo rustc go gradle mvn make cmake ninja tsc turbo nx webpack vite rollup esbuild jest vitest pytest playwright cypress}\" # subprocess names whose run time is NOT charged to the task budget\nSERVER_PROCS=\"${SERVER_PROCS:-mcp|langserver|language-server|serena}\" # persistent MCP/LSP helpers launch under uv/npm (which ARE slow-steps) but run the WHOLE session — exclude them, else the budget clock freezes and both the BIG-TASK timeout and the rathole supervisor are defeated\nDEEPSEEK_OVERSEER=\"${DEEPSEEK_OVERSEER:-deepseek/deepseek-v4-pro}\" # model used when the overseer is delegated to DeepSeek\nORCH_MODEL_OVERRIDE=\"${ORCH_MODEL_OVERRIDE:-}\" # set to delegate the overseer (auto-set on a Claude limit)\nCI_STATE=\"${CI_STATE:-idle}\" # live gate state for the dash chip: idle|running|green|red (set at the gate choke points)\nWAITED=0 # cumulative seconds spent waiting on the current limit\n# Same resolution order as core.sh:4 (ACE_CONFIG_DIR) — XDG_CONFIG_HOME first, $HOME/.config only as the\n# fallback. Hardcoding ~/.config here meant that on an XDG-relocated host the loop read NO config at all:\n# orch_model() found neither MODEL_orchestrator nor ORCH_PROVIDER, silently fell back to Opus, and then\n# PRINTED that fallback in preflight/run-summary/loop-state as if it were the configured overseer.\nACE_CFG=\"${ACE_CFG:-${XDG_CONFIG_HOME:-$HOME/.config}/ace/config}\"\n# SHARED (cross-project) lessons store — same resolution order as core.sh's ACE_LESSONS_SHARED, and mirrored\n# here for the same reason orch_model is: autoloop.sh cannot source core.sh (core.sh installs its own\n# `trap cleanup EXIT`, which would silently REPLACE the loop's EXIT trap — A9), so the two files carry\n# matching definitions on purpose. Keep this line and lib/core.sh in step if either changes.\nACE_LESSONS_SHARED=\"${ACE_LESSONS_SHARED:-${XDG_CONFIG_HOME:-$HOME/.config}/ace/lessons.md}\"\nACE_LESSONS_SHARED_MAX=\"${ACE_LESSONS_SHARED_MAX:-60}\" # own cap: this file is read by EVERY project, so its bloat is paid N times\nEXPECT_REPO=\"${EXPECT_REPO:-}\" # optional owner/name guard for the loop\nVERIFY=\"${VERIFY:-0}\" # 1 => after each deploy, run 'ace verify' (triage findings -> ROADMAP)\nHARVEST=\"${HARVEST:-1}\" # 1 => after each GREEN merge, curate build WARNINGS the gate let through into ROADMAP\nHARVEST_MAX=\"${HARVEST_MAX:-15}\" # cap candidate warning lines fed to the curator (noise/cost guard)\nLOCAL_CI_FALLBACK=\"${LOCAL_CI_FALLBACK:-0}\" # 1 => when Actions is BLOCKED (billing/infra: run failed but ran 0 jobs), accept a GREEN local ./ci.sh --container as the pass and merge\nJANITOR_EVERY=\"${JANITOR_EVERY:-3}\" # run the per-lap consistency/disk janitor every Nth lap (1 = every lap); housekeeping, not correctness\nHERMES_NOTIFY=\"${HERMES_NOTIFY:-0}\" # 1 => push milestone events to Hermes (-> Telegram/phone) via `hermes send`; opt-in, fail-soft\nHERMES_TO=\"${HERMES_TO:-telegram}\" # hermes target: telegram | telegram: | discord | slack | ...\nHERMES_SUBJECT=\"${HERMES_SUBJECT:-}\" # optional notification prefix (defaults to repo:branch)\nRESOLVE_CONFLICTS=\"${RESOLVE_CONFLICTS:-1}\" # 1 => auto-resolve a conflicting PR (preserve both intents) vs stop\nMAX_CONFLICT=\"${MAX_CONFLICT:-2}\" # max conflict-resolution attempts per branch before stopping\nSTOP_ON_DEPLOY_FAIL=\"${STOP_ON_DEPLOY_FAIL:-1}\" # 1 => a failed VPS deploy/health-check HALTS the loop (don't ship more onto a broken live deploy); 0 => log + keep going\nRESOLVE_INSTR=\"The PR for the current branch CONFLICTS with main. Resolve it per your CONFLICT RESOLUTION protocol: merge origin/main into this branch, delegate to the conflict_resolver subagent (preserve BOTH intents; NO reverts to old; escalate UNRESOLVABLE), then have the reviewer confirm NO intended change was lost or reverted (APPROVE required). Ensure ./ci.sh GREEN, commit the merge, and push. If UNRESOLVABLE or any intended change would be lost, abort the merge and report — do NOT force it. NEVER merge the PR.\"\nMAINFIX_INSTR=\"The main branch is RED ON ITS OWN — its latest container build (./ci.sh --container) FAILS independent of your branch (another flow's merge broke it). You are the designated FIXER. Read .opencode/ci-failure.log, find the ROOT cause, and fix it MINIMALLY on this branch — do NOT revert unrelated work, do NOT implement new features, touch only what the failing build needs to go GREEN. Ensure ./ci.sh is GREEN, commit, and push so this branch lands the repair onto main. If the failure is infra / not code-fixable (billing/network/host), report and stop — do NOT force a merge.\"\nSELF_IMPROVE=\"${SELF_IMPROVE:-0}\" # 1 => when all objectives done, keep improving\nIMPROVE_GOAL=\"${IMPROVE_GOAL:-generate income · solve real user problems · professional, reliable UX}\" # the end goal self-improvement optimizes toward\n# ── structured, traceable telemetry (feeds `ace swarm dash` now + the web UI later) ──\n# CURRENT_PHASE is the coarse stage the loop is in (set by drive()); SWARM_WORKER /\n# SWARM_FEATURE / SWARM_HASH / SWARM_RUNID come from the coordinator's env.\nCURRENT_PHASE=\"${CURRENT_PHASE:-boot}\"\n_ev(){ # level msg → one tagged JSON event line onto the shared bus\n [ -n \"${SWARM_DIR:-}\" ] || return 0; command -v jq >/dev/null 2>&1 || return 0\n printf '{\"ts\":%s,\"run\":\"%s\",\"worker\":\"%s\",\"feat\":%s,\"hash\":\"%s\",\"phase\":\"%s\",\"agent\":\"%s\",\"level\":\"%s\",\"msg\":%s}\\n' \\\n \"$(date +%s)\" \"${SWARM_RUNID:-}\" \"${SWARM_WORKER:-solo}\" \\\n \"$(printf '%s' \"${SWARM_FEATURE:-}\" | jq -Rsc .)\" \"${SWARM_HASH:-}\" \"$CURRENT_PHASE\" \"${AGENT:-}\" \"$1\" \\\n \"$(printf '%s' \"$2\" | jq -Rsc .)\" >> \"$SWARM_DIR/events.jsonl\" 2>/dev/null || true\n}\n_stat(){ # phase wall budget act → per-worker snapshot the dash reads for the pipeline box\n [ -n \"${SWARM_DIR:-}\" ] || return 0\n mkdir -p \"$SWARM_DIR/status\" 2>/dev/null\n printf 'worker=%s\\nfeat=%s\\nhash=%s\\nphase=%s\\nagent=%s\\nwall=%s\\nbudget=%s\\nact=%s\\nts=%s\\n' \\\n \"${SWARM_WORKER:-solo}\" \"${SWARM_FEATURE:-}\" \"${SWARM_HASH:-}\" \"$1\" \"${AGENT:-}\" \"$2\" \"$3\" \"$4\" \"$(date +%s)\" \\\n > \"$SWARM_DIR/status/${SWARM_WORKER:-solo}.stat\" 2>/dev/null || true\n}\n# Colour policy for the loop's OWN narration. The auto-loop is the most-redirected component in the\n# product — it runs under nohup, as N swarm workers into per-worker logs, gets tailed by `ace dash`, and\n# is captured wholesale by the eval harness — so hardcoded ANSI escapes ended up baked into every one of\n# those files (and into anything that greps them). Resolve the decision ONCE, at source time, honouring\n# the two standard signals: NO_COLOR set to anything at all (no-color.org) and a non-TTY stdout.\n# ACE_FORCE_COLOR=1 wins over both, so the dashboards/snapshots that deliberately capture colour off-TTY\n# keep working. Same contract as lib/ui.sh:9 — one rule for the whole product.\nACE_LOOP_COLOR=0\n{ [ -t 1 ] || [ \"${ACE_FORCE_COLOR:-0}\" = 1 ]; } && [ -z \"${NO_COLOR:-}\" ] && ACE_LOOP_COLOR=1\nif [ \"$ACE_LOOP_COLOR\" = 1 ]; then _LC_HDR=$'\\033[1;35m'; _LC_RST=$'\\033[0m'\nelse _LC_HDR=''; _LC_RST=''; fi\n\n_wtag(){ # stable per-worker colour (any N) so workers are eye-trackable in tail/split\n # plain tag when colour is off — a redirected swarm log stays greppable instead of carrying escapes\n [ \"$ACE_LOOP_COLOR\" = 1 ] || { printf '[worker %s · %s] ' \"${SWARM_WORKER#w}\" \"${SWARM_FEATURE:-?}\"; return; }\n local wc; case \"${SWARM_WORKER#w}\" in 1) wc='38;2;176;114;230';; 2) wc='38;2;212;160;74';; 3) wc='38;2;63;185;106';;\n 4) wc='38;2;208;80;70';; 5) wc='38;2;90;190;210';; 6) wc='38;2;110;150;230';;\n *) wc=\"$(awk -v n=\"${SWARM_WORKER#w}\" 'BEGIN{ h=n*137.508; h=h-int(h/360)*360; s=0.55; v=0.92; c=v*s; hp=h/60;\n hh=hp-int(hp/2)*2; if(hh<0)hh+=2; x=c*(1-(hh>1?hh-1:1-hh)); m=v-c;\n if(hp<1){r=c;g=x;b=0}else if(hp<2){r=x;g=c;b=0}else if(hp<3){r=0;g=c;b=x}\n else if(hp<4){r=0;g=x;b=c}else if(hp<5){r=x;g=0;b=c}else{r=c;g=0;b=x}\n printf \"38;2;%d;%d;%d\",(r+m)*255,(g+m)*255,(b+m)*255 }')\" ;;\n esac\n printf '\\033[%sm[worker %s · %s]\\033[0m ' \"$wc\" \"${SWARM_WORKER#w}\" \"${SWARM_FEATURE:-?}\"; }\nsay(){ local tag=\"\"; [ -n \"${SWARM_WORKER:-}\" ] && tag=\"$(_wtag)\"\n printf '\\n%s[auto-loop %s]%s %b%s\\n' \"$_LC_HDR\" \"$(date +%H:%M:%S)\" \"$_LC_RST\" \"$tag\" \"$*\"; _ev \"${_LVL:-info}\" \"$*\";\n}\n\n# phase