Skip to content

Latest commit

 

History

History
916 lines (695 loc) · 51.9 KB

File metadata and controls

916 lines (695 loc) · 51.9 KB
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
schedule workflow_dispatch slash_command
every 6h
inputs
program
description required type
Run a specific program by name (bypasses scheduling)
false
string
name
autoloop
permissions read-all
timeout-minutes 45
network
allowed
defaults
node
python
rust
java
dotnet
safe-outputs
max-patch-size max-patch-files add-comment create-pull-request push-to-pull-request-branch create-issue update-issue add-labels remove-labels
10240
500
max target hide-older-comments
7
*
false
draft labels protected-files preserve-branch-name recreate-ref max
true
automation
autoloop
policy exclude
fallback-to-issue
package.json
package-lock.json
bun.lockb
bunfig.toml
yarn.lock
pnpm-lock.yaml
tsconfig.json
biome.json
requirements.txt
pyproject.toml
setup.py
setup.cfg
true
true
1
signed-commits target title-prefix protected-files max
false
*
[Autoloop
policy exclude
allowed
package.json
package-lock.json
bun.lockb
bunfig.toml
yarn.lock
pnpm-lock.yaml
tsconfig.json
biome.json
requirements.txt
pyproject.toml
setup.py
setup.cfg
1
labels max
automation
autoloop
1
target title-prefix max
*
[Autoloop
3
target max
*
2
target max
*
2
checkout
fetch fetch-depth
*
0
tools
web-fetch github bash repo-memory
toolsets
all
true
branch-name file-glob max-file-size
memory/autoloop
*.md
30720
imports
shared/reporting.md
steps
name env run
Clone repo-memory for scheduling
GH_TOKEN GITHUB_REPOSITORY GITHUB_SERVER_URL
${{ github.token }}
${{ github.repository }}
${{ github.server_url }}
# Clone the repo-memory branch so the scheduling step can read persisted state # from previous runs. The framework-managed repo-memory clone happens after # pre-steps, so we perform an early shallow clone here. MEMORY_DIR="/tmp/gh-aw/repo-memory/autoloop" BRANCH="memory/autoloop" mkdir -p "$(dirname "$MEMORY_DIR")" REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" AUTH_URL="$(echo "$REPO_URL" | sed "s|https://|https://x-access-token:${GH_TOKEN}@|")" if git ls-remote --exit-code --heads "$AUTH_URL" "$BRANCH" > /dev/null 2>&1; then git clone --single-branch --branch "$BRANCH" --depth 1 "$AUTH_URL" "$MEMORY_DIR" 2>&1 echo "Cloned repo-memory branch to $MEMORY_DIR" else mkdir -p "$MEMORY_DIR" echo "No repo-memory branch found yet (first run). Created empty directory." fi
name env run
Check which programs are due
GITHUB_TOKEN GITHUB_REPOSITORY AUTOLOOP_PROGRAM
${{ github.token }}
${{ github.repository }}
${{ github.event.inputs.program }}
python3 .github/workflows/scripts/autoloop_scheduler.py
source githubnext/autoloop
engine copilot
features
copilot-requests
true

Autoloop

An iterative optimization agent that proposes changes, evaluates them against a metric, and keeps only improvements — running autonomously on a schedule.

Command Mode

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 the autoloop-program label). 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.

Program Locations

Autoloop supports three program layouts:

Directory-based programs (preferred)

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).

Bare markdown programs (simple/legacy)

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.

Issue-based programs

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, or null if it came from a file.
  • issue_programs: A mapping of program name → issue number for all issue-based programs found.

Reading Programs

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, or null if 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>.md for issue-based programs).
  • selected_issue: The GitHub issue number if the selected program came from an issue, or null if it came from a file.
  • selected_target_metric: The target-metric value from the program's frontmatter (a number), or null if 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's metric_direction frontmatter 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 with state_file_max_bytes to 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 configured max-file-size for repo-memory state files (default 30720, 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: If true, no program files exist at all.
  • not_due: If true, programs exist but none are due for this run.
  • head_branch: The canonical long-running branch name for the selected program — always exactly autoloop/{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 for autoloop/{program-name}, or null if 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:

  1. Read the program file from the selected_file path.
  2. Parse the three sections: Goal, Target, Evaluation.
  3. Read the current state of all target files.
  4. Read the state file {selected}.md from the repo-memory folder for all state: the ⚙️ Machine State table (scheduling fields) plus the research sections (priorities, lessons, foreclosed avenues, iteration history).
  5. If selected_issue is not null, this is an issue-based program — also read the issue comments for any human steering input.

Multiple Programs

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 labeled autoloop-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}.md in 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.

Per-Program Schedule

Programs can optionally specify their own schedule in a YAML frontmatter block:

---
schedule: every 1h
---

# Autoloop Program
...

Target Metric (Halting Condition)

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
...

Metric Direction

By default Autoloop assumes higher is betterbest_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.

Program Definition

Each program file defines three things:

  1. Goal: What the agent is trying to optimize (natural language description)
  2. Target: Which files the agent is allowed to modify
  3. Evaluation: How to measure whether a change is an improvement

Setup Guard

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:

  1. Skip that program — do not run any iterations for it.
  2. If no setup issue exists for that program, create one titled [Autoloop: {program-name}] Action required: configure your program.

Branching Model

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.

Branch Naming Convention

autoloop/{program-name}

Examples:

  • autoloop/function_minimization
  • autoloop/signal_processing
  • autoloop/coverage

⚠️ CRITICAL — Branch Name Must Be Exact

The 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-1234567890

Never 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_branch field of /tmp/gh-aw/autoloop.json — always use that value verbatim.

How It Works

  1. On the first accepted iteration, the branch is created from the default branch.
  2. 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 to origin/main to avoid stale commits. Otherwise, the default branch is merged into it.
  3. Accepted iterations are committed and pushed to the branch. Each commit message references the GitHub Actions run URL.
  4. Rejected or errored iterations do not commit — changes are discarded.
  5. A single draft PR is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR.
  6. 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.

Cross-Linking

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 (labeled autoloop-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}.md in 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.

Iteration Loop

Each run executes one iteration for the single selected program:

Step 1: Read State

  1. Read the program file to understand the goal, targets, and evaluation method.

  2. Read the state file {program-name}.md from 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.

Step 2: Analyze and Propose

  1. Read the target files and understand the current state.
  2. Review the state file's Lessons Learned, Foreclosed Avenues, and Current Priorities — what worked, what didn't, and what the maintainer wants.
  3. 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.
  4. Describe the proposed change in your reasoning before implementing it.

Step 3: Implement

  1. 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-lease rather than --force so that if anyone else is simultaneously pushing to the branch, the update is rejected rather than overwriting their commits.

  2. Make the proposed changes to the target files only.

  3. Respect the program constraints: do not modify files outside the target list.

Step 4: Evaluate

  1. Run the evaluation command specified in the program file.
  2. Parse the metric from the output.
  3. Compare against best_metric from the state file.

Step 5: Accept or Reject

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.

Step 5a: Push and wait for CI

Only entered if the metric improved (or this is the first run establishing a baseline).

Improvement is direction-aware:

  • If selected_metric_direction is "higher" (default): the metric improved when new_metric > best_metric.
  • If selected_metric_direction is "lower": the metric improved when new_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.

  1. 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.
  2. Push the commit to the long-running branch.

  3. Find or create the PR so CI runs and gh pr checks has a target. Follow these steps in order: a. Check existing_pr from /tmp/gh-aw/autoloop.json. If it is not null, that is the existing draft PR — use it as $EXISTING_PR below; never call create-pull-request. b. If existing_pr is null, also check the PR field 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 with create-pull-request, specifying branch: autoloop/{program-name} (the value of head_branch from autoloop.json) explicitly — do not let the framework auto-generate a branch name. See Step 5c for the title/body format.

  4. Wait for CI on the new HEAD and reduce all check-runs to a single status — success, failure, or pending:

    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, or pending. pending should be rare given --watch, but the awk fallback is defensive — never accept on pending. Treat pending as a non-terminal state: re-run the gh pr checks --watch step (it does not consume a fix attempt and the per-attempt --watch time still counts toward the 60-min wall-clock cap from Step 5b). If pending persists past the wall-clock cap, fall through to the ci-timeout handling in Step 5b.7.

  5. If status == "success", proceed to Step 5c. If status == "failure", proceed to Step 5b. If status == "pending", re-run this step (subject to the wall-clock cap defined in Step 5b.7).

Step 5b: Fix loop (up to 5 attempts per iteration)

If status == "failure", fix and retry — do not revert, do not accept:

  1. Fetch the failing check-run logs for the pushed SHA via gh run view --log or the Checks API.

  2. 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.)

  3. 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: true on the state file with pause_reason: "stuck in CI fix loop: <signature>", append "ci-fix-exhausted" to recent_statuses, comment on the program issue with the signature and the three most recent attempts, and end the iteration.

  4. 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.

  5. Loop back to Step 5a with the new HEAD.

  6. Budget: 5 fix attempts per iteration. If the 5th attempt still leaves CI red, set paused: true with pause_reason: "ci-fix-exhausted: <signature>", append "ci-fix-exhausted" to recent_statuses, comment on the program issue, and end the iteration.

  7. Wall-clock cap: 60 min per iteration including all CI waits across attempts. If exceeded mid-fix, set paused: true with pause_reason: "ci-timeout", append "ci-fix-exhausted" to recent_statuses, leave the current branch state in place, and end the iteration.

Step 5c: Accept

Only entered when status == "success" from Step 5a (possibly after one or more fix attempts in Step 5b).

  1. The commit(s) are already on the long-running branch (pushed in Step 5a / 5b). No further pushing needed.

  2. If a draft PR does not already exist for this branch (i.e., existing_pr from autoloop.json is null AND the state file's PR field is null or refers to a closed PR), create one — specify branch: autoloop/{program-name} (the value of head_branch from autoloop.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, use push-to-pull-request-branch (never create-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.
  3. Ensure the program issue exists (see Program Issue below) — for file-based programs that have no program issue yet (selected_issue is null in /tmp/gh-aw/autoloop.json), create one and record its number in the state file's Issue field.

  4. Update the state file {program-name}.md in the repo-memory folder:

    • Update the ⚙️ Machine State table: reset consecutive_errors to 0, set best_metric, increment iteration_count, set last_run to current UTC timestamp, append "accepted" to recent_statuses (keep last 10), set paused to false.
    • Prepend an entry to 📊 Iteration History (newest first) with status ✅, metric, signed delta (+<delta> for higher-direction programs, -<delta> for lower-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.
  5. 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.

  6. Check halting condition (see Halting Condition): If the program has a target-metric in its frontmatter, compare the new best_metric against it using the program's metric direction (read selected_metric_direction from /tmp/gh-aw/autoloop.json):

    • higher: completed when best_metric >= target-metric.
    • lower: completed when best_metric <= target-metric.

    When the target is met, mark the program as completed (set Completed: true, remove the autoloop-program label, add autoloop-completed).

If the metric did not improve:

  1. Discard the code changes (do not commit them to the long-running branch).
  2. Update the state file {program-name}.md in the repo-memory folder:
    • Update the ⚙️ Machine State table: increment iteration_count, set last_run, append "rejected" to recent_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.
  3. 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.):

  1. Discard the code changes (do not commit them to the long-running branch).
  2. Update the state file {program-name}.md in the repo-memory folder:
    • Update the ⚙️ Machine State table: increment consecutive_errors, increment iteration_count, set last_run, append "error" to recent_statuses (keep last 10).
    • If consecutive_errors reaches 3+, set paused to true and set pause_reason in the Machine State table, and create an issue describing the problem.
    • Prepend an entry to 📊 Iteration History with status ⚠️ and a brief error description.
  3. Update the program issue: edit the status comment and post a per-iteration comment on the program issue (see Program Issue).

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.

Auto-Creation for File-Based Programs

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.

Status Comment

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.}

Per-Iteration Comment

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}

Steering via Issue Comments

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.

Program Issue Rules

  • 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-program label 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 with autoloop-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].

Migration from the Old Three-Issue Model

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 Steering and monthly Experiment Log issues can be manually closed by maintainers; the agent must stop posting to them.
  • The state file's legacy Steering Issue field is deprecated; the new Issue field replaces it. If only the legacy field is present, copy its value into the new Issue field on the next iteration.

Halting Condition

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.

How It Works

  1. Parse the target-metric value from the program's YAML frontmatter (if present).
  2. After each accepted iteration, compare the new best_metric against the target-metric.
  3. Determine whether the target is met based on the program's metric_direction (read from selected_metric_direction in /tmp/gh-aw/autoloop.json; defaults to higher when unset):
    • higher (default): the target is met when best_metric >= target-metric.
    • lower: the target is met when best_metric <= target-metric.
  4. When the target is met, complete the program:
    • Set Completed to true in the state file's ⚙️ Machine State table.
    • Set Completed Reason to a human-readable message (e.g., target metric 0.95 reached with value 0.97).
    • For issue-based programs (selected_issue is not null):
      • Remove the autoloop-program label from the source issue.
      • Add the autoloop-completed label to the source issue.
    • 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).

Example

---
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 -- --json

The 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.)*

Machine State Field Reference

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.

Iteration History Entry Format

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.

Update Rules

  • 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/autoloop branch after the workflow completes.

  • Keep the state file compact. The state file must stay under the configured max-file-size (default 30 KB — see state_file_max_bytes in /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 range

    Also 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_bytes from /tmp/gh-aw/autoloop.json is already greater than 80% of state_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 than max-file-size, which breaks scheduling — so keeping the file under budget is mandatory, not optional.

Guidelines

  • 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}.md in 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 /autoloop command mode).
  • Read AGENTS.md first: before starting work, read the repository's AGENTS.md file (if present) to understand project-specific conventions.
  • Build and test: run any build/test commands before creating PRs.

Common Mistakes to Avoid

Do NOT create a new branch with a suffix for each iteration. Correct: autoloop/coverage Wrong: autoloop/coverage-abc123, autoloop/coverage-iter42, autoloop/coverage-deadbeef1234 Use the head_branch field 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 provides existing_pr in /tmp/gh-aw/autoloop.json. If it is not null, always use push-to-pull-request-branch — never call create-pull-request. Only create a PR when existing_pr is null AND the state file's PR field 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.