autoland fix-forward loop: fix runs on rejection, spawn-count bound, escalation #182

Closed
opened 2026-07-20 02:57:34 +02:00 by dominik.polakovics · 2 comments

What to build

The loop's second half: fix-forward runs on rejection, the attempt bound, and escalation. This is the "reject → re-queue-to-AFK loop" ADR-0024 deferred, implemented as fix-forward (bounded, carries new information) — NOT the forbidden auto-requeue.

  • Fix run trigger (poller rule): a PR in changes-requested, no live run on its branch, and fix-run spawns for this PR < max_fix_attempts → spawn a fix run. The trigger is forge state, so a human's rejection review feeds the same loop as a lander's.
  • Fix run mechanics: recreate a worktree from the surviving afk/<N> claim branch (the original AFK run was reaped at PR-open; sessions are never resumed — this is a fresh run on the existing claim). Fresh run credentials; provider/model inherited from the run that authored the branch; budget clock and three-strikes accounting apply. Seed prompt carries the issue, the PR, and the rejection review verbatim.
  • Fix run contract: push fixes to the existing branch, never open a new PR, end with labctl pr rerequest — the explicit forge-observable done-signal, symmetric with pr create for AFK runs. Review re-requested + no live run → the poller spawns a lander again (next validation round).
  • Attempt bound: counted as fix runs ever spawned per PR (runs store, keyed by branch) — not rejection reviews — so a fix run that dies or times out without delivering burns an attempt instead of respawning unbounded.
  • Escalation: at the bound with the PR still changes-requested, the poller spawns a lander in escalate mode (agents write to the forge; the engine never does): history-digest comment on the linked issue (what was rejected each round, what each fix attempt changed, why it still fails), label flip ready-for-agentready-for-human via labctl, push notification fired off the run outcome.
  • Terminal state: an escalated-outcome run for a PR makes that PR permanently invisible to the autoland poller; re-entry is a human running the interactive land-pr skill.

Acceptance criteria

  • A changes-requested review on a lander-engaged PR (whether posted by an agent or a human) spawns a fix run on the claim branch within one poll tick
  • The fix run pushes to the existing branch, opens no new PR, and its pr rerequest leads to a fresh lander validation round
  • A fix run that dies without delivering burns an attempt — no unbounded respawn on a crashing fix agent
  • At max_fix_attempts: digest comment on the issue, ready-for-agentready-for-human flip, push notification; no further autoland activity on that PR afterwards
  • Fix runs inherit the authoring run's provider and respect budget clock / three-strikes accounting
  • Full loop verified end-to-end: reject → fix → re-validate → merge; and a persistently-failing PR stops after the configured attempts and escalates

Blocked by

## What to build The loop's second half: fix-forward runs on rejection, the attempt bound, and escalation. This is the "reject → re-queue-to-AFK loop" ADR-0024 deferred, implemented as fix-forward (bounded, carries new information) — NOT the forbidden auto-requeue. - **Fix run trigger** (poller rule): a PR in **changes-requested**, no live run on its branch, and fix-run spawns for this PR < `max_fix_attempts` → spawn a fix run. The trigger is forge state, so a human's rejection review feeds the same loop as a lander's. - **Fix run mechanics**: recreate a worktree from the surviving `afk/<N>` claim branch (the original AFK run was reaped at PR-open; sessions are never resumed — this is a fresh run on the existing claim). Fresh run credentials; provider/model inherited from the run that authored the branch; budget clock and three-strikes accounting apply. Seed prompt carries the issue, the PR, and the rejection review verbatim. - **Fix run contract**: push fixes to the existing branch, never open a new PR, end with `labctl pr rerequest` — the explicit forge-observable done-signal, symmetric with `pr create` for AFK runs. Review re-requested + no live run → the poller spawns a lander again (next validation round). - **Attempt bound**: counted as **fix runs ever spawned per PR** (runs store, keyed by branch) — not rejection reviews — so a fix run that dies or times out without delivering burns an attempt instead of respawning unbounded. - **Escalation**: at the bound with the PR still changes-requested, the poller spawns a lander in **escalate mode** (agents write to the forge; the engine never does): history-digest comment on the linked issue (what was rejected each round, what each fix attempt changed, why it still fails), label flip `ready-for-agent` → `ready-for-human` via labctl, push notification fired off the run outcome. - **Terminal state**: an escalated-outcome run for a PR makes that PR permanently invisible to the autoland poller; re-entry is a human running the interactive land-pr skill. ## Acceptance criteria - [ ] A changes-requested review on a lander-engaged PR (whether posted by an agent or a human) spawns a fix run on the claim branch within one poll tick - [ ] The fix run pushes to the existing branch, opens no new PR, and its `pr rerequest` leads to a fresh lander validation round - [ ] A fix run that dies without delivering burns an attempt — no unbounded respawn on a crashing fix agent - [ ] At `max_fix_attempts`: digest comment on the issue, `ready-for-agent` → `ready-for-human` flip, push notification; no further autoland activity on that PR afterwards - [ ] Fix runs inherit the authoring run's provider and respect budget clock / three-strikes accounting - [ ] Full loop verified end-to-end: reject → fix → re-validate → merge; and a persistently-failing PR stops after the configured attempts and escalates ## Blocked by - #181
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Autoland fix-forward loop — fix runs spawned on rejection, a spawn-count attempt bound, and agent-executed escalation to ready-for-human. This implements the reject → re-engage loop ADR-0024 deferred, as fix-forward (bounded, carries the rejection review as new information) — NOT the grep-guard-forbidden auto-requeue.

Current behavior:
After #181, a lander (or a human) can post a changes-requested review, but the PR then sits: nothing re-engages an agent, the rejection findings go unread, and the only path forward is a human running the interactive land-pr skill. The original AFK run was reaped at PR-open and sessions are never resumed, so re-engagement must be a fresh run on the surviving claim branch.

Desired behavior:

  • Fix run trigger (poller rule in the same state-derived PR walk): PR in changes-requested, no live run on its branch, and fix runs ever spawned for this PR < max_fix_attempts → spawn a fix run. The trigger is forge state, so a human's rejection review feeds the same loop as a lander's.
  • Fix run mechanics: recreate a worktree from the surviving afk/<N> claim branch — a fresh run on the existing claim, never a session resume. Fresh run credentials; provider/model inherited from the run that authored the branch; budget clock and three-strikes accounting apply. Seed prompt carries the issue, the PR, and the rejection review verbatim.
  • Fix run contract: push fixes to the existing branch, never open a new PR, end with labctl pr rerequest — the explicit forge-observable done-signal, symmetric with pr create for AFK runs. Review re-requested + no live run → the poller spawns a lander again (next validation round).
  • Attempt bound: counted as fix runs ever spawned per PR (runs store, keyed by branch) — not rejection reviews — so a fix run that dies or times out without delivering burns an attempt instead of respawning unbounded.
  • Escalation: at the bound with the PR still changes-requested, the poller spawns a lander in escalate mode (agents write to the forge; the engine never does): history-digest comment on the linked issue (what was rejected each round, what each fix attempt changed, why it still fails), label flip ready-for-agentready-for-human via labctl, push notification fired off the run outcome.
  • Terminal state: an escalated-outcome run for a PR makes that PR permanently invisible to the autoland poller; re-entry is a human running the interactive land-pr skill.

Key interfaces:

  • Run kinds: fix run beside lander/AFK; escalate mode as a lander variant (or spawn knob), with a distinguishable escalated outcome the poller treats as terminal
  • Runs store query: fix-run spawn count per PR/branch — the bound's source of truth
  • Poller rules keyed on forge review state (changes-requested / re-requested) + live-run presence
  • Seed prompts: fix run (issue + PR + rejection review verbatim), escalate lander (full round history for the digest)
  • pr rerequest from #180 as the fix run's done-signal; labctl label verbs for the ready-for-human flip; push notification off run outcome

Acceptance criteria:

  • A changes-requested review on a lander-engaged PR (whether posted by an agent or a human) spawns a fix run on the claim branch within one poll tick
  • The fix run pushes to the existing branch, opens no new PR, and its pr rerequest leads to a fresh lander validation round
  • A fix run that dies without delivering burns an attempt — no unbounded respawn on a crashing fix agent
  • At max_fix_attempts: digest comment on the issue, ready-for-agentready-for-human flip, push notification; no further autoland activity on that PR afterwards
  • Fix runs inherit the authoring run's provider and respect budget clock / three-strikes accounting
  • Full loop verified end-to-end: reject → fix → re-validate → merge; and a persistently-failing PR stops after the configured attempts and escalates

Out of scope:

  • Resuming the original AFK session (never happens — fresh runs only)
  • Auto-requeue semantics (blind retry without the rejection review) — the grep guard must keep passing
  • Fix runs opening new PRs; engine-authored forge writes (comments/labels are agent-executed in escalate mode)
  • Re-engaging an escalated PR automatically — that is human-only via the interactive skill
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Autoland fix-forward loop — fix runs spawned on rejection, a spawn-count attempt bound, and agent-executed escalation to ready-for-human. This implements the reject → re-engage loop ADR-0024 deferred, as fix-forward (bounded, carries the rejection review as new information) — NOT the grep-guard-forbidden auto-requeue. **Current behavior:** After #181, a lander (or a human) can post a changes-requested review, but the PR then sits: nothing re-engages an agent, the rejection findings go unread, and the only path forward is a human running the interactive land-pr skill. The original AFK run was reaped at PR-open and sessions are never resumed, so re-engagement must be a fresh run on the surviving claim branch. **Desired behavior:** - **Fix run trigger** (poller rule in the same state-derived PR walk): PR in changes-requested, no live run on its branch, and fix runs ever spawned for this PR < `max_fix_attempts` → spawn a fix run. The trigger is forge state, so a human's rejection review feeds the same loop as a lander's. - **Fix run mechanics**: recreate a worktree from the surviving `afk/<N>` claim branch — a fresh run on the existing claim, never a session resume. Fresh run credentials; provider/model inherited from the run that authored the branch; budget clock and three-strikes accounting apply. Seed prompt carries the issue, the PR, and the rejection review verbatim. - **Fix run contract**: push fixes to the existing branch, never open a new PR, end with `labctl pr rerequest` — the explicit forge-observable done-signal, symmetric with `pr create` for AFK runs. Review re-requested + no live run → the poller spawns a lander again (next validation round). - **Attempt bound**: counted as fix runs ever spawned per PR (runs store, keyed by branch) — not rejection reviews — so a fix run that dies or times out without delivering burns an attempt instead of respawning unbounded. - **Escalation**: at the bound with the PR still changes-requested, the poller spawns a lander in **escalate mode** (agents write to the forge; the engine never does): history-digest comment on the linked issue (what was rejected each round, what each fix attempt changed, why it still fails), label flip `ready-for-agent` → `ready-for-human` via labctl, push notification fired off the run outcome. - **Terminal state**: an escalated-outcome run for a PR makes that PR permanently invisible to the autoland poller; re-entry is a human running the interactive land-pr skill. **Key interfaces:** - Run kinds: fix run beside lander/AFK; escalate mode as a lander variant (or spawn knob), with a distinguishable escalated outcome the poller treats as terminal - Runs store query: fix-run spawn count per PR/branch — the bound's source of truth - Poller rules keyed on forge review state (changes-requested / re-requested) + live-run presence - Seed prompts: fix run (issue + PR + rejection review verbatim), escalate lander (full round history for the digest) - `pr rerequest` from #180 as the fix run's done-signal; labctl label verbs for the ready-for-human flip; push notification off run outcome **Acceptance criteria:** - [ ] A changes-requested review on a lander-engaged PR (whether posted by an agent or a human) spawns a fix run on the claim branch within one poll tick - [ ] The fix run pushes to the existing branch, opens no new PR, and its `pr rerequest` leads to a fresh lander validation round - [ ] A fix run that dies without delivering burns an attempt — no unbounded respawn on a crashing fix agent - [ ] At `max_fix_attempts`: digest comment on the issue, `ready-for-agent` → `ready-for-human` flip, push notification; no further autoland activity on that PR afterwards - [ ] Fix runs inherit the authoring run's provider and respect budget clock / three-strikes accounting - [ ] Full loop verified end-to-end: reject → fix → re-validate → merge; and a persistently-failing PR stops after the configured attempts and escalates **Out of scope:** - Resuming the original AFK session (never happens — fresh runs only) - Auto-requeue semantics (blind retry without the rejection review) — the grep guard must keep passing - Fix runs opening new PRs; engine-authored forge writes (comments/labels are agent-executed in escalate mode) - Re-engaging an escalated PR automatically — that is human-only via the interactive skill
Author
Owner

This was generated by AI while landing a PR.

Land-PR audit — PR #187

Verdict: PASS (after fixes applied on the branch). Merging.

Verification signal relied on: Forgejo Actions ci / native, green in 6m42s on ef284fd (run 211) — the head commit as merged, not the original green on 01bcf7f. Not re-run locally.

Checked

  • Mergeable clean against origin/main; Conventional Commits title; body carries Closes #182.
  • Full diff reviewed across four dimensions: autoland state machine, data layer, agent/forge-write boundary, and acceptance-criteria coverage.
  • All six of this issue's acceptance criteria have real test coverage; four unambiguously genuine. Mutation testing confirmed the assertions bite — no test passes with the feature broken.
  • Data layer verified empirically: the sqlite 0014 table rebuild preserves all 19 columns, both indexes and child run_tokens rows; both dialects' CHECKs admit every enum value the Go code writes.
  • Engine-never-writes-to-the-forge invariant holds — the escalation hand-off is entirely agent-executed via labctl.

Blockers found and fixed on the branch (ef284fd)

Both from one root cause: the attempt bound counted runs ROWS, which record a spawn that reached a live session — not a spawn that was attempted.

  1. A fix launch failing on the launch pad burned no attempt. Start's rollback deletes the run row on every post-CreateRun failure, and pre-CreateRun failures never write one. A deterministically-failing launch retried every tick forever, never escalating — a miss against acceptance criterion 3.
  2. The escalate arm had no bound at all. Terminality is written only once the marker lands, so an escalate run that died first was re-escalated every tick. Un-braked: reapRun excludes escalate from the consecutive-failure counter in both directions, and the candidate sorts at StageFix, ahead of new AFK work — one wedged branch would starve its repo's new-work pipeline.

Fixed by counting spawn INTENTS in autoland_attempts (migration 0015, keyed repo+branch+kind), incremented at autolandLaunch; ErrOverCap checked first and never burns. Escalate gains MaxEscalateAttempts = 3, then the poller goes quiet and logs at error level. Regression tests TestSpawnOnce_failedFixLaunchBurnsAttempt and TestSpawnOnce_escalateArmIsBounded, both verified to fail against the unfixed code.

Not blocking — carried forward

  • Escalation has no way back in once terminal (requeue reuses the branch; a human fixing the PR themselves is still locked out). Filed as #188.
  • pr escalate is repo-scoped, not PR-scoped — the first irreversible verb on a surface ADR-0048 already notes is not PR-confined.
  • A human typing the marker grammar in a forge comment can spoof a done-signal or an escalation; tracker.Comment carries Author and nothing reads it.
  • No migration test for the 0014 table rebuild — reviewers wrote one by hand and confirmed the migration is correct, so a coverage gap rather than a defect.

CONTEXT.md's duplicate Escalation glossary entry (conflicting definitions from #180 and #182) was collapsed as part of the fix commit.

> *This was generated by AI while landing a PR.* ## Land-PR audit — PR #187 **Verdict: PASS (after fixes applied on the branch).** Merging. **Verification signal relied on:** Forgejo Actions `ci / native`, green in 6m42s on `ef284fd` (run 211) — the head commit as merged, not the original green on `01bcf7f`. Not re-run locally. ### Checked - Mergeable clean against `origin/main`; Conventional Commits title; body carries `Closes #182`. - Full diff reviewed across four dimensions: autoland state machine, data layer, agent/forge-write boundary, and acceptance-criteria coverage. - All six of this issue's acceptance criteria have real test coverage; four unambiguously genuine. Mutation testing confirmed the assertions bite — no test passes with the feature broken. - Data layer verified empirically: the sqlite 0014 table rebuild preserves all 19 columns, both indexes and child `run_tokens` rows; both dialects' CHECKs admit every enum value the Go code writes. - Engine-never-writes-to-the-forge invariant holds — the escalation hand-off is entirely agent-executed via labctl. ### Blockers found and fixed on the branch (`ef284fd`) Both from one root cause: **the attempt bound counted runs ROWS, which record a spawn that reached a live session — not a spawn that was attempted.** 1. **A fix launch failing on the launch pad burned no attempt.** Start's rollback deletes the run row on every post-`CreateRun` failure, and pre-`CreateRun` failures never write one. A deterministically-failing launch retried every tick forever, never escalating — a miss against acceptance criterion 3. 2. **The escalate arm had no bound at all.** Terminality is written only once the marker lands, so an escalate run that died first was re-escalated every tick. Un-braked: `reapRun` excludes escalate from the consecutive-failure counter in both directions, and the candidate sorts at `StageFix`, ahead of new AFK work — one wedged branch would starve its repo's new-work pipeline. Fixed by counting spawn INTENTS in `autoland_attempts` (migration 0015, keyed repo+branch+kind), incremented at `autolandLaunch`; `ErrOverCap` checked first and never burns. Escalate gains `MaxEscalateAttempts = 3`, then the poller goes quiet and logs at error level. Regression tests `TestSpawnOnce_failedFixLaunchBurnsAttempt` and `TestSpawnOnce_escalateArmIsBounded`, both verified to fail against the unfixed code. ### Not blocking — carried forward - **Escalation has no way back in** once terminal (requeue reuses the branch; a human fixing the PR themselves is still locked out). Filed as **#188**. - `pr escalate` is repo-scoped, not PR-scoped — the first irreversible verb on a surface ADR-0048 already notes is not PR-confined. - A human typing the marker grammar in a forge comment can spoof a done-signal or an escalation; `tracker.Comment` carries `Author` and nothing reads it. - No migration test for the 0014 table rebuild — reviewers wrote one by hand and confirmed the migration is correct, so a coverage gap rather than a defect. CONTEXT.md's duplicate **Escalation** glossary entry (conflicting definitions from #180 and #182) was collapsed as part of the fix commit.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#182
No description provided.