fix(reaper): extend allowlist to */research-2* daily-cron sessions

Apply Step 1 of stuck-PR triage. The May 7 reaper allowlist (extract/,
reweave/, fix/) deliberately excluded all agent-prefix branches per
Ganymede's review nit #3 — the rationale being that agent branches are
WIP feature work owned by the agent and shouldn't be auto-closed.

That decision was correct for theseus/feature-foo style branches.
It's wrong for {agent}/research-{YYYY-MM-DD} branches: those are daily
cron output, categorically disposable, regenerated by tomorrow's session.
Same shape as extract/ — content the pipeline-cron created and can
recreate, not feature work owned by the agent.

Production impact: 15 of 16 currently-stuck PRs are research-session
verdict-deadlocks aged 8-12 days. Without this change they sit forever
because the substantive_fixer can't classify (eval_issues=[] or
mechanical-only) and the reaper allowlist excludes them. Once live, next
hourly reaper cycle picks them up under the standard 24h-deadlock gate.

Pattern choice: %/research-2* (date-suffix) over %/research-% (loose).
Verified 15/15 stuck PRs match the tight pattern; sanity-check found
rio/research-batch-agents-memory-harnesses (manually-named, not date-
suffixed) which the loose pattern would catch and the tight pattern
correctly excludes. Closed-status today, but a future hand-named research
thesis branch sitting in request_changes for 24h would have been at risk.
The date prefix '2' threads the needle until 2030 and ages naturally.

Documented as an allowlist invariant ("disposable pipeline-generated
branches") rather than a list, per Step 3 of the plan — future additions
should match the invariant or update it explicitly.

Verified live before pushing:
- 15/15 currently stuck research PRs match the new pattern
- Zero false positives on existing branch namespace (closed branches
  excluded by status='open' guard regardless)
- Existing extract/ reweave/ fix/ allowlist members unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
m3taversal 2026-05-10 19:00:48 +01:00
parent 0eb26327fc
commit 50b888a751

View file

@ -647,10 +647,25 @@ async def verdict_deadlock_reaper_cycle(conn) -> int:
return 0
# Two stuck-verdict shapes: leo:rc+domain:approve, leo:skipped+domain:rc.
# Branch allowlist scopes the reaper to disposable pipeline-managed branches.
# extract/, reweave/, fix/ are content the pipeline created and can recreate;
# agent branches (theseus/, vida/, epimetheus/, etc.) are WIP feature work
# and must not be reaped — owners review their own PRs on their own cadence.
#
# Branch allowlist invariant: the reaper closes ONLY disposable, pipeline-
# generated branches — content the pipeline (or a daily cron) created and
# can recreate. Three classes qualify:
#
# extract/* — per-source extraction PRs, regenerated next ingest cycle
# reweave/* — nightly graph-edge maintenance, regenerated next reweave
# fix/* — pipeline-internal fix branches
# */research-2* — daily {agent}/research-{YYYY-MM-DD} sessions; the date
# suffix scopes this to cron-generated outputs only and
# excludes hand-named research branches like
# rio/research-batch-agents-memory-harnesses, which are
# feature work owned by the agent.
#
# WIP agent feature branches (theseus/feature-foo, epimetheus/some-fix,
# rio/research-thesis-name) are NEVER reaped — owners review their own PRs
# on their own cadence. The */research-2* pattern threads the needle: it
# picks up daily synthesis output that the agent will regenerate tomorrow
# while leaving manually-named research work alone.
rows = conn.execute(
"""SELECT number, branch, eval_issues, leo_verdict, domain_verdict,
last_attempt, fix_attempts
@ -659,7 +674,10 @@ async def verdict_deadlock_reaper_cycle(conn) -> int:
AND tier0_pass = 1
AND last_attempt IS NOT NULL
AND last_attempt < datetime('now', ? || ' hours')
AND (branch LIKE 'extract/%' OR branch LIKE 'reweave/%' OR branch LIKE 'fix/%')
AND (branch LIKE 'extract/%'
OR branch LIKE 'reweave/%'
OR branch LIKE 'fix/%'
OR branch LIKE '%/research-2%')
AND (
(leo_verdict = 'request_changes' AND domain_verdict = 'approve')
OR (leo_verdict = 'skipped' AND domain_verdict = 'request_changes')