Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/autoloop.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ A template program file is installed at `.autoloop/programs/example.md`. **Progr
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`.
2. If no setup issue exists for that program, create one titled `[Autoloop: {program-name}] Action required: configure your program` (supply this exact string — do NOT prepend `[Autoloop] `, the `create-issue` safe-output adds that automatically).

## Branching Model

Expand Down Expand Up @@ -570,7 +570,7 @@ There are no separate "steering" or "experiment log" issues — they have all be

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}]`
- **Title**: `[Autoloop: {program-name}]` — supply this exact string as the title. **Do NOT prepend `[Autoloop] `** yourself; the `create-issue` safe-output's `title-prefix` config adds that automatically. The final issue title will be `[Autoloop] [Autoloop: {program-name}]` on GitHub, and the scheduler recognizes that form when matching a program issue back to its file-based program.
- **Labels**: `autoloop-program`, `automation`, `autoloop`
- **Body**: the contents of the program file (so humans can read the goal/target/evaluation directly on the issue), prefixed with `🤖 *Autoloop program issue for `{program-name}`. The program definition below is mirrored from [`{selected_file}`]({link-to-file}). Edit the file to update the definition; comment on this issue to steer the agent.*`

Expand Down
73 changes: 65 additions & 8 deletions .github/workflows/scripts/autoloop_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,57 @@
autoloop_dir = ".autoloop/programs"
template_file = os.path.join(autoloop_dir, "example.md")

# Regex matching the canonical program-issue title, tolerating the
# `[Autoloop] ` safe-outputs prefix that the `create-issue` machinery
# auto-prepends. Both `[Autoloop: name]` (raw, before prefix is added) and
# `[Autoloop] [Autoloop: name]` (after prefix is applied) are recognised so
# the scheduler matches a program issue back to its file-based program
# regardless of whether the agent or the safe-outputs layer added the
# outer marker. The match is case-insensitive for robustness.
_AUTOLOOP_PREFIX_RE = re.compile(r'^\[Autoloop\]\s*', re.IGNORECASE)
_AUTOLOOP_NAME_RE = re.compile(r'^\[Autoloop:\s*([^\]]+?)\s*\]\s*$', re.IGNORECASE)

def extract_program_name_from_issue_title(title):
"""Return the program-name embedded in a canonical program-issue title.

Accepts titles of the form `[Autoloop: name]` and tolerates any number
of leading `[Autoloop] ` prefixes (the safe-outputs prefix can collide
with an agent-supplied `[Autoloop]` marker, producing doubly-prefixed
titles like `[Autoloop] [Autoloop: name]`). Returns ``None`` when the
title does not match the canonical pattern — callers should then fall
back to slugification for human-authored issue titles.
"""
if not title:
return None
s = title.strip()
while _AUTOLOOP_PREFIX_RE.match(s):
s = _AUTOLOOP_PREFIX_RE.sub('', s, count=1)
m = _AUTOLOOP_NAME_RE.match(s)
if m:
return m.group(1).strip()
return None

def slugify(title):
"""Slugify an issue title to a program name.

Defensively strips any leading `[Autoloop] ` markers (collapses
repeated prefixes) and a `[Autoloop: name]` wrapper before
slugifying, so doubly-prefixed titles authored under an old or buggy
prompt still collapse to the same slug as the canonical name. This
makes the scheduler self-healing: even if Fix 1 (prompt clarification)
regresses, this normalisation keeps file-based and issue-based
discovery from forking into two programs.
"""
s = (title or "").strip()
while _AUTOLOOP_PREFIX_RE.match(s):
s = _AUTOLOOP_PREFIX_RE.sub('', s, count=1)
m = re.match(r'^\[Autoloop:\s*([^\]]+?)\s*\]\s*', s, re.IGNORECASE)
if m:
s = m.group(1)
slug = re.sub(r'[^a-z0-9]+', '-', s.lower()).strip('-')
slug = re.sub(r'-+', '-', slug)
return slug

# Read program state from repo-memory (persistent git-backed storage)
github_token = os.environ.get("GITHUB_TOKEN", "")
repo = os.environ.get("GITHUB_REPOSITORY", "")
Expand Down Expand Up @@ -164,17 +215,20 @@ def read_program_state(program_name):
known_file_program_names.add(os.path.basename(os.path.dirname(pf)))
else:
known_file_program_names.add(os.path.splitext(os.path.basename(pf))[0])
file_program_issue_pattern = re.compile(r'^\s*\[Autoloop:\s*([^\]]+?)\s*\]\s*$')
consumed_issue_numbers = set()
for issue in issues:
if issue.get("pull_request"):
continue
title = issue.get("title") or ""
m = file_program_issue_pattern.match(title)
if m and m.group(1) in known_file_program_names:
file_program_issues[m.group(1)] = issue["number"]
# extract_program_name_from_issue_title tolerates the doubly-prefixed
# `[Autoloop] [Autoloop: name]` form produced when the safe-outputs
# `title-prefix` collides with an agent-supplied marker, so existing
# in-the-wild issues still merge with their file-based program here.
extracted = extract_program_name_from_issue_title(title)
if extracted and extracted in known_file_program_names:
file_program_issues[extracted] = issue["number"]
consumed_issue_numbers.add(issue["number"])
print(f" Found program issue for file-based program '{m.group(1)}': #{issue['number']}")
print(f" Found program issue for file-based program '{extracted}': #{issue['number']}")

# Second pass: any remaining autoloop-program issue is an issue-based program.
for issue in issues:
Expand All @@ -185,9 +239,12 @@ def read_program_state(program_name):
body = issue.get("body") or ""
title = issue.get("title") or ""
number = issue["number"]
# Derive program name from issue title: slugify to lowercase with hyphens
slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens
# Derive program name from issue title via the defensive slugify
# (strips known `[Autoloop]`/`[Autoloop: name]` prefixes before
# slugifying, so a stray doubly-prefixed title that didn't match a
# known file-based program still produces a clean slug rather than
# a `autoloop-autoloop-...` chimera).
slug = slugify(title)
if not slug:
slug = f"issue-{number}"
# Avoid slug collisions: if another issue already claimed this slug, append issue number
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ package-lock.json
*.tgz
playground/benchmarks/
playground/dist/
__pycache__/
*.pyc
Loading