Skip to content

Commit 2d32f0a

Browse files
authored
Merge pull request #193 from githubnext/copilot/autoloop-fix-duplicate-pr-prefix
Autoloop: stop double-prefixing program-issue titles and dedupe scheduler discovery
2 parents 09c9ba2 + a2136da commit 2d32f0a

3 files changed

Lines changed: 69 additions & 10 deletions

File tree

.github/workflows/autoloop.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ A template program file is installed at `.autoloop/programs/example.md`. **Progr
293293
At the start of every run, check each program file for this sentinel. For any program where it is present:
294294

295295
1. **Skip that program — do not run any iterations for it.**
296-
2. If no setup issue exists for that program, create one titled `[Autoloop: {program-name}] Action required: configure your program`.
296+
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).
297297

298298
## Branching Model
299299

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

571571
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`:
572572

573-
- **Title**: `[Autoloop: {program-name}]`
573+
- **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.
574574
- **Labels**: `autoloop-program`, `automation`, `autoloop`
575575
- **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.*`
576576

.github/workflows/scripts/autoloop_scheduler.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,57 @@
1414
autoloop_dir = ".autoloop/programs"
1515
template_file = os.path.join(autoloop_dir, "example.md")
1616

17+
# Regex matching the canonical program-issue title, tolerating the
18+
# `[Autoloop] ` safe-outputs prefix that the `create-issue` machinery
19+
# auto-prepends. Both `[Autoloop: name]` (raw, before prefix is added) and
20+
# `[Autoloop] [Autoloop: name]` (after prefix is applied) are recognised so
21+
# the scheduler matches a program issue back to its file-based program
22+
# regardless of whether the agent or the safe-outputs layer added the
23+
# outer marker. The match is case-insensitive for robustness.
24+
_AUTOLOOP_PREFIX_RE = re.compile(r'^\[Autoloop\]\s*', re.IGNORECASE)
25+
_AUTOLOOP_NAME_RE = re.compile(r'^\[Autoloop:\s*([^\]]+?)\s*\]\s*$', re.IGNORECASE)
26+
27+
def extract_program_name_from_issue_title(title):
28+
"""Return the program-name embedded in a canonical program-issue title.
29+
30+
Accepts titles of the form `[Autoloop: name]` and tolerates any number
31+
of leading `[Autoloop] ` prefixes (the safe-outputs prefix can collide
32+
with an agent-supplied `[Autoloop]` marker, producing doubly-prefixed
33+
titles like `[Autoloop] [Autoloop: name]`). Returns ``None`` when the
34+
title does not match the canonical pattern — callers should then fall
35+
back to slugification for human-authored issue titles.
36+
"""
37+
if not title:
38+
return None
39+
s = title.strip()
40+
while _AUTOLOOP_PREFIX_RE.match(s):
41+
s = _AUTOLOOP_PREFIX_RE.sub('', s, count=1)
42+
m = _AUTOLOOP_NAME_RE.match(s)
43+
if m:
44+
return m.group(1).strip()
45+
return None
46+
47+
def slugify(title):
48+
"""Slugify an issue title to a program name.
49+
50+
Defensively strips any leading `[Autoloop] ` markers (collapses
51+
repeated prefixes) and a `[Autoloop: name]` wrapper before
52+
slugifying, so doubly-prefixed titles authored under an old or buggy
53+
prompt still collapse to the same slug as the canonical name. This
54+
makes the scheduler self-healing: even if Fix 1 (prompt clarification)
55+
regresses, this normalisation keeps file-based and issue-based
56+
discovery from forking into two programs.
57+
"""
58+
s = (title or "").strip()
59+
while _AUTOLOOP_PREFIX_RE.match(s):
60+
s = _AUTOLOOP_PREFIX_RE.sub('', s, count=1)
61+
m = re.match(r'^\[Autoloop:\s*([^\]]+?)\s*\]\s*', s, re.IGNORECASE)
62+
if m:
63+
s = m.group(1)
64+
slug = re.sub(r'[^a-z0-9]+', '-', s.lower()).strip('-')
65+
slug = re.sub(r'-+', '-', slug)
66+
return slug
67+
1768
# Read program state from repo-memory (persistent git-backed storage)
1869
github_token = os.environ.get("GITHUB_TOKEN", "")
1970
repo = os.environ.get("GITHUB_REPOSITORY", "")
@@ -164,17 +215,20 @@ def read_program_state(program_name):
164215
known_file_program_names.add(os.path.basename(os.path.dirname(pf)))
165216
else:
166217
known_file_program_names.add(os.path.splitext(os.path.basename(pf))[0])
167-
file_program_issue_pattern = re.compile(r'^\s*\[Autoloop:\s*([^\]]+?)\s*\]\s*$')
168218
consumed_issue_numbers = set()
169219
for issue in issues:
170220
if issue.get("pull_request"):
171221
continue
172222
title = issue.get("title") or ""
173-
m = file_program_issue_pattern.match(title)
174-
if m and m.group(1) in known_file_program_names:
175-
file_program_issues[m.group(1)] = issue["number"]
223+
# extract_program_name_from_issue_title tolerates the doubly-prefixed
224+
# `[Autoloop] [Autoloop: name]` form produced when the safe-outputs
225+
# `title-prefix` collides with an agent-supplied marker, so existing
226+
# in-the-wild issues still merge with their file-based program here.
227+
extracted = extract_program_name_from_issue_title(title)
228+
if extracted and extracted in known_file_program_names:
229+
file_program_issues[extracted] = issue["number"]
176230
consumed_issue_numbers.add(issue["number"])
177-
print(f" Found program issue for file-based program '{m.group(1)}': #{issue['number']}")
231+
print(f" Found program issue for file-based program '{extracted}': #{issue['number']}")
178232

179233
# Second pass: any remaining autoloop-program issue is an issue-based program.
180234
for issue in issues:
@@ -185,9 +239,12 @@ def read_program_state(program_name):
185239
body = issue.get("body") or ""
186240
title = issue.get("title") or ""
187241
number = issue["number"]
188-
# Derive program name from issue title: slugify to lowercase with hyphens
189-
slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
190-
slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens
242+
# Derive program name from issue title via the defensive slugify
243+
# (strips known `[Autoloop]`/`[Autoloop: name]` prefixes before
244+
# slugifying, so a stray doubly-prefixed title that didn't match a
245+
# known file-based program still produces a clean slug rather than
246+
# a `autoloop-autoloop-...` chimera).
247+
slug = slugify(title)
191248
if not slug:
192249
slug = f"issue-{number}"
193250
# Avoid slug collisions: if another issue already claimed this slug, append issue number

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ package-lock.json
55
*.tgz
66
playground/benchmarks/
77
playground/dist/
8+
__pycache__/
9+
*.pyc

0 commit comments

Comments
 (0)