| description | An iterative optimization loop inspired by Karpathy's Autoresearch and Claude Code's /loop. Runs on a configurable schedule to autonomously improve a target artifact toward a measurable goal. Each iteration: reads the program definition, proposes a change, evaluates against a metric, and accepts or rejects the change. - User defines the optimization goal and evaluation criteria in a program.md file - Accepts changes only when they improve the metric (ratchet pattern) - Persists all state via repo-memory (human-readable, human-editable) - Commits accepted improvements to a long-running branch per program - Maintains a single draft PR per program that accumulates all accepted iterations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| true |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| permissions | read-all | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout-minutes | 45 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| network |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| safe-outputs |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| checkout |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imports |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| source | githubnext/autoloop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine | copilot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| features |
|
An iterative optimization agent that proposes changes, evaluates them against a metric, and keeps only improvements — running autonomously on a schedule.
Take heed of instructions: "${{ steps.sanitized.outputs.text }}"
If these are non-empty (not ""), then you have been triggered via /autoloop <instructions>. The instructions may be:
- A one-off directive targeting a specific program: e.g.,
/autoloop training: try a different approach to the loss function. The text before the colon is the program name (matching a directory in.autoloop/programs/or an issue with theautoloop-programlabel). Execute it as a single iteration for that program, then report results. - A general directive: e.g.,
/autoloop try cosine annealing. If no program name prefix is given and only one program exists, use that one. If multiple exist, ask which program to target. - A configuration change: e.g.,
/autoloop training: set metric to accuracy instead of loss. Update the relevant program file and confirm.
Then exit — do not run the normal loop after completing the instructions.
Autoloop supports three program layouts:
Each program is a directory under .autoloop/programs/ containing a program.md and all related code:
.autoloop/programs/
├── function_minimization/
│ ├── program.md ← program definition (goal, target, evaluation)
│ └── code/ ← code files the agent optimizes
│ ├── initial_program.py
│ ├── evaluator.py
│ ├── config.yaml
│ └── requirements.txt
├── signal_processing/
│ ├── program.md
│ └── code/
│ ├── initial_program.py
│ ├── evaluator.py
│ ├── config.yaml
│ └── requirements.txt
The program name is the directory name (e.g., function_minimization).
For simpler programs that don't need their own code directory:
.autoloop/programs/
├── coverage.md
└── build-perf.md
The program name is the filename without .md.
Programs can also be defined as GitHub issues with the autoloop-program label. The issue body uses the same format as a program.md file (with Goal, Target, and Evaluation sections). The program name is derived from the issue title (slugified to lowercase with hyphens).
The pre-step fetches open issues with the autoloop-program label via the GitHub API and writes each issue body to a temporary file for scheduling. Issue-based programs participate in the same scheduling and selection logic as file-based programs.
When a program is issue-based, /tmp/gh-aw/autoloop.json includes:
selected_issue: The issue number (e.g.,42) if the selected program came from an issue, ornullif it came from a file.issue_programs: A mapping of program name → issue number for all issue-based programs found.
The pre-step has already determined which program to run. Read /tmp/gh-aw/autoloop.json at the start of your run to get:
selected: The single program name to run this iteration, ornullif none are due.selected_file: The full path to the program's markdown file (either.autoloop/programs/<name>/program.md,.autoloop/programs/<name>.md, or/tmp/gh-aw/issue-programs/<name>.mdfor issue-based programs).selected_issue: The GitHub issue number if the selected program came from an issue, ornullif it came from a file.selected_target_metric: Thetarget-metricvalue from the program's frontmatter (a number), ornullif the program is open-ended. Used to check the halting condition after each accepted iteration.selected_metric_direction: One of"higher"(default) or"lower", parsed from the program'smetric_directionfrontmatter field. Determines whether larger or smaller metric values count as improvement. Used by the metric-improved check in Step 5, the iteration-history delta sign, and the halting condition.state_file_size_bytes: Current size of the selected program's state file in bytes (0 if it does not exist yet). Use this together withstate_file_max_bytesto decide whether to compact aggressively this iteration (see Update Rules — when size exceeds 80% of the max, collapse older iteration entries).state_file_max_bytes: The configuredmax-file-sizefor repo-memory state files (default30720, i.e. 30 KB). Files larger than this are rejected by repo-memory, breaking scheduling.issue_programs: A mapping of program name → issue number for all discovered issue-based programs.deferred: Other programs that were due but will be handled in future runs.unconfigured: Programs that still have the sentinel or placeholder content.skipped: Programs not due yet based on their per-program schedule.no_programs: Iftrue, no program files exist at all.not_due: Iftrue, programs exist but none are due for this run.head_branch: The canonical long-running branch name for the selected program — always exactlyautoloop/{program-name}, never with a suffix or hash. Use this value verbatim when creating, checking out, or pushing to the branch.existing_pr: The number of the open draft PR forautoloop/{program-name}, ornullif no PR exists yet. Use this to enforce the single-PR-per-program invariant — see Step 5a: Push and wait for CI and Step 5c: Accept.
If selected is not null:
- Read the program file from the
selected_filepath. - Parse the three sections: Goal, Target, Evaluation.
- Read the current state of all target files.
- Read the state file
{selected}.mdfrom the repo-memory folder for all state: the ⚙️ Machine State table (scheduling fields) plus the research sections (priorities, lessons, foreclosed avenues, iteration history). - If
selected_issueis not null, this is an issue-based program — also read the issue comments for any human steering input.
Autoloop supports multiple independent optimization loops in the same repository. Each loop is defined by a directory in .autoloop/programs/, a markdown file in .autoloop/programs/, or a GitHub issue with the autoloop-program label. For example:
.autoloop/programs/
├── function_minimization/ ← optimize search algorithm
│ ├── program.md
│ └── code/
├── signal_processing/ ← optimize signal filter
│ ├── program.md
│ └── code/
├── coverage.md ← maximize test coverage
└── build-perf.md ← minimize build time
GitHub Issues (labeled 'autoloop-program'):
├── Issue #5: "Reduce Latency" ← optimize API response time
└── Issue #8: "Improve Accuracy" ← optimize model accuracy
Each program runs independently with its own:
- Goal, target files, and evaluation command
- Metric tracking and best-metric history
- Program issue:
[Autoloop: {program-name}](a single GitHub issue labeledautoloop-program— created automatically for file-based programs, the source issue for issue-based programs — that hosts the status comment, per-iteration comments, and human steering) - Long-running branch:
autoloop/{program-name}(persists across iterations) - Single draft PR per program:
[Autoloop: {program-name}](accumulates all accepted iterations) - State file:
{program-name}.mdin repo-memory (all state: scheduling, research context, iteration history)
One program per run: On each scheduled trigger, a lightweight pre-step checks which programs are due and selects the single most-overdue program (oldest last_run, with never-run programs first). The agent runs one iteration for that program only.
Programs can optionally specify their own schedule in a YAML frontmatter block:
---
schedule: every 1h
---
# Autoloop Program
...Programs can optionally specify a target-metric in the frontmatter to define a halting condition. When the metric reaches or surpasses the target (in the direction set by metric_direction), the program is automatically completed: the autoloop-program label is removed and an autoloop-completed label is added (for issue-based programs), and the state file is marked Completed: true.
Programs without a target-metric are open-ended and run indefinitely until manually stopped.
---
schedule: every 6h
target-metric: 0.95
---
# Autoloop Program
...By default Autoloop assumes higher is better — best_metric is ratcheted up each accepted iteration, and a target-metric is met when best_metric >= target-metric. Programs whose natural fitness is lower is better (error, latency, cost, ratio, fitness score) can opt into reversed semantics with the optional metric_direction field:
---
schedule: every 6h
metric_direction: lower # defaults to "higher" if omitted
target-metric: 0.9 # interpreted as "program is complete when best_metric ≤ 0.9"
---Allowed values are higher (default) and lower. Any other value is rejected at frontmatter-parse time, the scheduler logs a warning, and the program falls back to higher.
When metric_direction: lower is set:
- An iteration's metric is "improved" when
new_metric < best_metric(instead of>). - Iteration History entries show a
-<delta>(negative delta = improvement) instead of+<delta>. - The halting condition fires when
best_metric <= target-metric(instead of>=).
The agent reads selected_metric_direction from /tmp/gh-aw/autoloop.json to determine which direction applies to the current iteration. Programs that omit the field are treated as higher — no behaviour change for existing programs.
Each program file defines three things:
- Goal: What the agent is trying to optimize (natural language description)
- Target: Which files the agent is allowed to modify
- Evaluation: How to measure whether a change is an improvement
A template program file is installed at .autoloop/programs/example.md. Programs will not run until the user has edited them. Each template contains a sentinel line:
<!-- AUTOLOOP:UNCONFIGURED -->
At the start of every run, check each program file for this sentinel. For any program where it is present:
- Skip that program — do not run any iterations for it.
- If no setup issue exists for that program, create one titled
[Autoloop: {program-name}] Action required: configure your program.
Each program uses a single long-running branch named autoloop/{program-name}. This branch persists across iterations — every accepted improvement is committed to it, building up a history of successful changes.
autoloop/{program-name}
Examples:
autoloop/function_minimizationautoloop/signal_processingautoloop/coverage
⚠️ CRITICAL — Branch Name Must Be ExactThe branch name is ALWAYS exactly
autoloop/{program-name}— no suffixes, no hashes, no run IDs, no iteration numbers, no random tokens. Never create branches like:
- ❌
autoloop/coverage-abc123- ❌
autoloop/coverage-iter42-deadbeef- ❌
autoloop/coverage-1234567890Never let the gh-aw framework auto-generate a branch name. You must explicitly name the branch when creating it. The pre-step provides the canonical name in the
head_branchfield of/tmp/gh-aw/autoloop.json— always use that value verbatim.
- On the first accepted iteration, the branch is created from the default branch.
- On subsequent iterations, the agent checks out the existing branch and ensures it is up to date with the default branch. If the branch's changes have already been merged into the default branch (i.e.,
git diff origin/main..autoloop/{program-name}is empty), the branch is reset toorigin/mainto avoid stale commits. Otherwise, the default branch is merged into it. - Accepted iterations are committed and pushed to the branch. Each commit message references the GitHub Actions run URL.
- Rejected or errored iterations do not commit — changes are discarded.
- A single draft PR is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR.
- The branch may be merged into the default branch at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. On the next iteration, the branch is automatically reset to the default branch (see step 2) so that already-merged commits do not cause patch conflicts.
Each program has three coordinated resources:
- Branch + PR:
autoloop/{program-name}with a single draft PR - Program Issue:
[Autoloop: {program-name}]— a single GitHub issue (labeledautoloop-program) that hosts the status comment, per-iteration comments, and human steering. For issue-based programs this is the source issue. For file-based programs it is auto-created on the first run. - State File:
{program-name}.mdin repo-memory — all state, history, and research context
All three reference each other. The program issue is created (or, for issue-based programs, adopted) on the first run and updated with links to the PR and state.
Each run executes one iteration for the single selected program:
-
Read the program file to understand the goal, targets, and evaluation method.
-
Read the state file
{program-name}.mdfrom the repo-memory folder. This is the single source of truth for all program state. The file contains:- ⚙️ Machine State table:
last_run,best_metric,target_metric,iteration_count,paused,pause_reason,completed,completed_reason,consecutive_errors,recent_statuses. These are machine-readable scheduling and control fields visible to both humans and the pre-step. - 🎯 Current Priorities: Human-set guidance for the next iterations (editable by maintainers).
- 📚 Lessons Learned: Key findings from past iterations.
- 🚧 Foreclosed Avenues: Approaches definitively ruled out, with reasons.
- 🔭 Future Directions: Promising ideas not yet tried.
- 📊 Iteration History: Reverse-chronological log of all past iterations.
If the state file does not yet exist, create it in the repo-memory folder using the template defined in the Repo Memory section.
- ⚙️ Machine State table:
- Read the target files and understand the current state.
- Review the state file's Lessons Learned, Foreclosed Avenues, and Current Priorities — what worked, what didn't, and what the maintainer wants.
- Think carefully about what change is most likely to improve the metric. Consider:
- What has been tried before and ruled out (Foreclosed Avenues — don't repeat failures).
- What the Current Priorities section asks for.
- What the evaluation criteria reward.
- Small, targeted changes are more likely to succeed than large rewrites.
- If many small optimizations have been exhausted, consider a larger architectural change.
- Describe the proposed change in your reasoning before implementing it.
-
Check out the program's long-running branch
autoloop/{program-name}, syncing it with the default branch using an explicit four-case decision tree based on commit ahead/behind counts. Run the following script (substituting{program-name}):git fetch origin main if git ls-remote --exit-code origin autoloop/{program-name}; then # Branch exists — fetch it too so the ahead/behind counts below are # computed against up-to-date local copies of the remote tips. git fetch origin autoloop/{program-name} ahead=$(git rev-list --count origin/main..origin/autoloop/{program-name}) behind=$(git rev-list --count origin/autoloop/{program-name}..origin/main) if [ "$ahead" = "0" ] && [ "$behind" != "0" ]; then # All of the branch's commits are already in main (typical case after a # successful merge of the previous iteration's PR). A merge here would # produce a noisy "Merge main into branch" commit that re-exposes every # historical file as a patch touch — the failure mode that triggers # gh-aw's E003 (>100 files) when a new PR is opened. Fast-forward the # canonical branch to main instead. This is lossless because ahead=0 # proves every commit on the branch is already reachable from main. git checkout -B autoloop/{program-name} origin/main git push --force-with-lease origin autoloop/{program-name} elif [ "$ahead" != "0" ] && [ "$behind" != "0" ]; then # True divergence: branch has unique commits AND main has moved on. git checkout -B autoloop/{program-name} origin/autoloop/{program-name} git rebase origin/main # If rebase conflicts occur, resolve them, run `git rebase --continue`, # and repeat until the rebase completes. git push --force-with-lease origin autoloop/{program-name} else # Already at main (ahead=0, behind=0) or only ahead of main (ahead>0, # behind=0). Nothing to rebase — just check out the branch. git checkout -B autoloop/{program-name} origin/autoloop/{program-name} fi else # Branch does not exist — create it from the default branch git checkout -b autoloop/{program-name} origin/main fi
The four cases:
ahead behind Action Rationale 0 0 checkout (nothing to do) branch is exactly at main 0 >0 fast-forward + force-push branch's commits already in main; merging would produce noisy merge commit >0 0 checkout (nothing to do) unique work preserved; no upstream drift to rebase >0 >0 checkout + rebase + force-push true divergence; preserves a linear branch Use
--force-with-leaserather than--forceso that if anyone else is simultaneously pushing to the branch, the update is rejected rather than overwriting their commits. -
Make the proposed changes to the target files only.
-
Respect the program constraints: do not modify files outside the target list.
- Run the evaluation command specified in the program file.
- Parse the metric from the output.
- Compare against
best_metricfrom the state file.
The sandbox-computed metric is necessary but not sufficient for acceptance. The agent's sandbox cannot reliably install many project toolchains (e.g., bun, tsc, cargo, go, pytest) due to network restrictions on asset hosts, so a "metric improved" signal from the sandbox can mask broken commits (e.g., type-check or test failures the sandbox couldn't observe). Acceptance must therefore be gated on CI green for the pushed HEAD commit. If CI fails, attempt to fix-and-retry within the same iteration rather than reverting — reverting throws away mostly-correct work and creates commit→revert→commit churn on the branch.
The accept path is split into three sub-steps: 5a (push and wait for CI), 5b (fix loop), 5c (accept).
If the metric did not improve, jump straight to the "metric did not improve" path below — no push, no CI gate.
Only entered if the metric improved (or this is the first run establishing a baseline).
Improvement is direction-aware:
- If
selected_metric_directionis"higher"(default): the metric improved whennew_metric > best_metric. - If
selected_metric_directionis"lower": the metric improved whennew_metric < best_metric.
Read selected_metric_direction from /tmp/gh-aw/autoloop.json to know which direction applies. The first run (no best_metric yet) always counts as an improvement regardless of direction.
-
Commit the changes to the long-running branch
autoloop/{program-name}with a commit message referencing the actions run:- Commit message subject line:
[Autoloop: {program-name}] Iteration <N>: <short description> - Commit message body (after a blank line):
Run: {run_url}referencing the GitHub Actions run URL.
- Commit message subject line:
-
Push the commit to the long-running branch.
-
Find or create the PR so CI runs and
gh pr checkshas a target. Follow these steps in order: a. Checkexisting_prfrom/tmp/gh-aw/autoloop.json. If it is not null, that is the existing draft PR — use it as$EXISTING_PRbelow; never callcreate-pull-request. b. Ifexisting_pris null, also check thePRfield in the state file's ⚙️ Machine State table as a fallback. Verify it is still open via the GitHub API; if it has been closed or merged, treat it as if no PR exists and proceed to step (c). c. If no PR exists (both sources are null): create one withcreate-pull-request, specifyingbranch: autoloop/{program-name}(the value ofhead_branchfromautoloop.json) explicitly — do not let the framework auto-generate a branch name. See Step 5c for the title/body format. -
Wait for CI on the new HEAD and reduce all check-runs to a single status —
success,failure, orpending:PR=${EXISTING_PR:-$(gh pr list --head autoloop/{program-name} --json number -q '.[0].number')} gh pr checks "$PR" --watch --interval 30 || true status=$(gh pr checks "$PR" --json conclusion,state -q '.[] | (.conclusion // .state // "")' \ | awk ' BEGIN { r = "success" } /^(FAILURE|CANCELLED|TIMED_OUT|ACTION_REQUIRED|STARTUP_FAILURE|STALE)$/ { r = "failure" } /^(PENDING|QUEUED|IN_PROGRESS|WAITING|REQUESTED)$/ { if (r == "success") r = "pending" } END { print r }')
Three outcomes:
success,failure, orpending.pendingshould be rare given--watch, but the awk fallback is defensive — never accept onpending. Treatpendingas a non-terminal state: re-run thegh pr checks --watchstep (it does not consume a fix attempt and the per-attempt--watchtime still counts toward the 60-min wall-clock cap from Step 5b). Ifpendingpersists past the wall-clock cap, fall through to theci-timeouthandling in Step 5b.7. -
If
status == "success", proceed to Step 5c. Ifstatus == "failure", proceed to Step 5b. Ifstatus == "pending", re-run this step (subject to the wall-clock cap defined in Step 5b.7).
If status == "failure", fix and retry — do not revert, do not accept:
-
Fetch the failing check-run logs for the pushed SHA via
gh run view --logor the Checks API. -
Extract a structured failure summary:
- Failing job names and the first error line for each.
- A failure signature — a stable, normalized fingerprint of the failures (e.g., sorted failing-test names + the top error code, like
TS2339:fromArrays:tests/stats/eval_query.test.ts). The signature is what the no-progress guard compares.
(The shared failure-signature extractor lives in the scheduler helper module — see issue #34 for the implementation.)
-
No-progress guard: if this attempt's failure signature exactly matches the previous attempt's signature, stop. The agent is stuck in a repeat-loop. Set
paused: trueon the state file withpause_reason: "stuck in CI fix loop: <signature>", append"ci-fix-exhausted"torecent_statuses, comment on the program issue with the signature and the three most recent attempts, and end the iteration. -
Attempt the fix: feed the structured failure summary back to the agent as the next sub-task (e.g., "CI failed on
<sha>. Here are the failures:<…>. Fix them and push again."). The agent commits the fix and pushes. -
Loop back to Step 5a with the new HEAD.
-
Budget: 5 fix attempts per iteration. If the 5th attempt still leaves CI red, set
paused: truewithpause_reason: "ci-fix-exhausted: <signature>", append"ci-fix-exhausted"torecent_statuses, comment on the program issue, and end the iteration. -
Wall-clock cap: 60 min per iteration including all CI waits across attempts. If exceeded mid-fix, set
paused: truewithpause_reason: "ci-timeout", append"ci-fix-exhausted"torecent_statuses, leave the current branch state in place, and end the iteration.
Only entered when status == "success" from Step 5a (possibly after one or more fix attempts in Step 5b).
-
The commit(s) are already on the long-running branch (pushed in Step 5a / 5b). No further pushing needed.
-
If a draft PR does not already exist for this branch (i.e.,
existing_prfromautoloop.jsonis null AND the state file'sPRfield is null or refers to a closed PR), create one — specifybranch: autoloop/{program-name}(the value ofhead_branchfromautoloop.json) explicitly so the framework does not auto-generate a branch name:- Title:
[Autoloop: {program-name}] - Body includes: a summary of the program goal, link to the program issue, the current best metric, and AI disclosure:
🤖 *This PR is maintained by Autoloop. Each accepted iteration adds a commit to this branch.*If a draft PR already exists, usepush-to-pull-request-branch(nevercreate-pull-request). Update the PR body with the latest metric and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what changed, old metric, new metric, improvement delta, the fix-attempt count if> 0, and a link to the actions run.
- Title:
-
Ensure the program issue exists (see Program Issue below) — for file-based programs that have no program issue yet (
selected_issueis null in/tmp/gh-aw/autoloop.json), create one and record its number in the state file'sIssuefield. -
Update the state file
{program-name}.mdin the repo-memory folder:- Update the ⚙️ Machine State table: reset
consecutive_errorsto 0, setbest_metric, incrementiteration_count, setlast_runto current UTC timestamp, append"accepted"torecent_statuses(keep last 10), setpausedto false. - Prepend an entry to 📊 Iteration History (newest first) with status ✅, metric, signed delta (
+<delta>forhigher-direction programs,-<delta>forlower-direction programs — both arrows point in the "improvement" direction), PR link, the fix-attempt count if> 0, and a one-line summary of what changed and why it worked. - Update 📚 Lessons Learned if this iteration revealed something new about the problem or what works.
- Update 🔭 Future Directions if this iteration opened new promising paths.
- Update the ⚙️ Machine State table: reset
-
Update the program issue: edit the status comment and post a per-iteration comment on the program issue (see Program Issue). Note the fix-attempt count in the per-iteration comment if
> 0. -
Check halting condition (see Halting Condition): If the program has a
target-metricin its frontmatter, compare the newbest_metricagainst it using the program's metric direction (readselected_metric_directionfrom/tmp/gh-aw/autoloop.json):higher: completed whenbest_metric >= target-metric.lower: completed whenbest_metric <= target-metric.
When the target is met, mark the program as completed (set
Completed: true, remove theautoloop-programlabel, addautoloop-completed).
If the metric did not improve:
- Discard the code changes (do not commit them to the long-running branch).
- Update the state file
{program-name}.mdin the repo-memory folder:- Update the ⚙️ Machine State table: increment
iteration_count, setlast_run, append"rejected"torecent_statuses(keep last 10). - Prepend an entry to 📊 Iteration History with status ❌, metric, and a one-line summary of what was tried.
- If this approach is conclusively ruled out (e.g., tried multiple variations and all fail), add it to 🚧 Foreclosed Avenues with a clear explanation.
- Update 🔭 Future Directions if this rejection clarified what to try next.
- Update the ⚙️ Machine State table: increment
- Update the program issue: edit the status comment and post a per-iteration comment on the program issue (see Program Issue).
If evaluation could not run (build failure, missing dependencies, etc.):
- Discard the code changes (do not commit them to the long-running branch).
- Update the state file
{program-name}.mdin the repo-memory folder:- Update the ⚙️ Machine State table: increment
consecutive_errors, incrementiteration_count, setlast_run, append"error"torecent_statuses(keep last 10). - If
consecutive_errorsreaches 3+, setpausedtotrueand setpause_reasonin the Machine State table, and create an issue describing the problem. - Prepend an entry to 📊 Iteration History with status
⚠️ and a brief error description.
- Update the ⚙️ Machine State table: increment
- Update the program issue: edit the status comment and post a per-iteration comment on the program issue (see Program Issue).
Each program has exactly one open GitHub issue (labeled autoloop-program) titled [Autoloop: {program-name}]. This single issue is the source of truth for the program — it hosts:
- The status comment (the earliest bot comment, edited in place each iteration) — a dashboard of current state.
- A per-iteration comment for every iteration (accepted, rejected, or error) — the rolling log.
- Human steering comments — plain-prose comments from maintainers, treated by the agent as directives.
There are no separate "steering" or "experiment log" issues — they have all been collapsed into this one issue.
If selected_issue is null in /tmp/gh-aw/autoloop.json, the program is file-based and has no program issue yet. On the first run, create one with create-issue:
- Title:
[Autoloop: {program-name}]. - Body: the contents of the program file (
program.md) plus a placeholder for the status comment so maintainers know one will be edited in place. - Labels:
[autoloop-program, automation, autoloop].
Record the new issue number in the state file's Issue field. On subsequent runs, the pre-step will discover the existing program issue (it scans open issues with the autoloop-program label) and selected_issue will be populated automatically.
For issue-based programs (selected_issue is not null on the very first run), no creation is needed — the source issue is already the program issue. The flow below is identical from there on.
On the first iteration, post a comment on the program issue. On every subsequent iteration, update that same comment (edit it, do not post a new one). This is the "status comment" — always the earliest bot comment on the issue.
Find the status comment by searching for a comment containing <!-- AUTOLOOP:STATUS -->. If multiple comments contain this sentinel, use the earliest one (lowest comment ID) and ignore the others.
Status comment format:
<!-- AUTOLOOP:STATUS -->
🤖 **Autoloop Status**
| | |
|---|---|
| **Status** | 🟢 Active / ⏸️ Paused / ⚠️ Error / ✅ Completed |
| **Best Metric** | {best_metric} |
| **Target Metric** | {target_metric or "— (open-ended)"} |
| **Iterations** | {iteration_count} |
| **Last Run** | [{YYYY-MM-DD HH:MM UTC}]({run_url}) |
| **Branch** | [`autoloop/{program-name}`](https://github.com/{owner}/{repo}/tree/autoloop/{program-name}) |
| **Pull Request** | #{pr_number} |
| **State File** | [`{program-name}.md`](https://github.com/{owner}/{repo}/blob/memory/autoloop/{program-name}.md) |
| **Paused** | {true/false} ({pause_reason if paused}) |
### Summary
{2-3 sentence summary of current state: what has been accomplished so far, what the current best approach is, and what direction the next iteration will likely take.}After every iteration (accepted, rejected, or error), post a new comment on the program issue with a summary of what happened:
🤖 **Iteration {N}** — [{status_emoji} {status}]({run_url})
- **Change**: {one-line description of what was tried}
- **Metric**: {value} (best: {best_metric}, delta: {+/-delta})
- **Commit**: {short_sha} *(if accepted)*
- **Result**: {one-sentence summary of what this iteration revealed}Human comments on the program issue act as steering input (in addition to the state file's Current Priorities section). Before proposing a change, read all comments on the program issue and treat any human (non-bot) comments posted since the last iteration as directives — similar to how the Current Priorities section works in the state file.
- For issue-based programs, the source issue body IS the program definition — do not modify it (the user owns it).
- For file-based programs, the program issue body is informational and may be lightly updated (e.g., to refresh the program summary), but the program file (
program.md) remains the source of truth for the goal/target/evaluation. - The
autoloop-programlabel must remain on the issue for the program to be discovered. When a program completes (target metric reached), the label is removed automatically and replaced withautoloop-completed. - Closing the program issue stops the program from being discovered (equivalent to deleting a program file). Do NOT close the program issue when the PR is merged — the branch continues to accumulate future iterations.
- Program issues are labeled
[autoloop-program, automation, autoloop].
Older Autoloop installations created up to three issues per program: the program issue (issue-based only), a separate [Autoloop: {name}] Steering issue, and monthly [Autoloop: {name}] Experiment Log issues. These have been collapsed into the single program issue described above.
- Before creating a new program issue for a file-based program, check whether one with the title
[Autoloop: {program-name}]already exists (open or closed). If found and open, adopt it; if closed, reopen it rather than creating a new one. - Existing
Steeringand monthlyExperiment Logissues can be manually closed by maintainers; the agent must stop posting to them. - The state file's legacy
Steering Issuefield is deprecated; the newIssuefield replaces it. If only the legacy field is present, copy its value into the newIssuefield on the next iteration.
Programs can be open-ended (run indefinitely until manually stopped) or goal-oriented (run until a target metric is reached). This is controlled by the optional target-metric frontmatter field.
- Parse the
target-metricvalue from the program's YAML frontmatter (if present). - After each accepted iteration, compare the new
best_metricagainst thetarget-metric. - Determine whether the target is met based on the program's
metric_direction(read fromselected_metric_directionin/tmp/gh-aw/autoloop.json; defaults tohigherwhen unset):higher(default): the target is met whenbest_metric >= target-metric.lower: the target is met whenbest_metric <= target-metric.
- When the target is met, complete the program:
- Set
Completedtotruein the state file's ⚙️ Machine State table. - Set
Completed Reasonto a human-readable message (e.g.,target metric 0.95 reached with value 0.97). - For issue-based programs (
selected_issueis not null):- Remove the
autoloop-programlabel from the source issue. - Add the
autoloop-completedlabel to the source issue.
- Remove the
- Update the status comment to show ✅ Completed status.
- Post a per-run comment celebrating the achievement:
🎉 **Target metric reached!** The program has achieved its goal. - Post a per-iteration comment on the program issue noting the completion.
- The program will not be selected for future runs (the pre-step skips completed programs).
- Set
---
schedule: every 6h
target-metric: 0.95
---
# Improve Test Coverage
## Goal
Increase test coverage to at least 95%. **Higher is better.**
## Target
Only modify these files:
- `src/tests/**`
## Evaluation
```bash
npm run coverage -- --jsonThe metric is coverage_pct. Higher is better.
In this example, once `coverage_pct` reaches or exceeds `0.95`, the program completes automatically.
### Programs Without a Target Metric
Programs that omit `target-metric` are **open-ended** — they run indefinitely, always seeking further improvement. They can only be stopped by:
- Closing the issue (issue-based programs)
- Deleting or removing the program file
- Setting `Paused: true` in the state file
- Auto-pause from plateau (5 consecutive rejections) or errors (3 consecutive failures)
## State and Memory
Autoloop uses the gh-aw **repo-memory** tool for persistent state storage. Each program's state is stored as a markdown file (`{program-name}.md`) on the `memory/autoloop` branch, automatically managed by the repo-memory infrastructure.
This means:
- Maintainers can see **everything** in the state file on the `memory/autoloop` branch: current best metric, last run, iteration history, lessons, priorities — all in one place.
- Maintainers can **edit any section** of the state file to set priorities, give feedback, or flag foreclosed approaches.
- The pre-step reads state files from the repo-memory directory to determine scheduling.
- The agent reads and writes state files in the repo-memory folder; changes are automatically committed and pushed after the workflow completes.
### Per-Program State File
Each program has a state file at `{program-name}.md` in the repo-memory folder. This file is divided into two logical areas:
1. **⚙️ Machine State** — a structured table at the top of the file that the pre-step can parse and the agent must keep updated after every iteration.
2. **Research sections** — human-editable sections: 🎯 Current Priorities, 📚 Lessons Learned, 🚧 Foreclosed Avenues, 🔭 Future Directions, 📊 Iteration History.
**After every iteration** (accepted, rejected, or error), update the state file — both the Machine State table and the relevant research sections.
See the [Repo Memory](#repo-memory) section for the full file structure, templates, and update rules.
## Repo Memory
Autoloop uses the gh-aw `repo-memory` tool with branch `memory/autoloop` and file glob `*.md`. Each program's state is stored as `{program-name}.md` in the repo-memory folder.
### Per-Program State File
When creating or updating a program's state file in the repo-memory folder, use this structure:
```markdown
# Autoloop: {program-name}
🤖 *This file is maintained by the Autoloop agent. Maintainers may freely edit any section.*
---
## ⚙️ Machine State
> 🤖 *Updated automatically after each iteration. The pre-step scheduler reads this table — keep it accurate.*
| Field | Value |
|-------|-------|
| Last Run | — |
| Iteration Count | 0 |
| Best Metric | — |
| Target Metric | — |
| Metric Direction | higher |
| Branch | `autoloop/{program-name}` |
| PR | — |
| Issue | — |
| Paused | false |
| Pause Reason | — |
| Completed | false |
| Completed Reason | — |
| Consecutive Errors | 0 |
| Recent Statuses | — |
---
## 📋 Program Info
**Goal**: {one-line summary from program.md}
**Metric**: {metric-name} ({higher/lower} is better)
**Branch**: [`autoloop/{program-name}`](../../tree/autoloop/{program-name})
**Pull Request**: #{pr_number}
**Issue**: #{issue_number}
---
## 🎯 Current Priorities
<!-- Maintainers: edit this section to guide the next iterations. The agent will read and follow these priorities. -->
*(No specific priorities set — agent is exploring freely.)*
---
## 📚 Lessons Learned
Key findings and insights accumulated over iterations. Updated by the agent when an iteration reveals something useful.
- *(none yet)*
---
## 🚧 Foreclosed Avenues
Approaches that have been tried and definitively ruled out. The agent will not repeat these.
- *(none yet)*
---
## 🔭 Future Directions
Promising ideas yet to be explored. Maintainers and the agent both contribute here.
- *(none yet)*
---
## 📊 Iteration History
All iterations in reverse chronological order (newest first).
<!-- Agent prepends entries here after each iteration -->
*(No iterations yet.)*
| Field | Type | Description |
|---|---|---|
| Last Run | ISO timestamp (e.g. 2025-01-15T12:00:00Z) |
UTC timestamp of the last iteration |
| Iteration Count | integer | Total iterations completed |
| Best Metric | number | Best metric value achieved so far |
| Target Metric | number or — |
Target metric from program frontmatter (halting condition). — if open-ended |
| Metric Direction | higher or lower |
Whether larger or smaller metric values count as improvement. Defaults to higher if absent (back-compat). Set from the program's metric_direction frontmatter field. |
| Branch | branch name | Long-running branch: autoloop/{program-name} |
| PR | #number or — |
Draft PR number for this program |
| Issue | #number or — |
The single program issue ([Autoloop: {program-name}]) for this program. Hosts the status comment, per-iteration comments, and human steering comments. |
| Paused | true or false |
Whether the program is paused |
| Pause Reason | text or — |
Why it is paused (if applicable). Common values include manual, consecutive errors, ci-fix-exhausted: <signature> (5 fix attempts didn't fix CI), stuck in CI fix loop: <signature> (no-progress guard tripped — same failure signature twice in a row), and ci-timeout (60-min wall-clock cap hit). |
| Completed | true or false |
Whether the program has reached its target metric |
| Completed Reason | text or — |
Why it completed (e.g., target metric 0.95 reached with value 0.97) |
| Consecutive Errors | integer | Count of consecutive evaluation failures |
| Recent Statuses | comma-separated words | Last 10 outcomes: accepted, rejected, error, or ci-fix-exhausted. The ci-fix-exhausted value is the coarse bucket for any iteration that ended because the CI gate could not be made green within the per-iteration budget — including no-progress-guard trips, 5-attempt budget exhaustion, and ci-timeout. The fine-grained reason is in pause_reason. |
After each iteration, prepend an entry to the 📊 Iteration History section. Use ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} for the run URL.
### Iteration {N} — {YYYY-MM-DD HH:MM UTC} — [Run](https://github.com/{owner}/{repo}/actions/runs/{run_id})
- **Status**: ✅ Accepted / ❌ Rejected / ⚠️ Error
- **Change**: {one-line description of what was tried}
- **Metric**: {value} (previous best: {previous_best}, delta: {signed-delta})
- **Commit**: {short_sha} *(if accepted)*
- **CI fix attempts**: {N} *(omit if 0; only present for accepted iterations that needed fix-and-retry)*
- **Notes**: {one or two sentences on what this iteration revealed}The delta is signed by metric direction: for higher-direction programs an improvement is +<delta>; for lower-direction programs an improvement is -<delta>. In both cases the sign points in the "improvement" direction so the entry reads naturally.
-
Always read the state file before proposing a change. It contains human guidance you must follow.
-
Always update the state file after each iteration, regardless of outcome.
-
Update the Machine State table first — the scheduling pre-step depends on it.
-
Prepend iteration history entries (newest first).
-
Accumulate Lessons Learned — add new insights, don't overwrite existing ones.
-
Add to Foreclosed Avenues only when an approach is conclusively ruled out (not just rejected once).
-
Respect Current Priorities — if a maintainer has written priorities, follow them in your next proposal.
-
Write the state file to the repo-memory folder. Changes are automatically committed and pushed to the
memory/autoloopbranch after the workflow completes. -
Keep the state file compact. The state file must stay under the configured
max-file-size(default 30 KB — seestate_file_max_bytesin/tmp/gh-aw/autoloop.json). When prepending a new iteration entry, collapse older iteration entries (beyond the most recent 10) into compressed summary lines. Example format for collapsed entries:### Iters 50–100 — ✅ (metrics 20→55): brief summary of what worked across this rangeAlso prune 📚 Lessons Learned to the most recent and most relevant entries, and consolidate similar entries in 🚧 Foreclosed Avenues if it grows beyond a page. If
state_file_size_bytesfrom/tmp/gh-aw/autoloop.jsonis already greater than 80% ofstate_file_max_bytes, compact aggressively this iteration: collapse to the most recent 5 detailed entries and merge older compressed ranges into broader bands. Repo-memory rejects files larger thanmax-file-size, which breaks scheduling — so keeping the file under budget is mandatory, not optional.
- One change per iteration. Keep changes small and targeted.
- No breaking changes. Target files must remain functional even if the iteration is rejected.
- Respect the evaluation budget. If the evaluation command has a time constraint, respect it.
- Repo-memory state file is the single source of truth. All state lives in
{program-name}.mdin the repo-memory folder — scheduling fields, history, lessons, priorities. Keep it up to date. - Learn from the state file. The Foreclosed Avenues and Lessons Learned sections exist to prevent repeating failures. Read them before every proposal.
- Respect human input. The Current Priorities section is set by maintainers — follow it.
- Diminishing returns. If the last 5 consecutive iterations were rejected, post a comment suggesting the user review the program definition or update the state file's Current Priorities.
- Transparency. Every PR and comment must include AI disclosure with 🤖.
- Safety. Never modify files outside the target list. Never modify the evaluation script. Never modify the program definition (except via
/autoloopcommand mode). - Read AGENTS.md first: before starting work, read the repository's
AGENTS.mdfile (if present) to understand project-specific conventions. - Build and test: run any build/test commands before creating PRs.
❌ Do NOT create a new branch with a suffix for each iteration. Correct:
autoloop/coverageWrong:autoloop/coverage-abc123,autoloop/coverage-iter42,autoloop/coverage-deadbeef1234Use thehead_branchfield from/tmp/gh-aw/autoloop.json— it is always the canonical name. Never let the gh-aw framework auto-generate a branch name.
❌ Do NOT create a new PR if one already exists for
autoloop/{program-name}. The pre-step providesexisting_prin/tmp/gh-aw/autoloop.json. If it is not null, always usepush-to-pull-request-branch— never callcreate-pull-request. Only create a PR whenexisting_pris null AND the state file'sPRfield is also null (or refers to a closed PR).
❌ Do NOT modify files outside the program's Target list. The Target section of the program file is the allowlist. Touching anything else (including the evaluation script or the program file itself) is forbidden.